diff --git a/apps/ad_toolbox b/apps/ad_toolbox index 9433614..56e3f6e 160000 --- a/apps/ad_toolbox +++ b/apps/ad_toolbox @@ -1 +1 @@ -Subproject commit 943361453a3149ce0c144c2c283c64b47e79576d +Subproject commit 56e3f6e9e0d6d19971981608566852cd73244a7b diff --git a/apps/global/heating.yaml b/apps/global/heating.yaml index 10085a2..a8935a8 100644 --- a/apps/global/heating.yaml +++ b/apps/global/heating.yaml @@ -26,15 +26,17 @@ smart_heating_templates_library: templates_library: clim_conditions: + local_expressions: + temp_threshold: "sleeping_in_the_room ? 0.5 : 1" trigger_conditions: - - "not binary_sensor.heatpump_winter_mode and (ext_temperature >= room_temperature or ext_temperature >= target_temperature or cant_open_window) and room_temperature >= ((quick_resume or sleeping_in_the_room)? target_temperature : (target_temperature + 1))" + - "not binary_sensor.heatpump_winter_mode and (ext_temperature >= room_temperature or ext_temperature >= target_temperature or cant_open_window) and room_temperature >= (quick_resume ? target_temperature : (target_temperature + temp_threshold))" - not binary_sensor.heatpump_winter_mode and self and (ext_temperature >= target_temperature or cant_open_window) - binary_sensor.heatpump_winter_mode and room_temperature <= (target_temperature - 1) and ext_temperature <= 10 - binary_sensor.heatpump_winter_mode and self and room_temperature < target_temperature blocking_conditions: - binary_sensor.is_everybody_sleeping and not sleeping_in_the_room and not stay_on_while_sleeping - not room_occupancy - - not binary_sensor.someone_home #and not (binary_sensor.someone_in_town and sensor.day_interval == 'Nuit') + - not binary_sensor.someone_home - window_open desk_heatpump: @@ -67,11 +69,17 @@ desk_heatpump: smart_conditions: callback_delay: 10 # on the morning it can happen that we ask to start and stop the heat pump immediately afterwards which in result in the heat pump ignoring the second command - template_conditions: + template_conditions: + + control_mode_selector: + sensor: input_select.desk_heatpump_override + reset_events: + - good_morning + - good_bye quick_resume: sensor: binary_sensor.desk_heatpump_quick_resume - reset_event: good_morning + reset_events: good_morning #input_buttons: # turn_on: input_button.clim_du_bureau_turn_on @@ -101,7 +109,7 @@ bedroom_heatpump: auto: binary_sensor.heatpump_winter_mode quiet: True - target_temperature: sensor.target_temperature_heatpump + target_temperature: sensor.target_temperature_heatpump + 0.5 hvac_mode: "binary_sensor.heatpump_winter_mode ? 'heat' : 'cool'" fan_mode: sensor.bedroom_heatpump_fan_mode @@ -111,7 +119,13 @@ bedroom_heatpump: quick_resume: sensor: binary_sensor.bedroom_heatpump_quick_resume - reset_event: good_morning + reset_events: good_morning + + control_mode_selector: + sensor: input_select.bedroom_heatpump_override + reset_events: + - good_morning + - good_bye living_room_heatpump: module: smartheatpump @@ -142,6 +156,12 @@ living_room_heatpump: callback_delay: 10 # on the morning it can happen that we ask to start and stop the heat pump immediately afterwards which in result in the heat pump ignoring the second command template_conditions: + control_mode_selector: + sensor: input_select.living_room_heatpump_override + reset_events: + - good_morning + - good_bye + quick_resume: sensor: binary_sensor.living_room_heatpump_quick_resume - reset_event: good_morning \ No newline at end of file + reset_events: good_morning \ No newline at end of file diff --git a/apps/smartheatpump.py b/apps/smartheatpump.py index 32a63e3..0a30782 100644 --- a/apps/smartheatpump.py +++ b/apps/smartheatpump.py @@ -1,19 +1,28 @@ import ad_toolbox.smartcondition as SmartCondition import ad_toolbox.smartvalue as SmartValue +from ad_toolbox.eventhandler import EventHandler from ad_toolbox.smartobject import SmartObject import time class SmartHeatpump(SmartObject): + AUTO_CONTROL_MODE = 'auto' + FORCE_ON_CONTROL_MODE = 'on' + FORCE_OFF_CONTROL_MODE = 'off' + def on_initialize_smart_object(self): super().on_initialize_smart_object() + self.event_handlers = [] + self.control_mode_entity_id = None + self.control_mode = self.AUTO_CONTROL_MODE + if 'quick_resume' in self.args: self.quick_resume_sensor = self.create_entity(self.args['quick_resume']['sensor']) #is_on = 'on' if self.entity.get_state() != 'off' else 'off' #if self.get_state(self.args['quick_resume']['sensor']) != 'on': # if the sensor doesn't exist I wan't to set it as well # self.set_state(self.args['quick_resume']['sensor'],state = is_on) - self.listen_event(lambda event_name, data, kwargs: self.set_state(self.args['quick_resume']['sensor'],state = 'off'),self.args['quick_resume']['reset_event']) + self.register_reset_events(self.args['quick_resume'], "quick_resume") self.target_temperature = SmartValue.Evaluator(self,self.args['target_temperature'], expression_name = 'target_temperature', on_change_cb = self.on_change_target_temperature, constants = self.constants) self.hvac_mode = SmartValue.Evaluator(self,self.args['hvac_mode'], expression_name = 'hvac_mode', on_change_cb = self.on_change_hvac_mode, constants = self.constants) @@ -28,12 +37,129 @@ class SmartHeatpump(SmartObject): self.fan_mode = None self.smart_conditions_evaluator = SmartCondition.Evaluator(self,self.args['smart_conditions'],condition_name = "smart_conditions",on_change_cb = self.on_smart_conditions_change,constants = self.constants, templates_library = self.templates_library) + if 'control_mode_selector' in self.args or 'heatpump_override' in self.args: + self.initialize_control_mode_selector() + if 'input_buttons' in self.args: if 'turn_on' in self.args['input_buttons']: self.listen_state(lambda entity, attribute, old, new, kwargs: self.on_turn_on_button(),self.args['input_buttons']['turn_on']) if 'turn_off' in self.args['input_buttons']: self.listen_state(lambda entity, attribute, old, new, kwargs: self.on_turn_off_button(),self.args['input_buttons']['turn_off']) + def initialize_control_mode_selector(self): + selector_args = self.args['control_mode_selector'] if 'control_mode_selector' in self.args else self.args['heatpump_override'] + + if isinstance(selector_args, str): + self.control_mode_entity_id = selector_args + elif isinstance(selector_args, dict) and 'sensor' in selector_args: + self.control_mode_entity_id = selector_args['sensor'] + else: + self.log_warning("control_mode_selector must be a string entity_id or a dict with a 'sensor' key") + return + + self.create_entity( + self.control_mode_entity_id, + state = self.AUTO_CONTROL_MODE, + attributes = { + 'options': [self.AUTO_CONTROL_MODE, self.FORCE_ON_CONTROL_MODE, self.FORCE_OFF_CONTROL_MODE] + }, + icon = 'mdi:power-settings' + ) + + current_mode = self.get_state(self.control_mode_entity_id) + self.control_mode = self.normalize_control_mode(current_mode) + self.listen_state(self.on_control_mode_change, self.control_mode_entity_id) + self.listen_event( + self.on_control_mode_select_option_service, + 'call_service', + domain='input_select', + service='select_option', + entity_id=self.control_mode_entity_id + ) + self.register_reset_events(selector_args, "control_mode_selector") + self.apply_control_mode(self.control_mode) + + def on_control_mode_select_option_service(self, event_name, data, kwargs): + service_data = data.get('service_data', {}) if isinstance(data, dict) else {} + service_entity_id = service_data.get('entity_id') + + if isinstance(service_entity_id, list): + if self.control_mode_entity_id not in service_entity_id: + return + elif isinstance(service_entity_id, str): + if service_entity_id != self.control_mode_entity_id: + return + else: + return + + mode = service_data.get('option') + + if mode == None or mode == self.control_mode: + return + + self.set_state(self.control_mode_entity_id, state = mode) + self.on_control_mode_change(self.control_mode_entity_id, 'state', self.control_mode, mode, {}) + + def register_reset_events(self, config, config_name): + if not isinstance(config, dict): + return + + reset_events = config.get('reset_events', config.get('reset_event')) + if reset_events is None: + return + + if isinstance(reset_events, str): + reset_events = [reset_events] + elif not isinstance(reset_events, list): + self.log_warning(f"{config_name}.reset_events must be a string or a list of events") + return + + self.event_handlers.append(EventHandler(self, reset_events, self.on_reset_event, config_name)) + + def on_reset_event(self, event_name, event_data, config_name): + if config_name == "quick_resume": + if 'quick_resume' in self.args: + self.set_state(self.args['quick_resume']['sensor'], state='off') + elif config_name == "control_mode_selector": + if self.control_mode_entity_id: + self.set_state(self.control_mode_entity_id, state=self.AUTO_CONTROL_MODE) + + def normalize_control_mode(self, mode): + if mode in [self.AUTO_CONTROL_MODE, self.FORCE_ON_CONTROL_MODE, self.FORCE_OFF_CONTROL_MODE]: + return mode + return self.AUTO_CONTROL_MODE + + def on_control_mode_change(self, entity, attribute, old, new, kwargs): + if old == new: + return + + previous_mode = self.control_mode + self.control_mode = self.normalize_control_mode(new) + + if self.control_mode != new: + self.log_warning(f"Invalid control mode '{new}' on {entity}, falling back to '{self.control_mode}'") + self.set_state(entity, state = self.control_mode) + + self.apply_control_mode(self.control_mode, previous_mode) + + def apply_control_mode(self, mode, previous_mode = None): + if mode == self.AUTO_CONTROL_MODE: + self.log_info("Heatpump control mode is auto, SmartCondition is enabled") + self.smart_conditions_evaluator.activate() + return + + self.smart_conditions_evaluator.deactivate() + + if mode == self.FORCE_ON_CONTROL_MODE: + self.log_info("Heatpump control mode is on, forcing Heatpump on") + self.start_cooling() + elif mode == self.FORCE_OFF_CONTROL_MODE: + self.log_info("Heatpump control mode is off, forcing Heatpump off") + self.stop_cooling() + + if 'quick_resume' in self.args: + self.set_state(self.args['quick_resume']['sensor'],state = 'off') + def on_turn_on_button(self): self.start_cooling() def on_turn_off_button(self): @@ -70,6 +196,10 @@ class SmartHeatpump(SmartObject): def on_smart_conditions_change(self,prev_result,result): + if self.control_mode != self.AUTO_CONTROL_MODE: + self.log_info(f"SmartCondition update ignored because control mode is '{self.control_mode}'") + return + if result == SmartCondition.Result.Succeeded: self.start_cooling() else: