Add tests for over switch AC mode

This commit is contained in:
Jean-Marc Collin
2023-10-21 17:18:56 +00:00
parent c0d422a916
commit 8b88ed5c9c
9 changed files with 228 additions and 36 deletions

View File

@@ -60,6 +60,9 @@ input_boolean:
fake_heater_switch1: fake_heater_switch1:
name: Heater 1 name: Heater 1
icon: mdi:radiator icon: mdi:radiator
fake_heater_ac1:
name: Air contionner 1
icon: mdi:air-conditioner
fake_heater_4switch1: fake_heater_4switch1:
name: Heater (multiswitch1) name: Heater (multiswitch1)
icon: mdi:radiator icon: mdi:radiator
@@ -114,22 +117,22 @@ climate:
name: Underlying thermostat 4-1 name: Underlying thermostat 4-1
heater: input_boolean.fake_heater_4climate1 heater: input_boolean.fake_heater_4climate1
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
ac_mode: true ac_mode: false
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat 4-2 name: Underlying thermostat 4-2
heater: input_boolean.fake_heater_4climate2 heater: input_boolean.fake_heater_4climate2
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
ac_mode: true ac_mode: false
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat 4-3 name: Underlying thermostat 4-3
heater: input_boolean.fake_heater_4climate3 heater: input_boolean.fake_heater_4climate3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
ac_mode: true ac_mode: false
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat 4-4 name: Underlying thermostat 4-4
heater: input_boolean.fake_heater_4climate4 heater: input_boolean.fake_heater_4climate4
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
ac_mode: true ac_mode: false
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat9 name: Underlying thermostat9
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3

View File

@@ -14,7 +14,7 @@
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"python.analysis.extraPaths": [ "python.analysis.extraPaths": [
// "/home/vscode/core", // "/home/vscode/core",
"/workspaces/custom_components/versatile_thermostat" "/workspaces/versatile_thermostat/custom_components/versatile_thermostat"
], ],
"python.formatting.provider": "none" "python.formatting.provider": "none"
} }

View File

@@ -428,7 +428,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._presence_on = self._presence_sensor_entity_id is not None self._presence_on = self._presence_sensor_entity_id is not None
if self._ac_mode: if self._ac_mode:
self._hvac_list = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF] self._hvac_list = [HVACMode.COOL, HVACMode.OFF]
else: else:
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF] self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
@@ -919,6 +919,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
return self._hvac_list return self._hvac_list
@property
def ac_mode(self) -> bool:
""" Get the ac_mode of the Themostat"""
return self._ac_mode
@property @property
def fan_mode(self) -> str | None: def fan_mode(self) -> str | None:
"""Return the fan setting. """Return the fan setting.
@@ -1345,8 +1350,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if preset_mode == PRESET_POWER: if preset_mode == PRESET_POWER:
return self._power_temp return self._power_temp
else: else:
# Select _ac presets if in COOL Mode # Select _ac presets if in COOL Mode (or over_switch with _ac_mode)
if self._ac_mode and self._hvac_mode == HVACMode.COOL: if self._ac_mode and (self._hvac_mode == HVACMode.COOL or not self._is_over_climate):
preset_mode = preset_mode + PRESET_AC_SUFFIX preset_mode = preset_mode + PRESET_AC_SUFFIX
if self._presence_on is False or self._presence_state in [ if self._presence_on is False or self._presence_state in [
@@ -1974,25 +1979,25 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if self._attr_preset_mode not in [PRESET_BOOST, PRESET_COMFORT, PRESET_ECO]: if self._attr_preset_mode not in [PRESET_BOOST, PRESET_COMFORT, PRESET_ECO]:
return return
# Change temperature with preset named _way # Change temperature with preset named _away
new_temp = None # new_temp = None
if new_state == STATE_ON or new_state == STATE_HOME: #if new_state == STATE_ON or new_state == STATE_HOME:
new_temp = self._presets[self._attr_preset_mode] # new_temp = self._presets[self._attr_preset_mode]
_LOGGER.info( # _LOGGER.info(
"%s - Someone is back home. Restoring temperature to %.2f", # "%s - Someone is back home. Restoring temperature to %.2f",
self, # self,
new_temp, # new_temp,
) # )
else: #else:
new_temp = self._presets_away[ # new_temp = self._presets_away[
self.get_preset_away_name(self._attr_preset_mode) # self.get_preset_away_name(self._attr_preset_mode)
] # ]
_LOGGER.info( # _LOGGER.info(
"%s - No one is at home. Apply temperature %.2f", # "%s - No one is at home. Apply temperature %.2f",
self, # self,
new_temp, # new_temp,
) # )
new_temp = self.find_preset_temp(self.preset_mode)
if new_temp is not None: if new_temp is not None:
_LOGGER.debug( _LOGGER.debug(
"%s - presence change in temperature mode new_temp will be: %.2f", "%s - presence change in temperature mode new_temp will be: %.2f",
@@ -2503,6 +2508,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"target_temp": self.target_temperature, "target_temp": self.target_temperature,
"current_temp": self._cur_temp, "current_temp": self._cur_temp,
"ext_current_temperature": self._cur_ext_temp, "ext_current_temperature": self._cur_ext_temp,
"ac_mode": self._ac_mode,
"current_power": self._current_power, "current_power": self._current_power,
"current_power_max": self._current_power_max, "current_power_max": self._current_power_max,
"saved_preset_mode": self._saved_preset_mode, "saved_preset_mode": self._saved_preset_mode,

View File

@@ -245,6 +245,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
vol.Optional(CONF_CLIMATE_4): selector.EntitySelector( vol.Optional(CONF_CLIMATE_4): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN), selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
), ),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
} }
) )

View File

@@ -1,12 +1,12 @@
""" Underlying entities classes """ """ Underlying entities classes """
import logging import logging
from typing import Any from typing import Any
from enum import StrEnum
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
from homeassistant.exceptions import ServiceNotFound from homeassistant.exceptions import ServiceNotFound
from enum import StrEnum
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateEntity, ClimateEntity,

View File

@@ -5,7 +5,7 @@ from unittest.mock import patch, MagicMock
import pytest # pylint: disable=unused-import import pytest # pylint: disable=unused-import
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF, ATTR_TEMPERATURE
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@@ -29,14 +29,17 @@ from .const import ( # pylint: disable=unused-import
MOCK_TH_OVER_4SWITCH_USER_CONFIG, MOCK_TH_OVER_4SWITCH_USER_CONFIG,
MOCK_TH_OVER_CLIMATE_USER_CONFIG, MOCK_TH_OVER_CLIMATE_USER_CONFIG,
MOCK_TH_OVER_SWITCH_TYPE_CONFIG, MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG,
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG, MOCK_TH_OVER_4SWITCH_TYPE_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG, MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
MOCK_TH_OVER_SWITCH_TPI_CONFIG, MOCK_TH_OVER_SWITCH_TPI_CONFIG,
MOCK_PRESETS_CONFIG, MOCK_PRESETS_CONFIG,
MOCK_PRESETS_AC_CONFIG,
MOCK_WINDOW_CONFIG, MOCK_WINDOW_CONFIG,
MOCK_MOTION_CONFIG, MOCK_MOTION_CONFIG,
MOCK_POWER_CONFIG, MOCK_POWER_CONFIG,
MOCK_PRESENCE_CONFIG, MOCK_PRESENCE_CONFIG,
MOCK_PRESENCE_AC_CONFIG,
MOCK_ADVANCED_CONFIG, MOCK_ADVANCED_CONFIG,
# MOCK_DEFAULT_FEATURE_CONFIG, # MOCK_DEFAULT_FEATURE_CONFIG,
PRESET_BOOST, PRESET_BOOST,
@@ -58,6 +61,19 @@ FULL_SWITCH_CONFIG = (
| MOCK_ADVANCED_CONFIG | MOCK_ADVANCED_CONFIG
) )
FULL_SWITCH_AC_CONFIG = (
MOCK_TH_OVER_SWITCH_USER_CONFIG
| MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_AC_CONFIG
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG
| MOCK_PRESENCE_AC_CONFIG
| MOCK_ADVANCED_CONFIG
)
PARTIAL_CLIMATE_CONFIG = ( PARTIAL_CLIMATE_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
@@ -83,7 +99,7 @@ _LOGGER = logging.getLogger(__name__)
class MockClimate(ClimateEntity): class MockClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode""" """A Mock Climate class used for Underlying climate mode"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.OFF) -> None: def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.OFF) -> None: # pylint: disable=unused-argument
"""Initialize the thermostat.""" """Initialize the thermostat."""
super().__init__() super().__init__()
@@ -101,12 +117,13 @@ class MockClimate(ClimateEntity):
self._attr_target_temperature = 20 self._attr_target_temperature = 20
self._attr_current_temperature = 15 self._attr_current_temperature = 15
def set_temperature(self, temperature): def set_temperature(self, **kwargs):
""" Set the target temperature""" """ Set the target temperature"""
temperature = kwargs.get(ATTR_TEMPERATURE)
self._attr_target_temperature = temperature self._attr_target_temperature = temperature
self.async_write_ha_state() self.async_write_ha_state()
def async_set_hvac_mode(self, hvac_mode): async def async_set_hvac_mode(self, hvac_mode):
""" The hvac mode""" """ The hvac mode"""
self._attr_hvac_mode = hvac_mode self._attr_hvac_mode = hvac_mode
self.async_write_ha_state() self.async_write_ha_state()
@@ -114,7 +131,7 @@ class MockClimate(ClimateEntity):
class MockUnavailableClimate(ClimateEntity): class MockUnavailableClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode""" """A Mock Climate class used for Underlying climate mode"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # pylint: disable=unused-argument
"""Initialize the thermostat.""" """Initialize the thermostat."""
super().__init__() super().__init__()

View File

@@ -51,7 +51,6 @@ from custom_components.versatile_thermostat.const import (
PRESET_AWAY_SUFFIX, PRESET_AWAY_SUFFIX,
CONF_CLIMATE, CONF_CLIMATE,
) )
MOCK_TH_OVER_SWITCH_USER_CONFIG = { MOCK_TH_OVER_SWITCH_USER_CONFIG = {
CONF_NAME: "TheOverSwitchMockName", CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
@@ -100,6 +99,12 @@ MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
CONF_AC_MODE: False, CONF_AC_MODE: False,
} }
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_air_conditioner",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: True,
}
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = { MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_4switch0", CONF_HEATER: "switch.mock_4switch0",
CONF_HEATER_2: "switch.mock_4switch1", CONF_HEATER_2: "switch.mock_4switch1",
@@ -116,6 +121,7 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = { MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
CONF_CLIMATE: "climate.mock_climate", CONF_CLIMATE: "climate.mock_climate",
CONF_AC_MODE: False,
} }
MOCK_PRESETS_CONFIG = { MOCK_PRESETS_CONFIG = {
@@ -124,6 +130,15 @@ MOCK_PRESETS_CONFIG = {
PRESET_BOOST + "_temp": 18, PRESET_BOOST + "_temp": 18,
} }
MOCK_PRESETS_AC_CONFIG = {
PRESET_ECO + "_temp": 17,
PRESET_COMFORT + "_temp": 19,
PRESET_BOOST + "_temp": 20,
PRESET_ECO + "_ac_temp": 25,
PRESET_COMFORT + "_ac_temp": 23,
PRESET_BOOST + "_ac_temp": 21,
}
MOCK_WINDOW_CONFIG = { MOCK_WINDOW_CONFIG = {
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor", CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
CONF_WINDOW_DELAY: 10, CONF_WINDOW_DELAY: 10,
@@ -156,6 +171,16 @@ MOCK_PRESENCE_CONFIG = {
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18, PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
} }
MOCK_PRESENCE_AC_CONFIG = {
CONF_PRESENCE_SENSOR: "person.presence_sensor",
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 16,
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17,
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18,
PRESET_ECO + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 27,
PRESET_COMFORT + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 26,
PRESET_BOOST + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 25,
}
MOCK_ADVANCED_CONFIG = { MOCK_ADVANCED_CONFIG = {
CONF_MINIMAL_ACTIVATION_DELAY: 10, CONF_MINIMAL_ACTIVATION_DELAY: 10,
CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_DELAY_MIN: 5,

View File

@@ -1,10 +1,10 @@
""" Test the Window management """ """ Test the Window management """
import asyncio import asyncio
from datetime import datetime, timedelta
import logging
from unittest.mock import patch, call, PropertyMock from unittest.mock import patch, call, PropertyMock
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from datetime import datetime, timedelta
import logging
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)

140
tests/test_switch_ac.py Normal file
View File

@@ -0,0 +1,140 @@
""" Test the normal start of a Switch AC Thermostat """
from unittest.mock import patch, call
from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.config_entries import ConfigEntryState
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.climate import VersatileThermostat
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_switch_ac_full_start(hass: HomeAssistant, skip_hass_states_is_state): # pylint: disable=unused-argument
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchACMockName",
unique_id="uniqueId",
data=FULL_SWITCH_AC_CONFIG,
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event:
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
# The name is in the CONF and not the title of the entry
entity: VersatileThermostat = find_my_entity("climate.theoverswitchmockname")
assert entity
assert entity.name == "TheOverSwitchMockName"
assert entity._is_over_climate is False # pylint: disable=protected-access
assert entity.ac_mode is True
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_modes == [HVACMode.COOL, HVACMode.OFF]
assert entity.target_temperature == entity.max_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
PRESET_ACTIVITY,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False # pylint: disable=protected-access
assert entity._window_state is None # pylint: disable=protected-access
assert entity._motion_state is None # pylint: disable=protected-access
assert entity._presence_state is None # pylint: disable=protected-access
assert entity._prop_algorithm is not None # pylint: disable=protected-access
# 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},
),
]
)
# Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.COOL)
assert entity.hvac_mode is HVACMode.COOL
event_timestamp = now - timedelta(minutes=4)
await send_presence_change_event(entity, True, False, event_timestamp)
assert entity._presence_state == STATE_ON # pylint: disable=protected-access
await entity.async_set_hvac_mode(HVACMode.COOL)
assert entity.hvac_mode is HVACMode.COOL
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode is PRESET_COMFORT
assert entity.target_temperature == 23
# switch to Eco
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO
assert entity.target_temperature == 25
# Unset the presence
event_timestamp = now - timedelta(minutes=3)
await send_presence_change_event(entity, False, True, event_timestamp)
assert entity._presence_state == STATE_OFF # pylint: disable=protected-access
assert entity.target_temperature == 27 # eco_ac_away
# Open a window
with patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=2)
try_condition = await send_window_change_event(entity, True, False, event_timestamp)
# Confirme the window event
await try_condition(None)
assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_action is HVACAction.OFF
assert entity.target_temperature == 27 # eco_ac_away
# Close a window
with patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=2)
try_condition = await send_window_change_event(entity, False, True, event_timestamp)
# Confirme the window event
await try_condition(None)
assert entity.hvac_mode is HVACMode.COOL
assert (entity.hvac_action is HVACAction.OFF or entity.hvac_action is HVACAction.IDLE)
assert entity.target_temperature == 27 # eco_ac_away