Issue #169 - Adds support for Versatile Thermostat UI Card Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
518 lines
19 KiB
Python
518 lines
19 KiB
Python
# pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long
|
|
|
|
""" Test the normal start of a Thermostat """
|
|
from unittest.mock import patch
|
|
from datetime import timedelta, datetime
|
|
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.components.climate import HVACMode
|
|
|
|
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
|
|
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
|
|
|
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
|
from custom_components.versatile_thermostat.binary_sensor import (
|
|
SecurityBinarySensor,
|
|
OverpoweringBinarySensor,
|
|
WindowBinarySensor,
|
|
MotionBinarySensor,
|
|
PresenceBinarySensor,
|
|
)
|
|
|
|
from .commons import *
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_security_binary_sensors(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_turn_on_off_heater,
|
|
skip_send_event,
|
|
): # pylint: disable=unused-argument
|
|
"""Test the security binary sensors in thermostat type"""
|
|
|
|
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.3,
|
|
},
|
|
)
|
|
|
|
entity: BaseThermostat = await create_thermostat (
|
|
hass, entry, "climate.theoverswitchmockname"
|
|
)
|
|
assert entity
|
|
|
|
security_binary_sensor: SecurityBinarySensor = search_entity(
|
|
hass, "binary_sensor.theoverswitchmockname_security_state", "binary_sensor"
|
|
)
|
|
assert security_binary_sensor
|
|
|
|
now: datetime = datetime.now(tz=get_tz(hass))
|
|
|
|
# Security should be disabled
|
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
|
|
|
assert security_binary_sensor.state == STATE_OFF
|
|
assert security_binary_sensor.device_class == BinarySensorDeviceClass.SAFETY
|
|
|
|
# Set temperature in the past
|
|
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)
|
|
|
|
assert entity.security_state is True
|
|
# Simulate the event reception
|
|
await security_binary_sensor.async_my_climate_changed()
|
|
assert security_binary_sensor.state == STATE_ON
|
|
|
|
# set temperature now
|
|
await send_temperature_change_event(entity, 15, now)
|
|
assert entity.security_state is False
|
|
# Simulate the event reception
|
|
await security_binary_sensor.async_my_climate_changed()
|
|
assert security_binary_sensor.state == STATE_OFF
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_overpowering_binary_sensors(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_turn_on_off_heater,
|
|
skip_send_event,
|
|
):
|
|
"""Test the overpowering binary sensors in thermostat type"""
|
|
|
|
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: BaseThermostat = await create_thermostat(
|
|
hass, entry, "climate.theoverswitchmockname"
|
|
)
|
|
assert entity
|
|
|
|
overpowering_binary_sensor: OverpoweringBinarySensor = search_entity(
|
|
hass, "binary_sensor.theoverswitchmockname_overpowering_state", "binary_sensor"
|
|
)
|
|
assert overpowering_binary_sensor
|
|
|
|
now: datetime = datetime.now(tz=get_tz(hass))
|
|
|
|
# Overpowering should be not set because poer have not been received
|
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
|
await send_temperature_change_event(entity, 15, now)
|
|
assert await entity.check_overpowering() is False
|
|
assert entity.overpowering_state is None
|
|
|
|
await overpowering_binary_sensor.async_my_climate_changed()
|
|
assert overpowering_binary_sensor.state is STATE_OFF
|
|
assert overpowering_binary_sensor.device_class == BinarySensorDeviceClass.POWER
|
|
|
|
await send_power_change_event(entity, 100, now)
|
|
await send_max_power_change_event(entity, 150, now)
|
|
assert await entity.check_overpowering() is True
|
|
assert entity.overpowering_state is True
|
|
|
|
# Simulate the event reception
|
|
await overpowering_binary_sensor.async_my_climate_changed()
|
|
assert overpowering_binary_sensor.state == STATE_ON
|
|
|
|
# set max power to a low value
|
|
await send_max_power_change_event(entity, 201, now)
|
|
assert await entity.check_overpowering() is False
|
|
assert entity.overpowering_state is False
|
|
# Simulate the event reception
|
|
await overpowering_binary_sensor.async_my_climate_changed()
|
|
assert overpowering_binary_sensor.state == STATE_OFF
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_window_binary_sensors(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_turn_on_off_heater,
|
|
skip_send_event,
|
|
):
|
|
"""Test the window binary sensors in thermostat type"""
|
|
|
|
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.3,
|
|
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
|
|
|
|
window_binary_sensor: WindowBinarySensor = search_entity(
|
|
hass, "binary_sensor.theoverswitchmockname_window_state", "binary_sensor"
|
|
)
|
|
assert window_binary_sensor
|
|
|
|
now: datetime = datetime.now(tz=get_tz(hass))
|
|
|
|
# Overpowering should be not set because poer have not been received
|
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
|
await send_temperature_change_event(entity, 15, now)
|
|
assert entity.window_state is STATE_OFF
|
|
|
|
await window_binary_sensor.async_my_climate_changed()
|
|
assert window_binary_sensor.state is STATE_OFF
|
|
assert window_binary_sensor.device_class == BinarySensorDeviceClass.WINDOW
|
|
|
|
# Open the window
|
|
with patch("homeassistant.helpers.condition.state", return_value=True):
|
|
try_window_condition = await send_window_change_event(entity, True, False, now)
|
|
# simulate the call to try_window_condition
|
|
await try_window_condition(None)
|
|
|
|
assert entity.window_state is STATE_ON
|
|
|
|
# Simulate the event reception
|
|
await window_binary_sensor.async_my_climate_changed()
|
|
assert window_binary_sensor.state == STATE_ON
|
|
|
|
# close the window
|
|
with patch("homeassistant.helpers.condition.state", return_value=True):
|
|
try_window_condition = await send_window_change_event(entity, False, True, now)
|
|
# simulate the call to try_window_condition
|
|
await try_window_condition(None)
|
|
|
|
assert entity.window_state is STATE_OFF
|
|
|
|
# Simulate the event reception
|
|
await window_binary_sensor.async_my_climate_changed()
|
|
assert window_binary_sensor.state == STATE_OFF
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_motion_binary_sensors(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_turn_on_off_heater,
|
|
skip_send_event,
|
|
):
|
|
"""Test the motion binary sensors in thermostat type"""
|
|
|
|
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: True,
|
|
CONF_USE_POWER_FEATURE: False,
|
|
CONF_USE_PRESENCE_FEATURE: False,
|
|
CONF_HEATER: "switch.mock_switch",
|
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
|
CONF_TPI_COEF_INT: 0.3,
|
|
CONF_TPI_COEF_EXT: 0.01,
|
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
|
CONF_SECURITY_DELAY_MIN: 5,
|
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
|
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
|
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
|
|
},
|
|
)
|
|
|
|
entity: BaseThermostat = await create_thermostat(
|
|
hass, entry, "climate.theoverswitchmockname"
|
|
)
|
|
assert entity
|
|
|
|
motion_binary_sensor: MotionBinarySensor = search_entity(
|
|
hass, "binary_sensor.theoverswitchmockname_motion_state", "binary_sensor"
|
|
)
|
|
assert motion_binary_sensor
|
|
|
|
now: datetime = datetime.now(tz=get_tz(hass))
|
|
|
|
# Overpowering should be not set because poer have not been received
|
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
|
await send_temperature_change_event(entity, 15, now)
|
|
assert entity.motion_state is None
|
|
|
|
await motion_binary_sensor.async_my_climate_changed()
|
|
assert motion_binary_sensor.state is STATE_OFF
|
|
assert motion_binary_sensor.device_class == BinarySensorDeviceClass.MOTION
|
|
|
|
# Detect motion
|
|
with patch("homeassistant.helpers.condition.state", return_value=True):
|
|
try_motion_condition = await send_motion_change_event(entity, True, False, now)
|
|
# simulate the call to try_window_condition
|
|
await try_motion_condition(None)
|
|
|
|
assert entity.motion_state is STATE_ON
|
|
|
|
# Simulate the event reception
|
|
await motion_binary_sensor.async_my_climate_changed()
|
|
assert motion_binary_sensor.state == STATE_ON
|
|
|
|
# Undetect motion
|
|
with patch("homeassistant.helpers.condition.state", return_value=True):
|
|
try_motion_condition = await send_motion_change_event(entity, False, True, now)
|
|
# simulate the call to try_motion_condition
|
|
await try_motion_condition(None)
|
|
|
|
assert entity.motion_state is STATE_OFF
|
|
|
|
# Simulate the event reception
|
|
await motion_binary_sensor.async_my_climate_changed()
|
|
assert motion_binary_sensor.state == STATE_OFF
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_presence_binary_sensors(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_turn_on_off_heater,
|
|
skip_send_event,
|
|
):
|
|
"""Test the presence binary sensors in thermostat type"""
|
|
|
|
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,
|
|
"eco_away_temp": 12,
|
|
"comfort_away_temp": 13,
|
|
"boost_away_temp": 14,
|
|
CONF_USE_WINDOW_FEATURE: False,
|
|
CONF_USE_MOTION_FEATURE: False,
|
|
CONF_USE_POWER_FEATURE: False,
|
|
CONF_USE_PRESENCE_FEATURE: True,
|
|
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_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
|
},
|
|
)
|
|
|
|
entity: BaseThermostat = await create_thermostat(
|
|
hass, entry, "climate.theoverswitchmockname"
|
|
)
|
|
assert entity
|
|
|
|
presence_binary_sensor: PresenceBinarySensor = search_entity(
|
|
hass, "binary_sensor.theoverswitchmockname_presence_state", "binary_sensor"
|
|
)
|
|
assert presence_binary_sensor
|
|
|
|
now: datetime = datetime.now(tz=get_tz(hass))
|
|
|
|
# Overpowering should be not set because poer have not been received
|
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
|
await send_temperature_change_event(entity, 15, now)
|
|
assert entity.presence_state is None
|
|
|
|
await presence_binary_sensor.async_my_climate_changed()
|
|
assert presence_binary_sensor.state is STATE_OFF
|
|
assert presence_binary_sensor.device_class == BinarySensorDeviceClass.PRESENCE
|
|
|
|
# Detect motion
|
|
await send_presence_change_event(entity, True, False, now)
|
|
|
|
assert entity.presence_state is STATE_ON
|
|
|
|
# Simulate the event reception
|
|
await presence_binary_sensor.async_my_climate_changed()
|
|
assert presence_binary_sensor.state == STATE_ON
|
|
|
|
# Undetect motion
|
|
await send_presence_change_event(entity, False, True, now)
|
|
|
|
assert entity.presence_state is STATE_OFF
|
|
|
|
# Simulate the event reception
|
|
await presence_binary_sensor.async_my_climate_changed()
|
|
assert presence_binary_sensor.state == STATE_OFF
|
|
|
|
|
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
async def test_binary_sensors_over_climate_minimal(
|
|
hass: HomeAssistant,
|
|
skip_hass_states_is_state,
|
|
skip_turn_on_off_heater,
|
|
skip_send_event,
|
|
):
|
|
"""Test the binary sensors with thermostat over climate type"""
|
|
|
|
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,
|
|
"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
|
|
assert entity.is_over_climate
|
|
|
|
security_binary_sensor: SecurityBinarySensor = search_entity(
|
|
hass, "binary_sensor.theoverclimatemockname_security_state", "binary_sensor"
|
|
)
|
|
assert security_binary_sensor is not None
|
|
|
|
overpowering_binary_sensor: OverpoweringBinarySensor = search_entity(
|
|
hass, "binary_sensor.theoverclimatemockname_overpowering_state", "binary_sensor"
|
|
)
|
|
assert overpowering_binary_sensor is None
|
|
|
|
window_binary_sensor: WindowBinarySensor = search_entity(
|
|
hass, "binary_sensor.theoverclimatemockname_window_state", "binary_sensor"
|
|
)
|
|
assert window_binary_sensor is None
|
|
|
|
motion_binary_sensor: MotionBinarySensor = search_entity(
|
|
hass, "binary_sensor.theoverclimatemockname_motion_state", "binary_sensor"
|
|
)
|
|
assert motion_binary_sensor is None
|
|
|
|
presence_binary_sensor: PresenceBinarySensor = search_entity(
|
|
hass, "binary_sensor.theoverclimatemockname_presence_state", "binary_sensor"
|
|
)
|
|
assert presence_binary_sensor is None
|