Enhance motion tracking and occupancy detection with new sensor configurations and improved logic

This commit is contained in:
2026-05-24 22:10:41 +02:00
parent f483c85502
commit 2c9044553e
4 changed files with 79 additions and 16 deletions

View File

@@ -29,12 +29,14 @@ import time
# update_on_both_front: true # optional, default false
# When true, motion is also recorded on the falling edge
# (sensor going on→off), not only on the rising edge.
# While any configured motion sensor remains on, the area
# timestamp is refreshed during the periodic update loop.
# max_time: <minutes> # optional
# Per-area cap, overrides the app-level max_time.
# door_sensor: <entity_id> # optional
# Binary sensor for the area door. When the door closes
# (on→off), the close timestamp is stored and exposed as
# a sensor (see OUTPUT ENTITIES below).
# door_sensors: <entity_id | list of entity_ids> # optional
# One or more binary sensors for the area door. When any
# door closes (on→off), the close timestamp is stored and
# exposed as a sensor (see OUTPUT ENTITIES below).
# # Shorthand form (no extra options needed):
# <area_name>: <entity_id | list of entity_ids>
#
@@ -98,7 +100,7 @@ import time
# - binary_sensor.living_room_motion_1
# - binary_sensor.living_room_motion_2
# max_time: 60
# door_sensor: binary_sensor.living_room_door
# door_sensors: binary_sensor.living_room_door
# garage:
# motion_sensors: binary_sensor.garage_motion
# update_on_both_front: true
@@ -150,6 +152,7 @@ class MotionTracker(SmartObject):
self.output_last_motion_sensors = dict()
self.output_last_motion_time_sensors = dict()
self.output_door_close_time_sensors = dict()
self.area_motion_sensors = dict()
self.app_max_time = self.args.get('max_time', self.MAX_TIME)
self.areas_max_time = dict()
self.update_cb_handle = None
@@ -159,16 +162,18 @@ class MotionTracker(SmartObject):
current_time = time.time()
for area in self.args['areas']:
update_on_both_front = False
door_generate_motion = 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")
self.area_motion_sensors[area] = list()
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
update_on_both_front = self.args['areas'][area].get('update_on_both_front', False)
door_generate_motion = self.args['areas'][area].get('door_generate_motion', False)
if 'max_time' in self.args['areas'][area]:
self.areas_max_time[area] = self.args['areas'][area]['max_time']
if 'door_sensor' in self.args['areas'][area]:
if 'door_sensors' in self.args['areas'][area]:
self.output_door_close_time_sensors[area] = self.create_entity(f"sensor.{area}_door_close_time")
if not area in self.dataset['areas_door_close_time']:
@@ -177,7 +182,13 @@ class MotionTracker(SmartObject):
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)
door_sensor_entities = self.args['areas'][area]['door_sensors']
if not isinstance(door_sensor_entities, list):
door_sensor_entities = [door_sensor_entities]
for door_entity in door_sensor_entities:
self.listen_state(self.on_door_close, door_entity, new = 'off', old = 'on', area = area)
if door_generate_motion:
self.listen_state(self.on_motion_detected, door_entity, area = area)
else:
sensor_entities = self.args['areas'][area]
@@ -233,6 +244,7 @@ class MotionTracker(SmartObject):
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.area_motion_sensors[area].append(sensor_entity)
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)
@@ -247,6 +259,11 @@ class MotionTracker(SmartObject):
assert time_elapsed != None
self.output_last_motion_sensors[area].set_state(state = int(time_elapsed),attributes = {'unit_of_measurement' : "min"})
# Returns True when at least one motion sensor in the area is currently on.
# Used for presence sensors that can remain latched on for long periods.
def is_area_active(self,area):
return any(self.get_state(sensor_entity) == "on" for sensor_entity in self.area_motion_sensors.get(area, []))
# Returns True when the area is listed under
# areas_excluded_from_last_area_with_movement and should not update
# sensor.last_area_with_movement
@@ -283,7 +300,11 @@ class MotionTracker(SmartObject):
#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)
if (self.is_area_initialized(area)):
if self.is_area_active(area):
self.dataset['areas_movement_time'][area] = current_time
self.output_last_motion_time_sensors[area].set_state(state = current_time)
self.update_area_sensor(area,current_time)
self.output_last_area_with_movement_sensor.set_state(self.dataset['last_area_with_movement'])
self.update_cb_handle = self.run_in(self.update_areas_data,30)
@@ -308,4 +329,4 @@ class MotionTracker(SmartObject):
# Fired when the door sensor for an area transitions on→off (door closed).
# Stores the current unix timestamp in the door_close_time sensor.
def on_door_close(self, entity, attribute, old, new, kwargs):
self.output_door_close_time_sensors[kwargs['area']].set_state(state = time.time())
self.output_door_close_time_sensors[kwargs['area']].set_state(state = time.time())