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

@@ -20,6 +20,7 @@ home_alarm:
- binary_sensor.living_room_presence_presence - binary_sensor.living_room_presence_presence
motion_intrusion_increment: 0.2 motion_intrusion_increment: 0.2
opening_intrusion_increment: 0.9
intrusion_cooldown_step: 0.1 intrusion_cooldown_step: 0.1
intrusion_cooldown_interval: 60 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 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: notifications:
home_alarm_armed_notification: alarm_armed_notification:
recipients: recipients:
all: all:
services: services:
- notify/mobile_app_iphone_de_pierre - notify/mobile_app_iphone_de_pierre
#- notify/mobile_app_mae - notify/mobile_app_mae
conditions: binary_sensor.home_alarm_armed conditions: binary_sensor.home_alarm_armed
message: message:
@@ -48,13 +49,13 @@ home_alarm_notifications:
data: data:
tag: home_alarm_notification tag: home_alarm_notification
home_alarm_windows_open_notification: windows_open_notification:
recipients: recipients:
all: all:
services: services:
- notify/mobile_app_iphone_de_pierre - notify/mobile_app_iphone_de_pierre
#- notify/mobile_app_mae - 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 conditions: binary_sensor.home_alarm_armed and sensor.ad_number_of_windows_open > 0 and binary_sensor.home_alarm_just_armed
message: message:
title: Alarme title: Alarme
@@ -64,13 +65,13 @@ home_alarm_notifications:
push: push:
interruption-level: critical interruption-level: critical
home_alarm_doors_open_notification: doors_open_notification:
recipients: recipients:
all: all:
services: services:
- notify/mobile_app_iphone_de_pierre - notify/mobile_app_iphone_de_pierre
#- notify/mobile_app_mae - 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 conditions: binary_sensor.home_alarm_armed and sensor.ad_number_of_doors_open > 0 and binary_sensor.home_alarm_just_armed
message: message:
title: Alarme title: Alarme
@@ -80,12 +81,42 @@ home_alarm_notifications:
push: push:
interruption-level: critical interruption-level: critical
home_alarm_unarmed_notification: intrusion_detected_notification:
recipients: recipients:
all: all:
services: services:
- notify/mobile_app_iphone_de_pierre - 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 conditions: not binary_sensor.home_alarm_armed
message: message:

View File

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