With cycle and Proportional calculation working
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import math
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback, CoreState
|
||||
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity
|
||||
@@ -62,9 +63,14 @@ from .const import (
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_PRESETS,
|
||||
CONF_CYCLE_MIN,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_PROP_BIAS,
|
||||
SUPPORT_FLAGS,
|
||||
)
|
||||
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -79,6 +85,9 @@ async def async_setup_entry(
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
heater_entity_id = entry.data.get(CONF_HEATER)
|
||||
cycle_min = entry.data.get(CONF_CYCLE_MIN)
|
||||
proportional_function = entry.data.get(CONF_PROP_FUNCTION)
|
||||
proportional_bias = entry.data.get(CONF_PROP_BIAS)
|
||||
temp_sensor_entity_id = entry.data.get(CONF_TEMP_SENSOR)
|
||||
power_sensor_entity_id = entry.data.get(CONF_POWER_SENSOR)
|
||||
max_power_sensor_entity_id = entry.data.get(CONF_MAX_POWER_SENSOR)
|
||||
@@ -100,6 +109,9 @@ async def async_setup_entry(
|
||||
unique_id,
|
||||
name,
|
||||
heater_entity_id,
|
||||
cycle_min,
|
||||
proportional_function,
|
||||
proportional_bias,
|
||||
temp_sensor_entity_id,
|
||||
power_sensor_entity_id,
|
||||
max_power_sensor_entity_id,
|
||||
@@ -118,12 +130,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
_name: str
|
||||
_heater_entity_id: str
|
||||
_prop_algorithm: PropAlgorithm
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id,
|
||||
name,
|
||||
heater_entity_id,
|
||||
cycle_min,
|
||||
proportional_function,
|
||||
proportional_bias,
|
||||
temp_sensor_entity_id,
|
||||
power_sensor_entity_id,
|
||||
max_power_sensor_entity_id,
|
||||
@@ -139,6 +155,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._unique_id = unique_id
|
||||
self._name = name
|
||||
self._heater_entity_id = heater_entity_id
|
||||
self._cycle_min = cycle_min
|
||||
self._proportional_function = proportional_function
|
||||
self._proportional_bias = proportional_bias
|
||||
self._temp_sensor_entity_id = temp_sensor_entity_id
|
||||
self._power_sensor_entity_id = power_sensor_entity_id
|
||||
self._max_power_sensor_entity_id = max_power_sensor_entity_id
|
||||
@@ -189,6 +208,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._swing_mode = None
|
||||
self._cur_temp = None
|
||||
|
||||
# Initiate the ProportionalAlgorithm
|
||||
self._prop_algorithm = PropAlgorithm(
|
||||
self._proportional_function, self._proportional_bias, self._cycle_min
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Creation of a new VersatileThermostat entity: unique_id=%s heater_entity_id=%s",
|
||||
self,
|
||||
@@ -268,10 +292,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
|
||||
if hvac_mode == HVAC_MODE_HEAT:
|
||||
self._hvac_mode = HVAC_MODE_HEAT
|
||||
# TODO await self._async_control_heating(force=True)
|
||||
await self._async_control_heating()
|
||||
elif hvac_mode == HVAC_MODE_COOL:
|
||||
self._hvac_mode = HVAC_MODE_COOL
|
||||
# TODO await self._async_control_heating(force=True)
|
||||
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
|
||||
@@ -296,19 +320,20 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
if preset_mode == PRESET_NONE:
|
||||
self._attr_preset_mode = PRESET_NONE
|
||||
self._target_temp = self._saved_target_temp
|
||||
# TODO await self._async_control_heating(force=True)
|
||||
await self._async_control_heating()
|
||||
elif preset_mode == PRESET_ACTIVITY:
|
||||
self._attr_preset_mode = PRESET_ACTIVITY
|
||||
# TODO self._target_temp = self._presets[self.no_motion_mode]
|
||||
# await self._async_control_heating(force=True)
|
||||
await self._async_control_heating()
|
||||
else:
|
||||
if self._attr_preset_mode == PRESET_NONE:
|
||||
self._saved_target_temp = self._target_temp
|
||||
self._attr_preset_mode = preset_mode
|
||||
self._target_temp = self._presets[preset_mode]
|
||||
# TODO await self._async_control_heating(force=True)
|
||||
await self._async_control_heating()
|
||||
|
||||
self.async_write_ha_state()
|
||||
self._prop_algorithm.calculate(self._target_temp, self._cur_temp)
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
@@ -341,7 +366,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
return
|
||||
self._target_temp = temperature
|
||||
self._attr_preset_mode = PRESET_NONE
|
||||
# TODO await self._async_control_heating(force=True)
|
||||
self._prop_algorithm.calculate(self._target_temp, self._cur_temp)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
@@ -387,12 +412,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._async_motion_changed,
|
||||
)
|
||||
)
|
||||
# if self._keep_alive:
|
||||
# self.async_on_remove(
|
||||
# async_track_time_interval(
|
||||
# self.hass, self._async_control_heating, self._keep_alive
|
||||
# )
|
||||
# )
|
||||
|
||||
if self._cycle_min:
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._async_control_heating,
|
||||
interval=timedelta(minutes=self._cycle_min),
|
||||
)
|
||||
)
|
||||
|
||||
if self._power_sensor_entity_id:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
@@ -431,7 +460,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self,
|
||||
float(temperature_state.state),
|
||||
)
|
||||
# TODO self._async_update_temp(temperature_state)
|
||||
self._async_update_temp(temperature_state)
|
||||
need_write_state = True
|
||||
|
||||
switch_state = self.hass.states.get(self._heater_entity_id)
|
||||
@@ -474,7 +503,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
if need_write_state:
|
||||
self.async_write_ha_state()
|
||||
# TODO self.hass.create_task(self._async_control_heating())
|
||||
self._prop_algorithm.calculate(self._target_temp, self._cur_temp)
|
||||
self.hass.create_task(self._async_control_heating())
|
||||
|
||||
if self.hass.state == CoreState.running:
|
||||
_async_startup_internal()
|
||||
@@ -551,7 +581,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
return
|
||||
|
||||
self._async_update_temp(new_state)
|
||||
# TODO await self._async_control_heating()
|
||||
self._prop_algorithm.calculate(self._target_temp, self._cur_temp)
|
||||
# TODO Not sure we need this - await self._async_control_heating()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
@@ -689,3 +720,15 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
|
||||
|
||||
async def _async_control_heating(self, time=None):
|
||||
"""The main function used to run the calculation at each cycle"""
|
||||
on_time_sec = self._prop_algorithm.on_time_sec
|
||||
off_time_sec = self._prop_algorithm.off_time_sec
|
||||
_LOGGER.info(
|
||||
"%s - Running new cycle at %s. on_time_sec=%f, off_time_sec=%f",
|
||||
self,
|
||||
time,
|
||||
on_time_sec,
|
||||
off_time_sec,
|
||||
)
|
||||
|
||||
@@ -28,8 +28,14 @@ from .const import (
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_CYCLE_MIN,
|
||||
ALL_CONF,
|
||||
CONF_PRESETS,
|
||||
CONF_FUNCTIONS,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_PROP_BIAS,
|
||||
PROPORTIONAL_FUNCTION_ATAN,
|
||||
PROPORTIONAL_FUNCTION_LINEAR,
|
||||
)
|
||||
|
||||
# from .climate import VersatileThermostat
|
||||
@@ -40,6 +46,11 @@ 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,
|
||||
|
||||
@@ -10,6 +10,8 @@ from homeassistant.components.climate.const import (
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
|
||||
from .prop_algorithm import PROPORTIONAL_FUNCTION_ATAN, PROPORTIONAL_FUNCTION_LINEAR
|
||||
|
||||
PRESET_POWER = "power"
|
||||
|
||||
DOMAIN = "versatile_thermostat"
|
||||
@@ -21,6 +23,9 @@ CONF_MAX_POWER_SENSOR = "max_power_sensor_entity_id"
|
||||
CONF_WINDOW_SENSOR = "window_sensor_entity_id"
|
||||
CONF_MOTION_SENSOR = "motion_sensor_entity_id"
|
||||
CONF_DEVICE_POWER = "device_power"
|
||||
CONF_CYCLE_MIN = "cycle_min"
|
||||
CONF_PROP_FUNCTION = "proportional_function"
|
||||
CONF_PROP_BIAS = "proportional_bias"
|
||||
|
||||
ALL_CONF = [
|
||||
CONF_NAME,
|
||||
@@ -31,6 +36,9 @@ ALL_CONF = [
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_CYCLE_MIN,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_PROP_BIAS,
|
||||
]
|
||||
|
||||
CONF_PRESETS = {
|
||||
@@ -44,4 +52,6 @@ CONF_PRESETS = {
|
||||
)
|
||||
}
|
||||
|
||||
CONF_FUNCTIONS = [PROPORTIONAL_FUNCTION_LINEAR, PROPORTIONAL_FUNCTION_ATAN]
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
83
custom_components/versatile_thermostat/prop_algorithm.py
Normal file
83
custom_components/versatile_thermostat/prop_algorithm.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import logging
|
||||
import math
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PROPORTIONAL_FUNCTION_ATAN = "atan"
|
||||
PROPORTIONAL_FUNCTION_LINEAR = "linear"
|
||||
|
||||
PROPORTIONAL_MIN_DURATION_SEC = 10
|
||||
|
||||
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):
|
||||
"""Initialisation of the Proportional Algorithm"""
|
||||
_LOGGER.debug(
|
||||
"Creation new PropAlgorithm function_type: %s, bias: %f, cycle_min:%d",
|
||||
function_type,
|
||||
bias,
|
||||
cycle_min,
|
||||
)
|
||||
# TODO test function_type, bias, cycle_min
|
||||
self._function = function_type
|
||||
self._bias = bias
|
||||
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):
|
||||
"""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"
|
||||
)
|
||||
on_percent = 0
|
||||
else:
|
||||
delta_temp = target_temp - current_temp
|
||||
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
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Proportional algorithm: unknown %s function. Heating will be disabled",
|
||||
self._function,
|
||||
)
|
||||
on_percent = 0
|
||||
|
||||
# calculated on_time duration in seconds
|
||||
if on_percent > 1:
|
||||
on_percent = 1
|
||||
self._on_time_sec = on_percent * self._cycle_min * 60
|
||||
|
||||
# Do not heat for less than xx sec
|
||||
if self._on_time_sec < PROPORTIONAL_MIN_DURATION_SEC:
|
||||
_LOGGER.debug(
|
||||
"No heating period due to heating period too small (%f < %f)",
|
||||
self._on_time_sec,
|
||||
PROPORTIONAL_MIN_DURATION_SEC,
|
||||
)
|
||||
self._on_time_sec = 0
|
||||
|
||||
self._off_time_sec = (1.0 - on_percent) * self._cycle_min * 60
|
||||
|
||||
_LOGGER.debug(
|
||||
"heating percent calculated is %f, on_time is %f (sec), off_time is %s (sec)",
|
||||
on_percent,
|
||||
self._on_time_sec,
|
||||
self._off_time_sec,
|
||||
)
|
||||
|
||||
@property
|
||||
def on_time_sec(self):
|
||||
"""Returns the calculated time in sec the heater must be ON"""
|
||||
return self._on_time_sec
|
||||
|
||||
@property
|
||||
def off_time_sec(self):
|
||||
"""Returns the calculated time in sec the heater must be OFF"""
|
||||
return self._off_time_sec
|
||||
@@ -13,6 +13,9 @@
|
||||
"window_sensor_entity_id": "[%key:config::data::window_sensor_entity_id%]",
|
||||
"motion_sensor_entity_id": "[%key:config::data::motion_sensor_entity_id%]",
|
||||
"device_power": "[%key:config::data::device_power%]",
|
||||
"cycle_min": "[%key:config::data::cycle_min%]",
|
||||
"proportional_function": "[%key:config::data::proportional_function%]",
|
||||
"proportional_bias": "[%key:config::data::proportional_bias%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"device_power": "Device power (kW)"
|
||||
"device_power": "Device power (kW)",
|
||||
"cycle_min": "Cycle duration (min)",
|
||||
"proportional_function": "Function to use (atan is more aggressive)",
|
||||
"proportional_bias": "A bias to use in proportional algorithm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user