"""Use of this source code is governed by the MIT license found in the LICENSE file. Plugwise backend module for Home Assistant Core - covering the legacy P1, Anna, and Stretch devices. """ from __future__ import annotations from collections.abc import Awaitable, Callable import datetime as dt from typing import Any from plugwise.constants import ( APPLIANCES, DOMAIN_OBJECTS, LOCATIONS, LOGGER, MODULES, OFF, REQUIRE_APPLIANCES, RULES, STATE_OFF, STATE_ON, GwEntityData, ThermoLoc, ) from plugwise.exceptions import ConnectionFailedError, DataMissingError, PlugwiseError from plugwise.legacy.data import SmileLegacyData from munch import Munch class SmileLegacyAPI(SmileLegacyData): """The Plugwise SmileLegacyAPI helper class for actual Plugwise legacy devices.""" # pylint: disable=too-many-instance-attributes, too-many-public-methods def __init__( self, _is_thermostat: bool, _loc_data: dict[str, ThermoLoc], _on_off_device: bool, _opentherm_device: bool, _request: Callable[..., Awaitable[Any]], _stretch_v2: bool, _target_smile: str, smile: Munch, ) -> None: """Set the constructor for this class.""" super().__init__() self._cooling_present = False self._is_thermostat = _is_thermostat self._loc_data = _loc_data self._on_off_device = _on_off_device self._opentherm_device = _opentherm_device self._request = _request self._stretch_v2 = _stretch_v2 self._target_smile = _target_smile self.smile = smile self._first_update = True self._previous_day_number: str = "0" @property def cooling_present(self) -> bool: """Return the cooling capability.""" return False async def full_xml_update(self) -> None: """Perform a first fetch of the Plugwise server XML data.""" self._domain_objects = await self._request(DOMAIN_OBJECTS) self._locations = await self._request(LOCATIONS) self._modules = await self._request(MODULES) # P1 legacy has no appliances if self.smile.type != "power": self._appliances = await self._request(APPLIANCES) def get_all_gateway_entities(self) -> None: """Collect the Plugwise gateway entities and their data and states from the received raw XML-data. First, collect all the connected entities and their initial data. Collect and add switching- and/or pump-group entities. Finally, collect the data and states for each entity. """ self._get_appliances() self._get_groups() self._all_entity_data() async def async_update(self) -> dict[str, GwEntityData]: """Perform an full update update at day-change: re-collect all gateway entities and their data and states. Otherwise perform an incremental update: only collect the entities updated data and states. """ day_number = dt.datetime.now().strftime("%w") if self._first_update or day_number != self._previous_day_number: LOGGER.info( "Performing daily full-update, reload the Plugwise integration when a single entity becomes unavailable." ) try: await self.full_xml_update() self.get_all_gateway_entities() # Detect failed data-retrieval _ = self.gw_entities[self.gateway_id]["location"] except KeyError as err: # pragma: no cover raise DataMissingError(f"No (full) legacy data: {err}") from err else: try: self._domain_objects = await self._request(DOMAIN_OBJECTS) match self._target_smile: case "smile_v2": self._modules = await self._request(MODULES) case self._target_smile if self._target_smile in REQUIRE_APPLIANCES: self._appliances = await self._request(APPLIANCES) self._update_gw_entities() # Detect failed data-retrieval _ = self.gw_entities[self.gateway_id]["location"] except KeyError as err: # pragma: no cover raise DataMissingError(f"No legacy data: {err}") from err self._first_update = False self._previous_day_number = day_number return self.gw_entities ######################################################################################################## ### API Set and HA Service-related Functions ### ######################################################################################################## async def delete_notification(self) -> None: """Set-function placeholder for legacy devices.""" async def reboot_gateway(self) -> None: """Set-function placeholder for legacy devices.""" async def set_dhw_mode(self, mode: str) -> None: """Set-function placeholder for legacy devices.""" async def set_gateway_mode(self, mode: str) -> None: """Set-function placeholder for legacy devices.""" async def set_number( self, dev_id: str, key: str, temperature: float, ) -> None: """Set-function placeholder for legacy devices.""" async def set_offset(self, dev_id: str, offset: float) -> None: """Set-function placeholder for legacy devices.""" async def set_preset(self, _: str, preset: str) -> None: """Set the given Preset on the relevant Thermostat - from DOMAIN_OBJECTS.""" if not (presets := self._presets()): raise PlugwiseError("Plugwise: no presets available.") # pragma: no cover if preset not in list(presets): raise PlugwiseError("Plugwise: invalid preset.") locator = f'rule/directives/when/then[@icon="{preset}"].../.../...' if (rule := self._domain_objects.find(locator)) is None: raise PlugwiseError("Plugwise: no preset rule found.") # pragma: no cover if (rule_id := rule.get("id")) is None: raise PlugwiseError("Plugwise: no preset id found.") # pragma: no cover data = f"true" await self.call_request(RULES, method="put", data=data) async def set_regulation_mode(self, mode: str) -> None: """Set-function placeholder for legacy devices.""" async def set_select( self, key: str, loc_id: str, option: str, state: str | None ) -> None: """Set the thermostat schedule option.""" # schedule name corresponds to select option await self.set_schedule_state("dummy", state, option) async def set_schedule_state( self, _: str, state: str | None, name: str | None ) -> None: """Activate/deactivate the Schedule. Determined from - DOMAIN_OBJECTS. Used in HA Core to set the hvac_mode: in practice switch between schedule on - off. """ if state not in (STATE_OFF, STATE_ON): raise PlugwiseError("Plugwise: invalid schedule state.") # Handle no schedule-name / Off-schedule provided if name is None or name == OFF: name = "Thermostat schedule" schedule_rule_id: str | None = None for rule in self._domain_objects.findall("rule"): if rule.find("name").text == name: schedule_rule_id = rule.get("id") break if schedule_rule_id is None: raise PlugwiseError( "Plugwise: no schedule with this name available." ) # pragma: no cover new_state = "false" if state == STATE_ON: new_state = "true" locator = f'.//*[@id="{schedule_rule_id}"]/template' template_id = self._domain_objects.find(locator).get("id") data = ( "" f"" f"" f"