[#28] Add a window open detection based on internal temperature change

This commit is contained in:
Jean-Marc Collin
2023-03-11 18:17:17 +01:00
parent 6e40a15262
commit 63cf77abc9
16 changed files with 1109 additions and 133 deletions

View File

@@ -103,6 +103,9 @@ from .const import (
CONF_MAX_POWER_SENSOR,
CONF_WINDOW_SENSOR,
CONF_WINDOW_DELAY,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
CONF_WINDOW_AUTO_MAX_DURATION,
CONF_MOTION_SENSOR,
CONF_MOTION_DELAY,
CONF_MOTION_PRESET,
@@ -144,6 +147,7 @@ from .const import (
)
from .prop_algorithm import PropAlgorithm
from .open_window_algorithm import WindowOpenDetectionAlgorithm
_LOGGER = logging.getLogger(__name__)
@@ -216,6 +220,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_motion_state: bool
_presence_state: bool
_security_state: bool
_window_auto_state: bool
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the thermostat."""
@@ -270,6 +275,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._underlying_climate_start_hvac_action_date = None
self._underlying_climate_delta_t = 0
self._window_auto_open_threshold = 0
self._window_auto_close_threshold = 0
self._window_auto_max_duration = 0
self._window_auto_state = False
self._window_auto_on = False
self._window_auto_algo = None
self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
self.post_init(entry_infos)
@@ -340,6 +352,27 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR)
self._window_delay_sec = entry_infos.get(CONF_WINDOW_DELAY)
self._window_auto_open_threshold = entry_infos.get(
CONF_WINDOW_AUTO_OPEN_THRESHOLD
)
self._window_auto_close_threshold = entry_infos.get(
CONF_WINDOW_AUTO_CLOSE_THRESHOLD
)
self._window_auto_max_duration = entry_infos.get(CONF_WINDOW_AUTO_MAX_DURATION)
self._window_auto_on = (
self._window_auto_open_threshold is not None
and self._window_auto_open_threshold > 0.0
and self._window_auto_close_threshold is not None
and self._window_auto_max_duration is not None
and self._window_auto_max_duration > 0
)
self._window_auto_state = False
self._window_auto_algo = WindowOpenDetectionAlgorithm(
alert_threshold=self._window_auto_open_threshold,
end_alert_threshold=self._window_auto_close_threshold,
)
self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR)
self._motion_delay_sec = entry_infos.get(CONF_MOTION_DELAY)
self._motion_preset = entry_infos.get(CONF_MOTION_PRESET)
@@ -1044,6 +1077,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Get the window_state"""
return self._window_state
@property
def window_auto_state(self) -> bool | None:
"""Get the window_auto_state"""
return STATE_ON if self._window_auto_state else STATE_OFF
@property
def security_state(self) -> bool | None:
"""Get the security_state"""
@@ -1074,6 +1112,41 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Get the last external temperature datetime"""
return self._last_ext_temperature_mesure
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp.
Requires ClimateEntityFeature.PRESET_MODE.
"""
return self._attr_preset_mode
@property
def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes.
Requires ClimateEntityFeature.PRESET_MODE.
"""
return self._attr_preset_modes
@property
def is_over_climate(self) -> bool | None:
"""return True is the thermostat is over a climate
or False is over switch"""
return self._is_over_climate
@property
def last_temperature_slope(self) -> float | None:
"""Return the last temperature slope curve if any"""
if not self._window_auto_algo:
return None
else:
return self._window_auto_algo.last_slope
@property
def is_window_auto_enabled(self) -> bool:
"""True if the Window auto feature is enabled"""
return self._window_auto_on
def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
if self._is_over_climate and self._underlying_climate:
@@ -1102,28 +1175,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
raise NotImplementedError()
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp.
Requires ClimateEntityFeature.PRESET_MODE.
"""
return self._attr_preset_mode
@property
def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes.
Requires ClimateEntityFeature.PRESET_MODE.
"""
return self._attr_preset_modes
@property
def is_over_climate(self) -> bool | None:
"""return True is the thermostat is over a climate
or False is over switch"""
return self._is_over_climate
async def async_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode."""
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
@@ -1621,6 +1672,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if self._security_state:
await self.check_security()
# check window_auto
await self._async_manage_window_auto()
except ValueError as ex:
_LOGGER.error("Unable to update temperature from sensor: %s", ex)
@@ -1815,6 +1869,80 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context
)
async def _async_manage_window_auto(self):
"""The management of the window auto feature"""
async def dearm_window_auto(_):
"""Callback that will be called after end of WINDOW_AUTO_MAX_DURATION"""
_LOGGER.info("Unset window auto because MAX_DURATION is exceeded")
await deactivate_window_auto(auto=True)
async def deactivate_window_auto(auto=False):
"""Deactivation of the Window auto state"""
_LOGGER.warning(
"%s - End auto detection of open window slope=%.3f", self, slope
)
# Send an event
cause = "max duration expiration" if auto else "end of slope alert"
self.send_event(
EventType.WINDOW_AUTO_EVENT,
{"type": "end", "cause": cause, "curve_slope": slope},
)
# Set attributes
self._window_auto_state = False
await self.restore_hvac_mode()
if self._window_call_cancel:
self._window_call_cancel()
self._window_call_cancel = None
if not self._window_auto_algo:
return
slope = self._window_auto_algo.add_temp_measurement(
temperature=self._cur_temp, datetime_measure=self._last_temperature_mesure
)
_LOGGER.debug(
"%s - Window auto is on, check the alert. last slope is %.3f",
self,
slope if slope is not None else 0.0,
)
if (
self._window_auto_algo.is_window_open_detected()
and self._window_auto_state is False
):
_LOGGER.warning(
"%s - Start auto detection of open window slope=%.3f", self, slope
)
# Send an event
self.send_event(
EventType.WINDOW_AUTO_EVENT,
{"type": "start", "cause": "slope alert", "curve_slope": slope},
)
# Set attributes
self._window_auto_state = True
self.save_hvac_mode()
await self.async_set_hvac_mode(HVACMode.OFF)
# Arm the end trigger
if self._window_call_cancel:
self._window_call_cancel()
self._window_call_cancel = None
self._window_call_cancel = async_call_later(
self.hass,
timedelta(minutes=self._window_auto_max_duration),
dearm_window_auto,
)
elif (
self._window_auto_algo.is_window_close_detected()
and self._window_auto_state is True
):
await deactivate_window_auto(False)
# For testing purpose we need to return the inner function
return dearm_window_auto
def save_preset_mode(self):
"""Save the current preset mode to be restored later
We never save a hidden preset mode