Compare commits

..

3 Commits

Author SHA1 Message Date
Jean-Marc Collin
8719349241 Don't store datetime of fake datapoint 2023-11-28 22:19:16 +00:00
Jean-Marc Collin
5540f6e8a9 Try auto window new algo 2023-11-28 06:38:47 +00:00
Jean-Marc Collin
18a72bd907 Change open_window_detection fake datapoint threshold 2023-11-28 06:20:14 +00:00
5 changed files with 59 additions and 36 deletions

View File

@@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
MIN_DELTA_T_SEC = 0 # two temp mesure should be > 0 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_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) MAX_DURATION_MIN = 30 # a fake data point is added in the cycle if last measurement was older than 30 min
MIN_NB_POINT = 4 # do not calculate slope until we have enough point MIN_NB_POINT = 4 # do not calculate slope until we have enough point
@@ -46,15 +46,15 @@ class WindowOpenDetectionAlgorithm:
if self._last_datetime is None: if self._last_datetime is None:
return self.add_temp_measurement(temperature, datetime_now) return self.add_temp_measurement(temperature, datetime_now)
delta_t_sec = float((datetime_now - self._last_datetime).total_seconds()) delta_t_sec = float((datetime_now - self._last_datetime).total_seconds()) / 60.0
if delta_t_sec >= MAX_DURATION_SEC: if delta_t_sec >= MAX_DURATION_MIN:
return self.add_temp_measurement(temperature, datetime_now) return self.add_temp_measurement(temperature, datetime_now, False)
else: else:
# do nothing # do nothing
return self._last_slope return self._last_slope
def add_temp_measurement( def add_temp_measurement(
self, temperature: float, datetime_measure: datetime self, temperature: float, datetime_measure: datetime, store_date: bool = True
) -> float: ) -> float:
"""Add a new temperature measurement """Add a new temperature measurement
returns the last slope returns the last slope
@@ -98,7 +98,11 @@ class WindowOpenDetectionAlgorithm:
else: else:
self._last_slope = round((0.2 * self._last_slope) + (0.8 * new_slope), 4) self._last_slope = round((0.2 * self._last_slope) + (0.8 * new_slope), 4)
# if we are in cycle check and so adding a fake datapoint, we don't store the event datetime
# so that, when we will receive a real temperature point we will not calculate a wrong slope
if store_date:
self._last_datetime = datetime_measure self._last_datetime = datetime_measure
self._last_temperature = temperature self._last_temperature = temperature
self._nb_point = self._nb_point + 1 self._nb_point = self._nb_point + 1

View File

@@ -506,17 +506,15 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Called when my climate have change""" """Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id) _LOGGER.debug("%s - climate state change", self._attr_unique_id)
if math.isnan(self.my_climate.regulated_target_temp) or math.isinf( new_temp = self.my_climate.regulated_target_temp
self.my_climate.regulated_target_temp if new_temp is None:
): return
raise ValueError(
f"Sensor has illegal state {self.my_climate.regulated_target_temp}" if math.isnan(new_temp) or math.isinf(new_temp):
) raise ValueError(f"Sensor has illegal state {new_temp}")
old_state = self._attr_native_value old_state = self._attr_native_value
self._attr_native_value = round( self._attr_native_value = round(new_temp, self.suggested_display_precision)
self.my_climate.regulated_target_temp, self.suggested_display_precision
)
if old_state != self._attr_native_value: if old_state != self._attr_native_value:
self.async_write_ha_state() self.async_write_ha_state()
return return
@@ -559,15 +557,15 @@ class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Called when my climate have change""" """Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id) _LOGGER.debug("%s - climate state change", self._attr_unique_id)
if math.isnan(self.my_climate.ema_temperature) or math.isinf( new_ema = self.my_climate.ema_temperature
self.my_climate.ema_temperature if new_ema is None:
): return
raise ValueError(
f"Sensor has illegal state {self.my_climate.ema_temperature}" if math.isnan(new_ema) or math.isinf(new_ema):
) raise ValueError(f"Sensor has illegal state {new_ema}")
old_state = self._attr_native_value old_state = self._attr_native_value
self._attr_native_value = self.my_climate.ema_temperature self._attr_native_value = new_ema
if old_state != self._attr_native_value: if old_state != self._attr_native_value:
self.async_write_ha_state() self.async_write_ha_state()
return return

View File

@@ -363,12 +363,12 @@ async def test_over_climate_regulation_limitations(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
): ):
await send_temperature_change_event(entity, 16, event_timestamp) await send_temperature_change_event(entity, 15, event_timestamp)
await send_ext_temperature_change_event(entity, 12, event_timestamp) await send_ext_temperature_change_event(entity, 12, event_timestamp)
# the regulated should have been done # the regulated should have been done
assert entity.regulated_target_temp != old_regulated_temp assert entity.regulated_target_temp != old_regulated_temp
assert entity.regulated_target_temp >= entity.target_temperature assert entity.regulated_target_temp >= entity.target_temperature
assert ( assert (
entity.regulated_target_temp == 17 + 0.5 entity.regulated_target_temp == 17 + 1.5
) # 0.7 without round_to_nearest ) # 0.7 without round_to_nearest

View File

@@ -2,7 +2,9 @@
""" Test the OpenWindow algorithm """ """ Test the OpenWindow algorithm """
from datetime import datetime, timedelta from datetime import datetime, timedelta
from custom_components.versatile_thermostat.open_window_algorithm import WindowOpenDetectionAlgorithm from custom_components.versatile_thermostat.open_window_algorithm import (
WindowOpenDetectionAlgorithm,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -19,18 +21,28 @@ async def test_open_window_algo(
tz = get_tz(hass) # pylint: disable=invalid-name tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz) now = datetime.now(tz)
event_timestamp = now - timedelta(minutes=5) event_timestamp = now - timedelta(minutes=10)
last_slope = the_algo.add_temp_measurement( last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp temperature=10, datetime_measure=event_timestamp
) )
# We need at least 2 measurement # We need at least 4 measurement
assert last_slope is None assert last_slope is None
assert the_algo.last_slope is None assert the_algo.last_slope is None
assert the_algo.is_window_close_detected() is False assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False assert the_algo.is_window_open_detected() is False
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=9)
last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp
)
event_timestamp = now - timedelta(minutes=8)
last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp
)
event_timestamp = now - timedelta(minutes=7)
last_slope = the_algo.add_temp_measurement( last_slope = the_algo.add_temp_measurement(
temperature=10, datetime_measure=event_timestamp temperature=10, datetime_measure=event_timestamp
) )
@@ -41,19 +53,19 @@ async def test_open_window_algo(
assert the_algo.is_window_close_detected() is True assert the_algo.is_window_close_detected() is True
assert the_algo.is_window_open_detected() is False assert the_algo.is_window_open_detected() is False
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=6)
last_slope = the_algo.add_temp_measurement( last_slope = the_algo.add_temp_measurement(
temperature=9, datetime_measure=event_timestamp temperature=9, datetime_measure=event_timestamp
) )
# A slope is calculated # A slope is calculated
assert last_slope == -0.5 assert last_slope == -0.8
assert the_algo.last_slope == -0.5 assert the_algo.last_slope == -0.8
assert the_algo.is_window_close_detected() is False assert the_algo.is_window_close_detected() is False
assert the_algo.is_window_open_detected() is False assert the_algo.is_window_open_detected() is False
# A new temperature with 2 degre less in one minute (value will be rejected) # A new temperature with 2 degre less in one minute (value will be rejected)
event_timestamp = now - timedelta(minutes=2) event_timestamp = now - timedelta(minutes=5)
last_slope = the_algo.add_temp_measurement( last_slope = the_algo.add_temp_measurement(
temperature=7, datetime_measure=event_timestamp temperature=7, datetime_measure=event_timestamp
) )
@@ -65,7 +77,7 @@ async def test_open_window_algo(
assert the_algo.is_window_open_detected() is True assert the_algo.is_window_open_detected() is True
# A new temperature with 1 degre less # A new temperature with 1 degre less
event_timestamp = now - timedelta(minutes=1) event_timestamp = now - timedelta(minutes=4)
last_slope = the_algo.add_temp_measurement( last_slope = the_algo.add_temp_measurement(
temperature=6, datetime_measure=event_timestamp temperature=6, datetime_measure=event_timestamp
) )
@@ -77,7 +89,7 @@ async def test_open_window_algo(
assert the_algo.is_window_open_detected() is True assert the_algo.is_window_open_detected() is True
# A new temperature with 0 degre less # A new temperature with 0 degre less
event_timestamp = now - timedelta(minutes=0) event_timestamp = now - timedelta(minutes=3)
last_slope = the_algo.add_temp_measurement( last_slope = the_algo.add_temp_measurement(
temperature=6, datetime_measure=event_timestamp temperature=6, datetime_measure=event_timestamp
) )
@@ -89,7 +101,7 @@ async def test_open_window_algo(
assert the_algo.is_window_open_detected() is False assert the_algo.is_window_open_detected() is False
# A new temperature with 1 degre more # A new temperature with 1 degre more
event_timestamp = now + timedelta(minutes=1) event_timestamp = now + timedelta(minutes=2)
last_slope = the_algo.add_temp_measurement( last_slope = the_algo.add_temp_measurement(
temperature=7, datetime_measure=event_timestamp temperature=7, datetime_measure=event_timestamp
) )

View File

@@ -477,6 +477,14 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
# Initialize the slope algo with 2 measurements
# event_timestamp = now - timedelta(minutes=9)
# await send_temperature_change_event(entity, 19, event_timestamp)
# event_timestamp = now - timedelta(minutes=8)
# await send_temperature_change_event(entity, 19, event_timestamp)
# event_timestamp = now - timedelta(minutes=7)
# await send_temperature_change_event(entity, 19, event_timestamp)
# Make the temperature down # Make the temperature down
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
@@ -486,6 +494,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.is_device_active",
return_value=True, return_value=True,
): ):
# This is the 3rd measurment. Slope is not ready
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 19, event_timestamp) await send_temperature_change_event(entity, 19, event_timestamp)