873 lines
36 KiB
Python
873 lines
36 KiB
Python
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable
|
|
|
|
""" Test the Multiple switch management """
|
|
import asyncio
|
|
from unittest.mock import patch, call, ANY, PropertyMock
|
|
from datetime import datetime, timedelta
|
|
import logging
|
|
|
|
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
|
|
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_one_switch_cycle(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_send_event,
|
|
): # pylint: disable=unused-argument
|
|
"""Test that when multiple switch are configured the activation is distributed"""
|
|
|
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
|
now: datetime = datetime.now(tz=tz)
|
|
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
title="TheOver4SwitchMockName",
|
|
unique_id="uniqueId",
|
|
data={
|
|
CONF_NAME: "TheOver4SwitchMockName",
|
|
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: 8,
|
|
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_switch1",
|
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
|
CONF_SAFETY_DELAY_MIN: 5,
|
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
|
CONF_TPI_COEF_INT: 0.3,
|
|
CONF_TPI_COEF_EXT: 0.01,
|
|
},
|
|
)
|
|
|
|
entity: BaseThermostat = await create_thermostat(
|
|
hass, entry, "climate.theover4switchmockname"
|
|
)
|
|
assert entity
|
|
assert entity.is_over_climate is False
|
|
|
|
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
|
):
|
|
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_UNAVAILABLE
|
|
|
|
event_timestamp = now - timedelta(minutes=4)
|
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
|
|
|
# Checks that all heaters are off
|
|
with patch(
|
|
"homeassistant.core.StateMachine.is_state", return_value=False
|
|
) as mock_is_state:
|
|
assert entity.is_device_active is False # pylint: disable=protected-access
|
|
|
|
# Should be call for the Switch
|
|
assert mock_is_state.call_count == 1
|
|
|
|
# Set temperature to a low level
|
|
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,
|
|
) as mock_device_active, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
|
|
return_value=None,
|
|
) as mock_call_later:
|
|
await send_ext_temperature_change_event(entity, 5, event_timestamp)
|
|
|
|
# No special event
|
|
assert mock_send_event.call_count == 0
|
|
assert mock_heater_off.call_count == 0
|
|
|
|
# The first heater should be on but because call_later is mocked heater_on is not called
|
|
# assert mock_heater_on.call_count == 1
|
|
assert mock_heater_on.call_count == 0
|
|
# There is no check if active
|
|
# don't work with PropertyMock
|
|
# assert mock_device_active.call_count == 0
|
|
|
|
# 4 calls dispatched along the cycle
|
|
assert mock_call_later.call_count == 1
|
|
mock_call_later.assert_has_calls(
|
|
[
|
|
call.call_later(hass, 0.0, ANY),
|
|
]
|
|
)
|
|
|
|
# Set a temperature at middle level
|
|
event_timestamp = now - timedelta(minutes=4)
|
|
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,
|
|
) as mock_device_active:
|
|
await send_temperature_change_event(entity, 18, event_timestamp)
|
|
|
|
# No special event
|
|
assert mock_send_event.call_count == 0
|
|
assert mock_heater_off.call_count == 0
|
|
|
|
# The first heater should be turned on but is already on but because above we mock
|
|
# call_later the heater is not on. But this time it will be really on
|
|
assert mock_heater_on.call_count == 1
|
|
|
|
# Set another temperature at middle level
|
|
event_timestamp = now - timedelta(minutes=3)
|
|
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=True,
|
|
) as mock_device_active:
|
|
await send_temperature_change_event(entity, 18.1, event_timestamp)
|
|
|
|
# No special event
|
|
assert mock_send_event.call_count == 0
|
|
assert mock_heater_off.call_count == 0
|
|
|
|
# The heater is already on cycle. So we wait that the cycle ends and no heater action
|
|
# is done
|
|
assert mock_heater_on.call_count == 0
|
|
# assert entity.underlying_entity(0)._should_relaunch_control_heating is True
|
|
|
|
# Simulate the relaunch
|
|
await entity.underlying_entity(
|
|
0
|
|
)._turn_on_later( # pylint: disable=protected-access
|
|
None
|
|
)
|
|
# wait restart
|
|
await asyncio.sleep(0.1)
|
|
|
|
assert mock_heater_on.call_count == 1
|
|
# normal ? assert entity.underlying_entity(0)._should_relaunch_control_heating is False
|
|
|
|
# Simulate the end of heater on cycle
|
|
event_timestamp = now - timedelta(minutes=3)
|
|
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=True,
|
|
) as mock_device_active:
|
|
await entity.underlying_entity(
|
|
0
|
|
)._turn_off_later( # pylint: disable=protected-access
|
|
None
|
|
)
|
|
|
|
# No special event
|
|
assert mock_send_event.call_count == 0
|
|
assert mock_heater_on.call_count == 0
|
|
# The heater should be turned off this time
|
|
assert mock_heater_off.call_count == 1
|
|
# assert entity.underlying_entity(0)._should_relaunch_control_heating is False
|
|
|
|
# Simulate the start of heater on cycle
|
|
event_timestamp = now - timedelta(minutes=3)
|
|
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=True,
|
|
) as mock_device_active:
|
|
await entity.underlying_entity(
|
|
0
|
|
)._turn_on_later( # pylint: disable=protected-access
|
|
None
|
|
)
|
|
|
|
# No special event
|
|
assert mock_send_event.call_count == 0
|
|
assert mock_heater_on.call_count == 1
|
|
# The heater should be turned off this time
|
|
assert mock_heater_off.call_count == 0
|
|
# assert entity.underlying_entity(0)._should_relaunch_control_heating is False
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_multiple_switchs(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_send_event,
|
|
): # pylint: disable=unused-argument
|
|
"""Test that when multiple switch are configured the activation is distributed"""
|
|
|
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
|
now: datetime = datetime.now(tz=tz)
|
|
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
title="TheOver4SwitchMockName",
|
|
unique_id="uniqueId",
|
|
data={
|
|
CONF_NAME: "TheOver4SwitchMockName",
|
|
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: 8,
|
|
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_switch1",
|
|
CONF_HEATER_2: "switch.mock_switch2",
|
|
CONF_HEATER_3: "switch.mock_switch3",
|
|
CONF_HEATER_4: "switch.mock_switch4",
|
|
CONF_HEATER_KEEP_ALIVE: 0,
|
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
|
CONF_SAFETY_DELAY_MIN: 5,
|
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
|
CONF_TPI_COEF_INT: 0.3,
|
|
CONF_TPI_COEF_EXT: 0.01,
|
|
},
|
|
)
|
|
|
|
entity: BaseThermostat = await create_thermostat(
|
|
hass, entry, "climate.theover4switchmockname"
|
|
)
|
|
assert entity
|
|
assert entity.is_over_climate is False
|
|
assert entity.nb_underlying_entities == 4
|
|
|
|
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
|
), patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.set_hvac_mode"
|
|
) as mock_underlying_set_hvac_mode:
|
|
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_UNAVAILABLE
|
|
|
|
event_timestamp = now - timedelta(minutes=4)
|
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
|
|
|
# Checks that all climates are off
|
|
assert entity.is_device_active is False # pylint: disable=protected-access
|
|
|
|
# Should be call for all Switch
|
|
assert mock_underlying_set_hvac_mode.call_count == 4
|
|
mock_underlying_set_hvac_mode.assert_has_calls(
|
|
[
|
|
call.set_hvac_mode(HVACMode.HEAT),
|
|
]
|
|
)
|
|
|
|
# Set temperature to a low level
|
|
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,
|
|
) as mock_device_active, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
|
|
return_value=None,
|
|
) as mock_call_later:
|
|
await send_ext_temperature_change_event(entity, 5, event_timestamp)
|
|
|
|
# No special event
|
|
assert mock_send_event.call_count == 0
|
|
assert mock_heater_off.call_count == 0
|
|
|
|
# The first heater should be on but because call_later is mocked heater_on is not called
|
|
# assert mock_heater_on.call_count == 1
|
|
assert mock_heater_on.call_count == 0
|
|
# There is no check if active
|
|
# don't work with PropertyMock
|
|
# assert mock_device_active.call_count == 0
|
|
|
|
# 4 calls dispatched along the cycle
|
|
assert mock_call_later.call_count == 4
|
|
mock_call_later.assert_has_calls(
|
|
[
|
|
call.call_later(hass, 0.0, ANY),
|
|
call.call_later(hass, 120.0, ANY),
|
|
call.call_later(hass, 240.0, ANY),
|
|
call.call_later(hass, 360.0, ANY),
|
|
]
|
|
)
|
|
|
|
# Set a temperature at middle level
|
|
event_timestamp = now - timedelta(minutes=4)
|
|
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,
|
|
) as mock_device_active:
|
|
await send_temperature_change_event(entity, 18, event_timestamp)
|
|
|
|
# No special event
|
|
assert mock_send_event.call_count == 0
|
|
assert mock_heater_off.call_count == 0
|
|
|
|
# The first heater should be turned on but is already on but because call_later
|
|
# is mocked, it is only turned on here
|
|
assert mock_heater_on.call_count == 1
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_multiple_climates(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_send_event,
|
|
): # pylint: disable=unused-argument
|
|
"""Test that when multiple climates are configured the activation and deactivation
|
|
is propagated to all climates"""
|
|
|
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
|
now: datetime = datetime.now(tz=tz)
|
|
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
title="TheOver4ClimateMockName",
|
|
unique_id="uniqueId",
|
|
data={
|
|
CONF_NAME: "TheOver4ClimateMockName",
|
|
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: 8,
|
|
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: "switch.mock_climate1",
|
|
CONF_CLIMATE_2: "switch.mock_climate2",
|
|
CONF_CLIMATE_3: "switch.mock_climate3",
|
|
CONF_CLIMATE_4: "switch.mock_climate4",
|
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
|
CONF_SAFETY_DELAY_MIN: 5,
|
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
|
},
|
|
)
|
|
|
|
entity: BaseThermostat = await create_thermostat(
|
|
hass, entry, "climate.theover4climatemockname"
|
|
)
|
|
assert entity
|
|
assert entity.is_over_climate is True
|
|
assert entity.nb_underlying_entities == 4
|
|
|
|
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
|
), patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
|
) as mock_underlying_set_hvac_mode:
|
|
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_UNAVAILABLE
|
|
|
|
event_timestamp = now - timedelta(minutes=4)
|
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
|
|
|
# Should be call for all Switch
|
|
assert mock_underlying_set_hvac_mode.call_count == 4
|
|
mock_underlying_set_hvac_mode.assert_has_calls(
|
|
[
|
|
call.set_hvac_mode(HVACMode.HEAT),
|
|
]
|
|
)
|
|
assert entity.is_device_active is False # pylint: disable=protected-access
|
|
|
|
# Stop heating, in boost mode. We block the control_heating to avoid running a cycle
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
|
), patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
|
) as mock_underlying_set_hvac_mode:
|
|
await entity.async_set_hvac_mode(HVACMode.OFF)
|
|
|
|
assert entity.hvac_mode is HVACMode.OFF
|
|
assert entity.preset_mode is PRESET_BOOST
|
|
assert entity.target_temperature == 19
|
|
assert entity.window_state is STATE_UNAVAILABLE
|
|
|
|
event_timestamp = now - timedelta(minutes=4)
|
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
|
|
|
# Should be call for all Switch
|
|
assert mock_underlying_set_hvac_mode.call_count == 4
|
|
mock_underlying_set_hvac_mode.assert_has_calls(
|
|
[
|
|
call.set_hvac_mode(HVACMode.OFF),
|
|
]
|
|
)
|
|
assert entity.is_device_active is False # pylint: disable=protected-access
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_multiple_climates_underlying_changes(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_send_event,
|
|
): # pylint: disable=unused-argument
|
|
"""Test that when multiple climate are configured the activation of one underlying
|
|
climate activate the others"""
|
|
|
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
|
now: datetime = datetime.now(tz=tz)
|
|
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
title="TheOver4ClimateMockName",
|
|
unique_id="uniqueId",
|
|
data={
|
|
CONF_NAME: "TheOver4ClimateMockName",
|
|
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: 8,
|
|
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: "switch.mock_climate1",
|
|
CONF_CLIMATE_2: "switch.mock_climate2",
|
|
CONF_CLIMATE_3: "switch.mock_climate3",
|
|
CONF_CLIMATE_4: "switch.mock_climate4",
|
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
|
CONF_SAFETY_DELAY_MIN: 5,
|
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
|
},
|
|
)
|
|
|
|
entity: BaseThermostat = await create_thermostat(
|
|
hass, entry, "climate.theover4climatemockname"
|
|
)
|
|
assert entity
|
|
assert entity.is_over_climate is True
|
|
assert entity.nb_underlying_entities == 4
|
|
|
|
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
|
), patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
|
) as mock_underlying_set_hvac_mode:
|
|
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_UNAVAILABLE
|
|
|
|
event_timestamp = now - timedelta(minutes=4)
|
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
|
|
|
# Should be call for all Switch
|
|
assert mock_underlying_set_hvac_mode.call_count == 4
|
|
mock_underlying_set_hvac_mode.assert_has_calls(
|
|
[
|
|
call.set_hvac_mode(HVACMode.HEAT),
|
|
]
|
|
)
|
|
assert entity.is_device_active is False # pylint: disable=protected-access
|
|
|
|
# Stop heating on one underlying climate
|
|
# All underlying supposed to be aligned with the hvac_mode now
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
|
), patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
|
) as mock_underlying_set_hvac_mode, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
|
|
HVACMode.HEAT,
|
|
):
|
|
# Wait 11 sec so that the event will not be discarded
|
|
event_timestamp = now + timedelta(seconds=11)
|
|
await send_climate_change_event(
|
|
entity,
|
|
HVACMode.OFF,
|
|
HVACMode.HEAT,
|
|
HVACAction.OFF,
|
|
HVACAction.HEATING,
|
|
event_timestamp,
|
|
underlying_entity_id="switch.mock_climate3",
|
|
)
|
|
|
|
# Should be call for all Switch
|
|
assert mock_underlying_set_hvac_mode.call_count == 4
|
|
mock_underlying_set_hvac_mode.assert_has_calls(
|
|
[
|
|
call.set_hvac_mode(HVACMode.OFF),
|
|
]
|
|
)
|
|
assert entity.hvac_mode == HVACMode.OFF
|
|
assert entity.is_device_active is False # pylint: disable=protected-access
|
|
|
|
# Start heating on one underlying climate
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
|
), patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
|
) as mock_underlying_set_hvac_mode, patch(
|
|
# notice that there is no need of return_value=HVACAction.IDLE because this is not
|
|
# a function but a property
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action",
|
|
HVACAction.IDLE,
|
|
), patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
|
|
HVACMode.OFF,
|
|
):
|
|
# Wait 11 sec so that the event will not be discarded
|
|
event_timestamp = now + timedelta(seconds=11)
|
|
await send_climate_change_event(
|
|
entity,
|
|
HVACMode.HEAT,
|
|
HVACMode.OFF,
|
|
HVACAction.IDLE,
|
|
HVACAction.OFF,
|
|
event_timestamp,
|
|
underlying_entity_id="switch.mock_climate3",
|
|
)
|
|
|
|
# Should be call for all Switch
|
|
assert mock_underlying_set_hvac_mode.call_count == 4
|
|
mock_underlying_set_hvac_mode.assert_has_calls(
|
|
[
|
|
call.set_hvac_mode(HVACMode.HEAT),
|
|
]
|
|
)
|
|
assert entity.hvac_mode == HVACMode.HEAT
|
|
assert entity.hvac_action == HVACAction.IDLE
|
|
assert entity.is_device_active is False # pylint: disable=protected-access
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_multiple_climates_underlying_changes_not_aligned(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_send_event,
|
|
): # pylint: disable=unused-argument
|
|
"""Test that when multiple climate are configured the activation of one underlying
|
|
climate don't activate the others if their havc_mode are not aligned"""
|
|
|
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
|
now: datetime = datetime.now(tz=tz)
|
|
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
title="TheOver4ClimateMockName",
|
|
unique_id="uniqueId",
|
|
data={
|
|
CONF_NAME: "TheOver4ClimateMockName",
|
|
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: 8,
|
|
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: "switch.mock_climate1",
|
|
CONF_CLIMATE_2: "switch.mock_climate2",
|
|
CONF_CLIMATE_3: "switch.mock_climate3",
|
|
CONF_CLIMATE_4: "switch.mock_climate4",
|
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
|
CONF_SAFETY_DELAY_MIN: 5,
|
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
|
},
|
|
)
|
|
|
|
entity: BaseThermostat = await create_thermostat(
|
|
hass, entry, "climate.theover4climatemockname"
|
|
)
|
|
assert entity
|
|
assert entity.is_over_climate is True
|
|
assert entity.nb_underlying_entities == 4
|
|
|
|
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
|
), patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
|
) as mock_underlying_set_hvac_mode:
|
|
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_UNAVAILABLE
|
|
|
|
event_timestamp = now - timedelta(minutes=4)
|
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
|
|
|
# Should be call for all Switch
|
|
assert mock_underlying_set_hvac_mode.call_count == 4
|
|
mock_underlying_set_hvac_mode.assert_has_calls(
|
|
[
|
|
call.set_hvac_mode(HVACMode.HEAT),
|
|
]
|
|
)
|
|
|
|
# Stop heating on one underlying climate
|
|
# All underlying supposed to be aligned with the hvac_mode now
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
|
), patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
|
) as mock_underlying_set_hvac_mode, patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_mode",
|
|
HVACMode.COOL,
|
|
):
|
|
# Wait 11 sec so that the event will not be discarded
|
|
event_timestamp = now + timedelta(seconds=11)
|
|
await send_climate_change_event(
|
|
entity,
|
|
HVACMode.OFF,
|
|
HVACMode.HEAT,
|
|
HVACAction.OFF,
|
|
HVACAction.HEATING,
|
|
event_timestamp,
|
|
underlying_entity_id="switch.mock_climate3",
|
|
)
|
|
|
|
# Should be call for all Switch
|
|
assert mock_underlying_set_hvac_mode.call_count == 0
|
|
# mock_underlying_set_hvac_mode.assert_has_calls(
|
|
# [
|
|
# call.set_hvac_mode(HVACMode.OFF),
|
|
# ]
|
|
# )
|
|
# No change
|
|
assert entity.hvac_mode == HVACMode.HEAT
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_multiple_switch_power_management(
|
|
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
|
):
|
|
"""Test the Power management"""
|
|
temps = {
|
|
"eco": 17,
|
|
"comfort": 18,
|
|
"boost": 19,
|
|
}
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
title="TheOverSwitchMockName",
|
|
unique_id="uniqueId",
|
|
data={
|
|
CONF_NAME: "TheOver4SwitchMockName",
|
|
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: 8,
|
|
CONF_TEMP_MIN: 15,
|
|
CONF_TEMP_MAX: 30,
|
|
CONF_USE_WINDOW_FEATURE: False,
|
|
CONF_USE_MOTION_FEATURE: False,
|
|
CONF_USE_POWER_FEATURE: True,
|
|
CONF_USE_PRESENCE_FEATURE: False,
|
|
CONF_UNDERLYING_LIST: [
|
|
"switch.mock_switch1",
|
|
"switch.mock_switch2",
|
|
"switch.mock_switch3",
|
|
"switch.mock_switch4",
|
|
],
|
|
CONF_HEATER_KEEP_ALIVE: 0,
|
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
|
CONF_SAFETY_DELAY_MIN: 5,
|
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
|
CONF_TPI_COEF_INT: 0.3,
|
|
CONF_TPI_COEF_EXT: 0.01,
|
|
CONF_DEVICE_POWER: 100,
|
|
CONF_PRESET_POWER: 12,
|
|
},
|
|
)
|
|
|
|
entity: BaseThermostat = await create_thermostat(
|
|
hass, entry, "climate.theover4switchmockname", temps
|
|
)
|
|
assert entity
|
|
assert entity.is_over_climate is False
|
|
assert entity.nb_underlying_entities == 4
|
|
|
|
tpi_algo = entity._prop_algorithm
|
|
assert tpi_algo
|
|
|
|
now: datetime = NowClass.get_now(hass)
|
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
|
|
|
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.power_manager.overpowering_state is STATE_UNKNOWN
|
|
assert entity.target_temperature == 19
|
|
|
|
# make the heater heats
|
|
await send_temperature_change_event(entity, 15, now)
|
|
await send_ext_temperature_change_event(entity, 1, now)
|
|
await hass.async_block_till_done()
|
|
|
|
# 1. Send power mesurement
|
|
side_effects = SideEffects(
|
|
{
|
|
"sensor.the_power_sensor": State("sensor.the_power_sensor", 50),
|
|
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 300),
|
|
},
|
|
State("unknown.entity_id", "unknown"),
|
|
)
|
|
|
|
# Send power max mesurement
|
|
# fmt:off
|
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
|
# fmt: on
|
|
now = now + timedelta(seconds=30)
|
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
|
|
|
await send_power_change_event(entity, 50, datetime.now())
|
|
await send_max_power_change_event(entity, 300, datetime.now())
|
|
assert entity.power_manager.is_overpowering_detected is False
|
|
# All configuration is complete and power is < power_max
|
|
assert entity.preset_mode is PRESET_BOOST
|
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
|
|
|
# 2. Send power max mesurement too low and HVACMode is on
|
|
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 49))
|
|
|
|
#fmt: off
|
|
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.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True):
|
|
#fmt: on
|
|
now = now + timedelta(seconds=30)
|
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
|
|
|
assert entity.power_percent > 0
|
|
# 100 of the device / 4 -> 25, current power 50 so max is 75
|
|
await send_max_power_change_event(entity, 49, datetime.now())
|
|
assert entity.power_manager.is_overpowering_detected is True
|
|
# All configuration is complete and power is > power_max we switch to POWER preset
|
|
assert entity.preset_mode is PRESET_POWER
|
|
assert entity.power_manager.overpowering_state is STATE_ON
|
|
assert entity.target_temperature == 12
|
|
|
|
assert mock_send_event.call_count == 2
|
|
mock_send_event.assert_has_calls(
|
|
[
|
|
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_POWER}),
|
|
call.send_event(
|
|
EventType.POWER_EVENT,
|
|
{
|
|
"type": "start",
|
|
"current_power": 50,
|
|
"device_power": 100,
|
|
"current_max_power": 49,
|
|
"current_power_consumption": 100,
|
|
},
|
|
),
|
|
],
|
|
any_order=True,
|
|
)
|
|
assert mock_heater_on.call_count == 0
|
|
assert mock_heater_off.call_count == 4 # The fourth are shutdown
|
|
|
|
# 3. change PRESET
|
|
with patch(
|
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
) as mock_send_event:
|
|
now = now + timedelta(seconds=30)
|
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
|
|
|
await entity.async_set_preset_mode(PRESET_ECO)
|
|
assert entity.preset_mode is PRESET_ECO
|
|
# No change cause temperature is very low
|
|
assert entity.power_manager.overpowering_state is STATE_ON
|
|
|
|
# 4. Send hugh power max mesurement to release overpowering
|
|
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 150))
|
|
|
|
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:
|
|
now = now + timedelta(seconds=30)
|
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
|
|
|
# 100 of the device / 4 -> 25, current power 50 so max is 75. With 150 no overheating
|
|
await send_max_power_change_event(entity, 150, datetime.now())
|
|
assert entity.power_manager.is_overpowering_detected is False
|
|
# All configuration is complete and power is > power_max we switch to POWER preset
|
|
assert entity.preset_mode is PRESET_ECO
|
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
|
assert entity.target_temperature == 17
|
|
|
|
assert (
|
|
mock_heater_on.call_count == 0
|
|
) # The fourth are not restarted because temperature is enought
|
|
assert mock_heater_off.call_count == 0
|