From 89984b3dfcdb98570ea7d6c5d291720a88bbdf49 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sun, 8 Jan 2023 12:29:50 +0100 Subject: [PATCH] Add presence management Fix many bugs on states actualisation Add extra attributes with all internal states --- .devcontainer/configuration.yaml | 13 +- .../versatile_thermostat/climate.py | 403 ++++++++++++++---- .../versatile_thermostat/config_flow.py | 87 ++-- .../versatile_thermostat/const.py | 6 + .../versatile_thermostat/prop_algorithm.py | 4 +- .../versatile_thermostat/strings.json | 26 +- .../versatile_thermostat/translations/en.json | 26 +- .../versatile_thermostat/translations/fr.json | 18 + 8 files changed, 465 insertions(+), 118 deletions(-) diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml index e58d4596..137b98c7 100644 --- a/.devcontainer/configuration.yaml +++ b/.devcontainer/configuration.yaml @@ -13,13 +13,13 @@ input_number: name: Temperature min: 0 max: 35 - step: .5 + step: .1 icon: mdi:thermometer fake_external_temperature_sensor1: name: Ext Temperature min: -10 max: 35 - step: .5 + step: .1 icon: mdi:home-thermometer fake_current_power: name: Current power @@ -45,9 +45,16 @@ input_boolean: name: Heater 1 (Linear) icon: mdi:radiator fake_heater_switch2: - name: Heater (TPI) + name: Heater (TPI with presence preset) + icon: mdi:radiator + fake_heater_switch3: + name: Heater (TPI with offset) icon: mdi:radiator # input_boolean to simulate the motion sensor entity. Only for development environment. fake_motion_sensor1: name: Motion Sensor 1 icon: mdi:run + # input_boolean to simulate the presence sensor entity. Only for development environment. + fake_presence_sensor1: + name: Presence Sensor 1 + icon: mdi:home diff --git a/custom_components/versatile_thermostat/climate.py b/custom_components/versatile_thermostat/climate.py index 37fcf299..7f134a98 100644 --- a/custom_components/versatile_thermostat/climate.py +++ b/custom_components/versatile_thermostat/climate.py @@ -1,7 +1,7 @@ import math import logging -from datetime import timedelta +from datetime import timedelta, datetime from typing import Any, Mapping from homeassistant.core import ( @@ -84,6 +84,9 @@ CONF_PROP_BIAS, CONF_TPI_COEF_C, CONF_TPI_COEF_T, + CONF_PRESENCE_SENSOR, + CONF_NO_PRESENCE_PRESET, + CONF_NO_PRESENCE_TEMP_OFFSET, SUPPORT_FLAGS, PRESET_POWER, PROPORTIONAL_FUNCTION_TPI, @@ -123,6 +126,9 @@ async def async_setup_entry( device_power = entry.data.get(CONF_DEVICE_POWER) tpi_coefc = entry.data.get(CONF_TPI_COEF_C) tpi_coeft = entry.data.get(CONF_TPI_COEF_T) + presence_sensor_entity_id = entry.data.get(CONF_PRESENCE_SENSOR) + no_presence_preset = entry.data.get(CONF_NO_PRESENCE_PRESET) + no_presence_offset = entry.data.get(CONF_NO_PRESENCE_TEMP_OFFSET) presets = {} for (key, value) in CONF_PRESETS.items(): @@ -156,6 +162,9 @@ async def async_setup_entry( device_power, tpi_coefc, tpi_coeft, + presence_sensor_entity_id, + no_presence_preset, + no_presence_offset, ) ], True, @@ -193,6 +202,9 @@ def __init__( device_power, tpi_coefc, tpi_coeft, + presence_sensor_entity_id, + no_presence_preset, + no_presence_offset, ) -> None: """Initialize the thermostat.""" @@ -220,6 +232,20 @@ def __init__( self._no_motion_preset = no_motion_preset self._tpi_coefc = tpi_coefc self._tpi_coeft = tpi_coeft + self._presence_sensor_entity_id = presence_sensor_entity_id + self._no_presence_preset = no_presence_preset + self._no_presence_offset = no_presence_offset + + self._presence_on = self._presence_sensor_entity_id and ( + self._no_presence_preset is not None or self._no_presence_offset is not None + ) + if self._presence_on: + if self._no_presence_preset is not None: + self._no_presence_offset = 0 + else: + self._no_presence_preset = None + self._no_presence_offset = 0 + _LOGGER.info("%s - Presence management is not fully configured.", self) # TODO if self.ac_mode: # self.hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF] @@ -259,6 +285,8 @@ def __init__( self._pmax_on = True self._current_power = 0 self._current_power_max = 0 + else: + _LOGGER.info("%s - Power management is not fully configured.", self) # will be restored if possible self._target_temp = None @@ -293,6 +321,14 @@ def __init__( self._window_call_cancel = None self._motion_call_cancel = None + self._should_relaunch_control_heating = False + + # Memory synthesis state + self._motion_state = None + self._window_state = None + self._overpowering_state = None + self._presence_state = None + _LOGGER.debug( "%s - Creation of a new VersatileThermostat entity: unique_id=%s heater_entity_id=%s", self, @@ -367,13 +403,6 @@ def current_temperature(self): """Return the sensor temperature.""" return self._cur_temp - # @property - # def extra_state_attributes(self) -> Mapping[str, Any] | None: - # _LOGGER.debug( - # "Calling extra_state_attributes: %s", self._hass.custom_attributes - # ) - # return self._hass.custom_attributes - async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" _LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode) @@ -415,20 +444,20 @@ async def _async_set_preset_mode_internal(self, preset_mode): self._target_temp = self._saved_target_temp elif preset_mode == PRESET_ACTIVITY: self._attr_preset_mode = PRESET_ACTIVITY - self._target_temp = self._presets[self._no_motion_preset] + self._update_motion_temp() else: if self._attr_preset_mode == PRESET_NONE: self._saved_target_temp = self._target_temp self._attr_preset_mode = preset_mode self._target_temp = self._presets[preset_mode] - if preset_mode != PRESET_POWER: + # Don't saved preset_mode if we are in POWER mode or in Away mode and presence detection is on + if preset_mode != PRESET_POWER and ( + not self._presence_on or preset_mode != self._no_presence_preset + ): self._saved_preset_mode = self._attr_preset_mode - self.async_write_ha_state() - self._prop_algorithm.calculate( - self._target_temp, self._cur_temp, self._cur_ext_temp - ) + self.recalculate() async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" @@ -461,10 +490,7 @@ async def async_set_temperature(self, **kwargs): return self._target_temp = temperature self._attr_preset_mode = PRESET_NONE - self._prop_algorithm.calculate( - self._target_temp, self._cur_temp, self._cur_ext_temp - ) - self.async_write_ha_state() + self.recalculate() @callback async def entry_update_listener( @@ -520,15 +546,6 @@ async def async_added_to_hass(self): ) ) - if self._cycle_min: - self.async_on_remove( - async_track_time_interval( - self.hass, - self._async_control_heating, - interval=timedelta(minutes=self._cycle_min), - ) - ) - if self._power_sensor_entity_id: self.async_on_remove( async_track_state_change_event( @@ -547,8 +564,27 @@ async def async_added_to_hass(self): ) ) + if self._presence_on: + self.async_on_remove( + async_track_state_change_event( + self.hass, + [self._presence_sensor_entity_id], + self._async_presence_changed, + ) + ) + await self.async_startup() + # starts the cycle + if self._cycle_min: + self.async_on_remove( + async_track_time_interval( + self.hass, + self._async_control_heating, + interval=timedelta(minutes=self._cycle_min), + ) + ) + async def async_startup(self): """Triggered on startup, used to get old state and set internal states accordingly""" _LOGGER.debug("%s - Calling async_startup", self) @@ -584,6 +620,16 @@ def _async_startup_internal(*_): float(ext_temperature_state.state), ) self._async_update_ext_temp(ext_temperature_state) + else: + _LOGGER.debug( + "%s - external temperature sensor have NOT been retrieved cause unknown or unavailable", + self, + ) + else: + _LOGGER.debug( + "%s - external temperature sensor have NOT been retrieved cause no external sensor", + self, + ) switch_state = self.hass.states.get(self._heater_entity_id) if switch_state and switch_state.state not in ( @@ -623,6 +669,53 @@ def _async_startup_internal(*_): ) need_write_state = True + # try to acquire window entity state + if self._window_sensor_entity_id: + window_state = self.hass.states.get(self._window_sensor_entity_id) + if window_state and window_state.state not in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + self._window_state = window_state.state + _LOGGER.debug( + "%s - Window state have been retrieved: %s", + self, + self._window_state, + ) + need_write_state = True + + # try to acquire motion entity state + if self._motion_sensor_entity_id: + motion_state = self.hass.states.get(self._motion_sensor_entity_id) + if motion_state and motion_state.state not in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + self._motion_state = motion_state.state + _LOGGER.debug( + "%s - Motion state have been retrieved: %s", + self, + self._motion_state, + ) + # recalculate the right target_temp in activity mode + self._update_motion_temp() + need_write_state = True + + if self._presence_on: + # try to acquire presence entity state + presence_state = self.hass.states.get(self._presence_sensor_entity_id) + if presence_state and presence_state.state not in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + self._update_presence(presence_state.state) + _LOGGER.debug( + "%s - Presence have been retrieved: %s", + self, + presence_state.state, + ) + need_write_state = True + if need_write_state: self.async_write_ha_state() self._prop_algorithm.calculate( @@ -630,6 +723,8 @@ def _async_startup_internal(*_): ) self.hass.create_task(self._async_control_heating()) + await self.get_my_previous_state() + if self.hass.state == CoreState.running: _async_startup_internal() else: @@ -637,8 +732,6 @@ def _async_startup_internal(*_): EVENT_HOMEASSISTANT_START, _async_startup_internal ) - await self.get_my_previous_state() - async def get_my_previous_state(self): """Try to get my previou state""" # Check If we have an old state @@ -670,9 +763,10 @@ async def get_my_previous_state(self): if not self._hvac_mode and old_state.state: self._hvac_mode = old_state.state - self._prop_algorithm.calculate( - self._target_temp, self._cur_temp, self._cur_ext_temp - ) + # is done in startup above + # self._prop_algorithm.calculate( + # self._target_temp, self._cur_temp, self._cur_ext_temp + # ) else: # No previous state, try and restore defaults @@ -712,10 +806,7 @@ async def _async_temperature_changed(self, event): return self._async_update_temp(new_state) - self._prop_algorithm.calculate( - self._target_temp, self._cur_temp, self._cur_ext_temp - ) - self.async_write_ha_state() + self.recalculate() async def _async_ext_temperature_changed(self, event): """Handle external temperature changes.""" @@ -729,10 +820,7 @@ async def _async_ext_temperature_changed(self, event): return self._async_update_ext_temp(new_state) - self._prop_algorithm.calculate( - self._target_temp, self._cur_temp, self._cur_ext_temp - ) - self.async_write_ha_state() + self.recalculate() @callback async def _async_windows_changed(self, event): @@ -771,14 +859,15 @@ async def try_window_condition(_): if not self._saved_hvac_mode: self._saved_hvac_mode = self._hvac_mode - if new_state.state == STATE_OFF: + self._window_state = new_state.state + if self._window_state == STATE_OFF: _LOGGER.info( "%s - Window is closed. Restoring hvac_mode '%s'", self, self._saved_hvac_mode, ) await self.async_set_hvac_mode(self._saved_hvac_mode) - elif new_state.state == STATE_ON: + elif self._window_state == STATE_ON: _LOGGER.info( "%s - Window is open. Set hvac_mode to '%s'", self, HVAC_MODE_OFF ) @@ -803,8 +892,7 @@ async def _async_motion_changed(self, event): self._attr_preset_mode, PRESET_ACTIVITY, ) - if self._attr_preset_mode != PRESET_ACTIVITY: - return + if new_state is None or new_state.state not in (STATE_OFF, STATE_ON): return @@ -827,21 +915,21 @@ async def try_motion_condition(_): return _LOGGER.debug("%s - Motion delay condition is satisfied", self) - new_preset = ( - self._motion_preset - if new_state.state == STATE_ON - else self._no_motion_preset - ) - _LOGGER.info( - "%s - Motion condition have changes. New preset temp will be %s", - self, - new_preset, - ) - self._target_temp = self._presets[new_preset] - self._prop_algorithm.calculate( - self._target_temp, self._cur_temp, self._cur_ext_temp - ) - self.async_write_ha_state() + self._motion_state = new_state.state + if self._attr_preset_mode == PRESET_ACTIVITY: + new_preset = ( + self._motion_preset + if self._motion_state == STATE_ON + else self._no_motion_preset + ) + _LOGGER.info( + "%s - Motion condition have changes. New preset temp will be %s", + self, + new_preset, + ) + # We do not change the preset which is kept to ACTIVITY but only the target_temperature + self._target_temp = self._presets[new_preset] + self.recalculate() if self._motion_call_cancel: self._motion_call_cancel() @@ -936,6 +1024,111 @@ async def _async_max_power_changed(self, event): except ValueError as ex: _LOGGER.error("Unable to update current_power from sensor: %s", ex) + @callback + async def _async_presence_changed(self, event): + """Handle presence changes.""" + new_state = event.data.get("new_state") + _LOGGER.info( + "%s - Presence changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s", + self, + new_state, + self._attr_preset_mode, + PRESET_ACTIVITY, + ) + if new_state is None: + return + + self._update_presence(new_state.state) + + def _update_presence(self, new_state): + _LOGGER.debug("%s - Updating presence. New state is %s", self, new_state) + self._presence_state = new_state + if self._attr_preset_mode == PRESET_POWER or self._presence_on is False: + return + if new_state is None or new_state not in (STATE_OFF, STATE_ON): + return + + # Change temperature or preset + if self._no_presence_preset: + _LOGGER.debug("%s - presence change in preset mode", self) + new_preset = None + no_presence_preset = self._no_presence_preset + if new_state == STATE_OFF: + new_preset = no_presence_preset + self._saved_preset_mode = self._attr_preset_mode + _LOGGER.info( + "%s - No one is at home. Set to preset %s (saved_preset is %s)", + self, + new_preset, + self._saved_preset_mode, + ) + elif self._attr_preset_mode == no_presence_preset: + new_preset = self._saved_preset_mode + _LOGGER.info( + "%s - Someone is back home. Restoring preset to %s", + self, + new_preset, + ) + else: + _LOGGER.debug( + "%s - presence change ignored (not in %s preset or not ON)", + self, + no_presence_preset, + ) + if new_preset: + self.hass.create_task(self.async_set_preset_mode(new_preset)) + else: + new_temp = None + if new_state == STATE_OFF: + self._saved_target_temp = self._target_temp + _LOGGER.info( + "%s - No one is at home. Apply offset to temperature %.2f (saved_target_temp is %.2f)", + self, + self._no_presence_offset, + self._saved_target_temp, + ) + new_temp = self._target_temp + self._no_presence_offset + else: + new_temp = self._saved_target_temp + _LOGGER.info( + "%s - Someone is back home. Restoring temperature to %.2f", + self, + self._saved_target_temp, + ) + if new_temp is not None: + _LOGGER.debug( + "%s - presence change in temperature mode new_temp will be: %.2f", + self, + new_temp, + ) + self._target_temp = new_temp + self.recalculate() + + def _update_motion_temp(self): + """Update the temperature considering the ACTIVITY preset and current motion state""" + _LOGGER.debug( + "%s - Calling _update_motion_temp preset_mode=%s, motion_state=%s", + self, + self._attr_preset_mode, + self._motion_state, + ) + if ( + self._motion_sensor_entity_id is None + or self._attr_preset_mode != PRESET_ACTIVITY + ): + return + + self._target_temp = self._presets[ + self._motion_preset + if self._motion_state == STATE_ON + else self._no_motion_preset + ] + _LOGGER.debug( + "%s - regarding motion, target_temp have been set to %.2f", + self, + self._target_temp, + ) + async def _async_heater_turn_on(self): """Turn heater toggleable device on.""" data = {ATTR_ENTITY_ID: self._heater_entity_id} @@ -965,19 +1158,19 @@ async def check_overpowering(self) -> bool: self._current_power_max, self._device_power, ) - overpowering: bool = ( + self._overpowering_state = ( self._current_power + self._device_power >= self._current_power_max ) - if overpowering: + if self._overpowering_state: _LOGGER.warning( "%s - overpowering is detected. Heater preset will be set to 'power'", self, ) await self._async_set_preset_mode_internal(PRESET_POWER) - return overpowering + return self._overpowering_state # Check if we need to remove the POWER preset - if self._attr_preset_mode == PRESET_POWER and not overpowering: + if self._attr_preset_mode == PRESET_POWER and not self._overpowering_state: _LOGGER.warning( "%s - end of overpowering is detected. Heater preset will be restored to '%s'", self, @@ -1012,12 +1205,16 @@ async def _async_control_heating(self, time=None): # Cancel eventual previous cycle if any if self._async_cancel_cycle is not None: - _LOGGER.debug("Cancelling the previous cycle that was running") - self._async_cancel_cycle() - self._async_cancel_cycle = None + _LOGGER.debug( + "%s - A previous cycle is alredy running -> waits for its end", self + ) + self._should_relaunch_control_heating = True + return + # await self._async_cancel_cycle() + # self._async_cancel_cycle = None # Don't turn off if we will turn on just after - if on_time_sec <= 0: - await self._async_heater_turn_off() + # if on_time_sec <= 0: + # await self._async_heater_turn_off() if self._hvac_mode == HVAC_MODE_HEAT and on_time_sec > 0: _LOGGER.info( @@ -1029,19 +1226,23 @@ async def _async_control_heating(self, time=None): await self._async_heater_turn_on() - self.update_custom_attributes() - async def _turn_off(_): - _LOGGER.info( - "%s - stop heating for %d min %d sec", - self, - off_time_sec // 60, - off_time_sec % 60, - ) - await self._async_heater_turn_off() self._async_cancel_cycle() self._async_cancel_cycle = None - self.update_custom_attributes() + if self._should_relaunch_control_heating: + _LOGGER.debug("Don't stop cause a cycle have to be relaunch") + self._should_relaunch_control_heating = False + await self._async_control_heating() + return + else: + _LOGGER.info( + "%s - stop heating for %d min %d sec", + self, + off_time_sec // 60, + off_time_sec % 60, + ) + await self._async_heater_turn_off() + self.update_custom_attributes() # Program turn off self._async_cancel_cycle = async_call_later( @@ -1050,6 +1251,31 @@ async def _turn_off(_): _turn_off, ) + elif self._is_device_active: + _LOGGER.info( + "%s - stop heating (2) for %d min %d sec", + self, + off_time_sec // 60, + off_time_sec % 60, + ) + await self._async_heater_turn_off() + + else: + _LOGGER.debug("%s - nothing to do", self) + + self.update_custom_attributes() + + def recalculate(self): + """A utility function to force the calculation of a the algo and + update the custom attributes and write the state + """ + _LOGGER.debug("%s - recalculate all", self) + self._prop_algorithm.calculate( + self._target_temp, self._cur_temp, self._cur_ext_temp + ) + self.update_custom_attributes() + self.async_write_ha_state() + def update_custom_attributes(self): """Update the custom extra attributes for the entity""" @@ -1057,7 +1283,7 @@ def update_custom_attributes(self): "away_temp": self._presets[PRESET_AWAY], "eco_temp": self._presets[PRESET_ECO], "boost_temp": self._presets[PRESET_BOOST], - "comfort_temp": self._presets[PRESET_BOOST], + "comfort_temp": self._presets[PRESET_COMFORT], "power_temp": self._presets[PRESET_POWER], "on_percent": self._prop_algorithm.on_percent, "on_time_sec": self._prop_algorithm.on_time_sec, @@ -1070,10 +1296,25 @@ def update_custom_attributes(self): "function": self._proportional_function, "tpi_coefc": self._tpi_coefc, "tpi_coeft": self._tpi_coeft, - "is_device_active": self._is_device_active, + "saved_preset_mode": self._saved_preset_mode, + "saved_target_temp": self._saved_target_temp, + "no_presence_preset": self._no_presence_preset + if self._presence_on + else None, + "no_presence_offset": self._no_presence_offset + if self._presence_on + else None, + "window_state": self._window_state, + "motion_state": self._motion_state, + "overpowering_state": self._overpowering_state, + "presence_state": self._presence_state, + "last_update_datetime": datetime.now().isoformat(), } + self.async_write_ha_state() _LOGGER.debug( - "Calling update_custom_attributes: %s", self._attr_extra_state_attributes + "%s - Calling update_custom_attributes: %s", + self, + self._attr_extra_state_attributes, ) @callback diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index 8b7c119e..13b2c06f 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -39,6 +39,9 @@ CONF_PROP_BIAS, CONF_TPI_COEF_T, CONF_TPI_COEF_C, + CONF_PRESENCE_SENSOR, + CONF_NO_PRESENCE_PRESET, + CONF_NO_PRESENCE_TEMP_OFFSET, PROPORTIONAL_FUNCTION_ATAN, PROPORTIONAL_FUNCTION_LINEAR, PROPORTIONAL_FUNCTION_TPI, @@ -63,23 +66,23 @@ ), } ) -USER_DATA_CONF = [ - CONF_NAME, - CONF_HEATER, - CONF_TEMP_SENSOR, - CONF_EXTERNAL_TEMP_SENSOR, - CONF_CYCLE_MIN, - CONF_PROP_FUNCTION, -] +# USER_DATA_CONF = [ +# CONF_NAME, +# CONF_HEATER, +# CONF_TEMP_SENSOR, +# CONF_EXTERNAL_TEMP_SENSOR, +# CONF_CYCLE_MIN, +# CONF_PROP_FUNCTION, +# ] STEP_P_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_PROP_BIAS, default=0.25): vol.Coerce(float), } ) -P_DATA_CONF = [ - CONF_PROP_BIAS, -] +# P_DATA_CONF = [ +# CONF_PROP_BIAS, +# ] STEP_TPI_DATA_SCHEMA = vol.Schema( { @@ -88,16 +91,16 @@ vol.Required(CONF_TPI_COEF_T, default=0.01): vol.Coerce(float), } ) -TPI_DATA_CONF = [ - CONF_EXTERNAL_TEMP_SENSOR, - CONF_TPI_COEF_C, - CONF_TPI_COEF_T, -] +# TPI_DATA_CONF = [ +# CONF_EXTERNAL_TEMP_SENSOR, +# CONF_TPI_COEF_C, +# CONF_TPI_COEF_T, +# ] STEP_PRESETS_DATA_SCHEMA = vol.Schema( {vol.Optional(v, default=17): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()} ) -PRESETS_DATA_CONF = [v for (_, v) in CONF_PRESETS.items()] +# PRESETS_DATA_CONF = [v for (_, v) in CONF_PRESETS.items()] STEP_WINDOW_DATA_SCHEMA = vol.Schema( { @@ -105,7 +108,7 @@ vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int, } ) -WINDOW_DATA_CONF = [CONF_WINDOW_SENSOR, CONF_WINDOW_DELAY] +# WINDOW_DATA_CONF = [CONF_WINDOW_SENSOR, CONF_WINDOW_DELAY] STEP_MOTION_DATA_SCHEMA = vol.Schema( { @@ -119,12 +122,12 @@ ), } ) -MOTION_DATA_CONF = [ - CONF_MOTION_SENSOR, - CONF_MOTION_DELAY, - CONF_MOTION_PRESET, - CONF_NO_MOTION_PRESET, -] +# MOTION_DATA_CONF = [ +# CONF_MOTION_SENSOR, +# CONF_MOTION_DELAY, +# CONF_MOTION_PRESET, +# CONF_NO_MOTION_PRESET, +# ] STEP_POWER_DATA_SCHEMA = vol.Schema( { @@ -133,7 +136,16 @@ vol.Optional(CONF_DEVICE_POWER): vol.Coerce(float), } ) -POWER_DATA_CONF = [CONF_POWER_SENSOR, CONF_MAX_POWER_SENSOR, CONF_DEVICE_POWER] +# POWER_DATA_CONF = [CONF_POWER_SENSOR, CONF_MAX_POWER_SENSOR, CONF_DEVICE_POWER] + +STEP_PRESENCE_DATA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_PRESENCE_SENSOR): cv.string, + vol.Optional(CONF_NO_PRESENCE_PRESET): vol.In(CONF_PRESETS_SELECTIONABLE), + vol.Optional(CONF_NO_PRESENCE_TEMP_OFFSET): vol.Coerce(float), + } +) +# PRESENCE_DATA_CONF = [CONF_PRESENCE_SENSOR, CONF_NO_PRESENCE_PRESET, CONF_NO_PRESENCE_TEMP_OFFSET] def schema_defaults(schema, **defaults): @@ -182,6 +194,7 @@ async def validate_input(self, data: dict) -> dict[str]: CONF_MOTION_SENSOR, CONF_POWER_SENSOR, CONF_MAX_POWER_SENSOR, + CONF_PRESENCE_SENSOR, ]: d = data.get(conf, None) # pylint: disable=invalid-name if d is not None and self.hass.states.get(d) is None: @@ -282,6 +295,17 @@ async def async_step_power(self, user_input: dict | None = None) -> FlowResult: "power", STEP_POWER_DATA_SCHEMA, user_input, + self.async_step_presence, + ) + + async def async_step_presence(self, user_input: dict | None = None) -> FlowResult: + """Handle the presence management flow steps""" + _LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input) + + return await self.generic_step( + "presence", + STEP_PRESENCE_DATA_SCHEMA, + user_input, self.async_finalize, # pylint: disable=no-member ) @@ -412,6 +436,19 @@ async def async_step_power(self, user_input: dict | None = None) -> FlowResult: "power", STEP_POWER_DATA_SCHEMA, user_input, + self.async_step_presence, # pylint: disable=no-member + ) + + async def async_step_presence(self, user_input: dict | None = None) -> FlowResult: + """Handle the presence management flow steps""" + _LOGGER.debug( + "Into OptionsFlowHandler.async_step_presence user_input=%s", user_input + ) + + return await self.generic_step( + "presence", + STEP_PRESENCE_DATA_SCHEMA, + user_input, self.async_finalize, # pylint: disable=no-member ) diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index c8b5ec75..51417b67 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -37,6 +37,9 @@ CONF_NO_MOTION_PRESET = "no_motion_preset" CONF_TPI_COEF_C = "tpi_coefc" CONF_TPI_COEF_T = "tpi_coeft" +CONF_PRESENCE_SENSOR = "presence_sensor_entity_id" +CONF_NO_PRESENCE_PRESET = "no_presence_preset" +CONF_NO_PRESENCE_TEMP_OFFSET = "no_presence_temp_offset" CONF_PRESETS = { p: f"{p}_temp" @@ -73,6 +76,9 @@ CONF_PROP_BIAS, CONF_TPI_COEF_C, CONF_TPI_COEF_T, + CONF_PRESENCE_SENSOR, + CONF_NO_PRESENCE_PRESET, + CONF_NO_PRESENCE_TEMP_OFFSET, ] + CONF_PRESETS_VALUES, ) diff --git a/custom_components/versatile_thermostat/prop_algorithm.py b/custom_components/versatile_thermostat/prop_algorithm.py index 776b1def..5e8fc473 100644 --- a/custom_components/versatile_thermostat/prop_algorithm.py +++ b/custom_components/versatile_thermostat/prop_algorithm.py @@ -20,9 +20,11 @@ def __init__( ): """Initialisation of the Proportional Algorithm""" _LOGGER.debug( - "Creation new PropAlgorithm function_type: %s, bias: %f, cycle_min:%d", + "Creation new PropAlgorithm function_type: %s, bias: %s, tpi_coefc: %s, tpi_coeft: %s, cycle_min:%d", function_type, bias, + tpi_coefc, + tpi_coeft, cycle_min, ) # TODO test function_type, bias, cycle_min diff --git a/custom_components/versatile_thermostat/strings.json b/custom_components/versatile_thermostat/strings.json index 727f33a6..e497e9ad 100644 --- a/custom_components/versatile_thermostat/strings.json +++ b/custom_components/versatile_thermostat/strings.json @@ -50,7 +50,7 @@ } }, "motion": { - "title": "Motion sensor management", + "title": "Motion management", "description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", "data": { "motion_sensor_entity_id": "Motion sensor entity id", @@ -61,12 +61,21 @@ }, "power": { "title": "Power management", - "description": "Power management attributes.\nGives the power and max power sensor pf your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", + "description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", "data": { "power_sensor_entity_id": "Power sensor entity id", "max_power_sensor_entity_id": "Max power sensor entity id", "device_power": "Device power (kW)" } + }, + "presence": { + "title": "Presence management", + "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.", + "data": { + "presence_sensor_entity_id": "Presence sensor entity id (true is present)", + "no_presence_preset": "Preset to use when no one is present", + "no_presence_offset": "Temperature offset to apply to current temperature is no one is present" + } } }, "error": { @@ -127,7 +136,7 @@ } }, "motion": { - "title": "Motion sensor management", + "title": "Motion management", "description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", "data": { "motion_sensor_entity_id": "Motion sensor entity id", @@ -138,12 +147,21 @@ }, "power": { "title": "Power management", - "description": "Power management attributes.\nGives the power and max power sensor pf your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", + "description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", "data": { "power_sensor_entity_id": "Power sensor entity id", "max_power_sensor_entity_id": "Max power sensor entity id", "device_power": "Device power (kW)" } + }, + "presence": { + "title": "Presence management", + "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.", + "data": { + "presence_sensor_entity_id": "Presence sensor entity id (true is present)", + "no_presence_preset": "Preset to use when no one is present", + "no_presence_offset": "Temperature offset to apply to current temperature is no one is present" + } } }, "error": { diff --git a/custom_components/versatile_thermostat/translations/en.json b/custom_components/versatile_thermostat/translations/en.json index 727f33a6..e497e9ad 100644 --- a/custom_components/versatile_thermostat/translations/en.json +++ b/custom_components/versatile_thermostat/translations/en.json @@ -50,7 +50,7 @@ } }, "motion": { - "title": "Motion sensor management", + "title": "Motion management", "description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", "data": { "motion_sensor_entity_id": "Motion sensor entity id", @@ -61,12 +61,21 @@ }, "power": { "title": "Power management", - "description": "Power management attributes.\nGives the power and max power sensor pf your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", + "description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", "data": { "power_sensor_entity_id": "Power sensor entity id", "max_power_sensor_entity_id": "Max power sensor entity id", "device_power": "Device power (kW)" } + }, + "presence": { + "title": "Presence management", + "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.", + "data": { + "presence_sensor_entity_id": "Presence sensor entity id (true is present)", + "no_presence_preset": "Preset to use when no one is present", + "no_presence_offset": "Temperature offset to apply to current temperature is no one is present" + } } }, "error": { @@ -127,7 +136,7 @@ } }, "motion": { - "title": "Motion sensor management", + "title": "Motion management", "description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", "data": { "motion_sensor_entity_id": "Motion sensor entity id", @@ -138,12 +147,21 @@ }, "power": { "title": "Power management", - "description": "Power management attributes.\nGives the power and max power sensor pf your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", + "description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", "data": { "power_sensor_entity_id": "Power sensor entity id", "max_power_sensor_entity_id": "Max power sensor entity id", "device_power": "Device power (kW)" } + }, + "presence": { + "title": "Presence management", + "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.", + "data": { + "presence_sensor_entity_id": "Presence sensor entity id (true is present)", + "no_presence_preset": "Preset to use when no one is present", + "no_presence_offset": "Temperature offset to apply to current temperature is no one is present" + } } }, "error": { diff --git a/custom_components/versatile_thermostat/translations/fr.json b/custom_components/versatile_thermostat/translations/fr.json index fc704de5..0155637d 100644 --- a/custom_components/versatile_thermostat/translations/fr.json +++ b/custom_components/versatile_thermostat/translations/fr.json @@ -67,6 +67,15 @@ "max_power_sensor_entity_id": "Capteur de puissance Max (entity id)", "device_power": "Puissance de l'équipement" } + }, + "presence": { + "title": "Gestion de la présence", + "description": "Donnez un capteur de présence (true si quelqu'un est présent).\nEnsuite spécifiez soit un preset à utiliser, soit un offset de température à appliquer lorsque personne n'est présent.\nSi le préset est utilisé, l'offset ne sera pas pris en compte.\nLaissez l'entity id vide si la gestion de la présence est non utilisée.", + "data": { + "presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)", + "no_presence_preset": "Preset à utiliser si personne n'est présent", + "no_presence_offset": "Offset de température à utiliser si personne n'est présent" + } } }, "error": { @@ -144,6 +153,15 @@ "max_power_sensor_entity_id": "Capteur de puissance Max (entity id)", "device_power": "Puissance de l'équipement" } + }, + "presence": { + "title": "Gestion de la présence", + "description": "Donnez un capteur de présence (true si quelqu'un est présent).\nEnsuite spécifiez soit un preset à utiliser, soit un offset de température à appliquer lorsque personne n'est présent.\nSi le préset est utilisé, l'offset ne sera pas pris en compte.\nLaissez l'entity id vide si la gestion de la présence est non utilisée.", + "data": { + "presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)", + "no_presence_preset": "Preset à utiliser si personne n'est présent", + "no_presence_offset": "Offset de température à utiliser si personne n'est présent" + } } }, "error": {