import appdaemon.plugins.hass.hassapi as hass from ad_toolbox.smartobject import SmartObject from ad_toolbox.eventhandler import EventHandler from ad_toolbox.expressionparser import ParsingException import time class MotionTracker(SmartObject): MAX_TIME = 120 def initialize(self): super().initialize() if self.dataset == None: self.dataset = { 'areas_movement_time' : dict(), 'last_area_with_movement' : 'Unknown', 'areas_door_close_time' : dict() } else: # clean obsolete keys self.dataset['areas_movement_time'] = {area : self.dataset['areas_movement_time'][area] for area in self.args['areas'] if area in self.dataset['areas_movement_time']} self.dataset['areas_door_close_time'] = {area : self.dataset['areas_door_close_time'][area] for area in self.args['areas'] if area in self.dataset['areas_door_close_time']} self.input_sensors = dict() self.output_last_motion_sensors = dict() self.output_last_motion_time_sensors = dict() self.output_door_close_time_sensors = dict() self.update_cb_handle = None if "areas" in self.args: current_time = time.time() for area in self.args['areas']: update_on_both_front = False self.output_last_motion_sensors[area] = self.create_entity(f"sensor.{area}_last_motion") self.output_last_motion_time_sensors[area] = self.create_entity(f"sensor.{area}_last_motion_time") if isinstance(self.args['areas'][area], dict): sensor_entities = self.args['areas'][area]['motion_sensors'] try: update_on_both_front = self.args['areas'][area]['update_on_both_front'] except KeyError: pass if 'door_sensor' in self.args['areas'][area]: self.output_door_close_time_sensors[area] = self.create_entity(f"{area}_sensor.door_close_time") if not area in self.dataset['areas_door_close_time']: self.dataset['areas_door_close_time'][area] = 0 if not self.output_door_close_time_sensors[area].exists(): self.output_door_close_time_sensors[area].set_state(state = self.dataset['areas_door_close_time'][area],attributes = {'unit_of_measurement' : "s"}) self.listen_state(self.on_door_close,self.args['areas'][area]['door_sensor'],new = 'off', old = 'on',area = area) else: sensor_entities = self.args['areas'][area] if isinstance(sensor_entities, list): for entity in sensor_entities: self.register_motion_sensor(area,entity,update_on_both_front) else: self.register_motion_sensor(area,sensor_entities,update_on_both_front) if not area in self.dataset['areas_movement_time']: self.dataset['areas_movement_time'][area] = 0 if not self.output_last_motion_time_sensors[area].exists(): self.output_last_motion_time_sensors[area].set_state(state = self.dataset['areas_movement_time'][area],attributes = {'unit_of_measurement' : "s"}) self.update_area_sensor(area,current_time) if "clear_areas_events" in self.args: self.event_handlers = list() for entry in self.args["clear_areas_events"]: yaml_block = self.args["clear_areas_events"][entry] try: self.event_handlers.append(EventHandler(self,yaml_block['events_to_listen'],self.on_clear_areas_event,entry)) except ParsingException as e: self.log_error(str(e)) continue self.update_areas_data() def on_clear_areas_event(self, event_name, event_data,entry): for area in self.args["clear_areas_events"][entry]['areas_to_clear']: self.log(f"{area} movement data reseted by {event_name} event") self.dataset['areas_movement_time'][area] = 0 self.output_last_motion_time_sensors[area].set_state(state = 0) self.update_areas_data() def register_motion_sensor(self,area,sensor_entity,update_on_both_front): self.log(f"Registering sensor {sensor_entity} for area {area}") if sensor_entity not in self.input_sensors: self.input_sensors[sensor_entity] = area self.listen_state(self.on_motion_detected,sensor_entity,old = "off", new = "on", area = area) if update_on_both_front: self.listen_state(self.on_motion_detected,sensor_entity,old = "on", new = "off", area = area) else: self.log_error(f"{sensor_entity} is already registered for area {self.input_sensors[sensor_entity]}") def update_area_sensor(self,area,current_time): time_elapsed = min((current_time - self.dataset['areas_movement_time'][area]) / 60,self.MAX_TIME) assert time_elapsed != None self.output_last_motion_sensors[area].set_state(state = int(time_elapsed),attributes = {'unit_of_measurement' : "min"}) def is_excluded_from_last_area_with_movement(self,area): if "areas_excluded_from_last_area_with_movement" in self.args: return area in self.args["areas_excluded_from_last_area_with_movement"] return False def is_area_initialized(self,area): return area in self.output_last_motion_sensors #might need a dedicated boolean at some point def update_areas_data(self,*args): current_time = time.time() #we don't want update_areas_data_to_fork if it's called directly from on_state_change if self.update_cb_handle and self.timer_running(self.update_cb_handle): self.cancel_timer(self.update_cb_handle) self.update_cb_handle = None #I want to start updating the oldest #if not, the order might not be respected for a fraction of seconds. #For example if A = 5 and B = 6 and add 2 to the both of them starting by A #A will be bigger than B (A = 7 and B = 6) for a very short time, and might trigger some automation for area in sorted(self.dataset['areas_movement_time'], key = lambda area: self.dataset['areas_movement_time'][area]): if (self.is_area_initialized(area)): self.update_area_sensor(area,current_time) self.set_state("sensor.last_motion",state = self.dataset['last_area_with_movement']) self.update_cb_handle = self.run_in(self.update_areas_data,30) def on_motion_detected(self, entity, attribute, old, new, kwargs): current_time = time.time() self.dataset['areas_movement_time'][kwargs['area']] = current_time self.output_last_motion_time_sensors[kwargs['area']].set_state(state = current_time) if not self.is_excluded_from_last_area_with_movement(kwargs['area']): self.dataset['last_area_with_movement'] = kwargs['area'] self.update_areas_data() def on_door_close(self, entity, attribute, old, new, kwargs): self.output_door_close_time_sensors[kwargs['area']].set_state(state = time.time())