Compare commits

...

9 Commits

9 changed files with 1386 additions and 17 deletions

View File

@@ -137,6 +137,7 @@ living_room_heatpump:
virtual_sensors: virtual_sensors:
sensors: sensors:
binary_sensor.living_room_any_window_open: binary_sensor.living_room_french_door or binary_sensor.living_room_window binary_sensor.living_room_any_window_open: binary_sensor.living_room_french_door or binary_sensor.living_room_window
binary_sensor.living_room_cant_open_window: binary_sensor.is_living_room_sleeping or binary_sensor.projector_on
constants: constants:
window_open: binary_sensor.living_room_any_window_open window_open: binary_sensor.living_room_any_window_open
@@ -144,7 +145,7 @@ living_room_heatpump:
room_temperature: sensor.living_room_temperature room_temperature: sensor.living_room_temperature
room_occupancy: binary_sensor.living_room_occupancy room_occupancy: binary_sensor.living_room_occupancy
target_temperature: sensor.target_temperature_heatpump target_temperature: sensor.target_temperature_heatpump
cant_open_window: binary_sensor.is_living_room_sleeping cant_open_window: binary_sensor.living_room_cant_open_window
sleeping_in_the_room: binary_sensor.is_living_room_sleeping sleeping_in_the_room: binary_sensor.is_living_room_sleeping
quick_resume: binary_sensor.living_room_heatpump_quick_resume quick_resume: binary_sensor.living_room_heatpump_quick_resume
stay_on_while_sleeping: false stay_on_while_sleeping: false

174
apps/global/home_alarm.yaml Normal file
View File

@@ -0,0 +1,174 @@
home_alarm:
module: homealarm
class: HomeAlarm
smart_conditions:
trigger_conditions: not binary_sensor.someone_home or input_select.alarm_mode == 'On'
blocking_conditions: input_select.alarm_mode == 'Off'
opening_sensors:
- binary_sensor.group_doors
- binary_sensor.group_windows
motion_sensors:
- binary_sensor.corridor_motion
- binary_sensor.hallway_motion
- binary_sensor.restroom_motion
- binary_sensor.garage_motion
- binary_sensor.kitchen_motion
- binary_sensor.mezzanine_motion
- 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
reset_button: input_button.alarm_reset
presence_simulation:
smart_conditions: input_boolean.simulate_presence and binary_sensor.home_alarm_armed
target_apps:
restroom:
# 5 minutes every 2 hours with 1 hour jitter
app: light_restroom
mode: method
start_payload:
brightness_pct: 100
transition: 1
stop_payload:
transition: 1
on_duration_min: 5
on_jitter_min: 1
interval_min: 120
interval_jitter_min: 60
start_delay_min: 60
start_delay_jitter_min: 20
light_living_room:
# Exemple "bureau toute la soiree": longues periodes allumees le soir
# on_duration_min / interval_min are optional:
# if omitted, the app stays active for the whole simulation window
# (still gated by smart_conditions + time_window).
app: light_living_room
status_sensor: binary_sensor.home_alarm_presence_mezzanine
mode: method
smart_conditions: binary_sensor.is_dark_outside
time_window:
stop: '23:30'
stop_jitter_min: 30
start_payload:
brightness_pct: 100
transition: 2
stop_payload:
transition: 2
home_alarm_notifications:
module: notificationsmanager
class: NotificationsManager
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:
alarm_armed_notification:
recipients:
all:
services:
- notify/mobile_app_iphone_de_pierre
- notify/mobile_app_mae
conditions: binary_sensor.home_alarm_armed
message:
title: Alarme
content: Activation de l'alarme
timeout: 600
data:
tag: home_alarm_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 binary_sensor.home_alarm_just_armed
message:
title: Alarme
content: "{sensor.ad_number_of_windows_open} fenêtre(s) ouvertes"
data:
tag: home_alarm_notification
push:
interruption-level: critical
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 binary_sensor.home_alarm_just_armed
message:
title: Alarme
content: "{sensor.ad_number_of_doors_open} porte(s) ouvertes"
data:
tag: home_alarm_notification
push:
interruption-level: critical
intrusion_detected_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 >= 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:
title: Alarme
content: Désactivation de l'alarme
timeout: 600
clear_home_alarm_notification:
recipients:
all:
services:
- notify/mobile_app_iphone_de_pierre
- notify/mobile_app_mae
conditions: not binary_sensor.home_alarm_armed
message:
content: clear_notification
data:
tag: home_alarm_notification

View File

@@ -33,6 +33,9 @@ occupancy_sensors:
class: VirtualSensorsApp class: VirtualSensorsApp
priority: 8 # default priority app is 50, since most of them require sensors created by virtual sensors, it's important that virtual_sensors start first priority: 8 # default priority app is 50, since most of them require sensors created by virtual sensors, it's important that virtual_sensors start first
default_values:
binary_sensor.someone_home: off
sensors: sensors:
binary_sensor.living_room_occupancy: binary_sensor.living_room_occupancy:
trigger_conditions: sensor.living_room_last_motion <= 5 trigger_conditions: sensor.living_room_last_motion <= 5

View File

@@ -4,24 +4,48 @@ virtual_sensors:
priority: 10 # default priority app is 50, since most of them require sensors created by virtual sensors, it's important that virtual_sensors start first priority: 10 # default priority app is 50, since most of them require sensors created by virtual sensors, it's important that virtual_sensors start first
sensors: sensors:
continuous_condition.is_apple_tv_idle:
time: 5
conditions: media_player.apple_tv == 'paused' or media_player.apple_tv == 'idle' or media_player.apple_tv == 'standby'
binary_sensor.is_apple_tv_on: media_player.apple_tv != 'off'
binary_sensor.someone_home: person.maeva == 'home' or person.pierre == 'home' binary_sensor.someone_home: person.maeva == 'home' or person.pierre == 'home'
binary_sensor.is_dark_outside: sun.sun == 'below_horizon' binary_sensor.is_dark_outside: sun.sun == 'below_horizon'
# living room sensors # living room sensors
value_selector.living_room_ambiance_light: value_selector.living_room_ambiance_light:
values: values:
0: 0:
- not binary_sensor.living_room_occupancy - not binary_sensor.living_room_occupancy
- binary_sensor.is_apple_tv_on and not binary_sensor.is_apple_tv_idle - binary_sensor.projector_on and not binary_sensor.is_vertex2_content_idle
- light.living_room - light.living_room
- sensor.mezzanine_motion_light_level > 10 - sensor.mezzanine_motion_light_level > 10
1: binary_sensor.is_everybody_sleeping 1:
- binary_sensor.is_everybody_sleeping
- binary_sensor.projector_on
2: true 2: true
# home cinema sensors
binary_sensor.projector_on: switch.living_room_projector_plug and sensor.living_room_projector_plug_power > 50
continuous_condition.projector_off_for_a_while:
time: 90
default_value: on
conditions: not binary_sensor.projector_on
# is_apple_tv_on is not reliable as the the apple tv integration sometimes return "paused" when it's actually off,
# you should use is_using_apple_tv instead for most automation
binary_sensor.is_apple_tv_on: media_player.apple_tv != 'off' and media_player.apple_tv != 'idle'
binary_sensor.is_xbox_on: media_player.xbox != 'off'
binary_sensor.is_playstation_on: switch.ps5_136_power
retain_condition.vertex2_has_input_signal:
retain_time: 10
conditions: sensor.hdfury_vertex2_07_output_tx0 or sensor.hdfury_vertex2_07_audio_output
binary_sensor.is_using_apple_tv: binary_sensor.is_apple_tv_on and binary_sensor.vertex2_has_input_signal and select.hdfury_vertex2_07_port_select_tx0 == 0
binary_sensor.is_using_xbox: binary_sensor.is_xbox_on and binary_sensor.vertex2_has_input_signal and select.hdfury_vertex2_07_port_select_tx0 == 1
binary_sensor.is_using_playstation: binary_sensor.is_playstation_on and binary_sensor.vertex2_has_input_signal and select.hdfury_vertex2_07_port_select_tx0 == 2
continuous_condition.is_vertex2_content_idle:
time: 5
conditions:
trigger_conditions:
- binary_sensor.is_using_apple_tv and (media_player.apple_tv == 'paused' or media_player.apple_tv == 'idle' or media_player.apple_tv == 'standby')
# - binary_sensor.is_using_playstation and sensor.ps5_136_activity == 'idle'
# - binary_sensor.is_using_xbox and not binary_sensor.yetanotherpedro_in_game
#p1p sensors #p1p sensors
binary_sensor.p1p_is_printing: binary_sensor.p1p_is_printing:
trigger_conditions: sensor.garage_p1p_etat_de_l_impression != 'idle' and sensor.garage_p1p_etat_de_l_impression != 'finish' and sensor.garage_p1p_etat_de_l_impression != 'idle' and sensor.garage_p1p_etat_de_l_impression != 'offline' trigger_conditions: sensor.garage_p1p_etat_de_l_impression != 'idle' and sensor.garage_p1p_etat_de_l_impression != 'finish' and sensor.garage_p1p_etat_de_l_impression != 'idle' and sensor.garage_p1p_etat_de_l_impression != 'offline'

1033
apps/homealarm.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,13 @@
import appdaemon.plugins.hass.hassapi as hass import appdaemon.plugins.hass.hassapi as hass
import appdaemon.exceptions as ad_exception import appdaemon.exceptions as ad_exception
import ad_toolbox.smartcondition as SmartCondition import ad_toolbox.smartcondition as SmartCondition
import re
import uuid import uuid
from expressionparser import ParsingException from expressionparser import ParsingException
class Notification: class Notification:
ENTITY_PLACEHOLDER_PATTERN = re.compile(r"\{([^{}]+)\}")
def __init__(self,ad_api,config,name): def __init__(self,ad_api,config,name):
self.recipients = dict() self.recipients = dict()
self.config = config self.config = config
@@ -50,14 +53,40 @@ class Notification:
for service in recipient_yaml['services']: for service in recipient_yaml['services']:
clear_notification(service,tag) clear_notification(service,tag)
def render_message_content(self, content):
if not isinstance(content, str):
return content
def replace_entity_state(match):
entity_id = match.group(1).strip()
if entity_id == "":
return match.group(0)
try:
state = self.ad_api.get_state(entity_id)
except Exception as e:
self.log_error(f"Failed to resolve '{entity_id}' in notification content: {e}")
return match.group(0)
if state is None:
self.log_error(f"Unable to resolve '{entity_id}' in notification content")
return match.group(0)
return str(state)
return self.ENTITY_PLACEHOLDER_PATTERN.sub(replace_entity_state, content)
def send_notification(self,recipient): def send_notification(self,recipient):
try: title = self.config['message']['title'] try: title_template = self.config['message']['title']
except KeyError: title = "" except KeyError: title_template = ""
try: message = self.config['message']['content'] try: message_content = self.config['message']['content']
except KeyError: message = "" except KeyError: message_content = ""
try: data = self.config['message']['data'] try: data = self.config['message']['data']
except KeyError: data = dict() except KeyError: data = dict()
title = self.render_message_content(title_template)
message = self.render_message_content(message_content)
#TODO : maybe it's better to not send tag when it's not needed #TODO : maybe it's better to not send tag when it's not needed
if not 'tag' in data: if not 'tag' in data:
data['tag'] = self.uuid data['tag'] = self.uuid

View File

@@ -10,7 +10,7 @@ light_mezzanine_01:
- light.living_room - light.living_room
light_brightness_pct: light_brightness_pct:
15: binary_sensor.is_everybody_sleeping 15: binary_sensor.is_everybody_sleeping or binary_sensor.projector_on
35: binary_sensor.day_interval_night 35: binary_sensor.day_interval_night
100: True 100: True
@@ -26,7 +26,7 @@ light_mezzanine_02:
- light.living_room - light.living_room
light_brightness_pct: light_brightness_pct:
5: binary_sensor.is_everybody_sleeping 5: binary_sensor.is_everybody_sleeping or binary_sensor.projector_on
25: binary_sensor.day_interval_night 25: binary_sensor.day_interval_night
100: True 100: True
@@ -76,4 +76,41 @@ light_living_room:
off_events: off_events:
- turn_off_all_lights - turn_off_all_lights
projection_screen:
module: smartshutter
class: SmartShutter
entity: cover.living_room_projection_screen
close_position: 22
close_conditions: # close mean screen down
# when the projector is turn back on, it can happen that this condition is evaluated before projector_off_for_a_while is updated
# so I need to test both binary_sensor.projector_on and not binary_sensor.projector_off_for_a_while
trigger_conditions: binary_sensor.projector_on or not binary_sensor.projector_off_for_a_while
disable_conditions:
- binary_sensor.day_interval_night and not binary_sensor.projector_on
- binary_sensor.is_everybody_sleeping and not binary_sensor.projector_on
bravia_theatre:
module: smartswitch
class: SmartSwitch
entity: media_player.bravia_theatre
smart_conditions: binary_sensor.vertex2_has_input_signal
w4000i:
module: smartswitch
class: SmartSwitch
entity: media_player.benq_w4000i
smart_conditions: binary_sensor.vertex2_has_input_signal
mezzanine_curtains:
module: smartshutter
class: SmartShutter
entity: cover.salon_mezzanine_curtain
close_conditions:
trigger_conditions: binary_sensor.projector_on or not binary_sensor.projector_off_for_a_while
disable_conditions: self == 'closed' and (binary_sensor.is_dark_outside or binary_sensor.is_anybody_sleeping)

View File

@@ -114,6 +114,8 @@ from ad_toolbox.eventhandler import EventHandler
class SmartLight(SmartSwitch): class SmartLight(SmartSwitch):
def on_initialize_smart_object(self): def on_initialize_smart_object(self):
self.presence_simulation_slots_active = set()
# light_brightness_pct_list : ordered list of (Evaluator, label) pairs # light_brightness_pct_list : ordered list of (Evaluator, label) pairs
# light_brightness_pct : currently active brightness % (str label) # light_brightness_pct : currently active brightness % (str label)
self.light_brightness_pct_list = list() self.light_brightness_pct_list = list()
@@ -196,6 +198,15 @@ class SmartLight(SmartSwitch):
# SmartSwitch overrides # SmartSwitch overrides
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# While simulation is active for this light, ignore auto-off transitions
# from smart_conditions to keep deterministic on-duration windows.
def on_smart_conditions_change(self,prev_result,result):
if len(self.presence_simulation_slots_active) > 0:
if result == SmartCondition.Result.Succeeded and self.is_off():
self.switch_on()
return
super().on_smart_conditions_change(prev_result,result)
# Override: apply light_brightness_pct when turning on, if one is active. # Override: apply light_brightness_pct when turning on, if one is active.
def switch_on(self): def switch_on(self):
if self.light_brightness_pct != None: if self.light_brightness_pct != None:
@@ -225,4 +236,61 @@ class SmartLight(SmartSwitch):
self.log_info("Turning off") self.log_info("Turning off")
self.call_service("light/turn_off", entity_id = self.entity_id) self.call_service("light/turn_off", entity_id = self.entity_id)
# ------------------------------------------------------------------
# Presence simulation API (called by HomeAlarm)
# ------------------------------------------------------------------
def start_presence_simulation(self, source_app=None, slot_name=None, payload=None):
if slot_name:
self.presence_simulation_slots_active.add(slot_name)
if not isinstance(payload, dict):
payload = {}
brightness_pct = payload.get('brightness_pct')
transition = payload.get('transition')
if brightness_pct is None and transition is None:
self.switch_on()
return
service_kwargs = {'entity_id': self.entity_id}
if brightness_pct is not None:
service_kwargs['brightness_pct'] = brightness_pct
if transition is not None:
service_kwargs['transition'] = transition
self.call_service("light/turn_on", **service_kwargs)
def stop_presence_simulation(self, source_app=None, slot_name=None, payload=None):
if slot_name and slot_name in self.presence_simulation_slots_active:
self.presence_simulation_slots_active.remove(slot_name)
if not isinstance(payload, dict):
payload = {}
if payload.get('keep_on', False):
return
transition = payload.get('transition')
if transition is None:
self.switch_off()
else:
self.call_service(
"light/turn_off",
entity_id=self.entity_id,
transition=transition,
)
# If no simulation slot is still active, re-apply current automation
# state so the light immediately converges to the expected result.
if len(self.presence_simulation_slots_active) == 0 and self.smart_conditions_evaluator:
result = self.smart_conditions_evaluator.evaluate(False)
if result == SmartCondition.Result.Succeeded:
if self.is_off():
self.switch_on()
else:
if self.is_on():
self.switch_off()