Add TPI algorithm
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user