Refactor home alarm notifications and enhance intrusion detection logic

This commit is contained in:
2026-06-19 08:51:18 +02:00
parent eca30960f9
commit 757ece983c
2 changed files with 106 additions and 21 deletions

View File

@@ -33,6 +33,9 @@ from datetime import datetime
# motion_intrusion_increment: <float> # optional, default 0.1
# Intrusion level increment applied per motion detection.
#
# opening_intrusion_increment: <float> # optional, default 1.0
# Intrusion level increment applied per opening detection.
#
# intrusion_cooldown_step: <float> # optional, default 0.1
# Value subtracted from the intrusion level on each cooldown tick.
#
@@ -50,15 +53,20 @@ from datetime import datetime
# off = alarm is disarmed
# Attributes: sensors_ignored_count
#
# binary_sensor.<app_name>_just_armed
# on = during the 1s window immediately after arming
# off = otherwise
#
# sensor.<app_name>_intrusion_level
# Numeric intrusion level.
# 0 means no intrusion.
# Each opening adds +1.
# Each opening adds +opening_intrusion_increment.
# Each motion adds +motion_intrusion_increment.
# Cooldown lowers the level over time but never below the integer count
# of opening intrusions (e.g. 1.5 can cool down to 1, not below).
# Attributes: breach_list, breach_count, opening_intrusions,
# last_intrusion_time, last_intrusion_source, cooldown config.
# last_intrusion_time, last_intrusion_source, event_log,
# event_log_markdown.
#
# binary_sensor.<app_name>_breach_<local_name> (one per opening sensor)
# on = breach detected on this sensor during the current arm cycle
@@ -84,6 +92,8 @@ from datetime import datetime
class HomeAlarm(SmartObject):
EVENT_LOG_MAX_SIZE = 30
# ------------------------------------------------------------------
# Initialisation
# ------------------------------------------------------------------
@@ -97,11 +107,17 @@ class HomeAlarm(SmartObject):
self._last_intrusion_source = None
self.intrusion_level = 0.0
self.opening_intrusions_count = 0
self.event_log = []
self.just_armed_off_timer = None
self.motion_intrusion_increment = self._read_positive_float(
'motion_intrusion_increment',
0.1,
)
self.opening_intrusion_increment = self._read_positive_float(
'opening_intrusion_increment',
1.0,
)
self.intrusion_cooldown_step = self._read_positive_float(
'intrusion_cooldown_step',
0.1,
@@ -143,6 +159,13 @@ class HomeAlarm(SmartObject):
attributes={'sensors_ignored_count': 0},
)
self.just_armed_sensor = self.create_entity(
f"binary_sensor.{self.name}_just_armed",
state='off',
icon='mdi:timer-sand',
friendly_name='Alarm Just Armed',
)
# Intrusion level sensor
self.intrusion_sensor = self.create_entity(
f"sensor.{self.name}_intrusion_level",
@@ -155,9 +178,8 @@ class HomeAlarm(SmartObject):
'opening_intrusions': 0,
'last_intrusion_time': None,
'last_intrusion_source': None,
'motion_intrusion_increment': self.motion_intrusion_increment,
'intrusion_cooldown_step': self.intrusion_cooldown_step,
'intrusion_cooldown_interval': self.intrusion_cooldown_interval,
'event_log': [],
'event_log_markdown': '',
},
)
@@ -199,8 +221,16 @@ class HomeAlarm(SmartObject):
def on_alarm_armed(self):
self.log_info("Alarm ARMED")
self.event_log = []
self._push_event("Alarm armed")
self.alarm_active = True
if self.just_armed_off_timer is not None:
self.cancel_timer(self.just_armed_off_timer)
self.just_armed_off_timer = None
self.just_armed_sensor.set_state(state='on')
self.just_armed_off_timer = self.run_in(self.on_just_armed_timeout, 1)
# Snapshot sensors that are already open — they will be ignored
self.sensors_open_at_activation = {
s for s in self.opening_sensors if self.get_state(s) == 'on'
@@ -229,17 +259,25 @@ class HomeAlarm(SmartObject):
def on_alarm_disarmed(self):
self.log_info("Alarm DISARMED")
self._push_event("Alarm disarmed")
self.alarm_active = False
self.sensors_open_at_activation = set()
if self.just_armed_off_timer is not None:
self.cancel_timer(self.just_armed_off_timer)
self.just_armed_off_timer = None
self.just_armed_sensor.set_state(state='off')
# Reset live intrusion level while keeping historical attributes/events.
self.intrusion_level = 0.0
self.armed_sensor.set_state(
state='off',
attributes={
'sensors_ignored_count': 0,
},
)
# Intentionally keep breach sensors and intrusion sensor as-is
# so detections remain visible until manually reset.
self._update_intrusion_sensor()
# ------------------------------------------------------------------
# Opening sensor state changes
@@ -257,8 +295,9 @@ class HomeAlarm(SmartObject):
return
self.log_info(f"INTRUSION DETECTED via {entity}")
self._push_event(f"Opening intrusion: {entity}")
self.breach_sensors[entity].set_state(state='on')
self._add_intrusion(1.0, entity, count_as_opening_intrusion=True)
self._add_intrusion(self.opening_intrusion_increment, entity, count_as_opening_intrusion=True)
def on_motion_sensor_change(self, entity, attribute, old, new, kwargs):
if not self.alarm_active:
@@ -268,9 +307,13 @@ class HomeAlarm(SmartObject):
return
self.log_info(f"MOTION DETECTED via {entity}")
self._push_event(f"Motion detected: {entity}")
self._add_intrusion(self.motion_intrusion_increment, entity)
def on_intrusion_cooldown(self, kwargs):
if not self.alarm_active:
return
floor = float(self.opening_intrusions_count)
if self.intrusion_level <= floor:
if self.intrusion_level != floor:
@@ -287,12 +330,17 @@ class HomeAlarm(SmartObject):
self.intrusion_level = new_value
self._update_intrusion_sensor()
def on_just_armed_timeout(self, kwargs):
self.just_armed_off_timer = None
self.just_armed_sensor.set_state(state='off')
# ------------------------------------------------------------------
# Reset button
# ------------------------------------------------------------------
def on_reset_button(self, entity, attribute, old, new, kwargs):
self.log_info("Breach sensors reset")
self._push_event("Alarm reset")
for handle in self.breach_sensors.values():
handle.set_state(state='off')
self.intrusion_level = 0.0
@@ -363,8 +411,15 @@ class HomeAlarm(SmartObject):
self._last_breach_time = datetime.now().isoformat()
self._last_intrusion_source = source
#self._push_event(f"Intrusion level +{increment} from {source} -> {self.intrusion_level}")
self._update_intrusion_sensor()
def _push_event(self, message):
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.event_log.insert(0, f"{now} - {message}")
if len(self.event_log) > self.EVENT_LOG_MAX_SIZE:
self.event_log = self.event_log[:self.EVENT_LOG_MAX_SIZE]
def _update_intrusion_sensor(self):
"""Recompute and push the intrusion level sensor state."""
active = [
@@ -385,8 +440,7 @@ class HomeAlarm(SmartObject):
'intrusion_floor': floor,
'last_intrusion_time': self._last_breach_time,
'last_intrusion_source': self._last_intrusion_source,
'motion_intrusion_increment': self.motion_intrusion_increment,
'intrusion_cooldown_step': self.intrusion_cooldown_step,
'intrusion_cooldown_interval': self.intrusion_cooldown_interval,
'event_log': self.event_log,
'event_log_markdown': "\n".join(f"- {line}" for line in self.event_log),
},
)