Add TPI algorithm

This commit is contained in:
Jean-Marc Collin
2023-01-07 10:14:29 +00:00
parent db4052d93b
commit 44174f23eb
9 changed files with 382 additions and 327 deletions

View File

@@ -4,13 +4,13 @@ import logging
from datetime import timedelta
from homeassistant.core import (
HomeAssistant,
# HomeAssistant,
callback,
CoreState,
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 +26,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,
@@ -35,24 +35,24 @@ from homeassistant.components.climate.const import (
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
PRESET_ACTIVITY,
PRESET_AWAY,
PRESET_BOOST,
PRESET_COMFORT,
PRESET_ECO,
PRESET_HOME,
# PRESET_AWAY,
# PRESET_BOOST,
# PRESET_COMFORT,
# PRESET_ECO,
# 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 +64,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 +81,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
@@ -90,7 +94,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
_, # hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
@@ -106,6 +110,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 +120,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():
@@ -134,6 +141,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 +152,8 @@ async def async_setup_entry(
no_motion_preset,
presets,
device_power,
tpi_coefc,
tpi_coeft,
)
],
True,
@@ -167,6 +177,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,6 +188,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
no_motion_preset,
presets,
device_power,
tpi_coefc,
tpi_coeft,
) -> None:
"""Initialize the thermostat."""
@@ -189,6 +202,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 +212,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 +261,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
@@ -340,9 +371,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 +389,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 +412,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 +447,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 +479,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 +556,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 +611,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:
@@ -632,7 +693,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 +819,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 +864,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."""
@@ -881,7 +974,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,
)
@@ -902,7 +995,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(
@@ -922,6 +1015,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
off_time_sec % 60,
)
await self._async_heater_turn_off()
self._async_cancel_cycle()
self._async_cancel_cycle = None
# Program turn off

View File

@@ -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(

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

View File

@@ -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,23 +28,35 @@ 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_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
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
elif self._function == PROPORTIONAL_FUNCTION_ATAN:
on_percent = math.atan(delta_temp + self._bias) / 1.4
elif self._function == PROPORTIONAL_FUNCTION_TPI:
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",
@@ -68,9 +83,10 @@ class PropAlgorithm:
self._off_time_sec = (1.0 - on_percent) * self._cycle_min * 60
_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,
"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,
on_percent,
self.on_time_sec,
self.off_time_sec,

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",