From 156d19666cf8511832de3de463e5d4068d3bc8a7 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sun, 3 Mar 2024 19:06:59 +0000 Subject: [PATCH] With calculation of VTherm temp entities + test ok --- .../versatile_thermostat/base_thermostat.py | 83 +++- .../versatile_thermostat/binary_sensor.py | 27 +- .../versatile_thermostat/climate.py | 2 +- .../versatile_thermostat/number.py | 122 +++--- .../versatile_thermostat/vtherm_api.py | 2 +- tests/commons.py | 7 +- tests/test_temp_number.py | 364 +++++++++++++++++- 7 files changed, 524 insertions(+), 83 deletions(-) diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 372a6ac..a059b43 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -137,8 +137,6 @@ from .prop_algorithm import PropAlgorithm from .open_window_algorithm import WindowOpenDetectionAlgorithm from .ema import ExponentialMovingAverage -from .temp_number import TemperatureNumber - _LOGGER = logging.getLogger(__name__) ConfigData = MappingProxyType[str, Any] @@ -216,6 +214,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): super().__init__() self._hass = hass + self._entry_infos = None self._attr_extra_state_attributes = {} self._unique_id = unique_id @@ -288,6 +287,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self._is_used_by_central_boiler = False self._support_flags = None + # Preset will be initialized from Number entities + self._presets: dict[str, Any] = {} # presets + self._presets_away: dict[str, Any] = {} # presets_away + self._attr_preset_modes: list[str] | None self.post_init(entry_infos) @@ -356,6 +359,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity): _LOGGER.info("%s - The merged configuration is %s", self, entry_infos) + self._entry_infos = entry_infos + self._ac_mode = entry_infos.get(CONF_AC_MODE) is True self._attr_max_temp = entry_infos.get(CONF_TEMP_MAX) self._attr_min_temp = entry_infos.get(CONF_TEMP_MIN) @@ -2649,18 +2654,68 @@ class BaseThermostat(ClimateEntity, RestoreEntity): """Send an event""" send_vtherm_event(self._hass, event_type=event_type, entity=self, data=data) - def get_temperature_number_entities(self, config_entry: ConfigData): - """Creates all TemperatureNumber depending of the configuration of the Climate""" + async def init_presets(self, central_config): + """Init all presets of the VTherm""" + # If preset central config is used and central config is set , take the presets from central config + vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api() - # TODO add the list of preset we want to use in the VTherm. Here we will suppose all preset will be available - entity = TemperatureNumber( - self._hass, - unique_id=config_entry.entry_id, - name=config_entry.data.get(CONF_NAME), - preset_name="comfort", - is_ac=False, - is_away=False, - entry_infos=config_entry.data, + presets: dict[str, Any] = {} + presets_away: dict[str, Any] = {} + + def calculate_presets(items, use_central_conf_key): + presets: dict[str, Any] = {} + config_id = self._unique_id + if ( + central_config + and self._entry_infos.get(use_central_conf_key, False) is True + ): + config_id = central_config.entry_id + + for key, preset_name in items: + _LOGGER.debug("looking for key=%s, preset_name=%s", key, preset_name) + value = vtherm_api.get_temperature_number_value( + config_id=config_id, preset_name=preset_name + ) + if value is not None: + presets[key] = value + else: + _LOGGER.debug("preset_name %s not found in VTherm API", preset_name) + presets[key] = ( + self._attr_max_temp if self._ac_mode else self._attr_min_temp + ) + return presets + + # Calculate all presets + presets = calculate_presets( + CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items(), + CONF_USE_PRESETS_CENTRAL_CONFIG, ) - return entity + if self._entry_infos.get(CONF_USE_PRESENCE_FEATURE) is True: + presets_away = calculate_presets( + ( + CONF_PRESETS_AWAY_WITH_AC.items() + if self._ac_mode + else CONF_PRESETS_AWAY.items() + ), + CONF_USE_PRESENCE_CENTRAL_CONFIG, + ) + + # aggregate all available presets now + self._presets: dict[str, Any] = presets + self._presets_away: dict[str, Any] = presets_away + + # Calculate all possible presets + self._attr_preset_modes = [PRESET_NONE] + if len(self._presets): + self._support_flags = SUPPORT_FLAGS | ClimateEntityFeature.PRESET_MODE + + for key, _ in CONF_PRESETS.items(): + if self.find_preset_temp(key) > 0: + self._attr_preset_modes.append(key) + + _LOGGER.debug( + "After adding presets, preset_modes to %s", self._attr_preset_modes + ) + else: + _LOGGER.debug("No preset_modes") diff --git a/custom_components/versatile_thermostat/binary_sensor.py b/custom_components/versatile_thermostat/binary_sensor.py index 7e30cc9..d59b9a2 100644 --- a/custom_components/versatile_thermostat/binary_sensor.py +++ b/custom_components/versatile_thermostat/binary_sensor.py @@ -7,11 +7,11 @@ from homeassistant.core import ( HomeAssistant, callback, Event, - CoreState, + # CoreState, HomeAssistantError, ) -from homeassistant.const import STATE_ON, STATE_OFF, EVENT_HOMEASSISTANT_START +from homeassistant.const import STATE_ON, STATE_OFF # , EVENT_HOMEASSISTANT_START from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType from homeassistant.helpers.event import async_track_state_change_event @@ -386,17 +386,18 @@ class CentralBoilerBinarySensor(BinarySensorEntity): api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass) api.register_central_boiler(self) - @callback - async def _async_startup_internal(*_): - _LOGGER.debug("%s - Calling async_startup_internal", self) - await self.listen_nb_active_vtherm_entity() - - if self.hass.state == CoreState.running: - await _async_startup_internal() - else: - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, _async_startup_internal - ) + # Should be not more needed and replaced by vtherm_api.init_vtherm_links + # @callback + # async def _async_startup_internal(*_): + # _LOGGER.debug("%s - Calling async_startup_internal", self) + # await self.listen_nb_active_vtherm_entity() + # + # if self.hass.state == CoreState.running: + # await _async_startup_internal() + # else: + # self.hass.bus.async_listen_once( + # EVENT_HOMEASSISTANT_START, _async_startup_internal + # ) async def listen_nb_active_vtherm_entity(self): """Initialize the listening of state change of VTherms""" diff --git a/custom_components/versatile_thermostat/climate.py b/custom_components/versatile_thermostat/climate.py index 151826c..8d5f0a0 100644 --- a/custom_components/versatile_thermostat/climate.py +++ b/custom_components/versatile_thermostat/climate.py @@ -76,7 +76,7 @@ async def async_setup_entry( elif vt_type == CONF_THERMOSTAT_VALVE: entity = ThermostatOverValve(hass, unique_id, name, entry.data) - async_add_entities([entity, entity.get_temperature_number_entities(entry)], True) + async_add_entities([entity], True) # Add services platform = entity_platform.async_get_current_platform() diff --git a/custom_components/versatile_thermostat/number.py b/custom_components/versatile_thermostat/number.py index 831bbc1..e413522 100644 --- a/custom_components/versatile_thermostat/number.py +++ b/custom_components/versatile_thermostat/number.py @@ -23,7 +23,7 @@ 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.util import ensure_unique_string, slugify from .vtherm_api import VersatileThermostatAPI from .commons import VersatileThermostatBaseEntity @@ -48,6 +48,9 @@ from .const import ( CONF_PRESETS_WITH_AC_VALUES, CONF_PRESETS_AWAY_VALUES, CONF_PRESETS_AWAY_WITH_AC_VALUES, + CONF_USE_PRESETS_CENTRAL_CONFIG, + CONF_USE_PRESENCE_CENTRAL_CONFIG, + CONF_USE_PRESENCE_FEATURE, overrides, ) @@ -84,28 +87,45 @@ async def async_setup_entry( unique_id = entry.entry_id name = entry.data.get(CONF_NAME) vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) - is_central_boiler = entry.data.get(CONF_ADD_CENTRAL_BOILER_CONTROL) + # is_central_boiler = entry.data.get(CONF_ADD_CENTRAL_BOILER_CONTROL) entities = [] if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG: - if not is_central_boiler: - pass - # for preset in CONF_PRESETS_VALUES: - # entities.append( - # TemperatureNumber( - # hass, unique_id, preset, preset, False, False, entry.data - # ) - # ) + # Creates non central temperature entities + if not entry.data.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False): + for preset in CONF_PRESETS_VALUES: + entities.append( + TemperatureNumber( + hass, unique_id, name, preset, False, False, entry.data + ) + ) + if entry.data.get(CONF_AC_MODE, False): + for preset in CONF_PRESETS_WITH_AC_VALUES: + entities.append( + TemperatureNumber( + hass, unique_id, name, preset, True, False, entry.data + ) + ) - # TODO - # if entry.data.get(CONF_AC_MODE, False): - # for preset in CONF_PRESETS_WITH_AC_VALUES: - # entities.append( - # TemperatureNumber( - # hass, unique_id, preset, preset, True, False, entry.data - # ) - # ) + if entry.data.get( + CONF_USE_PRESENCE_FEATURE, False + ) is True and not entry.data.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False): + for preset in CONF_PRESETS_AWAY_VALUES: + entities.append( + TemperatureNumber( + hass, unique_id, name, preset, False, True, entry.data + ) + ) + + if entry.data.get(CONF_AC_MODE, False): + for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES: + entities.append( + TemperatureNumber( + hass, unique_id, name, preset, True, True, entry.data + ) + ) + # For central config only else: entities.append( ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data) @@ -113,27 +133,27 @@ async def async_setup_entry( for preset in CONF_PRESETS_VALUES: entities.append( CentralConfigTemperatureNumber( - hass, unique_id, preset, preset, False, False, entry.data + hass, unique_id, name, preset, False, False, entry.data ) ) for preset in CONF_PRESETS_WITH_AC_VALUES: entities.append( CentralConfigTemperatureNumber( - hass, unique_id, preset, preset, True, False, entry.data + hass, unique_id, name, preset, True, False, entry.data ) ) for preset in CONF_PRESETS_AWAY_VALUES: entities.append( CentralConfigTemperatureNumber( - hass, unique_id, preset, preset, False, True, entry.data + hass, unique_id, name, preset, False, True, entry.data ) ) for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES: entities.append( CentralConfigTemperatureNumber( - hass, unique_id, preset, preset, True, True, entry.data + hass, unique_id, name, preset, True, True, entry.data ) ) @@ -211,7 +231,6 @@ class CentralConfigTemperatureNumber(NumberEntity, RestoreEntity): """Representation of one temperature number""" _attr_has_entity_name = True - # _attr_translation_key = "temperature" def __init__( self, @@ -227,7 +246,7 @@ class CentralConfigTemperatureNumber(NumberEntity, RestoreEntity): the restoration will do the trick.""" self._config_id = unique_id - self._device_name = entry_infos.get(CONF_NAME) + self._device_name = name # self._attr_name = name self._attr_translation_key = preset_name @@ -236,7 +255,8 @@ class CentralConfigTemperatureNumber(NumberEntity, RestoreEntity): # "ac": "-AC" if is_ac else "", # "away": "-AWAY" if is_away else "", # } - self.entity_id = f"{NUMBER_DOMAIN}.central_configuration_{preset_name}" + # self.entity_id = f"{NUMBER_DOMAIN}.central_configuration_{preset_name}" + self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_{preset_name}" self._attr_unique_id = f"central_configuration_{preset_name}" self._attr_device_class = NumberDeviceClass.TEMPERATURE self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS @@ -250,7 +270,7 @@ class CentralConfigTemperatureNumber(NumberEntity, RestoreEntity): # previous value # TODO remove this after the next major release and just keep the init min/max temp = None - if temp := entry_infos.get(preset_name, None): + if (temp := entry_infos.get(preset_name, None)) is not None: self._attr_value = self._attr_native_value = temp else: if entry_infos.get(CONF_AC_MODE) is True: @@ -346,7 +366,6 @@ class TemperatureNumber( # pylint: disable=abstract-method """Representation of one temperature number""" _attr_has_entity_name = True - _attr_translation_key = "temperature" def __init__( self, @@ -360,40 +379,41 @@ class TemperatureNumber( # pylint: disable=abstract-method ) -> None: """Initialize the temperature with entry_infos if available. Else the restoration will do the trick.""" - super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + super().__init__(hass, unique_id, name) - split = name.split("_") - # self._attr_name = split[0] - # if "_" + split[1] == PRESET_AC_SUFFIX: - # self._attr_name = self._attr_name + " AC" + self._attr_translation_key = preset_name + self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_{preset_name}" - # self._attr_name = self._attr_name + " temperature" - - self._attr_translation_placeholders = { - "preset": preset_name, - "ac": "-AC" if is_ac else "", - "away": "-AWAY" if is_away else "", - } - self._attr_unique_id = f"{self._device_name}_{name}" + # self._attr_translation_placeholders = { + # "preset": preset_name, + # "ac": "-AC" if is_ac else "", + # "away": "-AWAY" if is_away else "", + # } + self._attr_unique_id = f"{self._device_name}_{preset_name}" self._attr_device_class = NumberDeviceClass.TEMPERATURE self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + self._attr_native_step = entry_infos.get(CONF_STEP_TEMPERATURE, 0.5) + self._attr_native_min_value = entry_infos.get(CONF_TEMP_MIN) + self._attr_native_max_value = entry_infos.get(CONF_TEMP_MAX) + # Initialize the values if included into the entry_infos. This will do # the temperature migration. # TODO see if this should be replace by the central config if any temp = None - if temp := entry_infos.get(preset_name, None): + if (temp := entry_infos.get(preset_name, None)) is not None: self._attr_value = self._attr_native_value = temp + else: + if entry_infos.get(CONF_AC_MODE) is True: + self._attr_native_value = self._attr_native_max_value + else: + self._attr_native_value = self._attr_native_min_value self._attr_mode = NumberMode.BOX self._preset_name = preset_name self._is_away = is_away self._is_ac = is_ac - self._attr_native_step = entry_infos.get(CONF_STEP_TEMPERATURE, 0.5) - self._attr_native_min_value = entry_infos.get(CONF_TEMP_MIN) - self._attr_native_max_value = entry_infos.get(CONF_TEMP_MAX) - @property def icon(self) -> str | None: return PRESET_ICON_MAPPING[self._preset_name] @@ -402,12 +422,16 @@ class TemperatureNumber( # pylint: disable=abstract-method async def async_added_to_hass(self) -> None: await super().async_added_to_hass() + # register the temp entity for this device and preset + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass) + api.register_temperature_number(self._config_id, self._preset_name, self) + old_state: CoreState = await self.async_get_last_state() _LOGGER.debug( "%s - Calling async_added_to_hass old_state is %s", self, old_state ) try: - if old_state is not None and (value := float(old_state.state) > 0): + if old_state is not None and ((value := float(old_state.state)) > 0): self._attr_value = self._attr_native_value = value except ValueError: pass @@ -425,12 +449,6 @@ class TemperatureNumber( # pylint: disable=abstract-method ) return - # @overrides - # @property - # def native_step(self) -> float | None: - # """The native step""" - # return self.my_climate.target_temperature_step - @overrides async def async_set_native_value(self, value: float) -> None: """Change the value""" diff --git a/custom_components/versatile_thermostat/vtherm_api.py b/custom_components/versatile_thermostat/vtherm_api.py index 8c7ba18..19ff86c 100644 --- a/custom_components/versatile_thermostat/vtherm_api.py +++ b/custom_components/versatile_thermostat/vtherm_api.py @@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity_component import EntityComponent from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.number import NumberEntity, DOMAIN as NUMBER_DOMAIN +from homeassistant.components.number import NumberEntity from .const import ( DOMAIN, diff --git a/tests/commons.py b/tests/commons.py index a57baa5..656092e 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -500,7 +500,12 @@ async def create_thermostat( await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.LOADED - return search_entity(hass, entity_id, CLIMATE_DOMAIN) + # We should reload the VTherm links + vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api() + entity = search_entity(hass, entity_id, CLIMATE_DOMAIN) + if entity: + await entity.init_presets(vtherm_api.find_central_configuration()) + return entity async def create_central_config( # pylint: disable=dangerous-default-value diff --git a/tests/test_temp_number.py b/tests/test_temp_number.py index 1d0357b..14ef6fc 100644 --- a/tests/test_temp_number.py +++ b/tests/test_temp_number.py @@ -14,7 +14,10 @@ from homeassistant.components.number import NumberEntity, DOMAIN as NUMBER_DOMAI from pytest_homeassistant_custom_component.common import MockConfigEntry -# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat +from custom_components.versatile_thermostat.base_thermostat import BaseThermostat +from custom_components.versatile_thermostat.thermostat_switch import ( + ThermostatOverSwitch, +) from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI from .commons import * @@ -394,3 +397,362 @@ async def test_add_number_for_central_config_without_temp_restore( config_id=central_config_entry.entry_id, preset_name=preset_name ) assert val == value + + +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_add_number_for_over_switch_use_central( + hass: HomeAssistant, skip_hass_states_is_state +): + """Test the construction of a over switch vtherm with + use central config for PRESET and PRESENCE. + It also have old temp config value which should be not used. + So it should have no Temp NumberEntity""" + + vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + + temps = { + "frost_temp": 10, + "eco_temp": 17.1, + "comfort_temp": 18.1, + "boost_temp": 19.1, + "eco_ac_temp": 25.1, + "comfort_ac_temp": 23.1, + "boost_ac_temp": 21.1, + "frost_away_temp": 15.1, + "eco_away_temp": 15.2, + "comfort_away_temp": 15.3, + "boost_away_temp": 15.4, + "eco_ac_away_temp": 30.5, + "comfort_ac_away_temp": 30.6, + "boost_ac_away_temp": 30.7, + } + + vtherm_entry = MockConfigEntry( + domain=DOMAIN, + title="TheCentralConfigMockName", + unique_id="centralConfigUniqueId", + data={ + CONF_NAME: "TheOverSwitchVTherm", + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_central_ext_temp_sensor", + CONF_TEMP_MIN: 15, + CONF_TEMP_MAX: 30, + CONF_AC_MODE: False, + CONF_TPI_COEF_INT: 0.5, + CONF_TPI_COEF_EXT: 0.02, + CONF_USE_PRESENCE_CENTRAL_CONFIG: True, + CONF_USE_ADVANCED_CENTRAL_CONFIG: True, + CONF_USE_MAIN_CENTRAL_CONFIG: True, + CONF_USE_PRESETS_CENTRAL_CONFIG: True, + CONF_USE_WINDOW_CENTRAL_CONFIG: True, + CONF_USE_POWER_CENTRAL_CONFIG: True, + CONF_USE_MOTION_CENTRAL_CONFIG: True, + } + | temps, + ) + + # The restore should not be used + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=State(entity_id="number.mock_valve", state="23"), + ) as mock_restore_state: + vtherm_entry.add_to_hass(hass) + await hass.config_entries.async_setup(vtherm_entry.entry_id) + + assert mock_restore_state.call_count == 0 + + assert vtherm_entry.state is ConfigEntryState.LOADED + + # We search for NumberEntities + for preset_name, value in temps.items(): + temp_entity = search_entity( + hass, + "number.central_configuration_" + preset_name, + NUMBER_DOMAIN, + ) + assert temp_entity is None + + # Find temp Number into vtherm_api + val = vtherm_api.get_temperature_number_value( + config_id=vtherm_entry.entry_id, preset_name=preset_name + ) + assert val is None + + +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_add_number_for_over_switch_use_central_presence( + hass: HomeAssistant, skip_hass_states_is_state, init_central_config +): + """Test the construction of a over switch vtherm with + use central config for PRESET and PRESENCE. + It also have old temp config value which should be not used. + So it should have no Temp NumberEntity""" + + vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + + temps = { + "frost_temp": 10, + "eco_temp": 17.1, + "comfort_temp": 18.1, + "boost_temp": 19.1, + "eco_ac_temp": 25.1, + "comfort_ac_temp": 23.1, + "boost_ac_temp": 21.1, + } + temps_missing = { + "frost_away_temp": 15.1, + "eco_away_temp": 15.2, + "comfort_away_temp": 15.3, + "boost_away_temp": 15.4, + "eco_ac_away_temp": 30.5, + "comfort_ac_away_temp": 30.6, + "boost_ac_away_temp": 30.7, + } + + vtherm_entry = MockConfigEntry( + domain=DOMAIN, + title="TheCentralConfigMockName", + unique_id="centralConfigUniqueId", + data={ + CONF_NAME: "TheOverSwitchVTherm", + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, + CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, + CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_central_ext_temp_sensor", + CONF_TEMP_MIN: 15, + CONF_TEMP_MAX: 30, + CONF_AC_MODE: True, + CONF_TPI_COEF_INT: 0.5, + CONF_TPI_COEF_EXT: 0.02, + CONF_CYCLE_MIN: 5, + CONF_HEATER: "switch.mock_switch1", + CONF_USE_PRESENCE_FEATURE: True, + CONF_USE_PRESENCE_CENTRAL_CONFIG: True, + CONF_USE_ADVANCED_CENTRAL_CONFIG: True, + CONF_USE_MAIN_CENTRAL_CONFIG: True, + CONF_USE_PRESETS_CENTRAL_CONFIG: False, + CONF_USE_WINDOW_CENTRAL_CONFIG: True, + CONF_USE_POWER_CENTRAL_CONFIG: True, + CONF_USE_MOTION_CENTRAL_CONFIG: True, + } + | temps + | temps_missing, + ) + + vtherm: BaseThermostat = await create_thermostat( + hass, vtherm_entry, "climate.theoverswitchvtherm" + ) + + # 1. We search for NumberEntities + for preset_name, value in temps.items(): + temp_entity = search_entity( + hass, + "number.theoverswitchvtherm_" + preset_name, + NUMBER_DOMAIN, + ) + assert temp_entity + assert temp_entity.state == value + + # This test is dependent to translation en.json. If translations change + # this may fails + assert ( + temp_entity.name.lower() + == preset_name.replace("_temp", "") + .replace("_ac", " ac") + .replace("_away", " away") + .lower() + ) + + # Find temp Number into vtherm_api + val = vtherm_api.get_temperature_number_value( + config_id=vtherm_entry.entry_id, preset_name=preset_name + ) + assert val == value + + # 2. We search for NumberEntities to be missing + for preset_name, value in temps_missing.items(): + temp_entity = search_entity( + hass, + "number.theoverswitchvtherm_" + preset_name, + NUMBER_DOMAIN, + ) + assert temp_entity is None + + # Find temp Number into vtherm_api + val = vtherm_api.get_temperature_number_value( + config_id=vtherm_entry.entry_id, preset_name=preset_name + ) + assert val is None + + # 3. The VTherm should be initialized with all presets and correct temperature + assert vtherm + assert isinstance(vtherm, ThermostatOverSwitch) + assert vtherm.preset_modes == [ + PRESET_NONE, + PRESET_FROST_PROTECTION, + PRESET_ECO, + PRESET_COMFORT, + PRESET_BOOST, + ] + + assert vtherm._presets == { + PRESET_FROST_PROTECTION: temps["frost_temp"], + PRESET_ECO: temps["eco_temp"], + PRESET_COMFORT: temps["comfort_temp"], + PRESET_BOOST: temps["boost_temp"], + PRESET_ECO_AC: temps["eco_ac_temp"], + PRESET_COMFORT_AC: temps["comfort_ac_temp"], + PRESET_BOOST_AC: temps["boost_ac_temp"], + } + + # Preset away should be initialized with the central config + assert vtherm._presets_away == { + PRESET_FROST_PROTECTION + + PRESET_AWAY_SUFFIX: FULL_CENTRAL_CONFIG["frost_away_temp"], + PRESET_ECO + PRESET_AWAY_SUFFIX: FULL_CENTRAL_CONFIG["eco_away_temp"], + PRESET_COMFORT + PRESET_AWAY_SUFFIX: FULL_CENTRAL_CONFIG["comfort_away_temp"], + PRESET_BOOST + PRESET_AWAY_SUFFIX: FULL_CENTRAL_CONFIG["boost_away_temp"], + PRESET_ECO_AC + PRESET_AWAY_SUFFIX: FULL_CENTRAL_CONFIG["eco_ac_away_temp"], + PRESET_COMFORT_AC + + PRESET_AWAY_SUFFIX: FULL_CENTRAL_CONFIG["comfort_ac_away_temp"], + PRESET_BOOST_AC + PRESET_AWAY_SUFFIX: FULL_CENTRAL_CONFIG["boost_ac_away_temp"], + } + + +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_add_number_for_over_switch_use_central_presets_and_restore( + hass: HomeAssistant, skip_hass_states_is_state, init_central_config +): + """Test the construction of a over switch vtherm with + use central config for PRESET and PRESENCE. + It also have old temp config value which should be not used. + So it should have no Temp NumberEntity""" + + vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + + temps = { + "frost_away_temp": 23, + "eco_away_temp": 23, + "comfort_away_temp": 23, + "boost_away_temp": 23, + } + temps_missing = { + "frost_temp": 10, + "eco_temp": 17.1, + "comfort_temp": 18.1, + "boost_temp": 19.1, + "eco_ac_temp": 25.1, + "comfort_ac_temp": 23.1, + "boost_ac_temp": 21.1, + "eco_ac_away_temp": 30.5, + "comfort_ac_away_temp": 30.6, + "boost_ac_away_temp": 30.7, + } + + vtherm_entry = MockConfigEntry( + domain=DOMAIN, + title="TheCentralConfigMockName", + unique_id="centralConfigUniqueId", + data={ + CONF_NAME: "TheOverSwitchVTherm", + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_central_ext_temp_sensor", + CONF_TEMP_MIN: 15, + CONF_TEMP_MAX: 30, + CONF_AC_MODE: False, + CONF_TPI_COEF_INT: 0.5, + CONF_TPI_COEF_EXT: 0.02, + CONF_CYCLE_MIN: 5, + CONF_HEATER: "switch.mock_switch1", + CONF_USE_PRESENCE_FEATURE: True, + CONF_USE_PRESENCE_CENTRAL_CONFIG: False, + CONF_USE_ADVANCED_CENTRAL_CONFIG: True, + CONF_USE_MAIN_CENTRAL_CONFIG: True, + CONF_USE_PRESETS_CENTRAL_CONFIG: True, + CONF_USE_WINDOW_CENTRAL_CONFIG: True, + CONF_USE_POWER_CENTRAL_CONFIG: True, + CONF_USE_MOTION_CENTRAL_CONFIG: True, + } + | temps + | temps_missing, + ) + + # The restore should not be used + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=State(entity_id="number.mock_valve", state="23"), + ) as mock_restore_state: + vtherm: BaseThermostat = await create_thermostat( + hass, vtherm_entry, "climate.theoverswitchvtherm" + ) + + # We should try to restore all 4 temp entities + assert mock_restore_state.call_count == 4 + + # 1. We search for NumberEntities + for preset_name, value in temps.items(): + temp_entity = search_entity( + hass, + "number.theoverswitchvtherm_" + preset_name, + NUMBER_DOMAIN, + ) + assert temp_entity + assert temp_entity.state == value + + # This test is dependent to translation en.json. If translations change + # this may fails + assert ( + temp_entity.name.lower() + == preset_name.replace("_temp", "") + .replace("_ac", " ac") + .replace("_away", " away") + .lower() + ) + + # Find temp Number into vtherm_api + val = vtherm_api.get_temperature_number_value( + config_id=vtherm_entry.entry_id, preset_name=preset_name + ) + assert val == value + + # 2. We search for NumberEntities to be missing + for preset_name, value in temps_missing.items(): + temp_entity = search_entity( + hass, + "number.theoverswitchvtherm_" + preset_name, + NUMBER_DOMAIN, + ) + assert temp_entity is None + + # Find temp Number into vtherm_api + val = vtherm_api.get_temperature_number_value( + config_id=vtherm_entry.entry_id, preset_name=preset_name + ) + assert val is None + + # 3. The VTherm should be initialized with all presets and correct temperature + assert vtherm + assert isinstance(vtherm, ThermostatOverSwitch) + assert vtherm.preset_modes == [ + PRESET_NONE, + PRESET_FROST_PROTECTION, + PRESET_ECO, + # PRESET_COMFORT, because temp is 0 + PRESET_BOOST, + ] + + # Preset away should be empty cause we use central config for presets + assert vtherm._presets == { + PRESET_FROST_PROTECTION: FULL_CENTRAL_CONFIG["frost_temp"], + PRESET_ECO: FULL_CENTRAL_CONFIG["eco_temp"], + PRESET_COMFORT: FULL_CENTRAL_CONFIG["comfort_temp"], + PRESET_BOOST: FULL_CENTRAL_CONFIG["boost_temp"], + } + + assert vtherm._presets_away == { + PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX: temps["frost_away_temp"], + PRESET_ECO + PRESET_AWAY_SUFFIX: temps["eco_away_temp"], + PRESET_COMFORT + PRESET_AWAY_SUFFIX: temps["comfort_away_temp"], + PRESET_BOOST + PRESET_AWAY_SUFFIX: temps["boost_away_temp"], + }