From 757ece983c836d8f0df7f3528babb0bc30beefe2 Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 19 Jun 2026 08:51:18 +0200 Subject: [PATCH] Refactor home alarm notifications and enhance intrusion detection logic --- apps/global/home_alarm.yaml | 51 ++++++++++++++++++++----- apps/homealarm.py | 76 +++++++++++++++++++++++++++++++------ 2 files changed, 106 insertions(+), 21 deletions(-) diff --git a/apps/global/home_alarm.yaml b/apps/global/home_alarm.yaml index 1c3fd9d..2c1fb38 100644 --- a/apps/global/home_alarm.yaml +++ b/apps/global/home_alarm.yaml @@ -20,6 +20,7 @@ home_alarm: - binary_sensor.living_room_presence_presence motion_intrusion_increment: 0.2 + opening_intrusion_increment: 0.9 intrusion_cooldown_step: 0.1 intrusion_cooldown_interval: 60 @@ -33,12 +34,12 @@ home_alarm_notifications: priority: 100 # default priority app is 50, since the notification_manager doesn't create any sensor but is based on sensor created by many app, it's important it's created last notifications: - home_alarm_armed_notification: + alarm_armed_notification: recipients: all: services: - notify/mobile_app_iphone_de_pierre - #- notify/mobile_app_mae + - notify/mobile_app_mae conditions: binary_sensor.home_alarm_armed message: @@ -48,13 +49,13 @@ home_alarm_notifications: data: tag: home_alarm_notification - home_alarm_windows_open_notification: + windows_open_notification: recipients: all: services: - notify/mobile_app_iphone_de_pierre - #- notify/mobile_app_mae - conditions: binary_sensor.home_alarm_armed and sensor.ad_number_of_windows_open > 0 and sensor.home_alarm_intrusion_level == 0 + - notify/mobile_app_mae + conditions: binary_sensor.home_alarm_armed and sensor.ad_number_of_windows_open > 0 and binary_sensor.home_alarm_just_armed message: title: Alarme @@ -64,13 +65,13 @@ home_alarm_notifications: push: interruption-level: critical - home_alarm_doors_open_notification: + doors_open_notification: recipients: all: services: - notify/mobile_app_iphone_de_pierre - #- notify/mobile_app_mae - conditions: binary_sensor.home_alarm_armed and sensor.ad_number_of_doors_open > 0 and sensor.home_alarm_intrusion_level == 0 + - notify/mobile_app_mae + conditions: binary_sensor.home_alarm_armed and sensor.ad_number_of_doors_open > 0 and binary_sensor.home_alarm_just_armed message: title: Alarme @@ -80,12 +81,42 @@ home_alarm_notifications: push: interruption-level: critical - home_alarm_unarmed_notification: + intrusion_detected_notification: recipients: all: services: - notify/mobile_app_iphone_de_pierre - #- notify/mobile_app_mae + - notify/mobile_app_mae + conditions: binary_sensor.home_alarm_armed and sensor.home_alarm_intrusion_level >= 1 + + message: + title: Alarme + content: Intrusion détectée + data: + tag: home_alarm_notification + push: + interruption-level: critical + + intrusion_suspected_notification: + recipients: + all: + services: + - notify/mobile_app_iphone_de_pierre + - notify/mobile_app_mae + conditions: binary_sensor.home_alarm_armed and sensor.home_alarm_intrusion_level > 0 and sensor.home_alarm_intrusion_level < 1 + + message: + title: Alarme + content: Intrusion suspectée + data: + tag: home_alarm_notification + + alarm_unarmed_notification: + recipients: + all: + services: + - notify/mobile_app_iphone_de_pierre + - notify/mobile_app_mae conditions: not binary_sensor.home_alarm_armed message: diff --git a/apps/homealarm.py b/apps/homealarm.py index db9767d..a9c41b5 100644 --- a/apps/homealarm.py +++ b/apps/homealarm.py @@ -33,6 +33,9 @@ from datetime import datetime # motion_intrusion_increment: # optional, default 0.1 # Intrusion level increment applied per motion detection. # +# opening_intrusion_increment: # optional, default 1.0 +# Intrusion level increment applied per opening detection. +# # intrusion_cooldown_step: # 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._just_armed +# on = during the 1s window immediately after arming +# off = otherwise +# # sensor._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._breach_ (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), }, )