Add presence management

Fix many bugs on states actualisation
Add extra attributes with all internal states
This commit is contained in:
Jean-Marc Collin
2023-01-08 12:29:50 +01:00
parent 5fb148d445
commit 89984b3dfc
8 changed files with 465 additions and 118 deletions

View File

@@ -13,13 +13,13 @@ input_number:
name: Temperature name: Temperature
min: 0 min: 0
max: 35 max: 35
step: .5 step: .1
icon: mdi:thermometer icon: mdi:thermometer
fake_external_temperature_sensor1: fake_external_temperature_sensor1:
name: Ext Temperature name: Ext Temperature
min: -10 min: -10
max: 35 max: 35
step: .5 step: .1
icon: mdi:home-thermometer icon: mdi:home-thermometer
fake_current_power: fake_current_power:
name: Current power name: Current power
@@ -45,9 +45,16 @@ input_boolean:
name: Heater 1 (Linear) name: Heater 1 (Linear)
icon: mdi:radiator icon: mdi:radiator
fake_heater_switch2: 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 icon: mdi:radiator
# input_boolean to simulate the motion sensor entity. Only for development environment. # input_boolean to simulate the motion sensor entity. Only for development environment.
fake_motion_sensor1: fake_motion_sensor1:
name: Motion Sensor 1 name: Motion Sensor 1
icon: mdi:run 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

View File

@@ -1,7 +1,7 @@
import math import math
import logging import logging
from datetime import timedelta from datetime import timedelta, datetime
from typing import Any, Mapping from typing import Any, Mapping
from homeassistant.core import ( from homeassistant.core import (
@@ -84,6 +84,9 @@ from .const import (
CONF_PROP_BIAS, CONF_PROP_BIAS,
CONF_TPI_COEF_C, CONF_TPI_COEF_C,
CONF_TPI_COEF_T, CONF_TPI_COEF_T,
CONF_PRESENCE_SENSOR,
CONF_NO_PRESENCE_PRESET,
CONF_NO_PRESENCE_TEMP_OFFSET,
SUPPORT_FLAGS, SUPPORT_FLAGS,
PRESET_POWER, PRESET_POWER,
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
@@ -123,6 +126,9 @@ async def async_setup_entry(
device_power = entry.data.get(CONF_DEVICE_POWER) device_power = entry.data.get(CONF_DEVICE_POWER)
tpi_coefc = entry.data.get(CONF_TPI_COEF_C) tpi_coefc = entry.data.get(CONF_TPI_COEF_C)
tpi_coeft = entry.data.get(CONF_TPI_COEF_T) 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 = {} presets = {}
for (key, value) in CONF_PRESETS.items(): for (key, value) in CONF_PRESETS.items():
@@ -156,6 +162,9 @@ async def async_setup_entry(
device_power, device_power,
tpi_coefc, tpi_coefc,
tpi_coeft, tpi_coeft,
presence_sensor_entity_id,
no_presence_preset,
no_presence_offset,
) )
], ],
True, True,
@@ -193,6 +202,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
device_power, device_power,
tpi_coefc, tpi_coefc,
tpi_coeft, tpi_coeft,
presence_sensor_entity_id,
no_presence_preset,
no_presence_offset,
) -> None: ) -> None:
"""Initialize the thermostat.""" """Initialize the thermostat."""
@@ -220,6 +232,20 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._no_motion_preset = no_motion_preset self._no_motion_preset = no_motion_preset
self._tpi_coefc = tpi_coefc self._tpi_coefc = tpi_coefc
self._tpi_coeft = tpi_coeft 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: # TODO if self.ac_mode:
# self.hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF] # self.hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF]
@@ -259,6 +285,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._pmax_on = True self._pmax_on = True
self._current_power = 0 self._current_power = 0
self._current_power_max = 0 self._current_power_max = 0
else:
_LOGGER.info("%s - Power management is not fully configured.", self)
# will be restored if possible # will be restored if possible
self._target_temp = None self._target_temp = None
@@ -293,6 +321,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._window_call_cancel = None self._window_call_cancel = None
self._motion_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( _LOGGER.debug(
"%s - Creation of a new VersatileThermostat entity: unique_id=%s heater_entity_id=%s", "%s - Creation of a new VersatileThermostat entity: unique_id=%s heater_entity_id=%s",
self, self,
@@ -367,13 +403,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Return the sensor temperature.""" """Return the sensor temperature."""
return self._cur_temp 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): async def async_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode.""" """Set new target hvac mode."""
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode) _LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
@@ -415,20 +444,20 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._target_temp = self._saved_target_temp self._target_temp = self._saved_target_temp
elif preset_mode == PRESET_ACTIVITY: elif preset_mode == PRESET_ACTIVITY:
self._attr_preset_mode = PRESET_ACTIVITY self._attr_preset_mode = PRESET_ACTIVITY
self._target_temp = self._presets[self._no_motion_preset] self._update_motion_temp()
else: else:
if self._attr_preset_mode == PRESET_NONE: if self._attr_preset_mode == PRESET_NONE:
self._saved_target_temp = self._target_temp self._saved_target_temp = self._target_temp
self._attr_preset_mode = preset_mode self._attr_preset_mode = preset_mode
self._target_temp = self._presets[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._saved_preset_mode = self._attr_preset_mode
self.async_write_ha_state() self.recalculate()
self._prop_algorithm.calculate(
self._target_temp, self._cur_temp, self._cur_ext_temp
)
async def async_set_fan_mode(self, fan_mode): async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode.""" """Set new target fan mode."""
@@ -461,10 +490,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
return return
self._target_temp = temperature self._target_temp = temperature
self._attr_preset_mode = PRESET_NONE self._attr_preset_mode = PRESET_NONE
self._prop_algorithm.calculate( self.recalculate()
self._target_temp, self._cur_temp, self._cur_ext_temp
)
self.async_write_ha_state()
@callback @callback
async def entry_update_listener( async def entry_update_listener(
@@ -520,15 +546,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) )
) )
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: if self._power_sensor_entity_id:
self.async_on_remove( self.async_on_remove(
async_track_state_change_event( async_track_state_change_event(
@@ -547,8 +564,27 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) )
) )
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() 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): async def async_startup(self):
"""Triggered on startup, used to get old state and set internal states accordingly""" """Triggered on startup, used to get old state and set internal states accordingly"""
_LOGGER.debug("%s - Calling async_startup", self) _LOGGER.debug("%s - Calling async_startup", self)
@@ -584,6 +620,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
float(ext_temperature_state.state), float(ext_temperature_state.state),
) )
self._async_update_ext_temp(ext_temperature_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) switch_state = self.hass.states.get(self._heater_entity_id)
if switch_state and switch_state.state not in ( if switch_state and switch_state.state not in (
@@ -623,6 +669,53 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) )
need_write_state = True 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: if need_write_state:
self.async_write_ha_state() self.async_write_ha_state()
self._prop_algorithm.calculate( self._prop_algorithm.calculate(
@@ -630,6 +723,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) )
self.hass.create_task(self._async_control_heating()) self.hass.create_task(self._async_control_heating())
await self.get_my_previous_state()
if self.hass.state == CoreState.running: if self.hass.state == CoreState.running:
_async_startup_internal() _async_startup_internal()
else: else:
@@ -637,8 +732,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
EVENT_HOMEASSISTANT_START, _async_startup_internal EVENT_HOMEASSISTANT_START, _async_startup_internal
) )
await self.get_my_previous_state()
async def get_my_previous_state(self): async def get_my_previous_state(self):
"""Try to get my previou state""" """Try to get my previou state"""
# Check If we have an old state # Check If we have an old state
@@ -670,9 +763,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if not self._hvac_mode and old_state.state: if not self._hvac_mode and old_state.state:
self._hvac_mode = old_state.state self._hvac_mode = old_state.state
self._prop_algorithm.calculate( # is done in startup above
self._target_temp, self._cur_temp, self._cur_ext_temp # self._prop_algorithm.calculate(
) # self._target_temp, self._cur_temp, self._cur_ext_temp
# )
else: else:
# No previous state, try and restore defaults # No previous state, try and restore defaults
@@ -712,10 +806,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
return return
self._async_update_temp(new_state) self._async_update_temp(new_state)
self._prop_algorithm.calculate( self.recalculate()
self._target_temp, self._cur_temp, self._cur_ext_temp
)
self.async_write_ha_state()
async def _async_ext_temperature_changed(self, event): async def _async_ext_temperature_changed(self, event):
"""Handle external temperature changes.""" """Handle external temperature changes."""
@@ -729,10 +820,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
return return
self._async_update_ext_temp(new_state) self._async_update_ext_temp(new_state)
self._prop_algorithm.calculate( self.recalculate()
self._target_temp, self._cur_temp, self._cur_ext_temp
)
self.async_write_ha_state()
@callback @callback
async def _async_windows_changed(self, event): async def _async_windows_changed(self, event):
@@ -771,14 +859,15 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if not self._saved_hvac_mode: if not self._saved_hvac_mode:
self._saved_hvac_mode = self._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( _LOGGER.info(
"%s - Window is closed. Restoring hvac_mode '%s'", "%s - Window is closed. Restoring hvac_mode '%s'",
self, self,
self._saved_hvac_mode, self._saved_hvac_mode,
) )
await self.async_set_hvac_mode(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( _LOGGER.info(
"%s - Window is open. Set hvac_mode to '%s'", self, HVAC_MODE_OFF "%s - Window is open. Set hvac_mode to '%s'", self, HVAC_MODE_OFF
) )
@@ -803,8 +892,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._attr_preset_mode, self._attr_preset_mode,
PRESET_ACTIVITY, 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): if new_state is None or new_state.state not in (STATE_OFF, STATE_ON):
return return
@@ -827,21 +915,21 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
return return
_LOGGER.debug("%s - Motion delay condition is satisfied", self) _LOGGER.debug("%s - Motion delay condition is satisfied", self)
new_preset = ( self._motion_state = new_state.state
self._motion_preset if self._attr_preset_mode == PRESET_ACTIVITY:
if new_state.state == STATE_ON new_preset = (
else self._no_motion_preset self._motion_preset
) if self._motion_state == STATE_ON
_LOGGER.info( else self._no_motion_preset
"%s - Motion condition have changes. New preset temp will be %s", )
self, _LOGGER.info(
new_preset, "%s - Motion condition have changes. New preset temp will be %s",
) self,
self._target_temp = self._presets[new_preset] new_preset,
self._prop_algorithm.calculate( )
self._target_temp, self._cur_temp, self._cur_ext_temp # We do not change the preset which is kept to ACTIVITY but only the target_temperature
) self._target_temp = self._presets[new_preset]
self.async_write_ha_state() self.recalculate()
if self._motion_call_cancel: if self._motion_call_cancel:
self._motion_call_cancel() self._motion_call_cancel()
@@ -936,6 +1024,111 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
except ValueError as ex: except ValueError as ex:
_LOGGER.error("Unable to update current_power from sensor: %s", 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): async def _async_heater_turn_on(self):
"""Turn heater toggleable device on.""" """Turn heater toggleable device on."""
data = {ATTR_ENTITY_ID: self._heater_entity_id} data = {ATTR_ENTITY_ID: self._heater_entity_id}
@@ -965,19 +1158,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._current_power_max, self._current_power_max,
self._device_power, self._device_power,
) )
overpowering: bool = ( self._overpowering_state = (
self._current_power + self._device_power >= self._current_power_max self._current_power + self._device_power >= self._current_power_max
) )
if overpowering: if self._overpowering_state:
_LOGGER.warning( _LOGGER.warning(
"%s - overpowering is detected. Heater preset will be set to 'power'", "%s - overpowering is detected. Heater preset will be set to 'power'",
self, self,
) )
await self._async_set_preset_mode_internal(PRESET_POWER) await self._async_set_preset_mode_internal(PRESET_POWER)
return overpowering return self._overpowering_state
# Check if we need to remove the POWER preset # 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( _LOGGER.warning(
"%s - end of overpowering is detected. Heater preset will be restored to '%s'", "%s - end of overpowering is detected. Heater preset will be restored to '%s'",
self, self,
@@ -1012,12 +1205,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# Cancel eventual previous cycle if any # Cancel eventual previous cycle if any
if self._async_cancel_cycle is not None: if self._async_cancel_cycle is not None:
_LOGGER.debug("Cancelling the previous cycle that was running") _LOGGER.debug(
self._async_cancel_cycle() "%s - A previous cycle is alredy running -> waits for its end", self
self._async_cancel_cycle = None )
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 # Don't turn off if we will turn on just after
if on_time_sec <= 0: # if on_time_sec <= 0:
await self._async_heater_turn_off() # await self._async_heater_turn_off()
if self._hvac_mode == HVAC_MODE_HEAT and on_time_sec > 0: if self._hvac_mode == HVAC_MODE_HEAT and on_time_sec > 0:
_LOGGER.info( _LOGGER.info(
@@ -1029,19 +1226,23 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
await self._async_heater_turn_on() await self._async_heater_turn_on()
self.update_custom_attributes()
async def _turn_off(_): 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()
self._async_cancel_cycle = None 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 # Program turn off
self._async_cancel_cycle = async_call_later( self._async_cancel_cycle = async_call_later(
@@ -1050,6 +1251,31 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_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): def update_custom_attributes(self):
"""Update the custom extra attributes for the entity""" """Update the custom extra attributes for the entity"""
@@ -1057,7 +1283,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"away_temp": self._presets[PRESET_AWAY], "away_temp": self._presets[PRESET_AWAY],
"eco_temp": self._presets[PRESET_ECO], "eco_temp": self._presets[PRESET_ECO],
"boost_temp": self._presets[PRESET_BOOST], "boost_temp": self._presets[PRESET_BOOST],
"comfort_temp": self._presets[PRESET_BOOST], "comfort_temp": self._presets[PRESET_COMFORT],
"power_temp": self._presets[PRESET_POWER], "power_temp": self._presets[PRESET_POWER],
"on_percent": self._prop_algorithm.on_percent, "on_percent": self._prop_algorithm.on_percent,
"on_time_sec": self._prop_algorithm.on_time_sec, "on_time_sec": self._prop_algorithm.on_time_sec,
@@ -1070,10 +1296,25 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"function": self._proportional_function, "function": self._proportional_function,
"tpi_coefc": self._tpi_coefc, "tpi_coefc": self._tpi_coefc,
"tpi_coeft": self._tpi_coeft, "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( _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 @callback

View File

@@ -39,6 +39,9 @@ from .const import (
CONF_PROP_BIAS, CONF_PROP_BIAS,
CONF_TPI_COEF_T, CONF_TPI_COEF_T,
CONF_TPI_COEF_C, CONF_TPI_COEF_C,
CONF_PRESENCE_SENSOR,
CONF_NO_PRESENCE_PRESET,
CONF_NO_PRESENCE_TEMP_OFFSET,
PROPORTIONAL_FUNCTION_ATAN, PROPORTIONAL_FUNCTION_ATAN,
PROPORTIONAL_FUNCTION_LINEAR, PROPORTIONAL_FUNCTION_LINEAR,
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
@@ -63,23 +66,23 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
), ),
} }
) )
USER_DATA_CONF = [ # USER_DATA_CONF = [
CONF_NAME, # CONF_NAME,
CONF_HEATER, # CONF_HEATER,
CONF_TEMP_SENSOR, # CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR, # CONF_EXTERNAL_TEMP_SENSOR,
CONF_CYCLE_MIN, # CONF_CYCLE_MIN,
CONF_PROP_FUNCTION, # CONF_PROP_FUNCTION,
] # ]
STEP_P_DATA_SCHEMA = vol.Schema( STEP_P_DATA_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_PROP_BIAS, default=0.25): vol.Coerce(float), vol.Required(CONF_PROP_BIAS, default=0.25): vol.Coerce(float),
} }
) )
P_DATA_CONF = [ # P_DATA_CONF = [
CONF_PROP_BIAS, # CONF_PROP_BIAS,
] # ]
STEP_TPI_DATA_SCHEMA = vol.Schema( STEP_TPI_DATA_SCHEMA = vol.Schema(
{ {
@@ -88,16 +91,16 @@ STEP_TPI_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_TPI_COEF_T, default=0.01): vol.Coerce(float), vol.Required(CONF_TPI_COEF_T, default=0.01): vol.Coerce(float),
} }
) )
TPI_DATA_CONF = [ # TPI_DATA_CONF = [
CONF_EXTERNAL_TEMP_SENSOR, # CONF_EXTERNAL_TEMP_SENSOR,
CONF_TPI_COEF_C, # CONF_TPI_COEF_C,
CONF_TPI_COEF_T, # CONF_TPI_COEF_T,
] # ]
STEP_PRESETS_DATA_SCHEMA = vol.Schema( STEP_PRESETS_DATA_SCHEMA = vol.Schema(
{vol.Optional(v, default=17): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()} {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( STEP_WINDOW_DATA_SCHEMA = vol.Schema(
{ {
@@ -105,7 +108,7 @@ STEP_WINDOW_DATA_SCHEMA = vol.Schema(
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int, 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( STEP_MOTION_DATA_SCHEMA = vol.Schema(
{ {
@@ -119,12 +122,12 @@ STEP_MOTION_DATA_SCHEMA = vol.Schema(
), ),
} }
) )
MOTION_DATA_CONF = [ # MOTION_DATA_CONF = [
CONF_MOTION_SENSOR, # CONF_MOTION_SENSOR,
CONF_MOTION_DELAY, # CONF_MOTION_DELAY,
CONF_MOTION_PRESET, # CONF_MOTION_PRESET,
CONF_NO_MOTION_PRESET, # CONF_NO_MOTION_PRESET,
] # ]
STEP_POWER_DATA_SCHEMA = vol.Schema( STEP_POWER_DATA_SCHEMA = vol.Schema(
{ {
@@ -133,7 +136,16 @@ STEP_POWER_DATA_SCHEMA = vol.Schema(
vol.Optional(CONF_DEVICE_POWER): vol.Coerce(float), 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): def schema_defaults(schema, **defaults):
@@ -182,6 +194,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_MOTION_SENSOR, CONF_MOTION_SENSOR,
CONF_POWER_SENSOR, CONF_POWER_SENSOR,
CONF_MAX_POWER_SENSOR, CONF_MAX_POWER_SENSOR,
CONF_PRESENCE_SENSOR,
]: ]:
d = data.get(conf, None) # pylint: disable=invalid-name d = data.get(conf, None) # pylint: disable=invalid-name
if d is not None and self.hass.states.get(d) is None: if d is not None and self.hass.states.get(d) is None:
@@ -282,6 +295,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"power", "power",
STEP_POWER_DATA_SCHEMA, STEP_POWER_DATA_SCHEMA,
user_input, 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 self.async_finalize, # pylint: disable=no-member
) )
@@ -412,6 +436,19 @@ class VersatileThermostatOptionsFlowHandler(
"power", "power",
STEP_POWER_DATA_SCHEMA, STEP_POWER_DATA_SCHEMA,
user_input, 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 self.async_finalize, # pylint: disable=no-member
) )

View File

@@ -37,6 +37,9 @@ CONF_MOTION_PRESET = "motion_preset"
CONF_NO_MOTION_PRESET = "no_motion_preset" CONF_NO_MOTION_PRESET = "no_motion_preset"
CONF_TPI_COEF_C = "tpi_coefc" CONF_TPI_COEF_C = "tpi_coefc"
CONF_TPI_COEF_T = "tpi_coeft" 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 = { CONF_PRESETS = {
p: f"{p}_temp" p: f"{p}_temp"
@@ -73,6 +76,9 @@ ALL_CONF = (
CONF_PROP_BIAS, CONF_PROP_BIAS,
CONF_TPI_COEF_C, CONF_TPI_COEF_C,
CONF_TPI_COEF_T, CONF_TPI_COEF_T,
CONF_PRESENCE_SENSOR,
CONF_NO_PRESENCE_PRESET,
CONF_NO_PRESENCE_TEMP_OFFSET,
] ]
+ CONF_PRESETS_VALUES, + CONF_PRESETS_VALUES,
) )

View File

@@ -20,9 +20,11 @@ class PropAlgorithm:
): ):
"""Initialisation of the Proportional Algorithm""" """Initialisation of the Proportional Algorithm"""
_LOGGER.debug( _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, function_type,
bias, bias,
tpi_coefc,
tpi_coeft,
cycle_min, cycle_min,
) )
# TODO test function_type, bias, cycle_min # TODO test function_type, bias, cycle_min

View File

@@ -50,7 +50,7 @@
} }
}, },
"motion": { "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", "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": { "data": {
"motion_sensor_entity_id": "Motion sensor entity id", "motion_sensor_entity_id": "Motion sensor entity id",
@@ -61,12 +61,21 @@
}, },
"power": { "power": {
"title": "Power management", "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": { "data": {
"power_sensor_entity_id": "Power sensor entity id", "power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id", "max_power_sensor_entity_id": "Max power sensor entity id",
"device_power": "Device power (kW)" "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": { "error": {
@@ -127,7 +136,7 @@
} }
}, },
"motion": { "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", "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": { "data": {
"motion_sensor_entity_id": "Motion sensor entity id", "motion_sensor_entity_id": "Motion sensor entity id",
@@ -138,12 +147,21 @@
}, },
"power": { "power": {
"title": "Power management", "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": { "data": {
"power_sensor_entity_id": "Power sensor entity id", "power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id", "max_power_sensor_entity_id": "Max power sensor entity id",
"device_power": "Device power (kW)" "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": { "error": {

View File

@@ -50,7 +50,7 @@
} }
}, },
"motion": { "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", "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": { "data": {
"motion_sensor_entity_id": "Motion sensor entity id", "motion_sensor_entity_id": "Motion sensor entity id",
@@ -61,12 +61,21 @@
}, },
"power": { "power": {
"title": "Power management", "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": { "data": {
"power_sensor_entity_id": "Power sensor entity id", "power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id", "max_power_sensor_entity_id": "Max power sensor entity id",
"device_power": "Device power (kW)" "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": { "error": {
@@ -127,7 +136,7 @@
} }
}, },
"motion": { "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", "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": { "data": {
"motion_sensor_entity_id": "Motion sensor entity id", "motion_sensor_entity_id": "Motion sensor entity id",
@@ -138,12 +147,21 @@
}, },
"power": { "power": {
"title": "Power management", "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": { "data": {
"power_sensor_entity_id": "Power sensor entity id", "power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id", "max_power_sensor_entity_id": "Max power sensor entity id",
"device_power": "Device power (kW)" "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": { "error": {

View File

@@ -67,6 +67,15 @@
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)", "max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
"device_power": "Puissance de l'équipement" "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": { "error": {
@@ -144,6 +153,15 @@
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)", "max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
"device_power": "Puissance de l'équipement" "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": { "error": {