Compare commits

..

4 Commits

Author SHA1 Message Date
Jean-Marc Collin
62425bb91f FIX don't trigger cycle calculation at each power event 2023-01-21 17:34:28 +01:00
Jean-Marc Collin
952d6d5e97 Issue #13 - After reconfiguration of the thermostat we need to reload the integration
Issue #22 - Change preset to "security" when the Thermostat goes in security mode
Issue #30 - When shutting of a thermostat after it has been in security mode, the thermostat can start up alone
2023-01-21 13:38:37 +01:00
Jean-Marc Collin
638e007c21 Add min_temp and max_temp in configuration #17
Mise en sécurité du thermostat si pas de changement de température #18
Add a configurable minimal delay of activation #19
2023-01-18 23:40:55 +01:00
Jean-Marc Collin
c520d7fad5 FIX services.yaml error 2023-01-15 22:40:00 +01:00
8 changed files with 436 additions and 255 deletions

View File

@@ -1,8 +1,10 @@
""" Implements the VersatileThermostat climate component """
import math import math
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta, datetime
from typing import Any, Mapping
# from typing import Any
import voluptuous as vol import voluptuous as vol
@@ -11,7 +13,6 @@ from homeassistant.core import (
callback, callback,
CoreState, CoreState,
DOMAIN as HA_DOMAIN, DOMAIN as HA_DOMAIN,
CALLBACK_TYPE,
) )
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
@@ -20,12 +21,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change_event, async_track_state_change_event,
async_track_time_interval,
async_call_later, async_call_later,
) )
from homeassistant.exceptions import ConditionError from homeassistant.exceptions import ConditionError
from homeassistant.helpers import condition, entity_platform, config_validation as cv from homeassistant.helpers import (
condition,
entity_platform,
) # , config_validation as cv
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
ATTR_PRESET_MODE, ATTR_PRESET_MODE,
@@ -38,7 +41,7 @@ from homeassistant.components.climate.const import (
HVAC_MODE_HEAT, HVAC_MODE_HEAT,
HVAC_MODE_OFF, HVAC_MODE_OFF,
PRESET_ACTIVITY, PRESET_ACTIVITY,
PRESET_AWAY, # PRESET_AWAY,
PRESET_BOOST, PRESET_BOOST,
PRESET_COMFORT, PRESET_COMFORT,
PRESET_ECO, PRESET_ECO,
@@ -92,10 +95,16 @@ from .const import (
CONF_PRESET_POWER, CONF_PRESET_POWER,
SUPPORT_FLAGS, SUPPORT_FLAGS,
PRESET_POWER, PRESET_POWER,
PRESET_SECURITY,
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
SERVICE_SET_PRESENCE, SERVICE_SET_PRESENCE,
SERVICE_SET_PRESET_TEMPERATURE, SERVICE_SET_PRESET_TEMPERATURE,
PRESET_AWAY_SUFFIX, PRESET_AWAY_SUFFIX,
CONF_SECURITY_DELAY_MIN,
CONF_MINIMAL_ACTIVATION_DELAY,
CONF_TEMP_MAX,
CONF_TEMP_MIN,
HIDDEN_PRESETS,
) )
from .prop_algorithm import PropAlgorithm from .prop_algorithm import PropAlgorithm
@@ -115,71 +124,11 @@ async def async_setup_entry(
unique_id = entry.entry_id unique_id = entry.entry_id
name = entry.data.get(CONF_NAME) name = entry.data.get(CONF_NAME)
heater_entity_id = entry.data.get(CONF_HEATER)
cycle_min = entry.data.get(CONF_CYCLE_MIN)
proportional_function = entry.data.get(CONF_PROP_FUNCTION)
temp_sensor_entity_id = entry.data.get(CONF_TEMP_SENSOR)
ext_temp_sensor_entity_id = entry.data.get(CONF_EXTERNAL_TEMP_SENSOR)
power_sensor_entity_id = entry.data.get(CONF_POWER_SENSOR)
max_power_sensor_entity_id = entry.data.get(CONF_MAX_POWER_SENSOR)
window_sensor_entity_id = entry.data.get(CONF_WINDOW_SENSOR)
window_delay_sec = entry.data.get(CONF_WINDOW_DELAY)
motion_sensor_entity_id = entry.data.get(CONF_MOTION_SENSOR)
motion_delay_sec = entry.data.get(CONF_MOTION_DELAY)
motion_preset = entry.data.get(CONF_MOTION_PRESET)
no_motion_preset = entry.data.get(CONF_NO_MOTION_PRESET)
device_power = entry.data.get(CONF_DEVICE_POWER)
tpi_coef_int = entry.data.get(CONF_TPI_COEF_INT)
tpi_coef_ext = entry.data.get(CONF_TPI_COEF_EXT)
presence_sensor_entity_id = entry.data.get(CONF_PRESENCE_SENSOR)
power_temp = entry.data.get(CONF_PRESET_POWER)
presets = {} entity = VersatileThermostat(hass, unique_id, name, entry.data)
for (key, value) in CONF_PRESETS.items():
_LOGGER.debug("looking for key=%s, value=%s", key, value)
if value in entry.data:
presets[key] = entry.data.get(value)
else:
_LOGGER.debug("value %s not found in Entry", value)
presets_away = {} async_add_entities([entity], True)
for (key, value) in CONF_PRESETS_AWAY.items(): VersatileThermostat.add_entity(entry.entry_id, entity)
_LOGGER.debug("looking for key=%s, value=%s", key, value)
if value in entry.data:
presets_away[key] = entry.data.get(value)
else:
_LOGGER.debug("value %s not found in Entry", value)
async_add_entities(
[
VersatileThermostat(
hass,
unique_id,
name,
heater_entity_id,
cycle_min,
proportional_function,
temp_sensor_entity_id,
ext_temp_sensor_entity_id,
power_sensor_entity_id,
max_power_sensor_entity_id,
window_sensor_entity_id,
window_delay_sec,
motion_sensor_entity_id,
motion_delay_sec,
motion_preset,
no_motion_preset,
presets,
presets_away,
device_power,
tpi_coef_int,
tpi_coef_ext,
presence_sensor_entity_id,
power_temp,
)
],
True,
)
# Add services # Add services
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()
@@ -207,38 +156,10 @@ async def async_setup_entry(
class VersatileThermostat(ClimateEntity, RestoreEntity): class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Representation of a Versatile Thermostat device.""" """Representation of a Versatile Thermostat device."""
_name: str # The list of VersatileThermostat entities
_heater_entity_id: str _registry: dict[str, object] = {}
_prop_algorithm: PropAlgorithm
_async_cancel_cycle: CALLBACK_TYPE
_attr_preset_modes: list[str] | None
def __init__( def __init__(self, hass, unique_id, name, entry_infos) -> None:
self,
hass,
unique_id,
name,
heater_entity_id,
cycle_min,
proportional_function,
temp_sensor_entity_id,
ext_temp_sensor_entity_id,
power_sensor_entity_id,
max_power_sensor_entity_id,
window_sensor_entity_id,
window_delay_sec,
motion_sensor_entity_id,
motion_delay_sec,
motion_preset,
no_motion_preset,
presets,
presets_away,
device_power,
tpi_coef_int,
tpi_coef_ext,
presence_sensor_entity_id,
power_temp,
) -> None:
"""Initialize the thermostat.""" """Initialize the thermostat."""
super().__init__() super().__init__()
@@ -248,32 +169,100 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._unique_id = unique_id self._unique_id = unique_id
self._name = name self._name = name
self._heater_entity_id = heater_entity_id self._prop_algorithm = None
self._cycle_min = cycle_min self._async_cancel_cycle = None
self._proportional_function = proportional_function self._hvac_mode = None
self._temp_sensor_entity_id = temp_sensor_entity_id self._target_temp = None
self._ext_temp_sensor_entity_id = ext_temp_sensor_entity_id self._saved_target_temp = None
self._power_sensor_entity_id = power_sensor_entity_id self._saved_preset_mode = None
self._max_power_sensor_entity_id = max_power_sensor_entity_id self._fan_mode = None
self._window_sensor_entity_id = window_sensor_entity_id self._humidity = None
self._window_delay_sec = window_delay_sec self._swing_mode = None
self._window_delay_sec = window_delay_sec self._current_power = None
self._motion_sensor_entity_id = motion_sensor_entity_id self._current_power_max = None
self._motion_delay_sec = motion_delay_sec self._window_state = None
self._motion_preset = motion_preset self._motion_state = None
self._no_motion_preset = no_motion_preset self._saved_hvac_mode = None
self._window_call_cancel = None
self._motion_call_cancel = None
self._cur_ext_temp = None
self._cur_temp = None
self._ac_mode = None
self._last_ext_temperature_mesure = None
self._last_temperature_mesure = None
self._cur_ext_temp = None
self._presence_state = None
self._overpowering_state = None
self._should_relaunch_control_heating = None
self._security_delay_min = None
self._security_state = None
self.post_init(entry_infos)
def post_init(self, entry_infos):
"""Finish the initialization of the thermostast"""
_LOGGER.info(
"%s - Updating VersatileThermostat with infos %s",
self,
entry_infos,
)
# convert entry_infos into usable attributes
presets = {}
for (key, value) in CONF_PRESETS.items():
_LOGGER.debug("looking for key=%s, value=%s", key, value)
if value in entry_infos:
presets[key] = entry_infos.get(value)
else:
_LOGGER.debug("value %s not found in Entry", value)
presets_away = {}
for (key, value) in CONF_PRESETS_AWAY.items():
_LOGGER.debug("looking for key=%s, value=%s", key, value)
if value in entry_infos:
presets_away[key] = entry_infos.get(value)
else:
_LOGGER.debug("value %s not found in Entry", value)
# Stop eventual cycle running
if self._async_cancel_cycle is not None:
self._async_cancel_cycle()
self._async_cancel_cycle = None
if self._window_call_cancel is not None:
self._window_call_cancel()
self._window_call_cancel = None
if self._motion_call_cancel is not None:
self._motion_call_cancel()
self._motion_call_cancel = None
# Exploit usable attributs
self._heater_entity_id = entry_infos.get(CONF_HEATER)
self._cycle_min = entry_infos.get(CONF_CYCLE_MIN)
self._proportional_function = entry_infos.get(CONF_PROP_FUNCTION)
self._temp_sensor_entity_id = entry_infos.get(CONF_TEMP_SENSOR)
self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_SENSOR)
self._attr_max_temp = entry_infos.get(CONF_TEMP_MAX)
self._attr_min_temp = entry_infos.get(CONF_TEMP_MIN)
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR)
self._window_delay_sec = entry_infos.get(CONF_WINDOW_DELAY)
self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR)
self._motion_delay_sec = entry_infos.get(CONF_MOTION_DELAY)
self._motion_preset = entry_infos.get(CONF_MOTION_PRESET)
self._no_motion_preset = entry_infos.get(CONF_NO_MOTION_PRESET)
self._motion_on = ( self._motion_on = (
self._motion_sensor_entity_id is not None self._motion_sensor_entity_id is not None
and self._motion_preset is not None and self._motion_preset is not None
and self._no_motion_preset is not None and self._no_motion_preset is not None
) )
self._tpi_coef_int = tpi_coef_int self._tpi_coef_int = entry_infos.get(CONF_TPI_COEF_INT)
self._tpi_coef_ext = tpi_coef_ext self._tpi_coef_ext = entry_infos.get(CONF_TPI_COEF_EXT)
self._presence_sensor_entity_id = presence_sensor_entity_id self._presence_sensor_entity_id = entry_infos.get(CONF_PRESENCE_SENSOR)
self._power_temp = power_temp self._power_temp = entry_infos.get(CONF_PRESET_POWER)
self._presence_on = self._presence_sensor_entity_id != None self._presence_on = self._presence_sensor_entity_id is not None
# 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]
@@ -300,7 +289,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._saved_preset_mode = None self._saved_preset_mode = None
# Power management # Power management
self._device_power = device_power self._device_power = entry_infos.get(CONF_DEVICE_POWER)
self._pmax_on = False self._pmax_on = False
self._current_power = None self._current_power = None
self._current_power_max = None self._current_power_max = None
@@ -335,18 +324,23 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) )
self._tpi_coef_ext = 0 self._tpi_coef_ext = 0
self._security_delay_min = entry_infos.get(CONF_SECURITY_DELAY_MIN)
self._minimal_activation_delay = entry_infos.get(CONF_MINIMAL_ACTIVATION_DELAY)
self._last_temperature_mesure = datetime.now()
self._last_ext_temperature_mesure = datetime.now()
self._security_state = False
self._saved_hvac_mode = None
# Initiate the ProportionalAlgorithm # Initiate the ProportionalAlgorithm
if self._prop_algorithm is not None:
del self._prop_algorithm
self._prop_algorithm = PropAlgorithm( self._prop_algorithm = PropAlgorithm(
self._proportional_function, self._proportional_function,
self._tpi_coef_int, self._tpi_coef_int,
self._tpi_coef_ext, self._tpi_coef_ext,
self._cycle_min, self._cycle_min,
self._minimal_activation_delay,
) )
self._async_cancel_cycle = None
self._window_call_cancel = None
self._motion_call_cancel = None
self._should_relaunch_control_heating = False self._should_relaunch_control_heating = False
# Memory synthesis state # Memory synthesis state
@@ -360,9 +354,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if len(presets): if len(presets):
self._support_flags = SUPPORT_FLAGS | SUPPORT_PRESET_MODE self._support_flags = SUPPORT_FLAGS | SUPPORT_PRESET_MODE
for k, v in presets.items(): for key, val in presets.items():
if v != 0.0: if val != 0.0:
self._attr_preset_modes.append(k) self._attr_preset_modes.append(key)
# self._attr_preset_modes = ( # self._attr_preset_modes = (
# [PRESET_NONE] + list(presets.keys()) + [PRESET_ACTIVITY] # [PRESET_NONE] + list(presets.keys()) + [PRESET_ACTIVITY]
@@ -380,7 +374,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"%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,
self.unique_id, self.unique_id,
heater_entity_id, self._heater_entity_id,
) )
def __str__(self): def __str__(self):
@@ -480,7 +474,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force) _LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force)
if ( if (
preset_mode not in (self._attr_preset_modes or []) preset_mode not in (self._attr_preset_modes or [])
and preset_mode != PRESET_POWER and preset_mode not in HIDDEN_PRESETS
): ):
raise ValueError( raise ValueError(
f"Got unsupported preset_mode {preset_mode}. Must be one of {self._attr_preset_modes}" # pylint: disable=line-too-long f"Got unsupported preset_mode {preset_mode}. Must be one of {self._attr_preset_modes}" # pylint: disable=line-too-long
@@ -500,20 +494,24 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
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
preset_temp = self.find_preset_temp(preset_mode) self._target_temp = self.find_preset_temp(preset_mode)
self._target_temp = (
preset_temp if preset_mode != PRESET_POWER else self._power_temp
)
# Don't saved preset_mode if we are in POWER mode or in Away mode and presence detection is on self.save_preset_mode()
if preset_mode != PRESET_POWER:
self._saved_preset_mode = self._attr_preset_mode
self.recalculate() self.recalculate()
def find_preset_temp(self, preset_mode): def find_preset_temp(self, preset_mode):
"""Find the right temperature of a preset considering the presence if configured""" """Find the right temperature of a preset considering the presence if configured"""
if self._presence_on is False or self._presence_state in [STATE_ON, STATE_HOME]: if preset_mode == PRESET_SECURITY:
return (
self._target_temp
) # in security just keep the current target temperature, the thermostat should be off
if preset_mode == PRESET_POWER:
return self._power_temp
elif self._presence_on is False or self._presence_state in [
STATE_ON,
STATE_HOME,
]:
return self._presets[preset_mode] return self._presets[preset_mode]
else: else:
return self._presets_away[self.get_preset_away_name(preset_mode)] return self._presets_away[self.get_preset_away_name(preset_mode)]
@@ -654,7 +652,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_LOGGER.debug("%s - Calling async_startup", self) _LOGGER.debug("%s - Calling async_startup", self)
@callback @callback
def _async_startup_internal(*_): async def _async_startup_internal(*_):
_LOGGER.debug("%s - Calling async_startup_internal", self) _LOGGER.debug("%s - Calling async_startup_internal", self)
need_write_state = False need_write_state = False
temperature_state = self.hass.states.get(self._temp_sensor_entity_id) temperature_state = self.hass.states.get(self._temp_sensor_entity_id)
@@ -667,7 +665,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self, self,
float(temperature_state.state), float(temperature_state.state),
) )
self._async_update_temp(temperature_state) await self._async_update_temp(temperature_state)
need_write_state = True need_write_state = True
if self._ext_temp_sensor_entity_id: if self._ext_temp_sensor_entity_id:
@@ -683,7 +681,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self, self,
float(ext_temperature_state.state), float(ext_temperature_state.state),
) )
self._async_update_ext_temp(ext_temperature_state) await self._async_update_ext_temp(ext_temperature_state)
else: else:
_LOGGER.debug( _LOGGER.debug(
"%s - external temperature sensor have NOT been retrieved cause unknown or unavailable", "%s - external temperature sensor have NOT been retrieved cause unknown or unavailable",
@@ -790,7 +788,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
await self.get_my_previous_state() await self.get_my_previous_state()
if self.hass.state == CoreState.running: if self.hass.state == CoreState.running:
_async_startup_internal() await _async_startup_internal()
else: else:
self.hass.bus.async_listen_once( self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, _async_startup_internal EVENT_HOMEASSISTANT_START, _async_startup_internal
@@ -822,7 +820,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if old_state.attributes.get(ATTR_PRESET_MODE) in self._attr_preset_modes: if old_state.attributes.get(ATTR_PRESET_MODE) in self._attr_preset_modes:
self._attr_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE) self._attr_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE)
self._saved_preset_mode = self._attr_preset_mode self.save_preset_mode()
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
@@ -869,7 +867,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return return
self._async_update_temp(new_state) await self._async_update_temp(new_state)
self.recalculate() self.recalculate()
await self._async_control_heating(force=False) await self._async_control_heating(force=False)
@@ -884,7 +882,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return return
self._async_update_ext_temp(new_state) await self._async_update_ext_temp(new_state)
self.recalculate() self.recalculate()
await self._async_control_heating(force=False) await self._async_control_heating(force=False)
@@ -1028,24 +1026,35 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback
def _async_update_temp(self, state): async def _async_update_temp(self, state):
"""Update thermostat with latest state from sensor.""" """Update thermostat with latest state from sensor."""
try: try:
cur_temp = float(state.state) cur_temp = float(state.state)
if math.isnan(cur_temp) or math.isinf(cur_temp): if math.isnan(cur_temp) or math.isinf(cur_temp):
raise ValueError(f"Sensor has illegal state {state.state}") raise ValueError(f"Sensor has illegal state {state.state}")
self._cur_temp = cur_temp self._cur_temp = cur_temp
self._last_temperature_mesure = datetime.now()
# try to restart if we were in security mode
if self._security_state:
await self.async_set_hvac_mode(self._saved_hvac_mode)
await self.restore_preset_mode()
except ValueError as ex: except ValueError as ex:
_LOGGER.error("Unable to update temperature from sensor: %s", ex) _LOGGER.error("Unable to update temperature from sensor: %s", ex)
@callback @callback
def _async_update_ext_temp(self, state): async def _async_update_ext_temp(self, state):
"""Update thermostat with latest state from sensor.""" """Update thermostat with latest state from sensor."""
try: try:
cur_ext_temp = float(state.state) cur_ext_temp = float(state.state)
if math.isnan(cur_ext_temp) or math.isinf(cur_ext_temp): if math.isnan(cur_ext_temp) or math.isinf(cur_ext_temp):
raise ValueError(f"Sensor has illegal state {state.state}") raise ValueError(f"Sensor has illegal state {state.state}")
self._cur_ext_temp = cur_ext_temp self._cur_ext_temp = cur_ext_temp
self._last_ext_temperature_mesure = datetime.now()
# try to restart if we were in security mode
if self._security_state:
await self.async_set_hvac_mode(self._saved_hvac_mode)
await self.restore_preset_mode()
except ValueError as ex: except ValueError as ex:
_LOGGER.error("Unable to update external temperature from sensor: %s", ex) _LOGGER.error("Unable to update external temperature from sensor: %s", ex)
@@ -1069,6 +1078,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
raise ValueError(f"Sensor has illegal state {new_state.state}") raise ValueError(f"Sensor has illegal state {new_state.state}")
self._current_power = current_power self._current_power = current_power
if self._attr_preset_mode == PRESET_POWER:
await self._async_control_heating()
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)
@@ -1091,6 +1103,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if math.isnan(current_power_max) or math.isinf(current_power_max): if math.isnan(current_power_max) or math.isinf(current_power_max):
raise ValueError(f"Sensor has illegal state {new_state.state}") raise ValueError(f"Sensor has illegal state {new_state.state}")
self._current_power_max = current_power_max self._current_power_max = current_power_max
if self._attr_preset_mode == PRESET_POWER:
await self._async_control_heating()
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)
@@ -1115,9 +1129,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
def _update_presence(self, new_state): def _update_presence(self, new_state):
_LOGGER.debug("%s - Updating presence. New state is %s", self, new_state) _LOGGER.debug("%s - Updating presence. New state is %s", self, new_state)
self._presence_state = new_state self._presence_state = new_state
if self._attr_preset_mode == PRESET_POWER or self._presence_on is False: if self._attr_preset_mode in HIDDEN_PRESETS or self._presence_on is False:
_LOGGER.info( _LOGGER.info(
"%s - Ignoring presence change cause in Power preset or presence not configured", "%s - Ignoring presence change cause in Power or Security preset or presence not configured",
self, self,
) )
return return
@@ -1198,6 +1212,26 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context
) )
def save_preset_mode(self):
"""Save the current preset mode to be restored later
We never save a hidden preset mode
"""
if (
self._attr_preset_mode not in HIDDEN_PRESETS
and self._attr_preset_mode is not None
):
self._saved_preset_mode = self._attr_preset_mode
async def restore_preset_mode(self):
"""Restore a previous preset mode
We never restore a hidden preset mode. Normally that is not possible
"""
if (
self._saved_preset_mode not in HIDDEN_PRESETS
and self._saved_preset_mode is not None
):
await self._async_set_preset_mode_internal(self._saved_preset_mode)
async def check_overpowering(self) -> bool: async def check_overpowering(self) -> bool:
"""Check the overpowering condition """Check the overpowering condition
Turn the preset_mode of the heater to 'power' if power conditions are exceeded Turn the preset_mode of the heater to 'power' if power conditions are exceeded
@@ -1231,9 +1265,29 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self, self,
self._saved_preset_mode, self._saved_preset_mode,
) )
await self.async_set_preset_mode( await self.restore_preset_mode()
self._saved_preset_mode if self._saved_preset_mode else PRESET_NONE
def check_date_temperature(self) -> bool:
"""Check if last temperature date is too long"""
now = datetime.now()
delta_temp = (now - self._last_temperature_mesure).total_seconds() / 60.0
delta_ext_temp = (
now - self._last_ext_temperature_mesure
).total_seconds() / 60.0
if (
delta_temp > self._security_delay_min
or delta_ext_temp > self._security_delay_min
):
_LOGGER.warning(
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f)",
self,
self._security_delay_min,
delta_temp,
delta_ext_temp,
) )
return False
return True
async def _async_control_heating(self, force=False, time=None): async def _async_control_heating(self, force=False, time=None):
"""The main function used to run the calculation at each cycle""" """The main function used to run the calculation at each cycle"""
@@ -1246,22 +1300,24 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._attr_preset_mode, self._attr_preset_mode,
) )
await self._async_heater_turn_off() await self._async_heater_turn_off()
_LOGGER.debug("%s - End of cycle (0)", self)
return return
on_time_sec: int = self._prop_algorithm.on_time_sec on_time_sec: int = self._prop_algorithm.on_time_sec
off_time_sec: int = self._prop_algorithm.off_time_sec off_time_sec: int = self._prop_algorithm.off_time_sec
_LOGGER.info( _LOGGER.info(
"%s - Running new cycle at %s. on_time_sec=%.0f, off_time_sec=%.0f", "%s - Checking new cycle. on_time_sec=%.0f, off_time_sec=%.0f, security_state=%s, preset_mode=%s",
self, self,
time,
on_time_sec, on_time_sec,
off_time_sec, off_time_sec,
self._security_state,
self._attr_preset_mode,
) )
# 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:
if force: if force:
_LOGGER.debug("%s - we force a new cycle") _LOGGER.debug("%s - we force a new cycle", self)
self._async_cancel_cycle() self._async_cancel_cycle()
self._async_cancel_cycle = None self._async_cancel_cycle = None
else: else:
@@ -1270,22 +1326,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self, self,
) )
self._should_relaunch_control_heating = True self._should_relaunch_control_heating = True
_LOGGER.debug("%s - End of cycle (1)", self)
return 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 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(
# "%s - start heating for %d min %d sec ",
# self,
# on_time_sec // 60,
# on_time_sec % 60,
# )
#
# await self._async_heater_turn_on()
async def _turn_on_off_later( async def _turn_on_off_later(
on: bool, time, heater_action, next_cycle_action on: bool, time, heater_action, next_cycle_action
@@ -1293,6 +1337,21 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if self._async_cancel_cycle: if self._async_cancel_cycle:
self._async_cancel_cycle() self._async_cancel_cycle()
self._async_cancel_cycle = None self._async_cancel_cycle = None
_LOGGER.debug("%s - Stopping cycle during calculation", self)
check_dates = self.check_date_temperature()
if time > 0 and on is True and check_dates is False:
_LOGGER.warning("%s - Set the thermostat into security mode", self)
self._security_state = True
self._saved_hvac_mode = self.hvac_mode
self.save_preset_mode()
await self._async_set_preset_mode_internal(PRESET_SECURITY)
await self.async_set_hvac_mode(HVAC_MODE_OFF)
# The cycle is not restarted in security mode. It will be restarted by a condition changes
_LOGGER.debug("%s - End of cycle (2)", self)
return
if check_dates:
self._security_state = False
action_label = "start" if on else "stop" action_label = "start" if on else "stop"
if self._should_relaunch_control_heating: if self._should_relaunch_control_heating:
@@ -1300,28 +1359,28 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"Don't %s cause a cycle have to be relaunch", action_label "Don't %s cause a cycle have to be relaunch", action_label
) )
self._should_relaunch_control_heating = False self._should_relaunch_control_heating = False
await self._async_control_heating() self.hass.create_task(self._async_control_heating())
# await self._async_control_heating()
_LOGGER.debug("%s - End of cycle (3)", self)
return return
else:
if time > 0:
_LOGGER.info( _LOGGER.info(
"%s - %s heating for %d min %d sec", "%s - !!! %s heating for %d min %d sec",
self, self,
action_label, action_label,
time // 60, time // 60,
time % 60, time % 60,
) )
if time > 0: await heater_action()
await heater_action() else:
else: _LOGGER.debug("%s - No action on heater cause duration is 0", self)
_LOGGER.debug( self.update_custom_attributes()
"%s - No action on heater cause duration is 0", self self._async_cancel_cycle = async_call_later(
) self.hass,
self.update_custom_attributes() time,
self._async_cancel_cycle = async_call_later( next_cycle_action,
self.hass, )
time,
next_cycle_action,
)
async def _turn_on_later(_): async def _turn_on_later(_):
await _turn_on_off_later( await _turn_on_off_later(
@@ -1331,30 +1390,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
next_cycle_action=_turn_off_later, next_cycle_action=_turn_off_later,
) )
# if self._async_cancel_cycle:
# self._async_cancel_cycle()
# self._async_cancel_cycle = None
#
# 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()
# self._async_cancel_cycle = async_call_later(
# self.hass,
# off_time_sec,
# _turn_off_later,
# )
async def _turn_off_later(_): async def _turn_off_later(_):
await _turn_on_off_later( await _turn_on_off_later(
on=False, on=False,
@@ -1363,36 +1398,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
next_cycle_action=_turn_on_later, next_cycle_action=_turn_on_later,
) )
# if self._async_cancel_cycle:
# self._async_cancel_cycle()
# self._async_cancel_cycle = None
#
# 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()
# self._async_cancel_cycle = async_call_later(
# self.hass,
# on_time_sec,
# _turn_on_later,
# )
await _turn_on_later(None) await _turn_on_later(None)
# # Program turn off
# self._async_cancel_cycle = async_call_later(
# self.hass,
# on_time_sec,
# _turn_off_later,
# )
elif self._is_device_active: elif self._is_device_active:
_LOGGER.info( _LOGGER.info(
@@ -1446,10 +1452,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"tpi_coef_ext": self._tpi_coef_ext, "tpi_coef_ext": self._tpi_coef_ext,
"saved_preset_mode": self._saved_preset_mode, "saved_preset_mode": self._saved_preset_mode,
"saved_target_temp": self._saved_target_temp, "saved_target_temp": self._saved_target_temp,
"saved_hvac_mode": self._saved_hvac_mode,
"window_state": self._window_state, "window_state": self._window_state,
"motion_state": self._motion_state, "motion_state": self._motion_state,
"overpowering_state": self._overpowering_state, "overpowering_state": self._overpowering_state,
"presence_state": self._presence_state, "presence_state": self._presence_state,
"security_delay_min": self._security_delay_min,
"last_temperature_datetime": self._last_temperature_mesure.isoformat(),
"last_ext_temperature_datetime": self._last_ext_temperature_mesure.isoformat(),
"security_state": self._security_state,
"minimal_activation_delay_sec": self._minimal_activation_delay,
"last_update_datetime": datetime.now().isoformat(), "last_update_datetime": datetime.now().isoformat(),
} }
self.async_write_ha_state() self.async_write_ha_state()
@@ -1513,3 +1525,26 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if self._attr_preset_mode == preset: if self._attr_preset_mode == preset:
await self._async_set_preset_mode_internal(preset, force=True) await self._async_set_preset_mode_internal(preset, force=True)
await self._async_control_heating(force=True) await self._async_control_heating(force=True)
@classmethod
def add_entity(cls, entry_id, entity):
"""Adds an entity into the VersatileRegistry entities"""
_LOGGER.debug("Adding entity %s", entry_id)
cls._registry[entry_id] = entity
_LOGGER.debug("Entity registry is now %s", cls._registry)
@classmethod
async def update_entity(cls, entry_id, infos):
"""Updates an existing entity referenced by entry_id with the infos in arguments"""
entity: VersatileThermostat = cls._registry.get(entry_id)
if entity is None:
_LOGGER.warning(
"Tries to update VersatileThermostat entity %s but was not found in thermostat registry",
entry_id,
)
return
_LOGGER.debug("We have found the entity to update")
entity.post_init(infos)
await entity.async_added_to_hass()

View File

@@ -3,9 +3,9 @@ from __future__ import annotations
import logging import logging
import copy import copy
from collections.abc import Mapping
import voluptuous as vol import voluptuous as vol
from collections.abc import Mapping
from typing import Any from typing import Any
from homeassistant.core import callback from homeassistant.core import callback
@@ -14,6 +14,8 @@ from homeassistant.config_entries import (
ConfigFlow as HAConfigFlow, ConfigFlow as HAConfigFlow,
OptionsFlow, OptionsFlow,
) )
# import homeassistant.helpers.entity_registry as entity_registry
from homeassistant.data_entry_flow import FlowHandler from homeassistant.data_entry_flow import FlowHandler
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@@ -45,8 +47,14 @@ from .const import (
CONF_TPI_COEF_INT, CONF_TPI_COEF_INT,
CONF_PRESENCE_SENSOR, CONF_PRESENCE_SENSOR,
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
CONF_SECURITY_DELAY_MIN,
CONF_MINIMAL_ACTIVATION_DELAY,
CONF_TEMP_MAX,
CONF_TEMP_MIN,
) )
from .climate import VersatileThermostat
# from .climate import VersatileThermostat # from .climate import VersatileThermostat
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -117,6 +125,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
] ]
), ),
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
} }
) )
@@ -174,6 +184,15 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
} }
) )
self.STEP_ADVANCED_DATA_SCHEMA = vol.Schema(
{
vol.Required(
CONF_MINIMAL_ACTIVATION_DELAY, default=10
): cv.positive_int,
vol.Required(CONF_SECURITY_DELAY_MIN, default=60): cv.positive_int,
}
)
async def validate_input(self, data: dict) -> dict[str]: async def validate_input(self, data: dict) -> dict[str]:
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
@@ -303,6 +322,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"presence", "presence",
self.STEP_PRESENCE_DATA_SCHEMA, self.STEP_PRESENCE_DATA_SCHEMA,
user_input, user_input,
self.async_step_advanced,
)
async def async_step_advanced(self, user_input: dict | None = None) -> FlowResult:
"""Handle the advanced parameter flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_advanced user_input=%s", user_input)
return await self.generic_step(
"advanced",
self.STEP_ADVANCED_DATA_SCHEMA,
user_input,
self.async_finalize, # pylint: disable=no-member self.async_finalize, # pylint: disable=no-member
) )
@@ -417,7 +447,7 @@ class VersatileThermostatOptionsFlowHandler(
"power", "power",
self.STEP_POWER_DATA_SCHEMA, self.STEP_POWER_DATA_SCHEMA,
user_input, user_input,
self.async_step_presence, # pylint: disable=no-member self.async_step_presence,
) )
async def async_step_presence(self, user_input: dict | None = None) -> FlowResult: async def async_step_presence(self, user_input: dict | None = None) -> FlowResult:
@@ -430,13 +460,52 @@ class VersatileThermostatOptionsFlowHandler(
"presence", "presence",
self.STEP_PRESENCE_DATA_SCHEMA, self.STEP_PRESENCE_DATA_SCHEMA,
user_input, user_input,
self.async_finalize, # pylint: disable=no-member self.async_step_advanced,
) )
async def async_finalize(self): async def async_step_advanced(self, user_input: dict | None = None) -> FlowResult:
"""Handle the advanced flow steps"""
_LOGGER.debug(
"Into OptionsFlowHandler.async_step_advanced user_input=%s", user_input
)
return await self.generic_step(
"advanced",
self.STEP_ADVANCED_DATA_SCHEMA,
user_input,
self.async_end,
)
async def async_end(self):
"""Finalization of the ConfigEntry creation""" """Finalization of the ConfigEntry creation"""
_LOGGER.debug( _LOGGER.debug(
"CTOR ConfigFlow.async_finalize - updating entry with: %s", self._infos "CTOR ConfigFlow.async_finalize - updating entry with: %s", self._infos
) )
# Find eventual existing entity to update it
# removing entities from registry (they will be recreated)
# ent_reg = entity_registry.async_get(self.hass)
# reg_entities = {
# ent.unique_id: ent.entity_id
# for ent in entity_registry.async_entries_for_config_entry(
# ent_reg, self.config_entry.entry_id
# )
# }
#
# for entry in entity_registry.async_entries_for_config_entry(
# ent_reg, self.config_entry.entry_id
# ):
# entity: VersatileThermostat = ent_reg.async_get(entry.entity_id)
# entity.async_registry_entry_updated(self._infos)
_LOGGER.debug(
"We have found entities to update: %s", self.config_entry.entry_id
)
await VersatileThermostat.update_entity(self.config_entry.entry_id, self._infos)
# for entity_id in reg_entities.values():
# _LOGGER.info("Recreating entity %s due to configuration change", entity_id)
# ent_reg.async_remove(entity_id)
#
self.hass.config_entries.async_update_entry(self.config_entry, data=self._infos) self.hass.config_entries.async_update_entry(self.config_entry, data=self._infos)
return self.async_create_entry(title=None, data=None) return self.async_create_entry(title=None, data=None)

View File

@@ -14,6 +14,9 @@ from .prop_algorithm import (
) )
PRESET_POWER = "power" PRESET_POWER = "power"
PRESET_SECURITY = "security"
HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
DOMAIN = "versatile_thermostat" DOMAIN = "versatile_thermostat"
@@ -35,6 +38,10 @@ CONF_TPI_COEF_INT = "tpi_coef_int"
CONF_TPI_COEF_EXT = "tpi_coef_ext" CONF_TPI_COEF_EXT = "tpi_coef_ext"
CONF_PRESENCE_SENSOR = "presence_sensor_entity_id" CONF_PRESENCE_SENSOR = "presence_sensor_entity_id"
CONF_PRESET_POWER = "power_temp" CONF_PRESET_POWER = "power_temp"
CONF_MINIMAL_ACTIVATION_DELAY = "minimal_activation_delay"
CONF_TEMP_MIN = "temp_min"
CONF_TEMP_MAX = "temp_max"
CONF_SECURITY_DELAY_MIN = "security_delay_min"
CONF_PRESETS = { CONF_PRESETS = {
p: f"{p}_temp" p: f"{p}_temp"
@@ -81,6 +88,10 @@ ALL_CONF = (
CONF_TPI_COEF_INT, CONF_TPI_COEF_INT,
CONF_TPI_COEF_EXT, CONF_TPI_COEF_EXT,
CONF_PRESENCE_SENSOR, CONF_PRESENCE_SENSOR,
CONF_MINIMAL_ACTIVATION_DELAY,
CONF_TEMP_MIN,
CONF_TEMP_MAX,
CONF_SECURITY_DELAY_MIN,
] ]
+ CONF_PRESETS_VALUES + CONF_PRESETS_VALUES
+ CONF_PRESETS_AWAY_VALUES, + CONF_PRESETS_AWAY_VALUES,

View File

@@ -1,5 +1,4 @@
import logging import logging
import math
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -21,20 +20,22 @@ class PropAlgorithm:
tpi_coef_int, tpi_coef_int,
tpi_coef_ext, tpi_coef_ext,
cycle_min: int, cycle_min: int,
minimal_activation_delay: int,
): ):
"""Initialisation of the Proportional Algorithm""" """Initialisation of the Proportional Algorithm"""
_LOGGER.debug( _LOGGER.debug(
"Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d", "Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d, minimal_activation_delay:%d",
function_type, function_type,
tpi_coef_int, tpi_coef_int,
tpi_coef_ext, tpi_coef_ext,
cycle_min, cycle_min,
minimal_activation_delay,
) )
# TODO test function_type, bias, cycle_min
self._function = function_type self._function = function_type
self._tpi_coef_int = tpi_coef_int self._tpi_coef_int = tpi_coef_int
self._tpi_coef_ext = tpi_coef_ext self._tpi_coef_ext = tpi_coef_ext
self._cycle_min = cycle_min self._cycle_min = cycle_min
self._minimal_activation_delay = minimal_activation_delay
self._on_percent = 0 self._on_percent = 0
self._on_time_sec = 0 self._on_time_sec = 0
self._off_time_sec = self._cycle_min * 60 self._off_time_sec = self._cycle_min * 60
@@ -74,12 +75,19 @@ class PropAlgorithm:
self._on_time_sec = self._on_percent * self._cycle_min * 60 self._on_time_sec = self._on_percent * self._cycle_min * 60
# Do not heat for less than xx sec # Do not heat for less than xx sec
if self._on_time_sec < PROPORTIONAL_MIN_DURATION_SEC: if self._on_time_sec < self._minimal_activation_delay:
_LOGGER.debug( if self._on_time_sec > 0:
"No heating period due to heating period too small (%f < %f)", _LOGGER.info(
self._on_time_sec, "No heating period due to heating period too small (%f < %f)",
PROPORTIONAL_MIN_DURATION_SEC, self._on_time_sec,
) self._minimal_activation_delay,
)
else:
_LOGGER.debug(
"No heating period due to heating period too small (%f < %f)",
self._on_time_sec,
self._minimal_activation_delay,
)
self._on_time_sec = 0 self._on_time_sec = 0
self._off_time_sec = self._cycle_min * 60 - self._on_time_sec self._off_time_sec = self._cycle_min * 60 - self._on_time_sec

View File

@@ -3,7 +3,6 @@ set_presence:
description: Force the presence mode in thermostat description: Force the presence mode in thermostat
target: target:
entity: entity:
multiple: true
integration: versatile_thermostat integration: versatile_thermostat
fields: fields:
presence: presence:
@@ -26,7 +25,6 @@ set_preset_temperature:
description: Change the target temperature of a preset description: Change the target temperature of a preset
target: target:
entity: entity:
multiple: true
integration: versatile_thermostat integration: versatile_thermostat
fields: fields:
preset: preset:

View File

@@ -12,7 +12,9 @@
"temperature_sensor_entity_id": "Temperature sensor entity id", "temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "External temperature sensor entity id", "external_temperature_sensor_entity_id": "External temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"proportional_function": "Algorithm to use (TPI is the only one for now)" "proportional_function": "Algorithm to use (TPI is the only one for now)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed"
} }
}, },
"tpi": { "tpi": {
@@ -69,6 +71,14 @@
"comfort_away_temp": "Temperature in Comfort preset when no presence", "comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence" "boost_away_temp": "Temperature in Boost preset when no presence"
} }
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"data": {
"minimal_activation_delay": "Delay in secondes under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state"
}
} }
}, },
"error": { "error": {
@@ -91,7 +101,9 @@
"temperature_sensor_entity_id": "Temperature sensor entity id", "temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "External temperature sensor entity id", "external_temperature_sensor_entity_id": "External temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"proportional_function": "Algorithm to use (TPI is the only one for now)" "proportional_function": "Algorithm to use (TPI is the only one for now)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed"
} }
}, },
"tpi": { "tpi": {
@@ -148,6 +160,14 @@
"comfort_away_temp": "Temperature in Comfort preset when no presence", "comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence" "boost_away_temp": "Temperature in Boost preset when no presence"
} }
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"data": {
"minimal_activation_delay": "Delay in secondes under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state"
}
} }
}, },
"error": { "error": {

View File

@@ -12,7 +12,9 @@
"temperature_sensor_entity_id": "Temperature sensor entity id", "temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "External temperature sensor entity id", "external_temperature_sensor_entity_id": "External temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"proportional_function": "Algorithm to use (TPI is the only one for now)" "proportional_function": "Algorithm to use (TPI is the only one for now)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed"
} }
}, },
"tpi": { "tpi": {
@@ -69,6 +71,14 @@
"comfort_away_temp": "Temperature in Comfort preset when no presence", "comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence" "boost_away_temp": "Temperature in Boost preset when no presence"
} }
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"data": {
"minimal_activation_delay": "Delay in secondes under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state"
}
} }
}, },
"error": { "error": {
@@ -91,7 +101,9 @@
"temperature_sensor_entity_id": "Temperature sensor entity id", "temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "External temperature sensor entity id", "external_temperature_sensor_entity_id": "External temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"proportional_function": "Algorithm to use (TPI is the only one for now)" "proportional_function": "Algorithm to use (TPI is the only one for now)",
"temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed"
} }
}, },
"tpi": { "tpi": {
@@ -148,6 +160,14 @@
"comfort_away_temp": "Temperature in Comfort preset when no presence", "comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence" "boost_away_temp": "Temperature in Boost preset when no presence"
} }
},
"advanced": {
"title": "Advanced parameters",
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"data": {
"minimal_activation_delay": "Delay in secondes under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state"
}
} }
}, },
"error": { "error": {

View File

@@ -12,7 +12,9 @@
"temperature_sensor_entity_id": "Température sensor entity id", "temperature_sensor_entity_id": "Température sensor entity id",
"external_temperature_sensor_entity_id": "Temperature exterieure sensor entity id", "external_temperature_sensor_entity_id": "Temperature exterieure sensor entity id",
"cycle_min": "Durée du cycle (minutes)", "cycle_min": "Durée du cycle (minutes)",
"proportional_function": "Algorithm à utiliser (Seul TPI est disponible pour l'instant)" "proportional_function": "Algorithm à utiliser (Seul TPI est disponible pour l'instant)",
"temp_min": "Température minimale permise",
"temp_max": "Température maximale permise"
} }
}, },
"tpi": { "tpi": {
@@ -69,6 +71,14 @@
"comfort_away_temp": "Température en preset Comfort en cas d'absence", "comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence" "boost_away_temp": "Température en preset Boost en cas d'absence"
} }
},
"advanced": {
"title": "Parameters avancés",
"description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.",
"data": {
"minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé",
"security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position éteinte de sécurité"
}
} }
}, },
"error": { "error": {
@@ -91,7 +101,9 @@
"temperature_sensor_entity_id": "Température sensor entity id", "temperature_sensor_entity_id": "Température sensor entity id",
"external_temperature_sensor_entity_id": "Temperature exterieure sensor entity id", "external_temperature_sensor_entity_id": "Temperature exterieure sensor entity id",
"cycle_min": "Durée du cycle (minutes)", "cycle_min": "Durée du cycle (minutes)",
"proportional_function": "Algorithm à utiliser (Seul TPI est disponible pour l'instant)" "proportional_function": "Algorithm à utiliser (Seul TPI est disponible pour l'instant)",
"temp_min": "Température minimale permise",
"temp_max": "Température maximale permise"
} }
}, },
"tpi": { "tpi": {
@@ -148,6 +160,14 @@
"comfort_away_temp": "Température en preset Comfort en cas d'absence", "comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence" "boost_away_temp": "Température en preset Boost en cas d'absence"
} }
},
"advanced": {
"title": "Parameters avancés",
"description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.",
"data": {
"minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé",
"security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position éteinte de sécurité"
}
} }
}, },
"error": { "error": {