Implement write actions
This commit is contained in:
@@ -190,6 +190,31 @@ class DknCloudNaClient:
|
||||
async with self._socket_lock:
|
||||
await self._disconnect_socket_locked()
|
||||
|
||||
async def async_send_machine_event(
|
||||
self,
|
||||
installation_id: str,
|
||||
mac: str,
|
||||
property_name: str,
|
||||
value: Any,
|
||||
) -> None:
|
||||
"""Send a device control event over Socket.IO."""
|
||||
async with self._socket_lock:
|
||||
socket = self._socket
|
||||
namespace = self._installation_namespace(installation_id)
|
||||
if socket is None or not socket.connected:
|
||||
raise DknConnectionError("Socket not connected")
|
||||
if namespace not in socket.namespaces:
|
||||
raise DknConnectionError(
|
||||
f"Socket namespace unavailable for installation {installation_id}"
|
||||
)
|
||||
|
||||
payload = {"mac": mac, "property": property_name, "value": value}
|
||||
LOGGER.debug("DKN socket send %s %s", namespace, payload)
|
||||
try:
|
||||
await socket.emit("create-machine-event", payload, namespace=namespace)
|
||||
except Exception as err: # noqa: BLE001
|
||||
raise DknConnectionError(str(err) or type(err).__name__) from err
|
||||
|
||||
async def _disconnect_socket_locked(self) -> None:
|
||||
"""Disconnect the Socket.IO client while holding the socket lock."""
|
||||
socket = self._socket
|
||||
|
||||
@@ -9,10 +9,12 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import PRECISION_WHOLE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from .const import (
|
||||
DEVICE_MODE_AUTO,
|
||||
@@ -21,18 +23,17 @@ from .const import (
|
||||
DEVICE_MODE_FAN,
|
||||
DEVICE_MODE_HEAT,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
SPEED_20,
|
||||
SPEED_40,
|
||||
SPEED_60,
|
||||
SPEED_80,
|
||||
SPEED_100,
|
||||
SPEED_AUTO,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from .coordinator import DknCoordinator
|
||||
from .entity import DknEntity
|
||||
|
||||
# Map DKN device mode integers to HA HVACMode
|
||||
_MODE_TO_HVAC: dict[int, HVACMode] = {
|
||||
DEVICE_MODE_AUTO: HVACMode.AUTO,
|
||||
DEVICE_MODE_COOL: HVACMode.COOL,
|
||||
@@ -42,7 +43,6 @@ _MODE_TO_HVAC: dict[int, HVACMode] = {
|
||||
}
|
||||
_HVAC_TO_MODE: dict[HVACMode, int] = {v: k for k, v in _MODE_TO_HVAC.items()}
|
||||
|
||||
# Fan speed labels
|
||||
_FAN_MODES = ["auto", "20%", "40%", "60%", "80%", "100%"]
|
||||
_SPEED_TO_FAN: dict[int, str] = {
|
||||
SPEED_AUTO: "auto",
|
||||
@@ -54,7 +54,6 @@ _SPEED_TO_FAN: dict[int, str] = {
|
||||
}
|
||||
_FAN_TO_SPEED: dict[str, int] = {v: k for k, v in _SPEED_TO_FAN.items()}
|
||||
|
||||
# Modes where a temperature target makes no sense
|
||||
_NO_TARGET_TEMP_MODES = {HVACMode.FAN_ONLY, HVACMode.DRY, HVACMode.OFF}
|
||||
|
||||
|
||||
@@ -73,7 +72,7 @@ async def async_setup_entry(
|
||||
class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
"""Climate entity representing one DKN Cloud NA AC unit."""
|
||||
|
||||
_attr_name = None # entity name = device name
|
||||
_attr_name = None
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_precision = PRECISION_WHOLE
|
||||
_attr_hvac_modes = [
|
||||
@@ -98,40 +97,51 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
def supported_features(self) -> ClimateEntityFeature:
|
||||
"""Return feature flags appropriate for the current HVAC mode."""
|
||||
features = ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
||||
mode = self.hvac_mode
|
||||
if mode not in _NO_TARGET_TEMP_MODES:
|
||||
if self.hvac_mode not in _NO_TARGET_TEMP_MODES:
|
||||
features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
features |= ClimateEntityFeature.FAN_MODE
|
||||
features |= ClimateEntityFeature.SWING_MODE
|
||||
return features
|
||||
return (
|
||||
features | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.SWING_MODE
|
||||
)
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
data = self._device_data
|
||||
if not data.get("power", False):
|
||||
power = self._optimistic_get("power", data.get("power", False))
|
||||
if not power:
|
||||
return HVACMode.OFF
|
||||
optimistic_mode = self._optimistic_get("hvac_mode", None)
|
||||
if optimistic_mode is not None:
|
||||
return optimistic_mode
|
||||
mode_int = data.get("real_mode") or data.get("mode", DEVICE_MODE_AUTO)
|
||||
return _MODE_TO_HVAC.get(int(mode_int), HVACMode.AUTO)
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
return self._device_data.get("work_temp")
|
||||
value = self._device_data.get("work_temp")
|
||||
if value is None:
|
||||
return None
|
||||
return self._from_device_temperature(float(value))
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
if self.hvac_mode in _NO_TARGET_TEMP_MODES:
|
||||
mode = self.hvac_mode
|
||||
if mode in _NO_TARGET_TEMP_MODES:
|
||||
return None
|
||||
data = self._device_data
|
||||
mode = data.get("real_mode") or data.get("mode", DEVICE_MODE_AUTO)
|
||||
if int(mode) == DEVICE_MODE_HEAT:
|
||||
return self._optimistic_get("target_temp", data.get("setpoint_air_heat"))
|
||||
if int(mode) == DEVICE_MODE_COOL:
|
||||
return self._optimistic_get("target_temp", data.get("setpoint_air_cool"))
|
||||
return self._optimistic_get("target_temp", data.get("setpoint_air_auto"))
|
||||
if mode == HVACMode.HEAT:
|
||||
value = data.get("setpoint_air_heat")
|
||||
elif mode == HVACMode.COOL:
|
||||
value = data.get("setpoint_air_cool")
|
||||
else:
|
||||
value = data.get("setpoint_air_auto")
|
||||
if value is None:
|
||||
return None
|
||||
fallback = self._from_device_temperature(float(value))
|
||||
return self._optimistic_get("target_temp", fallback)
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
return None # no range mode
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> float | None:
|
||||
@@ -147,28 +157,141 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
slat = self._device_data.get("slats_vertical_1", 0)
|
||||
return self._optimistic_get("swing_mode", "swing" if int(slat) == 9 else "off")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Service handlers — all stub (raise NotImplementedError for now)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
LOGGER.debug("set_hvac_mode(%s) stub for %s", hvac_mode, self._mac)
|
||||
raise NotImplementedError("Device control not yet implemented")
|
||||
installation_id = self._installation_id
|
||||
async with self._get_device_lock():
|
||||
try:
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self.coordinator.client.async_send_machine_event(
|
||||
installation_id, self._mac, "power", False
|
||||
)
|
||||
self._optimistic_set("power", False)
|
||||
self._optimistic_set("hvac_mode", HVACMode.OFF)
|
||||
else:
|
||||
mode = _HVAC_TO_MODE.get(hvac_mode)
|
||||
if mode is None:
|
||||
raise HomeAssistantError(f"Unsupported HVAC mode: {hvac_mode}")
|
||||
await self.coordinator.client.async_send_machine_event(
|
||||
installation_id, self._mac, "power", True
|
||||
)
|
||||
await self.coordinator.client.async_send_machine_event(
|
||||
installation_id, self._mac, "mode", mode
|
||||
)
|
||||
self._optimistic_set("power", True)
|
||||
self._optimistic_set("hvac_mode", hvac_mode)
|
||||
except Exception as err: # noqa: BLE001
|
||||
raise HomeAssistantError(f"Failed to set HVAC mode: {err}") from err
|
||||
|
||||
self._schedule_refresh()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
LOGGER.debug("set_temperature(%s) stub for %s", kwargs, self._mac)
|
||||
raise NotImplementedError("Device control not yet implemented")
|
||||
hvac_mode = kwargs.get("hvac_mode")
|
||||
if hvac_mode is not None:
|
||||
await self.async_set_hvac_mode(hvac_mode)
|
||||
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
target_mode = hvac_mode or self.hvac_mode
|
||||
if target_mode in _NO_TARGET_TEMP_MODES:
|
||||
raise HomeAssistantError(
|
||||
f"Target temperature is not supported in {target_mode} mode"
|
||||
)
|
||||
|
||||
installation_id = self._installation_id
|
||||
property_name = self._temperature_property_for_mode(target_mode)
|
||||
device_temp = self._to_device_temperature(float(temperature))
|
||||
|
||||
async with self._get_device_lock():
|
||||
try:
|
||||
await self.coordinator.client.async_send_machine_event(
|
||||
installation_id, self._mac, property_name, device_temp
|
||||
)
|
||||
except Exception as err: # noqa: BLE001
|
||||
raise HomeAssistantError(f"Failed to set temperature: {err}") from err
|
||||
|
||||
self._optimistic_set("target_temp", float(temperature))
|
||||
self._schedule_refresh()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
LOGGER.debug("set_fan_mode(%s) stub for %s", fan_mode, self._mac)
|
||||
raise NotImplementedError("Device control not yet implemented")
|
||||
speed = _FAN_TO_SPEED.get(fan_mode)
|
||||
if speed is None:
|
||||
raise HomeAssistantError(f"Unsupported fan mode: {fan_mode}")
|
||||
|
||||
installation_id = self._installation_id
|
||||
async with self._get_device_lock():
|
||||
try:
|
||||
await self.coordinator.client.async_send_machine_event(
|
||||
installation_id, self._mac, "speed_state", speed
|
||||
)
|
||||
except Exception as err: # noqa: BLE001
|
||||
raise HomeAssistantError(f"Failed to set fan mode: {err}") from err
|
||||
|
||||
self._optimistic_set("fan_mode", fan_mode)
|
||||
self._schedule_refresh()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
LOGGER.debug("set_swing_mode(%s) stub for %s", swing_mode, self._mac)
|
||||
raise NotImplementedError("Device control not yet implemented")
|
||||
if swing_mode not in {"off", "swing"}:
|
||||
raise HomeAssistantError(f"Unsupported swing mode: {swing_mode}")
|
||||
|
||||
installation_id = self._installation_id
|
||||
slat = 9 if swing_mode == "swing" else 0
|
||||
async with self._get_device_lock():
|
||||
try:
|
||||
await self.coordinator.client.async_send_machine_event(
|
||||
installation_id, self._mac, "slats_vertical_1", slat
|
||||
)
|
||||
except Exception as err: # noqa: BLE001
|
||||
raise HomeAssistantError(f"Failed to set swing mode: {err}") from err
|
||||
|
||||
self._optimistic_set("swing_mode", swing_mode)
|
||||
self._schedule_refresh()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
await self.async_set_hvac_mode(HVACMode.AUTO)
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
@property
|
||||
def _installation_id(self) -> str:
|
||||
installation_id = str(self._device_data.get("_installation_id") or "").strip()
|
||||
if not installation_id:
|
||||
raise HomeAssistantError("Missing installation id for device")
|
||||
return installation_id
|
||||
|
||||
def _temperature_property_for_mode(self, hvac_mode: HVACMode) -> str:
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
return "setpoint_air_heat"
|
||||
if hvac_mode == HVACMode.COOL:
|
||||
return "setpoint_air_cool"
|
||||
if hvac_mode not in {HVACMode.AUTO, HVACMode.HEAT, HVACMode.COOL}:
|
||||
raise HomeAssistantError(
|
||||
f"Target temperature is not supported in {hvac_mode} mode"
|
||||
)
|
||||
return "setpoint_air_auto"
|
||||
|
||||
def _to_device_temperature(self, temperature_c: float) -> float | int:
|
||||
if int(self._device_data.get("units", 0)) != TEMP_FAHRENHEIT:
|
||||
return temperature_c
|
||||
fahrenheit = TemperatureConverter.convert(
|
||||
temperature_c,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
)
|
||||
return round(fahrenheit)
|
||||
|
||||
def _from_device_temperature(self, value: float) -> float:
|
||||
if int(self._device_data.get("units", 0)) != TEMP_FAHRENHEIT:
|
||||
return value
|
||||
celsius = TemperatureConverter.convert(
|
||||
value,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
)
|
||||
return round(celsius, 1)
|
||||
|
||||
Reference in New Issue
Block a user