* Add minimal_deactivation_delay In some cases users may want to keep the equipment on if it will deactivate for a short time. This could help reduce wear on gas boilers. * fix(prop_algorithm): correct the log message for minimal_deactivation_delay * fix(translations): correct the translations for minimal_deactivation_delay
965 lines
39 KiB
Python
965 lines
39 KiB
Python
# pylint: disable=protected-access, unused-argument, line-too-long
|
|
""" Test the Power management """
|
|
from unittest.mock import patch, call, AsyncMock, MagicMock, PropertyMock
|
|
from datetime import datetime, timedelta
|
|
import logging
|
|
|
|
from custom_components.versatile_thermostat.thermostat_switch import (
|
|
ThermostatOverSwitch,
|
|
)
|
|
from custom_components.versatile_thermostat.feature_power_manager import (
|
|
FeaturePowerManager,
|
|
)
|
|
|
|
from custom_components.versatile_thermostat.prop_algorithm import PropAlgorithm
|
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
|
|
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"is_over_climate, is_device_active, power, max_power, check_power_available",
|
|
[
|
|
# don't switch to overpower (power is enough)
|
|
(False, False, 1000, 3000, True),
|
|
# switch to overpower (power is not enough)
|
|
(False, False, 2000, 3000, False),
|
|
# don't switch to overpower (power is not enough but device is already on)
|
|
(False, True, 2000, 3000, True),
|
|
# Same with a over_climate
|
|
# don't switch to overpower (power is enough)
|
|
(True, False, 1000, 3000, True),
|
|
# switch to overpower (power is not enough)
|
|
(True, False, 2000, 3000, False),
|
|
# don't switch to overpower (power is not enough but device is already on)
|
|
(True, True, 2000, 3000, True),
|
|
# Leave overpowering state
|
|
# switch to not overpower (power is enough)
|
|
(False, False, 1000, 3000, True),
|
|
# don't switch to overpower (power is still not enough)
|
|
(False, False, 2000, 3000, False),
|
|
# keep overpower (power is not enough but device is already on)
|
|
(False, True, 3000, 3000, False),
|
|
],
|
|
)
|
|
async def test_power_feature_manager(
|
|
hass: HomeAssistant,
|
|
is_over_climate,
|
|
is_device_active,
|
|
power,
|
|
max_power,
|
|
check_power_available,
|
|
):
|
|
"""Test the FeaturePresenceManager class direclty"""
|
|
|
|
fake_vtherm = MagicMock(spec=BaseThermostat)
|
|
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
|
|
|
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
|
|
|
# 1. creation
|
|
power_manager = FeaturePowerManager(fake_vtherm, hass)
|
|
|
|
assert power_manager is not None
|
|
assert power_manager.is_configured is False
|
|
assert power_manager.overpowering_state == STATE_UNAVAILABLE
|
|
assert power_manager.name == "the name"
|
|
|
|
assert len(power_manager._active_listener) == 0
|
|
|
|
custom_attributes = {}
|
|
power_manager.add_custom_attributes(custom_attributes)
|
|
assert custom_attributes["power_sensor_entity_id"] is None
|
|
assert custom_attributes["max_power_sensor_entity_id"] is None
|
|
assert custom_attributes["overpowering_state"] == STATE_UNAVAILABLE
|
|
assert custom_attributes["is_power_configured"] is False
|
|
assert custom_attributes["device_power"] is 0
|
|
assert custom_attributes["power_temp"] is None
|
|
assert custom_attributes["current_power"] is None
|
|
assert custom_attributes["current_max_power"] is None
|
|
|
|
# 2. post_init
|
|
vtherm_api.find_central_configuration = MagicMock()
|
|
vtherm_api.central_power_manager.post_init(
|
|
{
|
|
CONF_POWER_SENSOR: "sensor.the_power_sensor",
|
|
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
|
|
CONF_USE_POWER_FEATURE: True,
|
|
CONF_PRESET_POWER: 13,
|
|
}
|
|
)
|
|
assert vtherm_api.central_power_manager.is_configured
|
|
|
|
power_manager.post_init(
|
|
{
|
|
CONF_USE_POWER_FEATURE: True,
|
|
CONF_PRESET_POWER: 10,
|
|
CONF_DEVICE_POWER: 1234,
|
|
}
|
|
)
|
|
|
|
await power_manager.start_listening()
|
|
|
|
assert power_manager.is_configured is True
|
|
assert power_manager.overpowering_state == STATE_UNKNOWN
|
|
|
|
custom_attributes = {}
|
|
power_manager.add_custom_attributes(custom_attributes)
|
|
assert custom_attributes["power_sensor_entity_id"] == "sensor.the_power_sensor"
|
|
assert (
|
|
custom_attributes["max_power_sensor_entity_id"] == "sensor.the_max_power_sensor"
|
|
)
|
|
assert custom_attributes["overpowering_state"] == STATE_UNKNOWN
|
|
assert custom_attributes["is_power_configured"] is True
|
|
assert custom_attributes["device_power"] == 1234
|
|
assert custom_attributes["power_temp"] == 10
|
|
assert custom_attributes["current_power"] is None
|
|
assert custom_attributes["current_max_power"] is None
|
|
|
|
# 3. start listening
|
|
await power_manager.start_listening()
|
|
assert power_manager.is_configured is True
|
|
assert power_manager.overpowering_state == STATE_UNKNOWN
|
|
|
|
assert len(power_manager._active_listener) == 0 # no more listening
|
|
|
|
# 4. test refresh and check_overpowering with the parametrized
|
|
# fmt:off
|
|
with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=max_power), \
|
|
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=power):
|
|
# fmt:on
|
|
|
|
# Finish the mock configuration
|
|
tpi_algo = PropAlgorithm(PROPORTIONAL_FUNCTION_TPI, 0.6, 0.01, 5, 0, 0, "climate.vtherm")
|
|
tpi_algo._on_percent = 1 # pylint: disable="protected-access"
|
|
type(fake_vtherm).hvac_mode = PropertyMock(return_value=HVACMode.HEAT)
|
|
type(fake_vtherm).is_device_active = PropertyMock(return_value=is_device_active)
|
|
type(fake_vtherm).is_over_climate = PropertyMock(return_value=is_over_climate)
|
|
type(fake_vtherm).proportional_algorithm = PropertyMock(return_value=tpi_algo)
|
|
type(fake_vtherm).nb_underlying_entities = PropertyMock(return_value=1)
|
|
|
|
ret = await power_manager.check_power_available()
|
|
assert ret == check_power_available
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"is_over_climate, current_overpowering_state, is_overpowering, new_overpowering_state, msg_sent",
|
|
[
|
|
# false -> false
|
|
(False, STATE_OFF, False, STATE_OFF, False),
|
|
# false -> true
|
|
(False, STATE_OFF, True, STATE_ON, True),
|
|
# true -> true
|
|
(False, STATE_ON, True, STATE_ON, False),
|
|
# true -> False
|
|
(False, STATE_ON, False, STATE_OFF, True),
|
|
# Same with over_climate
|
|
# false -> false
|
|
(True, STATE_OFF, False, STATE_OFF, False),
|
|
# false -> true
|
|
(True, STATE_OFF, True, STATE_ON, True),
|
|
# true -> true
|
|
(True, STATE_ON, True, STATE_ON, False),
|
|
# true -> False
|
|
(True, STATE_ON, False, STATE_OFF, True),
|
|
],
|
|
)
|
|
async def test_power_feature_manager_set_overpowering(
|
|
hass,
|
|
is_over_climate,
|
|
current_overpowering_state,
|
|
is_overpowering,
|
|
new_overpowering_state,
|
|
msg_sent,
|
|
):
|
|
"""Test the set_overpowering method of FeaturePowerManager"""
|
|
fake_vtherm = MagicMock(spec=BaseThermostat)
|
|
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
|
|
|
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
|
|
|
# 1. creation / init
|
|
power_manager = FeaturePowerManager(fake_vtherm, hass)
|
|
vtherm_api.find_central_configuration = MagicMock()
|
|
vtherm_api.central_power_manager.post_init(
|
|
{
|
|
CONF_POWER_SENSOR: "sensor.the_power_sensor",
|
|
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
|
|
CONF_USE_POWER_FEATURE: True,
|
|
CONF_PRESET_POWER: 13,
|
|
}
|
|
)
|
|
assert vtherm_api.central_power_manager.is_configured
|
|
|
|
power_manager.post_init(
|
|
{
|
|
CONF_USE_POWER_FEATURE: True,
|
|
CONF_PRESET_POWER: 10,
|
|
CONF_DEVICE_POWER: 1234,
|
|
}
|
|
)
|
|
|
|
await power_manager.start_listening()
|
|
|
|
assert power_manager.is_configured is True
|
|
assert power_manager.overpowering_state == STATE_UNKNOWN
|
|
|
|
# check overpowering
|
|
power_manager._overpowering_state = current_overpowering_state
|
|
|
|
# fmt:off
|
|
with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=2000), \
|
|
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=1000):
|
|
# fmt:on
|
|
# Finish mocking
|
|
fake_vtherm.is_over_climate = is_over_climate
|
|
fake_vtherm.preset_mode = MagicMock(return_value=PRESET_COMFORT if current_overpowering_state == STATE_OFF else PRESET_POWER)
|
|
fake_vtherm._saved_preset_mode = PRESET_ECO
|
|
|
|
fake_vtherm.save_hvac_mode = MagicMock()
|
|
fake_vtherm.restore_hvac_mode = AsyncMock()
|
|
fake_vtherm.save_preset_mode = MagicMock()
|
|
fake_vtherm.restore_preset_mode = AsyncMock()
|
|
fake_vtherm.async_underlying_entity_turn_off = AsyncMock()
|
|
fake_vtherm.async_set_preset_mode_internal = AsyncMock()
|
|
fake_vtherm.send_event = MagicMock()
|
|
fake_vtherm.update_custom_attributes = MagicMock()
|
|
|
|
|
|
# Call set_overpowering
|
|
await power_manager.set_overpowering(is_overpowering, 1234)
|
|
|
|
assert power_manager.overpowering_state == new_overpowering_state
|
|
|
|
if not is_overpowering:
|
|
assert power_manager.overpowering_state == STATE_OFF
|
|
assert fake_vtherm.save_hvac_mode.call_count == 0
|
|
assert fake_vtherm.save_preset_mode.call_count == 0
|
|
assert fake_vtherm.async_underlying_entity_turn_off.call_count == 0
|
|
assert fake_vtherm.async_set_preset_mode_internal.call_count == 0
|
|
|
|
if current_overpowering_state == STATE_ON:
|
|
assert fake_vtherm.update_custom_attributes.call_count == 1
|
|
assert fake_vtherm.restore_preset_mode.call_count == 1
|
|
if is_over_climate:
|
|
assert fake_vtherm.restore_hvac_mode.call_count == 1
|
|
else:
|
|
assert fake_vtherm.restore_hvac_mode.call_count == 0
|
|
else:
|
|
assert fake_vtherm.update_custom_attributes.call_count == 0
|
|
|
|
if msg_sent:
|
|
fake_vtherm.send_event.assert_has_calls(
|
|
[
|
|
call.fake_vtherm.send_event(
|
|
EventType.POWER_EVENT,
|
|
{
|
|
"type": "end",
|
|
"current_power": 1000,
|
|
"device_power": 1234,
|
|
"current_max_power": 2000,
|
|
},
|
|
),
|
|
]
|
|
)
|
|
# is_overpowering is True
|
|
else:
|
|
assert power_manager.overpowering_state == STATE_ON
|
|
if is_over_climate and current_overpowering_state == STATE_OFF:
|
|
assert fake_vtherm.save_hvac_mode.call_count == 1
|
|
else:
|
|
assert fake_vtherm.save_hvac_mode.call_count == 0
|
|
|
|
if current_overpowering_state == STATE_OFF:
|
|
assert fake_vtherm.save_preset_mode.call_count == 1
|
|
assert fake_vtherm.async_underlying_entity_turn_off.call_count == 1
|
|
assert fake_vtherm.async_set_preset_mode_internal.call_count == 1
|
|
assert fake_vtherm.send_event.call_count == 1
|
|
assert fake_vtherm.update_custom_attributes.call_count == 1
|
|
else:
|
|
assert fake_vtherm.save_preset_mode.call_count == 0
|
|
assert fake_vtherm.async_underlying_entity_turn_off.call_count == 0
|
|
assert fake_vtherm.async_set_preset_mode_internal.call_count == 0
|
|
assert fake_vtherm.send_event.call_count == 0
|
|
assert fake_vtherm.update_custom_attributes.call_count == 0
|
|
assert fake_vtherm.restore_hvac_mode.call_count == 0
|
|
assert fake_vtherm.restore_preset_mode.call_count == 0
|
|
|
|
if msg_sent:
|
|
fake_vtherm.send_event.assert_has_calls(
|
|
[
|
|
call.fake_vtherm.send_event(
|
|
EventType.POWER_EVENT,
|
|
{
|
|
"type": "start",
|
|
"current_power": 1000,
|
|
"device_power": 1234,
|
|
"current_max_power": 2000,
|
|
"current_power_consumption": 1234.0,
|
|
},
|
|
),
|
|
]
|
|
)
|
|
|
|
fake_vtherm.reset_mock()
|
|
|
|
# 5. Check custom_attributes
|
|
custom_attributes = {}
|
|
power_manager.add_custom_attributes(custom_attributes)
|
|
assert custom_attributes["power_sensor_entity_id"] == "sensor.the_power_sensor"
|
|
assert (
|
|
custom_attributes["max_power_sensor_entity_id"] == "sensor.the_max_power_sensor"
|
|
)
|
|
assert custom_attributes["overpowering_state"] == new_overpowering_state
|
|
assert custom_attributes["is_power_configured"] is True
|
|
assert custom_attributes["device_power"] == 1234
|
|
assert custom_attributes["power_temp"] == 10
|
|
assert custom_attributes["current_power"] == 1000
|
|
assert custom_attributes["current_max_power"] == 2000
|
|
|
|
power_manager.stop_listening()
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_power_management_hvac_off(
|
|
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: "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,
|
|
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_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_MINIMAL_DEACTIVATION_DELAY: 0,
|
|
CONF_SAFETY_DELAY_MIN: 5,
|
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
|
CONF_DEVICE_POWER: 100,
|
|
CONF_PRESET_POWER: 12,
|
|
},
|
|
)
|
|
|
|
entity: ThermostatOverSwitch = await create_thermostat(
|
|
hass, entry, "climate.theoverswitchmockname", temps
|
|
)
|
|
assert entity
|
|
|
|
tpi_algo = entity._prop_algorithm
|
|
assert tpi_algo
|
|
|
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
|
assert entity.preset_mode is PRESET_BOOST
|
|
assert entity.target_temperature == 19
|
|
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
|
assert entity.hvac_mode == HVACMode.OFF
|
|
|
|
now: datetime = NowClass.get_now(hass)
|
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
|
|
|
# Send power mesurement
|
|
# fmt:off
|
|
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"),
|
|
)
|
|
# fmt:off
|
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
|
# fmt: on
|
|
await send_power_change_event(entity, 50, now)
|
|
assert entity.power_manager.is_overpowering_detected is False
|
|
|
|
# All configuration is not complete
|
|
assert entity.preset_mode is PRESET_BOOST
|
|
assert entity.power_manager.overpowering_state is STATE_UNKNOWN # due to hvac_off
|
|
|
|
# Send power max mesurement
|
|
now = now + timedelta(seconds=30)
|
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
|
await send_max_power_change_event(entity, 300, 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_UNKNOWN # # due to hvac_off
|
|
|
|
# Send power max mesurement too low but HVACMode is off
|
|
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149))
|
|
# fmt:off
|
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
|
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:
|
|
# fmt: on
|
|
now = now + timedelta(seconds=30)
|
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
|
|
|
await send_max_power_change_event(entity, 149, datetime.now())
|
|
assert entity.power_manager.is_overpowering_detected is False
|
|
# All configuration is complete and power is > power_max but we stay in Boost cause thermostat if Off
|
|
assert entity.preset_mode is PRESET_BOOST
|
|
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
|
|
|
assert mock_send_event.call_count == 0
|
|
assert mock_heater_on.call_count == 0
|
|
assert mock_heater_off.call_count == 0
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_power_management_hvac_on(
|
|
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: "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,
|
|
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_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_MINIMAL_DEACTIVATION_DELAY: 0,
|
|
CONF_SAFETY_DELAY_MIN: 5,
|
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
|
CONF_DEVICE_POWER: 100,
|
|
CONF_PRESET_POWER: 12,
|
|
},
|
|
)
|
|
|
|
entity: ThermostatOverSwitch = await create_thermostat(
|
|
hass, entry, "climate.theoverswitchmockname", temps
|
|
)
|
|
assert entity
|
|
|
|
now: datetime = NowClass.get_now(hass)
|
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
|
|
|
tpi_algo = entity._prop_algorithm
|
|
assert tpi_algo
|
|
|
|
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()
|
|
|
|
assert entity.power_percent > 0
|
|
|
|
# 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"),
|
|
)
|
|
# fmt:off
|
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
|
# fmt: on
|
|
await send_power_change_event(entity, 50, datetime.now())
|
|
# Send power max mesurement
|
|
now = now + timedelta(seconds=30)
|
|
VersatileThermostatAPI.get_vtherm_api()._set_now(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
|
|
|
|
# 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("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
|
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)
|
|
|
|
await send_max_power_change_event(entity, 49, 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.0,
|
|
},
|
|
),
|
|
],
|
|
any_order=True,
|
|
)
|
|
assert mock_heater_on.call_count == 0
|
|
assert mock_heater_off.call_count == 1
|
|
|
|
# Send power mesurement low to unset power preset
|
|
side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 48))
|
|
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149))
|
|
# fmt:off
|
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
|
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:
|
|
# fmt: on
|
|
now = now + timedelta(seconds=30)
|
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
|
|
|
await send_power_change_event(entity, 48, now)
|
|
assert entity.power_manager.is_overpowering_detected is False
|
|
# All configuration is complete and power is < power_max, we restore previous preset
|
|
assert entity.preset_mode is PRESET_BOOST
|
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
|
assert entity.target_temperature == 19
|
|
|
|
assert mock_send_event.call_count == 2
|
|
mock_send_event.assert_has_calls(
|
|
[
|
|
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_BOOST}),
|
|
call.send_event(
|
|
EventType.POWER_EVENT,
|
|
{
|
|
"type": "end",
|
|
"current_power": 48,
|
|
"device_power": 100,
|
|
"current_max_power": 149,
|
|
},
|
|
),
|
|
],
|
|
any_order=True,
|
|
)
|
|
# No current temperature is set so the heater wont be turned on
|
|
assert mock_heater_on.call_count == 0
|
|
assert mock_heater_off.call_count == 0
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_power_management_energy_over_switch(
|
|
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
|
):
|
|
"""Test the Power management energy mesurement"""
|
|
|
|
temps = {
|
|
"eco": 17,
|
|
"comfort": 18,
|
|
"boost": 19,
|
|
}
|
|
|
|
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,
|
|
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_switch", "switch.mock_switch2"],
|
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
|
CONF_TPI_COEF_INT: 0.3,
|
|
CONF_TPI_COEF_EXT: 0.01,
|
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
|
CONF_MINIMAL_DEACTIVATION_DELAY: 0,
|
|
CONF_SAFETY_DELAY_MIN: 5,
|
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
|
CONF_DEVICE_POWER: 100,
|
|
CONF_PRESET_POWER: 12,
|
|
},
|
|
)
|
|
|
|
entity: ThermostatOverSwitch = await create_thermostat(
|
|
hass, entry, "climate.theoverswitchmockname", temps
|
|
)
|
|
assert entity
|
|
|
|
tpi_algo = entity._prop_algorithm
|
|
assert tpi_algo
|
|
|
|
assert entity.total_energy == 0
|
|
assert entity.nb_underlying_entities == 2
|
|
|
|
# set temperature to 15 so that on_percent will be set
|
|
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:
|
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
|
await send_temperature_change_event(entity, 15, datetime.now())
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
assert entity.hvac_mode is HVACMode.HEAT
|
|
assert entity.preset_mode is PRESET_BOOST
|
|
assert entity.target_temperature == 19
|
|
assert entity.current_temperature == 15
|
|
assert tpi_algo.on_percent == 1
|
|
|
|
assert entity.power_manager.device_power == 100.0
|
|
|
|
assert mock_send_event.call_count == 2
|
|
assert mock_heater_on.call_count == 1
|
|
assert mock_heater_off.call_count == 0
|
|
|
|
entity.incremente_energy()
|
|
assert entity.total_energy == round(100 * 5 / 60.0, 2)
|
|
entity.incremente_energy()
|
|
assert entity.total_energy == round(2 * 100 * 5 / 60.0, 2)
|
|
|
|
# change temperature to a higher value
|
|
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:
|
|
await send_temperature_change_event(entity, 18, datetime.now())
|
|
assert tpi_algo.on_percent == 0.3
|
|
assert entity.power_manager.mean_cycle_power == 30.0
|
|
|
|
assert mock_send_event.call_count == 0
|
|
assert mock_heater_on.call_count == 0
|
|
assert mock_heater_off.call_count == 0
|
|
|
|
entity.incremente_energy()
|
|
assert round(entity.total_energy, 2) == round((2.0 + 0.3) * 100 * 5 / 60.0, 2)
|
|
|
|
entity.incremente_energy()
|
|
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
|
|
|
|
# change temperature to a much higher value so that heater will be 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:
|
|
await send_temperature_change_event(entity, 20, datetime.now())
|
|
assert tpi_algo.on_percent == 0.0
|
|
assert entity.power_manager.mean_cycle_power == 0.0
|
|
|
|
assert mock_send_event.call_count == 0
|
|
assert mock_heater_on.call_count == 0
|
|
assert mock_heater_off.call_count == 0
|
|
|
|
entity.incremente_energy()
|
|
# No change on energy
|
|
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
|
|
|
|
# Still no change
|
|
entity.incremente_energy()
|
|
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_power_management_energy_over_climate(
|
|
hass: HomeAssistant, skip_hass_states_is_state
|
|
):
|
|
"""Test the Power management for a over_climate thermostat"""
|
|
|
|
temps = {
|
|
"eco": 17,
|
|
"comfort": 18,
|
|
"boost": 19,
|
|
}
|
|
|
|
the_mock_underlying = MagicMockClimate()
|
|
with patch(
|
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
|
return_value=the_mock_underlying,
|
|
):
|
|
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,
|
|
CONF_USE_WINDOW_FEATURE: False,
|
|
CONF_USE_MOTION_FEATURE: False,
|
|
CONF_USE_POWER_FEATURE: True,
|
|
CONF_USE_PRESENCE_FEATURE: False,
|
|
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
|
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
|
CONF_MINIMAL_DEACTIVATION_DELAY: 0,
|
|
CONF_SAFETY_DELAY_MIN: 5,
|
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
|
CONF_DEVICE_POWER: 100,
|
|
CONF_PRESET_POWER: 12,
|
|
},
|
|
)
|
|
|
|
entity: ThermostatOverSwitch = await create_thermostat(
|
|
hass, entry, "climate.theoverclimatemockname", temps
|
|
)
|
|
assert entity
|
|
assert entity.is_over_climate
|
|
|
|
now = datetime.now(tz=get_tz(hass))
|
|
await send_temperature_change_event(entity, 15, 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.hvac_action is HVACAction.IDLE
|
|
assert entity.preset_mode is PRESET_BOOST
|
|
assert entity.target_temperature == 19
|
|
assert entity.current_temperature == 15
|
|
|
|
# Not initialised yet
|
|
assert entity.power_manager.mean_cycle_power is None
|
|
assert entity._underlying_climate_start_hvac_action_date is None
|
|
|
|
# Send a climate_change event with HVACAction=HEATING
|
|
event_timestamp = now - timedelta(minutes=3)
|
|
await send_climate_change_event(
|
|
entity,
|
|
new_hvac_mode=HVACMode.HEAT,
|
|
old_hvac_mode=HVACMode.HEAT,
|
|
new_hvac_action=HVACAction.HEATING,
|
|
old_hvac_action=HVACAction.OFF,
|
|
date=event_timestamp,
|
|
underlying_entity_id="climate.mock_climate",
|
|
)
|
|
# We have the start event and not the end event
|
|
assert (entity._underlying_climate_start_hvac_action_date - now).total_seconds() < 1
|
|
|
|
entity.incremente_energy()
|
|
assert entity.total_energy == 0
|
|
|
|
# Send a climate_change event with HVACAction=IDLE (end of heating)
|
|
await send_climate_change_event(
|
|
entity,
|
|
new_hvac_mode=HVACMode.HEAT,
|
|
old_hvac_mode=HVACMode.HEAT,
|
|
new_hvac_action=HVACAction.IDLE,
|
|
old_hvac_action=HVACAction.HEATING,
|
|
date=now,
|
|
underlying_entity_id="climate.mock_climate",
|
|
)
|
|
# We have the end event -> we should have some power and on_percent
|
|
assert entity._underlying_climate_start_hvac_action_date is None
|
|
|
|
# 3 minutes at 100 W
|
|
assert entity.total_energy == 100 * 3.0 / 60
|
|
|
|
# Test the re-increment
|
|
entity.incremente_energy()
|
|
assert entity.total_energy == 2 * 100 * 3.0 / 60
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_power_management_turn_off_while_shedding(hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager):
|
|
"""Test the Power management and that we can turn off a Vtherm that
|
|
is in overpowering state"""
|
|
|
|
temps = {
|
|
"eco": 17,
|
|
"comfort": 18,
|
|
"boost": 19,
|
|
}
|
|
|
|
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,
|
|
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_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_MINIMAL_DEACTIVATION_DELAY: 0,
|
|
CONF_SAFETY_DELAY_MIN: 5,
|
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
|
CONF_DEVICE_POWER: 100,
|
|
CONF_PRESET_POWER: 12,
|
|
},
|
|
)
|
|
|
|
entity: ThermostatOverSwitch = await create_thermostat(hass, entry, "climate.theoverswitchmockname", temps)
|
|
assert entity
|
|
|
|
now: datetime = NowClass.get_now(hass)
|
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
|
|
|
tpi_algo = entity._prop_algorithm
|
|
assert tpi_algo
|
|
|
|
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()
|
|
|
|
assert entity.power_percent > 0
|
|
|
|
side_effects = SideEffects(
|
|
{
|
|
"sensor.the_power_sensor": State("sensor.the_power_sensor", 50),
|
|
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 49),
|
|
},
|
|
State("unknown.entity_id", "unknown"),
|
|
)
|
|
# # fmt:off
|
|
# with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
|
# # fmt: on
|
|
# await send_power_change_event(entity, 50, datetime.now())
|
|
# # Send power max mesurement
|
|
# now = now + timedelta(seconds=30)
|
|
# VersatileThermostatAPI.get_vtherm_api()._set_now(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
|
|
|
|
# 1. Set VTherm to overpowering
|
|
# Send power max mesurement too low and HVACMode is on and device is active
|
|
|
|
#
|
|
#
|
|
# fmt:off
|
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
|
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.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)
|
|
|
|
await send_max_power_change_event(entity, 49, 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_heater_on.call_count == 0
|
|
assert mock_heater_off.call_count == 1
|
|
|
|
# 2. Turn-off Vtherm
|
|
# fmt:off
|
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
|
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)
|
|
|
|
await entity.async_set_hvac_mode(HVACMode.OFF)
|
|
await VersatileThermostatAPI.get_vtherm_api().central_power_manager._do_immediate_shedding()
|
|
await hass.async_block_till_done()
|
|
|
|
# VTherm is off and overpowering if off also
|
|
assert entity.hvac_mode == HVACMode.OFF
|
|
assert entity.power_manager.is_overpowering_detected is False
|
|
assert entity.preset_mode is PRESET_BOOST
|
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
|
assert entity.target_temperature == 19
|