Compare commits

..

4 Commits

Author SHA1 Message Date
Jean-Marc Collin
ae568c8be2 Take Maia feedbacks on the algo. 2023-11-18 23:33:40 +00:00
Jean-Marc Collin
8fe4eb7ac0 15 sec between two slope calculation 2023-11-18 18:10:51 +00:00
Jean-Marc Collin
328f5f7cb0 Fix ema_temp unknown and remove slope smoothing 2023-11-18 18:08:33 +00:00
Jean-Marc Collin
41ae572875 Removes circular dependency error 2023-11-18 17:10:06 +00:00
5 changed files with 31 additions and 18 deletions

View File

@@ -109,17 +109,21 @@ from .const import (
PRESET_AC_SUFFIX, PRESET_AC_SUFFIX,
) )
from .commons import get_tz
from .underlyings import UnderlyingEntity from .underlyings import UnderlyingEntity
from .prop_algorithm import PropAlgorithm from .prop_algorithm import PropAlgorithm
from .open_window_algorithm import WindowOpenDetectionAlgorithm from .open_window_algorithm import WindowOpenDetectionAlgorithm
from .ema import EstimatedMobileAverage from .ema import ExponentialMovingAverage
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
return dt_util.get_time_zone(hass.config.time_zone)
class BaseThermostat(ClimateEntity, RestoreEntity): class BaseThermostat(ClimateEntity, RestoreEntity):
"""Representation of a base class for all Versatile Thermostat device.""" """Representation of a base class for all Versatile Thermostat device."""
@@ -249,7 +253,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._underlyings = [] self._underlyings = []
self._smooth_temp = None self._ema_temp = None
self._ema_algo = None self._ema_algo = None
self.post_init(entry_infos) self.post_init(entry_infos)
@@ -455,11 +459,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._total_energy = 0 self._total_energy = 0
self._ema_algo = EstimatedMobileAverage( self._ema_algo = ExponentialMovingAverage(
self.name, self.name,
self._cycle_min * 60, self._cycle_min * 60,
# Needed for time calculation # Needed for time calculation
get_tz(self._hass), get_tz(self._hass),
# one digit after the coma for temperature
1,
) )
_LOGGER.debug( _LOGGER.debug(

View File

@@ -8,7 +8,7 @@ from datetime import datetime, tzinfo
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MIN_TIME_DECAY_SEC = 5 MIN_TIME_DECAY_SEC = 0
# As for the EMA calculation of irregular time series, I've seen that it might be useful to # As for the EMA calculation of irregular time series, I've seen that it might be useful to
# have an upper limit for alpha in case the last measurement was too long ago. # have an upper limit for alpha in case the last measurement was too long ago.
# For example when using a half life of 10 minutes a measurement that is 60 minutes ago # For example when using a half life of 10 minutes a measurement that is 60 minutes ago
@@ -17,16 +17,19 @@ MIN_TIME_DECAY_SEC = 5
MAX_ALPHA = 0.9375 MAX_ALPHA = 0.9375
class EstimatedMobileAverage: class ExponentialMovingAverage:
"""A class that will do the Estimated Mobile Average calculation""" """A class that will do the Estimated Mobile Average calculation"""
def __init__(self, vterm_name: str, halflife: float, timezone: tzinfo): def __init__(
self, vterm_name: str, halflife: float, timezone: tzinfo, precision: int = 3
):
"""The halflife is the duration in secondes of a normal cycle""" """The halflife is the duration in secondes of a normal cycle"""
self._halflife: float = halflife self._halflife: float = halflife
self._timezone = timezone self._timezone = timezone
self._current_ema: float = None self._current_ema: float = None
self._last_timestamp: datetime = datetime.now(self._timezone) self._last_timestamp: datetime = datetime.now(self._timezone)
self._name = vterm_name self._name = vterm_name
self._precision = precision
def __str__(self) -> str: def __str__(self) -> str:
return f"EMA-{self._name}" return f"EMA-{self._name}"
@@ -64,8 +67,8 @@ class EstimatedMobileAverage:
alpha = 1 - math.exp(math.log(0.5) * time_decay / self._halflife) alpha = 1 - math.exp(math.log(0.5) * time_decay / self._halflife)
# capping alpha to avoid gap if last measurement was long time ago # capping alpha to avoid gap if last measurement was long time ago
alpha = min(alpha, 0.9375) alpha = min(alpha, MAX_ALPHA)
new_ema = round(alpha * measurement + (1 - alpha) * self._current_ema, 1) new_ema = alpha * measurement + (1 - alpha) * self._current_ema
self._last_timestamp = timestamp self._last_timestamp = timestamp
self._current_ema = new_ema self._current_ema = new_ema
@@ -77,4 +80,4 @@ class EstimatedMobileAverage:
self._last_timestamp, self._last_timestamp,
) )
return self._current_ema return round(self._current_ema, self._precision)

View File

@@ -1,3 +1,4 @@
# pylint: disable=line-too-long
""" This file implements the Open Window by temperature algorithm """ This file implements the Open Window by temperature algorithm
This algo works the following way: This algo works the following way:
- each time a new temperature is measured - each time a new temperature is measured
@@ -12,7 +13,7 @@ from datetime import datetime
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# To filter bad values # To filter bad values
MIN_DELTA_T_SEC = 30 # two temp mesure should be > 10 sec MIN_DELTA_T_SEC = 15 # two temp mesure should be > 10 sec
MAX_SLOPE_VALUE = 2 # slope cannot be > 2 or < -2 -> else this is an aberrant point MAX_SLOPE_VALUE = 2 # slope cannot be > 2 or < -2 -> else this is an aberrant point
@@ -71,10 +72,10 @@ class WindowOpenDetectionAlgorithm:
) )
return lspe return lspe
if self._last_slope is None: # if self._last_slope is None:
self._last_slope = new_slope self._last_slope = round(new_slope, 4)
else: # else:
self._last_slope = (0.5 * self._last_slope) + (0.5 * new_slope) # self._last_slope = (0.5 * self._last_slope) + (0.5 * new_slope)
self._last_datetime = datetime_measure self._last_datetime = datetime_measure
self._last_temperature = temperature self._last_temperature = temperature

View File

@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from custom_components.versatile_thermostat.ema import EstimatedMobileAverage from custom_components.versatile_thermostat.ema import ExponentialMovingAverage
from .commons import get_tz from .commons import get_tz
@@ -15,12 +15,13 @@ def test_ema_basics(hass: HomeAssistant):
tz = get_tz(hass) # pylint: disable=invalid-name tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
the_ema = EstimatedMobileAverage( the_ema = ExponentialMovingAverage(
"test", "test",
# 5 minutes # 5 minutes
300, 300,
# Needed for time calculation # Needed for time calculation
get_tz(hass), get_tz(hass),
1,
) )
assert the_ema assert the_ema

View File

@@ -386,6 +386,7 @@ async def test_multiple_climates(
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
"eco_temp": 17, "eco_temp": 17,
@@ -486,6 +487,7 @@ async def test_multiple_climates_underlying_changes(
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
"eco_temp": 17, "eco_temp": 17,