"""Shared device-state helpers for DKN Cloud NA.""" from __future__ import annotations from typing import Any from .const import ( DEVICE_MODE_AUTO, DEVICE_MODE_COOL, DEVICE_MODE_DRY, DEVICE_MODE_FAN, DEVICE_MODE_HEAT, SPEED_20, SPEED_40, SPEED_60, SPEED_80, SPEED_100, SPEED_AUTO, TEMP_FAHRENHEIT, ) def as_bool(value: Any) -> bool | None: """Return a boolean for a real boolean value, else None.""" if isinstance(value, bool): return value return None def as_int(value: Any) -> int | None: """Return an integer for int-like values, else None.""" if isinstance(value, bool): return int(value) if isinstance(value, int): return value return None def to_celsius(value: Any, units: Any) -> float | None: """Convert a device temperature to Celsius if needed.""" if value is None: return None temp = float(value) if as_int(units) != TEMP_FAHRENHEIT: return temp return round((temp - 32) * 5 / 9, 1) def to_device_temperature(value_c: float, units: Any) -> float | int: """Convert a Celsius temperature to the device units.""" if as_int(units) != TEMP_FAHRENHEIT: return value_c return round((value_c * 9 / 5) + 32) def requested_mode(data: dict[str, Any]) -> int | None: """Return the requested device mode.""" return as_int(data.get("mode")) def live_mode(data: dict[str, Any]) -> int | None: """Return the live device mode, when reported.""" return as_int(data.get("real_mode")) def current_temperature(data: dict[str, Any]) -> float | None: """Return the indoor temperature in Celsius.""" return to_celsius(data.get("work_temp", data.get("local_temp")), data.get("units")) def exterior_temperature(data: dict[str, Any]) -> float | None: """Return the exterior temperature in Celsius.""" return to_celsius(data.get("ext_temp"), data.get("units")) def target_temperature_key(mode: int | None) -> str | None: """Return the setpoint key for the requested mode.""" if mode == DEVICE_MODE_HEAT: return "setpoint_air_heat" if mode == DEVICE_MODE_COOL: return "setpoint_air_cool" if mode == DEVICE_MODE_AUTO: return "setpoint_air_auto" return None def target_temperature(data: dict[str, Any], mode: int | None = None) -> float | None: """Return the target temperature in Celsius for ``mode``. ``mode`` defaults to the device's requested mode. Callers that already know the effective mode (for example while an optimistic mode change is still propagating) should pass it so the read uses the same setpoint key the write used. """ key = target_temperature_key(mode if mode is not None else requested_mode(data)) if key is None: return None return to_celsius(data.get(key), data.get("units")) def writable_target_temperature_key( data: dict[str, Any], mode: int | None = None ) -> str | None: """Return the setpoint key to write for ``mode``. ``mode`` is the mode the write is intended for and takes priority over anything in ``data``, which may still describe the pre-change state while a mode change propagates through the cloud. The device-reported modes are only consulted as a fallback for payloads that do not expose the requested mode's setpoint at all. """ candidates: list[str] = [] preference = (mode, live_mode(data), requested_mode(data), DEVICE_MODE_AUTO) for candidate_mode in preference: key = target_temperature_key(candidate_mode) if key is not None and key not in candidates: candidates.append(key) for key in candidates: if data.get(key) is not None: return key return candidates[0] if candidates else None def inferred_hvac_action( data: dict[str, Any], mode: int | None = None, target: float | None = None, power: bool | None = None, ) -> str: """Infer the active HVAC action from the effective mode and temperatures. ``mode``, ``target`` and ``power`` let callers supply the effective (possibly still optimistic) values so the reported action matches the reported mode. """ if power is None: power = as_bool(data.get("power")) if not power: return "off" if mode is None: mode = requested_mode(data) current = current_temperature(data) if target is None: target = target_temperature(data, mode) if mode == DEVICE_MODE_HEAT: if current is not None and target is not None and current < target: return "heating" return "idle" if mode == DEVICE_MODE_COOL: if current is not None and target is not None and current > target: return "cooling" return "idle" if mode == DEVICE_MODE_AUTO: if current is not None and target is not None: if current < target: return "heating" if current > target: return "cooling" return "idle" if mode == DEVICE_MODE_FAN: return "fan" if mode == DEVICE_MODE_DRY: return "drying" return "on" def available_fan_speeds(data: dict[str, Any]) -> list[int]: """Return the supported fan speed codes for the device.""" raw = data.get("speed_available") if isinstance(raw, list): return [speed for speed in (as_int(item) for item in raw) if speed is not None] return [SPEED_AUTO, SPEED_20, SPEED_40, SPEED_60, SPEED_80, SPEED_100] def fan_mode_labels(data: dict[str, Any]) -> list[str]: """Return the supported HA fan mode labels for the device.""" mapping = { SPEED_AUTO: "auto", SPEED_20: "20%", SPEED_40: "40%", SPEED_60: "60%", SPEED_80: "80%", SPEED_100: "100%", } labels: list[str] = [] for speed in available_fan_speeds(data): label = mapping.get(speed) if label is not None: labels.append(label) return labels def supports_swing(data: dict[str, Any]) -> bool: """Return whether the device appears to support vertical swing control.""" return "slats_vertical_1" in data or as_int(data.get("slats_vnum")) not in (None, 0)