def setUp(self): self.clock = Clock() el_id, _type, name, mod_id, reg_id = 0, 1, '', 0, 0 self.output_element = OutputElement(el_id, _type, name, mod_id, reg_id) ef_id, value, _time = 0, 1, 1 self.effect = Effect(ef_id, self.output_element, value, _time)
class TestEffect(unittest.TestCase): def setUp(self): self.clock = Clock() el_id, _type, name, mod_id, reg_id = 0, 1, '', 0, 0 self.output_element = OutputElement(el_id, _type, name, mod_id, reg_id) ef_id, value, _time = 0, 1, 1 self.effect = Effect(ef_id, self.output_element, value, _time) def test_start(self): # when cause time is 1000ms self.effect.start(1000) # then effects cause time should be set to 1000ms and out_el value should be saved self.assertEqual(self.effect.cause_time, 1000) self.assertEqual(self.effect.prev_value, self.output_element.value) def test_run_cause_is_before_effect_time(self): # given effect time 1ms after cause self.effect.time = 1 # when self.effect.start(self.clock.get_millis()) time.sleep(0.001) # then self.assertTrue(self.effect.run()) # effect should happen self.assertEqual(self.output_element.desired_value, self.effect.value) def test_run_more_times(self): # given self.effect.time = 0 # when self.effect.start(self.clock.get_millis()) time.sleep(0.001) # then Effect should happen only once self.assertTrue(self.effect.run()) # effect should happen self.assertFalse(self.effect.run()) # effect should not happen self.assertFalse(self.effect.run()) # effect should not happen def test_revert_set(self): # when normal effect flow self.effect.start(self.clock.get_millis()) time.sleep(0.001) self.effect.run() self.effect.revert() # then output_element desired value should be reverted self.assertEqual(self.output_element.desired_value, self.effect.prev_value)
def system_loader(): """Loads all necessary objects for system operation from database Configures system and returns system named tuple""" system = namedtuple("System", ["clock", "elements", "modules", "dependancies", "regulations"]) system.elements = namedtuple("elements", ['all', 'input', 'output']) system.modules = namedtuple("modules", ['all', 'input', 'output']) system.modules.input = namedtuple("input_modules", ['input_board', 'ambient_board']) system.modules.output = namedtuple("input_modules", ['output_board', 'led_light_board']) system.clock = Clock() # instantiate global clock. Reference is stored in clock.py db = create_db_object() db.load_objects_from_table(InputElement) db.load_objects_from_table(OutputElement) db.load_objects_from_table(Blind) db.load_objects_from_table(OutputBoard) db.load_objects_from_table(LedLightBoard) db.load_objects_from_table(AmbientBoard) db.load_objects_from_table(InputBoard) db.load_objects_from_table(Dependancy) db.load_objects_from_table(Regulation) for element in Element.items.values(): module = Module.items[element.module_id] module.elements[element.reg_id] = element # pass elements to modules registers # pair blinds so two motors of the same blinds never turn on both. It would cause shortcut! for blind in Blind.items.values(): other_blind_id = blind.other_blind blind.other_blind = Blind.items[other_blind_id] for dep in Dependancy.items.values(): dep._parse_cause(all_elements=Element.items) for reg in Regulation.items.values(): InputElement.items[reg.feed_el_id].subscribe(reg) system.elements.all = Element.items system.elements.input = InputElement.items system.elements.output = OutputElement.items system.modules.input = InputModule.items system.modules.output = OutputModule.items system.dependancies = Dependancy.items system.regulations = Regulation.items return system
class Effect: clock = Clock() def __init__(self, _id, out_el, value, _time): self.id = _id self.output_element = out_el self.value = value self.prev_value = None self.time = _time self.priority = 5 self.done = True # Should the effect be started. if not done it should be started self.cause_time = None # Time when cause was True def start(self, milis): """Api for notifing effect that couse changed to True and at what time""" self.cause_time = milis self.done = False self.prev_value = self.output_element.value def run(self, ): """Sets desired value of element if it was not set yet and if the time is right""" if not self.done: if self.clock.get_millis() - self.cause_time >= self.time: self.done = self.output_element.set_desired_value( self.value, self.priority, set_flag=True) return True return False def revert(self): """Reverts effect. Sets output_element previous value""" if self.done: # efect can be reverted only once when it is done self.output_element.desired_value = self.prev_value def __repr__(self, ): return "El id: {} done: {}".format(self.el_id, self.done)
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 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 LogicManager(threading.Thread): clock = Clock() def __init__( self, args=(), ): threading.Thread.__init__(self, group=None, target=None, name='LOGIC') self._comunication_out_buffer = args[0] self._comunication_in_buffer = args[1] system = args[2] self.elements = system.elements.all self.output_elements = system.elements.output self.output_modules = system.modules.output self.regulations = system.regulations self.dependancies = system.dependancies self._client_priority = 5 self.logger = logging.getLogger('LOGIC') self.tasks = queue.Queue() def set_desired_value(self, _type, _id, value, _msg): """Sets elements desired values""" if _type == 'e': set_flag = False if value > 0: set_flag = True self.output_elements[_id].set_desired_value( value, self._client_priority, set_flag) # Client has low priority. self.logger.debug('Set desired value el: %s val: %s', _id, value) elif _type == 'r': self.regulations[_id].set_point = value self._comunication_in_buffer.put( _msg) # Ack that regulation was set # self.logger.debug(self.output_elements.str()) def process_input_communication(self, ): """Checks if there are any commands from client. """ def parse_msg(msg): try: msg = msg.split(',') _type = msg[0][0] _id = int(msg[0][1:]) value = int(msg[1]) except: return None return _type, _id, value while not self._comunication_out_buffer.empty(): msg = self._comunication_out_buffer.get() self.logger.debug(msg) _type, _id, value = parse_msg(msg) if not msg: yield None yield _type, _id, value, msg def _check_elements_values_and_notify(self, ): """Check elements new value flags which are set by modbus. If there are new values notify interested components and put message to communication thread""" for element in self.elements.values(): if element.new_val_flag: self.logger.debug(element) element.notify_objects( ) # Notifies objects which are interested element.new_val_flag = False if element.type in (Et.pir, Et.rs, Et.switch, Et.heater, Et.blind): msg = 'e' + str(element.id) + ',' + str( element.value) + ',' + 's' else: msg = 'e' + str(element.id) + ',' + str(element.value) yield msg def _run_relations(self, ): """Runs dependancies and regulations""" for dep in self.dependancies.values(): dep.run() for reg in self.regulations.values(): reg.run() def _generate_new_tasks(self, ): """Generates queue with modules which have elements with changed value""" modules_to_notify = set() for out_element in self.output_elements.values(): if out_element.value != out_element.desired_value: modules_to_notify.add( self.output_modules[out_element.module_id]) while modules_to_notify: self.tasks.put(modules_to_notify.pop()) def run(self, ): """Main logic loop""" self.logger.info('Thread {} start'.format(self.name)) while True: self.clock.evaluate_time() for msg in self.process_input_communication(): self.set_desired_value(*msg) for ack_msg in self._check_elements_values_and_notify(): self._comunication_in_buffer.put(ack_msg) self._run_relations() self._generate_new_tasks() time.sleep(0.1)
class TestClock(unittest.TestCase): def setUp(self): self.clock = Clock() self.clock.restart() def test_get_seconds(self): self.assertAlmostEqual(self.clock.get_seconds(), 0, 3) def test_get_milis(self): time.sleep(0.001) self.assertAlmostEqual(self.clock.get_millis(), 1, 0) def test_subscribe(self): notifiables = [Notifiable() for _ in range(3)] for notifiable in notifiables: self.clock.subscribe_for_weekday(notifiable) self.clock.subscribe_for_minute(notifiable) for notifiable in notifiables: self.assertIn(notifiable, self.clock.objects_to_notify_weekday) self.assertIn(notifiable, self.clock.objects_to_notify_time) def test_evaluate_time(self): self.clock.evaluate_time() self.assertEqual(self.clock.now.minute, self.clock.minute) self.assertEqual(self.clock.now.weekday(), self.clock.weekday) def test_notification(self): notifiable_minute = Notifiable() notifiable_weekday = Notifiable() self.clock.subscribe_for_minute(notifiable_minute) self.clock.subscribe_for_weekday(notifiable_weekday) self.clock.now = datetime.datetime.now() self.clock.weekday = 6 self.clock.notify_minute() self.clock.notify_weekday() self.assertEqual(notifiable_minute.val, self.clock.now) self.assertEqual(notifiable_weekday.val, self.clock.weekday)
def setUp(self): self.clock = Clock() self.clock.restart()
def setUp(self): self.clock = Clock() dep_id, name = 0, '' dep_str = '[e1=2] and [e2=3] and [d=mon] and [t=5:30] then e3=20{0}; e3=0{200}; e4=1{0}' self.dep = Dependancy(dep_id, name, dep_str)