With EMA entity and slope calculation optimisations

This commit is contained in:
Jean-Marc Collin
2023-11-22 10:31:14 +00:00
parent 9e15aa48b9
commit 6a44d0dc4a
3 changed files with 110 additions and 23 deletions

View File

@@ -880,6 +880,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"""Return the unit of measurement."""
return self._unit
@property
def ema_temperature(self) -> str:
"""Return the EMA temperature."""
return self._ema_temp
@property
def hvac_mode(self) -> HVACMode | None:
"""Return current operation."""
@@ -1671,7 +1676,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
for under in self._underlyings:
await under.turn_off()
async def _async_manage_window_auto(self):
async def _async_manage_window_auto(self, in_cycle=False):
"""The management of the window auto feature"""
async def dearm_window_auto(_):
@@ -1701,9 +1706,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
if not self._window_auto_algo:
return
slope = self._window_auto_algo.add_temp_measurement(
temperature=self._ema_temp, datetime_measure=self._last_temperature_mesure
)
if in_cycle:
slope = self._window_auto_algo.check_age_last_measurement(
temperature=self._ema_temp,
datetime_now=datetime.now(get_tz(self._hass)),
)
else:
slope = self._window_auto_algo.add_temp_measurement(
temperature=self._ema_temp,
datetime_measure=self._last_temperature_mesure,
)
_LOGGER.debug(
"%s - Window auto is on, check the alert. last slope is %.3f",
self,
@@ -2052,6 +2065,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._attr_preset_mode,
)
# check auto_window conditions
await self._async_manage_window_auto(in_cycle=True)
# Issue 56 in over_climate mode, if the underlying climate is not initialized, try to initialize it
for under in self._underlyings:
if not under.is_initialized:
@@ -2095,9 +2111,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
force,
)
# calculate the smooth_temperature with EMA calculation
await self._async_manage_window_auto()
self.update_custom_attributes()
return True

View File

@@ -13,9 +13,13 @@ from datetime import datetime
_LOGGER = logging.getLogger(__name__)
# To filter bad values
MIN_DELTA_T_SEC = 15 # two temp mesure should be > 10 sec
MIN_DELTA_T_SEC = 0 # two temp mesure should be > 0 sec
MAX_SLOPE_VALUE = 2 # slope cannot be > 2 or < -2 -> else this is an aberrant point
MAX_DURATION_SEC = 600 # a fake data point is added in the cycle if last measurement was older than 600 sec (10 min)
MIN_NB_POINT = 4 # do not calculate slope until we have enough point
class WindowOpenDetectionAlgorithm:
"""The class that implements the algorithm listed above"""
@@ -25,6 +29,7 @@ class WindowOpenDetectionAlgorithm:
_last_slope: float
_last_datetime: datetime
_last_temperature: float
_nb_point: int
def __init__(self, alert_threshold, end_alert_threshold) -> None:
"""Initalize a new algorithm with the both threshold"""
@@ -32,6 +37,21 @@ class WindowOpenDetectionAlgorithm:
self._end_alert_threshold = end_alert_threshold
self._last_slope = None
self._last_datetime = None
self._nb_point = 0
def check_age_last_measurement(self, temperature, datetime_now) -> float:
""" " Check if last measurement is old and add
a fake measurement point if this is the case
"""
if self._last_datetime is None:
return self.add_temp_measurement(temperature, datetime_now)
delta_t_sec = float((datetime_now - self._last_datetime).total_seconds())
if delta_t_sec >= MAX_DURATION_SEC:
return self.add_temp_measurement(temperature, datetime_now)
else:
# do nothing
return self._last_slope
def add_temp_measurement(
self, temperature: float, datetime_measure: datetime
@@ -43,6 +63,7 @@ class WindowOpenDetectionAlgorithm:
_LOGGER.debug("First initialisation")
self._last_datetime = datetime_measure
self._last_temperature = temperature
self._nb_point = self._nb_point + 1
return None
_LOGGER.debug(
@@ -72,22 +93,25 @@ class WindowOpenDetectionAlgorithm:
)
return lspe
# if self._last_slope is None:
self._last_slope = round(new_slope, 4)
# else:
# self._last_slope = (0.5 * self._last_slope) + (0.5 * new_slope)
if self._last_slope is None:
self._last_slope = round(new_slope, 4)
else:
self._last_slope = round((0.2 * self._last_slope) + (0.8 * new_slope), 4)
self._last_datetime = datetime_measure
self._last_temperature = temperature
self._nb_point = self._nb_point + 1
_LOGGER.debug(
"delta_t=%.3f delta_temp=%.3f new_slope=%.3f last_slope=%s slope=%.3f",
"delta_t=%.3f delta_temp=%.3f new_slope=%.3f last_slope=%s slope=%.3f nb_point=%s",
delta_t,
delta_temp,
new_slope,
lspe,
self._last_slope,
self._nb_point,
)
return self._last_slope
def is_window_open_detected(self) -> bool:
@@ -95,22 +119,20 @@ class WindowOpenDetectionAlgorithm:
if self._alert_threshold is None:
return False
return (
self._last_slope < -self._alert_threshold
if self._last_slope is not None
else False
)
if self._nb_point < MIN_NB_POINT or self._last_slope is None:
return False
return self._last_slope < -self._alert_threshold
def is_window_close_detected(self) -> bool:
"""True if the last calculated slope is above (cause negative) the _end_alert_threshold"""
if self._end_alert_threshold is None:
return False
return (
self._last_slope >= self._end_alert_threshold
if self._last_slope is not None
else False
)
if self._nb_point < MIN_NB_POINT or self._last_slope is None:
return False
return self._last_slope >= self._end_alert_threshold
@property
def last_slope(self) -> float:

View File

@@ -51,6 +51,7 @@ async def async_setup_entry(
LastTemperatureSensor(hass, unique_id, name, entry.data),
LastExtTemperatureSensor(hass, unique_id, name, entry.data),
TemperatureSlopeSensor(hass, unique_id, name, entry.data),
EMATemperatureSensor(hass, unique_id, name, entry.data),
]
if entry.data.get(CONF_DEVICE_POWER):
entities.append(EnergySensor(hass, unique_id, name, entry.data))
@@ -542,3 +543,54 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
return 1
class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a Exponential Moving Average temp"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the regulated temperature sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "EMA temperature"
self._attr_unique_id = f"{self._device_name}_ema_temperature"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
if math.isnan(self.my_climate.ema_temperature) or math.isinf(
self.my_climate.ema_temperature
):
raise ValueError(
f"Sensor has illegal state {self.my_climate.ema_temperature}"
)
old_state = self._attr_native_value
self._attr_native_value = self.my_climate.ema_temperature
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:thermometer-lines"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.TEMPERATURE
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.MEASUREMENT
@property
def native_unit_of_measurement(self) -> str | None:
if not self.my_climate:
return UnitOfTemperature.CELSIUS
return self.my_climate.temperature_unit
@property
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
return 2