Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
018343ec53 | ||
|
|
89984b3dfc | ||
|
|
5fb148d445 | ||
|
|
62a5e05a7a | ||
|
|
a6a5fd38a0 | ||
|
|
44174f23eb |
@@ -13,8 +13,14 @@ input_number:
|
||||
name: Temperature
|
||||
min: 0
|
||||
max: 35
|
||||
step: 1
|
||||
step: .1
|
||||
icon: mdi:thermometer
|
||||
fake_external_temperature_sensor1:
|
||||
name: Ext Temperature
|
||||
min: -10
|
||||
max: 35
|
||||
step: .1
|
||||
icon: mdi:home-thermometer
|
||||
fake_current_power:
|
||||
name: Current power
|
||||
min: 0
|
||||
@@ -36,9 +42,19 @@ input_boolean:
|
||||
icon: mdi:window-closed-variant
|
||||
# input_boolean to simulate the heater entity switch. Only for development environment.
|
||||
fake_heater_switch1:
|
||||
name: Heater 1
|
||||
name: Heater 1 (Linear)
|
||||
icon: mdi:radiator
|
||||
fake_heater_switch2:
|
||||
name: Heater (TPI with presence preset)
|
||||
icon: mdi:radiator
|
||||
fake_heater_switch3:
|
||||
name: Heater (TPI with offset)
|
||||
icon: mdi:radiator
|
||||
# input_boolean to simulate the motion sensor entity. Only for development environment.
|
||||
fake_motion_sensor1:
|
||||
name: Motion Sensor 1
|
||||
icon: mdi:run
|
||||
# input_boolean to simulate the presence sensor entity. Only for development environment.
|
||||
fake_presence_sensor1:
|
||||
name: Presence Sensor 1
|
||||
icon: mdi:home
|
||||
|
||||
@@ -9,9 +9,10 @@ _Component developed by using the amazing development template [blueprint][bluep
|
||||
This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
|
||||
|
||||
## When to use / not use
|
||||
This thermostat aims to command a heater which works only in on/off mode. This thermostat is not suitable for heaters with a pilot wire. The minimal needed configuration to use this thermostat is:
|
||||
This thermostat aims to command a heater which works only in on/off mode. The minimal needed configuration to use this thermostat is:
|
||||
1. an equipement like a heater (a switch),
|
||||
2. a temperature sensor (or an input_number)
|
||||
2. a temperature sensor for the room (or an input_number),
|
||||
3. an external temperature sensor (think of the meteo integration if you don't have one)
|
||||
|
||||
Because this integration aims to command the heater considering the preset configured and the room temperature, those informations are mandatory.
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import math
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
from typing import Any, Mapping
|
||||
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
@@ -10,7 +11,7 @@ from homeassistant.core import (
|
||||
DOMAIN as HA_DOMAIN,
|
||||
CALLBACK_TYPE,
|
||||
)
|
||||
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity
|
||||
from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -26,7 +27,7 @@ from homeassistant.helpers import condition
|
||||
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_FAN_MODE,
|
||||
# ATTR_FAN_MODE,
|
||||
CURRENT_HVAC_COOL,
|
||||
CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_IDLE,
|
||||
@@ -39,20 +40,20 @@ from homeassistant.components.climate.const import (
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
PRESET_HOME,
|
||||
# PRESET_HOME,
|
||||
PRESET_NONE,
|
||||
PRESET_SLEEP,
|
||||
# PRESET_SLEEP,
|
||||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
# SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
|
||||
from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
# UnitOfTemperature,
|
||||
ATTR_TEMPERATURE,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
# TEMP_FAHRENHEIT,
|
||||
CONF_NAME,
|
||||
CONF_UNIQUE_ID,
|
||||
# CONF_UNIQUE_ID,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
STATE_OFF,
|
||||
@@ -64,10 +65,11 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
# DOMAIN,
|
||||
CONF_HEATER,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_WINDOW_DELAY,
|
||||
@@ -80,8 +82,14 @@ from .const import (
|
||||
CONF_CYCLE_MIN,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_PROP_BIAS,
|
||||
CONF_TPI_COEF_C,
|
||||
CONF_TPI_COEF_T,
|
||||
CONF_PRESENCE_SENSOR,
|
||||
CONF_NO_PRESENCE_PRESET,
|
||||
CONF_NO_PRESENCE_TEMP_OFFSET,
|
||||
SUPPORT_FLAGS,
|
||||
PRESET_POWER,
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
)
|
||||
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
@@ -106,6 +114,7 @@ async def async_setup_entry(
|
||||
proportional_function = entry.data.get(CONF_PROP_FUNCTION)
|
||||
proportional_bias = entry.data.get(CONF_PROP_BIAS)
|
||||
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)
|
||||
@@ -115,6 +124,11 @@ async def async_setup_entry(
|
||||
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_coefc = entry.data.get(CONF_TPI_COEF_C)
|
||||
tpi_coeft = entry.data.get(CONF_TPI_COEF_T)
|
||||
presence_sensor_entity_id = entry.data.get(CONF_PRESENCE_SENSOR)
|
||||
no_presence_preset = entry.data.get(CONF_NO_PRESENCE_PRESET)
|
||||
no_presence_offset = entry.data.get(CONF_NO_PRESENCE_TEMP_OFFSET)
|
||||
|
||||
presets = {}
|
||||
for (key, value) in CONF_PRESETS.items():
|
||||
@@ -127,6 +141,7 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
[
|
||||
VersatileThermostat(
|
||||
hass,
|
||||
unique_id,
|
||||
name,
|
||||
heater_entity_id,
|
||||
@@ -134,6 +149,7 @@ async def async_setup_entry(
|
||||
proportional_function,
|
||||
proportional_bias,
|
||||
temp_sensor_entity_id,
|
||||
ext_temp_sensor_entity_id,
|
||||
power_sensor_entity_id,
|
||||
max_power_sensor_entity_id,
|
||||
window_sensor_entity_id,
|
||||
@@ -144,6 +160,11 @@ async def async_setup_entry(
|
||||
no_motion_preset,
|
||||
presets,
|
||||
device_power,
|
||||
tpi_coefc,
|
||||
tpi_coeft,
|
||||
presence_sensor_entity_id,
|
||||
no_presence_preset,
|
||||
no_presence_offset,
|
||||
)
|
||||
],
|
||||
True,
|
||||
@@ -160,6 +181,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
unique_id,
|
||||
name,
|
||||
heater_entity_id,
|
||||
@@ -167,6 +189,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
proportional_function,
|
||||
proportional_bias,
|
||||
temp_sensor_entity_id,
|
||||
ext_temp_sensor_entity_id,
|
||||
power_sensor_entity_id,
|
||||
max_power_sensor_entity_id,
|
||||
window_sensor_entity_id,
|
||||
@@ -177,11 +200,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
no_motion_preset,
|
||||
presets,
|
||||
device_power,
|
||||
tpi_coefc,
|
||||
tpi_coeft,
|
||||
presence_sensor_entity_id,
|
||||
no_presence_preset,
|
||||
no_presence_offset,
|
||||
) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self._hass = hass
|
||||
self._attr_extra_state_attributes = {}
|
||||
|
||||
self._unique_id = unique_id
|
||||
self._name = name
|
||||
self._heater_entity_id = heater_entity_id
|
||||
@@ -189,6 +220,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._proportional_function = proportional_function
|
||||
self._proportional_bias = proportional_bias
|
||||
self._temp_sensor_entity_id = temp_sensor_entity_id
|
||||
self._ext_temp_sensor_entity_id = ext_temp_sensor_entity_id
|
||||
self._power_sensor_entity_id = power_sensor_entity_id
|
||||
self._max_power_sensor_entity_id = max_power_sensor_entity_id
|
||||
self._window_sensor_entity_id = window_sensor_entity_id
|
||||
@@ -198,6 +230,22 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._motion_delay_sec = motion_delay_sec
|
||||
self._motion_preset = motion_preset
|
||||
self._no_motion_preset = no_motion_preset
|
||||
self._tpi_coefc = tpi_coefc
|
||||
self._tpi_coeft = tpi_coeft
|
||||
self._presence_sensor_entity_id = presence_sensor_entity_id
|
||||
self._no_presence_preset = no_presence_preset
|
||||
self._no_presence_offset = no_presence_offset
|
||||
|
||||
self._presence_on = self._presence_sensor_entity_id and (
|
||||
self._no_presence_preset is not None or self._no_presence_offset is not None
|
||||
)
|
||||
if self._presence_on:
|
||||
if self._no_presence_preset is not None:
|
||||
self._no_presence_offset = 0
|
||||
else:
|
||||
self._no_presence_preset = None
|
||||
self._no_presence_offset = 0
|
||||
_LOGGER.info("%s - Presence management is not fully configured.", self)
|
||||
|
||||
# TODO if self.ac_mode:
|
||||
# self.hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF]
|
||||
@@ -226,16 +274,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
# Power management
|
||||
self._device_power = device_power
|
||||
self._pmax_on = False
|
||||
self._current_power = None
|
||||
self._current_power_max = None
|
||||
if (
|
||||
self._max_power_sensor_entity_id
|
||||
and self._power_sensor_entity_id
|
||||
and self._device_power
|
||||
):
|
||||
self._pmax_on = True
|
||||
self._current_power = -1
|
||||
self._current_power_max = -1
|
||||
self._current_power = 0
|
||||
self._current_power_max = 0
|
||||
else:
|
||||
self._pmax_on = False
|
||||
_LOGGER.info("%s - Power management is not fully configured.", self)
|
||||
|
||||
# will be restored if possible
|
||||
self._target_temp = None
|
||||
@@ -245,16 +296,39 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._fan_mode = None
|
||||
self._swing_mode = None
|
||||
self._cur_temp = None
|
||||
self._cur_ext_temp = None
|
||||
|
||||
# Fix parameters for TPI
|
||||
if (
|
||||
self._proportional_function == PROPORTIONAL_FUNCTION_TPI
|
||||
and self._ext_temp_sensor_entity_id is None
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"Using TPI function but not external temperature sensor is set. Removing the delta temp ext factor. Thermostat will not be fully operationnal" # pylint: disable=line-too-long
|
||||
)
|
||||
self._tpi_coeft = 0
|
||||
|
||||
# Initiate the ProportionalAlgorithm
|
||||
self._prop_algorithm = PropAlgorithm(
|
||||
self._proportional_function, self._proportional_bias, self._cycle_min
|
||||
self._proportional_function,
|
||||
self._proportional_bias,
|
||||
self._tpi_coefc,
|
||||
self._tpi_coeft,
|
||||
self._cycle_min,
|
||||
)
|
||||
|
||||
self._async_cancel_cycle = None
|
||||
self._window_call_cancel = None
|
||||
self._motion_call_cancel = None
|
||||
|
||||
self._should_relaunch_control_heating = False
|
||||
|
||||
# Memory synthesis state
|
||||
self._motion_state = None
|
||||
self._window_state = None
|
||||
self._overpowering_state = None
|
||||
self._presence_state = None
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Creation of a new VersatileThermostat entity: unique_id=%s heater_entity_id=%s",
|
||||
self,
|
||||
@@ -340,9 +414,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
await self._async_control_heating()
|
||||
elif hvac_mode == HVAC_MODE_OFF:
|
||||
self._hvac_mode = HVAC_MODE_OFF
|
||||
# TODO self.prop_current_phase = PROP_PHASE_NONE
|
||||
# if self._is_device_active:
|
||||
# await self._async_heater_turn_off()
|
||||
if self._is_device_active:
|
||||
await self._async_heater_turn_off()
|
||||
else:
|
||||
_LOGGER.error("Unrecognized hvac mode: %s", hvac_mode)
|
||||
return
|
||||
@@ -359,7 +432,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.info("%s - Set preset_mode: %s", self, preset_mode)
|
||||
if preset_mode not in (self._attr_preset_modes or []):
|
||||
raise ValueError(
|
||||
f"Got unsupported preset_mode {preset_mode}. Must be one of {self._attr_preset_modes}"
|
||||
f"Got unsupported preset_mode {preset_mode}. Must be one of {self._attr_preset_modes}" # pylint: disable=line-too-long
|
||||
)
|
||||
|
||||
if preset_mode == self._attr_preset_mode:
|
||||
@@ -371,18 +444,20 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._target_temp = self._saved_target_temp
|
||||
elif preset_mode == PRESET_ACTIVITY:
|
||||
self._attr_preset_mode = PRESET_ACTIVITY
|
||||
self._target_temp = self._presets[self._no_motion_preset]
|
||||
self._update_motion_temp()
|
||||
else:
|
||||
if self._attr_preset_mode == PRESET_NONE:
|
||||
self._saved_target_temp = self._target_temp
|
||||
self._attr_preset_mode = preset_mode
|
||||
self._target_temp = self._presets[preset_mode]
|
||||
|
||||
if preset_mode != PRESET_POWER:
|
||||
# Don't saved preset_mode if we are in POWER mode or in Away mode and presence detection is on
|
||||
if preset_mode != PRESET_POWER and (
|
||||
not self._presence_on or preset_mode != self._no_presence_preset
|
||||
):
|
||||
self._saved_preset_mode = self._attr_preset_mode
|
||||
|
||||
self.async_write_ha_state()
|
||||
self._prop_algorithm.calculate(self._target_temp, self._cur_temp)
|
||||
self.recalculate()
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
@@ -415,12 +490,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
return
|
||||
self._target_temp = temperature
|
||||
self._attr_preset_mode = PRESET_NONE
|
||||
self._prop_algorithm.calculate(self._target_temp, self._cur_temp)
|
||||
self.async_write_ha_state()
|
||||
self.recalculate()
|
||||
|
||||
@callback
|
||||
async def entry_update_listener(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry
|
||||
self, _, config_entry: ConfigEntry # hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Called when the entry have changed in ConfigFlow"""
|
||||
_LOGGER.info("%s - Change entry with the values: %s", self, config_entry.data)
|
||||
@@ -445,6 +519,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._async_temperature_changed,
|
||||
)
|
||||
)
|
||||
|
||||
if self._ext_temp_sensor_entity_id:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._ext_temp_sensor_entity_id],
|
||||
self._async_ext_temperature_changed,
|
||||
)
|
||||
)
|
||||
|
||||
if self._window_sensor_entity_id:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
@@ -462,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:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
@@ -489,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()
|
||||
|
||||
# starts the cycle
|
||||
if self._cycle_min:
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._async_control_heating,
|
||||
interval=timedelta(minutes=self._cycle_min),
|
||||
)
|
||||
)
|
||||
|
||||
async def async_startup(self):
|
||||
"""Triggered on startup, used to get old state and set internal states accordingly"""
|
||||
_LOGGER.debug("%s - Calling async_startup", self)
|
||||
@@ -512,6 +606,31 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._async_update_temp(temperature_state)
|
||||
need_write_state = True
|
||||
|
||||
if self._ext_temp_sensor_entity_id:
|
||||
ext_temperature_state = self.hass.states.get(
|
||||
self._ext_temp_sensor_entity_id
|
||||
)
|
||||
if ext_temperature_state and ext_temperature_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - external temperature sensor have been retrieved: %.1f",
|
||||
self,
|
||||
float(ext_temperature_state.state),
|
||||
)
|
||||
self._async_update_ext_temp(ext_temperature_state)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - external temperature sensor have NOT been retrieved cause unknown or unavailable",
|
||||
self,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - external temperature sensor have NOT been retrieved cause no external sensor",
|
||||
self,
|
||||
)
|
||||
|
||||
switch_state = self.hass.states.get(self._heater_entity_id)
|
||||
if switch_state and switch_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
@@ -550,11 +669,62 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
need_write_state = True
|
||||
|
||||
# try to acquire window entity state
|
||||
if self._window_sensor_entity_id:
|
||||
window_state = self.hass.states.get(self._window_sensor_entity_id)
|
||||
if window_state and window_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
self._window_state = window_state.state
|
||||
_LOGGER.debug(
|
||||
"%s - Window state have been retrieved: %s",
|
||||
self,
|
||||
self._window_state,
|
||||
)
|
||||
need_write_state = True
|
||||
|
||||
# try to acquire motion entity state
|
||||
if self._motion_sensor_entity_id:
|
||||
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
|
||||
if motion_state and motion_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
self._motion_state = motion_state.state
|
||||
_LOGGER.debug(
|
||||
"%s - Motion state have been retrieved: %s",
|
||||
self,
|
||||
self._motion_state,
|
||||
)
|
||||
# recalculate the right target_temp in activity mode
|
||||
self._update_motion_temp()
|
||||
need_write_state = True
|
||||
|
||||
if self._presence_on:
|
||||
# try to acquire presence entity state
|
||||
presence_state = self.hass.states.get(self._presence_sensor_entity_id)
|
||||
if presence_state and presence_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
self._update_presence(presence_state.state)
|
||||
_LOGGER.debug(
|
||||
"%s - Presence have been retrieved: %s",
|
||||
self,
|
||||
presence_state.state,
|
||||
)
|
||||
need_write_state = True
|
||||
|
||||
if need_write_state:
|
||||
self.async_write_ha_state()
|
||||
self._prop_algorithm.calculate(self._target_temp, self._cur_temp)
|
||||
self._prop_algorithm.calculate(
|
||||
self._target_temp, self._cur_temp, self._cur_ext_temp
|
||||
)
|
||||
self.hass.create_task(self._async_control_heating())
|
||||
|
||||
await self.get_my_previous_state()
|
||||
|
||||
if self.hass.state == CoreState.running:
|
||||
_async_startup_internal()
|
||||
else:
|
||||
@@ -562,8 +732,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
EVENT_HOMEASSISTANT_START, _async_startup_internal
|
||||
)
|
||||
|
||||
await self.get_my_previous_state()
|
||||
|
||||
async def get_my_previous_state(self):
|
||||
"""Try to get my previou state"""
|
||||
# Check If we have an old state
|
||||
@@ -590,10 +758,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
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._saved_preset_mode = self._attr_preset_mode
|
||||
|
||||
if not self._hvac_mode and old_state.state:
|
||||
self._hvac_mode = old_state.state
|
||||
|
||||
# is done in startup above
|
||||
# self._prop_algorithm.calculate(
|
||||
# self._target_temp, self._cur_temp, self._cur_ext_temp
|
||||
# )
|
||||
|
||||
else:
|
||||
# No previous state, try and restore defaults
|
||||
if self._target_temp is None:
|
||||
@@ -632,8 +806,21 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
return
|
||||
|
||||
self._async_update_temp(new_state)
|
||||
self._prop_algorithm.calculate(self._target_temp, self._cur_temp)
|
||||
self.async_write_ha_state()
|
||||
self.recalculate()
|
||||
|
||||
async def _async_ext_temperature_changed(self, event):
|
||||
"""Handle external temperature changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
_LOGGER.info(
|
||||
"%s - external Temperature changed. Event.new_state is %s",
|
||||
self,
|
||||
new_state,
|
||||
)
|
||||
if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return
|
||||
|
||||
self._async_update_ext_temp(new_state)
|
||||
self.recalculate()
|
||||
|
||||
@callback
|
||||
async def _async_windows_changed(self, event):
|
||||
@@ -672,14 +859,15 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
if not self._saved_hvac_mode:
|
||||
self._saved_hvac_mode = self._hvac_mode
|
||||
|
||||
if new_state.state == STATE_OFF:
|
||||
self._window_state = new_state.state
|
||||
if self._window_state == STATE_OFF:
|
||||
_LOGGER.info(
|
||||
"%s - Window is closed. Restoring hvac_mode '%s'",
|
||||
self,
|
||||
self._saved_hvac_mode,
|
||||
)
|
||||
await self.async_set_hvac_mode(self._saved_hvac_mode)
|
||||
elif new_state.state == STATE_ON:
|
||||
elif self._window_state == STATE_ON:
|
||||
_LOGGER.info(
|
||||
"%s - Window is open. Set hvac_mode to '%s'", self, HVAC_MODE_OFF
|
||||
)
|
||||
@@ -704,8 +892,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._attr_preset_mode,
|
||||
PRESET_ACTIVITY,
|
||||
)
|
||||
if self._attr_preset_mode != PRESET_ACTIVITY:
|
||||
return
|
||||
|
||||
if new_state is None or new_state.state not in (STATE_OFF, STATE_ON):
|
||||
return
|
||||
|
||||
@@ -728,19 +915,21 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
return
|
||||
|
||||
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
|
||||
new_preset = (
|
||||
self._motion_preset
|
||||
if new_state.state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
)
|
||||
_LOGGER.info(
|
||||
"%s - Motion condition have changes. New preset temp will be %s",
|
||||
self,
|
||||
new_preset,
|
||||
)
|
||||
self._target_temp = self._presets[new_preset]
|
||||
self._prop_algorithm.calculate(self._target_temp, self._cur_temp)
|
||||
self.async_write_ha_state()
|
||||
self._motion_state = new_state.state
|
||||
if self._attr_preset_mode == PRESET_ACTIVITY:
|
||||
new_preset = (
|
||||
self._motion_preset
|
||||
if self._motion_state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
)
|
||||
_LOGGER.info(
|
||||
"%s - Motion condition have changes. New preset temp will be %s",
|
||||
self,
|
||||
new_preset,
|
||||
)
|
||||
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
|
||||
self._target_temp = self._presets[new_preset]
|
||||
self.recalculate()
|
||||
|
||||
if self._motion_call_cancel:
|
||||
self._motion_call_cancel()
|
||||
@@ -782,6 +971,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update temperature from sensor: %s", ex)
|
||||
|
||||
@callback
|
||||
def _async_update_ext_temp(self, state):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
try:
|
||||
cur_ext_temp = float(state.state)
|
||||
if math.isnan(cur_ext_temp) or math.isinf(cur_ext_temp):
|
||||
raise ValueError(f"Sensor has illegal state {state.state}")
|
||||
self._cur_ext_temp = cur_ext_temp
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update external temperature from sensor: %s", ex)
|
||||
|
||||
@callback
|
||||
async def _async_power_changed(self, event):
|
||||
"""Handle power changes."""
|
||||
@@ -789,8 +989,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.debug(event)
|
||||
new_state = event.data.get("new_state")
|
||||
old_state = event.data.get("old_state")
|
||||
if new_state is None or (
|
||||
old_state is not None and new_state.state == old_state.state
|
||||
if (
|
||||
new_state is None
|
||||
or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
or (old_state is not None and new_state.state == old_state.state)
|
||||
):
|
||||
return
|
||||
|
||||
@@ -810,8 +1012,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.debug(event)
|
||||
new_state = event.data.get("new_state")
|
||||
old_state = event.data.get("old_state")
|
||||
if new_state is None or (
|
||||
old_state is not None and new_state.state == old_state.state
|
||||
if (
|
||||
new_state is None
|
||||
or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
or (old_state is not None and new_state.state == old_state.state)
|
||||
):
|
||||
return
|
||||
|
||||
@@ -824,6 +1028,111 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
|
||||
|
||||
@callback
|
||||
async def _async_presence_changed(self, event):
|
||||
"""Handle presence changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
_LOGGER.info(
|
||||
"%s - Presence changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s",
|
||||
self,
|
||||
new_state,
|
||||
self._attr_preset_mode,
|
||||
PRESET_ACTIVITY,
|
||||
)
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
self._update_presence(new_state.state)
|
||||
|
||||
def _update_presence(self, new_state):
|
||||
_LOGGER.debug("%s - Updating presence. New state is %s", self, new_state)
|
||||
self._presence_state = new_state
|
||||
if self._attr_preset_mode == PRESET_POWER or self._presence_on is False:
|
||||
return
|
||||
if new_state is None or new_state not in (STATE_OFF, STATE_ON):
|
||||
return
|
||||
|
||||
# Change temperature or preset
|
||||
if self._no_presence_preset:
|
||||
_LOGGER.debug("%s - presence change in preset mode", self)
|
||||
new_preset = None
|
||||
no_presence_preset = self._no_presence_preset
|
||||
if new_state == STATE_OFF:
|
||||
new_preset = no_presence_preset
|
||||
self._saved_preset_mode = self._attr_preset_mode
|
||||
_LOGGER.info(
|
||||
"%s - No one is at home. Set to preset %s (saved_preset is %s)",
|
||||
self,
|
||||
new_preset,
|
||||
self._saved_preset_mode,
|
||||
)
|
||||
elif self._attr_preset_mode == no_presence_preset:
|
||||
new_preset = self._saved_preset_mode
|
||||
_LOGGER.info(
|
||||
"%s - Someone is back home. Restoring preset to %s",
|
||||
self,
|
||||
new_preset,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - presence change ignored (not in %s preset or not ON)",
|
||||
self,
|
||||
no_presence_preset,
|
||||
)
|
||||
if new_preset:
|
||||
self.hass.create_task(self.async_set_preset_mode(new_preset))
|
||||
else:
|
||||
new_temp = None
|
||||
if new_state == STATE_OFF:
|
||||
self._saved_target_temp = self._target_temp
|
||||
_LOGGER.info(
|
||||
"%s - No one is at home. Apply offset to temperature %.2f (saved_target_temp is %.2f)",
|
||||
self,
|
||||
self._no_presence_offset,
|
||||
self._saved_target_temp,
|
||||
)
|
||||
new_temp = self._target_temp + self._no_presence_offset
|
||||
else:
|
||||
new_temp = self._saved_target_temp
|
||||
_LOGGER.info(
|
||||
"%s - Someone is back home. Restoring temperature to %.2f",
|
||||
self,
|
||||
self._saved_target_temp,
|
||||
)
|
||||
if new_temp is not None:
|
||||
_LOGGER.debug(
|
||||
"%s - presence change in temperature mode new_temp will be: %.2f",
|
||||
self,
|
||||
new_temp,
|
||||
)
|
||||
self._target_temp = new_temp
|
||||
self.recalculate()
|
||||
|
||||
def _update_motion_temp(self):
|
||||
"""Update the temperature considering the ACTIVITY preset and current motion state"""
|
||||
_LOGGER.debug(
|
||||
"%s - Calling _update_motion_temp preset_mode=%s, motion_state=%s",
|
||||
self,
|
||||
self._attr_preset_mode,
|
||||
self._motion_state,
|
||||
)
|
||||
if (
|
||||
self._motion_sensor_entity_id is None
|
||||
or self._attr_preset_mode != PRESET_ACTIVITY
|
||||
):
|
||||
return
|
||||
|
||||
self._target_temp = self._presets[
|
||||
self._motion_preset
|
||||
if self._motion_state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
]
|
||||
_LOGGER.debug(
|
||||
"%s - regarding motion, target_temp have been set to %.2f",
|
||||
self,
|
||||
self._target_temp,
|
||||
)
|
||||
|
||||
async def _async_heater_turn_on(self):
|
||||
"""Turn heater toggleable device on."""
|
||||
data = {ATTR_ENTITY_ID: self._heater_entity_id}
|
||||
@@ -853,19 +1162,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._current_power_max,
|
||||
self._device_power,
|
||||
)
|
||||
overpowering: bool = (
|
||||
self._overpowering_state = (
|
||||
self._current_power + self._device_power >= self._current_power_max
|
||||
)
|
||||
if overpowering:
|
||||
if self._overpowering_state:
|
||||
_LOGGER.warning(
|
||||
"%s - overpowering is detected. Heater preset will be set to 'power'",
|
||||
self,
|
||||
)
|
||||
await self._async_set_preset_mode_internal(PRESET_POWER)
|
||||
return overpowering
|
||||
return self._overpowering_state
|
||||
|
||||
# Check if we need to remove the POWER preset
|
||||
if self._attr_preset_mode == PRESET_POWER and not overpowering:
|
||||
if self._attr_preset_mode == PRESET_POWER and not self._overpowering_state:
|
||||
_LOGGER.warning(
|
||||
"%s - end of overpowering is detected. Heater preset will be restored to '%s'",
|
||||
self,
|
||||
@@ -881,10 +1190,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
overpowering: bool = await self.check_overpowering()
|
||||
if overpowering:
|
||||
_LOGGER.debug(
|
||||
"%s - The max power is exceeded. Heater will not be started. preset_mode is now '%s'",
|
||||
"%s - The max power is exceeded. Heater will not be started. preset_mode is now '%s'", # pylint: disable=line-too-long
|
||||
self,
|
||||
self._attr_preset_mode,
|
||||
)
|
||||
await self._async_heater_turn_off()
|
||||
return
|
||||
|
||||
on_time_sec: int = self._prop_algorithm.on_time_sec
|
||||
@@ -899,10 +1209,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
# Cancel eventual previous cycle if any
|
||||
if self._async_cancel_cycle is not None:
|
||||
_LOGGER.debug("Cancelling the previous cycle that was running")
|
||||
self._async_cancel_cycle()
|
||||
self._async_cancel_cycle = None
|
||||
await self._async_heater_turn_off()
|
||||
_LOGGER.debug(
|
||||
"%s - A previous cycle is alredy running -> waits for its end", self
|
||||
)
|
||||
self._should_relaunch_control_heating = True
|
||||
return
|
||||
# await self._async_cancel_cycle()
|
||||
# self._async_cancel_cycle = None
|
||||
# Don't turn off if we will turn on just after
|
||||
# if on_time_sec <= 0:
|
||||
# await self._async_heater_turn_off()
|
||||
|
||||
if self._hvac_mode == HVAC_MODE_HEAT and on_time_sec > 0:
|
||||
_LOGGER.info(
|
||||
@@ -915,14 +1231,22 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
await self._async_heater_turn_on()
|
||||
|
||||
async def _turn_off(_):
|
||||
_LOGGER.info(
|
||||
"%s - stop heating for %d min %d sec",
|
||||
self,
|
||||
off_time_sec // 60,
|
||||
off_time_sec % 60,
|
||||
)
|
||||
await self._async_heater_turn_off()
|
||||
self._async_cancel_cycle()
|
||||
self._async_cancel_cycle = None
|
||||
if self._should_relaunch_control_heating:
|
||||
_LOGGER.debug("Don't stop cause a cycle have to be relaunch")
|
||||
self._should_relaunch_control_heating = False
|
||||
await self._async_control_heating()
|
||||
return
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"%s - stop heating for %d min %d sec",
|
||||
self,
|
||||
off_time_sec // 60,
|
||||
off_time_sec % 60,
|
||||
)
|
||||
await self._async_heater_turn_off()
|
||||
self.update_custom_attributes()
|
||||
|
||||
# Program turn off
|
||||
self._async_cancel_cycle = async_call_later(
|
||||
@@ -931,6 +1255,72 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
_turn_off,
|
||||
)
|
||||
|
||||
elif self._is_device_active:
|
||||
_LOGGER.info(
|
||||
"%s - stop heating (2) for %d min %d sec",
|
||||
self,
|
||||
off_time_sec // 60,
|
||||
off_time_sec % 60,
|
||||
)
|
||||
await self._async_heater_turn_off()
|
||||
|
||||
else:
|
||||
_LOGGER.debug("%s - nothing to do", self)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
def recalculate(self):
|
||||
"""A utility function to force the calculation of a the algo and
|
||||
update the custom attributes and write the state
|
||||
"""
|
||||
_LOGGER.debug("%s - recalculate all", self)
|
||||
self._prop_algorithm.calculate(
|
||||
self._target_temp, self._cur_temp, self._cur_ext_temp
|
||||
)
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def update_custom_attributes(self):
|
||||
"""Update the custom extra attributes for the entity"""
|
||||
|
||||
self._attr_extra_state_attributes = {
|
||||
"away_temp": self._presets[PRESET_AWAY],
|
||||
"eco_temp": self._presets[PRESET_ECO],
|
||||
"boost_temp": self._presets[PRESET_BOOST],
|
||||
"comfort_temp": self._presets[PRESET_COMFORT],
|
||||
"power_temp": self._presets[PRESET_POWER],
|
||||
"on_percent": self._prop_algorithm.on_percent,
|
||||
"on_time_sec": self._prop_algorithm.on_time_sec,
|
||||
"off_time_sec": self._prop_algorithm.off_time_sec,
|
||||
"ext_current_temperature": self._cur_ext_temp,
|
||||
"current_power": self._current_power,
|
||||
"current_power_max": self._current_power_max,
|
||||
"cycle_min": self._cycle_min,
|
||||
"bias": self._proportional_bias,
|
||||
"function": self._proportional_function,
|
||||
"tpi_coefc": self._tpi_coefc,
|
||||
"tpi_coeft": self._tpi_coeft,
|
||||
"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(
|
||||
"%s - Calling update_custom_attributes: %s",
|
||||
self,
|
||||
self._attr_extra_state_attributes,
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_registry_entry_updated(self):
|
||||
"""update the entity if the config entry have been updated
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
"""Config flow for Versatile Thermostat integration."""
|
||||
from __future__ import annotations
|
||||
from homeassistant.core import callback
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow as HAConfigFlow,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.data_entry_flow import FlowHandler
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
@@ -24,6 +22,7 @@ from .const import (
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
@@ -34,14 +33,18 @@ from .const import (
|
||||
CONF_NO_MOTION_PRESET,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_CYCLE_MIN,
|
||||
ALL_CONF,
|
||||
CONF_PRESETS,
|
||||
CONF_PRESETS_SELECTIONABLE,
|
||||
CONF_FUNCTIONS,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_PROP_BIAS,
|
||||
CONF_TPI_COEF_T,
|
||||
CONF_TPI_COEF_C,
|
||||
CONF_PRESENCE_SENSOR,
|
||||
CONF_NO_PRESENCE_PRESET,
|
||||
CONF_NO_PRESENCE_TEMP_OFFSET,
|
||||
PROPORTIONAL_FUNCTION_ATAN,
|
||||
PROPORTIONAL_FUNCTION_LINEAR,
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
)
|
||||
|
||||
# from .climate import VersatileThermostat
|
||||
@@ -55,24 +58,49 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_TEMP_SENSOR): cv.string,
|
||||
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
|
||||
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_LINEAR): vol.In(
|
||||
[PROPORTIONAL_FUNCTION_LINEAR, PROPORTIONAL_FUNCTION_ATAN]
|
||||
[
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
PROPORTIONAL_FUNCTION_LINEAR,
|
||||
PROPORTIONAL_FUNCTION_ATAN,
|
||||
]
|
||||
),
|
||||
}
|
||||
)
|
||||
# USER_DATA_CONF = [
|
||||
# CONF_NAME,
|
||||
# CONF_HEATER,
|
||||
# CONF_TEMP_SENSOR,
|
||||
# CONF_EXTERNAL_TEMP_SENSOR,
|
||||
# CONF_CYCLE_MIN,
|
||||
# CONF_PROP_FUNCTION,
|
||||
# ]
|
||||
|
||||
STEP_P_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PROP_BIAS, default=0.25): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
USER_DATA_CONF = [
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_CYCLE_MIN,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_PROP_BIAS,
|
||||
]
|
||||
# P_DATA_CONF = [
|
||||
# CONF_PROP_BIAS,
|
||||
# ]
|
||||
|
||||
STEP_TPI_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_EXTERNAL_TEMP_SENSOR): cv.string,
|
||||
vol.Required(CONF_TPI_COEF_C, default=0.6): vol.Coerce(float),
|
||||
vol.Required(CONF_TPI_COEF_T, default=0.01): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
# TPI_DATA_CONF = [
|
||||
# CONF_EXTERNAL_TEMP_SENSOR,
|
||||
# CONF_TPI_COEF_C,
|
||||
# CONF_TPI_COEF_T,
|
||||
# ]
|
||||
|
||||
STEP_PRESETS_DATA_SCHEMA = vol.Schema(
|
||||
{vol.Optional(v, default=17): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()}
|
||||
)
|
||||
PRESETS_DATA_CONF = [v for (_, v) in CONF_PRESETS.items()]
|
||||
# PRESETS_DATA_CONF = [v for (_, v) in CONF_PRESETS.items()]
|
||||
|
||||
STEP_WINDOW_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -80,7 +108,7 @@ STEP_WINDOW_DATA_SCHEMA = vol.Schema(
|
||||
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(
|
||||
{
|
||||
@@ -94,12 +122,12 @@ STEP_MOTION_DATA_SCHEMA = vol.Schema(
|
||||
),
|
||||
}
|
||||
)
|
||||
MOTION_DATA_CONF = [
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_MOTION_DELAY,
|
||||
CONF_MOTION_PRESET,
|
||||
CONF_NO_MOTION_PRESET,
|
||||
]
|
||||
# MOTION_DATA_CONF = [
|
||||
# CONF_MOTION_SENSOR,
|
||||
# CONF_MOTION_DELAY,
|
||||
# CONF_MOTION_PRESET,
|
||||
# CONF_NO_MOTION_PRESET,
|
||||
# ]
|
||||
|
||||
STEP_POWER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -108,36 +136,16 @@ STEP_POWER_DATA_SCHEMA = vol.Schema(
|
||||
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_USER_DATA_SCHEMA = vol.Schema(
|
||||
# {
|
||||
# vol.Required(CONF_NAME): cv.string,
|
||||
# vol.Required(CONF_HEATER): cv.string,
|
||||
# vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
|
||||
# vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_LINEAR): vol.In(
|
||||
# [PROPORTIONAL_FUNCTION_LINEAR, PROPORTIONAL_FUNCTION_ATAN]
|
||||
# ),
|
||||
# vol.Required(CONF_PROP_BIAS, default=0.25): vol.Coerce(float),
|
||||
# vol.Required(CONF_TEMP_SENSOR): cv.string,
|
||||
# vol.Optional(CONF_POWER_SENSOR): cv.string,
|
||||
# vol.Optional(CONF_MAX_POWER_SENSOR): cv.string,
|
||||
# vol.Optional(CONF_WINDOW_SENSOR): cv.string,
|
||||
# vol.Required(CONF_WINDOW_DELAY, default=30): cv.positive_int,
|
||||
# vol.Optional(CONF_MOTION_SENSOR): cv.string,
|
||||
# vol.Required(CONF_MOTION_DELAY, default=30): cv.positive_int,
|
||||
# vol.Optional(CONF_MOTION_PRESET, default="comfort"): vol.In(
|
||||
# CONF_PRESETS_SELECTIONABLE
|
||||
# ),
|
||||
# vol.Optional(CONF_NO_MOTION_PRESET, default="eco"): vol.In(
|
||||
# CONF_PRESETS_SELECTIONABLE
|
||||
# ),
|
||||
# vol.Required(CONF_MOTION_DELAY, default=30): cv.positive_int,
|
||||
# vol.Optional(CONF_DEVICE_POWER): vol.Coerce(float),
|
||||
# }
|
||||
# ).extend(
|
||||
# {vol.Optional(v, default=17): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()}
|
||||
# )
|
||||
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):
|
||||
@@ -181,15 +189,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
for conf in [
|
||||
CONF_HEATER,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_PRESENCE_SENSOR,
|
||||
]:
|
||||
d = data.get(conf, None)
|
||||
d = data.get(conf, None) # pylint: disable=invalid-name
|
||||
if d is not None and self.hass.states.get(d) is None:
|
||||
_LOGGER.error(
|
||||
"Entity id %s doesn't have any state. We cannot use it in the Versatile Thermostat configuration",
|
||||
"Entity id %s doesn't have any state. We cannot use it in the Versatile Thermostat configuration", # pylint: disable=line-too-long
|
||||
d,
|
||||
)
|
||||
raise UnknownEntity(conf)
|
||||
@@ -217,7 +227,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
_LOGGER.debug("_info is now: %s", self._infos)
|
||||
return await next_step_function()
|
||||
|
||||
ds = schema_defaults(data_schema, **defaults)
|
||||
ds = schema_defaults(data_schema, **defaults) # pylint: disable=invalid-name
|
||||
|
||||
return self.async_show_form(step_id=step_id, data_schema=ds, errors=errors)
|
||||
|
||||
@@ -225,33 +235,33 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"""Handle the flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_user user_input=%s", user_input)
|
||||
|
||||
async def choose_next_step():
|
||||
"""Choose next configuration flow"""
|
||||
_LOGGER.debug("function is %s", self._infos.get(CONF_PROP_FUNCTION, None))
|
||||
if self._infos.get(CONF_PROP_FUNCTION, None) == PROPORTIONAL_FUNCTION_TPI:
|
||||
return await self.async_step_tpi()
|
||||
else:
|
||||
return await self.async_step_p()
|
||||
|
||||
return await self.generic_step(
|
||||
"user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_presets
|
||||
"user", STEP_USER_DATA_SCHEMA, user_input, choose_next_step
|
||||
)
|
||||
|
||||
# defaults = self._infos.copy()
|
||||
# errors = {}
|
||||
async def async_step_p(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_p user_input=%s", user_input)
|
||||
|
||||
#
|
||||
# if user_input is not None:
|
||||
# defaults.update(user_input or {})
|
||||
# try:
|
||||
# await self.validate_input(user_input)
|
||||
# except UnknownEntity as err:
|
||||
# errors[str(err)] = "unknown_entity"
|
||||
# except Exception: # pylint: disable=broad-except
|
||||
# _LOGGER.exception("Unexpected exception")
|
||||
# errors["base"] = "unknown"
|
||||
# else:
|
||||
# self._infos.update(user_input)
|
||||
# _LOGGER.debug("_info is now: %s", self._infos)
|
||||
# return await self.async_step_presets()
|
||||
#
|
||||
# user_data_schema = schema_defaults(STEP_USER_DATA_SCHEMA, **defaults)
|
||||
#
|
||||
# return self.async_show_form(
|
||||
# step_id="user", data_schema=user_data_schema, errors=errors
|
||||
# )
|
||||
return await self.generic_step(
|
||||
"p", STEP_P_DATA_SCHEMA, user_input, self.async_step_presets
|
||||
)
|
||||
|
||||
async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input)
|
||||
|
||||
return await self.generic_step(
|
||||
"tpi", STEP_TPI_DATA_SCHEMA, user_input, self.async_step_presets
|
||||
)
|
||||
|
||||
async def async_step_presets(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the presets flow steps"""
|
||||
@@ -261,16 +271,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"presets", STEP_PRESETS_DATA_SCHEMA, user_input, self.async_step_window
|
||||
)
|
||||
|
||||
# if user_input is None:
|
||||
# return self.async_show_form(
|
||||
# step_id="presets", data_schema=STEP_PRESETS_DATA_SCHEMA
|
||||
# )
|
||||
#
|
||||
# self._infos.update(user_input)
|
||||
# _LOGGER.debug("_info is now: %s", self._infos)
|
||||
#
|
||||
# return await self.async_step_window()
|
||||
|
||||
async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the window sensor flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_window user_input=%s", user_input)
|
||||
@@ -279,32 +279,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"window", STEP_WINDOW_DATA_SCHEMA, user_input, self.async_step_motion
|
||||
)
|
||||
|
||||
# if user_input is None:
|
||||
# return self.async_show_form(
|
||||
# step_id="window", data_schema=STEP_WINDOW_DATA_SCHEMA
|
||||
# )
|
||||
#
|
||||
# errors = {}
|
||||
#
|
||||
# try:
|
||||
# await self.validate_input(user_input)
|
||||
# except UnknownEntity as err:
|
||||
# errors[str(err)] = "unknown_entity"
|
||||
# except Exception: # pylint: disable=broad-except
|
||||
# _LOGGER.exception("Unexpected exception")
|
||||
# errors["base"] = "unknown"
|
||||
# else:
|
||||
# self._infos.update(user_input)
|
||||
# _LOGGER.debug("_info is now: %s", self._infos)
|
||||
#
|
||||
# return await self.async_step_motion()
|
||||
#
|
||||
# return self.async_show_form(
|
||||
# step_id="window",
|
||||
# data_schema=schema_defaults(STEP_WINDOW_DATA_SCHEMA, **user_input),
|
||||
# errors=errors,
|
||||
# )
|
||||
|
||||
async def async_step_motion(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the window and motion sensor flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_motion user_input=%s", user_input)
|
||||
@@ -313,32 +287,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"motion", STEP_MOTION_DATA_SCHEMA, user_input, self.async_step_power
|
||||
)
|
||||
|
||||
# if user_input is None:
|
||||
# return self.async_show_form(
|
||||
# step_id="motion", data_schema=STEP_MOTION_DATA_SCHEMA
|
||||
# )
|
||||
#
|
||||
# errors = {}
|
||||
#
|
||||
# try:
|
||||
# await self.validate_input(user_input)
|
||||
# except UnknownEntity as err:
|
||||
# errors[str(err)] = "unknown_entity"
|
||||
# except Exception: # pylint: disable=broad-except
|
||||
# _LOGGER.exception("Unexpected exception")
|
||||
# errors["base"] = "unknown"
|
||||
# else:
|
||||
# self._infos.update(user_input)
|
||||
# _LOGGER.debug("_info is now: %s", self._infos)
|
||||
#
|
||||
# return await self.async_step_power()
|
||||
#
|
||||
# return self.async_show_form(
|
||||
# step_id="motion",
|
||||
# data_schema=schema_defaults(STEP_MOTION_DATA_SCHEMA, **user_input),
|
||||
# errors=errors,
|
||||
# )
|
||||
|
||||
async def async_step_power(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the power management flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_power user_input=%s", user_input)
|
||||
@@ -347,37 +295,19 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"power",
|
||||
STEP_POWER_DATA_SCHEMA,
|
||||
user_input,
|
||||
self.async_finalize, # pylint: disable=no-member
|
||||
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)
|
||||
|
||||
# if user_input is None:
|
||||
# return self.async_show_form(
|
||||
# step_id="power", data_schema=STEP_POWER_DATA_SCHEMA
|
||||
# )
|
||||
#
|
||||
# errors = {}
|
||||
#
|
||||
# try:
|
||||
# await self.validate_input(user_input)
|
||||
# except UnknownEntity as err:
|
||||
# errors[str(err)] = "unknown_entity"
|
||||
# except Exception: # pylint: disable=broad-except
|
||||
# _LOGGER.exception("Unexpected exception")
|
||||
# errors["base"] = "unknown"
|
||||
# else:
|
||||
# self._infos.update(user_input)
|
||||
# _LOGGER.debug("_info is now: %s", self._infos)
|
||||
#
|
||||
# return self.async_create_entry(
|
||||
# title=self._infos[CONF_NAME], data=self._infos
|
||||
# )
|
||||
#
|
||||
# return self.async_show_form(
|
||||
# step_id="power",
|
||||
# data_schema=schema_defaults(STEP_POWER_DATA_SCHEMA, **user_input),
|
||||
# errors=errors,
|
||||
# )
|
||||
return await self.generic_step(
|
||||
"presence",
|
||||
STEP_PRESENCE_DATA_SCHEMA,
|
||||
user_input,
|
||||
self.async_finalize, # pylint: disable=no-member
|
||||
)
|
||||
|
||||
|
||||
class VersatileThermostatConfigFlow(
|
||||
@@ -436,8 +366,34 @@ class VersatileThermostatOptionsFlowHandler(
|
||||
"Into OptionsFlowHandler.async_step_user user_input=%s", user_input
|
||||
)
|
||||
|
||||
async def choose_next_step():
|
||||
"""Choose next configuration flow"""
|
||||
_LOGGER.debug("function is %s", self._infos.get(CONF_PROP_FUNCTION, None))
|
||||
if self._infos.get(CONF_PROP_FUNCTION, None) == PROPORTIONAL_FUNCTION_TPI:
|
||||
return await self.async_step_tpi()
|
||||
else:
|
||||
return await self.async_step_p()
|
||||
|
||||
return await self.generic_step(
|
||||
"user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_presets
|
||||
"user", STEP_USER_DATA_SCHEMA, user_input, choose_next_step
|
||||
)
|
||||
|
||||
async def async_step_p(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the p flow steps"""
|
||||
_LOGGER.debug("Into OptionsFlowHandler.async_step_p user_input=%s", user_input)
|
||||
|
||||
return await self.generic_step(
|
||||
"p", STEP_P_DATA_SCHEMA, user_input, self.async_step_presets
|
||||
)
|
||||
|
||||
async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the tpi flow steps"""
|
||||
_LOGGER.debug(
|
||||
"Into OptionsFlowHandler.async_step_tpi user_input=%s", user_input
|
||||
)
|
||||
|
||||
return await self.generic_step(
|
||||
"tpi", STEP_TPI_DATA_SCHEMA, user_input, self.async_step_presets
|
||||
)
|
||||
|
||||
async def async_step_presets(self, user_input: dict | None = None) -> FlowResult:
|
||||
@@ -480,113 +436,22 @@ class VersatileThermostatOptionsFlowHandler(
|
||||
"power",
|
||||
STEP_POWER_DATA_SCHEMA,
|
||||
user_input,
|
||||
self.async_step_presence, # pylint: disable=no-member
|
||||
)
|
||||
|
||||
async def async_step_presence(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the presence management flow steps"""
|
||||
_LOGGER.debug(
|
||||
"Into OptionsFlowHandler.async_step_presence user_input=%s", user_input
|
||||
)
|
||||
|
||||
return await self.generic_step(
|
||||
"presence",
|
||||
STEP_PRESENCE_DATA_SCHEMA,
|
||||
user_input,
|
||||
self.async_finalize, # pylint: disable=no-member
|
||||
)
|
||||
|
||||
#
|
||||
# async def async_step_presets(self, user_input=None):
|
||||
# """Manage presets options."""
|
||||
# _LOGGER.debug(
|
||||
# "Into OptionsFlowHandler.async_step_presets user_input =%s",
|
||||
# user_input,
|
||||
# )
|
||||
#
|
||||
# if user_input is not None:
|
||||
# _LOGGER.debug("We receive the new values: %s", user_input)
|
||||
# # data = dict(self.config_entry.data)
|
||||
# for conf in PRESETS_DATA_CONF:
|
||||
# self._info[conf] = user_input.get(conf)
|
||||
#
|
||||
# _LOGGER.debug("updating entry with: %s", self._info)
|
||||
# return await self.async_step_window()
|
||||
# else:
|
||||
# defaults = self._info.copy()
|
||||
# defaults.update(user_input or {})
|
||||
# presets_data_schema = schema_defaults(STEP_PRESETS_DATA_SCHEMA, **defaults)
|
||||
#
|
||||
# return self.async_show_form(
|
||||
# step_id="presets",
|
||||
# data_schema=presets_data_schema,
|
||||
# )
|
||||
#
|
||||
# async def async_step_window(self, user_input=None):
|
||||
# """Manage window options."""
|
||||
# _LOGGER.debug(
|
||||
# "Into OptionsFlowHandler.async_step_window user_input =%s",
|
||||
# user_input,
|
||||
# )
|
||||
#
|
||||
# if user_input is not None:
|
||||
# _LOGGER.debug("We receive the new values: %s", user_input)
|
||||
# # data = dict(self.config_entry.data)
|
||||
# for conf in WINDOW_DATA_CONF:
|
||||
# self._info[conf] = user_input.get(conf)
|
||||
#
|
||||
# _LOGGER.debug("updating entry with: %s", self._info)
|
||||
# return await self.async_step_motion()
|
||||
# else:
|
||||
# defaults = self._info.copy()
|
||||
# defaults.update(user_input or {})
|
||||
# window_data_schema = schema_defaults(STEP_WINDOW_DATA_SCHEMA, **defaults)
|
||||
#
|
||||
# return self.async_show_form(
|
||||
# step_id="window",
|
||||
# data_schema=window_data_schema,
|
||||
# )
|
||||
#
|
||||
# async def async_step_motion(self, user_input=None):
|
||||
# """Manage motion options."""
|
||||
# _LOGGER.debug(
|
||||
# "Into OptionsFlowHandler.async_step_motion user_input =%s",
|
||||
# user_input,
|
||||
# )
|
||||
#
|
||||
# if user_input is not None:
|
||||
# _LOGGER.debug("We receive the new values: %s", user_input)
|
||||
# # data = dict(self.config_entry.data)
|
||||
# for conf in MOTION_DATA_CONF:
|
||||
# self._info[conf] = user_input.get(conf)
|
||||
#
|
||||
# _LOGGER.debug("updating entry with: %s", self._info)
|
||||
# return await self.async_step_power()
|
||||
# else:
|
||||
# defaults = self._info.copy()
|
||||
# defaults.update(user_input or {})
|
||||
# motion_data_schema = schema_defaults(STEP_MOTION_DATA_SCHEMA, **defaults)
|
||||
#
|
||||
# return self.async_show_form(
|
||||
# step_id="motion",
|
||||
# data_schema=motion_data_schema,
|
||||
# )
|
||||
#
|
||||
# async def async_step_power(self, user_input=None):
|
||||
# """Manage power options."""
|
||||
# _LOGGER.debug(
|
||||
# "Into OptionsFlowHandler.async_step_power user_input =%s",
|
||||
# user_input,
|
||||
# )
|
||||
#
|
||||
# if user_input is not None:
|
||||
# _LOGGER.debug("We receive the new values: %s", user_input)
|
||||
# # data = dict(self.config_entry.data)
|
||||
# for conf in POWER_DATA_CONF:
|
||||
# self._info[conf] = user_input.get(conf)
|
||||
#
|
||||
# _LOGGER.debug("updating entry with: %s", self._info)
|
||||
# self.hass.config_entries.async_update_entry(
|
||||
# self.config_entry, data=self._info
|
||||
# )
|
||||
# return self.async_create_entry(title=None, data=None)
|
||||
# else:
|
||||
# defaults = self._info.copy()
|
||||
# defaults.update(user_input or {})
|
||||
# power_data_schema = schema_defaults(STEP_POWER_DATA_SCHEMA, **defaults)
|
||||
#
|
||||
# return self.async_show_form(
|
||||
# step_id="power",
|
||||
# data_schema=power_data_schema,
|
||||
# )
|
||||
#
|
||||
async def async_finalize(self):
|
||||
"""Finalization of the ConfigEntry creation"""
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.climate.const import (
|
||||
PRESET_ACTIVITY,
|
||||
# PRESET_ACTIVITY,
|
||||
PRESET_AWAY,
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
@@ -10,7 +10,11 @@ from homeassistant.components.climate.const import (
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
|
||||
from .prop_algorithm import PROPORTIONAL_FUNCTION_ATAN, PROPORTIONAL_FUNCTION_LINEAR
|
||||
from .prop_algorithm import (
|
||||
PROPORTIONAL_FUNCTION_ATAN,
|
||||
PROPORTIONAL_FUNCTION_LINEAR,
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
)
|
||||
|
||||
PRESET_POWER = "power"
|
||||
|
||||
@@ -18,6 +22,7 @@ DOMAIN = "versatile_thermostat"
|
||||
|
||||
CONF_HEATER = "heater_entity_id"
|
||||
CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
|
||||
CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id"
|
||||
CONF_POWER_SENSOR = "power_sensor_entity_id"
|
||||
CONF_MAX_POWER_SENSOR = "max_power_sensor_entity_id"
|
||||
CONF_WINDOW_SENSOR = "window_sensor_entity_id"
|
||||
@@ -30,6 +35,11 @@ CONF_WINDOW_DELAY = "window_delay"
|
||||
CONF_MOTION_DELAY = "motion_delay"
|
||||
CONF_MOTION_PRESET = "motion_preset"
|
||||
CONF_NO_MOTION_PRESET = "no_motion_preset"
|
||||
CONF_TPI_COEF_C = "tpi_coefc"
|
||||
CONF_TPI_COEF_T = "tpi_coeft"
|
||||
CONF_PRESENCE_SENSOR = "presence_sensor_entity_id"
|
||||
CONF_NO_PRESENCE_PRESET = "no_presence_preset"
|
||||
CONF_NO_PRESENCE_TEMP_OFFSET = "no_presence_temp_offset"
|
||||
|
||||
CONF_PRESETS = {
|
||||
p: f"{p}_temp"
|
||||
@@ -46,24 +56,37 @@ CONF_PRESETS_SELECTIONABLE = [PRESET_ECO, PRESET_COMFORT, PRESET_AWAY, PRESET_BO
|
||||
|
||||
CONF_PRESETS_VALUES = list(CONF_PRESETS.values())
|
||||
|
||||
ALL_CONF = [
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_WINDOW_DELAY,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_MOTION_DELAY,
|
||||
CONF_MOTION_PRESET,
|
||||
CONF_NO_MOTION_PRESET,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_CYCLE_MIN,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_PROP_BIAS,
|
||||
] + CONF_PRESETS_VALUES
|
||||
ALL_CONF = (
|
||||
[
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_WINDOW_DELAY,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_MOTION_DELAY,
|
||||
CONF_MOTION_PRESET,
|
||||
CONF_NO_MOTION_PRESET,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_CYCLE_MIN,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_PROP_BIAS,
|
||||
CONF_TPI_COEF_C,
|
||||
CONF_TPI_COEF_T,
|
||||
CONF_PRESENCE_SENSOR,
|
||||
CONF_NO_PRESENCE_PRESET,
|
||||
CONF_NO_PRESENCE_TEMP_OFFSET,
|
||||
]
|
||||
+ CONF_PRESETS_VALUES,
|
||||
)
|
||||
|
||||
CONF_FUNCTIONS = [PROPORTIONAL_FUNCTION_LINEAR, PROPORTIONAL_FUNCTION_ATAN]
|
||||
CONF_FUNCTIONS = [
|
||||
PROPORTIONAL_FUNCTION_LINEAR,
|
||||
PROPORTIONAL_FUNCTION_ATAN,
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
]
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
@@ -5,6 +5,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PROPORTIONAL_FUNCTION_ATAN = "atan"
|
||||
PROPORTIONAL_FUNCTION_LINEAR = "linear"
|
||||
PROPORTIONAL_FUNCTION_TPI = "tpi"
|
||||
|
||||
PROPORTIONAL_MIN_DURATION_SEC = 10
|
||||
|
||||
@@ -14,47 +15,64 @@ FUNCTION_TYPE = [PROPORTIONAL_FUNCTION_ATAN, PROPORTIONAL_FUNCTION_LINEAR]
|
||||
class PropAlgorithm:
|
||||
"""This class aims to do all calculation of the Proportional alogorithm"""
|
||||
|
||||
def __init__(self, function_type: str, bias: float, cycle_min: int):
|
||||
def __init__(
|
||||
self, function_type: str, bias: float, tpi_coefc, tpi_coeft, cycle_min: int
|
||||
):
|
||||
"""Initialisation of the Proportional Algorithm"""
|
||||
_LOGGER.debug(
|
||||
"Creation new PropAlgorithm function_type: %s, bias: %f, cycle_min:%d",
|
||||
"Creation new PropAlgorithm function_type: %s, bias: %s, tpi_coefc: %s, tpi_coeft: %s, cycle_min:%d",
|
||||
function_type,
|
||||
bias,
|
||||
tpi_coefc,
|
||||
tpi_coeft,
|
||||
cycle_min,
|
||||
)
|
||||
# TODO test function_type, bias, cycle_min
|
||||
self._function = function_type
|
||||
self._bias = bias
|
||||
self._tpi_coefc = tpi_coefc
|
||||
self._tpi_coeft = tpi_coeft
|
||||
self._cycle_min = cycle_min
|
||||
self._on_percent = 0
|
||||
self._on_time_sec = 0
|
||||
self._off_time_sec = self._cycle_min * 60
|
||||
|
||||
def calculate(self, target_temp: float, current_temp: float):
|
||||
def calculate(
|
||||
self, target_temp: float, current_temp: float, ext_current_temp: float
|
||||
):
|
||||
"""Do the calculation of the duration"""
|
||||
if target_temp is None or current_temp is None:
|
||||
_LOGGER.warning(
|
||||
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating will be disabled"
|
||||
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating will be disabled" # pylint: disable=line-too-long
|
||||
)
|
||||
on_percent = 0
|
||||
self._on_percent = 0
|
||||
else:
|
||||
delta_temp = target_temp - current_temp
|
||||
delta_ext_temp = (
|
||||
target_temp - ext_current_temp if ext_current_temp is not None else 0
|
||||
)
|
||||
|
||||
if self._function == PROPORTIONAL_FUNCTION_LINEAR:
|
||||
on_percent = 0.25 * delta_temp + self._bias
|
||||
self._on_percent = 0.25 * delta_temp + self._bias
|
||||
elif self._function == PROPORTIONAL_FUNCTION_ATAN:
|
||||
on_percent = math.atan(delta_temp + self._bias) / 1.4
|
||||
self._on_percent = math.atan(delta_temp + self._bias) / 1.4
|
||||
elif self._function == PROPORTIONAL_FUNCTION_TPI:
|
||||
self._on_percent = (
|
||||
self._tpi_coefc * delta_temp + self._tpi_coeft * delta_ext_temp
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Proportional algorithm: unknown %s function. Heating will be disabled",
|
||||
self._function,
|
||||
)
|
||||
on_percent = 0
|
||||
self._on_percent = 0
|
||||
|
||||
# calculated on_time duration in seconds
|
||||
if on_percent > 1:
|
||||
on_percent = 1
|
||||
if on_percent < 0:
|
||||
on_percent = 0
|
||||
self._on_time_sec = on_percent * self._cycle_min * 60
|
||||
if self._on_percent > 1:
|
||||
self._on_percent = 1
|
||||
if self._on_percent < 0:
|
||||
self._on_percent = 0
|
||||
self._on_time_sec = self._on_percent * self._cycle_min * 60
|
||||
|
||||
# Do not heat for less than xx sec
|
||||
if self._on_time_sec < PROPORTIONAL_MIN_DURATION_SEC:
|
||||
@@ -65,17 +83,23 @@ class PropAlgorithm:
|
||||
)
|
||||
self._on_time_sec = 0
|
||||
|
||||
self._off_time_sec = (1.0 - on_percent) * self._cycle_min * 60
|
||||
self._off_time_sec = self._cycle_min * 60 - self._on_time_sec
|
||||
|
||||
_LOGGER.debug(
|
||||
"heating percent calculated for current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)",
|
||||
current_temp if current_temp else -1.0,
|
||||
target_temp if target_temp else -1.0,
|
||||
on_percent,
|
||||
"heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", # pylint: disable=line-too-long
|
||||
current_temp if current_temp else -9999.0,
|
||||
ext_current_temp if ext_current_temp else -9999.0,
|
||||
target_temp if target_temp else -9999.0,
|
||||
self._on_percent,
|
||||
self.on_time_sec,
|
||||
self.off_time_sec,
|
||||
)
|
||||
|
||||
@property
|
||||
def on_percent(self) -> float:
|
||||
"""Returns the percentage the heater must be ON (1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
|
||||
return round(self._on_percent, 2)
|
||||
|
||||
@property
|
||||
def on_time_sec(self) -> int:
|
||||
"""Returns the calculated time in sec the heater must be ON"""
|
||||
|
||||
@@ -4,17 +4,32 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Add new Versatile Thermostat2",
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"heater_entity_id": "Heater entity id",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"proportional_function": "Function to use (atan is more aggressive)",
|
||||
"proportional_function": "Function to use (linear is less aggressive)"
|
||||
}
|
||||
},
|
||||
"p": {
|
||||
"title": "Proportional",
|
||||
"description": "Proportional attributes",
|
||||
"data": {
|
||||
"proportional_bias": "A bias to use in proportional algorithm"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Time Proportional Integral attributes",
|
||||
"data": {
|
||||
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
|
||||
"tpi_coefc": "Coefficient to use for internal temperature delta",
|
||||
"tpi_coeft": "Coefficient to use for external temperature delta"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature",
|
||||
@@ -35,7 +50,7 @@
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion sensor management",
|
||||
"title": "Motion management",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
@@ -46,12 +61,21 @@
|
||||
},
|
||||
"power": {
|
||||
"title": "Power management",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor pf your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power sensor entity id",
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"device_power": "Device power (kW)"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence management",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
|
||||
"no_presence_preset": "Preset to use when no one is present",
|
||||
"no_presence_offset": "Temperature offset to apply to current temperature is no one is present"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -66,17 +90,32 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Change a Versatile Thermostat",
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"heater_entity_id": "Heater entity id",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"proportional_function": "Function to use in proportional algorithm (atan is more aggressive)",
|
||||
"proportional_function": "Function to use (linear is less aggressive)"
|
||||
}
|
||||
},
|
||||
"p": {
|
||||
"title": "Proportional",
|
||||
"description": "Proportional attributes",
|
||||
"data": {
|
||||
"proportional_bias": "A bias to use in proportional algorithm"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Time Proportional Integral attributes",
|
||||
"data": {
|
||||
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
|
||||
"tpi_coefc": "Coefficient to use for internal temperature delta",
|
||||
"tpi_coeft": "Coefficient to use for external temperature delta"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature",
|
||||
@@ -97,7 +136,7 @@
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion sensor management",
|
||||
"title": "Motion management",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
@@ -108,12 +147,21 @@
|
||||
},
|
||||
"power": {
|
||||
"title": "Power management",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor pf your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power sensor entity id",
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"device_power": "Device power (kW)"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence management",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
|
||||
"no_presence_preset": "Preset to use when no one is present",
|
||||
"no_presence_offset": "Temperature offset to apply to current temperature is no one is present"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -4,17 +4,32 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Add new Versatile Thermostat2",
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"heater_entity_id": "Heater entity id",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"proportional_function": "Function to use (atan is more aggressive)",
|
||||
"proportional_function": "Function to use (linear is less aggressive)"
|
||||
}
|
||||
},
|
||||
"p": {
|
||||
"title": "Proportional",
|
||||
"description": "Proportional attributes",
|
||||
"data": {
|
||||
"proportional_bias": "A bias to use in proportional algorithm"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Time Proportional Integral attributes",
|
||||
"data": {
|
||||
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
|
||||
"tpi_coefc": "Coefficient to use for internal temperature delta",
|
||||
"tpi_coeft": "Coefficient to use for external temperature delta"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature",
|
||||
@@ -35,7 +50,7 @@
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion sensor management",
|
||||
"title": "Motion management",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
@@ -46,12 +61,21 @@
|
||||
},
|
||||
"power": {
|
||||
"title": "Power management",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor pf your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power sensor entity id",
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"device_power": "Device power (kW)"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence management",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
|
||||
"no_presence_preset": "Preset to use when no one is present",
|
||||
"no_presence_offset": "Temperature offset to apply to current temperature is no one is present"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -66,17 +90,32 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Change a Versatile Thermostat",
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"heater_entity_id": "Heater entity id",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"proportional_function": "Function to use in proportional algorithm (atan is more aggressive)",
|
||||
"proportional_function": "Function to use (linear is less aggressive)"
|
||||
}
|
||||
},
|
||||
"p": {
|
||||
"title": "Proportional",
|
||||
"description": "Proportional attributes",
|
||||
"data": {
|
||||
"proportional_bias": "A bias to use in proportional algorithm"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Time Proportional Integral attributes",
|
||||
"data": {
|
||||
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
|
||||
"tpi_coefc": "Coefficient to use for internal temperature delta",
|
||||
"tpi_coeft": "Coefficient to use for external temperature delta"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature",
|
||||
@@ -97,7 +136,7 @@
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion sensor management",
|
||||
"title": "Motion management",
|
||||
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
@@ -108,12 +147,21 @@
|
||||
},
|
||||
"power": {
|
||||
"title": "Power management",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor pf your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power sensor entity id",
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"device_power": "Device power (kW)"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Presence management",
|
||||
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
|
||||
"no_presence_preset": "Preset to use when no one is present",
|
||||
"no_presence_offset": "Temperature offset to apply to current temperature is no one is present"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -11,10 +11,25 @@
|
||||
"heater_entity_id": "Radiateur entity id",
|
||||
"temperature_sensor_entity_id": "Température sensor entity id",
|
||||
"cycle_min": "Durée du cycle (minutes)",
|
||||
"proportional_function": "Fonction de l'algorithm proportionnel à utiliser (atan est plus aggressive)",
|
||||
"proportional_function": "Fonction de l'algorithm proportionnel à utiliser (linear est moins aggressive)"
|
||||
}
|
||||
},
|
||||
"p": {
|
||||
"title": "Proportional",
|
||||
"description": "Attributs des algos Proportionnel",
|
||||
"data": {
|
||||
"proportional_bias": "Un biais à utiliser dans l'algorithm proportionnel"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Attributs de l'algo Time Proportional Integral",
|
||||
"data": {
|
||||
"external_temperature_sensor_entity_id": "Temperature exterieure sensor entity id",
|
||||
"tpi_coefc": "coeff_c : Coefficient à utiliser pour le delta de température interne",
|
||||
"tpi_coeft": "coeff_t : Coefficient à utiliser pour le delta de température externe"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "Pour chaque preset, donnez la température cible",
|
||||
@@ -52,6 +67,15 @@
|
||||
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
|
||||
"device_power": "Puissance de l'équipement"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Gestion de la présence",
|
||||
"description": "Donnez un capteur de présence (true si quelqu'un est présent).\nEnsuite spécifiez soit un preset à utiliser, soit un offset de température à appliquer lorsque personne n'est présent.\nSi le préset est utilisé, l'offset ne sera pas pris en compte.\nLaissez l'entity id vide si la gestion de la présence est non utilisée.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
|
||||
"no_presence_preset": "Preset à utiliser si personne n'est présent",
|
||||
"no_presence_offset": "Offset de température à utiliser si personne n'est présent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -66,17 +90,32 @@
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Configuration d'un thermostat",
|
||||
"title": "Ajout d'un nouveau thermostat",
|
||||
"description": "Principaux attributs obligatoires",
|
||||
"data": {
|
||||
"name": "Nom",
|
||||
"heater_entity_id": "Radiateur entity id",
|
||||
"temperature_sensor_entity_id": "Température sensor entity id",
|
||||
"cycle_min": "Durée du cycle (minutes)",
|
||||
"proportional_function": "Fonction de l'algorithm proportionnel à utiliser (atan est plus aggressive)",
|
||||
"proportional_function": "Fonction de l'algorithm proportionnel à utiliser (linear est moins aggressive)"
|
||||
}
|
||||
},
|
||||
"p": {
|
||||
"title": "Proportional",
|
||||
"description": "Attributs des algos Proportionnel",
|
||||
"data": {
|
||||
"proportional_bias": "Un biais à utiliser dans l'algorithm proportionnel"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
"title": "TPI",
|
||||
"description": "Attributs de l'algo Time Proportional Integral",
|
||||
"data": {
|
||||
"external_temperature_sensor_entity_id": "Temperature exterieure sensor entity id",
|
||||
"tpi_coefc": "coeff_c : Coefficient à utiliser pour le delta de température interne",
|
||||
"tpi_coeft": "coeff_t : Coefficient à utiliser pour le delta de température externe"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "Pour chaque preset, donnez la température cible",
|
||||
@@ -114,6 +153,15 @@
|
||||
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
|
||||
"device_power": "Puissance de l'équipement"
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Gestion de la présence",
|
||||
"description": "Donnez un capteur de présence (true si quelqu'un est présent).\nEnsuite spécifiez soit un preset à utiliser, soit un offset de température à appliquer lorsque personne n'est présent.\nSi le préset est utilisé, l'offset ne sera pas pris en compte.\nLaissez l'entity id vide si la gestion de la présence est non utilisée.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
|
||||
"no_presence_preset": "Preset à utiliser si personne n'est présent",
|
||||
"no_presence_offset": "Offset de température à utiliser si personne n'est présent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
||||
Reference in New Issue
Block a user