Compare commits
3 Commits
4.2.0.alph
...
4.2.0.alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8719349241 | ||
|
|
5540f6e8a9 | ||
|
|
18a72bd907 |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user