Compare commits

..

5 Commits

Author SHA1 Message Date
Jean-Marc Collin a6a5fd38a0 Add custom attributes
Fix initialization temperature bug
2023-01-07 17:00:10 +01:00
Jean-Marc Collin 44174f23eb Add TPI algorithm 2023-01-07 10:14:29 +00:00
Jean-Marc Collin db4052d93b typo in README 2023-01-03 17:18:16 +01:00
Jean-Marc Collin a0486a3f54 FIX exception when no power sensor are configured
Add more explantions on README
2023-01-03 17:15:29 +01:00
Jean-Marc Collin bb7322854e FIX exception when power is not retrieved at first run 2023-01-02 15:59:57 +01:00
12 changed files with 488 additions and 332 deletions
+11 -2
View File
@@ -13,8 +13,14 @@ input_number:
name: Temperature
min: 0
max: 35
step: 1
step: .5
icon: mdi:thermometer
fake_external_temperature_sensor1:
name: Ext Temperature
min: -10
max: 35
step: .5
icon: mdi:home-thermometer
fake_current_power:
name: Current power
min: 0
@@ -36,7 +42,10 @@ 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)
icon: mdi:radiator
# input_boolean to simulate the motion sensor entity. Only for development environment.
fake_motion_sensor1:
+45 -1
View File
@@ -6,7 +6,15 @@
_Component developed by using the amazing development template [blueprint][blueprint]._
This custom component for Home Assistant is an upgrade and complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
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. The minimal needed configuration to use this thermostat is:
1. an equipement like a heater (a switch),
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.
## Why another thermostat implementation ?
For my personnal usage, I needed to add a couple of features and also to update the behavior that I implemented in my previous component "Awesome thermostat".
@@ -129,6 +137,13 @@ Depending of your area and heater, the convergente temperature can be under the
A function parameter is available. Set it to "Linear" to have a linéar growth of temperature or set it to "Atan" to have a more aggressive curve to target temperature depending of your need.
### Some results
Convergence of temperature to target configured by preset:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/dev/images/results-1.png?raw=true)
Cycle of on/off calculated by the integration:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/dev/images/results-2.png?raw=true)
Enjoy !
@@ -158,6 +173,35 @@ In this example I set ECO mode during the night and the day when nobody's at hom
I hope this example helps you, don't hesitate to give me your feedbacks !
## Even / even better with custom:simple-thermostat front integration
The custom:simple-thermostat (see https://home.clouderial.fr/hacs/repository/158654878) is a great integration which allow some customisation which fits well with this thermostat.
You can have something like that very easily ![image](https://github.com/jmcollin78/versatile_thermostat/blob/dev/images/simple-thermostat.png?raw=true)
Example configuration:
```
type: custom:simple-thermostat
entity: climate.thermostat_sam2
layout:
step: row
label:
temperature: T°
state: Etat
hide:
state: false
control:
hvac:
_name: Mode
preset:
_name: Preset
sensors:
- entity: sensor.total_puissance_radiateur_sam2
icon: mdi:lightning-bolt-outline
header:
toggle:
entity: input_boolean.etat_ouverture_porte_sam
name: Porte sam
```
## Contributions are welcome!
If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md)
+169 -23
View File
@@ -2,6 +2,7 @@ import math
import logging
from datetime import timedelta
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,11 @@ from .const import (
CONF_CYCLE_MIN,
CONF_PROP_FUNCTION,
CONF_PROP_BIAS,
CONF_TPI_COEF_C,
CONF_TPI_COEF_T,
SUPPORT_FLAGS,
PRESET_POWER,
PROPORTIONAL_FUNCTION_TPI,
)
from .prop_algorithm import PropAlgorithm
@@ -106,6 +111,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 +121,8 @@ 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)
presets = {}
for (key, value) in CONF_PRESETS.items():
@@ -127,6 +135,7 @@ async def async_setup_entry(
async_add_entities(
[
VersatileThermostat(
hass,
unique_id,
name,
heater_entity_id,
@@ -134,6 +143,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 +154,8 @@ async def async_setup_entry(
no_motion_preset,
presets,
device_power,
tpi_coefc,
tpi_coeft,
)
],
True,
@@ -160,6 +172,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
def __init__(
self,
hass,
unique_id,
name,
heater_entity_id,
@@ -167,6 +180,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 +191,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
no_motion_preset,
presets,
device_power,
tpi_coefc,
tpi_coeft,
) -> 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 +208,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 +218,8 @@ 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
# TODO if self.ac_mode:
# self.hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF]
@@ -245,10 +267,25 @@ 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
@@ -329,6 +366,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Return the sensor temperature."""
return self._cur_temp
# @property
# def extra_state_attributes(self) -> Mapping[str, Any] | None:
# _LOGGER.debug(
# "Calling extra_state_attributes: %s", self._hass.custom_attributes
# )
# return self._hass.custom_attributes
async def async_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode."""
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
@@ -340,9 +384,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 +402,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:
@@ -382,7 +425,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._saved_preset_mode = self._attr_preset_mode
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
)
async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
@@ -415,12 +460,14 @@ 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._prop_algorithm.calculate(
self._target_temp, self._cur_temp, self._cur_ext_temp
)
self.async_write_ha_state()
@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 +492,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(
@@ -512,6 +569,21 @@ 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)
switch_state = self.hass.states.get(self._heater_entity_id)
if switch_state and switch_state.state not in (
STATE_UNAVAILABLE,
@@ -552,7 +624,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
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())
if self.hass.state == CoreState.running:
@@ -594,6 +668,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if not self._hvac_mode and old_state.state:
self._hvac_mode = old_state.state
self._prop_algorithm.calculate(
self._target_temp, self._cur_temp, self._cur_ext_temp
)
else:
# No previous state, try and restore defaults
if self._target_temp is None:
@@ -632,7 +710,26 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
return
self._async_update_temp(new_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.async_write_ha_state()
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._prop_algorithm.calculate(
self._target_temp, self._cur_temp, self._cur_ext_temp
)
self.async_write_ha_state()
@callback
@@ -739,7 +836,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
new_preset,
)
self._target_temp = self._presets[new_preset]
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.async_write_ha_state()
if self._motion_call_cancel:
@@ -782,6 +881,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."""
@@ -810,7 +920,9 @@ 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 new_state.state == old_state.state:
if new_state is None or (
old_state is not None and new_state.state == old_state.state
):
return
try:
@@ -840,6 +952,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Check the overpowering condition
Turn the preset_mode of the heater to 'power' if power conditions are exceeded
"""
if not self._pmax_on:
return
_LOGGER.debug(
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
self,
@@ -875,7 +991,7 @@ 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,
)
@@ -896,7 +1012,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_LOGGER.debug("Cancelling the previous cycle that was running")
self._async_cancel_cycle()
self._async_cancel_cycle = None
await self._async_heater_turn_off()
# await self._async_heater_turn_off()
if self._hvac_mode == HVAC_MODE_HEAT and on_time_sec > 0:
_LOGGER.info(
@@ -908,6 +1024,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
await self._async_heater_turn_on()
self.update_custom_attributes()
async def _turn_off(_):
_LOGGER.info(
"%s - stop heating for %d min %d sec",
@@ -916,7 +1034,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
off_time_sec % 60,
)
await self._async_heater_turn_off()
self._async_cancel_cycle()
self._async_cancel_cycle = None
self.update_custom_attributes()
# Program turn off
self._async_cancel_cycle = async_call_later(
@@ -925,6 +1045,32 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_turn_off,
)
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_BOOST],
"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,
"is_device_active": self._is_device_active,
}
_LOGGER.debug(
"Calling update_custom_attributes: %s", 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,15 @@ 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,
PROPORTIONAL_FUNCTION_ATAN,
PROPORTIONAL_FUNCTION_LINEAR,
PROPORTIONAL_FUNCTION_TPI,
)
# from .climate import VersatileThermostat
@@ -55,20 +55,45 @@ 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,
]
),
vol.Required(CONF_PROP_BIAS, default=0.25): vol.Coerce(float),
}
)
USER_DATA_CONF = [
CONF_NAME,
CONF_HEATER,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_CYCLE_MIN,
CONF_PROP_FUNCTION,
]
STEP_P_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_PROP_BIAS, default=0.25): vol.Coerce(float),
}
)
P_DATA_CONF = [
CONF_PROP_BIAS,
]
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()}
)
@@ -110,35 +135,6 @@ STEP_POWER_DATA_SCHEMA = vol.Schema(
)
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()}
# )
def schema_defaults(schema, **defaults):
"""Create a new schema with default values filled in."""
@@ -181,15 +177,16 @@ 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,
]:
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 +214,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 +222,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 +258,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 +266,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 +274,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)
@@ -351,35 +286,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
)
# 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,
# )
class VersatileThermostatConfigFlow(
VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN
):
@@ -436,8 +342,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:
@@ -483,110 +415,6 @@ class VersatileThermostatOptionsFlowHandler(
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(
+37 -20
View File
@@ -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,8 @@ 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_PRESETS = {
p: f"{p}_temp"
@@ -46,24 +53,34 @@ 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_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,7 +15,9 @@ 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",
@@ -25,36 +28,49 @@ class PropAlgorithm:
# 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 +81,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",
@@ -66,17 +81,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",
@@ -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",
@@ -66,17 +81,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",
@@ -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",
@@ -66,17 +81,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",
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB