From fc6673e98b89710118b39c790bbf772d12faa1f7 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 31 May 2026 21:49:48 +0200 Subject: [PATCH] Add notifications manager --- apps/apps.yaml | 11 +++ apps/global/notificationsmanagers.yaml | 32 +++++++++ apps/global/sleep.yaml | 13 ++-- apps/notificationsmanager.py | 94 ++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 apps/global/notificationsmanagers.yaml create mode 100644 apps/notificationsmanager.py diff --git a/apps/apps.yaml b/apps/apps.yaml index 26927fb..f5722f1 100644 --- a/apps/apps.yaml +++ b/apps/apps.yaml @@ -60,3 +60,14 @@ informations_collector: # - group.dumb_lights # output_sensor: sensor.ad_number_of_lights_on +haw_switch: + module: smartswitch + class: SmartSwitch + + entity: input_boolean.haw + + auto_switch_off_after: 28800 # 8h + + off_events: + - WAKEUP + diff --git a/apps/global/notificationsmanagers.yaml b/apps/global/notificationsmanagers.yaml new file mode 100644 index 0000000..9b97aee --- /dev/null +++ b/apps/global/notificationsmanagers.yaml @@ -0,0 +1,32 @@ +notifications_manager: + 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: + back_home_notification: + recipients: + all: + services: + - notify/mobile_app_ipad_pro_11_de_pierre + conditions: + trigger_conditions: + - person.maeva == 'home' + - binary_sensor.is_bedroom_sleeping and (binary_sensor.hallway_motion or binary_sensor.corridor_motion or binary_sensor.bedroom_motion_bed_pierre) + blocking_conditions: not input_boolean.haw or not binary_sensor.someone_home + + message: + content: Bisous + timeout: 600 + + leave_home_notification: + recipients: + all: + services: + - notify/mobile_app_macbook_m3_pro_de_pierre + conditions: input_boolean.haw and person.maeva != 'home' and binary_sensor.someone_home + + message: + content: Good Bye + timeout: 600 diff --git a/apps/global/sleep.yaml b/apps/global/sleep.yaml index 6e2139e..b3699ae 100644 --- a/apps/global/sleep.yaml +++ b/apps/global/sleep.yaml @@ -10,12 +10,9 @@ sleep_sensors: binary_sensor.is_guestroom_sleeping: input_boolean.guestroom_sleeping binary_sensor.is_everybody_sleeping: - - binary_sensor.is_bedroom_sleeping - #- binary_sensor.is_living_room_sleeping and ... - #- binary_sensor.is_desk_sleeping and ... - #- binary_sensor.is_guestroom_sleeping and ... + - (binary_sensor.is_bedroom_sleeping or not input_boolean.bedroom_occupied_tonight) and (binary_sensor.is_living_room_sleeping or not input_boolean.living_room_occupied_tonight) and (binary_sensor.is_desk_sleeping or not input_boolean.desk_occupied_tonight) and (binary_sensor.is_guestroom_sleeping or not input_boolean.guestroom_occupied_tonight) binary_sensor.is_anybody_sleeping: - - binary_sensor.is_bedroom_sleeping - - binary_sensor.is_living_room_sleeping - - binary_sensor.is_desk_sleeping - - binary_sensor.is_guestroom_sleeping \ No newline at end of file + - binary_sensor.is_bedroom_sleeping and input_boolean.bedroom_occupied_tonight + - binary_sensor.is_living_room_sleeping and input_boolean.living_room_occupied_tonight + - binary_sensor.is_desk_sleeping and input_boolean.desk_occupied_tonight + - binary_sensor.is_guestroom_sleeping and input_boolean.guestroom_occupied_tonight \ No newline at end of file diff --git a/apps/notificationsmanager.py b/apps/notificationsmanager.py new file mode 100644 index 0000000..ac5b82c --- /dev/null +++ b/apps/notificationsmanager.py @@ -0,0 +1,94 @@ +import appdaemon.plugins.hass.hassapi as hass +import appdaemon.exceptions as ad_exception +import ad_toolbox.smartcondition as SmartCondition +import uuid +from expressionparser import ParsingException + +class Notification: + def __init__(self,ad_api,config,name): + self.recipients = dict() + self.config = config + self.ad_api = ad_api + try: self.auto_clear = config['message']['auto_clear'] + except KeyError: self.auto_clear = False + self.uuid = str(uuid.uuid4()) + for recipient in config["recipients"]: + try: self.recipients[recipient] = SmartCondition.Evaluator(self.ad_api,self.config["recipients"][recipient]['conditions'],condition_name = name,on_succeed_cb = self.on_condition_succeed, on_fail_cb = self.on_conditon_fail,pass_condition_name_to_cb = recipient, trigger_callback_on_activation = False) + except ParsingException as e: + self.log_error(str(e)) + continue + + def on_conditon_fail(self, recipient): + if self.auto_clear: + try: tag = self.config['message']['data']['tag'] + except KeyError: tag = self.uuid + + self.clear_notification(recipient,tag) + + def on_condition_succeed(self,recipient): + self.send_notification(recipient) + + def on_timeout_expired(self,kwargs): + tag = kwargs['tag'] + recipient = kwargs['recipient'] + + self.clear_notification(recipient,tag) + + def clear_notification(self,recipient,tag): + message = 'clear_notification' + data = { 'tag' : tag } + + def clear_notification(service,tag): + self.ad_api.log(f"Clear notification {data} from {service}") + try: self.ad_api.call_service(service,message = message, data = data) + except ad_exception.ServiceException: self.log_error(f"Invalid service: {service}") + + recipient_yaml = self.config["recipients"][recipient] + if isinstance(recipient_yaml['services'], str): + clear_notification(recipient_yaml['services'],tag) + else: + for service in recipient_yaml['services']: + clear_notification(service,tag) + + def send_notification(self,recipient): + try: title = self.config['message']['title'] + except KeyError: title = "" + try: message = self.config['message']['content'] + except KeyError: message = "" + try: data = self.config['message']['data'] + except KeyError: data = dict() + + #TODO : maybe it's better to not send tag when it's not needed + if not 'tag' in data: + data['tag'] = self.uuid + + if 'timeout' in self.config['message']: + timeout = self.config['message']['timeout'] + self.ad_api.run_in(self.on_timeout_expired,timeout,tag = data['tag'],recipient = recipient) + + def send_notification(service): + self.ad_api.log(f"Sending notification '{message}' to {service}{f' with data {data}' if data else ''}") + try: self.ad_api.call_service(service,title = title,message = message, data = data) + except ad_exception.ServiceException: self.log_error(f"Invalid service: {service}") + + recipient_yaml = self.config["recipients"][recipient] + if isinstance(recipient_yaml['services'], str): + send_notification(recipient_yaml['services']) + else: + for service in recipient_yaml['services']: + send_notification(service) + + def log_error(self,message,stop_app = False): + self.ad_api.log(message,level = "ERROR", log = "main_log") + self.ad_api.log(message,level = "ERROR", log = "error_log") + if stop_app: + self.ad_api.stop_app(self.ad_api.name) + +class NotificationsManager(hass.Hass): + + def initialize(self): + self.notifications = dict() + + if "notifications" in self.args: + for notification in self.args["notifications"]: + self.notifications[notification] = Notification(self,self.args["notifications"][notification],notification) \ No newline at end of file