Feature 234 add central boiler helper (#352)

* Creation of the central boiler config + binary_sensor entity

* Fonctional before testu. Miss the service call

* Full featured but without testu

* Documentation and release.

* Add events in README

* FIX #341 - when window state change, open_valve_percent should be resend

* Issue #343 - disable safety mode for outdoor thermometer

* Issue #255 - Specify window action on window open detection

* Add en and string translation

* central boiler - add entites to fine tune the boiler start

* With testu ok

* Add testus for valve and climate

* Add testus in pipelines

* With pip 3

* With more pytest options

* Ass coverage tests

* Add coverage report in github

* Release 5.3.0

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
This commit is contained in:
Jean-Marc Collin
2024-01-21 00:31:16 +01:00
committed by GitHub
parent 3da271b671
commit c12a91a5ff
41 changed files with 3165 additions and 207 deletions

View File

@@ -19,6 +19,14 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
)
from homeassistant.components.switch import (
SwitchEntity,
)
from homeassistant.components.number import (
NumberEntity,
)
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
@@ -60,6 +68,7 @@ from .const import ( # pylint: disable=unused-import
PRESET_NONE,
PRESET_ECO,
PRESET_ACTIVITY,
overrides,
)
@@ -168,8 +177,52 @@ FULL_CENTRAL_CONFIG = {
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_ADD_CENTRAL_BOILER_CONTROL: False,
}
FULL_CENTRAL_CONFIG_WITH_BOILER = {
CONF_NAME: CENTRAL_CONFIG_NAME,
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
"frost_temp": 10,
"eco_temp": 17.1,
"comfort_temp": 0,
"boost_temp": 19.1,
"eco_ac_temp": 25.1,
"comfort_ac_temp": 23.1,
"boost_ac_temp": 21.1,
"frost_away_temp": 15.1,
"eco_away_temp": 15.2,
"comfort_away_temp": 0,
"boost_away_temp": 15.4,
"eco_ac_away_temp": 30.5,
"comfort_ac_away_temp": 0,
"boost_ac_away_temp": 30.7,
CONF_WINDOW_DELAY: 15,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
CONF_WINDOW_AUTO_MAX_DURATION: 31,
CONF_MOTION_DELAY: 31,
CONF_MOTION_OFF_DELAY: 301,
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "frost",
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_ADD_CENTRAL_BOILER_CONTROL: True,
CONF_CENTRAL_BOILER_ACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_on",
CONF_CENTRAL_BOILER_DEACTIVATION_SRV: "switch.pompe_chaudiere/switch.turn_off",
}
_LOGGER = logging.getLogger(__name__)
@@ -213,6 +266,11 @@ class MockClimate(ClimateEntity):
self._fan_modes = fan_modes if fan_modes else None
self._attr_fan_mode = None
@property
def name(self) -> str:
"""The name"""
return self._name
@property
def hvac_action(self):
"""The hvac action of the mock climate"""
@@ -244,6 +302,10 @@ class MockClimate(ClimateEntity):
"""The hvac mode"""
self._attr_hvac_mode = hvac_mode
def set_hvac_mode(self, hvac_mode):
"""The hvac mode"""
self._attr_hvac_mode = hvac_mode
def set_hvac_action(self, hvac_action: HVACAction):
"""Set the HVACaction"""
self._attr_hvac_action = hvac_action
@@ -350,6 +412,66 @@ class MagicMockClimate(MagicMock):
return 19
class MockSwitch(SwitchEntity):
"""A fake switch to be used instead real switch"""
def __init__( # pylint: disable=unused-argument, dangerous-default-value
self, hass: HomeAssistant, unique_id, name, entry_infos={}
):
"""Init the switch"""
super().__init__()
self.hass = hass
self.platform = "switch"
self.entity_id = self.platform + "." + unique_id
self._name = name
self._attr_is_on = False
@property
def name(self) -> str:
"""The name"""
return self._name
@overrides
def turn_on(self, **kwargs: Any):
"""Turns the switch on and notify the state change"""
self._attr_is_on = True
# self.async_write_ha_state()
@overrides
def turn_off(self, **kwargs: Any):
"""Turns the switch on and notify the state change"""
self._attr_is_on = False
# self.async_write_ha_state()
class MockNumber(NumberEntity):
"""A fake switch to be used instead real switch"""
def __init__( # pylint: disable=unused-argument, dangerous-default-value
self, hass: HomeAssistant, unique_id, name, entry_infos={}
):
"""Init the switch"""
super().__init__()
self.hass = hass
self.platform = "number"
self.entity_id = self.platform + "." + unique_id
self._name = name
self._attr_native_value = 0
self._attr_native_min_value = 0
@property
def name(self) -> str:
"""The name"""
return self._name
@overrides
def set_native_value(self, value: float):
"""Change the value"""
self._attr_native_value = value
async def create_thermostat(
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
) -> BaseThermostat:
@@ -361,13 +483,6 @@ async def create_thermostat(
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
return search_entity(hass, entity_id, CLIMATE_DOMAIN)

View File

@@ -29,7 +29,11 @@ from custom_components.versatile_thermostat.config_flow import (
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from .commons import create_central_config
from .commons import (
create_central_config,
FULL_CENTRAL_CONFIG,
FULL_CENTRAL_CONFIG_WITH_BOILER,
)
pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name
@@ -127,6 +131,16 @@ async def init_central_config_fixture(
hass, init_vtherm_api
): # pylint: disable=unused-argument
"""Initialize the VTherm API"""
await create_central_config(hass)
await create_central_config(hass, FULL_CENTRAL_CONFIG)
yield
@pytest.fixture(name="init_central_config_with_boiler_fixture")
async def init_central_config_with_boiler_fixture(
hass, init_vtherm_api
): # pylint: disable=unused-argument
"""Initialize the VTherm API"""
await create_central_config(hass, FULL_CENTRAL_CONFIG_WITH_BOILER)
yield

View File

@@ -152,6 +152,7 @@ MOCK_WINDOW_AUTO_CONFIG = {
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 1.0,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.0,
CONF_WINDOW_AUTO_MAX_DURATION: 5.0,
CONF_WINDOW_ACTION: CONF_WINDOW_FAN_ONLY,
}
MOCK_MOTION_CONFIG = {

View File

@@ -0,0 +1,835 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the central_configuration """
import asyncio
from datetime import datetime
from unittest.mock import patch, call
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntryState
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
from custom_components.versatile_thermostat.thermostat_valve import (
ThermostatOverValve,
)
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from custom_components.versatile_thermostat.binary_sensor import (
CentralBoilerBinarySensor,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
# @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_add_a_central_config_with_boiler(
hass: HomeAssistant,
skip_hass_states_is_state,
):
"""Tests the clean_central_config_doubon of base_thermostat"""
central_config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheCentralConfigMockName",
unique_id="centralConfigUniqueId",
data=FULL_CENTRAL_CONFIG_WITH_BOILER,
)
central_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(central_config_entry.entry_id)
assert central_config_entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.thecentralconfigmockname", "climate"
)
assert entity is None
assert count_entities(hass, "climate.thecentralconfigmockname", "climate") == 0
# Test that VTherm API find the CentralConfig
api = VersatileThermostatAPI.get_vtherm_api(hass)
central_configuration = api.find_central_configuration()
assert central_configuration is not None
# Test that VTherm API have any central boiler entities
assert api.nb_active_device_for_boiler_entity is not None
assert api.nb_active_device_for_boiler == 0
assert api.nb_active_device_for_boiler_threshold_entity is not None
assert api.nb_active_device_for_boiler_threshold is 1 # the default value is 1
async def test_update_central_boiler_state_simple(
hass: HomeAssistant,
# skip_hass_states_is_state,
init_central_config_with_boiler_fixture,
):
"""Test that the central boiler state behavoir"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
switch1 = MockSwitch(hass, "switch1", "theSwitch1")
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
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_HEATER: switch1.entity_id,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
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_TPI_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity.name == "TheOverSwitchMockName"
assert entity.is_over_switch
assert entity.underlying_entities[0].entity_id == "switch.switch1"
assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0
# Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
await send_temperature_change_event(entity, 10, now)
assert entity.hvac_mode == HVACMode.HEAT
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 heater
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:
await switch1.async_turn_on()
switch1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
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},
)
]
)
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON
# 2. stop a heater
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:
await switch1.async_turn_off()
switch1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.IDLE
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
entity.remove_thermostat()
async def test_update_central_boiler_state_multiple(
hass: HomeAssistant,
# skip_hass_states_is_state,
init_central_config_with_boiler_fixture,
):
"""Test that the central boiler state behavoir"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
switch1 = MockSwitch(hass, "switch1", "theSwitch1")
switch2 = MockSwitch(hass, "switch2", "theSwitch2")
switch3 = MockSwitch(hass, "switch3", "theSwitch3")
switch4 = MockSwitch(hass, "switch4", "theSwitch4")
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
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_HEATER: switch1.entity_id,
CONF_HEATER_2: switch2.entity_id,
CONF_HEATER_3: switch3.entity_id,
CONF_HEATER_4: switch4.entity_id,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
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_TPI_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity.name == "TheOverSwitchMockName"
assert entity.is_over_switch
assert entity.underlying_entities[0].entity_id == "switch.switch1"
assert entity.underlying_entities[1].entity_id == "switch.switch2"
assert entity.underlying_entities[2].entity_id == "switch.switch3"
assert entity.underlying_entities[3].entity_id == "switch.switch4"
assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0
# Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
await send_temperature_change_event(entity, 10, now)
assert entity.hvac_mode == HVACMode.HEAT
# 0. set threshold to 3
api.nb_active_device_for_boiler_threshold_entity.set_native_value(3)
assert api.nb_active_device_for_boiler_threshold == 3
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 first heater
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:
await switch1.async_turn_on()
switch1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count == 1
# No switch of the boiler
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
{"entity_id": "switch.switch1"},
),
]
)
assert mock_send_event.call_count == 0
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_OFF
# 2. start a 2nd heater
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:
await switch2.async_turn_on()
switch2.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
# Only the first heater is started by the algo
assert mock_service_call.call_count == 1
# No switch of the boiler. Caution: each time a underlying heater state change itself,
# the cycle restarts. So it is always the first heater that is started
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
{"entity_id": "switch.switch1"},
),
]
)
assert mock_send_event.call_count == 0
assert api.nb_active_device_for_boiler == 2
assert boiler_binary_sensor.state == STATE_OFF
# 3. start a 3rd heater
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:
await switch3.async_turn_on()
switch3.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
# Only the first heater is started by the algo
assert mock_service_call.call_count == 2
# No switch of the boiler. Caution: each time a underlying heater state change itself,
# the cycle restarts. So it is always the first heater that is started
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
),
call.service_call(
"switch",
"turn_on",
{"entity_id": "switch.switch1"},
),
],
any_order=True,
)
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},
)
]
)
assert api.nb_active_device_for_boiler == 3
assert boiler_binary_sensor.state == STATE_ON
# 4. start a 4th heater
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:
await switch4.async_turn_on()
switch4.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
# Only the first heater is started by the algo
assert mock_service_call.call_count == 1
# No switch of the boiler. Caution: each time a underlying heater state change itself,
# the cycle restarts. So it is always the first heater that is started
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
{"entity_id": "switch.switch1"},
),
]
)
assert mock_send_event.call_count == 0
assert api.nb_active_device_for_boiler == 4
assert boiler_binary_sensor.state == STATE_ON
# 5. stop a heater
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:
await switch1.async_turn_off()
switch1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
assert mock_service_call.call_count == 0
assert mock_send_event.call_count == 0
assert api.nb_active_device_for_boiler == 3
assert boiler_binary_sensor.state == STATE_ON
# 6. stop a 2nd heater
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:
await switch4.async_turn_off()
switch4.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
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 == 2
assert boiler_binary_sensor.state == STATE_OFF
entity.remove_thermostat()
async def test_update_central_boiler_state_simple_valve(
hass: HomeAssistant,
# skip_hass_states_is_state,
init_central_config_with_boiler_fixture,
):
"""Test that the central boiler state behavoir"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
valve1 = MockNumber(hass, "valve1", "theValve1")
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverValveMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverValveMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE,
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_VALVE: valve1.entity_id,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
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_TPI_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
entity: ThermostatOverValve = await create_thermostat(
hass, entry, "climate.theovervalvemockname"
)
assert entity
assert entity.name == "TheOverValveMockName"
assert entity.is_over_valve
assert entity.underlying_entities[0].entity_id == "number.valve1"
assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0
# Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
assert entity.hvac_mode == HVACMode.HEAT
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 valve
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:
await send_temperature_change_event(entity, 10, now)
# we have to simulate the valve also else the test don't work
valve1.set_native_value(10)
valve1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
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},
)
]
)
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON
# 2. stop a heater
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:
await send_temperature_change_event(entity, 25, now)
# Change the valve value to 0
valve1.set_native_value(0)
valve1.async_write_ha_state()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.IDLE
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
entity.remove_thermostat()
async def test_update_central_boiler_state_simple_climate(
hass: HomeAssistant,
# skip_hass_states_is_state,
init_central_config_with_boiler_fixture,
):
"""Test that the central boiler state behavoir"""
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_CLIMATE: climate1.entity_id,
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,
},
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=climate1,
):
entity: ThermostatOverValve = 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
# Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
assert entity.hvac_mode == HVACMode.HEAT
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
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:
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()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
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},
)
]
)
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON
# 2. stop a climate
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:
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()
# Wait for state event propagation
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.IDLE
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
entity.remove_thermostat()

View File

@@ -31,8 +31,8 @@ from .commons import * # pylint: disable=wildcard-import, unused-wildcard-impor
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
# @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_state):
"""Tests the clean_central_config_doubon of base_thermostat"""
central_config_entry = MockConfigEntry(
@@ -76,6 +76,7 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_ADD_CENTRAL_BOILER_CONTROL: False,
},
)
@@ -96,9 +97,16 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
central_configuration = api.find_central_configuration()
assert central_configuration is not None
# Test that VTherm API doesn't have any central boiler entities
assert api.nb_active_device_for_boiler_entity is None
assert api.nb_active_device_for_boiler is None
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
assert api.nb_active_device_for_boiler_threshold_entity is None
assert api.nb_active_device_for_boiler_threshold is None
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
# @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_minimal_over_switch_wo_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
):
@@ -172,9 +180,11 @@ async def test_minimal_over_switch_wo_central_config(
assert entity._security_default_on_percent == 0.1
assert entity.is_inversed
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
# @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_full_over_switch_wo_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
):
@@ -286,9 +296,11 @@ async def test_full_over_switch_wo_central_config(
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
# @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_full_over_switch_with_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
):
@@ -396,9 +408,11 @@ async def test_full_over_switch_with_central_config(
assert entity._presence_sensor_entity_id == "binary_sensor.mock_presence_sensor"
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
# @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_switch_with_central_config_but_no_central_config(
hass: HomeAssistant, skip_hass_states_get, init_vtherm_api
):

View File

@@ -143,6 +143,8 @@ async def test_user_config_flow_over_switch(
CONF_USE_POWER_CENTRAL_CONFIG: True,
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USE_CENTRAL_MODE: True,
CONF_USED_BY_CENTRAL_BOILER: False,
}
)
assert result["result"]
@@ -239,6 +241,7 @@ async def test_user_config_flow_over_climate(
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_USE_ADVANCED_CENTRAL_CONFIG: False,
CONF_USED_BY_CENTRAL_BOILER: False,
}
assert result["result"]
assert result["result"].domain == DOMAIN
@@ -287,6 +290,7 @@ async def test_user_config_flow_window_auto_ok(
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
@@ -373,6 +377,7 @@ async def test_user_config_flow_window_auto_ok(
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
}
assert result["result"]
assert result["result"].domain == DOMAIN
@@ -512,6 +517,7 @@ async def test_user_config_flow_over_4_switches(
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_CENTRAL_MODE: False,
CONF_USED_BY_CENTRAL_BOILER: False,
}
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name

View File

@@ -321,9 +321,6 @@ async def test_over_valve_full_start(
await try_condition(None)
assert entity.hvac_mode is HVACMode.HEAT
assert (
entity.hvac_action is HVACAction.OFF
or entity.hvac_action is HVACAction.IDLE
)
assert entity.hvac_action is HVACAction.HEATING
assert entity.target_temperature == 17.1 # eco
assert entity.valve_open_percent == 10

View File

@@ -6,6 +6,9 @@ from unittest.mock import patch, call, PropertyMock
from datetime import datetime, timedelta
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
@@ -46,6 +49,7 @@ async def test_window_management_time_not_enough(
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF,
},
)
@@ -134,6 +138,7 @@ async def test_window_management_time_enough(
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF,
},
)
@@ -242,7 +247,7 @@ async def test_window_management_time_enough(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Window management"""
"""Test the auto Window management with fast slope down"""
entry = MockConfigEntry(
domain=DOMAIN,
@@ -822,7 +827,6 @@ async def test_window_auto_no_on_percent(
entity.remove_thermostat()
# PR - Adding Window Bypass
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
@@ -1207,3 +1211,694 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
# Clean the entity
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_action_fan_only(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Window management with the fan_only option"""
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: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 1,
CONF_WINDOW_ACTION: CONF_WINDOW_FAN_ONLY,
# CONF_WINDOW_AUTO_OPEN_THRESHOLD: 6,
# CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 6,
# CONF_WINDOW_AUTO_MAX_DURATION: 1, # 0 will deactivate window auto detection
},
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mockUniqueId",
name="MockClimateName",
hvac_modes=[HVACMode.HEAT, HVACMode.COOL, HVACMode.FAN_ONLY],
)
# 1. intialize climate entity
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.theoverclimatemockname", "climate"
)
assert entity
assert entity.is_over_climate is True
assert entity.window_action == CONF_WINDOW_FAN_ONLY
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode == PRESET_COMFORT
assert entity.target_temperature == 18
assert entity.window_state is STATE_OFF
# 2. Open the window, condition of time is satisfied, check the thermostat and heater turns off
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
event_timestamp = now - timedelta(minutes=2)
try_window_condition = await send_window_change_event(
entity, True, False, event_timestamp
)
await try_window_condition(None)
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.FAN_ONLY}
)
]
)
# The underlying should be in OFF hvac_mode
assert mock_underlying_set_hvac_mode.call_count == 1
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.FAN_ONLY),
]
)
assert entity.window_state == STATE_ON
# The underlying should be in FAN_ONLY hvac_mode
assert entity.hvac_mode is HVACMode.FAN_ONLY
assert entity._saved_hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT
# 3. Close the window
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
event_timestamp = now - timedelta(minutes=1)
try_function = await send_window_change_event(
entity, False, True, event_timestamp, sleep=False
)
await try_function(None)
# Wait for initial delay of heater
await asyncio.sleep(0.3)
assert entity.window_state == STATE_OFF
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT}
),
],
any_order=False,
)
# The underlying should be in OFF hvac_mode
assert mock_underlying_set_hvac_mode.call_count == 1
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT
# Clean the entity
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_action_fan_only_ko(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Window management with the fan_only option but the underlyings doesn't have the FAN_ONLY mode
So the VTherm switch to OFF which is the fallback mode"""
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: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate",
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 1,
CONF_WINDOW_ACTION: CONF_WINDOW_FAN_ONLY,
# CONF_WINDOW_AUTO_OPEN_THRESHOLD: 6,
# CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 6,
# CONF_WINDOW_AUTO_MAX_DURATION: 1, # 0 will deactivate window auto detection
},
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mockUniqueId",
name="MockClimateName",
hvac_modes=[HVACMode.HEAT, HVACMode.COOL, HVACMode.AUTO],
)
# 1. intialize climate entity
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
entity: ThermostatOverClimate = search_entity(
hass, "climate.theoverclimatemockname", "climate"
)
assert entity
assert entity.is_over_climate is True
assert entity.window_action == CONF_WINDOW_FAN_ONLY
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode == PRESET_COMFORT
assert entity.target_temperature == 18
assert entity.window_state is STATE_OFF
# 2. Open the window, condition of time is satisfied, check the thermostat and heater turns off
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
event_timestamp = now - timedelta(minutes=2)
try_window_condition = await send_window_change_event(
entity, True, False, event_timestamp
)
await try_window_condition(None)
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF})]
)
assert entity.window_state == STATE_ON
assert entity.hvac_mode is HVACMode.OFF
# The underlying should be in OFF hvac_mode
assert mock_underlying_set_hvac_mode.call_count == 1
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.OFF),
]
)
assert entity._saved_hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT
# 3. Close the window
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"homeassistant.helpers.condition.state", return_value=True
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
event_timestamp = now - timedelta(minutes=1)
try_function = await send_window_change_event(
entity, False, True, event_timestamp, sleep=False
)
await try_function(None)
# Wait for initial delay of heater
await asyncio.sleep(0.3)
assert entity.window_state == STATE_OFF
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT}
),
],
any_order=False,
)
# The underlying should be in OFF hvac_mode
assert mock_underlying_set_hvac_mode.call_count == 1
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT
# Clean the entity
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Window management with the eco_temp option"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 10, # Should be 0 for test
CONF_WINDOW_ACTION: CONF_WINDOW_ECO_TEMP,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is None
assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF
assert entity.is_window_auto_enabled is True
# 1. Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1)
await send_temperature_change_event(entity, 19, event_timestamp)
event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 19, event_timestamp)
event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 19, event_timestamp)
# 2. Make the temperature down
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 19, event_timestamp)
# The heater turns on
assert mock_send_event.call_count == 0
assert entity.is_device_active is True
assert entity.hvac_mode is HVACMode.HEAT
assert entity.window_state is STATE_OFF
assert entity.window_auto_state is STATE_OFF
# 3. send one degre down in one minute
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 18, event_timestamp)
# The heater turns on
assert mock_send_event.call_count == 1
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
assert entity.last_temperature_slope == -6.24
assert entity.window_auto_state == STATE_ON
assert entity.window_state == STATE_OFF
# No change on HVACMode
assert entity.hvac_mode is HVACMode.HEAT
# No change on preset
assert entity.preset_mode is PRESET_BOOST
# The eco temp
assert entity.target_temperature == 17
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.WINDOW_AUTO_EVENT,
{"type": "start", "cause": "slope alert", "curve_slope": -6.24},
),
],
any_order=True,
)
# 4. send another 0.1 degre in one minute -> no change
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 17.9, event_timestamp)
# The heater turns on
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
assert round(entity.last_temperature_slope, 3) == -7.49
assert entity.window_auto_state == STATE_ON
assert entity.hvac_mode is HVACMode.HEAT
# No change on preset
assert entity.preset_mode is PRESET_BOOST
# The eco temp
assert entity.target_temperature == 17
# 5. send another plus 1.1 degre in one minute -> restore state
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 19, event_timestamp)
# The heater turns on
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.WINDOW_AUTO_EVENT,
{
"type": "end",
"cause": "end of slope alert",
"curve_slope": 0.42,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
assert entity.last_temperature_slope == 0.42
assert entity.window_auto_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
# No change on preset
assert entity.preset_mode is PRESET_BOOST
# The eco temp
assert entity.target_temperature == 21
# Clean the entity
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Window management with the frost_temp option"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
"frost_temp": 10,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 10, # Should be 0 for test
CONF_WINDOW_ACTION: CONF_WINDOW_FROST_TEMP,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is None
assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF
assert entity.is_window_auto_enabled is True
# 1. Initialize the slope algo with 2 measurements
event_timestamp = now + timedelta(minutes=1)
await send_temperature_change_event(entity, 19, event_timestamp)
event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 19, event_timestamp)
event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 19, event_timestamp)
# 2. Make the temperature down
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 19, event_timestamp)
# The heater turns on
assert mock_send_event.call_count == 0
assert entity.is_device_active is True
assert entity.hvac_mode is HVACMode.HEAT
assert entity.window_state is STATE_OFF
assert entity.window_auto_state is STATE_OFF
# 3. send one degre down in one minute
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 18, event_timestamp)
# The heater turns on
assert mock_send_event.call_count == 1
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
assert entity.last_temperature_slope == -6.24
assert entity.window_auto_state == STATE_ON
assert entity.window_state == STATE_OFF
# No change on HVACMode
assert entity.hvac_mode is HVACMode.HEAT
# No change on preset
assert entity.preset_mode is PRESET_BOOST
# The eco temp
assert entity.target_temperature == 10
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.WINDOW_AUTO_EVENT,
{"type": "start", "cause": "slope alert", "curve_slope": -6.24},
),
],
any_order=True,
)
# 4. send another 0.1 degre in one minute -> no change
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 17.9, event_timestamp)
# The heater turns on
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
assert round(entity.last_temperature_slope, 3) == -7.49
assert entity.window_auto_state == STATE_ON
assert entity.hvac_mode is HVACMode.HEAT
# No change on preset
assert entity.preset_mode is PRESET_BOOST
# The eco temp
assert entity.target_temperature == 10
# 5. send another plus 1.1 degre in one minute -> restore state
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 19, event_timestamp)
# The heater turns on
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.WINDOW_AUTO_EVENT,
{
"type": "end",
"cause": "end of slope alert",
"curve_slope": 0.42,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
assert entity.last_temperature_slope == 0.42
assert entity.window_auto_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
# No change on preset
assert entity.preset_mode is PRESET_BOOST
# The eco temp
assert entity.target_temperature == 21
# Clean the entity
entity.remove_thermostat()