Dynamic fan speeds
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections import deque
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -62,6 +63,8 @@ class DknCloudNaClient:
|
|||||||
Callable[[str, dict[str, Any]], Awaitable[None]] | None
|
Callable[[str, dict[str, Any]], Awaitable[None]] | None
|
||||||
) = None
|
) = None
|
||||||
self._socket_refresh_callback: Callable[[], Awaitable[None]] | None = None
|
self._socket_refresh_callback: Callable[[], Awaitable[None]] | None = None
|
||||||
|
self._last_command_ack: dict[str, Any] | None = None
|
||||||
|
self._recent_socket_events: deque[dict[str, Any]] = deque(maxlen=20)
|
||||||
|
|
||||||
def clear_password(self) -> None:
|
def clear_password(self) -> None:
|
||||||
"""Discard password from memory after token exchange."""
|
"""Discard password from memory after token exchange."""
|
||||||
@@ -241,10 +244,27 @@ class DknCloudNaClient:
|
|||||||
namespace=namespace,
|
namespace=namespace,
|
||||||
timeout=REQUEST_TIMEOUT,
|
timeout=REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
|
self._last_command_ack = {
|
||||||
|
"namespace": namespace,
|
||||||
|
"payload": payload,
|
||||||
|
"ack": ack,
|
||||||
|
}
|
||||||
LOGGER.debug("DKN socket ack %s %s", namespace, ack)
|
LOGGER.debug("DKN socket ack %s %s", namespace, ack)
|
||||||
except Exception as err: # noqa: BLE001
|
except Exception as err: # noqa: BLE001
|
||||||
raise DknConnectionError(str(err) or type(err).__name__) from err
|
raise DknConnectionError(str(err) or type(err).__name__) from err
|
||||||
|
|
||||||
|
def pop_last_command_debug(self) -> dict[str, Any] | None:
|
||||||
|
"""Return and clear the latest command ack plus recent socket events."""
|
||||||
|
if self._last_command_ack is None and not self._recent_socket_events:
|
||||||
|
return None
|
||||||
|
debug = {
|
||||||
|
"last_command_ack": self._last_command_ack,
|
||||||
|
"recent_socket_events": list(self._recent_socket_events),
|
||||||
|
}
|
||||||
|
self._last_command_ack = None
|
||||||
|
self._recent_socket_events.clear()
|
||||||
|
return debug
|
||||||
|
|
||||||
async def _disconnect_socket_locked(self) -> None:
|
async def _disconnect_socket_locked(self) -> None:
|
||||||
"""Disconnect the Socket.IO client while holding the socket lock."""
|
"""Disconnect the Socket.IO client while holding the socket lock."""
|
||||||
socket = self._socket
|
socket = self._socket
|
||||||
@@ -281,14 +301,19 @@ class DknCloudNaClient:
|
|||||||
|
|
||||||
@sio.on("control-new-device", namespace=API_USERS_NAMESPACE)
|
@sio.on("control-new-device", namespace=API_USERS_NAMESPACE)
|
||||||
async def _on_new_device(_: Any) -> None:
|
async def _on_new_device(_: Any) -> None:
|
||||||
|
self._record_socket_event(API_USERS_NAMESPACE, "control-new-device", _)
|
||||||
await self._request_socket_refresh()
|
await self._request_socket_refresh()
|
||||||
|
|
||||||
@sio.on("control-deleted-device", namespace=API_USERS_NAMESPACE)
|
@sio.on("control-deleted-device", namespace=API_USERS_NAMESPACE)
|
||||||
async def _on_deleted_device(_: Any) -> None:
|
async def _on_deleted_device(_: Any) -> None:
|
||||||
|
self._record_socket_event(API_USERS_NAMESPACE, "control-deleted-device", _)
|
||||||
await self._request_socket_refresh()
|
await self._request_socket_refresh()
|
||||||
|
|
||||||
@sio.on("control-deleted-installation", namespace=API_USERS_NAMESPACE)
|
@sio.on("control-deleted-installation", namespace=API_USERS_NAMESPACE)
|
||||||
async def _on_deleted_installation(_: Any) -> None:
|
async def _on_deleted_installation(_: Any) -> None:
|
||||||
|
self._record_socket_event(
|
||||||
|
API_USERS_NAMESPACE, "control-deleted-installation", _
|
||||||
|
)
|
||||||
await self._request_socket_refresh()
|
await self._request_socket_refresh()
|
||||||
|
|
||||||
for installation_id in installation_ids:
|
for installation_id in installation_ids:
|
||||||
@@ -298,6 +323,7 @@ class DknCloudNaClient:
|
|||||||
async def _on_device_data(
|
async def _on_device_data(
|
||||||
message: Any, *, _namespace: str = namespace
|
message: Any, *, _namespace: str = namespace
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self._record_socket_event(_namespace, "device-data", message)
|
||||||
if not isinstance(message, dict):
|
if not isinstance(message, dict):
|
||||||
return
|
return
|
||||||
mac = str(message.get("mac") or "").strip().lower()
|
mac = str(message.get("mac") or "").strip().lower()
|
||||||
@@ -326,6 +352,12 @@ class DknCloudNaClient:
|
|||||||
"""Return the Socket.IO namespace for one installation."""
|
"""Return the Socket.IO namespace for one installation."""
|
||||||
return f"/{installation_id}::dknUsa"
|
return f"/{installation_id}::dknUsa"
|
||||||
|
|
||||||
|
def _record_socket_event(self, namespace: str, event: str, payload: Any) -> None:
|
||||||
|
"""Track recent socket events for write debugging."""
|
||||||
|
self._recent_socket_events.append(
|
||||||
|
{"namespace": namespace, "event": event, "payload": payload}
|
||||||
|
)
|
||||||
|
|
||||||
async def _request(
|
async def _request(
|
||||||
self,
|
self,
|
||||||
method: str,
|
method: str,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from .const import (
|
|||||||
from .model import (
|
from .model import (
|
||||||
available_fan_speeds,
|
available_fan_speeds,
|
||||||
current_temperature as model_current_temperature,
|
current_temperature as model_current_temperature,
|
||||||
|
fan_mode_labels,
|
||||||
inferred_hvac_action,
|
inferred_hvac_action,
|
||||||
requested_mode,
|
requested_mode,
|
||||||
supports_swing,
|
supports_swing,
|
||||||
@@ -52,7 +53,6 @@ _MODE_TO_HVAC: dict[int, HVACMode] = {
|
|||||||
}
|
}
|
||||||
_HVAC_TO_MODE: dict[HVACMode, int] = {v: k for k, v in _MODE_TO_HVAC.items()}
|
_HVAC_TO_MODE: dict[HVACMode, int] = {v: k for k, v in _MODE_TO_HVAC.items()}
|
||||||
|
|
||||||
_FAN_MODES = ["auto", "20%", "40%", "60%", "80%", "100%"]
|
|
||||||
_SPEED_TO_FAN: dict[int, str] = {
|
_SPEED_TO_FAN: dict[int, str] = {
|
||||||
SPEED_AUTO: "auto",
|
SPEED_AUTO: "auto",
|
||||||
SPEED_20: "20%",
|
SPEED_20: "20%",
|
||||||
@@ -92,7 +92,6 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
|||||||
HVACMode.DRY,
|
HVACMode.DRY,
|
||||||
HVACMode.FAN_ONLY,
|
HVACMode.FAN_ONLY,
|
||||||
]
|
]
|
||||||
_attr_fan_modes = _FAN_MODES
|
|
||||||
_attr_swing_modes = ["off", "swing"]
|
_attr_swing_modes = ["off", "swing"]
|
||||||
_attr_min_temp = 16
|
_attr_min_temp = 16
|
||||||
_attr_max_temp = 32
|
_attr_max_temp = 32
|
||||||
@@ -114,6 +113,11 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
|||||||
features |= ClimateEntityFeature.SWING_MODE
|
features |= ClimateEntityFeature.SWING_MODE
|
||||||
return features
|
return features
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_modes(self) -> list[str] | None:
|
||||||
|
labels = fan_mode_labels(self._device_data)
|
||||||
|
return labels or None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self) -> HVACMode:
|
def hvac_mode(self) -> HVACMode:
|
||||||
data = self._device_data
|
data = self._device_data
|
||||||
@@ -186,16 +190,10 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
|||||||
)
|
)
|
||||||
self._optimistic_set("power", False)
|
self._optimistic_set("power", False)
|
||||||
self._optimistic_set("hvac_mode", HVACMode.OFF)
|
self._optimistic_set("hvac_mode", HVACMode.OFF)
|
||||||
|
await self._wait_for_device_value("power", False)
|
||||||
else:
|
else:
|
||||||
mode = _HVAC_TO_MODE.get(hvac_mode)
|
await self._ensure_power_on()
|
||||||
if mode is None:
|
await self._ensure_mode_synced(hvac_mode)
|
||||||
raise HomeAssistantError(f"Unsupported HVAC mode: {hvac_mode}")
|
|
||||||
await self.coordinator.client.async_send_machine_event(
|
|
||||||
installation_id, self._command_mac, "power", True
|
|
||||||
)
|
|
||||||
await self.coordinator.client.async_send_machine_event(
|
|
||||||
installation_id, self._command_mac, "mode", mode
|
|
||||||
)
|
|
||||||
self._optimistic_set("power", True)
|
self._optimistic_set("power", True)
|
||||||
self._optimistic_set("hvac_mode", hvac_mode)
|
self._optimistic_set("hvac_mode", hvac_mode)
|
||||||
except Exception as err: # noqa: BLE001
|
except Exception as err: # noqa: BLE001
|
||||||
@@ -225,9 +223,23 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
|||||||
|
|
||||||
async with self._get_device_lock():
|
async with self._get_device_lock():
|
||||||
try:
|
try:
|
||||||
|
await self._ensure_power_on()
|
||||||
|
await self._ensure_mode_synced(target_mode)
|
||||||
await self.coordinator.client.async_send_machine_event(
|
await self.coordinator.client.async_send_machine_event(
|
||||||
installation_id, self._command_mac, property_name, device_temp
|
installation_id, self._command_mac, property_name, device_temp
|
||||||
)
|
)
|
||||||
|
if not await self._wait_for_device_value(property_name, device_temp):
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
await self._ensure_mode_synced(target_mode)
|
||||||
|
await self.coordinator.client.async_send_machine_event(
|
||||||
|
installation_id, self._command_mac, property_name, device_temp
|
||||||
|
)
|
||||||
|
if not await self._wait_for_device_value(
|
||||||
|
property_name, device_temp
|
||||||
|
):
|
||||||
|
raise HomeAssistantError(
|
||||||
|
f"Device temperature did not sync to {temperature}"
|
||||||
|
)
|
||||||
except Exception as err: # noqa: BLE001
|
except Exception as err: # noqa: BLE001
|
||||||
raise HomeAssistantError(f"Failed to set temperature: {err}") from err
|
raise HomeAssistantError(f"Failed to set temperature: {err}") from err
|
||||||
|
|
||||||
@@ -245,6 +257,7 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
|||||||
installation_id = self._installation_id
|
installation_id = self._installation_id
|
||||||
async with self._get_device_lock():
|
async with self._get_device_lock():
|
||||||
try:
|
try:
|
||||||
|
await self._ensure_power_on()
|
||||||
await self.coordinator.client.async_send_machine_event(
|
await self.coordinator.client.async_send_machine_event(
|
||||||
installation_id, self._command_mac, "speed_state", speed
|
installation_id, self._command_mac, "speed_state", speed
|
||||||
)
|
)
|
||||||
@@ -265,6 +278,7 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
|||||||
slat = 9 if swing_mode == "swing" else 0
|
slat = 9 if swing_mode == "swing" else 0
|
||||||
async with self._get_device_lock():
|
async with self._get_device_lock():
|
||||||
try:
|
try:
|
||||||
|
await self._ensure_power_on()
|
||||||
await self.coordinator.client.async_send_machine_event(
|
await self.coordinator.client.async_send_machine_event(
|
||||||
installation_id, self._command_mac, "slats_vertical_1", slat
|
installation_id, self._command_mac, "slats_vertical_1", slat
|
||||||
)
|
)
|
||||||
@@ -307,3 +321,46 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
|||||||
|
|
||||||
def _to_device_temperature(self, temperature_c: float) -> float | int:
|
def _to_device_temperature(self, temperature_c: float) -> float | int:
|
||||||
return to_device_temperature(temperature_c, self._device_data.get("units"))
|
return to_device_temperature(temperature_c, self._device_data.get("units"))
|
||||||
|
|
||||||
|
async def _ensure_mode_synced(self, hvac_mode: HVACMode) -> int:
|
||||||
|
"""Ensure the device mode is synced before dependent writes."""
|
||||||
|
mode = _HVAC_TO_MODE.get(hvac_mode)
|
||||||
|
if mode is None:
|
||||||
|
raise HomeAssistantError(f"Unsupported HVAC mode: {hvac_mode}")
|
||||||
|
|
||||||
|
await self._ensure_power_on()
|
||||||
|
|
||||||
|
if self._device_data.get("mode") == mode:
|
||||||
|
return mode
|
||||||
|
|
||||||
|
installation_id = self._installation_id
|
||||||
|
await self.coordinator.client.async_send_machine_event(
|
||||||
|
installation_id, self._command_mac, "mode", mode
|
||||||
|
)
|
||||||
|
self._optimistic_set("hvac_mode", hvac_mode)
|
||||||
|
if not await self._wait_for_device_value("mode", mode):
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
await self.coordinator.client.async_send_machine_event(
|
||||||
|
installation_id, self._command_mac, "mode", mode
|
||||||
|
)
|
||||||
|
if not await self._wait_for_device_value("mode", mode):
|
||||||
|
raise HomeAssistantError(f"Device mode did not sync to {hvac_mode}")
|
||||||
|
return mode
|
||||||
|
|
||||||
|
async def _ensure_power_on(self) -> None:
|
||||||
|
"""Ensure the device is powered on before sending dependent commands."""
|
||||||
|
if self._device_data.get("power") is True:
|
||||||
|
return
|
||||||
|
|
||||||
|
installation_id = self._installation_id
|
||||||
|
await self.coordinator.client.async_send_machine_event(
|
||||||
|
installation_id, self._command_mac, "power", True
|
||||||
|
)
|
||||||
|
self._optimistic_set("power", True)
|
||||||
|
if not await self._wait_for_device_value("power", True):
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
await self.coordinator.client.async_send_machine_event(
|
||||||
|
installation_id, self._command_mac, "power", True
|
||||||
|
)
|
||||||
|
if not await self._wait_for_device_value("power", True):
|
||||||
|
raise HomeAssistantError("Device power did not sync to on")
|
||||||
|
|||||||
@@ -125,3 +125,22 @@ class DknEntity(CoordinatorEntity[DknCoordinator]):
|
|||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
bucket["pending_refresh"] = self.hass.async_create_task(_do_refresh())
|
bucket["pending_refresh"] = self.hass.async_create_task(_do_refresh())
|
||||||
|
|
||||||
|
async def _wait_for_device_value(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
expected: Any,
|
||||||
|
timeout: float = 10.0,
|
||||||
|
interval: float = 0.5,
|
||||||
|
) -> bool:
|
||||||
|
"""Wait for a device property to match the expected value."""
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
next_refresh = time.monotonic() + 2.0
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
if self._device_data.get(key) == expected:
|
||||||
|
return True
|
||||||
|
if time.monotonic() >= next_refresh:
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
next_refresh = time.monotonic() + 2.0
|
||||||
|
await asyncio.sleep(interval)
|
||||||
|
return self._device_data.get(key) == expected
|
||||||
|
|||||||
@@ -132,6 +132,24 @@ def available_fan_speeds(data: dict[str, Any]) -> list[int]:
|
|||||||
return [SPEED_AUTO, SPEED_20, SPEED_40, SPEED_60, SPEED_80, SPEED_100]
|
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:
|
def supports_swing(data: dict[str, Any]) -> bool:
|
||||||
"""Return whether the device appears to support vertical swing control."""
|
"""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)
|
return "slats_vertical_1" in data or as_int(data.get("slats_vnum")) not in (None, 0)
|
||||||
|
|||||||
+118
-1
@@ -75,6 +75,17 @@ def _next_fan_speed(current: int) -> int:
|
|||||||
return speeds[(speeds.index(current) + 1) % len(speeds)]
|
return speeds[(speeds.index(current) + 1) % len(speeds)]
|
||||||
|
|
||||||
|
|
||||||
|
def _next_available_fan_speed(device: dict[str, Any], current: int) -> int:
|
||||||
|
available = device.get("speed_available")
|
||||||
|
if isinstance(available, list):
|
||||||
|
speeds = [int(value) for value in available if isinstance(value, int)]
|
||||||
|
if current in speeds and len(speeds) > 1:
|
||||||
|
return speeds[(speeds.index(current) + 1) % len(speeds)]
|
||||||
|
if speeds:
|
||||||
|
return speeds[0]
|
||||||
|
return _next_fan_speed(current)
|
||||||
|
|
||||||
|
|
||||||
def _bool_or_none(value: Any) -> bool | None:
|
def _bool_or_none(value: Any) -> bool | None:
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
return value
|
return value
|
||||||
@@ -208,6 +219,7 @@ async def _main() -> None:
|
|||||||
results: dict[str, Any] = {
|
results: dict[str, Any] = {
|
||||||
"login": False,
|
"login": False,
|
||||||
"discovery": None,
|
"discovery": None,
|
||||||
|
"probes": {},
|
||||||
"socket_connect": False,
|
"socket_connect": False,
|
||||||
"write_debug": {},
|
"write_debug": {},
|
||||||
"writes": {},
|
"writes": {},
|
||||||
@@ -348,6 +360,25 @@ async def _main() -> None:
|
|||||||
installation_id, command_mac, property_name, value
|
installation_id, command_mac, property_name, value
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def ensure_mode(mode_value: int, timeout: float = 20.0) -> bool:
|
||||||
|
if current_device.get("mode") == mode_value:
|
||||||
|
return True
|
||||||
|
await send("mode", mode_value)
|
||||||
|
return await wait_for_property("mode", mode_value, timeout=timeout)
|
||||||
|
|
||||||
|
async def ensure_power_on(timeout: float = 20.0) -> bool:
|
||||||
|
if current_device.get("power") is True:
|
||||||
|
return True
|
||||||
|
await send("power", True)
|
||||||
|
return await wait_for_property("power", True, timeout=timeout)
|
||||||
|
|
||||||
|
async def recover_known_good_state() -> dict[str, Any]:
|
||||||
|
recovery = {"power_on": False, "heat_mode": False}
|
||||||
|
recovery["power_on"] = await ensure_power_on()
|
||||||
|
if recovery["power_on"]:
|
||||||
|
recovery["heat_mode"] = await ensure_mode(3)
|
||||||
|
return recovery
|
||||||
|
|
||||||
async def send_and_verify(
|
async def send_and_verify(
|
||||||
property_name: str, value: Any, timeout: float = 20.0
|
property_name: str, value: Any, timeout: float = 20.0
|
||||||
) -> bool | str:
|
) -> bool | str:
|
||||||
@@ -357,17 +388,99 @@ async def _main() -> None:
|
|||||||
await send(property_name, value)
|
await send(property_name, value)
|
||||||
verified = await wait_for_property(property_name, value, timeout=timeout)
|
verified = await wait_for_property(property_name, value, timeout=timeout)
|
||||||
refreshed = await fetch_current_device()
|
refreshed = await fetch_current_device()
|
||||||
|
command_debug = client.pop_last_command_debug() or {}
|
||||||
results["write_debug"][property_name] = {
|
results["write_debug"][property_name] = {
|
||||||
"requested": value,
|
"requested": value,
|
||||||
"before": baseline,
|
"before": baseline,
|
||||||
"after": refreshed.get(property_name),
|
"after": refreshed.get(property_name),
|
||||||
"acknowledged": True,
|
"acknowledged": True,
|
||||||
"socket_deltas": await collect_socket_deltas(),
|
"socket_deltas": await collect_socket_deltas(),
|
||||||
|
**command_debug,
|
||||||
}
|
}
|
||||||
return verified
|
return verified
|
||||||
|
|
||||||
|
async def probe_setpoint_keys() -> dict[str, Any]:
|
||||||
|
mode = int(current_device.get("mode", 1) or 1)
|
||||||
|
original_values = {
|
||||||
|
key: current_device.get(key)
|
||||||
|
for key in (
|
||||||
|
"setpoint_air_auto",
|
||||||
|
"setpoint_air_cool",
|
||||||
|
"setpoint_air_heat",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
probe_results: dict[str, Any] = {
|
||||||
|
"mode": mode,
|
||||||
|
"original": original_values,
|
||||||
|
"attempts": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
target_key = _temp_property(mode)
|
||||||
|
if target_key is None or original_values.get(target_key) is None:
|
||||||
|
probe_results["status"] = "skipped_missing_target"
|
||||||
|
return probe_results
|
||||||
|
|
||||||
|
original_value = original_values[target_key]
|
||||||
|
test_value = (
|
||||||
|
original_value + 1 if original_value < 31 else original_value - 1
|
||||||
|
)
|
||||||
|
if test_value == original_value:
|
||||||
|
probe_results["status"] = "skipped_no_room"
|
||||||
|
return probe_results
|
||||||
|
|
||||||
|
for key in ("setpoint_air_auto", "setpoint_air_cool", "setpoint_air_heat"):
|
||||||
|
if current_device.get(key) is None:
|
||||||
|
probe_results["attempts"][key] = "missing"
|
||||||
|
continue
|
||||||
|
await send(key, test_value)
|
||||||
|
accepted = await wait_for_property(key, test_value, timeout=8.0)
|
||||||
|
refreshed = await fetch_current_device()
|
||||||
|
probe_results["attempts"][key] = {
|
||||||
|
"requested": test_value,
|
||||||
|
"accepted": accepted,
|
||||||
|
"after": refreshed.get(key),
|
||||||
|
"command_debug": client.pop_last_command_debug() or {},
|
||||||
|
}
|
||||||
|
await send(key, original_values[key])
|
||||||
|
await wait_for_property(key, original_values[key], timeout=8.0)
|
||||||
|
|
||||||
|
probe_results["status"] = "completed"
|
||||||
|
return probe_results
|
||||||
|
|
||||||
|
async def probe_mode_transitions() -> dict[str, Any]:
|
||||||
|
original_mode = int(current_device.get("mode", 1) or 1)
|
||||||
|
original_power = bool(current_device.get("power", False))
|
||||||
|
attempts: dict[str, Any] = {}
|
||||||
|
|
||||||
|
for label, value in (("auto", 1), ("cool", 2), ("heat", 3), ("fan", 4)):
|
||||||
|
await send("mode", value)
|
||||||
|
accepted = await wait_for_property("mode", value, timeout=8.0)
|
||||||
|
refreshed = await fetch_current_device()
|
||||||
|
attempts[label] = {
|
||||||
|
"requested": value,
|
||||||
|
"accepted": accepted,
|
||||||
|
"after": refreshed.get("mode"),
|
||||||
|
"power": refreshed.get("power"),
|
||||||
|
"command_debug": client.pop_last_command_debug() or {},
|
||||||
|
}
|
||||||
|
|
||||||
|
await send("mode", original_mode)
|
||||||
|
await wait_for_property("mode", original_mode, timeout=8.0)
|
||||||
|
if not original_power:
|
||||||
|
await send("power", False)
|
||||||
|
await wait_for_property("power", False, timeout=8.0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"original_mode": original_mode,
|
||||||
|
"attempts": attempts,
|
||||||
|
}
|
||||||
|
|
||||||
restore_actions: list[tuple[str, Any]] = []
|
restore_actions: list[tuple[str, Any]] = []
|
||||||
try:
|
try:
|
||||||
|
results["probes"]["recovery"] = await recover_known_good_state()
|
||||||
|
results["probes"]["setpoint_keys"] = await probe_setpoint_keys()
|
||||||
|
results["probes"]["mode_transitions"] = await probe_mode_transitions()
|
||||||
|
|
||||||
await send("power", current_device.get("power", False))
|
await send("power", current_device.get("power", False))
|
||||||
results["writes"]["power_emit"] = "sent_same_value"
|
results["writes"]["power_emit"] = "sent_same_value"
|
||||||
|
|
||||||
@@ -382,7 +495,7 @@ async def _main() -> None:
|
|||||||
restore_actions.append(("slats_vertical_1", original_swing))
|
restore_actions.append(("slats_vertical_1", original_swing))
|
||||||
|
|
||||||
original_fan = int(current_device.get("speed_state", 0) or 0)
|
original_fan = int(current_device.get("speed_state", 0) or 0)
|
||||||
test_fan = _next_fan_speed(original_fan)
|
test_fan = _next_available_fan_speed(current_device, original_fan)
|
||||||
if test_fan == original_fan:
|
if test_fan == original_fan:
|
||||||
results["writes"]["fan_speed"] = "skipped_no_alternate"
|
results["writes"]["fan_speed"] = "skipped_no_alternate"
|
||||||
else:
|
else:
|
||||||
@@ -414,6 +527,10 @@ async def _main() -> None:
|
|||||||
if test_temp == original_temp:
|
if test_temp == original_temp:
|
||||||
results["writes"]["temperature"] = "skipped_no_room"
|
results["writes"]["temperature"] = "skipped_no_room"
|
||||||
else:
|
else:
|
||||||
|
mode_ready = await ensure_mode(mode)
|
||||||
|
results["write_debug"].setdefault(property_name, {})[
|
||||||
|
"mode_ready"
|
||||||
|
] = mode_ready
|
||||||
results["writes"]["temperature"] = await send_and_verify(
|
results["writes"]["temperature"] = await send_and_verify(
|
||||||
property_name, test_temp
|
property_name, test_temp
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user