class Dependancy:

    table_name = 'dependancy'

    column_headers_and_types = [['id', 'integer primary key'], 
                                ['name', 'text'],
                                ['dep_str', 'text']]

    cond_start = '['  # flag of condition start
    cond_stop = ']'
    effect_start_t = '{'  # flag of effect start
    effect_stop_t = '}'
    cond_marker = '!'  # flag used by cause parser to mark conditions places.
    cause_effect_sep = 'then'  # separates cause and effects
    time_marker = 't'
    day_marker = 'd'
    element_marker = 'e'

    day_dict = {'mon': 0,
                'tue': 1,
                'wed': 2,
                'thu': 3,
                'fri': 4,
                'sat': 5,
                'sun': 6, }
    clock = Clock()
    items = {}

    def __init__(self, _id, name, dep_str):
        self.id = _id
        Dependancy.items[self.id] = self

        self.name = name
        self.dep_str = dep_str
        self.conditions = []  # Conditions which make the cause
        self.effects = []     # Effects which will happen after condition changes from False to True

        self.num_of_conds = 0
        self.num_of_effect = 0

        self.prev_cause_result = False

        self.cause_str, self.effects_str = dep_str.split(Dependancy.cause_effect_sep)
        self.cause_template = ''  # evaluated conditions are applied to it. Finnally template goes to eval()
               
    def run(self, ):
        """Evaluates cause. If it is true and prev result is false it notifies all efects.
       When the cause changes from True to false it restores effects to initial state """
      
        cause_result = self._evaluate_cause()
        
        if cause_result and not self.prev_cause_result:  # if the cause changes from False to True
            for effect in self.effects:
                effect.start(self.clock.get_millis())

        # if the cause changes from True to False effects should be undone
        if not cause_result and self.prev_cause_result:
            for effect in self.effects:
                effect.revert()

        self.prev_cause_result = cause_result

        for effect in self.effects:
            effect.run()  # perform effect

    def _evaluate_cause(self):
        eval_cause_string = ''
        condition_num = 0
        for s in self.cause_template:  # Evaluate all conditions and put their results into eval strin
            if s == Dependancy.cond_marker:
                eval_cause_string += self.conditions[condition_num].evaluate()
                condition_num += 1
            else:
                eval_cause_string += s

        return eval(eval_cause_string)

    def _parse_cause(self, all_elements=dict()):
        """Parses cause string"""

        for condition in self._find_condition():

            element, op, comp_value = self._parse_condition(condition)

            if element[0] == Dependancy.element_marker:
                element_id = int(element[1:])

                if element_id not in all_elements:
                    raise DependancyConfigError('Element does not exists: ' + str(element_id))
                comp_value = int(comp_value)
                subscribe = all_elements[element_id].subscribe

            if element[0] == Dependancy.day_marker:
                comp_value = comp_value.split(',')
                comp_value = [Dependancy.day_dict[day] for day in comp_value]
                subscribe = self.clock.subscribe_for_weekday

            if element[0] == Dependancy.time_marker:
                comp_value = comp_value.split(':')
                comp_value = [int(val) for val in comp_value]
                subscribe = self.clock.subscribe_for_minute

            condition = Condition(self.num_of_conds, op, comp_value)
            self.num_of_conds += 1
            subscribe(condition)
            self.conditions.append(condition)

    def _find_condition(self):
        """Yields condition and updates cause template"""
        condition = ''

        is_condition = False
        for s_pos, s in enumerate(self.cause_str):

            if s == Dependancy.cond_start:
                is_condition = True
                self.cause_template += Dependancy.cond_marker

            if is_condition:
                condition += s
            else:
                self.cause_template += s

            if s == Dependancy.cond_stop:
                yield condition[1:-1]  # First and last char are flags of begining and end of condition
                is_condition = False
                condition = ''

    @staticmethod
    def _parse_condition(condition):
        """Creates condition objects based on condition string"""

        op_pos = 0
        op = None  # operator
        for s_pos, s in enumerate(condition):
            if s in Condition.operator_dict.keys():
                op_pos = s_pos
                op = s
                break

        element = condition[:op_pos]
        comp_value = condition[op_pos+1:]

        if op not in Condition.operator_dict.keys():
            raise DependancyConfigError("Condition has wrong operator: {}".format(op))

        return element, op, comp_value

    def _parse_effects(self, output_elements=None):
        """Creates effect objects based on effect string"""

        effects_array = self.effects_str.strip().rstrip(';').split(';')
        for effect_str in effects_array:

            element_id, set_value, _time = self._parse_effect(effect_str)

            if element_id not in output_elements.keys():
                raise DependancyConfigError('Output element: ' + str(element_id) + ' not in output elements')

            effect = Effect(self.num_of_effect, output_elements[element_id], set_value, _time)
            self.num_of_effect += 1
            self.effects.append(effect)

    @staticmethod
    def _parse_effect(effect_str):

        effect_str = effect_str.strip()
        op_pos = 0
        time_pos = None
        _time = ''
        is_time = False
        for s_pos, s in enumerate(effect_str):
            if s == '=':
                op_pos = s_pos

            if s == Dependancy.effect_start_t:
                time_pos = s_pos
                is_time = True

            if is_time:
                _time += s

            if s == Dependancy.effect_stop_t:
                is_time = False

        try:
            element_id = int(effect_str[1:op_pos])
            set_value = int(effect_str[op_pos + 1:time_pos])
            _time = int(effect_str[time_pos+1:-1])*1000  # First and last char are flags of begining and end of time
        except:
            raise DependancyConfigError('Effect parsing error. Effect string: {}'.format(effect_str))

        if set_value < 0:
            raise DependancyConfigError('Set value cant be negative. Effect string: {}'.format(effect_str))

        if _time < 0:
            raise DependancyConfigError('Time cant be negative. Effect string: {}'.format(effect_str))

        return element_id, set_value, _time

    def __str__(self, ):    
        return "".join(["ID: ", str(self.id), "\ttype: ", "\tname: ", self.name, '\tdep_str: ', self.dep_str])

    def __repr__(self):
        return "".join(["ID: ", str(self.id), " - ", self.name]) class DependancyConfigError(Exception):
    def __init__(self, msg):
        self.msg = msg class Element(BaseComponent):

    table_name = "elements"
    COL_MODULE_ID, COL_REG,  = 3, 4,
    column_headers_and_types = BaseComponent.column_headers_and_types + [['module_id', 'integer'], ['register', 'integer']]

    items = {}

    def __init__(self, *args):
        super().__init__(args[0], Et(args[1]), args[2])  # inicjalizuj id type, name
        Element.items[self.id] = self        
        self.value = None
        self.module_id = args[Element.COL_MODULE_ID]
        self.reg_id = args[Element.COL_REG]
        self.objects_to_notify = set()
        self.new_val_flag = False

    def subscribe(self, who):
        self.objects_to_notify.add(who)

    def notify_objects(self, ):
        for _object in self.objects_to_notify:
            _object.notify(self.value)

    def __str__(self, ):
        return "".join([super().__str__(), "\tvalue: ", str(self.value)]) class InputElement(Element):

    types = {Et.ds, Et.dht_hum, Et.dht_temp, Et.pir, Et.rs, Et.ls, Et.switch}
    items = {}

    def __init__(self, *args):
        """arguments: type name """
        super().__init__(*args)  # inicjalizuj type, name
        InputElement.items[self.id] = self class OutputElement(Element):
 
    types = {Et.led, Et.heater, Et.ventilator, Et.blind}

    defualt_priority = 15
    items = {}

    def __init__(self, *args):
        super().__init__(*args)
        OutputElement.items[self.id] = self
        self.desired_value = 0
        self.setter_priority = OutputElement.defualt_priority  # Default priority can be overriden by everybody

    def set_desired_value(self, value, priority, set_flag=False):

        if priority <= self.setter_priority:
            self.desired_value = value

            if set_flag:
                self.setter_priority = priority
            else: 
                self.setter_priority = self.defualt_priority

            return True

        else:
            return False

    def __str__(self, ):
        return "".join([super().__str__(), "\tdesired_value: ", str(self.desired_value)])

    @staticmethod
    def str():
        string = "\n"
        for element in OutputElement.items.values():
            el_str = str(element) 
            string += el_str + "\n"

        return string class Blind(OutputElement):
    table_name = "blinds"
    column_headers_and_types = Element.column_headers_and_types + [['direction', 'text'], ['other_blind', 'integer']] 

    COL_DIRECTION, COL_OTHER_BLIND = 5, 6
    items = {}

    def __init__(self, *args):
        super().__init__(*args[0:self.COL_DIRECTION])
        Blind.items[self.id] = self
        self.direction = args[self.COL_DIRECTION]
        self.other_blind = args[self.COL_OTHER_BLIND]  # if blind is up then it is down and the other way around
    
    def set_desired_value(self, value, priority, set_flag=False):

        if super().set_desired_value(value, priority, set_flag=False):
            self.other_blind.desired_value = 0 class Module(BaseComponent):
    """Base class for all modules.
    It implements prototype of command that decorates all read and write functions"""

    table_name = 'modules'

    types = {Mt.led_light, Mt.output, Mt.ambient, Mt.input}  # Needed for loading objects from database
    ID = 0
    start_timeout = 10  # timeout in ms
    clock = Clock()
    items = {}

    def __init__(self, *args):

        super().__init__(args[0], Mt(args[1]), args[2])
        Module.items[self.id] = self 
        self.ports = {}  # Module's physical ports. Dictionary stores elements during configuration so not to connect elment twice to same port
        self.elements = {}  # Module's elements. Keys are registers in which elements values are going to be stored
        self.modbus = ModbusNetwork()  # Reference to modbus. Modbus is a singleton.

        self.available = True  # Flag indicating if there is communication with module
        self.last_timeout = 0
        self.timeout = Module.start_timeout
        self.max_timeout = 2 
        self.correct_trans_num = 0
        self.transmission_num = 0
        self.courupted_trans_num = 0

    def is_available(self, ):
        """Checks if module is available.
        If it is not but timeout expired it makes module
        available so there would be communication trial"""
        if self.available:
            self.timeout = Module.start_timeout
            return True

        else:
            current_time = self.clock.get_millis()
            if current_time - self.last_timeout >= self.timeout:
                self.last_timeout = current_time
                self.available = True
                return True
        return False

    @staticmethod
    def command(func):
        """Decorator for all modbus commands. It counts correct and corupted transmisions.
            It sets timeout if the transmission was corrupted """

        @wraps(func)
        def func_wrapper(self, ):
            self.transmission_num += 1
            result = func(self)

            if result:  # if there is response from module
                self.correct_trans_num += 1
                return result
            else:
                self.available = False
                self.courupted_trans_num += 1
                if self.timeout <= self.max_timeout:
                    self.timeout *= 2  # Increase timeout
                # TODO notification about module failures
                return result
        return func_wrapper

    def check_port_range(self, port):
        if port > self.num_of_ports-1 or port < 0:
            raise AddElementError('Port: ' + str(port) + ' out of range')

    def check_port_usage(self, port):
        try:
            self.ports[port]
            raise AddElementError('Port: ' + str(port) + ' is in use')
        except KeyError:
            pass

    def check_element_type(self, element):
        if element.type not in self.accepted_elements:
            raise AddElementError('Element: ' + element.type.name + ' is not valid for ' + self.type.name)
        
    def check_if_element_connected(self, element):
        if element.module_id and element.module_id != self.id and element.type != Et.blind: # roleta moze byc podlaczona 2 razy do jednego modulu - gora i dol
            raise AddElementError('Element: ' + str(element.id) + ' already connected to ' + str(element.module_id))

    def add_element(self, port, element):
        self.check_element_type(element)
        self.check_port_range(port)
        self.check_port_usage(port)
        self.check_if_element_connected(element)

        self.ports[port] = element
        element.reg_id = port
        element.module_id = self.id
        self.elements[element.id] = element class OutputBoard(OutputModule):

    num_of_ports = 10
    num_of_regs = 10
    accepted_elements = {Et.led, Et.heater, Et.ventilator, Et.blind}
    types = {Mt.output}
    items = {}

    def __init__(self, *args):
        super().__init__(*args)
        OutputBoard.items[self.id] = self

    @Module.command
    @OutputModule.write_command
    def write(self, ):
        return self.modbus.write_coils(self.id, 0, self.values) class LedLightBoard(OutputModule):
    
    num_of_ports = 3
    num_of_regs = 3
    accepted_elements = {Et.led}
    types = {Mt.led_light}
    items = {}

    def __init__(self, *args):
        super().__init__(args[0], Mt(args[1]), args[2])
        LedLightBoard.items[self.id] = self

    @Module.command
    @OutputModule.write_command
    def write(self, ):
        return self.modbus.write_regs(self.id, 0, self.values) class AmbientBoard(InputModule):

    num_of_ports = 4
    num_of_regs = 19
    accepted_elements = {Et.ds, Et.dht_hum, Et.dht_temp, Et.ls}
    types = {Mt.ambient}
    items = {}

    def __init__(self, *args):
        InputModule.__init__(self, *args)
        AmbientBoard.items[self.id] = self
        self.ds18b20_counter = 0
    
    def add_element(self, port, element):
        """For Ambient board ports are not the same as registers. ds18b20 sensors are all working on one port"""
        self.check_element_type(element)
        self.check_port_range(port)
        self.check_if_element_connected(element)
        try:
            if self.ports[port]:  # Check if something is already on the port
                if self.ports[port].type == Et.ds:  # If on this port is ds18b20
                    if element.type == Et.ds:   # And user wants to connect another ds18b20
                        pass  # everything is fine
                    else:
                        raise AddElementError('Port: ' + port + ' in use') # raise error
        except KeyError:           
            self.ports[port] = element  # Add element to port

        if element.type == Et.ls:  # dodanie elementu do rejestru
            element.reg_id = 0
        elif element.type == Et.dht_temp:
            element.reg_id = 1
        elif element.type == Et.dht_hum:
            element.reg_id = 2
        elif element.type == Et.ds:
            element.reg_id = 3 + self.ds18b20_counter
            self.ds18b20_counter += 1
       
        element.module_id = self.id
        self.elements[element.id] = element class InputBoard(InputModule):

    num_of_ports = 15
    num_of_regs = 15
    accepted_elements = {Et.pir, Et.rs, Et.switch}
    types = {Mt.input}
    items = {}

    def __init__(self, *args):
        InputModule.__init__(self, *args)
        InputBoard.items[self.id] = self class AddElementError(Exception):
    def __init__(self, msg):
        self.msg = msg class Regulation(BaseComponent):
    """Rehulation algoritms for controlling outputs based on inputs values"""
    
    table_name = "regulation"

    column_headers_and_types = BaseComponent.column_headers_and_types + [['feed_el_id', 'integer'],
                                                                         ['out_el_id', 'integer'],
                                                                         ['set_point', 'integer'],
                                                                         ['deviation', 'integer'], ]
                                                                        
    ID = 0

    ON = 1
    OFF = 0
     
    COL_FEED_EL_ID, COL_OUT_EL_ID, COL_SET_POINT, COL_DEVIATION = 3, 4, 5, 6
    items = {}

    def __init__(self, *args):

        super().__init__(args[0], Regt(args[1]), args[2])
        Regulation.items[self.id] = self
        self.feed_el_id = args[Regulation.COL_FEED_EL_ID]
        self.out_element = OutputElement.items[args[Regulation.COL_OUT_EL_ID]]
        self.set_point = args[Regulation.COL_SET_POINT]
        self.dev = args[Regulation.COL_DEVIATION]    # max alowable deviation from set point
        self.control = self.proportional_control  # Control algorithm

        self.priority = 10
        self.feed_val = None

    def notify(self, val):
        self.feed_val = val

    def run(self, ):
        """calculates whether output element should be on or off"""
        out_val = self.control()
        if out_val == Regulation.ON:
            self.out_element.desired_value = (out_val, self.priority, True)

        if out_val == Regulation.OFF:
            self.out_element.desired_value = (out_val, self.priority, False)

    def proportional_control(self, ):
        """If feed value is less than set value turn on regulation. Otherwise turn off"""
        if not self.feed_val: # if sensor does not returns any valu - its val == None
            return Regulation.OFF

        if self.feed_val < self.set_point:
            return Regulation.ON

        else:
            return Regulation.OFF

    def inverse_control(self, ):
        pass class RegulationConfigError(Exception):
    def __init__(self, msg):
        self.msg = msg def color_logs():
    os.environ["COLOREDLOGS_LOG_FORMAT"]='[%(name)s] %(asctime)s - %(message)s'
    os.environ["COLOREDLOGS_DATE_FORMAT"]='%H:%M:%S'
    os.environ["COLOREDLOGS_LEVEL_STYLES"]='debug=green;warning=yellow;error=red;critical=red,bold'
    coloredlogs.install(level='DEBUG') class Mt(AutoNumber):
    input = ()
    output = ()
    ambient = ()
    led_light = () class Et(AutoNumber):
    ds = ()
    dht = ()
    dht_temp = ()
    dht_hum = ()
    led = ()
    pir = ()
    rs = ()
    heater = ()
    ls = ()
    ventilator = ()
    blind = ()
    switch = () class Rt(AutoNumber):
    inside = ()
    outside = ()
    kitchen = ()
    sleeping_room = ()
    wc = ()
    corridor = ()
    living_room = () class Regt(AutoNumber):
    temp = ()
    hum = () def create_db_object():

    root = None
    for path in sys.path:
        if path.endswith("system"):
            root = path

    if is_RPI:
        db_path = "/".join([root, 'backend', 'sys_database', "sys_database.db"])
    else:   
        db_path = "\\".join([root, 'backend', 'sys_database', "sys_database.db"])

    return Database(db_path) class Room(BaseComponent):
    """Defines room display in system"""
    table_name = 'rooms'
    column_headers_and_types = BaseComponent.column_headers_and_types + [['els', 'text'], ['regs', 'text']]
    
    COL_REGS = 4
    COL_ELS = 3                                     
    ID = 0
    items = {}
    groups_per_row = 4

    def __init__(self, *args):
        super().__init__(args[0], Rt(args[1]), args[2]) # inicjalizuj id type, name
        Room.items[self.id] = self
        self.elements = args[Room.COL_ELS]
        self.regulations = args[Room.COL_REGS]
        self.groups = OrderedDict() # Thanks to ordered dict groups always apear in the same order

    def add_element(self, *elements):
        for element in elements:
            self.elements.append(element)

    def get_display_data(self,):
        """Returns data organized in bootstrap rows and columns. There are Room.groups_per_row columns per row"""
        rows = []
        row = []
        for group_num, group in enumerate(self.groups.values()):
            row.append(group)
            if group_num == Room.groups_per_row-1:
                rows.append(row)
                row = []
        if row:
            rows.append(row) # dla ostatniego niepelnego rzedu

        return rows

    def get_html(self, ):
        """Generates room html"""
        rows = self.get_display_data()

        room_container = div(cls = "well", id='room' + str(self.id))
        room_name = h3(self.name, cls="text-center")
        room_container.add(room_name)
        for row in rows:            
            r = div(cls='row')
            with r:
                for group in row:
                    div(cls="col-sm-3 group").add(group.get_html())
            room_container.add(r)
        return room_container.render()


    def __str__(self, ):
        return "".join([super().__str__(), '\tELEMENTS: ',  ",".join([str(el.id) for el in self.elements])])
import logging
from backend.components.dependancy import Dependancy, DependancyConfigError
from backend.components.element import Element, InputElement, OutputElement, Blind
from backend.components.module import Module, OutputBoard, LedLightBoard, AmbientBoard, InputBoard, AddElementError
from backend.components.regulation import Regulation, RegulationConfigError
from backend.misc.color_logs import color_logs
from backend.misc.sys_types import Mt, Et, Rt, Regt
from backend.sys_database.database import create_db_object
from server_client.server.models.room import Room

            for room in Room.items.values(): #  przypisz regulacje do pokoju w ktorym znajduje sie odpowiadajacy jej el wyjsciowy
                for el in room.elements:
                    if el.id == out_el_id:
                        room.regulations.append(self.regulation_id) 

            self.regulation_id += 1
        except RegulationConfigError as e:
            self.logger.error(e.msg)
            self.number_of_errors += 1

    def save(self,):
        self.__save_dependancies()
        self.__save_elements()
        self.__save_modules()
        self.__save_regulations()
        self.__save_rooms()

    def __save_modules(self, ):
        for module in Module.items.values():
            self.db.save(Module, (module.id, module.type.value, module.name, ))            
                       
    def __save_rooms(self, ):
        for room in Room.items.values():
            els = ",".join([str(element.id) for element in room.elements])
            regs = ",".join([str(regulation_id) for regulation_id in room.regulations])
            self.db.save(Room, (room.id, room.type.value, room.name, els, regs))

    def __save_elements(self, ):
        for element in Element.items.values():
            self.db.save(Element, (element.id, element.type.value, element.name, element.module_id, element.reg_id))
        for blind in Blind.items.values():           
            self.db.save(Blind, (blind.id, blind.type.value, blind.name, blind.module_id, blind.reg_id, blind.direction, blind.other_blind))

    def __save_dependancies(self, ):
        for dep in Dependancy.items.values():
            self.db.save(Dependancy, (dep.id, dep.name, dep.dep_str,))

    def __save_regulations(self, ):
        for reg in Regulation.items.values():
            self.db.save(Regulation, (reg.id, reg.type.value, reg.name, reg.feed_el_id, reg.out_el_id, reg.set_point, reg.dev))

    def print(self, ):
        self.logger.warn("\n-----------------------SYSTEM--------------------------------")
        for module in Module.items.values():
            self.logger.info(str(module))
        self.logger.info("\n")

        for room in Room.items.values():
            self.logger.info(str(room))

        self.logger.info("\n")

        for element in Element.items.values():
            self.logger.info(str(element))
 
        self.logger.info("\n")
     
        for dep in Dependancy.items.values():
            self.logger.info(str(dep))

        self.logger.info("\n")

        for reg in Regulation.items.values():
            self.logger.info(str(reg))

        if self.number_of_errors:
            self.logger.error("There was {} errors. Scroll up to find them and fix them".format(self.number_of_errors))

color_logs()

system = SystemCreator()
system.logger.setLevel('DEBUG')

system.create_tables()

print("\n")

#  Add module giving module type and module name
system.add_module(Mt.input, 'Input')           # 1
system.add_module(Mt.output, 'Output')         # 2
system.add_module(Mt.led_light, 'Led light')   # 3
system.add_module(Mt.ambient, 'Ambient')       # 4

print("\n")

#  Add rooms giving room type and room name. Room name is going to be displayed in client application
system.add_room(type=Rt.wc, name='Water Closet')  # 0
system.add_room(type=Rt.sleeping_room, name='Sleeping room')       # 1
system.add_room(type=Rt.sleeping_room, name='Sleeping room')       # 2
system.add_room(type=Rt.corridor, name='Main corridor')     # 3
system.add_room(type=Rt.living_room, name='Living room')  # 4
system.add_room(type=Rt.kitchen, name='Kitchen')      # 5
system.add_room(type=Rt.outside, name='Outside')      # 6

print("\n")

#  Add system elements giving element type (you can find all types in backend.misc.sys_types),
#  name
#  id of the room in which the element is placed. You have to count rooms from above starting from 0.
#  module id. Same as with room
#  port. Physical port to which element is connected
system.add_element(type=Et.dht,
                   name='Humidity and temperature',
                   room_id=0,
                   module_id=4,
                   port=0)  # 0,1

system.add_element(type=Et.led,
                   name='Led strip',
                   room_id=0,
                   module_id=3,
                   port=0)  # 2

system.add_element(type=Et.pir,
                   name='Motion',
                   room_id=0,
                   module_id=1,
                   port=0)  # 3


system.add_element(type=Et.switch,