Issue #244 - make ema params configurable. Try to reproduce bug on Security mode

This commit is contained in:
Jean-Marc Collin
2023-12-02 09:13:12 +00:00
parent 23f9c7c52f
commit fad1c4136a
10 changed files with 341 additions and 48 deletions

View File

@@ -5,6 +5,7 @@ from typing import Dict
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.config_entries import ConfigEntry, ConfigType
from homeassistant.core import HomeAssistant
@@ -19,39 +20,34 @@ from .const import (
CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_SLOW,
CONF_AUTO_REGULATION_EXPERT,
CONF_SHORT_EMA_PARAMS,
)
from .vtherm_api import VersatileThermostatAPI
_LOGGER = logging.getLogger(__name__)
SELF_REGULATION_PARAM_SCHEMA = (
vol.Schema(
{
vol.Required("kp"): vol.Coerce(float),
vol.Required("ki"): vol.Coerce(float),
vol.Required("k_ext"): vol.Coerce(float),
vol.Required("offset_max"): vol.Coerce(float),
vol.Required("stabilization_threshold"): vol.Coerce(float),
vol.Required("accumulated_error_threshold"): vol.Coerce(float),
}
),
)
SELF_REGULATION_PARAM_SCHEMA = {
vol.Required("kp"): vol.Coerce(float),
vol.Required("ki"): vol.Coerce(float),
vol.Required("k_ext"): vol.Coerce(float),
vol.Required("offset_max"): vol.Coerce(float),
vol.Required("stabilization_threshold"): vol.Coerce(float),
vol.Required("accumulated_error_threshold"): vol.Coerce(float),
}
EMA_PARAM_SCHEMA = {
vol.Required("max_alpha"): vol.Coerce(float),
vol.Required("halflife_sec"): vol.Coerce(float),
vol.Required("precision"): cv.positive_int,
}
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
CONF_AUTO_REGULATION_EXPERT: vol.Schema(
{
vol.Required("kp"): vol.Coerce(float),
vol.Required("ki"): vol.Coerce(float),
vol.Required("k_ext"): vol.Coerce(float),
vol.Required("offset_max"): vol.Coerce(float),
vol.Required("stabilization_threshold"): vol.Coerce(float),
vol.Required("accumulated_error_threshold"): vol.Coerce(float),
}
),
CONF_AUTO_REGULATION_EXPERT: vol.Schema(SELF_REGULATION_PARAM_SCHEMA),
CONF_SHORT_EMA_PARAMS: vol.Schema(EMA_PARAM_SCHEMA),
}
),
},

View File

@@ -107,8 +107,10 @@ from .const import (
ATTR_MEAN_POWER_CYCLE,
ATTR_TOTAL_ENERGY,
PRESET_AC_SUFFIX,
DEFAULT_SHORT_EMA_PARAMS,
)
from .vtherm_api import VersatileThermostatAPI
from .underlyings import UnderlyingEntity
from .prop_algorithm import PropAlgorithm
@@ -255,6 +257,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._ema_temp = None
self._ema_algo = None
self._now = None
self.post_init(entry_infos)
def post_init(self, entry_infos):
@@ -459,13 +462,21 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._total_energy = 0
# Read the parameter from configuration.yaml if it exists
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
short_ema_params = DEFAULT_SHORT_EMA_PARAMS
if api is not None and api.short_ema_params:
short_ema_params = api.short_ema_params
self._ema_algo = ExponentialMovingAverage(
self.name,
self._cycle_min * 60,
short_ema_params.get("halflife_sec"),
# Needed for time calculation
get_tz(self._hass),
# two digits after the coma for temperature slope calculation
2,
short_ema_params.get("precision"),
short_ema_params.get("max_alpha"),
)
_LOGGER.debug(
@@ -1905,9 +1916,18 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
return self._overpowering_state
def _set_now(self, now: datetime):
"""Set the now timestamp. This is only for tests purpose"""
self._now = now
@property
def now(self) -> datetime:
"""Get now. The local datetime or the overloaded _set_now date"""
return self._now if self._now is not None else datetime.now(self._current_tz)
async def check_security(self) -> bool:
"""Check if last temperature date is too long"""
now = datetime.now(self._current_tz)
now = self.now
delta_temp = (
now - self._last_temperature_mesure.replace(tzinfo=self._current_tz)
).total_seconds() / 60.0
@@ -1995,6 +2015,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
},
)
# Start security mode
if shouldStartSecurity:
self._security_state = True
self.save_hvac_mode()
@@ -2022,6 +2043,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
},
)
# Stop security mode
if shouldStopSecurity:
_LOGGER.warning(
"%s - End of security mode. restoring hvac_mode to %s and preset_mode to %s",

View File

@@ -94,6 +94,14 @@ CONF_AUTO_REGULATION_EXPERT = "auto_regulation_expert"
CONF_AUTO_REGULATION_DTEMP = "auto_regulation_dtemp"
CONF_AUTO_REGULATION_PERIOD_MIN = "auto_regulation_periode_min"
CONF_INVERSE_SWITCH = "inverse_switch_command"
CONF_SHORT_EMA_PARAMS = "short_ema_params"
DEFAULT_SHORT_EMA_PARAMS = {
"max_alpha": 0.5,
# In sec
"halflife_sec": 300,
"precision": 2,
}
CONF_PRESETS = {
p: f"{p}_temp"

View File

@@ -9,19 +9,25 @@ from datetime import datetime, tzinfo
_LOGGER = logging.getLogger(__name__)
MIN_TIME_DECAY_SEC = 0
# MAX_ALPHA:
# 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.
# For example when using a half life of 10 minutes a measurement that is 60 minutes ago
# (if there's nothing inbetween) would contribute to the smoothed value with 1,5%,
# giving the current measurement 98,5% relevance. It could be wise to limit the alpha to e.g. 4x the half life (=0.9375).
MAX_ALPHA = 0.5
class ExponentialMovingAverage:
"""A class that will do the Estimated Mobile Average calculation"""
def __init__(
self, vterm_name: str, halflife: float, timezone: tzinfo, precision: int = 3
self,
vterm_name: str,
halflife: float,
timezone: tzinfo,
precision: int = 3,
max_alpha: float = 0.5,
):
"""The halflife is the duration in secondes of a normal cycle"""
self._halflife: float = halflife
@@ -30,6 +36,7 @@ class ExponentialMovingAverage:
self._last_timestamp: datetime = datetime.now(self._timezone)
self._name = vterm_name
self._precision = precision
self._max_alpha = max_alpha
def __str__(self) -> str:
return f"EMA-{self._name}"
@@ -67,7 +74,7 @@ class ExponentialMovingAverage:
alpha = 1 - math.exp(math.log(0.5) * time_decay / self._halflife)
# capping alpha to avoid gap if last measurement was long time ago
alpha = min(alpha, MAX_ALPHA)
alpha = min(alpha, self._max_alpha)
new_ema = alpha * measurement + (1 - alpha) * self._current_ema
self._last_timestamp = timestamp

View File

@@ -85,7 +85,7 @@
"boost_ac_temp": "Θερμοκρασία στο προκαθορισμένο Boost για λειτουργία AC"
}
},
"window": {
"window": {
"title": "Διαχείριση Παραθύρων",
"description": "Ανοίξτε τη διαχείριση παραθύρων.\nΑφήστε το αντίστοιχο entity_id κενό αν δεν χρησιμοποιείται\nΜπορείτε επίσης να ρυθμίσετε αυτόματη ανίχνευση ανοίγματος παραθύρου με βάση τη μείωση της θερμοκρασίας",
"data": {
@@ -164,7 +164,7 @@
"unknown": "Απρόσμενο σφάλμα",
"unknown_entity": "Άγνωστο αναγνωριστικό οντότητας",
"window_open_detection_method": "Πρέπει να χρησιμοποιείται μόνο μία μέθοδος ανίχνευσης ανοιχτού παραθύρου. Χρησιμοποιήστε αισθητήρα ή αυτόματη ανίχνευση μέσω του κατωφλίου θερμοκρασίας, αλλά όχι και τα δύο"
},
},
"abort": {
"already_configured": "Η συσκευή έχει ήδη ρυθμιστεί"
}

View File

@@ -3,10 +3,7 @@ import logging
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from .const import (
DOMAIN,
CONF_AUTO_REGULATION_EXPERT,
)
from .const import DOMAIN, CONF_AUTO_REGULATION_EXPERT, CONF_SHORT_EMA_PARAMS
VTHERM_API_NAME = "vtherm_api"
@@ -33,6 +30,7 @@ class VersatileThermostatAPI(dict):
super().__init__()
self._hass = hass
self._expert_params = None
self._short_ema_params = None
def add_entry(self, entry: ConfigEntry):
"""Add a new entry"""
@@ -59,11 +57,20 @@ class VersatileThermostatAPI(dict):
if self._expert_params:
_LOGGER.debug("We have found expert params %s", self._expert_params)
self._short_ema_params = config.get(CONF_SHORT_EMA_PARAMS)
if self._short_ema_params:
_LOGGER.debug("We have found short ema params %s", self._short_ema_params)
@property
def self_regulation_expert(self):
"""Get the self regulation params"""
return self._expert_params
@property
def short_ema_params(self):
"""Get the self regulation params"""
return self._short_ema_params
@property
def hass(self):
"""Get the HomeAssistant object"""