Compare commits

..

2 Commits

Author SHA1 Message Date
Jean-Marc Collin
dc7739d53b Fix slope None 2024-12-08 17:18:02 +00:00
Jean-Marc Collin
729f263cc8 Fix 2024-12-08 17:05:39 +00:00
15 changed files with 59 additions and 584 deletions

View File

@@ -57,13 +57,10 @@ class AutoStartStopDetectionAlgorithm:
_accumulated_error: float = 0 _accumulated_error: float = 0
_error_threshold: float | None = None _error_threshold: float | None = None
_last_calculation_date: datetime | None = None _last_calculation_date: datetime | None = None
_last_switch_date: datetime | None = None
def __init__(self, level: TYPE_AUTO_START_STOP_LEVELS, vtherm_name) -> None: def __init__(self, level: TYPE_AUTO_START_STOP_LEVELS, vtherm_name) -> None:
"""Initalize a new algorithm with the right constants""" """Initalize a new algorithm with the right constants"""
self._vtherm_name = vtherm_name self._vtherm_name = vtherm_name
self._last_calculation_date = None
self._last_switch_date = None
self._init_level(level) self._init_level(level)
def _init_level(self, level: TYPE_AUTO_START_STOP_LEVELS): def _init_level(self, level: TYPE_AUTO_START_STOP_LEVELS):
@@ -146,26 +143,17 @@ class AutoStartStopDetectionAlgorithm:
temp_at_dt = current_temp + slope_min * self._dt temp_at_dt = current_temp + slope_min * self._dt
# Calculate the number of minute from last_switch
nb_minutes_since_last_switch = 999
if self._last_switch_date is not None:
nb_minutes_since_last_switch = (
now - self._last_switch_date
).total_seconds() / 60
# Check to turn-off # Check to turn-off
# When we hit the threshold, that mean we can turn off # When we hit the threshold, that mean we can turn off
if hvac_mode == HVACMode.HEAT: if hvac_mode == HVACMode.HEAT:
if ( if (
self._accumulated_error <= -self._error_threshold self._accumulated_error <= -self._error_threshold
and temp_at_dt >= target_temp + TEMP_HYSTERESIS and temp_at_dt >= target_temp + TEMP_HYSTERESIS
and nb_minutes_since_last_switch >= self._dt
): ):
_LOGGER.info( _LOGGER.info(
"%s - We need to stop, there is no need for heating for a long time.", "%s - We need to stop, there is no need for heating for a long time.",
self, self,
) )
self._last_switch_date = now
return AUTO_START_STOP_ACTION_OFF return AUTO_START_STOP_ACTION_OFF
else: else:
_LOGGER.debug("%s - nothing to do, we are heating", self) _LOGGER.debug("%s - nothing to do, we are heating", self)
@@ -175,13 +163,11 @@ class AutoStartStopDetectionAlgorithm:
if ( if (
self._accumulated_error >= self._error_threshold self._accumulated_error >= self._error_threshold
and temp_at_dt <= target_temp - TEMP_HYSTERESIS and temp_at_dt <= target_temp - TEMP_HYSTERESIS
and nb_minutes_since_last_switch >= self._dt
): ):
_LOGGER.info( _LOGGER.info(
"%s - We need to stop, there is no need for cooling for a long time.", "%s - We need to stop, there is no need for cooling for a long time.",
self, self,
) )
self._last_switch_date = now
return AUTO_START_STOP_ACTION_OFF return AUTO_START_STOP_ACTION_OFF
else: else:
_LOGGER.debug( _LOGGER.debug(
@@ -192,15 +178,11 @@ class AutoStartStopDetectionAlgorithm:
# check to turn on # check to turn on
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT: if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.HEAT:
if ( if temp_at_dt <= target_temp - TEMP_HYSTERESIS:
temp_at_dt <= target_temp - TEMP_HYSTERESIS
and nb_minutes_since_last_switch >= self._dt
):
_LOGGER.info( _LOGGER.info(
"%s - We need to start, because it will be time to heat", "%s - We need to start, because it will be time to heat",
self, self,
) )
self._last_switch_date = now
return AUTO_START_STOP_ACTION_ON return AUTO_START_STOP_ACTION_ON
else: else:
_LOGGER.debug( _LOGGER.debug(
@@ -210,15 +192,11 @@ class AutoStartStopDetectionAlgorithm:
return AUTO_START_STOP_ACTION_NOTHING return AUTO_START_STOP_ACTION_NOTHING
if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.COOL: if hvac_mode == HVACMode.OFF and saved_hvac_mode == HVACMode.COOL:
if ( if temp_at_dt >= target_temp + TEMP_HYSTERESIS:
temp_at_dt >= target_temp + TEMP_HYSTERESIS
and nb_minutes_since_last_switch >= self._dt
):
_LOGGER.info( _LOGGER.info(
"%s - We need to start, because it will be time to cool", "%s - We need to start, because it will be time to cool",
self, self,
) )
self._last_switch_date = now
return AUTO_START_STOP_ACTION_ON return AUTO_START_STOP_ACTION_ON
else: else:
_LOGGER.debug( _LOGGER.debug(
@@ -257,10 +235,5 @@ class AutoStartStopDetectionAlgorithm:
"""Get the level value""" """Get the level value"""
return self._level return self._level
@property
def last_switch_date(self) -> datetime | None:
"""Get the last of the last switch"""
return self._last_switch_date
def __str__(self) -> str: def __str__(self) -> str:
return f"AutoStartStopDetectionAlgorithm-{self._vtherm_name}" return f"AutoStartStopDetectionAlgorithm-{self._vtherm_name}"

View File

@@ -82,10 +82,6 @@ T = TypeVar("T", bound=UnderlyingEntity)
class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Representation of a base class for all Versatile Thermostat device.""" """Representation of a base class for all Versatile Thermostat device."""
# breaking change with 2024.12 climate workaround
_attr_swing_horizontal_modes = []
_attr_swing_horizontal_mode = ""
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = (
ClimateEntity._entity_component_unrecorded_attributes.union( ClimateEntity._entity_component_unrecorded_attributes.union(
frozenset( frozenset(
@@ -131,7 +127,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"max_power_sensor_entity_id", "max_power_sensor_entity_id",
"temperature_unit", "temperature_unit",
"is_device_active", "is_device_active",
"device_actives", "nb_device_actives",
"target_temperature_step", "target_temperature_step",
"is_used_by_central_boiler", "is_used_by_central_boiler",
"temperature_slope", "temperature_slope",
@@ -1000,19 +996,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return True return True
return False return False
@property
def device_actives(self) -> int:
"""Calculate the active devices"""
ret = []
for under in self._underlyings:
if under.is_device_active:
ret.append(under.entity_id)
return ret
@property @property
def nb_device_actives(self) -> int: def nb_device_actives(self) -> int:
"""Calculate the number of active devices""" """Calculate the number of active devices"""
return len(self.device_actives) ret = 0
for under in self._underlyings:
if under.is_device_active:
ret += 1
return ret
@property @property
def current_temperature(self) -> float | None: def current_temperature(self) -> float | None:
@@ -2685,7 +2676,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"timezone": str(self._current_tz), "timezone": str(self._current_tz),
"temperature_unit": self.temperature_unit, "temperature_unit": self.temperature_unit,
"is_device_active": self.is_device_active, "is_device_active": self.is_device_active,
"device_actives": self.device_actives,
"nb_device_actives": self.nb_device_actives, "nb_device_actives": self.nb_device_actives,
"ema_temp": self._ema_temp, "ema_temp": self._ema_temp,
"is_used_by_central_boiler": self.is_used_by_central_boiler, "is_used_by_central_boiler": self.is_used_by_central_boiler,

View File

@@ -118,7 +118,7 @@ async def async_setup_entry(
SERVICE_SET_AUTO_REGULATION_MODE, SERVICE_SET_AUTO_REGULATION_MODE,
{ {
vol.Required("auto_regulation_mode"): vol.In( vol.Required("auto_regulation_mode"): vol.In(
["None", "Light", "Medium", "Strong", "Slow", "Expert"] ["None", "Light", "Medium", "Strong", "Slow"]
), ),
}, },
"service_set_auto_regulation_mode", "service_set_auto_regulation_mode",

View File

@@ -14,6 +14,6 @@
"quality_scale": "silver", "quality_scale": "silver",
"requirements": [], "requirements": [],
"ssdp": [], "ssdp": [],
"version": "6.8.3", "version": "6.8.0",
"zeroconf": [] "zeroconf": []
} }

View File

@@ -644,10 +644,6 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
"""Representation of the threshold of the number of VTherm """Representation of the threshold of the number of VTherm
which should be active to activate the boiler""" which should be active to activate the boiler"""
_entity_component_unrecorded_attributes = SensorEntity._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
frozenset({"active_device_ids"})
)
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor""" """Initialize the energy sensor"""
self._hass = hass self._hass = hass
@@ -657,14 +653,6 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
self._attr_unique_id = "nb_device_active_boiler" self._attr_unique_id = "nb_device_active_boiler"
self._attr_value = self._attr_native_value = None # default value self._attr_value = self._attr_native_value = None # default value
self._entities = [] self._entities = []
self._attr_active_device_ids = [] # Holds the entity ids of active devices
@property
def extra_state_attributes(self) -> dict:
"""Return additional attributes for the sensor."""
return {
"active_device_ids": self._attr_active_device_ids,
}
@property @property
def icon(self) -> str | None: def icon(self) -> str | None:
@@ -730,19 +718,19 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
self.calculate_nb_active_devices, self.calculate_nb_active_devices,
) )
_LOGGER.info( _LOGGER.info(
"%s - the underlyings that could control the central boiler are %s", "%s - the underlyings that could controls the central boiler are %s",
self, self,
underlying_entities_id, underlying_entities_id,
) )
self.async_on_remove(listener_cancel) self.async_on_remove(listener_cancel)
else: else:
_LOGGER.debug("%s - no VTherm could control the central boiler", self) _LOGGER.debug("%s - no VTherm could controls the central boiler", self)
await self.calculate_nb_active_devices(None) await self.calculate_nb_active_devices(None)
async def calculate_nb_active_devices(self, event: Event): async def calculate_nb_active_devices(self, event: Event):
"""Calculate the number of active VTherm that have an """Calculate the number of active VTherm that have an
influence on the central boiler and update the list of active device names.""" influence on central boiler"""
# _LOGGER.debug("%s- calculate_nb_active_devices - the event is %s ", self, event) # _LOGGER.debug("%s- calculate_nb_active_devices - the event is %s ", self, event)
@@ -769,8 +757,6 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
old_state is not None old_state is not None
and new_state.state == old_state.state and new_state.state == old_state.state
and new_hvac_action == old_hvac_action and new_hvac_action == old_hvac_action
# issue 698 - force recalculation when underlying climate doesn't have any hvac_action
and new_hvac_action is not None
): ):
# A false state change # A false state change
return return
@@ -788,28 +774,20 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
) )
nb_active = 0 nb_active = 0
active_device_ids = []
for entity in self._entities: for entity in self._entities:
device_actives = entity.device_actives nb_active += entity.nb_device_actives
_LOGGER.debug( _LOGGER.debug(
"After examining the hvac_action of %s, device_actives is %s", "After examining the hvac_action of %s, nb_active is %s",
entity.name, entity.name,
device_actives, nb_active,
) )
nb_active += len(device_actives)
active_device_ids.extend(device_actives)
self._attr_native_value = nb_active self._attr_native_value = nb_active
self._attr_active_device_ids = active_device_ids _LOGGER.debug(
"%s - Number of active underlying entities is %s", self, nb_active
)
self.async_write_ha_state() self.async_write_ha_state()
@property
def active_device_ids(self) -> list:
"""Get the list of active device id"""
return self._attr_active_device_ids
def __str__(self): def __str__(self):
return f"VersatileThermostat-{self.name}" return f"VersatileThermostat-{self.name}"

View File

@@ -60,7 +60,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"auto_start_stop_enable", "auto_start_stop_enable",
"auto_start_stop_accumulated_error", "auto_start_stop_accumulated_error",
"auto_start_stop_accumulated_error_threshold", "auto_start_stop_accumulated_error_threshold",
"auto_start_stop_last_switch_date",
"follow_underlying_temp_change", "follow_underlying_temp_change",
} }
) )
@@ -183,8 +182,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
await super()._async_internal_set_temperature(temperature) await super()._async_internal_set_temperature(temperature)
self._regulation_algo.set_target_temp(self.target_temperature) self._regulation_algo.set_target_temp(self.target_temperature)
# Is necessary cause control_heating method will not force the update. # is done by control_heating method. No need to do it here
await self._send_regulated_temperature(force=True) # await self._send_regulated_temperature(force=True)
async def _send_regulated_temperature(self, force=False): async def _send_regulated_temperature(self, force=False):
"""Sends the regulated temperature to all underlying""" """Sends the regulated temperature to all underlying"""
@@ -556,10 +555,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"auto_start_stop_accumulated_error_threshold" "auto_start_stop_accumulated_error_threshold"
] = self._auto_start_stop_algo.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._attr_extra_state_attributes["follow_underlying_temp_change"] = (
self._follow_underlying_temp_change self._follow_underlying_temp_change
) )
@@ -1119,6 +1114,15 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
return self._support_flags return self._support_flags
# We keep the step configured for the VTherm and not the step of the underlying
# @property
# def target_temperature_step(self) -> float | None:
# """Return the supported step of target temperature."""
# if self.underlying_entity(0):
# return self.underlying_entity(0).target_temperature_step
#
# return None
@property @property
def target_temperature_high(self) -> float | None: def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach. """Return the highbound target temperature we try to reach.

View File

@@ -277,15 +277,12 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
return self.valve_open_percent > 0 return self.valve_open_percent > 0
@property @property
def device_actives(self) -> int: def nb_device_actives(self) -> int:
"""Calculate the number of active devices""" """Calculate the number of active devices"""
if self.is_device_active: if self.is_device_active:
return [ return len(self._underlyings_valve_regulation)
under.opening_degree_entity_id
for under in self._underlyings_valve_regulation
]
else: else:
return [] return 0
@property @property
def activable_underlying_entities(self) -> list | None: def activable_underlying_entities(self) -> list | None:

View File

@@ -218,7 +218,7 @@
} }
}, },
"valve_regulation": { "valve_regulation": {
"title": "Auto-régulation par vanne", "title": "Auto-régulation par vanne - {name}",
"description": "Configuration de l'auto-régulation par controle direct de la vanne", "description": "Configuration de l'auto-régulation par controle direct de la vanne",
"data": { "data": {
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''", "offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
@@ -257,7 +257,7 @@
} }
}, },
"menu": { "menu": {
"title": "Menu", "title": "Menu - {name}",
"description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.", "description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.",
"menu_options": { "menu_options": {
"main": "Principaux Attributs", "main": "Principaux Attributs",

View File

@@ -3,5 +3,5 @@
"content_in_root": false, "content_in_root": false,
"render_readme": true, "render_readme": true,
"hide_default_branch": false, "hide_default_branch": false,
"homeassistant": "2024.12.3" "homeassistant": "2024.10.4"
} }

View File

@@ -1 +1 @@
homeassistant==2024.12.3 homeassistant==2024.10.4

View File

@@ -579,7 +579,6 @@ class MockNumber(NumberEntity):
def set_native_value(self, value: float): def set_native_value(self, value: float):
"""Change the value""" """Change the value"""
self._attr_native_value = value self._attr_native_value = value
self.async_write_ha_state()
async def create_thermostat( async def create_thermostat(

View File

@@ -309,7 +309,7 @@ async def test_over_climate_regulation_limitations(
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
# the regulated temperature will not change because when we set temp manually it is forced # the regulated temperature will not change because when we set temp manually it is forced
assert entity.regulated_target_temp == 19.5 assert entity.regulated_target_temp == 17 # 19.5
# 2. set manual target temp (at now - 18) -> the regulation should be taken into account # 2. set manual target temp (at now - 18) -> the regulation should be taken into account
event_timestamp = now - timedelta(minutes=18) event_timestamp = now - timedelta(minutes=18)

View File

@@ -15,7 +15,6 @@ from custom_components.versatile_thermostat.auto_start_stop_algorithm import (
AutoStartStopDetectionAlgorithm, AutoStartStopDetectionAlgorithm,
AUTO_START_STOP_ACTION_NOTHING, AUTO_START_STOP_ACTION_NOTHING,
AUTO_START_STOP_ACTION_OFF, AUTO_START_STOP_ACTION_OFF,
AUTO_START_STOP_ACTION_ON,
) )
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -45,7 +44,6 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
) )
assert ret == AUTO_START_STOP_ACTION_NOTHING assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.accumulated_error == -1 assert algo.accumulated_error == -1
assert algo.last_switch_date is None
# 2. should not stop (accumulated_error too low) # 2. should not stop (accumulated_error too low)
now = now + timedelta(minutes=5) now = now + timedelta(minutes=5)
@@ -59,7 +57,6 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
) )
assert ret == AUTO_START_STOP_ACTION_NOTHING assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.accumulated_error == -6 assert algo.accumulated_error == -6
assert algo.last_switch_date is None
# 3. should not stop (accumulated_error too low) # 3. should not stop (accumulated_error too low)
now = now + timedelta(minutes=2) now = now + timedelta(minutes=2)
@@ -73,7 +70,6 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
) )
assert algo.accumulated_error == -8 assert algo.accumulated_error == -8
assert ret == AUTO_START_STOP_ACTION_NOTHING assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.last_switch_date is None
# 4 .No change on accumulated error because the new measure is too near the last one # 4 .No change on accumulated error because the new measure is too near the last one
now = now + timedelta(seconds=11) now = now + timedelta(seconds=11)
@@ -87,7 +83,6 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
) )
assert algo.accumulated_error == -8 assert algo.accumulated_error == -8
assert ret == AUTO_START_STOP_ACTION_NOTHING assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.last_switch_date is None
# 5. should stop now because accumulated_error is > ERROR_THRESHOLD for slow (10) # 5. should stop now because accumulated_error is > ERROR_THRESHOLD for slow (10)
now = now + timedelta(minutes=4) now = now + timedelta(minutes=4)
@@ -101,9 +96,6 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
) )
assert algo.accumulated_error == -10 assert algo.accumulated_error == -10
assert ret == AUTO_START_STOP_ACTION_OFF assert ret == AUTO_START_STOP_ACTION_OFF
assert algo.last_switch_date is not None
assert algo.last_switch_date == now
last_now = now
# 6. inverse the temperature (target > current) -> accumulated_error should be divided by 2 # 6. inverse the temperature (target > current) -> accumulated_error should be divided by 2
now = now + timedelta(minutes=2) now = now + timedelta(minutes=2)
@@ -117,111 +109,14 @@ async def test_auto_start_stop_algo_slow_heat_off(hass: HomeAssistant):
) )
assert algo.accumulated_error == -4 # -10/2 + 1 assert algo.accumulated_error == -4 # -10/2 + 1
assert ret == AUTO_START_STOP_ACTION_NOTHING assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.last_switch_date == last_now
# 7. change level to slow (no real change) -> error_accumulated should not reset to 0 # 7. change level to slow (no real change) -> error_accumulated should not reset to 0
algo.set_level(AUTO_START_STOP_LEVEL_SLOW) algo.set_level(AUTO_START_STOP_LEVEL_SLOW)
assert algo.accumulated_error == -4 assert algo.accumulated_error == -4
assert algo.last_switch_date == last_now
# 8. change level -> error_accumulated should reset to 0 # 8. change level -> error_accumulated should reset to 0
algo.set_level(AUTO_START_STOP_LEVEL_FAST) algo.set_level(AUTO_START_STOP_LEVEL_FAST)
assert algo.accumulated_error == 0 assert algo.accumulated_error == 0
assert algo.last_switch_date == last_now
async def test_auto_start_stop_too_fast_change(hass: HomeAssistant):
"""Testing directly the algorithm in Slow level"""
algo: AutoStartStopDetectionAlgorithm = AutoStartStopDetectionAlgorithm(
AUTO_START_STOP_LEVEL_SLOW, "testu"
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
assert algo._dt == 30
assert algo._vtherm_name == "testu"
#
# Testing with turn_on
#
# 1. should stop
algo._accumulated_error = -100
ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF,
target_temp=10,
current_temp=21,
slope_min=0.5,
now=now,
)
assert ret == AUTO_START_STOP_ACTION_OFF
assert algo.last_switch_date is not None
assert algo.last_switch_date == now
last_now = now
# 2. now we should turn on but to near the last change -> no nothing to do
now = now + timedelta(minutes=2)
algo._accumulated_error = -100
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.HEAT,
target_temp=21,
current_temp=17,
slope_min=-0.1,
now=now,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.last_switch_date == last_now
# 3. now we should turn on and now is much later ->
now = now + timedelta(minutes=30)
algo._accumulated_error = -100
ret = algo.calculate_action(
hvac_mode=HVACMode.OFF,
saved_hvac_mode=HVACMode.HEAT,
target_temp=21,
current_temp=17,
slope_min=-0.1,
now=now,
)
assert ret == AUTO_START_STOP_ACTION_ON
assert algo.last_switch_date == now
last_now = now
#
# Testing with turn_off
#
# 4. try to turn_off but too speed (29 min)
now = now + timedelta(minutes=29)
algo._accumulated_error = -100
ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF,
target_temp=17,
current_temp=21,
slope_min=0.5,
now=now,
)
assert ret == AUTO_START_STOP_ACTION_NOTHING
assert algo.last_switch_date == last_now
# 5. turn_off much later (29 min + 1 min)
now = now + timedelta(minutes=1)
algo._accumulated_error = -100
ret = algo.calculate_action(
hvac_mode=HVACMode.HEAT,
saved_hvac_mode=HVACMode.OFF,
target_temp=17,
current_temp=21,
slope_min=0.5,
now=now,
)
assert ret == AUTO_START_STOP_ACTION_OFF
assert algo.last_switch_date == now
async def test_auto_start_stop_algo_medium_cool_off(hass: HomeAssistant): async def test_auto_start_stop_algo_medium_cool_off(hass: HomeAssistant):

View File

@@ -2,7 +2,7 @@
""" Test the central_configuration """ """ Test the central_configuration """
import asyncio import asyncio
from datetime import datetime, timedelta from datetime import datetime
from unittest.mock import patch, call from unittest.mock import patch, call
@@ -29,8 +29,6 @@ from custom_components.versatile_thermostat.binary_sensor import (
CentralBoilerBinarySensor, CentralBoilerBinarySensor,
) )
from custom_components.versatile_thermostat.sensor import NbActiveDeviceForBoilerSensor
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -105,7 +103,7 @@ async def test_update_central_boiler_state_simple(
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [switch1.entity_id], CONF_HEATER: switch1.entity_id,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
@@ -149,13 +147,6 @@ async def test_update_central_boiler_state_simple(
assert boiler_binary_sensor is not None assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# 1. start a heater # 1. start a heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -204,9 +195,6 @@ async def test_update_central_boiler_state_simple(
assert api.nb_active_device_for_boiler == 1 assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == ["switch.switch1"]
# 2. stop a heater # 2. stop a heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -247,9 +235,6 @@ async def test_update_central_boiler_state_simple(
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat() entity.remove_thermostat()
@@ -287,12 +272,10 @@ async def test_update_central_boiler_state_multiple(
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [ CONF_HEATER: switch1.entity_id,
switch1.entity_id, CONF_HEATER_2: switch2.entity_id,
switch2.entity_id, CONF_HEATER_3: switch3.entity_id,
switch3.entity_id, CONF_HEATER_4: switch4.entity_id,
switch4.entity_id,
],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
@@ -319,18 +302,10 @@ async def test_update_central_boiler_state_multiple(
assert entity.underlying_entities[1].entity_id == "switch.switch2" assert entity.underlying_entities[1].entity_id == "switch.switch2"
assert entity.underlying_entities[2].entity_id == "switch.switch3" assert entity.underlying_entities[2].entity_id == "switch.switch3"
assert entity.underlying_entities[3].entity_id == "switch.switch4" assert entity.underlying_entities[3].entity_id == "switch.switch4"
assert entity.device_actives == [] assert entity.nb_device_actives == 0
assert api.nb_active_device_for_boiler_threshold == 1 assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# Force the VTherm to heat # Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
@@ -363,7 +338,7 @@ async def test_update_central_boiler_state_multiple(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["switch.switch1"] assert entity.nb_device_actives == 1
assert mock_service_call.call_count == 1 assert mock_service_call.call_count == 1
# No switch of the boiler # No switch of the boiler
@@ -381,9 +356,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 1 assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == ["switch.switch1"]
# 2. start a 2nd heater # 2. start a 2nd heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -396,7 +368,7 @@ async def test_update_central_boiler_state_multiple(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["switch.switch1", "switch.switch2"] assert entity.nb_device_actives == 2
# Only the first heater is started by the algo # Only the first heater is started by the algo
assert mock_service_call.call_count == 1 assert mock_service_call.call_count == 1
@@ -416,12 +388,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 2 assert api.nb_active_device_for_boiler == 2
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 2
assert nb_device_active_sensor.active_device_ids == [
"switch.switch1",
"switch.switch2",
]
# 3. start a 3rd heater # 3. start a 3rd heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -470,13 +436,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 3 assert api.nb_active_device_for_boiler == 3
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 3
assert nb_device_active_sensor.active_device_ids == [
"switch.switch1",
"switch.switch2",
"switch.switch3",
]
# 4. start a 4th heater # 4. start a 4th heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -507,14 +466,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 4 assert api.nb_active_device_for_boiler == 4
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 4
assert nb_device_active_sensor.active_device_ids == [
"switch.switch1",
"switch.switch2",
"switch.switch3",
"switch.switch4",
]
# 5. stop a heater # 5. stop a heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -533,13 +484,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 3 assert api.nb_active_device_for_boiler == 3
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 3
assert nb_device_active_sensor.active_device_ids == [
"switch.switch2",
"switch.switch3",
"switch.switch4",
]
# 6. stop a 2nd heater # 6. stop a 2nd heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -580,12 +524,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 2 assert api.nb_active_device_for_boiler == 2
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 2
assert nb_device_active_sensor.active_device_ids == [
"switch.switch2",
"switch.switch3",
]
entity.remove_thermostat() entity.remove_thermostat()
@@ -620,7 +558,7 @@ async def test_update_central_boiler_state_simple_valve(
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [valve1.entity_id], CONF_VALVE: valve1.entity_id,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
@@ -656,7 +594,7 @@ async def test_update_central_boiler_state_simple_valve(
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
assert entity.hvac_mode == HVACMode.HEAT assert entity.hvac_mode == HVACMode.HEAT
assert entity.device_actives == [] assert entity.nb_device_actives == 0
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity( boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor" hass, "binary_sensor.central_boiler", "binary_sensor"
@@ -664,13 +602,6 @@ async def test_update_central_boiler_state_simple_valve(
assert boiler_binary_sensor is not None assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# 1. start a valve # 1. start a valve
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -685,7 +616,7 @@ async def test_update_central_boiler_state_simple_valve(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["number.valve1"] assert entity.nb_device_actives == 1
assert mock_service_call.call_count >= 1 assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
@@ -713,11 +644,6 @@ async def test_update_central_boiler_state_simple_valve(
assert api.nb_active_device_for_boiler == 1 assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == [
"number.valve1",
]
# 2. stop a heater # 2. stop a heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -732,7 +658,7 @@ async def test_update_central_boiler_state_simple_valve(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.IDLE assert entity.hvac_action == HVACAction.IDLE
assert entity.device_actives == [] assert entity.nb_device_actives == 0
assert mock_service_call.call_count >= 1 assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
@@ -761,9 +687,6 @@ async def test_update_central_boiler_state_simple_valve(
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat() entity.remove_thermostat()
@@ -798,7 +721,7 @@ async def test_update_central_boiler_state_simple_climate(
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [climate1.entity_id], CONF_CLIMATE: climate1.entity_id,
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3, CONF_SECURITY_MIN_ON_PERCENT: 0.3,
@@ -825,13 +748,6 @@ async def test_update_central_boiler_state_simple_climate(
assert api.nb_active_device_for_boiler_threshold == 1 assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# Force the VTherm to heat # Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
@@ -840,7 +756,7 @@ async def test_update_central_boiler_state_simple_climate(
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
assert entity.hvac_mode == HVACMode.HEAT assert entity.hvac_mode == HVACMode.HEAT
assert entity.device_actives == [] assert entity.nb_device_actives == 0
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity( boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor" hass, "binary_sensor.central_boiler", "binary_sensor"
@@ -863,7 +779,7 @@ async def test_update_central_boiler_state_simple_climate(
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["climate.climate1"] assert entity.nb_device_actives == 1
assert mock_service_call.call_count >= 1 assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
@@ -891,11 +807,6 @@ async def test_update_central_boiler_state_simple_climate(
assert api.nb_active_device_for_boiler == 1 assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == [
"climate.climate1",
]
# 2. stop a climate # 2. stop a climate
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -910,7 +821,7 @@ async def test_update_central_boiler_state_simple_climate(
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.IDLE assert entity.hvac_action == HVACAction.IDLE
assert entity.device_actives == [] assert entity.nb_device_actives == 0
assert mock_service_call.call_count >= 1 assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
@@ -939,277 +850,6 @@ async def test_update_central_boiler_state_simple_climate(
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat()
async def test_update_central_boiler_state_simple_climate_valve_regulation(
hass: HomeAssistant,
# skip_hass_states_is_state,
# skip_hass_states_get,
init_central_config_with_boiler_fixture,
):
"""Test that the central boiler state behavior with a climate with valve regulation"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
climate1 = MockClimate(hass, "climate1", "theClimate1")
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [climate1.entity_id],
CONF_OPENING_DEGREE_LIST: ["number.mock_opening_degree"],
CONF_CLOSING_DEGREE_LIST: [],
CONF_OFFSET_CALIBRATION_LIST: [],
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE,
CONF_AUTO_REGULATION_DTEMP: 0,
CONF_AUTO_REGULATION_PERIOD_MIN: 0,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.1,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
open_degree_entity = MockNumber(hass, "mock_opening_degree", "Opening degree")
open_degree_entity.set_native_value(0)
# mock_get_state will be called for each OPENING/CLOSING/OFFSET_CALIBRATION list
mock_get_state_side_effect = SideEffects(
{
open_degree_entity.entity_id: State(
open_degree_entity.entity_id,
open_degree_entity.state,
{"min": 0, "max": 100},
),
"number.mock_closing_degree": State(
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
),
"number.mock_offset_calibration": State(
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=climate1,
), patch(
"homeassistant.core.StateMachine.get",
side_effect=mock_get_state_side_effect.get_side_effects(),
):
entity: ThermostatOverClimate = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate
assert entity.underlying_entities[0].entity_id == "climate.climate1"
assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# Force the VTherm to heat
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entity._set_now(now)
await send_temperature_change_event(entity, 30, now)
await send_ext_temperature_change_event(entity, 30, now)
await hass.async_block_till_done()
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
# the VTherm should not heat now
assert entity.hvac_mode == HVACMode.HEAT
assert entity.hvac_action == HVACAction.OFF
assert entity.activable_underlying_entities[0]._percent_open == 0
assert entity.device_actives == []
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor"
)
assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF
# 1. start a climate
open_degree_entity.set_native_value(100)
mock_get_state_side_effect = SideEffects(
{
open_degree_entity.entity_id: State(
open_degree_entity.entity_id,
open_degree_entity.state,
{"min": 0, "max": 100},
),
"number.mock_closing_degree": State(
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
),
"number.mock_offset_calibration": State(
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event, patch(
"homeassistant.core.StateMachine.get",
side_effect=mock_get_state_side_effect.get_side_effects(),
):
now = now + timedelta(minutes=1)
entity._set_now(now)
await send_temperature_change_event(entity, 10, now)
# we have to simulate the climate also else the test don't work
climate1.set_hvac_mode(HVACMode.HEAT)
climate1.set_hvac_action(HVACAction.HEATING)
climate1.async_write_ha_state()
open_degree_entity.set_native_value(100)
# Wait for state event propagation
await hass.async_block_till_done()
assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["number.mock_opening_degree"]
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == [
"number.mock_opening_degree",
]
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
),
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": True},
)
]
)
# 2. stop a climate
open_degree_entity.set_native_value(0)
mock_get_state_side_effect = SideEffects(
{
open_degree_entity.entity_id: State(
open_degree_entity.entity_id,
open_degree_entity.state,
{"min": 0, "max": 100},
),
"number.mock_closing_degree": State(
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
),
"number.mock_offset_calibration": State(
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event, patch(
"homeassistant.core.StateMachine.get",
side_effect=mock_get_state_side_effect.get_side_effects(),
):
await send_temperature_change_event(entity, 25, now)
climate1.set_hvac_mode(HVACMode.HEAT)
climate1.set_hvac_action(HVACAction.IDLE)
climate1.async_write_ha_state()
open_degree_entity.set_native_value(0)
# Wait for state event propagation
await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.OFF
assert entity.device_actives == []
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call(
"switch",
"turn_off",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
)
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": False},
)
]
)
assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat() entity.remove_thermostat()

View File

@@ -18,8 +18,8 @@ from .const import *
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True]) # @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) # @pytest.mark.parametrize("expected_lingering_timers", [True])
# this test fails if run in // with the next because the underlying_valve_regulation is mixed. Don't know why # this test fails if run in // with the next because the underlying_valve_regulation is mixed. Don't know why
# @pytest.mark.skip # @pytest.mark.skip
async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get): async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get):
@@ -138,13 +138,13 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
assert mock_service_call.call_count == 3 assert mock_service_call.call_count == 3
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
call("climate","set_temperature",{ call("climate","set_temperature",{
"entity_id": "climate.mock_climate", "entity_id": "climate.mock_climate",
"temperature": 15, # temp-min "temperature": 15, # temp-min
}, },
), ),
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
# we have no current_temperature yet # we have no current_temperature yet
# call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}), # call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}),
] ]
@@ -300,7 +300,6 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
await hass.async_block_till_done() await hass.async_block_till_done()
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_valve_multi_presence( async def test_over_climate_valve_multi_presence(
hass: HomeAssistant, skip_hass_states_get hass: HomeAssistant, skip_hass_states_get
): ):