All tests ok. Add a multi test for climate with valve regulation

This commit is contained in:
Jean-Marc Collin
2024-11-24 12:09:11 +00:00
parent 36cab0c91f
commit ce4ea866cb
10 changed files with 141 additions and 277 deletions

View File

@@ -9,7 +9,6 @@ from datetime import timedelta, datetime
from types import MappingProxyType from types import MappingProxyType
from typing import Any, TypeVar, Generic from typing import Any, TypeVar, Generic
from homeassistant.util import dt as dt_util
from homeassistant.core import ( from homeassistant.core import (
HomeAssistant, HomeAssistant,
callback, callback,
@@ -80,13 +79,6 @@ _LOGGER = logging.getLogger(__name__)
ConfigData = MappingProxyType[str, Any] ConfigData = MappingProxyType[str, Any]
T = TypeVar("T", bound=UnderlyingEntity) T = TypeVar("T", bound=UnderlyingEntity)
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
return dt_util.get_time_zone(hass.config.time_zone)
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."""
@@ -2293,7 +2285,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
@property @property
def now(self) -> datetime: def now(self) -> datetime:
"""Get now. The local datetime or the overloaded _set_now date""" """Get now. The local datetime or the overloaded _set_now date"""
return self._now if self._now is not None else datetime.now(self._current_tz) return self._now if self._now is not None else NowClass.get_now(self._hass)
async def check_safety(self) -> bool: async def check_safety(self) -> bool:
"""Check if last temperature date is too long""" """Check if last temperature date is too long"""

View File

@@ -3,38 +3,20 @@
# pylint: disable=line-too-long # pylint: disable=line-too-long
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta
from homeassistant.core import HomeAssistant, callback, Event from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.event import async_track_state_change_event, async_call_later from homeassistant.helpers.event import async_track_state_change_event, async_call_later
from homeassistant.util import dt as dt_util
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat
from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError from .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
return dt_util.get_time_zone(hass.config.time_zone)
class NowClass:
"""For testing purpose only"""
@staticmethod
def get_now(hass: HomeAssistant) -> datetime:
"""A test function to get the now.
For testing purpose this method can be overriden to get a specific
timestamp.
"""
return datetime.now(get_tz(hass))
def round_to_nearest(n: float, x: float) -> float: def round_to_nearest(n: float, x: float) -> float:
"""Round a number to the nearest x (which should be decimal but not null) """Round a number to the nearest x (which should be decimal but not null)
Example: Example:

View File

@@ -4,8 +4,10 @@
import logging import logging
import math import math
from typing import Literal from typing import Literal
from datetime import datetime
from enum import Enum from enum import Enum
from homeassistant.core import HomeAssistant
from homeassistant.const import CONF_NAME, Platform from homeassistant.const import CONF_NAME, Platform
from homeassistant.components.climate import ( from homeassistant.components.climate import (
@@ -17,6 +19,7 @@ from homeassistant.components.climate import (
) )
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import dt as dt_util
from .prop_algorithm import ( from .prop_algorithm import (
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
@@ -506,6 +509,24 @@ def get_safe_float(hass, entity_id: str):
return None if math.isinf(float_val) or not math.isfinite(float_val) else float_val return None if math.isinf(float_val) or not math.isfinite(float_val) else float_val
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
return dt_util.get_time_zone(hass.config.time_zone)
class NowClass:
"""For testing purpose only"""
@staticmethod
def get_now(hass: HomeAssistant) -> datetime:
"""A test function to get the now.
For testing purpose this method can be overriden to get a specific
timestamp.
"""
return datetime.now(get_tz(hass))
class UnknownEntity(HomeAssistantError): class UnknownEntity(HomeAssistantError):
"""Error to indicate there is an unknown entity_id given.""" """Error to indicate there is an unknown entity_id given."""

View File

@@ -16,7 +16,7 @@ from homeassistant.components.climate import (
ClimateEntityFeature, ClimateEntityFeature,
) )
from .commons import NowClass, round_to_nearest from .commons import round_to_nearest
from .base_thermostat import BaseThermostat, ConfigData from .base_thermostat import BaseThermostat, ConfigData
from .pi_algorithm import PITemperatureRegulator from .pi_algorithm import PITemperatureRegulator
@@ -90,7 +90,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
# super.__init__ calls post_init at the end. So it must be called after regulation initialization # super.__init__ calls post_init at the end. So it must be called after regulation initialization
super().__init__(hass, unique_id, name, entry_infos) super().__init__(hass, unique_id, name, entry_infos)
self._regulated_target_temp = self.target_temperature self._regulated_target_temp = self.target_temperature
self._last_regulation_change = NowClass.get_now(hass) self._last_regulation_change = None # NowClass.get_now(hass)
@overrides @overrides
def post_init(self, config_entry: ConfigData): def post_init(self, config_entry: ConfigData):
@@ -206,16 +206,18 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
force, force,
) )
now: datetime = NowClass.get_now(self._hass) if self._last_regulation_change is not None:
period = float((now - self._last_regulation_change).total_seconds()) / 60.0 period = (
if not force and period < self._auto_regulation_period_min: float((self.now - self._last_regulation_change).total_seconds()) / 60.0
_LOGGER.info(
"%s - period (%.1f) min is < %.0f min -> forget the regulation send",
self,
period,
self._auto_regulation_period_min,
) )
return if not force and period < self._auto_regulation_period_min:
_LOGGER.info(
"%s - period (%.1f) min is < %.0f min -> forget the regulation send",
self,
period,
self._auto_regulation_period_min,
)
return
if not self._regulated_target_temp: if not self._regulated_target_temp:
self._regulated_target_temp = self.target_temperature self._regulated_target_temp = self.target_temperature
@@ -253,7 +255,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
new_regulated_temp, new_regulated_temp,
) )
self._last_regulation_change = now self._last_regulation_change = self.now
for under in self._underlyings: for under in self._underlyings:
# issue 348 - use device temperature if configured as offset # issue 348 - use device temperature if configured as offset
offset_temp = 0 offset_temp = 0

View File

@@ -31,10 +31,6 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import
from custom_components.versatile_thermostat.underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import from custom_components.versatile_thermostat.underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import
from custom_components.versatile_thermostat.commons import ( # pylint: disable=unused-import
get_tz,
NowClass,
)
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI

View File

@@ -46,7 +46,7 @@ async def test_over_climate_regulation(
event_timestamp = now - timedelta(minutes=10) event_timestamp = now - timedelta(minutes=10)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
), patch( ), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
@@ -87,7 +87,7 @@ async def test_over_climate_regulation(
# set manual target temp (at now - 7) -> the regulation should occurs # set manual target temp (at now - 7) -> the regulation should occurs
event_timestamp = now - timedelta(minutes=7) event_timestamp = now - timedelta(minutes=7)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
): ):
await entity.async_set_temperature(temperature=18) await entity.async_set_temperature(temperature=18)
@@ -108,7 +108,7 @@ async def test_over_climate_regulation(
# change temperature so that the regulated temperature should slow down # change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=5) event_timestamp = now - timedelta(minutes=5)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
): ):
await send_temperature_change_event(entity, 23, event_timestamp) await send_temperature_change_event(entity, 23, event_timestamp)
@@ -144,7 +144,7 @@ async def test_over_climate_regulation_ac_mode(
event_timestamp = now - timedelta(minutes=10) event_timestamp = now - timedelta(minutes=10)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
), patch( ), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
@@ -183,7 +183,7 @@ async def test_over_climate_regulation_ac_mode(
# set manual target temp # set manual target temp
event_timestamp = now - timedelta(minutes=7) event_timestamp = now - timedelta(minutes=7)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
): ):
await entity.async_set_temperature(temperature=25) await entity.async_set_temperature(temperature=25)
@@ -204,7 +204,7 @@ async def test_over_climate_regulation_ac_mode(
# change temperature so that the regulated temperature should slow down # change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=5) event_timestamp = now - timedelta(minutes=5)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
): ):
await send_temperature_change_event(entity, 26, event_timestamp) await send_temperature_change_event(entity, 26, event_timestamp)
@@ -219,7 +219,7 @@ async def test_over_climate_regulation_ac_mode(
# change temperature so that the regulated temperature should slow down # change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=3)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
): ):
await send_temperature_change_event(entity, 18, event_timestamp) await send_temperature_change_event(entity, 18, event_timestamp)
@@ -260,7 +260,7 @@ async def test_over_climate_regulation_limitations(
event_timestamp = now - timedelta(minutes=20) event_timestamp = now - timedelta(minutes=20)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
), patch( ), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
@@ -286,71 +286,61 @@ async def test_over_climate_regulation_limitations(
assert entity.is_over_climate is True assert entity.is_over_climate is True
assert entity.is_regulated is True assert entity.is_regulated is True
entity._set_now(event_timestamp)
# Will initialize the _last_regulation_change
# Activate the heating by changing HVACMode and temperature # Activate the heating by changing HVACMode and temperature
# Select a hvacmode, presence and preset # Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
await entity.async_set_temperature(temperature=17)
# it is cold today # it is cold today
await send_temperature_change_event(entity, 15, event_timestamp) await send_temperature_change_event(entity, 15, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp) await send_ext_temperature_change_event(entity, 10, event_timestamp)
# set manual target temp (at now - 19) -> the regulation should be ignored because too early # 1. set manual target temp (at now - 19) -> the regulation should be ignored because too early
event_timestamp = now - timedelta(minutes=19) event_timestamp = now - timedelta(minutes=19)
with patch( entity._set_now(event_timestamp)
"custom_components.versatile_thermostat.commons.NowClass.get_now", await entity.async_set_temperature(temperature=18)
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=18)
fake_underlying_climate.set_hvac_action( fake_underlying_climate.set_hvac_action(
HVACAction.HEATING HVACAction.HEATING
) # simulate under heating ) # simulate under heating
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
# the regulated temperature will 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
# 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)
with patch( entity._set_now(event_timestamp)
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=17)
assert entity.regulated_target_temp > entity.target_temperature
assert (
entity.regulated_target_temp == 18 + 0
) # In strong we could go up to +3 degre. 0.7 without round_to_nearest
old_regulated_temp = entity.regulated_target_temp
# change temperature so that dtemp < 0.5 and time is > period_min (+ 3min) await entity.async_set_temperature(temperature=17)
assert entity.regulated_target_temp > entity.target_temperature
assert (
entity.regulated_target_temp == 18 + 0
) # In strong we could go up to +3 degre. 0.7 without round_to_nearest
old_regulated_temp = entity.regulated_target_temp
# 3. change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
event_timestamp = now - timedelta(minutes=15) event_timestamp = now - timedelta(minutes=15)
with patch( entity._set_now(event_timestamp)
"custom_components.versatile_thermostat.commons.NowClass.get_now", await send_temperature_change_event(entity, 16, event_timestamp)
return_value=event_timestamp, await send_ext_temperature_change_event(entity, 10, event_timestamp)
):
await send_temperature_change_event(entity, 16, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# the regulated temperature should be under # the regulated temperature should be under
assert entity.regulated_target_temp <= old_regulated_temp assert entity.regulated_target_temp <= old_regulated_temp
# change temperature so that dtemp > 0.5 and time is > period_min (+ 3min) # 4. change temperature so that dtemp > 0.5 and time is > period_min (+ 3min)
event_timestamp = now - timedelta(minutes=12) event_timestamp = now - timedelta(minutes=12)
with patch( entity._set_now(event_timestamp)
"custom_components.versatile_thermostat.commons.NowClass.get_now", await send_ext_temperature_change_event(entity, 12, event_timestamp)
return_value=event_timestamp, await send_temperature_change_event(entity, 15, event_timestamp)
):
await send_temperature_change_event(entity, 15, 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 + 1.5 # 0.7 without round_to_nearest
entity.regulated_target_temp == 17 + 1.5
) # 0.7 without round_to_nearest
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -383,7 +373,7 @@ async def test_over_climate_regulation_use_device_temp(
event_timestamp = now - timedelta(minutes=10) event_timestamp = now - timedelta(minutes=10)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
), patch( ), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
@@ -416,7 +406,7 @@ async def test_over_climate_regulation_use_device_temp(
fake_underlying_climate.set_current_temperature(15) fake_underlying_climate.set_current_temperature(15)
event_timestamp = now - timedelta(minutes=7) event_timestamp = now - timedelta(minutes=7)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: ), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
await entity.async_set_temperature(temperature=16) await entity.async_set_temperature(temperature=16)
@@ -462,7 +452,7 @@ async def test_over_climate_regulation_use_device_temp(
event_timestamp = now - timedelta(minutes=5) event_timestamp = now - timedelta(minutes=5)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: ), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
await send_temperature_change_event(entity, 15, event_timestamp) await send_temperature_change_event(entity, 15, event_timestamp)
@@ -497,7 +487,7 @@ async def test_over_climate_regulation_use_device_temp(
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=3)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: ), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
await send_temperature_change_event(entity, 25, event_timestamp) await send_temperature_change_event(entity, 25, event_timestamp)
@@ -545,7 +535,7 @@ async def test_over_climate_regulation_dtemp_null(
event_timestamp = now - timedelta(minutes=20) event_timestamp = now - timedelta(minutes=20)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
), patch( ), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
@@ -573,7 +563,7 @@ async def test_over_climate_regulation_dtemp_null(
# set manual target temp # set manual target temp
event_timestamp = now - timedelta(minutes=17) event_timestamp = now - timedelta(minutes=17)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
): ):
await entity.async_set_temperature(temperature=20) await entity.async_set_temperature(temperature=20)
@@ -594,7 +584,7 @@ async def test_over_climate_regulation_dtemp_null(
# change temperature so that the regulated temperature should slow down # change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=15) event_timestamp = now - timedelta(minutes=15)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
): ):
await send_temperature_change_event(entity, 19, event_timestamp) await send_temperature_change_event(entity, 19, event_timestamp)
@@ -607,7 +597,7 @@ async def test_over_climate_regulation_dtemp_null(
# change temperature so that the regulated temperature should slow down # change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=13) event_timestamp = now - timedelta(minutes=13)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
): ):
await send_temperature_change_event(entity, 20, event_timestamp) await send_temperature_change_event(entity, 20, event_timestamp)
@@ -621,7 +611,7 @@ async def test_over_climate_regulation_dtemp_null(
# Test if a small temperature change is taken into account : change temperature so that dtemp < 0.5 and time is > period_min (+ 3min) # Test if a small temperature change is taken into account : change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
event_timestamp = now - timedelta(minutes=10) event_timestamp = now - timedelta(minutes=10)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp, return_value=event_timestamp,
): ):
await send_temperature_change_event(entity, 19.6, event_timestamp) await send_temperature_change_event(entity, 19.6, event_timestamp)

View File

@@ -161,19 +161,6 @@ async def test_bug_272(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call: ) as mock_service_call:
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname") entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
# entry.add_to_hass(hass)
# await hass.config_entries.async_setup(entry.entry_id)
# assert entry.state is ConfigEntryState.LOADED
#
# def find_my_entity(entity_id) -> ClimateEntity:
# """Find my new entity"""
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
# for entity in component.entities:
# if entity.entity_id == entity_id:
# return entity
#
# entity = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
assert entity.name == "TheOverClimateMockName" assert entity.name == "TheOverClimateMockName"
@@ -215,16 +202,18 @@ async def test_bug_272(
) )
tz = get_tz(hass) # pylint: disable=invalid-name tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz) event_timestamp: datetime = datetime.now(tz=tz)
entity._set_now(now)
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: ), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
# Set room temperature to something very cold # Set room temperature to something very cold
event_timestamp = now + timedelta(minutes=1) await send_temperature_change_event(entity, 13, now)
await send_ext_temperature_change_event(entity, 9, now)
await send_temperature_change_event(entity, 13, event_timestamp) event_timestamp = event_timestamp + timedelta(minutes=3)
await send_ext_temperature_change_event(entity, 9, event_timestamp) entity._set_now(event_timestamp)
# Not in the accepted interval (15-19) # Not in the accepted interval (15-19)
await entity.async_set_temperature(temperature=10) await entity.async_set_temperature(temperature=10)
@@ -248,12 +237,15 @@ async def test_bug_272(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: ), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
# Set room temperature to something very cold # Set room temperature to something very cold
event_timestamp = now + timedelta(minutes=1) event_timestamp = event_timestamp + timedelta(minutes=1)
entity._set_now(event_timestamp)
await send_temperature_change_event(entity, 13, event_timestamp) await send_temperature_change_event(entity, 13, event_timestamp)
await send_ext_temperature_change_event(entity, 9, event_timestamp) await send_ext_temperature_change_event(entity, 9, event_timestamp)
# In the accepted interval # In the accepted interval
event_timestamp = event_timestamp + timedelta(minutes=3)
entity._set_now(event_timestamp)
await entity.async_set_temperature(temperature=20.8) await entity.async_set_temperature(temperature=20.8)
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(

View File

@@ -517,6 +517,9 @@ async def test_bug_508(
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
) )
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# Min_temp is 10 and max_temp is 31 and features contains TARGET_TEMPERATURE_RANGE # Min_temp is 10 and max_temp is 31 and features contains TARGET_TEMPERATURE_RANGE
fake_underlying_climate = MagicMockClimateWithTemperatureRange() fake_underlying_climate = MagicMockClimateWithTemperatureRange()
@@ -545,6 +548,9 @@ async def test_bug_508(
# Set the hvac_mode to HEAT # Set the hvac_mode to HEAT
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
now = now + timedelta(minutes=3) # avoid temporal filter
entity._set_now(now)
# Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low # Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low
await entity.async_set_temperature(temperature=8.5) await entity.async_set_temperature(temperature=8.5)
@@ -568,6 +574,9 @@ async def test_bug_508(
with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
# Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low # Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low
now = now + timedelta(minutes=3) # avoid temporal filter
entity._set_now(now)
await entity.async_set_temperature(temperature=32) await entity.async_set_temperature(temperature=32)
# MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call # MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call

View File

@@ -136,15 +136,15 @@ 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'}),
# we have no current_temperature yet
# call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}),
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
# call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}),
] ]
) )
@@ -416,6 +416,7 @@ async def test_over_climate_valve_multi_presence(
assert vtherm.target_temperature == 17.2 assert vtherm.target_temperature == 17.2
# 2: set presence on -> should activate the valve and change target
# fmt: off # fmt: off
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
@@ -432,164 +433,43 @@ async def test_over_climate_valve_multi_presence(
# the underlying set temperature call and the call to the valve # the underlying set temperature call and the call to the valve
assert mock_service_call.call_count == 8 assert mock_service_call.call_count == 8
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls([
[ call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate1', 'temperature': 19.0}),
call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree1'}), call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate2', 'temperature': 19.0}),
call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree1'}), call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree1'}),
call(domain='number', service='set_value', service_data={'value': 3}, target={'entity_id': 'number.mock_offset_calibration1'}), call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree1'}),
call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree2'}), call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree2'}), call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree2'}),
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'}), call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree2'}),
call("climate","set_temperature",{ call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
"entity_id": "climate.mock_climate1",
"temperature": 19,
},
),
call("climate","set_temperature",{
"entity_id": "climate.mock_climate2",
"temperature": 19,
},
),
] ]
) )
assert mock_get_state.call_count > 5 # each temp sensor + each valve # 3: set presence off -> should deactivate the valve and change target
# 2. Starts heating slowly (18 vs 19)
now = now + timedelta(minutes=1)
vtherm._set_now(now)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
# fmt: off # fmt: off
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\ patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state: patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
# fmt: on # fmt: on
now = now + timedelta(minutes=2) # avoid temporal filter now = now + timedelta(minutes=3)
vtherm._set_now(now) vtherm._set_now(now)
await vtherm.async_set_preset_mode(PRESET_COMFORT) await send_presence_change_event(vtherm, False, True, now)
await hass.async_block_till_done() await hass.async_block_till_done()
assert vtherm.hvac_mode is HVACMode.HEAT
assert vtherm.preset_mode is PRESET_COMFORT
assert vtherm.target_temperature == 19
assert vtherm.current_temperature == 18
assert vtherm.valve_open_percent == 40 # 0.3*1 + 0.1*1
assert mock_service_call.call_count == 4
mock_service_call.assert_has_calls(
[
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate', 'temperature': 19.0}),
call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree'}),
# 3 = 18 (room) - 15 (current of underlying) + 0 (current offset)
call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration'})
]
)
# set the opening to 40%
mock_get_state_side_effect.add_or_update_side_effect(
"number.mock_opening_degree",
State(
"number.mock_opening_degree", "40", {"min": 0, "max": 100}
))
assert vtherm.hvac_action is HVACAction.HEATING
assert vtherm.is_device_active is True
# 2. Starts heating very slowly (18.9 vs 19)
now = now + timedelta(minutes=2)
vtherm._set_now(now)
# fmt: off
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
# fmt: on
# set the offset to 3
mock_get_state_side_effect.add_or_update_side_effect(
"number.mock_offset_calibration",
State(
"number.mock_offset_calibration", "3", {"min": -12, "max": 12}
))
await send_temperature_change_event(vtherm, 18.9, now, True)
await hass.async_block_till_done()
assert vtherm.hvac_mode is HVACMode.HEAT
assert vtherm.preset_mode is PRESET_COMFORT
assert vtherm.target_temperature == 19
assert vtherm.current_temperature == 18.9
assert vtherm.valve_open_percent == 13 # 0.3*0.1 + 0.1*1
assert mock_service_call.call_count == 3
mock_service_call.assert_has_calls(
[
call(domain='number', service='set_value', service_data={'value': 13}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 87}, target={'entity_id': 'number.mock_closing_degree'}),
# 6 = 18 (room) - 15 (current of underlying) + 3 (current offset)
call(domain='number', service='set_value', service_data={'value': 6.899999999999999}, target={'entity_id': 'number.mock_offset_calibration'})
]
)
# set the opening to 13%
mock_get_state_side_effect.add_or_update_side_effect(
"number.mock_opening_degree",
State(
"number.mock_opening_degree", "13", {"min": 0, "max": 100}
))
assert vtherm.hvac_action is HVACAction.HEATING
assert vtherm.is_device_active is True
# 3. Stop heating 21 > 19
now = now + timedelta(minutes=2)
vtherm._set_now(now)
# fmt: off
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
# fmt: on
# set the offset to 3
mock_get_state_side_effect.add_or_update_side_effect(
"number.mock_offset_calibration",
State(
"number.mock_offset_calibration", "3", {"min": -12, "max": 12}
))
await send_temperature_change_event(vtherm, 21, now, True)
await hass.async_block_till_done()
assert vtherm.hvac_mode is HVACMode.HEAT
assert vtherm.preset_mode is PRESET_COMFORT
assert vtherm.target_temperature == 19
assert vtherm.current_temperature == 21
assert vtherm.valve_open_percent == 0 # 0.3* (-2) + 0.1*1
assert mock_service_call.call_count == 3
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'}),
# 6 = 18 (room) - 15 (current of underlying) + 3 (current offset)
call(domain='number', service='set_value', service_data={'value': 9.0}, target={'entity_id': 'number.mock_offset_calibration'})
]
)
# set the opening to 13%
mock_get_state_side_effect.add_or_update_side_effect(
"number.mock_opening_degree",
State(
"number.mock_opening_degree", "0", {"min": 0, "max": 100}
))
assert vtherm.hvac_action is HVACAction.OFF
assert vtherm.is_device_active is False assert vtherm.is_device_active is False
assert vtherm.valve_open_percent == 0
# the underlying set temperature call and the call to the valve
assert mock_service_call.call_count == 8
await hass.async_block_till_done() mock_service_call.assert_has_calls([
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate1', 'temperature': 17.2}),
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate2', 'temperature': 17.2}),
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree1'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree1'}),
call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree2'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree2'}),
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
]
)

View File

@@ -17,7 +17,7 @@ from custom_components.versatile_thermostat.base_thermostat import BaseThermosta
from custom_components.versatile_thermostat.thermostat_switch import ( from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch, ThermostatOverSwitch,
) )
from custom_components.versatile_thermostat.commons import NowClass from custom_components.versatile_thermostat.const import NowClass
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from .commons import * from .commons import *