171 lines
5.1 KiB
Python
171 lines
5.1 KiB
Python
"""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]) -> float | None:
|
|
"""Return the requested target temperature in Celsius."""
|
|
key = target_temperature_key(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]) -> str | None:
|
|
"""Return the setpoint key most likely to be writable for the current state."""
|
|
candidates: list[str] = []
|
|
for mode in (live_mode(data), requested_mode(data), DEVICE_MODE_AUTO):
|
|
key = target_temperature_key(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]) -> str:
|
|
"""Infer the active HVAC action from requested mode and temperatures."""
|
|
power = as_bool(data.get("power"))
|
|
if not power:
|
|
return "off"
|
|
|
|
mode = requested_mode(data)
|
|
current = current_temperature(data)
|
|
target = target_temperature(data)
|
|
|
|
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)
|