Add test_auto_start_stop feature manager. All tests ok

This commit is contained in:
Jean-Marc Collin
2024-12-27 17:49:09 +00:00
parent 9fc8f9c909
commit 7ec7d3a26a
7 changed files with 472 additions and 210 deletions

View File

@@ -125,6 +125,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
.union(FeaturePresenceManager.unrecorded_attributes)
.union(FeaturePowerManager.unrecorded_attributes)
.union(FeatureMotionManager.unrecorded_attributes)
.union(FeatureWindowManager.unrecorded_attributes)
)
def __init__(
@@ -216,6 +217,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# Instanciate all features manager
self._managers: list[BaseFeatureManager] = []
self._presence_manager: FeaturePresenceManager = FeaturePresenceManager(
self, hass
)
@@ -1299,7 +1301,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return
# TODO ce code avec du dearm est curieux. A voir après refacto
dearm_window_auto = await self._async_update_temp(new_state)
self.recalculate()
await self.async_control_heating(force=False)
@@ -1866,7 +1867,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
),
}
self._presence_manager.add_custom_attributes(self._attr_extra_state_attributes)
for manager in self._managers:
manager.add_custom_attributes(self._attr_extra_state_attributes)
@overrides
def async_write_ha_state(self):

View File

@@ -0,0 +1,236 @@
""" Implements the Auto-start/stop Feature Manager """
# pylint: disable=line-too-long
import logging
from typing import Any
from homeassistant.core import (
HomeAssistant,
)
from homeassistant.components.climate import HVACMode
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
from .commons import ConfigData
from .base_manager import BaseFeatureManager
from .auto_start_stop_algorithm import (
AutoStartStopDetectionAlgorithm,
AUTO_START_STOP_ACTION_OFF,
AUTO_START_STOP_ACTION_ON,
)
_LOGGER = logging.getLogger(__name__)
class FeatureAutoStartStopManager(BaseFeatureManager):
"""The implementation of the AutoStartStop feature"""
unrecorded_attributes = frozenset(
{
"auto_start_stop_level",
"auto_start_stop_dtmin",
"auto_start_stop_enable",
"auto_start_stop_accumulated_error",
"auto_start_stop_accumulated_error_threshold",
"auto_start_stop_last_switch_date",
}
)
def __init__(self, vtherm: Any, hass: HomeAssistant):
"""Init of a featureManager"""
super().__init__(vtherm, hass)
self._auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = (
AUTO_START_STOP_LEVEL_NONE
)
self._auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
self._is_configured: bool = False
self._is_auto_start_stop_enabled: bool = False
@overrides
def post_init(self, entry_infos: ConfigData):
"""Reinit of the manager"""
use_auto_start_stop = entry_infos.get(CONF_USE_AUTO_START_STOP_FEATURE, False)
if use_auto_start_stop:
self._auto_start_stop_level = (
entry_infos.get(CONF_AUTO_START_STOP_LEVEL, None)
or AUTO_START_STOP_LEVEL_NONE
)
self._is_configured = True
else:
self._auto_start_stop_level = AUTO_START_STOP_LEVEL_NONE
self._is_configured = False
# Instanciate the auto start stop algo
self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm(
self._auto_start_stop_level, self.name
)
@overrides
def start_listening(self):
"""Start listening the underlying entity"""
@overrides
def stop_listening(self):
"""Stop listening and remove the eventual timer still running"""
@overrides
async def refresh_state(self) -> bool:
"""Check the auto-start-stop and an eventual action
Return False if we should stop the control_heating method"""
if not self._is_configured or not self._is_auto_start_stop_enabled:
_LOGGER.debug("%s - auto start/stop is disabled (or not configured)", self)
return True
slope = (
self._vtherm.last_temperature_slope or 0
) / 60 # to have the slope in °/min
action = self._auto_start_stop_algo.calculate_action(
self._vtherm.hvac_mode,
self._vtherm.saved_hvac_mode,
self._vtherm.target_temperature,
self._vtherm.current_temperature,
slope,
self._vtherm.now,
)
_LOGGER.debug("%s - auto_start_stop action is %s", self, action)
if action == AUTO_START_STOP_ACTION_OFF and self._vtherm.is_on:
_LOGGER.info(
"%s - Turning OFF the Vtherm due to auto-start-stop conditions",
self,
)
self._vtherm.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP)
await self._vtherm.async_turn_off()
# Send an event
self._vtherm.send_event(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "stop",
"name": self.name,
"cause": "Auto stop conditions reached",
"hvac_mode": self._vtherm.hvac_mode,
"saved_hvac_mode": self._vtherm.saved_hvac_mode,
"target_temperature": self._vtherm.target_temperature,
"current_temperature": self._vtherm.current_temperature,
"temperature_slope": round(slope, 3),
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
},
)
# Stop here
return False
elif (
action == AUTO_START_STOP_ACTION_ON
and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
):
_LOGGER.info(
"%s - Turning ON the Vtherm due to auto-start-stop conditions", self
)
await self._vtherm.async_turn_on()
# Send an event
self._vtherm.send_event(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "start",
"name": self.name,
"cause": "Auto start conditions reached",
"hvac_mode": self._vtherm.hvac_mode,
"saved_hvac_mode": self._vtherm.saved_hvac_mode,
"target_temperature": self._vtherm.target_temperature,
"current_temperature": self._vtherm.current_temperature,
"temperature_slope": round(slope, 3),
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
},
)
self._vtherm.update_custom_attributes()
return True
def set_auto_start_stop_enable(self, is_enabled: bool):
"""Enable/Disable the auto-start/stop feature"""
self._is_auto_start_stop_enabled = is_enabled
if (
self._vtherm.hvac_mode == HVACMode.OFF
and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
):
_LOGGER.debug(
"%s - the vtherm is off cause auto-start/stop and enable have been set to false -> starts the VTherm"
)
self.hass.create_task(self._vtherm.async_turn_on())
# Send an event
self._vtherm.send_event(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "start",
"name": self.name,
"cause": "Auto start stop disabled",
"hvac_mode": self._vtherm.hvac_mode,
"saved_hvac_mode": self._vtherm.saved_hvac_mode,
"target_temperature": self._vtherm.target_temperature,
"current_temperature": self._vtherm.current_temperature,
"temperature_slope": round(
self._vtherm.last_temperature_slope or 0, 3
),
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
},
)
self._vtherm.update_custom_attributes()
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
"""Add some custom attributes"""
extra_state_attributes.update(
{
"is_auto_start_stop_configured": self.is_configured,
}
)
if self.is_configured:
extra_state_attributes.update(
{
"auto_start_stop_enable": self.auto_start_stop_enable,
"auto_start_stop_level": self._auto_start_stop_algo.level,
"auto_start_stop_dtmin": self._auto_start_stop_algo.dt_min,
"auto_start_stop_accumulated_error": self._auto_start_stop_algo.accumulated_error,
"auto_start_stop_accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
"auto_start_stop_last_switch_date": self._auto_start_stop_algo.last_switch_date,
}
)
@overrides
@property
def is_configured(self) -> bool:
"""Return True of the window feature is configured"""
return self._is_configured
@property
def auto_start_stop_level(self) -> TYPE_AUTO_START_STOP_LEVELS:
"""Return the auto start/stop level."""
return self._auto_start_stop_level
@property
def auto_start_stop_enable(self) -> bool:
"""Returns the auto_start_stop_enable"""
return self._is_auto_start_stop_enabled
@property
def is_auto_stopped(self) -> bool:
"""Returns the is vtherm is stopped and reason is AUTO_START_STOP"""
return (
self._vtherm.hvac_mode == HVACMode.OFF
and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
)
def __str__(self):
return f"AutoStartStopManager-{self.name}"

View File

@@ -84,8 +84,13 @@ class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEn
def update_my_state_and_vtherm(self):
"""Update the auto_start_stop_enable flag in my VTherm"""
self.async_write_ha_state()
if self.my_climate is not None:
self.my_climate.set_auto_start_stop_enable(self._attr_is_on)
if (
self.my_climate is not None
and self.my_climate.auto_start_stop_manager is not None
):
self.my_climate.auto_start_stop_manager.set_auto_start_stop_enable(
self._attr_is_on
)
@callback
async def async_turn_on(self, **kwargs: Any) -> None:

View File

@@ -24,11 +24,7 @@ from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
from .vtherm_api import VersatileThermostatAPI
from .underlyings import UnderlyingClimate
from .auto_start_stop_algorithm import (
AutoStartStopDetectionAlgorithm,
AUTO_START_STOP_ACTION_OFF,
AUTO_START_STOP_ACTION_ON,
)
from .feature_auto_start_stop_manager import FeatureAutoStartStopManager
_LOGGER = logging.getLogger(__name__)
@@ -55,15 +51,9 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"auto_activated_fan_mode",
"auto_deactivated_fan_mode",
"auto_regulation_use_device_temp",
"auto_start_stop_level",
"auto_start_stop_dtmin",
"auto_start_stop_enable",
"auto_start_stop_accumulated_error",
"auto_start_stop_accumulated_error_threshold",
"auto_start_stop_last_switch_date",
"follow_underlying_temp_change",
}
)
).union(FeatureAutoStartStopManager.unrecorded_attributes)
)
def __init__(
@@ -83,11 +73,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
# The fan_mode name depending of the current_mode
self._auto_activated_fan_mode: str | None = None
self._auto_deactivated_fan_mode: str | None = None
self._auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = (
AUTO_START_STOP_LEVEL_NONE
)
self._auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
self._is_auto_start_stop_enabled: bool = False
self._follow_underlying_temp_change: bool = False
self._last_regulation_change = None # NowClass.get_now(hass)
@@ -99,6 +84,12 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat"""
self._auto_start_stop_manager: FeatureAutoStartStopManager = (
FeatureAutoStartStopManager(self, self._hass)
)
self.register_manager(self._auto_start_stop_manager)
super().post_init(config_entry)
for climate in config_entry.get(CONF_UNDERLYING_LIST):
@@ -136,19 +127,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False
)
use_auto_start_stop = config_entry.get(CONF_USE_AUTO_START_STOP_FEATURE, False)
if use_auto_start_stop:
self._auto_start_stop_level = config_entry.get(
CONF_AUTO_START_STOP_LEVEL, AUTO_START_STOP_LEVEL_NONE
)
else:
self._auto_start_stop_level = AUTO_START_STOP_LEVEL_NONE
# Instanciate the auto start stop algo
self._auto_start_stop_algo = AutoStartStopDetectionAlgorithm(
self._auto_start_stop_level, self.name
)
@property
def is_over_climate(self) -> bool:
"""True if the Thermostat is over_climate"""
@@ -538,28 +516,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
self.auto_regulation_use_device_temp
)
self._attr_extra_state_attributes["auto_start_stop_enable"] = (
self.auto_start_stop_enable
)
self._attr_extra_state_attributes["auto_start_stop_level"] = (
self._auto_start_stop_algo.level
)
self._attr_extra_state_attributes["auto_start_stop_dtmin"] = (
self._auto_start_stop_algo.dt_min
)
self._attr_extra_state_attributes["auto_start_stop_accumulated_error"] = (
self._auto_start_stop_algo.accumulated_error
)
self._attr_extra_state_attributes[
"auto_start_stop_accumulated_error_threshold"
] = self._auto_start_stop_algo.accumulated_error_threshold
self._attr_extra_state_attributes["auto_start_stop_last_switch_date"] = (
self._auto_start_stop_algo.last_switch_date
)
self._attr_extra_state_attributes["follow_underlying_temp_change"] = (
self._follow_underlying_temp_change
)
@@ -899,90 +855,17 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
await end_climate_changed(changes)
async def check_auto_start_stop(self):
"""Check the auto-start-stop and an eventual action
Return False if we should stop the control_heating method"""
slope = (self.last_temperature_slope or 0) / 60 # to have the slope in °/min
action = self._auto_start_stop_algo.calculate_action(
self.hvac_mode,
self._saved_hvac_mode,
self.target_temperature,
self.current_temperature,
slope,
self.now,
)
_LOGGER.debug("%s - auto_start_stop action is %s", self, action)
if action == AUTO_START_STOP_ACTION_OFF and self.is_on:
_LOGGER.info(
"%s - Turning OFF the Vtherm due to auto-start-stop conditions",
self,
)
self.set_hvac_off_reason(HVAC_OFF_REASON_AUTO_START_STOP)
await self.async_turn_off()
# Send an event
self.send_event(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "stop",
"name": self.name,
"cause": "Auto stop conditions reached",
"hvac_mode": self.hvac_mode,
"saved_hvac_mode": self._saved_hvac_mode,
"target_temperature": self.target_temperature,
"current_temperature": self.current_temperature,
"temperature_slope": round(slope, 3),
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
},
)
# Stop here
return False
elif (
action == AUTO_START_STOP_ACTION_ON
and self.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
):
_LOGGER.info(
"%s - Turning ON the Vtherm due to auto-start-stop conditions", self
)
await self.async_turn_on()
# Send an event
self.send_event(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "start",
"name": self.name,
"cause": "Auto start conditions reached",
"hvac_mode": self.hvac_mode,
"saved_hvac_mode": self._saved_hvac_mode,
"target_temperature": self.target_temperature,
"current_temperature": self.current_temperature,
"temperature_slope": round(slope, 3),
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
},
)
self.update_custom_attributes()
return True
@overrides
async def async_control_heating(self, force=False, _=None) -> bool:
"""The main function used to run the calculation at each cycle"""
ret = await super().async_control_heating(force, _)
# Check if we need to auto start/stop the Vtherm
if self.auto_start_stop_enable:
continu = await self.check_auto_start_stop()
if not continu:
return ret
else:
_LOGGER.debug("%s - auto start/stop is disabled", self)
continu = await self.auto_start_stop_manager.refresh_state()
if not continu:
return ret
# Continue the normal async_control_heating
# Continue the normal async_control_heating
# Send the regulated temperature to the underlyings
await self._send_regulated_temperature()
@@ -992,37 +875,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
return ret
def set_auto_start_stop_enable(self, is_enabled: bool):
"""Enable/Disable the auto-start/stop feature"""
self._is_auto_start_stop_enabled = is_enabled
if (
self.hvac_mode == HVACMode.OFF
and self.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
):
_LOGGER.debug(
"%s - the vtherm is off cause auto-start/stop and enable have been set to false -> starts the VTherm"
)
self.hass.create_task(self.async_turn_on())
# Send an event
self.send_event(
event_type=EventType.AUTO_START_STOP_EVENT,
data={
"type": "start",
"name": self.name,
"cause": "Auto start stop disabled",
"hvac_mode": self.hvac_mode,
"saved_hvac_mode": self._saved_hvac_mode,
"target_temperature": self.target_temperature,
"current_temperature": self.current_temperature,
"temperature_slope": round(self.last_temperature_slope or 0, 3),
"accumulated_error": self._auto_start_stop_algo.accumulated_error,
"accumulated_error_threshold": self._auto_start_stop_algo.accumulated_error_threshold,
},
)
self.update_custom_attributes()
def set_follow_underlying_temp_change(self, follow: bool):
"""Set the flaf follow the underlying temperature changes"""
self._follow_underlying_temp_change = follow
@@ -1173,21 +1025,16 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
return False
return True
@property
def auto_start_stop_level(self) -> TYPE_AUTO_START_STOP_LEVELS:
"""Return the auto start/stop level."""
return self._auto_start_stop_level
@property
def auto_start_stop_enable(self) -> bool:
"""Returns the auto_start_stop_enable"""
return self._is_auto_start_stop_enabled
@property
def follow_underlying_temp_change(self) -> bool:
"""Get the follow underlying temp change flag"""
return self._follow_underlying_temp_change
@property
def auto_start_stop_manager(self) -> FeatureAutoStartStopManager:
"""Return the auto-start-stop Manager"""
return self._auto_start_stop_manager
@overrides
def init_underlyings(self):
"""Init the underlyings if not already done"""