def _devices_to_process(self) -> list: # todo: area filtering to_check = self.condition.check_instance to_process = [] disabled_list = [] if isinstance(to_check, GaInputModel): if to_check.enabled == 1: for device in to_check.member_list: if device.enabled == 1: to_process.append(device) else: disabled_list.append(device) else: if to_check.enabled == 1: to_process.append(to_check) else: disabled_list.append(to_check) if len(disabled_list) > 0: device_log(f"Condition match \"{self.condition.name}\" has some disabled inputs: \"{disabled_list}\"", add=self.name, level=8) if len(to_process) == 0: device_log(f"Got no inputs to pull data from for condition match \"{self.condition.name}\"", add=self.name, level=4) raise ValueError(f"No data to process for match \"{self.condition.name}\"") return to_process
def _get_link_data(self, link: GaConditionLink) -> dict: """Will get the two results of its condition-link members. :param link: Condition link to process :type link: GaConditionLink :return: Member results :rtype: dict """ device_log(f"Getting data for link \"{link.name}\"", add=self.name, level=9) result_dict = {} # process all conditions in link for order, condition in link.condition_match_dict.items(): if condition.data is not None: # get its data from a previous link-result if it is set result_dict[order] = condition.data else: # else process the condition result_dict[order] = ConditionResult( condition=condition, device=self.group.name).get() for order, condition in link.condition_group_dict.items(): # if group -> run function recursively result_dict[order] = self._process_links(group=condition) device_log( f"Data of condition-link \"{link.name}\": \"{result_dict}\"", add=self.name, level=8) return result_dict
def _get_link_data_result(self, link: GaConditionLink, result_dict: dict) -> bool: """Will get the result for a link while using its members results. :param link: Condition link to process :type link: GaConditionLink :param result_dict: Results of the links members :type result_dict: dict :return: Result of the link :rtype: bool """ device_log(f"Getting result of condition-link \"{link.name}\"", add=self.name, level=9) try: result = self._get_result( link=link, result_dict=result_dict, ) device_log( f"Result of condition-link \"{link.name}\": \"{result}\"", add=self.name, level=8) return result except (KeyError, ValueError) as error_msg: return self._error(error_exception=error_msg)
def _downlink(self, instance): dl = instance.downlink if self.manually or dl.enabled == 1: device_log(f"Device \"{instance.name}\" is connected via downlink \"{dl.name}\"", add=self.name, level=7) self.task_instance_list.append({'downlink': dl, 'device': instance}) else: device_log(f"Device \"{dl.name}\" is disabled and will not be processed!", add=self.name, level=3)
def _model(self): if self.manually or self.instance.enabled == 1: for device_instance in self.instance.member_list: if device_instance.enabled == 1: self._device(instance=device_instance) else: device_log(f"Device \"{self.instance.name}\" is disabled and will not be processed!", add=self.name, level=2)
def _parse_error(self): device_log( f'Unable to parse {self.condition.check_instance.name} value provided for condition match \"{self.condition.name}\". ' f'Please check the documentation for supported formats.', add=self.name, level=4) raise ValueError( f'Unsupported value for condition match \"{self.condition.name}\"')
def _device(self, instance): if self.manually or instance.enabled == 1: if instance.downlink is not None: self._downlink(instance=instance) else: self.task_instance_list.append({'device': instance}) else: device_log(f"Device \"{instance.name}\" is disabled and will not be processed!", add=self.name, level=3)
def _reset_flags(self, group: GaConditionGroup) -> None: """Resets states of links and data of link members and resets them to the defaults. :param group: Condition to process for resetting :type group: GaConditionGroup :rtype: None """ device_log(f"Resetting flags for condition \"{group.name}\"", add=self.name, level=9) for link in group.member_list: link.processed = False for condition in self._get_member_list(link): condition.data = None
def _fail_check(self): if not self.manually and self.instance.fail_sleep is not None: # skip fail-sleep if executed manually if datetime.now() < self.instance.fail_sleep: device_log( f"Skipping execution of device \"{self.instance.name}\" since it has reached the max error threshold", add=self.name, level=4) device_log( f"Device \"{self.instance.name}\" will be skipped until \"{self.instance.fail_sleep.strftime('%Y-%m-%d %H:%M:%S:%f')}\"", add=self.name, level=6) return False return True
def _get_data(self) -> None: self.process_list = self._devices_to_process() self.data_method = self._get_data_prerequisites() self.data_type = self._get_data_type() if isinstance(self.condition.check_instance, GaInputModel): self._get_data_group() else: self.data_list = self._get_data_device(device=self.process_list[0]) if len(self.data_list) == 0: device_log(f"No data received for condition match \"{self.condition.name}\" (id \"{self.condition.object_id}\")", add=self.name, level=5) raise ValueError(f"Got no data for condition match \"{self.condition.name}\"")
def _get_data_prerequisites(self): # should only run once since its the same for all devices processed period_type = self.condition.period device_log(f"Condition match \"{self.condition.name}\", period type \"{period_type}\", period \"{self.condition.period_data}\"", add=self.name, level=8) if period_type == 'time': data_method = self._get_data_by_time elif period_type == 'range': data_method = self._get_data_by_range else: device_log(f"Condition match \"{self.condition.name}\" has an unsupported period_type \"{period_type}\"", add=self.name, level=4) raise ValueError(f"Unsupported period type for condition match \"{self.condition.name}\"") return data_method
def get(instance) -> bool: start_time = time() while time() < start_time + LOCK_MAX_WAIT: if not instance.locked: instance.locked = True return True else: sleep(LOCK_CHECK_INTERVAL) device_log( f"Gave up to get lock for device \"{instance.name}\" after \"{LOCK_MAX_WAIT}\" sec", add=instance.name, level=6) return False
def _datetime_compare(self, value: datetime, equal_format: str): """ Will compare a given datetime to the current one. :param value: Datetime to match :param equal_format: Datetime format used for equality check :return: bool """ now = datetime.now() operator = self.condition.operator result = False def _equal(_now, _data) -> bool: # will use custom time format since some make no sense for this comparison if now.strftime(equal_format) == value.strftime(equal_format): return True return False if operator == '=': result = _equal(now, value) elif operator == '!=': result = not _equal(now, value) elif operator == '>': if value > now: result = True elif operator == '<': if value < now: result = True else: device_log( f"Condition match \"{self.condition.name}\" has an unsupported operator \"{operator}\"", add=self.name, level=4) raise ValueError( f"Unsupported operator for condition \"{self.condition.name}\"" ) device_log( f"Condition match \"{self.condition.name}\" result for comparison \"{value} {operator} {now}\" = {result}", add=self.name, level=8) return result
def get(self) -> bool: if self.condition.check_instance.name == 'time': return self._time(raw_value=self.condition.value.copy()) elif self.condition.check_instance.name == 'date': return self._date(raw_value=self.condition.value.copy()) elif self.condition.check_instance.name.startswith('day_'): return self._day() device_log( f"Condition match \"{self.condition.name}\" has an unsupported special match set: \"{self.condition.check_instance.name}\"", add=self.name, level=4) raise ValueError( f"Unsupported special match type for condition \"{self.condition.name}\"" )
def _run(self) -> bool: if self.manually: if type(self.instance) == GaOutputModel: output_list = self.instance.member_list elif type(self.instance) == GaOutputDevice: output_list = [self.instance] else: device_log( f'Manual execution only allows OutputModels and OutputDevices to be supplied - got neither!', add=self.name, level=3) return False device_log(f"Output list to process: \"{output_list}\"", add=self.name, level=7) areas = None else: # service will always supply a GaConditionGroup output_list = self.instance.output_object_list.copy() device_log( f"Output list of \"{self.instance.name}\": \"{output_list}\"", add=self.name, level=7) output_list.extend(self.instance.output_group_list) device_log( f"Output-group list of \"{self.instance.name}\": \"{self.instance.output_group_list}\"", add=self.name, level=7) areas = self.instance.area_group_list task_instance_list = [] for output in output_list: task_instance_list.extend( Check( instance=output, model_obj=GaOutputModel, device_obj=GaOutputDevice, areas=areas, manually=self.manually, ).get()) results = [] for task_dict in task_instance_list: results.append(self._process(task_dict=task_dict)) if len(results) > 0: return all(results) return False
def _error_action(self, error) -> None: if error not in config.NONE_RESULTS: device_log( f"An error occurred while processing device \"{self.instance.name}\": \"{error}\"", add=self.name) self.instance.fail_count += 1 if self.instance.fail_count > config.AGENT.device_fail_count: self.instance.fail_sleep = datetime.fromtimestamp( datetime.now().timestamp() + config.AGENT.device_fail_sleep) device_log( f"Device \"{self.instance.name}\" has reached its fail threshold -> will skip execution " f"until \"{self.instance.fail_sleep.strftime('%Y-%m-%d %H:%M:%S:%f')}\"", add=self.name, level=3) else: self.instance.fail_count = 0
def start(self) -> (str, None, bool): # output: # None -> Error # False -> Fail-Sleep # True -> successful output # str/data -> successful input if self._fail_check(): if lock.get(instance=self.instance): result = self._execute() lock.remove(instance=self.instance) if isinstance(self.instance, (GaInputDevice, GaConnectionDevice)): if result not in config.NONE_RESULTS: device_log( f"Got result for device \"{self.instance.name}\": \"{result}\"", add=self.name, level=6) else: device_log( f"Got no result for device \"{self.instance.name}\"!", add=self.name, level=3) if self.instance.fail_count == 0: try: data = json_loads(result) return data[self.INPUT_DATA_KEY] except (KeyError, JSONDecodeError) as error: device_log( f"Unable decode received data; error: \"{error}\"", add=self.name, level=2) return None else: if result not in config.NONE_RESULTS: # output devices do not need to return a useful result device_log( f"Got result for device \"{self.instance.name}\": \"{result}\"", add=self.name, level=6) return result else: return False return None
def _reverse_condition(self, task_dict: dict) -> None: device = task_dict['device'] device_log( f"Entering reverse-condition loop for output-device \"{device.name}\"", add=self.name, level=6) thread = Thread() thread_description = f"Conditional reversing for '{device.name}'" @thread.add_thread( sleep_time=int(1), thread_data=task_dict, once=True, description=thread_description, ) def thread_task(data): tries = 0 while not self._process(task_dict=data, reverse=True): if config.REVERSE_CONDITION_MAX_RETRIES is not None and tries >= config.REVERSE_CONDITION_MAX_RETRIES: device_log( f"Reversing of device \"{device.name}\" failed: reached maximum number of retries {tries}", add=self.name, level=3) break device_log(f"Reversing of device \"{device.name}\" continues", add=self.name, level=8) sleep(config.REVERSE_CONDITION_INTERVAL) tries += 1 if config.REVERSE_CONDITION_MAX_RETRIES is None or tries < config.REVERSE_CONDITION_MAX_RETRIES: device_log(f"Reversing of device \"{device.name}\" finished", add=self.name, level=6) self._exit() thread.stop_thread(description=device.name) thread.start_thread(description=thread_description)
def _get_data_type(self) -> (bool, float, int, str): data_type = self.condition.check_instance.datatype if data_type == 'bool': typ = bool elif data_type == 'float': typ = float elif data_type == 'int': typ = int elif data_type == 'str': typ = str else: device_log(f"Input device/model \"{self.condition.check_instance.name}\" has an unsupported data data_type set \"{data_type}\"", level=4) raise ValueError(f"Unsupported data type for input \"{self.condition.check_instance.name}\"") return typ
def start(self) -> bool: task_instance_list = Check( instance=self.instance, model_obj=GaInputModel, device_obj=GaInputDevice, ).get() results = [] for task_dict in task_instance_list: device = task_dict['device'] task_name = device.name task_id = device.object_id device_log(f"Processing device instance: \"{device.__dict__}\"", add=self.name, level=7) if 'downlink' in task_dict: data = Process( instance=task_dict['downlink'], nested_instance=device, script_dir='connection', manually=self.manually, ).start() else: data = Process( instance=device, script_dir='input', manually=self.manually, ).start() if data is None: device_log(f"No data received for device \"{task_name}\"", add=self.name, level=3) results.append(False) elif data is False: device_log(f"Device \"{task_name}\" is in fail-sleep", add=self.name, level=4) results.append(False) else: self.database.put(command=self.SQL_DATA_COMMAND % (datetime.now(), data, task_id)) device_log(f"Processing of input-device \"{task_name}\" succeeded", add=self.name, level=7) results.append(True) del self.database if len(results) > 0: return all(results) return False
def _reverse_timer(self, task_dict: dict) -> None: device = task_dict['device'] device_log( f"Entering wait timer ({device.reverse_type_data} secs) for output-device \"{device.name}\"", add=self.name, level=6) thread = Thread() @thread.add_thread( sleep_time=int(device.reverse_type_data), thread_data=task_dict, once=True, description=f"Timed reversing for '{device.name}'", ) def thread_task(data): self._process(task_dict=data, reverse=True) device_log(f"Reversing of device \"{device.name}\" finished", add=self.name, level=6) thread.stop_thread(description=device.name) thread.start()
def _reverse_check(self): if self.manually: # if a user manually executes an action we will not check if it is active => let the user overrule possible bugs.. return True elif isinstance( self.instance, GaOutputDevice ) and self.instance.reverse == 1 and self.instance.active and not self.reverse: device_log( f"Reversible device \"{self.instance.name}\" is active and should not be reversed", add=self.name, level=5) return False elif isinstance(self.instance, GaConnectionDevice) and isinstance(self.nested_instance, GaOutputDevice) \ and self.nested_instance.reverse == 1 and self.nested_instance.active and not self.reverse: device_log( f"Reversible device \"{self.nested_instance.name}\" is active and should not be reversed", add=self.name, level=5) return False return True
def _compare_day(self, value: int, compare: int) -> bool: """ Simple comparison of current day against the match-day. :param value: Day to match :param compare: Current day :return: bool """ operator = self.condition.operator result = False if operator == '=': if value == compare: result = True elif operator == '!=': if value != compare: result = True elif operator == '>': if value > compare: result = True elif operator == '<': if value < compare: result = True else: device_log( f"Condition match \"{self.condition.name}\" has an unsupported operator \"{operator}\"", add=self.name, level=4) raise ValueError( f"Unsupported operator for condition \"{self.condition.name}\"" ) return result
def _reverse_flags(self, error) -> (bool, None): if isinstance(self.instance, GaOutputDevice): if error is None: if self.instance.reverse == 1: if self.reverse: self.instance.active = False device_log( f"Reversible device \"{self.instance.name}\" was stopped", add=self.name, level=6) else: self.instance.active = True device_log( f"Reversible device \"{self.instance.name}\" entered the active state", add=self.name, level=6) return True else: return None return False
def _get_single_link_member_list(self, group: GaConditionGroup) -> list: """Will get a list of link members that are only linked at one non-processed link. We must always start processing at a 'edge' link. Else we might break the chain. :param group: Condition to check the link from :type group: GaConditionGroup :return: List of link members that are linked only once :rtype: list """ device_log( f"Getting single link members for condition \"{group.name}\"", add=self.name, level=9) lm_list = [] slm_list = [] for link in group.member_list: if not link.processed: lm_list.extend(self._get_member_list(link)) # check all non-processed link members for multiple occurrence counted = Counter(lm_list) for instance, count in counted.items(): if count == 1: slm_list.append(instance) device_log( f"Condition \"{group.name}\" has the following single link members \"{slm_list}\"", add=self.name, level=8) if len(slm_list) == 0: raise ValueError('It looks like you have a configuration error.') return slm_list
def _process_links(self, group: GaConditionGroup) -> bool: """Will be called to process links inside of a condition. Is used recursively. :param group: The condition object :type group: GaConditionGroup :return: Will return the calculated result of the condition :rtype: bool """ device_log(f"Processing links for condition \"{group.name}\"", add=self.name, level=8) group_member_count = len(group.member_list) last_result = None for process_nr in range(1, group_member_count + 1): link = self._get_link_to_process(group=group) result_dict = self._get_link_data(link=link) result = self._get_link_data_result(link=link, result_dict=result_dict) self._post_process(link=link, result=result, group=group, process_nr=process_nr, group_member_count=group_member_count) link.processed = True last_result = result device_log(f"Result of condition \"{group.name}\": \"{last_result}\" ", add=self.name, level=6) self._reset_flags(group) return last_result
def _post_process(self, link: GaConditionLink, result: bool, group: GaConditionGroup, process_nr: int, group_member_count: int) -> None: """Check which of the two members will be further processed. Updating the value of the link to further process to the links result. :param link: Link to check the members from :type link: GaConditionLink :param result: Result of the link :type result: bool :param group: Condition to which the link belongs :type group: GaConditionGroup :param process_nr: The links process number :type process_nr: int :param group_member_count: Number of links in the group :type group_member_count: int :rtype: None :raises: RuntimeError: There is no condition match to further process """ device_log( f"Post-process of condition \"{group.name}\", link \"{link.name}\", result \"{result}\", process_nr \"{process_nr}\", group_member_count \"{group_member_count}\"", add=self.name, level=9) slm_list = self._get_single_link_member_list(group=group) nslm = False for condition in self._get_member_list(link): if nslm: # there can only be one member to further process break if condition not in slm_list: # if the condition will be further processed -> update its data to the link result device_log( f"Updating data of condition \"{condition.name}\" to result \"{result}\"", add=self.name, level=9) condition.data = result nslm = True if process_nr != group_member_count and not nslm: # there is no condition that should be further processed and the link is not the last one device_log( f"Link \"{link.name}\" (id \"{link.object_id}\") has only single members \"{slm_list}\" and is not the last one to process", add=self.name, level=4) self._error( RuntimeError( f"Link with id \"{link.object_id}\" is not the last to process but it does not have any link-neighbors." ))
def get(self) -> list: if isinstance(self.instance, self.model_obj): self._model() elif isinstance(self.instance, self.device_obj): self._device(instance=self.instance) else: device_log(f"Object \"{self.instance.name}\" matches neither provided objects", add=self.name, level=3) device_log(f"Object \"{self.instance.name}\" - unfiltered device list to process: \"{self.task_instance_list}\"", add=self.name, level=8) filtered_instance_list = area_filter(areas=self.areas, devices=self.task_instance_list) device_log(f"Object \"{self.instance.name}\" - filtered device list to process: \"{filtered_instance_list}\"", add=self.name, level=7) return filtered_instance_list
def _get_script_params(self) -> (None, dict): params_dict = { 'script': self.instance.script, 'bin': self.instance.script_bin, 'arg': self.instance.script_arg } if params_dict['bin'] == 'python3': params_dict[ 'bin'] = f'{config.AGENT.path_home}{config.PATH_HOME_VENV}/python3' # reverse parameters if isinstance(self.instance, GaOutputDevice) and self.instance.reverse == 1: if self.instance.active or self.manually: if self.reverse: if self.instance.reverse_script is not None: params_dict['script'] = self.instance.reverse_script if self.instance.reverse_script_arg is not None: params_dict['arg'] = self.instance.reverse_script_arg if self.instance.reverse_script_bin is not None: params_dict['bin'] = self.instance.reverse_script_bin else: if not self.manually: device_log( f"Device \"{self.instance.name}\" is reversible and active, but should not be reversed", add=self.name, level=4) return None else: device_log( f"Device \"{self.instance.name}\" is either not reversible or not active", add=self.name, level=7) if params_dict[ 'bin'] in config.NONE_RESULTS: # it should only be possible to be NoneType-None device_log( f"No binary provided to execute for device \"{self.instance.name}\"", add=self.name, level=2) return None return params_dict
def start(self) -> bool: if self.manually: # if the action is triggered manually by the user we don't want to check the conditions _ = 'stopp' if self.action == 'stop' else self.action device_log( f"{_.capitalize()}ing \"{self.instance.name}\" manually", add=self.name, level=5) return self._run() elif GetGroupResult(group=self.instance).go(): device_log(f"Conditions for \"{self.instance.name}\" were met", add=self.name, level=6) return self._run() else: device_log(f"Conditions for \"{self.instance.name}\" were not met", add=self.name, level=3) return False