Issue #244 - make ema params configurable. Try to reproduce bug on Security mode
This commit is contained in:
@@ -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),
|
||||
}
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": "Η συσκευή έχει ήδη ρυθμιστεί"
|
||||
}
|
||||
|
||||
@@ -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"""
|
||||
|
||||
Reference in New Issue
Block a user