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 typing import Any, TypeVar, Generic
from homeassistant.util import dt as dt_util
from homeassistant.core import (
HomeAssistant,
callback,
@@ -80,13 +79,6 @@ _LOGGER = logging.getLogger(__name__)
ConfigData = MappingProxyType[str, Any]
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]):
"""Representation of a base class for all Versatile Thermostat device."""
@@ -2293,7 +2285,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
@property
def now(self) -> datetime:
"""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:
"""Check if last temperature date is too long"""

View File

@@ -3,38 +3,20 @@
# pylint: disable=line-too-long
import logging
from datetime import timedelta, datetime
from datetime import timedelta
from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
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 .const import DOMAIN, DEVICE_MANUFACTURER, ServiceConfigurationError
_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:
"""Round a number to the nearest x (which should be decimal but not null)
Example:

View File

@@ -4,8 +4,10 @@
import logging
import math
from typing import Literal
from datetime import datetime
from enum import Enum
from homeassistant.core import HomeAssistant
from homeassistant.const import CONF_NAME, Platform
from homeassistant.components.climate import (
@@ -17,6 +19,7 @@ from homeassistant.components.climate import (
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import dt as dt_util
from .prop_algorithm import (
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
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):
"""Error to indicate there is an unknown entity_id given."""

View File

@@ -16,7 +16,7 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
)
from .commons import NowClass, round_to_nearest
from .commons import round_to_nearest
from .base_thermostat import BaseThermostat, ConfigData
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__(hass, unique_id, name, entry_infos)
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
def post_init(self, config_entry: ConfigData):
@@ -206,16 +206,18 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
force,
)
now: datetime = NowClass.get_now(self._hass)
period = float((now - self._last_regulation_change).total_seconds()) / 60.0
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,
if self._last_regulation_change is not None:
period = (
float((self.now - self._last_regulation_change).total_seconds()) / 60.0
)
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:
self._regulated_target_temp = self.target_temperature
@@ -253,7 +255,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
new_regulated_temp,
)
self._last_regulation_change = now
self._last_regulation_change = self.now
for under in self._underlyings:
# issue 348 - use device temperature if configured as offset
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.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.commons import ( # pylint: disable=unused-import
get_tz,
NowClass,
)
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)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp,
), patch(
"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
event_timestamp = now - timedelta(minutes=7)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp,
):
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
event_timestamp = now - timedelta(minutes=5)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=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)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp,
), patch(
"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
event_timestamp = now - timedelta(minutes=7)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp,
):
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
event_timestamp = now - timedelta(minutes=5)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=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
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=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)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp,
), patch(
"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_regulated is True
entity._set_now(event_timestamp)
# Will initialize the _last_regulation_change
# Activate the heating by changing HVACMode and temperature
# Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT
await entity.async_set_temperature(temperature=17)
# it is cold today
await send_temperature_change_event(entity, 15, 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)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=18)
entity._set_now(event_timestamp)
await entity.async_set_temperature(temperature=18)
fake_underlying_climate.set_hvac_action(
HVACAction.HEATING
) # simulate under heating
assert entity.hvac_action == HVACAction.HEATING
fake_underlying_climate.set_hvac_action(
HVACAction.HEATING
) # simulate under heating
assert entity.hvac_action == HVACAction.HEATING
# the regulated temperature will change because when we set temp manually it is forced
assert entity.regulated_target_temp == 19.5
# the regulated temperature will not change because when we set temp manually it is forced
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)
with patch(
"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
entity._set_now(event_timestamp)
# 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)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 16, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
entity._set_now(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
assert entity.regulated_target_temp <= old_regulated_temp
# the regulated temperature should be under
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)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 15, event_timestamp)
await send_ext_temperature_change_event(entity, 12, event_timestamp)
entity._set_now(event_timestamp)
await send_ext_temperature_change_event(entity, 12, event_timestamp)
await send_temperature_change_event(entity, 15, event_timestamp)
# the regulated should have been done
assert entity.regulated_target_temp != old_regulated_temp
assert entity.regulated_target_temp >= entity.target_temperature
assert (
entity.regulated_target_temp == 17 + 1.5
) # 0.7 without round_to_nearest
# the regulated should have been done
assert entity.regulated_target_temp != old_regulated_temp
assert entity.regulated_target_temp >= entity.target_temperature
assert entity.regulated_target_temp == 17 + 1.5 # 0.7 without round_to_nearest
@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)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp,
), patch(
"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)
event_timestamp = now - timedelta(minutes=7)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp,
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
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)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp,
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
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)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp,
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
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)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp,
), patch(
"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
event_timestamp = now - timedelta(minutes=17)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=event_timestamp,
):
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
event_timestamp = now - timedelta(minutes=15)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=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
event_timestamp = now - timedelta(minutes=13)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=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)
event_timestamp = now - timedelta(minutes=10)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
"custom_components.versatile_thermostat.const.NowClass.get_now",
return_value=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"
) as mock_service_call:
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.name == "TheOverClimateMockName"
@@ -215,16 +202,18 @@ async def test_bug_272(
)
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(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
# 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)
await send_ext_temperature_change_event(entity, 9, event_timestamp)
event_timestamp = event_timestamp + timedelta(minutes=3)
entity._set_now(event_timestamp)
# Not in the accepted interval (15-19)
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"
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
# 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_ext_temperature_change_event(entity, 9, event_timestamp)
# In the accepted interval
event_timestamp = event_timestamp + timedelta(minutes=3)
entity._set_now(event_timestamp)
await entity.async_set_temperature(temperature=20.8)
assert mock_service_call.call_count == 1
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
)
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
fake_underlying_climate = MagicMockClimateWithTemperatureRange()
@@ -545,6 +548,9 @@ async def test_bug_508(
# Set the hvac_mode to 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
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:
# 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)
# 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
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",{
"entity_id": "climate.mock_climate",
"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
# 2: set presence on -> should activate the valve and change target
# 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,\
@@ -432,164 +433,43 @@ async def test_over_climate_valve_multi_presence(
# the underlying set temperature call and the call to the valve
assert mock_service_call.call_count == 8
mock_service_call.assert_has_calls(
[
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': 60}, target={'entity_id': 'number.mock_closing_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': 40}, target={'entity_id': 'number.mock_opening_degree2'}),
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': 12}, target={'entity_id': 'number.mock_offset_calibration2'}),
call("climate","set_temperature",{
"entity_id": "climate.mock_climate1",
"temperature": 19,
},
),
call("climate","set_temperature",{
"entity_id": "climate.mock_climate2",
"temperature": 19,
},
),
mock_service_call.assert_has_calls([
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate1', 'temperature': 19.0}),
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate2', 'temperature': 19.0}),
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': 60}, 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': 40}, target={'entity_id': 'number.mock_opening_degree2'}),
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': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
]
)
assert mock_get_state.call_count > 5 # each temp sensor + each valve
# 2. Starts heating slowly (18 vs 19)
now = now + timedelta(minutes=1)
vtherm._set_now(now)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
# 3: set presence off -> should deactivate the valve and change target
# 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
now = now + timedelta(minutes=2) # avoid temporal filter
now = now + timedelta(minutes=3)
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()
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.valve_open_percent == 0
await hass.async_block_till_done()
# the underlying set temperature call and the call to the valve
assert mock_service_call.call_count == 8
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 (
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 .commons import *