927 lines
35 KiB
Python
927 lines
35 KiB
Python
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
|
|
|
""" Test the Window management """
|
|
import asyncio
|
|
|
|
from unittest.mock import patch, call, PropertyMock
|
|
from datetime import datetime, timedelta
|
|
|
|
import logging
|
|
|
|
from homeassistant.components.climate import (
|
|
SERVICE_SET_TEMPERATURE,
|
|
)
|
|
|
|
from .commons import *
|
|
|
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_bug_56(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_turn_on_off_heater,
|
|
skip_send_event,
|
|
):
|
|
"""Test that in over_climate mode there is no error when underlying climate is not available"""
|
|
|
|
the_mock_underlying = MagicMockClimate()
|
|
with patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
|
return_value=None, # dont find the underlying climate
|
|
):
|
|
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: False,
|
|
CONF_USE_MOTION_FEATURE: False,
|
|
CONF_USE_POWER_FEATURE: False,
|
|
CONF_USE_PRESENCE_FEATURE: False,
|
|
CONF_CLIMATE: "climate.mock_climate",
|
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
|
CONF_SECURITY_DELAY_MIN: 5,
|
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
|
},
|
|
)
|
|
|
|
entity: BaseThermostat = await create_thermostat(
|
|
hass, entry, "climate.theoverclimatemockname"
|
|
)
|
|
assert entity
|
|
# cause the underlying climate was not found
|
|
assert entity.is_over_climate is True
|
|
assert entity.underlying_entity(0)._underlying_climate is None
|
|
|
|
# Should not failed
|
|
entity.update_custom_attributes()
|
|
|
|
# try to call async_control_heating
|
|
try:
|
|
ret = await entity.async_control_heating()
|
|
# an exception should be send
|
|
assert ret is False
|
|
except Exception: # pylint: disable=broad-exception-caught
|
|
assert False
|
|
|
|
# This time the underlying will be found
|
|
with patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
|
return_value=the_mock_underlying, # dont find the underlying climate
|
|
):
|
|
# try to call async_control_heating
|
|
try:
|
|
await entity.async_control_heating()
|
|
except UnknownEntity:
|
|
assert False
|
|
except Exception: # pylint: disable=broad-exception-caught
|
|
assert False
|
|
|
|
# Should not failed
|
|
entity.update_custom_attributes()
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_bug_63(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_turn_on_off_heater,
|
|
skip_send_event,
|
|
):
|
|
"""Test that it should be possible to set the security_default_on_percent to 0"""
|
|
|
|
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": 19,
|
|
CONF_USE_WINDOW_FEATURE: False,
|
|
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.0, # !! here
|
|
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.0, # !! here
|
|
CONF_DEVICE_POWER: 200,
|
|
},
|
|
)
|
|
|
|
entity: BaseThermostat = await create_thermostat(
|
|
hass, entry, "climate.theoverswitchmockname"
|
|
)
|
|
assert entity
|
|
|
|
assert entity._security_min_on_percent == 0
|
|
assert entity._security_default_on_percent == 0
|
|
|
|
|
|
# Waiting for answer in https://github.com/jmcollin78/versatile_thermostat/issues/64
|
|
# Repro case not evident
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_bug_64(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_turn_on_off_heater,
|
|
skip_send_event,
|
|
):
|
|
"""Test that it should be possible to set the security_default_on_percent to 0"""
|
|
|
|
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": 19,
|
|
CONF_USE_WINDOW_FEATURE: False,
|
|
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.5,
|
|
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
|
|
CONF_DEVICE_POWER: 200,
|
|
},
|
|
)
|
|
|
|
entity: BaseThermostat = await create_thermostat(
|
|
hass, entry, "climate.theoverswitchmockname"
|
|
)
|
|
assert entity
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_bug_66(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_turn_on_off_heater,
|
|
skip_send_event,
|
|
):
|
|
"""Test that it should be possible to open/close the window rapidly without side effect"""
|
|
|
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
|
now: datetime = datetime.now(tz=tz)
|
|
|
|
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": 19,
|
|
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.5,
|
|
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
|
|
CONF_DEVICE_POWER: 200,
|
|
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
|
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
|
},
|
|
)
|
|
|
|
entity: BaseThermostat = await create_thermostat(
|
|
hass, entry, "climate.theoverswitchmockname"
|
|
)
|
|
assert entity
|
|
|
|
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.target_temperature == 19
|
|
assert entity.window_state is STATE_OFF
|
|
|
|
# Open the window and let the thermostat shut 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(
|
|
"homeassistant.helpers.condition.state", return_value=True
|
|
) as mock_condition, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
|
return_value=True,
|
|
):
|
|
await send_temperature_change_event(entity, 15, now)
|
|
try_window_condition = await send_window_change_event(
|
|
entity, True, False, now, False
|
|
)
|
|
|
|
# simulate the call to try_window_condition
|
|
await try_window_condition(None)
|
|
|
|
assert mock_send_event.call_count == 1
|
|
assert mock_heater_on.call_count == 1
|
|
assert mock_heater_off.call_count >= 1
|
|
assert mock_condition.call_count == 1
|
|
|
|
assert entity.window_state == STATE_ON
|
|
|
|
# Close the window but too shortly
|
|
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(
|
|
"homeassistant.helpers.condition.state", return_value=False
|
|
) as mock_condition, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
|
return_value=False,
|
|
):
|
|
event_timestamp = now + timedelta(minutes=1)
|
|
try_window_condition = await send_window_change_event(
|
|
entity, False, True, event_timestamp
|
|
)
|
|
# simulate the call to try_window_condition
|
|
await try_window_condition(None)
|
|
|
|
# window state should not have change
|
|
assert entity.window_state == STATE_ON
|
|
|
|
# Reopen immediatly with sufficient time
|
|
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(
|
|
"homeassistant.helpers.condition.state", return_value=True
|
|
) as mock_condition, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
|
return_value=False,
|
|
):
|
|
try_window_condition = await send_window_change_event(
|
|
entity, True, False, event_timestamp
|
|
)
|
|
# simulate the call to try_window_condition
|
|
await try_window_condition(None)
|
|
|
|
# still no change
|
|
assert entity.window_state == STATE_ON
|
|
assert entity.hvac_mode == HVACMode.OFF
|
|
|
|
# Close the window but with sufficient time this time
|
|
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(
|
|
"homeassistant.helpers.condition.state", return_value=True
|
|
) as mock_condition, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
|
return_value=False,
|
|
):
|
|
event_timestamp = now + timedelta(minutes=2)
|
|
try_window_condition = await send_window_change_event(
|
|
entity, False, True, event_timestamp
|
|
)
|
|
# simulate the call to try_window_condition
|
|
await try_window_condition(None)
|
|
|
|
# window state should be Off this time and old state should have been restored
|
|
assert entity.window_state == STATE_OFF
|
|
assert entity.hvac_mode is HVACMode.HEAT
|
|
assert entity.preset_mode is PRESET_BOOST
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_bug_82(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_turn_on_off_heater,
|
|
skip_send_event,
|
|
):
|
|
"""Test that when a underlying climate is not available the VTherm doesn't go into safety mode"""
|
|
|
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
|
now: datetime = datetime.now(tz=tz)
|
|
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
title="TheOverClimateMockName",
|
|
unique_id="uniqueId",
|
|
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
|
)
|
|
|
|
fake_underlying_climate = MockUnavailableClimate(
|
|
hass, "mockUniqueId", "MockClimateName", {}
|
|
)
|
|
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
) as mock_send_event, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
|
return_value=fake_underlying_climate,
|
|
) as mock_find_climate:
|
|
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"
|
|
assert entity.is_over_climate is True
|
|
# assert entity.hvac_action is HVACAction.OFF
|
|
assert entity.hvac_mode is HVACMode.OFF
|
|
# assert entity.hvac_mode is None
|
|
assert entity.target_temperature == entity.min_temp
|
|
assert entity.preset_modes == [
|
|
PRESET_NONE,
|
|
PRESET_FROST_PROTECTION,
|
|
PRESET_ECO,
|
|
PRESET_COMFORT,
|
|
PRESET_BOOST,
|
|
]
|
|
assert entity.preset_mode is PRESET_NONE
|
|
assert entity._security_state is False
|
|
|
|
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
|
assert mock_send_event.call_count == 2
|
|
mock_send_event.assert_has_calls(
|
|
[
|
|
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
|
call.send_event(
|
|
EventType.HVAC_MODE_EVENT,
|
|
{"hvac_mode": HVACMode.OFF},
|
|
),
|
|
]
|
|
)
|
|
|
|
assert mock_find_climate.call_count == 1
|
|
assert mock_find_climate.mock_calls[0] == call()
|
|
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
|
|
|
# Force safety mode
|
|
assert entity._last_ext_temperature_measure is not None
|
|
assert entity._last_temperature_measure is not None
|
|
assert (
|
|
entity._last_temperature_measure.astimezone(tz) - now
|
|
).total_seconds() < 1
|
|
assert (
|
|
entity._last_ext_temperature_measure.astimezone(tz) - now
|
|
).total_seconds() < 1
|
|
|
|
# Tries to turns on the Thermostat
|
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
|
assert entity.hvac_mode == HVACMode.HEAT
|
|
|
|
# 2. activate security feature when date is expired
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
) as mock_send_event, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
|
):
|
|
event_timestamp = now - timedelta(minutes=6)
|
|
|
|
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
|
# Should stay False
|
|
assert entity.security_state is False
|
|
assert entity.preset_mode == "none"
|
|
assert entity._saved_preset_mode == "none"
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_bug_101(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_turn_on_off_heater,
|
|
skip_send_event,
|
|
):
|
|
"""Test that when a underlying climate target temp is changed, the VTherm change its own temperature target and switch to manual"""
|
|
|
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
|
now: datetime = datetime.now(tz=tz)
|
|
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
title="TheOverClimateMockName",
|
|
unique_id="uniqueId",
|
|
data=PARTIAL_CLIMATE_NOT_REGULATED_CONFIG, # 5 minutes security delay
|
|
)
|
|
|
|
# Underlying is in HEAT mode but should be shutdown at startup
|
|
fake_underlying_climate = MockClimate(
|
|
hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING
|
|
)
|
|
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
) as mock_send_event, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
|
return_value=fake_underlying_climate,
|
|
) as mock_find_climate, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
|
) as mock_underlying_set_hvac_mode:
|
|
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"
|
|
assert entity.is_over_climate is True
|
|
assert entity.hvac_mode is HVACMode.OFF
|
|
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
|
assert entity.hvac_action is HVACAction.HEATING
|
|
# Underlying should have been shutdown
|
|
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.target_temperature == entity.min_temp
|
|
assert entity.preset_mode is PRESET_NONE
|
|
|
|
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
|
assert mock_send_event.call_count == 2
|
|
mock_send_event.assert_has_calls(
|
|
[
|
|
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
|
call.send_event(
|
|
EventType.HVAC_MODE_EVENT,
|
|
{"hvac_mode": HVACMode.OFF},
|
|
),
|
|
]
|
|
)
|
|
|
|
assert mock_find_climate.call_count == 1
|
|
assert mock_find_climate.mock_calls[0] == call()
|
|
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
|
|
|
# Force preset mode
|
|
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
|
|
|
|
# 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121)
|
|
await send_climate_change_event_with_temperature(
|
|
entity,
|
|
HVACMode.HEAT,
|
|
HVACMode.HEAT,
|
|
HVACAction.OFF,
|
|
HVACAction.OFF,
|
|
now,
|
|
12.75,
|
|
)
|
|
# Should NOT have been switched to Manual preset
|
|
assert entity.target_temperature == 17
|
|
assert entity.preset_mode is PRESET_COMFORT
|
|
|
|
# 2. Change the target temp of underlying thermostat at 11 sec later -> the event will be taken
|
|
# Wait 11 sec
|
|
event_timestamp = now + timedelta(seconds=11)
|
|
assert entity.is_regulated is False
|
|
await send_climate_change_event_with_temperature(
|
|
entity,
|
|
HVACMode.HEAT,
|
|
HVACMode.HEAT,
|
|
HVACAction.OFF,
|
|
HVACAction.OFF,
|
|
event_timestamp,
|
|
12.75,
|
|
)
|
|
assert entity.target_temperature == 12.75
|
|
assert entity.preset_mode is PRESET_NONE
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_bug_272(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_turn_on_off_heater,
|
|
skip_send_event,
|
|
):
|
|
"""Test that it not possible to set the target temperature under the min_temp setting"""
|
|
|
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
|
now: datetime = datetime.now(tz=tz)
|
|
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
title="TheOverClimateMockName",
|
|
unique_id="uniqueId",
|
|
# default value are min 15°, max 30°, step 0.1
|
|
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
|
)
|
|
|
|
# Min_temp is 15 and max_temp is 19
|
|
fake_underlying_climate = MagicMockClimate()
|
|
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
), patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
|
return_value=fake_underlying_climate,
|
|
), patch(
|
|
"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"
|
|
assert entity.is_over_climate is True
|
|
assert entity.hvac_mode is HVACMode.OFF
|
|
# The VTherm value and not the underlying value
|
|
assert entity.target_temperature_step == 0.1
|
|
assert entity.target_temperature == entity.min_temp
|
|
assert entity.is_regulated is True
|
|
|
|
assert mock_service_call.call_count == 0
|
|
|
|
# Set the hvac_mode to HEAT
|
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
|
|
|
# In the accepted interval
|
|
await entity.async_set_temperature(temperature=17.5)
|
|
|
|
# MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call
|
|
assert mock_service_call.call_count == 1
|
|
mock_service_call.assert_has_calls(
|
|
[
|
|
# call.async_call(
|
|
# "climate",
|
|
# SERVICE_SET_HVAC_MODE,
|
|
# {"entity_id": "climate.mock_climate", "hvac_mode": HVACMode.HEAT},
|
|
# ),
|
|
call.async_call(
|
|
"climate",
|
|
SERVICE_SET_TEMPERATURE,
|
|
{
|
|
"entity_id": "climate.mock_climate",
|
|
"temperature": 17.5,
|
|
"target_temp_high": 30,
|
|
"target_temp_low": 15,
|
|
},
|
|
),
|
|
]
|
|
)
|
|
|
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
|
now: datetime = datetime.now(tz=tz)
|
|
|
|
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, event_timestamp)
|
|
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
|
|
|
# Not in the accepted interval (15-19)
|
|
await entity.async_set_temperature(temperature=10)
|
|
assert mock_service_call.call_count == 1
|
|
mock_service_call.assert_has_calls(
|
|
[
|
|
call.async_call(
|
|
"climate",
|
|
SERVICE_SET_TEMPERATURE,
|
|
{
|
|
"entity_id": "climate.mock_climate",
|
|
"temperature": 15, # the minimum acceptable
|
|
"target_temp_high": 30,
|
|
"target_temp_low": 15,
|
|
},
|
|
),
|
|
]
|
|
)
|
|
|
|
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, event_timestamp)
|
|
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
|
|
|
# In the accepted interval
|
|
await entity.async_set_temperature(temperature=20.8)
|
|
assert mock_service_call.call_count == 1
|
|
mock_service_call.assert_has_calls(
|
|
[
|
|
call.async_call(
|
|
"climate",
|
|
SERVICE_SET_TEMPERATURE,
|
|
{
|
|
"entity_id": "climate.mock_climate",
|
|
"temperature": 19, # the maximum acceptable
|
|
"target_temp_high": 30,
|
|
"target_temp_low": 15,
|
|
},
|
|
),
|
|
]
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
|
"""Test the followin case in power management:
|
|
1. a heater is active (heating). So the power consumption takes the heater power into account. We suppose the power consumption is near the threshold,
|
|
2. the user switch preset let's say from Comfort to Boost,
|
|
3. expected: no shredding should occur because the heater was already active,
|
|
4. constated: the heater goes into shredding.
|
|
|
|
"""
|
|
|
|
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": 19,
|
|
CONF_USE_WINDOW_FEATURE: False,
|
|
CONF_USE_MOTION_FEATURE: False,
|
|
CONF_USE_POWER_FEATURE: True,
|
|
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_POWER_SENSOR: "sensor.mock_power_sensor",
|
|
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
|
CONF_DEVICE_POWER: 100,
|
|
CONF_PRESET_POWER: 12,
|
|
},
|
|
)
|
|
|
|
entity: ThermostatOverSwitch = await create_thermostat(
|
|
hass, entry, "climate.theoverswitchmockname"
|
|
)
|
|
assert entity
|
|
|
|
tpi_algo = entity._prop_algorithm
|
|
assert tpi_algo
|
|
|
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
|
now: datetime = datetime.now(tz=tz)
|
|
|
|
await send_temperature_change_event(entity, 16, now)
|
|
await send_ext_temperature_change_event(entity, 10, now)
|
|
|
|
# 1. An already active heater will not switch to overpowering
|
|
with patch(
|
|
"homeassistant.core.ServiceRegistry.async_call"
|
|
) as mock_service_call, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
|
new_callable=PropertyMock,
|
|
return_value=True,
|
|
):
|
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
|
assert entity.hvac_mode is HVACMode.HEAT
|
|
assert entity.preset_mode is PRESET_COMFORT
|
|
assert entity.overpowering_state is None
|
|
assert entity.target_temperature == 18
|
|
# waits that the heater starts
|
|
await asyncio.sleep(0.1)
|
|
assert mock_service_call.call_count >= 1
|
|
assert entity.is_device_active is True
|
|
|
|
# Send power max mesurement
|
|
await send_max_power_change_event(entity, 110, datetime.now())
|
|
# Send power mesurement (theheater is already in the power measurement)
|
|
await send_power_change_event(entity, 100, datetime.now())
|
|
# No overpowering yet
|
|
assert await entity.check_overpowering() is False
|
|
# All configuration is complete and power is < power_max
|
|
assert entity.preset_mode is PRESET_COMFORT
|
|
assert entity.overpowering_state is False
|
|
assert entity.is_device_active is True
|
|
|
|
# 2. An already active heater that switch preset will not switch to overpowering
|
|
with patch(
|
|
"homeassistant.core.ServiceRegistry.async_call"
|
|
) as mock_service_call, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
|
new_callable=PropertyMock,
|
|
return_value=True,
|
|
):
|
|
# change preset to Boost
|
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
|
# waits that the heater starts
|
|
await asyncio.sleep(0.1)
|
|
|
|
assert await entity.check_overpowering() is False
|
|
assert entity.hvac_mode is HVACMode.HEAT
|
|
assert entity.preset_mode is PRESET_BOOST
|
|
assert entity.overpowering_state is False
|
|
assert entity.target_temperature == 19
|
|
assert mock_service_call.call_count >= 1
|
|
|
|
# 3. if heater is stopped (is_device_active==False), then overpowering should be started
|
|
with patch(
|
|
"homeassistant.core.ServiceRegistry.async_call"
|
|
) as mock_service_call, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
|
new_callable=PropertyMock,
|
|
return_value=False,
|
|
):
|
|
# change preset to Boost
|
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
|
# waits that the heater starts
|
|
await asyncio.sleep(0.1)
|
|
|
|
assert await entity.check_overpowering() is True
|
|
assert entity.hvac_mode is HVACMode.HEAT
|
|
assert entity.preset_mode is PRESET_POWER
|
|
assert entity.overpowering_state is True
|
|
|
|
|
|
async def test_bug_339(
|
|
hass: HomeAssistant,
|
|
# skip_hass_states_is_state,
|
|
init_central_config_with_boiler_fixture,
|
|
):
|
|
"""Test that the counter of active Vtherm in central boiler is
|
|
correctly updated with underlying is in auto and device is active
|
|
"""
|
|
|
|
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
|
|
|
climate1 = MockClimate(
|
|
hass=hass,
|
|
unique_id="climate1",
|
|
name="theClimate1",
|
|
hvac_mode=HVACMode.AUTO,
|
|
hvac_modes=[HVACMode.AUTO, HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL],
|
|
hvac_action=HVACAction.HEATING,
|
|
)
|
|
|
|
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
|
|
|
|
await entity.async_set_hvac_mode(HVACMode.AUTO)
|
|
# Simulate a state change in underelying
|
|
await api.nb_active_device_for_boiler_entity.calculate_nb_active_devices(None)
|
|
|
|
# The VTherm should be active
|
|
assert entity.underlying_entity(0).is_device_active is True
|
|
assert entity.is_device_active is True
|
|
assert api.nb_active_device_for_boiler == 1
|
|
|
|
entity.remove_thermostat()
|