From a17aba45faef35f68ffdb2793a88ab086acd3a29 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Wed, 3 Jan 2024 09:57:18 +0000 Subject: [PATCH] Normal algo working and testu ok --- .../versatile_thermostat/base_thermostat.py | 107 ++- .../versatile_thermostat/config_schema.py | 1 + .../versatile_thermostat/const.py | 23 +- .../versatile_thermostat/select.py | 134 ++++ .../versatile_thermostat/strings.json | 4 + .../versatile_thermostat/translations/en.json | 4 + .../versatile_thermostat/translations/fr.json | 4 + .../versatile_thermostat/underlyings.py | 1 + .../versatile_thermostat/vtherm_api.py | 3 - pyrightconfig.json | 7 + tests/commons.py | 7 +- tests/const.py | 3 +- tests/test_central_mode.py | 734 ++++++++++++++++++ tests/test_config_flow.py | 2 + tests/test_switch_ac.py | 2 +- tests/test_valve.py | 12 + 16 files changed, 1035 insertions(+), 13 deletions(-) create mode 100644 custom_components/versatile_thermostat/select.py create mode 100644 pyrightconfig.json create mode 100644 tests/test_central_mode.py diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 23f39cc..ebbef5d 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -116,6 +116,11 @@ from .const import ( ATTR_TOTAL_ENERGY, PRESET_AC_SUFFIX, DEFAULT_SHORT_EMA_PARAMS, + CENTRAL_MODE_AUTO, + CENTRAL_MODE_STOPPED, + CENTRAL_MODE_HEAT_ONLY, + CENTRAL_MODE_COOL_ONLY, + CENTRAL_MODE_FROST_PROTECTION, ) from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import @@ -158,6 +163,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity): frozenset( { "is_on", + "is_controlled_by_central_mode", + "last_central_mode", "type", "frost_temp", "eco_temp", @@ -273,6 +280,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self._now = None self._attr_fan_mode = None + + self._is_central_mode = None + self._last_central_mode = None self.post_init(entry_infos) def clean_central_config_doublon(self, config_entry, central_config) -> dict: @@ -434,6 +444,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self._presence_on = self._presence_sensor_entity_id is not None if self._ac_mode: + # Added by https://github.com/jmcollin78/versatile_thermostat/pull/144 + # Some over_switch can do both heating and cooling self._hvac_list = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF] else: self._hvac_list = [HVACMode.HEAT, HVACMode.OFF] @@ -552,6 +564,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity): short_ema_params.get("max_alpha"), ) + self._is_central_mode = not ( + entry_infos.get(CONF_USE_CENTRAL_MODE) is False + ) # Default value (None) is True + _LOGGER.debug( "%s - Creation of a new VersatileThermostat entity: unique_id=%s", self, @@ -1130,6 +1146,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity): """True if the VTherm is on (! HVAC_OFF)""" return self.hvac_mode and self.hvac_mode != HVACMode.OFF + @property + def is_controlled_by_central_mode(self) -> bool: + """Returns True if this VTherm can be controlled by the central_mode""" + return self._is_central_mode + + @property + def last_central_mode(self) -> str | None: + """Returns the last central_mode taken into account. + Is None if the VTherm is not controlled by central_mode""" + return self._last_central_mode + def underlying_entity_id(self, index=0) -> str | None: """The climate_entity_id. Added for retrocompatibility reason""" if index < self.nb_underlying_entities: @@ -1177,11 +1204,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity): ) # If AC is on maybe we have to change the temperature in force mode, but not in frost mode (there is no Frost protection possible in AC mode) - if self._ac_mode: + if self._hvac_mode == HVACMode.COOL: if self.preset_mode != PRESET_FROST_PROTECTION: await self._async_set_preset_mode_internal(self._attr_preset_mode, True) else: - await self._async_set_preset_mode_internal(PRESET_ECO, True) + await self._async_set_preset_mode_internal(PRESET_ECO, True, False) if need_control_heating and sub_need_control_heating: await self.async_control_heating(force=True) @@ -1195,12 +1222,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self.async_write_ha_state() self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode}) - async def async_set_preset_mode(self, preset_mode): + @overrides + async def async_set_preset_mode(self, preset_mode, overwrite_saved_preset=True): """Set new preset mode.""" - await self._async_set_preset_mode_internal(preset_mode) + await self._async_set_preset_mode_internal( + preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset + ) await self.async_control_heating(force=True) - async def _async_set_preset_mode_internal(self, preset_mode, force=False): + async def _async_set_preset_mode_internal( + self, preset_mode, force=False, overwrite_saved_preset=True + ): """Set new preset mode.""" _LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force) if ( @@ -1242,7 +1274,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self.reset_last_temperature_time(old_preset_mode) - self.save_preset_mode() + if overwrite_saved_preset: + self.save_preset_mode() self.recalculate() self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode}) @@ -1998,6 +2031,66 @@ class BaseThermostat(ClimateEntity, RestoreEntity): return self._overpowering_state + async def check_central_mode(self, new_central_mode, old_central_mode) -> None: + """Take into account a central mode change""" + if not self.is_controlled_by_central_mode: + self._last_central_mode = None + return + + _LOGGER.info( + "%s - Central mode have change from %s to %s", + self, + old_central_mode, + new_central_mode, + ) + + self._last_central_mode = new_central_mode + + def save_all(): + """save preset and hvac_mode""" + self.save_preset_mode() + self.save_hvac_mode() + + if new_central_mode == CENTRAL_MODE_AUTO: + await self.restore_hvac_mode() + await self.restore_preset_mode() + + return + + if old_central_mode == CENTRAL_MODE_AUTO: + save_all() + + if new_central_mode == CENTRAL_MODE_STOPPED: + await self.async_set_hvac_mode(HVACMode.OFF) + return + + if new_central_mode == CENTRAL_MODE_COOL_ONLY: + if HVACMode.COOL in self.hvac_modes: + await self.async_set_hvac_mode(HVACMode.COOL) + else: + await self.async_set_hvac_mode(HVACMode.OFF) + return + + if new_central_mode == CENTRAL_MODE_HEAT_ONLY: + if HVACMode.HEAT in self.hvac_modes: + await self.async_set_hvac_mode(HVACMode.HEAT) + else: + await self.async_set_hvac_mode(HVACMode.OFF) + return + + if new_central_mode == CENTRAL_MODE_FROST_PROTECTION: + if ( + PRESET_FROST_PROTECTION in self.preset_modes + and HVACMode.HEAT in self.hvac_modes + ): + await self.async_set_hvac_mode(HVACMode.HEAT) + await self.async_set_preset_mode( + PRESET_FROST_PROTECTION, overwrite_saved_preset=False + ) + else: + await self.async_set_hvac_mode(HVACMode.OFF) + return + def _set_now(self, now: datetime): """Set the now timestamp. This is only for tests purpose""" self._now = now @@ -2239,6 +2332,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity): "hvac_mode": self.hvac_mode, "preset_mode": self.preset_mode, "type": self._thermostat_type, + "is_controlled_by_central_mode": self.is_controlled_by_central_mode, + "last_central_mode": self.last_central_mode, "frost_temp": self._presets[PRESET_FROST_PROTECTION], "eco_temp": self._presets[PRESET_ECO], "boost_temp": self._presets[PRESET_BOOST], diff --git a/custom_components/versatile_thermostat/config_schema.py b/custom_components/versatile_thermostat/config_schema.py index 4450ee3..5971e29 100644 --- a/custom_components/versatile_thermostat/config_schema.py +++ b/custom_components/versatile_thermostat/config_schema.py @@ -42,6 +42,7 @@ STEP_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name ), vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int, vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float), + vol.Optional(CONF_USE_CENTRAL_MODE, default=True): cv.boolean, vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean, vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean, vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean, diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index e5236fb..794d70d 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -35,7 +35,12 @@ HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY] DOMAIN = "versatile_thermostat" -PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.CLIMATE, + Platform.BINARY_SENSOR, + Platform.SENSOR, + Platform.SELECT, +] CONF_HEATER = "heater_entity_id" CONF_HEATER_2 = "heater_entity2_id" @@ -113,6 +118,8 @@ CONF_USE_PRESENCE_CENTRAL_CONFIG = "use_presence_central_config" CONF_USE_PRESETS_CENTRAL_CONFIG = "use_presets_central_config" CONF_USE_ADVANCED_CENTRAL_CONFIG = "use_advanced_central_config" +CONF_USE_CENTRAL_MODE = "use_central_mode" + DEFAULT_SHORT_EMA_PARAMS = { "max_alpha": 0.5, # In sec @@ -242,6 +249,7 @@ ALL_CONF = ( CONF_USE_POWER_CENTRAL_CONFIG, CONF_USE_PRESENCE_CENTRAL_CONFIG, CONF_USE_ADVANCED_CENTRAL_CONFIG, + CONF_USE_CENTRAL_MODE, ] + CONF_PRESETS_VALUES + CONF_PRESETS_AWAY_VALUES @@ -297,6 +305,19 @@ AUTO_FAN_DEACTIVATED_MODES = ["mute", "auto", "low"] CENTRAL_CONFIG_NAME = "Central configuration" +CENTRAL_MODE_AUTO = "Auto" +CENTRAL_MODE_STOPPED = "Stopped" +CENTRAL_MODE_HEAT_ONLY = "Heat only" +CENTRAL_MODE_COOL_ONLY = "Cool only" +CENTRAL_MODE_FROST_PROTECTION = "Frost protection" +CENTRAL_MODES = [ + CENTRAL_MODE_AUTO, + CENTRAL_MODE_STOPPED, + CENTRAL_MODE_HEAT_ONLY, + CENTRAL_MODE_COOL_ONLY, + CENTRAL_MODE_FROST_PROTECTION, +] + # A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154 class RegulationParamSlow: diff --git a/custom_components/versatile_thermostat/select.py b/custom_components/versatile_thermostat/select.py new file mode 100644 index 0000000..299f84e --- /dev/null +++ b/custom_components/versatile_thermostat/select.py @@ -0,0 +1,134 @@ +# pylint: disable=unused-argument + +""" Implements the VersatileThermostat select component """ +import logging + +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import HomeAssistant, CoreState, callback + +from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.select import SelectEntity +from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_component import EntityComponent + + +from custom_components.versatile_thermostat.base_thermostat import BaseThermostat +from .const import ( + DOMAIN, + DEVICE_MANUFACTURER, + CONF_NAME, + CONF_THERMOSTAT_TYPE, + CONF_THERMOSTAT_CENTRAL_CONFIG, + CENTRAL_MODE_AUTO, + CENTRAL_MODES, + overrides, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the VersatileThermostat selects with config flow.""" + _LOGGER.debug( + "Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data + ) + + unique_id = entry.entry_id + name = entry.data.get(CONF_NAME) + vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) + + if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG: + return + + entities = [ + CentralModeSelect(hass, unique_id, name, entry.data), + ] + + async_add_entities(entities, True) + + +class CentralModeSelect(SelectEntity, RestoreEntity): + """Representation of a Energy sensor which exposes the energy""" + + def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: + """Initialize the energy sensor""" + self._config_id = unique_id + self._device_name = entry_infos.get(CONF_NAME) + self._attr_name = "Central Mode" + self._attr_unique_id = "central_mode" + self._attr_options = CENTRAL_MODES + self._attr_current_option = CENTRAL_MODE_AUTO + + @property + def icon(self) -> str | None: + return "mdi:form-select" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, self._config_id)}, + name=self._device_name, + manufacturer=DEVICE_MANUFACTURER, + model=DOMAIN, + ) + + @overrides + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + _LOGGER.debug( + "%s - Calling async_added_to_hass old_state is %s", self, old_state + ) + if old_state is not None: + self.async_select_option = old_state.state + + @callback + async def _async_startup_internal(*_): + _LOGGER.debug("%s - Calling async_startup_internal", self) + await self.notify_central_mode_change() + + if self.hass.state == CoreState.running: + await _async_startup_internal() + else: + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, _async_startup_internal + ) + + @overrides + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + old_option = self._attr_current_option + + if option == old_option: + return + + if option in CENTRAL_MODES: + self._attr_current_option = option + await self.notify_central_mode_change(old_central_mode=old_option) + + async def notify_central_mode_change(self, old_central_mode=None): + """Notify all VTherm that the central_mode have change""" + # Update all VTherm states + component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN] + for entity in component.entities: + if isinstance(entity, BaseThermostat): + _LOGGER.debug( + "Changing the central_mode. We have find %s to update", + entity.name, + ) + await entity.check_central_mode( + self._attr_current_option, old_central_mode + ) + + def __str__(self): + return f"VersatileThermostat-{self.name}" diff --git a/custom_components/versatile_thermostat/strings.json b/custom_components/versatile_thermostat/strings.json index 0a7188d..4a000a5 100644 --- a/custom_components/versatile_thermostat/strings.json +++ b/custom_components/versatile_thermostat/strings.json @@ -24,6 +24,7 @@ "temp_min": "Minimal temperature allowed", "temp_max": "Maximal temperature allowed", "device_power": "Device power", + "use_central_mode": "Enable the control by central mode ('central_mode')", "use_window_feature": "Use window detection", "use_motion_feature": "Use motion detection", "use_power_feature": "Use power management", @@ -31,6 +32,7 @@ "use_main_central_config": "Use central main configuration" }, "data_description": { + "use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities", "use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" } @@ -254,6 +256,7 @@ "temp_min": "Minimal temperature allowed", "temp_max": "Maximal temperature allowed", "device_power": "Device power", + "use_central_mode": "Enable the control by central mode ('central_mode')", "use_window_feature": "Use window detection", "use_motion_feature": "Use motion detection", "use_power_feature": "Use power management", @@ -261,6 +264,7 @@ "use_main_central_config": "Use central main configuration" }, "data_description": { + "use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities", "use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" } diff --git a/custom_components/versatile_thermostat/translations/en.json b/custom_components/versatile_thermostat/translations/en.json index 0a7188d..4a000a5 100644 --- a/custom_components/versatile_thermostat/translations/en.json +++ b/custom_components/versatile_thermostat/translations/en.json @@ -24,6 +24,7 @@ "temp_min": "Minimal temperature allowed", "temp_max": "Maximal temperature allowed", "device_power": "Device power", + "use_central_mode": "Enable the control by central mode ('central_mode')", "use_window_feature": "Use window detection", "use_motion_feature": "Use motion detection", "use_power_feature": "Use power management", @@ -31,6 +32,7 @@ "use_main_central_config": "Use central main configuration" }, "data_description": { + "use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities", "use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" } @@ -254,6 +256,7 @@ "temp_min": "Minimal temperature allowed", "temp_max": "Maximal temperature allowed", "device_power": "Device power", + "use_central_mode": "Enable the control by central mode ('central_mode')", "use_window_feature": "Use window detection", "use_motion_feature": "Use motion detection", "use_power_feature": "Use power management", @@ -261,6 +264,7 @@ "use_main_central_config": "Use central main configuration" }, "data_description": { + "use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities", "use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm", "external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected" } diff --git a/custom_components/versatile_thermostat/translations/fr.json b/custom_components/versatile_thermostat/translations/fr.json index 15ce526..09b4125 100644 --- a/custom_components/versatile_thermostat/translations/fr.json +++ b/custom_components/versatile_thermostat/translations/fr.json @@ -24,6 +24,7 @@ "temp_min": "Température minimale permise", "temp_max": "Température maximale permise", "device_power": "Puissance de l'équipement", + "use_central_mode": "Autoriser le controle par le mode central ('central_mode`)", "use_window_feature": "Avec détection des ouvertures", "use_motion_feature": "Avec détection de mouvement", "use_power_feature": "Avec gestion de la puissance", @@ -31,6 +32,7 @@ "use_main_central_config": "Utiliser la configuration centrale principale" }, "data_description": { + "use_central_mode": "Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale", "external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée", "use_main_central_config": "Cochez pour utiliser la configuration centrale principale. Décochez et saisissez les attributs pour utiliser une configuration spécifique principale" } @@ -254,6 +256,7 @@ "temp_min": "Température minimale permise", "temp_max": "Température maximale permise", "device_power": "Puissance de l'équipement", + "use_central_mode": "Autoriser le controle par le mode central ('central_mode`)", "use_window_feature": "Avec détection des ouvertures", "use_motion_feature": "Avec détection de mouvement", "use_power_feature": "Avec gestion de la puissance", @@ -261,6 +264,7 @@ "use_main_central_config": "Utiliser la configuration centrale" }, "data_description": { + "use_central_mode": "Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale", "use_main_central_config": "Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique", "external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée" } diff --git a/custom_components/versatile_thermostat/underlyings.py b/custom_components/versatile_thermostat/underlyings.py index ec2bb92..1f99805 100644 --- a/custom_components/versatile_thermostat/underlyings.py +++ b/custom_components/versatile_thermostat/underlyings.py @@ -790,6 +790,7 @@ class UnderlyingValve(UnderlyingEntity): ): """We use this function to change the on_percent""" if force: + self._percent_open = self.cap_sent_value(self._percent_open) await self.send_percent_open() @overrides diff --git a/custom_components/versatile_thermostat/vtherm_api.py b/custom_components/versatile_thermostat/vtherm_api.py index 9d93935..964e790 100644 --- a/custom_components/versatile_thermostat/vtherm_api.py +++ b/custom_components/versatile_thermostat/vtherm_api.py @@ -20,7 +20,6 @@ class VersatileThermostatAPI(dict): """The VersatileThermostatAPI""" _hass: HomeAssistant = None - # _entries: Dict(str, ConfigEntry) @classmethod def get_vtherm_api(cls, hass=None): @@ -64,14 +63,12 @@ class VersatileThermostatAPI(dict): def add_entry(self, entry: ConfigEntry): """Add a new entry""" _LOGGER.debug("Add the entry %s", entry.entry_id) - # self._entries[entry.entry_id] = entry # Add the entry in hass.data VersatileThermostatAPI._hass.data[DOMAIN][entry.entry_id] = entry def remove_entry(self, entry: ConfigEntry): """Remove an entry""" _LOGGER.debug("Remove the entry %s", entry.entry_id) - # self._entries.pop(entry.entry_id) VersatileThermostatAPI._hass.data[DOMAIN].pop(entry.entry_id) # If not more entries are preset, remove the API if len(self) == 0: diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..e9662b8 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,7 @@ +{ + "include": [ + "custom_components/versatile_thermostat/**", + "homeassistant/**" + ], + "reportShadowedImports": false +} \ No newline at end of file diff --git a/tests/commons.py b/tests/commons.py index c7ef56f..6a30a56 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -185,6 +185,7 @@ class MockClimate(ClimateEntity): hvac_mode: HVACMode = HVACMode.OFF, hvac_action: HVACAction = HVACAction.OFF, fan_modes: list[str] = None, + hvac_modes: list[str] = None, ) -> None: """Initialize the thermostat.""" @@ -200,7 +201,11 @@ class MockClimate(ClimateEntity): HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING ) self._attr_hvac_mode = hvac_mode - self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT] + self._attr_hvac_modes = ( + hvac_modes + if hvac_modes is not None + else [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT] + ) self._attr_temperature_unit = UnitOfTemperature.CELSIUS self._attr_target_temperature = 20 self._attr_current_temperature = 15 diff --git a/tests/const.py b/tests/const.py index 3552a5b..bb19197 100644 --- a/tests/const.py +++ b/tests/const.py @@ -50,7 +50,8 @@ MOCK_TH_OVER_CLIMATE_MAIN_CONFIG = { CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", CONF_CYCLE_MIN: 5, CONF_DEVICE_POWER: 1, - CONF_USE_MAIN_CENTRAL_CONFIG: False + CONF_USE_MAIN_CENTRAL_CONFIG: False, + CONF_USE_CENTRAL_MODE: True # Keep default values which are False } diff --git a/tests/test_central_mode.py b/tests/test_central_mode.py new file mode 100644 index 0000000..bab5e8d --- /dev/null +++ b/tests/test_central_mode.py @@ -0,0 +1,734 @@ +# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long + +""" Test the central_configuration """ +from unittest.mock import patch # , call + +# from datetime import datetime # , timedelta + +from homeassistant.core import HomeAssistant + +from homeassistant.components.climate import HVACMode + +# from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN + +from pytest_homeassistant_custom_component.common import MockConfigEntry + +from custom_components.versatile_thermostat.thermostat_switch import ( + ThermostatOverSwitch, +) + +from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import +from .const 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_config_with_central_mode_true( + hass: HomeAssistant, skip_hass_states_is_state +): + """A config with central_mode True""" + + # Add a Switch VTherm + 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_USE_CENTRAL_MODE: True, + CONF_CYCLE_MIN: 5, + CONF_TEMP_MIN: 8, + CONF_TEMP_MAX: 18, + "frost_temp": 10, + "eco_temp": 17, + "comfort_temp": 18, + "boost_temp": 21, + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_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_SECURITY_DEFAULT_ON_PERCENT: 0.1, + }, + ) + + with patch("homeassistant.core.ServiceRegistry.async_call"): + entity: ThermostatOverSwitch = await create_thermostat( + hass, entry, "climate.theoverswitchmockname" + ) + assert entity + assert entity.name == "TheOverSwitchMockName" + assert entity.is_over_switch + assert entity.is_controlled_by_central_mode + assert entity.last_central_mode is None # cause no central config exists + + +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_config_with_central_mode_false( + hass: HomeAssistant, skip_hass_states_is_state +): + """A config with central_mode False""" + + # Add a Climate VTherm + 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_USE_CENTRAL_MODE: False, + CONF_CYCLE_MIN: 5, + CONF_TEMP_MIN: 8, + CONF_TEMP_MAX: 18, + "frost_temp": 10, + "eco_temp": 17, + "comfort_temp": 18, + "boost_temp": 21, + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_CLIMATE: "climate.mock_climate", + CONF_MINIMAL_ACTIVATION_DELAY: 30, + CONF_SECURITY_DELAY_MIN: 5, + CONF_SECURITY_MIN_ON_PERCENT: 0.3, + CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, + }, + ) + + with patch("homeassistant.core.ServiceRegistry.async_call"): + entity: ThermostatOverSwitch = await create_thermostat( + hass, entry, "climate.theoverclimatemockname" + ) + assert entity + assert entity.name == "TheOverClimateMockName" + assert entity.is_over_climate + assert entity.is_controlled_by_central_mode is False + assert entity.last_central_mode is None # cause no central config exists + + +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_config_with_central_mode_none( + hass: HomeAssistant, skip_hass_states_is_state +): + """A config with central_mode is None""" + + # Add a Switch VTherm + entry = MockConfigEntry( + domain=DOMAIN, + title="TheOverValveMockName", + unique_id="uniqueId", + data={ + CONF_NAME: "TheOverValveMockName", + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE, + CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", + CONF_USE_CENTRAL_MODE: True, + CONF_CYCLE_MIN: 5, + CONF_TEMP_MIN: 8, + CONF_TEMP_MAX: 18, + "frost_temp": 10, + "eco_temp": 17, + "comfort_temp": 18, + "boost_temp": 21, + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_VALVE: "number.mock_valve", + 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_SECURITY_DEFAULT_ON_PERCENT: 0.1, + }, + ) + + with patch("homeassistant.core.ServiceRegistry.async_call"): + entity: ThermostatOverSwitch = await create_thermostat( + hass, entry, "climate.theovervalvemockname" + ) + assert entity + assert entity.name == "TheOverValveMockName" + assert entity.is_over_valve + assert entity.is_controlled_by_central_mode + assert entity.last_central_mode is None # cause no central config exists + + +async def test_switch_change_central_mode_true( + hass: HomeAssistant, skip_hass_states_is_state, init_central_config +): + """test that changes with over_switch config with central_mode True are + taken into account""" + + # Add a Switch VTherm + 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_USE_CENTRAL_MODE: True, + CONF_CYCLE_MIN: 5, + CONF_TEMP_MIN: 8, + CONF_TEMP_MAX: 18, + "frost_temp": 10, + "eco_temp": 17, + "comfort_temp": 18, + "boost_temp": 21, + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_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_SECURITY_DEFAULT_ON_PERCENT: 0.1, + }, + ) + + # 1 initialize entity and find select entity + with patch("homeassistant.core.ServiceRegistry.async_call"): + entity: ThermostatOverSwitch = await create_thermostat( + hass, entry, "climate.theoverswitchmockname" + ) + assert entity + assert entity.is_controlled_by_central_mode + assert entity.last_central_mode is None + + # Find the select entity + select_entity = search_entity(hass, "select.central_mode", SELECT_DOMAIN) + + assert select_entity + assert select_entity.current_option == CENTRAL_MODE_AUTO + assert select_entity.options == CENTRAL_MODES + + # start entity + await entity.async_set_hvac_mode(HVACMode.HEAT) + await entity.async_set_preset_mode(PRESET_BOOST) + + assert entity.hvac_mode == HVACMode.HEAT + assert entity.preset_mode == PRESET_BOOST + + # 2 change central_mode to STOPPED + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_STOPPED) + + assert entity.last_central_mode is CENTRAL_MODE_STOPPED + assert entity.hvac_mode == HVACMode.OFF + assert entity.preset_mode == PRESET_BOOST + + # 3 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await entity.async_set_preset_mode(PRESET_COMFORT) + assert entity.preset_mode == PRESET_COMFORT + + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # hvac_mode should be restored as before the STOP and preset should be restored with the last choosen preset (COMFORT here) + assert entity.last_central_mode is CENTRAL_MODE_AUTO + assert entity.hvac_mode == HVACMode.HEAT + assert entity.preset_mode == PRESET_COMFORT + + # 4 change central_mode to COOL_ONLY + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_COOL_ONLY) + + # hvac_mode should be set to OFF because there is no COOL mode for this VTherm + assert entity.last_central_mode is CENTRAL_MODE_COOL_ONLY + assert entity.hvac_mode == HVACMode.OFF + assert entity.preset_mode == PRESET_COMFORT + + # 5 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # hvac_mode should be restored to HEAT + assert entity.last_central_mode is CENTRAL_MODE_AUTO + assert entity.hvac_mode == HVACMode.HEAT + assert entity.preset_mode == PRESET_COMFORT + + # 6 change central_mode to HEAT_ONLY + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_HEAT_ONLY) + + # hvac_mode should stay in HEAT mode + assert entity.last_central_mode is CENTRAL_MODE_HEAT_ONLY + assert entity.hvac_mode == HVACMode.HEAT + # No change + assert entity.preset_mode == PRESET_COMFORT + + # 7 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # hvac_mode should be restored to HEAT + assert entity.last_central_mode is CENTRAL_MODE_AUTO + assert entity.hvac_mode == HVACMode.HEAT + assert entity.preset_mode == PRESET_COMFORT + + # 8 change central_mode to FROST_PROTECTION + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_FROST_PROTECTION) + + # hvac_mode should stay in HEAT mode + assert entity.last_central_mode is CENTRAL_MODE_FROST_PROTECTION + assert entity.hvac_mode == HVACMode.HEAT + # change to Frost + assert entity.preset_mode == PRESET_FROST_PROTECTION + + # 9 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # hvac_mode should be restored to HEAT + assert entity.last_central_mode is CENTRAL_MODE_AUTO + assert entity.hvac_mode == HVACMode.HEAT + # preset restored to COMFORT + assert entity.preset_mode == PRESET_COMFORT + + +async def test_switch_ac_change_central_mode_true( + hass: HomeAssistant, skip_hass_states_is_state, init_central_config +): + """test that changes with over_switch config with central_mode True are + taken into account""" + + # Add a Switch VTherm + 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_USE_CENTRAL_MODE: True, + CONF_CYCLE_MIN: 5, + CONF_TEMP_MIN: 8, + CONF_TEMP_MAX: 18, + "frost_temp": 10, + "eco_temp": 17, + "comfort_temp": 18, + "boost_temp": 21, + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_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_SECURITY_DEFAULT_ON_PERCENT: 0.1, + CONF_AC_MODE: True, + }, + ) + + # 1 initialize entity and find select entity + with patch("homeassistant.core.ServiceRegistry.async_call"): + entity: ThermostatOverSwitch = await create_thermostat( + hass, entry, "climate.theoverswitchmockname" + ) + assert entity + assert entity.is_controlled_by_central_mode + assert entity.ac_mode is True + assert entity.hvac_modes == [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF] + + # Find the select entity + select_entity = search_entity(hass, "select.central_mode", SELECT_DOMAIN) + + assert select_entity + assert select_entity.current_option == CENTRAL_MODE_AUTO + assert select_entity.options == CENTRAL_MODES + + # start entity in cooling mode + await entity.async_set_hvac_mode(HVACMode.COOL) + await entity.async_set_preset_mode(PRESET_BOOST) + + assert entity.hvac_mode == HVACMode.COOL + assert entity.preset_mode == PRESET_BOOST + + # 2 change central_mode to STOPPED + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_STOPPED) + + assert entity.hvac_mode == HVACMode.OFF + assert entity.preset_mode == PRESET_BOOST + + # 3 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await entity.async_set_preset_mode(PRESET_COMFORT) + assert entity.preset_mode == PRESET_COMFORT + + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # hvac_mode should be restored as before the STOP and preset should be restored with the last choosen preset (COMFORT here) + assert entity.hvac_mode == HVACMode.COOL + assert entity.preset_mode == PRESET_COMFORT + + # 4 change central_mode to COOL_ONLY + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_COOL_ONLY) + + # hvac_mode should be set to OFF because there is no COOL mode for this VTherm + assert entity.hvac_mode == HVACMode.COOL + assert entity.preset_mode == PRESET_COMFORT + + # 5 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # hvac_mode should be restored to HEAT + assert entity.hvac_mode == HVACMode.COOL + assert entity.preset_mode == PRESET_COMFORT + + # 6 change central_mode to HEAT_ONLY + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_HEAT_ONLY) + + # hvac_mode should stay in HEAT mode + assert entity.hvac_mode == HVACMode.HEAT + # No change + assert entity.preset_mode == PRESET_COMFORT + + # 7 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # hvac_mode should be restored to COOL + assert entity.hvac_mode == HVACMode.COOL + assert entity.preset_mode == PRESET_COMFORT + + # 8 change central_mode to FROST_PROTECTION + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_FROST_PROTECTION) + + # hvac_mode should stay in COOL mode + assert entity.hvac_mode == HVACMode.HEAT + # change to Frost + assert entity.preset_mode == PRESET_FROST_PROTECTION + + # 9 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # hvac_mode should be restored to COOL + assert entity.hvac_mode == HVACMode.COOL + # preset restored to COMFORT + assert entity.preset_mode == PRESET_COMFORT + + +async def test_climate_ac_change_central_mode_false( + hass: HomeAssistant, skip_hass_states_is_state, init_central_config +): + """test that changes with over_climate config with central_mode False are + not taken into account""" + + fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}) + + # Add a Climate VTherm + 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_USE_CENTRAL_MODE: False, + CONF_CYCLE_MIN: 5, + CONF_TEMP_MIN: 8, + CONF_TEMP_MAX: 18, + "frost_temp": 10, + "eco_temp": 17, + "comfort_temp": 18, + "boost_temp": 21, + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_CLIMATE: "climate.mock_climate", + CONF_MINIMAL_ACTIVATION_DELAY: 30, + CONF_SECURITY_DELAY_MIN: 5, + CONF_SECURITY_MIN_ON_PERCENT: 0.3, + CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, + }, + ) + + with patch("homeassistant.core.ServiceRegistry.async_call"), patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", + return_value=fake_underlying_climate, + ): + entity: ThermostatOverSwitch = await create_thermostat( + hass, entry, "climate.theoverclimatemockname" + ) + assert entity + assert entity.name == "TheOverClimateMockName" + assert entity.is_over_climate + assert entity.is_controlled_by_central_mode is False + assert entity.hvac_modes == [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT] + + # Find the select entity + select_entity = search_entity(hass, "select.central_mode", SELECT_DOMAIN) + + assert select_entity + assert select_entity.current_option == CENTRAL_MODE_AUTO + assert select_entity.options == CENTRAL_MODES + + # start entity in Heating mode + await entity.async_set_hvac_mode(HVACMode.HEAT) + await entity.async_set_preset_mode(PRESET_BOOST) + + assert entity.hvac_mode == HVACMode.HEAT + assert entity.preset_mode == PRESET_BOOST + + # 2 change central_mode to STOPPED + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_STOPPED) + + # No change + assert entity.hvac_mode == HVACMode.HEAT + assert entity.preset_mode == PRESET_BOOST + + # 3 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await entity.async_set_preset_mode(PRESET_COMFORT) + assert entity.preset_mode == PRESET_COMFORT + + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # No change + assert entity.hvac_mode == HVACMode.HEAT + assert entity.preset_mode == PRESET_COMFORT + + # 4 change central_mode to COOL_ONLY + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_COOL_ONLY) + + # No change + assert entity.hvac_mode == HVACMode.HEAT + assert entity.preset_mode == PRESET_COMFORT + + # 5 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # No change + assert entity.hvac_mode == HVACMode.HEAT + assert entity.preset_mode == PRESET_COMFORT + + # 6 change central_mode to HEAT_ONLY + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_HEAT_ONLY) + + # No change + assert entity.hvac_mode == HVACMode.HEAT + assert entity.preset_mode == PRESET_COMFORT + + # 7 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # No change + assert entity.hvac_mode == HVACMode.HEAT + assert entity.preset_mode == PRESET_COMFORT + + # 8 change central_mode to FROST_PROTECTION + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_FROST_PROTECTION) + + # No change + assert entity.hvac_mode == HVACMode.HEAT + assert entity.preset_mode == PRESET_COMFORT + + # 9 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # No change + assert entity.hvac_mode == HVACMode.HEAT + assert entity.preset_mode == PRESET_COMFORT + + +async def test_climate_ac_only_change_central_mode_true( + hass: HomeAssistant, skip_hass_states_is_state, init_central_config +): + """test that changes with over_climate with AC only config with central_mode True are + taken into account + Test also switching from central_mode without coming to AUTO each time""" + + fake_underlying_climate = MockClimate( + hass, + "mockUniqueId", + "MockClimateName", + entry_infos={}, + hvac_modes=[HVACMode.OFF, HVACMode.COOL], + ) + + # Add a Climate VTherm + 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_USE_CENTRAL_MODE: True, + CONF_CYCLE_MIN: 5, + CONF_TEMP_MIN: 8, + CONF_TEMP_MAX: 18, + "frost_temp": 10, + "eco_temp": 17, + "comfort_temp": 18, + "boost_temp": 21, + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_CLIMATE: "climate.mock_climate", + CONF_MINIMAL_ACTIVATION_DELAY: 30, + CONF_SECURITY_DELAY_MIN: 5, + CONF_SECURITY_MIN_ON_PERCENT: 0.3, + CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, + }, + ) + + with patch("homeassistant.core.ServiceRegistry.async_call"), patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", + return_value=fake_underlying_climate, + ): + entity: ThermostatOverSwitch = await create_thermostat( + hass, entry, "climate.theoverclimatemockname" + ) + assert entity + assert entity.name == "TheOverClimateMockName" + assert entity.is_over_climate + assert entity.is_controlled_by_central_mode is True + assert entity.hvac_modes == [HVACMode.OFF, HVACMode.COOL] + + # Find the select entity + select_entity = search_entity(hass, "select.central_mode", SELECT_DOMAIN) + + assert select_entity + assert select_entity.current_option == CENTRAL_MODE_AUTO + assert select_entity.options == CENTRAL_MODES + + # start entity in Cooling mode + await entity.async_set_hvac_mode(HVACMode.COOL) + await entity.async_set_preset_mode(PRESET_ECO) + + assert entity.hvac_mode == HVACMode.COOL + assert entity.preset_mode == PRESET_ECO + + # 2 change central_mode to STOPPED + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_STOPPED) + + # No change + assert entity.hvac_mode == HVACMode.OFF + assert entity.preset_mode == PRESET_ECO + + # 3 change central_mode to HEAT ONLY after switching to COMFORT preset + with patch("homeassistant.core.ServiceRegistry.async_call"): + await entity.async_set_preset_mode(PRESET_COMFORT) + assert entity.preset_mode == PRESET_COMFORT + + await select_entity.async_select_option(CENTRAL_MODE_HEAT_ONLY) + + # Stay in OFF because HEAT is not permitted + assert entity.hvac_mode == HVACMode.OFF + assert entity.preset_mode == PRESET_COMFORT + + # 4 change central_mode to COOL_ONLY + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_COOL_ONLY) + + # switch back to COOL restoring the preset + assert entity.hvac_mode == HVACMode.COOL + assert entity.preset_mode == PRESET_COMFORT + + # 5 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # No change + assert entity.hvac_mode == HVACMode.COOL + assert entity.preset_mode == PRESET_COMFORT + + # 6 change central_mode to FROST_PROTECTION + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_FROST_PROTECTION) + + # No change + assert entity.hvac_mode == HVACMode.OFF + assert entity.preset_mode == PRESET_COMFORT + + # 7 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # No change + assert entity.hvac_mode == HVACMode.COOL + assert entity.preset_mode == PRESET_COMFORT + + # 8 change central_mode to FROST_PROTECTION + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_FROST_PROTECTION) + + # No change + assert entity.hvac_mode == HVACMode.OFF + assert entity.preset_mode == PRESET_COMFORT + + # 9 change back central_mode to COOL_ONLY + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_COOL_ONLY) + + # No change + assert entity.hvac_mode == HVACMode.COOL + assert entity.preset_mode == PRESET_COMFORT + + await entity.async_set_preset_mode(PRESET_ECO) + # 10 change back central_mode to HEAT_ONLY + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_HEAT_ONLY) + + # Shutdown cause no HEAT + assert entity.hvac_mode == HVACMode.OFF + assert entity.preset_mode == PRESET_ECO + + # 11 change back central_mode to AUTO + with patch("homeassistant.core.ServiceRegistry.async_call"): + await select_entity.async_select_option(CENTRAL_MODE_AUTO) + + # No change + assert entity.hvac_mode == HVACMode.COOL + assert entity.preset_mode == PRESET_ECO diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index ff37153..d04f613 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -363,6 +363,7 @@ async def test_user_config_flow_window_auto_ok( CONF_USE_POWER_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False, CONF_WINDOW_DELAY: 30, # the default value is added + CONF_USE_CENTRAL_MODE: True, # True is the defaulf value } | MOCK_TH_OVER_SWITCH_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_WINDOW_AUTO_CONFIG | { CONF_USE_MAIN_CENTRAL_CONFIG: True, CONF_USE_TPI_CENTRAL_CONFIG: False, @@ -510,6 +511,7 @@ async def test_user_config_flow_over_4_switches( CONF_USE_POWER_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False, CONF_USE_MAIN_CENTRAL_CONFIG: True, + CONF_USE_CENTRAL_MODE: False, } TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name diff --git a/tests/test_switch_ac.py b/tests/test_switch_ac.py index 3d4bb12..bd98257 100644 --- a/tests/test_switch_ac.py +++ b/tests/test_switch_ac.py @@ -129,7 +129,7 @@ async def test_over_switch_ac_full_start( assert entity.hvac_mode is HVACMode.OFF assert entity.hvac_action is HVACAction.OFF - assert entity.target_temperature == 16 # eco_ac_away + assert entity.target_temperature == 27 # eco_ac_away (no change) # Close a window with patch("homeassistant.helpers.condition.state", return_value=True): diff --git a/tests/test_valve.py b/tests/test_valve.py index 2807e0e..47593fc 100644 --- a/tests/test_valve.py +++ b/tests/test_valve.py @@ -283,6 +283,18 @@ async def test_over_valve_full_start( assert entity.is_device_active is True assert entity.hvac_action == HVACAction.HEATING + # Test window open/close (with a normal min/max so that is_device_active is False when open_percent is 0) + expected_state = State( + entity_id="number.mock_valve", state="0", attributes={"min": 0, "max": 99} + ) + + with patch( + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" + ) as mock_send_event, patch( + "homeassistant.core.ServiceRegistry.async_call" + ) as mock_service_call, patch( + "homeassistant.core.StateMachine.get", return_value=expected_state + ): # Open a window with patch("homeassistant.helpers.condition.state", return_value=True): event_timestamp = now - timedelta(minutes=1)