With cycle and Proportional calculation working

This commit is contained in:
Jean-Marc Collin
2022-12-28 23:51:28 +00:00
parent 3a0a475810
commit f1dc006904
6 changed files with 169 additions and 16 deletions

View File

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

View File

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

View File

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

View 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

View File

@@ -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%]"
}
}
},

View File

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