"""Python wrapper for the Palazzetti Connection Box API.""" import json from json.decoder import JSONDecodeError import aiohttp from .config import PalazzettiClientConfig from .const import ( API_COMMAND_URL_TEMPLATE, COMMAND_CHECK_ONLINE, COMMAND_SET_FAN_SILENT, COMMAND_SET_MAIN_FAN_SPEED, COMMAND_SET_LEFT_FAN_SPEED, COMMAND_SET_RIGHT_FAN_SPEED, COMMAND_SET_OFF, COMMAND_SET_ON, COMMAND_SET_POWER_MODE, COMMAND_SET_TEMPERATURE, COMMAND_UPDATE_PROPERTIES, COMMAND_UPDATE_STATE, REDACTED_DATA, ) from .exceptions import CommunicationError, ValidationError from .fan import FanType from .state import _PalazzettiAPIData, _PalazzettiState from .temperature import TemperatureDefinition class PalazzettiClient: """Interface class for the Overkiz API.""" connected = False def __init__( self, hostname: str, session: aiohttp.ClientSession | None = None, config: PalazzettiClientConfig = PalazzettiClientConfig(), ): self._hostname = hostname self._session = session or aiohttp.ClientSession() self._config = config self._state = _PalazzettiState(config) async def connect(self) -> bool: """Connect to the device.""" r = await self._execute_command( command=COMMAND_UPDATE_PROPERTIES, merge_state=False, ) if r.success: self._state.merge_properties(r) self.connected = True else: self.connected = False return self.connected async def is_online(self) -> bool: """Test if the device is online.""" if not self.connected: await self.connect() return (await self._execute_command(command=COMMAND_CHECK_ONLINE)).success async def update_state(self) -> bool: """Update the device's state.""" # Connect if not connected yet if not self.connected: await self.connect() # Check if connection was successful before updating if self.connected: self.connected = ( await self._execute_command(command=COMMAND_UPDATE_STATE) ).success return self.connected @property def sw_version(self) -> str: """Return the software version.""" return self._state.sw_version @property def hw_version(self) -> str: """Return the hardware version""" return self._state.hw_version @property def has_on_off_switch(self) -> bool: """Return the availability of the on/of switch""" return self._state.has_on_off_switch @property def target_temperature(self) -> int: """Return the target temperature""" return self._state.target_temperature @property def current_temperature(self) -> float: """Return the current temperature.""" return self._state.current_temperature @property def has_air_outlet_temperature(self) -> bool: """Return the air outlet temperature.""" return self._state.has_air_outlet_temperature @property def air_outlet_temperature(self) -> float: """Return the air outlet temperature.""" return self._state.air_outlet_temperature @property def room_temperature(self) -> float: """DEPRECATED - Return the room temperature.""" return self._state.current_temperature @property def outlet_temperature(self) -> float: """DEPRECATED - Return the outlet temperature.""" return self._state.air_outlet_temperature @property def has_wood_combustion_temperature(self) -> bool: """Return the air outlet temperature.""" return self._state.has_wood_combustion_temperature @property def wood_combustion_temperature(self) -> float: """Return the wood combustion temperature.""" return self._state.wood_combustion_temperature @property def T1(self) -> float: """Return the T1 temperature.""" return self._state.T1 @property def T2(self) -> float: """Return the T2 temperature.""" return self._state.T2 @property def T3(self) -> float: """Return the T3 temperature.""" return self._state.T3 @property def T4(self) -> float: """Return the T4 temperature.""" return self._state.T4 @property def T5(self) -> float: """Return the T5 temperature.""" return self._state.T5 def list_temperatures(self) -> list[TemperatureDefinition]: """Return a list of all available temperatures.""" return self._state.list_temperatures() @property def host(self) -> str: """Return the host name or IP address.""" return self._hostname @property def mac(self) -> str: """Return the mac address.""" return self._state.mac @property def name(self) -> str: """Return the stove's name.""" return self._state.name @property def status(self) -> int: """Return the stove's status.""" return self._state.status @property def fan_speed(self) -> int: """DEPRECATED - Return the fan mode.""" return self._state.main_fan_speed def current_fan_speed(self, fan: FanType = FanType.MAIN) -> int: """Return a fan's speed""" if fan == FanType.MAIN: return self._state.main_fan_speed elif fan == FanType.LEFT: return self._state.left_fan_speed elif fan == FanType.RIGHT: return self._state.right_fan_speed else: return 0 @property def power_mode(self) -> int: """Return the power mode.""" return self._state.power_mode @property def pellet_quantity(self) -> int: """Return the pellet quantity.""" return self._state.pellet_quantity @property def has_pellet_level(self) -> bool: """Return the availability of the pellet level.""" return ( self._state.has_leveltronic_pellet_sensor or self._state.has_capacitive_pellet_sensor ) @property def pellet_level(self) -> float: """Return the pellet level.""" return self._state.pellet_level @property def pellet_level_min(self) -> float: """Return the minimum pellet level.""" return self._state.pellet_level_min @property def pellet_level_max(self) -> float: """Return the maximum pellet level.""" return self._state.pellet_level_max @property def pellet_level_threshold(self) -> float: """Return the pellet level threshold.""" return self._state.pellet_level_threshold @property def is_on(self) -> bool: """Check if the stove is on.""" return self._state.is_on @property def is_heating(self) -> bool: """Check if the stove is currently heating.""" return self._state.is_heating @property def has_fan_silent(self) -> bool: """Check if the fan has the silent mode available.""" return self._state.has_fan_mode_silent @property def has_fan_high(self) -> bool: """Check if the fan has the high mode available.""" return self._state.has_fan_mode_high @property def has_fan_auto(self) -> bool: """Check if the fan has the auto mode available.""" return self._state.has_fan_mode_auto def has_fan(self, fan: FanType = FanType.MAIN) -> bool: """Check if a fan is available""" if fan == FanType.MAIN: return self._state.has_main_fan elif fan == FanType.LEFT: return self._state.has_left_fan elif fan == FanType.RIGHT: return self._state.has_right_fan return False @property def fan_speed_min(self) -> int: """DEPRRECATED - Return the minimum fan speed.""" return self._state.main_fan_min @property def fan_speed_max(self) -> int: """DEPRRECATED - Return the maximum fan speed.""" return self._state.main_fan_max def min_fan_speed(self, fan: FanType = FanType.MAIN) -> int: """Return the minimum fan speed.""" if fan == FanType.MAIN: return self._state.main_fan_min elif fan == FanType.LEFT: return self._state.left_fan_min elif fan == FanType.RIGHT: return self._state.right_fan_min return 0 def max_fan_speed(self, fan: FanType = FanType.MAIN) -> int: """Return the maximum fan speed.""" if fan == FanType.MAIN: return self._state.main_fan_max elif fan == FanType.LEFT: return self._state.left_fan_max elif fan == FanType.RIGHT: return self._state.right_fan_max return 1 @property def target_temperature_min(self) -> int: """Return the minimum target temperature.""" return self._state.target_temperature_min @property def target_temperature_max(self) -> int: """Return the maximum target temperature.""" return self._state.target_temperature_max async def set_target_temperature(self, temperature: int) -> bool: """Sets the target temperature.""" if ( temperature >= self._state.target_temperature_min and temperature <= self._state.target_temperature_max ): res = await self._execute_command( command=COMMAND_SET_TEMPERATURE, parameter=temperature, ) return self._state.merge_state(res) return False async def set_fan_silent(self) -> bool: """Set the fan to silent mode.""" return ( await self._execute_command( command=COMMAND_SET_FAN_SILENT, merge_state=True ) ).success async def set_fan_high(self) -> bool: """Set the fan to high mode.""" return await self.set_fan_speed(6) async def set_fan_auto(self) -> bool: """Set the fan to auto mode.""" return await self.set_fan_speed(7) async def set_fan_speed(self, fan_speed: int, fan: FanType = FanType.MAIN) -> bool: """Set the fan speed.""" if not self.has_fan(fan): raise ValidationError(f'Fan "{fan}" not available.') if ( (self.min_fan_speed(fan) <= fan_speed <= self.max_fan_speed(fan)) or ( fan == FanType.MAIN and fan_speed == 6 and self._state.has_fan_mode_high ) or ( fan == FanType.MAIN and fan_speed == 7 and self._state.has_fan_mode_auto ) ): if fan == FanType.LEFT: command = COMMAND_SET_LEFT_FAN_SPEED elif fan == FanType.RIGHT: command = COMMAND_SET_RIGHT_FAN_SPEED else: command = COMMAND_SET_MAIN_FAN_SPEED return ( await self._execute_command( command=command, parameter=fan_speed, merge_state=True, ) ).success raise ValidationError(f'Fan "{fan}" speed ({fan_speed}) out of range.') async def set_power_mode(self, power: int) -> bool: """Set the power mode.""" if 1 <= power <= 5: return ( await self._execute_command( command=COMMAND_SET_POWER_MODE, parameter=power, ) ).success raise ValidationError(f"Power mode ({power}) out of range.") async def set_on(self, on: bool) -> bool: """Set the stove on or off.""" if self._state.has_on_off_switch: return ( await self._execute_command( command=COMMAND_SET_ON if on else COMMAND_SET_OFF, ) ).success raise ValidationError("Main operation switch not available.") async def _execute_command( self, command: str, parameter: str | int | None = None, merge_state=True, ) -> _PalazzettiAPIData: request_url = API_COMMAND_URL_TEMPLATE.format( host=self._hostname, command_and_parameter=f"{command} {parameter}" if parameter is not None else command, ) try: async with self._session.get(request_url) as response: payload = _PalazzettiAPIData(await response.text()) except (TypeError, JSONDecodeError) as ex: self.connected = False raise CommunicationError("Invalid API response") from ex except aiohttp.ClientError as ex: self.connected = False raise CommunicationError("API communication error") from ex if merge_state: self._state.merge_state(payload) return payload def to_json(self, redact: bool = False) -> str: """Return a snapshot of the client as a json string.""" return json.dumps(self.to_dict(redact)) def to_dict( self, redact: bool = False, ) -> dict[str, bool | dict[str, str | bool | int | float | list[int | str]]]: """Return a snapshot of the client as a dict.""" data = { "host": self._hostname, "connected": self.connected, "state": self._state.to_dict(), } if redact: redacted = { "DNS": [REDACTED_DATA], "EADDR": REDACTED_DATA, "EGW": REDACTED_DATA, "EMAC": REDACTED_DATA, "GATEWAY": REDACTED_DATA, "MAC": REDACTED_DATA, "SN": REDACTED_DATA, "WADR": REDACTED_DATA, "WBCST": REDACTED_DATA, "WMAC": REDACTED_DATA, "WGW": REDACTED_DATA, "WSSID": REDACTED_DATA, } data["host"] = REDACTED_DATA data["state"]["properties"] = data["state"]["properties"] | redacted data["state"]["attributes"] = data["state"]["attributes"] | redacted return data