Send + ack
This commit is contained in:
@@ -235,7 +235,13 @@ class DknCloudNaClient:
|
|||||||
payload = {"mac": mac, "property": property_name, "value": value}
|
payload = {"mac": mac, "property": property_name, "value": value}
|
||||||
LOGGER.debug("DKN socket send %s %s", namespace, payload)
|
LOGGER.debug("DKN socket send %s %s", namespace, payload)
|
||||||
try:
|
try:
|
||||||
await socket.emit("create-machine-event", payload, namespace=namespace)
|
ack = await socket.call(
|
||||||
|
"create-machine-event",
|
||||||
|
payload,
|
||||||
|
namespace=namespace,
|
||||||
|
timeout=REQUEST_TIMEOUT,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
|||||||
try:
|
try:
|
||||||
if hvac_mode == HVACMode.OFF:
|
if hvac_mode == HVACMode.OFF:
|
||||||
await self.coordinator.client.async_send_machine_event(
|
await self.coordinator.client.async_send_machine_event(
|
||||||
installation_id, self._mac, "power", False
|
installation_id, self._command_mac, "power", False
|
||||||
)
|
)
|
||||||
self._optimistic_set("power", False)
|
self._optimistic_set("power", False)
|
||||||
self._optimistic_set("hvac_mode", HVACMode.OFF)
|
self._optimistic_set("hvac_mode", HVACMode.OFF)
|
||||||
@@ -191,10 +191,10 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
|||||||
if mode is None:
|
if mode is None:
|
||||||
raise HomeAssistantError(f"Unsupported HVAC mode: {hvac_mode}")
|
raise HomeAssistantError(f"Unsupported HVAC mode: {hvac_mode}")
|
||||||
await self.coordinator.client.async_send_machine_event(
|
await self.coordinator.client.async_send_machine_event(
|
||||||
installation_id, self._mac, "power", True
|
installation_id, self._command_mac, "power", True
|
||||||
)
|
)
|
||||||
await self.coordinator.client.async_send_machine_event(
|
await self.coordinator.client.async_send_machine_event(
|
||||||
installation_id, self._mac, "mode", mode
|
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)
|
||||||
@@ -226,7 +226,7 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
|||||||
async with self._get_device_lock():
|
async with self._get_device_lock():
|
||||||
try:
|
try:
|
||||||
await self.coordinator.client.async_send_machine_event(
|
await self.coordinator.client.async_send_machine_event(
|
||||||
installation_id, self._mac, property_name, device_temp
|
installation_id, self._command_mac, property_name, device_temp
|
||||||
)
|
)
|
||||||
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
|
||||||
@@ -246,7 +246,7 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
|||||||
async with self._get_device_lock():
|
async with self._get_device_lock():
|
||||||
try:
|
try:
|
||||||
await self.coordinator.client.async_send_machine_event(
|
await self.coordinator.client.async_send_machine_event(
|
||||||
installation_id, self._mac, "speed_state", speed
|
installation_id, self._command_mac, "speed_state", speed
|
||||||
)
|
)
|
||||||
except Exception as err: # noqa: BLE001
|
except Exception as err: # noqa: BLE001
|
||||||
raise HomeAssistantError(f"Failed to set fan mode: {err}") from err
|
raise HomeAssistantError(f"Failed to set fan mode: {err}") from err
|
||||||
@@ -266,7 +266,7 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
|||||||
async with self._get_device_lock():
|
async with self._get_device_lock():
|
||||||
try:
|
try:
|
||||||
await self.coordinator.client.async_send_machine_event(
|
await self.coordinator.client.async_send_machine_event(
|
||||||
installation_id, self._mac, "slats_vertical_1", slat
|
installation_id, self._command_mac, "slats_vertical_1", slat
|
||||||
)
|
)
|
||||||
except Exception as err: # noqa: BLE001
|
except Exception as err: # noqa: BLE001
|
||||||
raise HomeAssistantError(f"Failed to set swing mode: {err}") from err
|
raise HomeAssistantError(f"Failed to set swing mode: {err}") from err
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ from typing import Any
|
|||||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN, MANUFACTURER, OPTIMISTIC_TTL_SEC, POST_WRITE_REFRESH_DELAY_SEC
|
from .const import (
|
||||||
|
DOMAIN,
|
||||||
|
MANUFACTURER,
|
||||||
|
OPTIMISTIC_TTL_SEC,
|
||||||
|
POST_WRITE_REFRESH_DELAY_SEC,
|
||||||
|
)
|
||||||
from .coordinator import DknCoordinator
|
from .coordinator import DknCoordinator
|
||||||
|
|
||||||
|
|
||||||
@@ -35,6 +40,14 @@ class DknEntity(CoordinatorEntity[DknCoordinator]):
|
|||||||
"""Return raw device dict from coordinator, or empty dict if unavailable."""
|
"""Return raw device dict from coordinator, or empty dict if unavailable."""
|
||||||
return (self.coordinator.data or {}).get(self._mac, {})
|
return (self.coordinator.data or {}).get(self._mac, {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _command_mac(self) -> str:
|
||||||
|
"""Return the device MAC in the form expected by the cloud API."""
|
||||||
|
mac = self._device_data.get("mac")
|
||||||
|
if isinstance(mac, str) and mac.strip():
|
||||||
|
return mac.strip()
|
||||||
|
return self._mac.upper()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_info(self) -> DeviceInfo:
|
def device_info(self) -> DeviceInfo:
|
||||||
data = self._device_data
|
data = self._device_data
|
||||||
@@ -71,13 +84,14 @@ class DknEntity(CoordinatorEntity[DknCoordinator]):
|
|||||||
)
|
)
|
||||||
overlays: dict[str, dict[str, Any]] = bucket.setdefault("optimistic", {})
|
overlays: dict[str, dict[str, Any]] = bucket.setdefault("optimistic", {})
|
||||||
device_overlays = overlays.setdefault(self._mac, {})
|
device_overlays = overlays.setdefault(self._mac, {})
|
||||||
device_overlays[key] = {"value": value, "expires": time.monotonic() + OPTIMISTIC_TTL_SEC}
|
device_overlays[key] = {
|
||||||
|
"value": value,
|
||||||
|
"expires": time.monotonic() + OPTIMISTIC_TTL_SEC,
|
||||||
|
}
|
||||||
|
|
||||||
def _optimistic_get(self, key: str, fallback: Any) -> Any:
|
def _optimistic_get(self, key: str, fallback: Any) -> Any:
|
||||||
"""Return the optimistic value if still fresh, else fallback."""
|
"""Return the optimistic value if still fresh, else fallback."""
|
||||||
bucket = self.hass.data.get(DOMAIN, {}).get(
|
bucket = self.hass.data.get(DOMAIN, {}).get(self.coordinator.entry_id, {})
|
||||||
self.coordinator.entry_id, {}
|
|
||||||
)
|
|
||||||
overlays = bucket.get("optimistic", {}).get(self._mac, {})
|
overlays = bucket.get("optimistic", {}).get(self._mac, {})
|
||||||
entry = overlays.get(key)
|
entry = overlays.get(key)
|
||||||
if entry and time.monotonic() < entry["expires"]:
|
if entry and time.monotonic() < entry["expires"]:
|
||||||
@@ -86,9 +100,7 @@ class DknEntity(CoordinatorEntity[DknCoordinator]):
|
|||||||
|
|
||||||
def _optimistic_clear(self, key: str) -> None:
|
def _optimistic_clear(self, key: str) -> None:
|
||||||
"""Expire an optimistic overlay immediately."""
|
"""Expire an optimistic overlay immediately."""
|
||||||
bucket = self.hass.data.get(DOMAIN, {}).get(
|
bucket = self.hass.data.get(DOMAIN, {}).get(self.coordinator.entry_id, {})
|
||||||
self.coordinator.entry_id, {}
|
|
||||||
)
|
|
||||||
overlays = bucket.get("optimistic", {}).get(self._mac, {})
|
overlays = bucket.get("optimistic", {}).get(self._mac, {})
|
||||||
overlays.pop(key, None)
|
overlays.pop(key, None)
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ async def _main() -> None:
|
|||||||
"login": False,
|
"login": False,
|
||||||
"discovery": None,
|
"discovery": None,
|
||||||
"socket_connect": False,
|
"socket_connect": False,
|
||||||
|
"write_debug": {},
|
||||||
"writes": {},
|
"writes": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,8 +233,11 @@ async def _main() -> None:
|
|||||||
for device in installation.get("devices", []):
|
for device in installation.get("devices", []):
|
||||||
device_mac = str(device.get("mac") or "").strip().lower()
|
device_mac = str(device.get("mac") or "").strip().lower()
|
||||||
if device_mac == mac:
|
if device_mac == mac:
|
||||||
current_device = dict(device)
|
current_device = {
|
||||||
current_device["_installation_id"] = installation_id
|
**current_device,
|
||||||
|
**dict(device),
|
||||||
|
"_installation_id": installation_id,
|
||||||
|
}
|
||||||
return current_device
|
return current_device
|
||||||
raise RuntimeError("Selected device disappeared during smoke test")
|
raise RuntimeError("Selected device disappeared during smoke test")
|
||||||
|
|
||||||
@@ -270,6 +274,7 @@ async def _main() -> None:
|
|||||||
|
|
||||||
installation_id = str(selected_installation.get("_id") or "")
|
installation_id = str(selected_installation.get("_id") or "")
|
||||||
mac = str(selected_device.get("mac") or "").strip().lower()
|
mac = str(selected_device.get("mac") or "").strip().lower()
|
||||||
|
command_mac = str(selected_device.get("mac") or "").strip() or mac.upper()
|
||||||
current_device = dict(selected_device)
|
current_device = dict(selected_device)
|
||||||
current_device["_installation_id"] = installation_id
|
current_device["_installation_id"] = installation_id
|
||||||
|
|
||||||
@@ -301,6 +306,7 @@ async def _main() -> None:
|
|||||||
"installation_id": installation_id,
|
"installation_id": installation_id,
|
||||||
"device_name": selected_device.get("name"),
|
"device_name": selected_device.get("name"),
|
||||||
"mac": mac,
|
"mac": mac,
|
||||||
|
"command_mac": command_mac,
|
||||||
**_effective_state(current_device),
|
**_effective_state(current_device),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,9 +328,24 @@ async def _main() -> None:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def collect_socket_deltas(window: float = 5.0) -> list[dict[str, Any]]:
|
||||||
|
deltas: list[dict[str, Any]] = []
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
deadline = loop.time() + window
|
||||||
|
while loop.time() < deadline:
|
||||||
|
remaining = max(0.05, deadline - loop.time())
|
||||||
|
try:
|
||||||
|
payload = await asyncio.wait_for(
|
||||||
|
queue.get(), timeout=min(0.5, remaining)
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
continue
|
||||||
|
deltas.append(payload)
|
||||||
|
return deltas
|
||||||
|
|
||||||
async def send(property_name: str, value: Any) -> None:
|
async def send(property_name: str, value: Any) -> None:
|
||||||
await client.async_send_machine_event(
|
await client.async_send_machine_event(
|
||||||
installation_id, mac, property_name, value
|
installation_id, command_mac, property_name, value
|
||||||
)
|
)
|
||||||
|
|
||||||
async def send_and_verify(
|
async def send_and_verify(
|
||||||
@@ -332,8 +353,18 @@ async def _main() -> None:
|
|||||||
) -> bool | str:
|
) -> bool | str:
|
||||||
if not _supports_write_verification(current_device, property_name):
|
if not _supports_write_verification(current_device, property_name):
|
||||||
return "unsupported_by_payload"
|
return "unsupported_by_payload"
|
||||||
|
baseline = current_device.get(property_name)
|
||||||
await send(property_name, value)
|
await send(property_name, value)
|
||||||
return await wait_for_property(property_name, value, timeout=timeout)
|
verified = await wait_for_property(property_name, value, timeout=timeout)
|
||||||
|
refreshed = await fetch_current_device()
|
||||||
|
results["write_debug"][property_name] = {
|
||||||
|
"requested": value,
|
||||||
|
"before": baseline,
|
||||||
|
"after": refreshed.get(property_name),
|
||||||
|
"acknowledged": True,
|
||||||
|
"socket_deltas": await collect_socket_deltas(),
|
||||||
|
}
|
||||||
|
return verified
|
||||||
|
|
||||||
restore_actions: list[tuple[str, Any]] = []
|
restore_actions: list[tuple[str, Any]] = []
|
||||||
try:
|
try:
|
||||||
@@ -367,13 +398,18 @@ async def _main() -> None:
|
|||||||
else:
|
else:
|
||||||
original_temp = current_device.get(property_name)
|
original_temp = current_device.get(property_name)
|
||||||
low, high = _temp_bounds(current_device, mode)
|
low, high = _temp_bounds(current_device, mode)
|
||||||
if original_temp is None or low is None or high is None:
|
if original_temp is None:
|
||||||
results["writes"]["temperature"] = "skipped_missing_bounds"
|
results["writes"]["temperature"] = "skipped_missing_setpoint"
|
||||||
else:
|
else:
|
||||||
test_temp = original_temp
|
test_temp = original_temp
|
||||||
if original_temp < high:
|
if low is not None and high is not None:
|
||||||
|
if original_temp < high:
|
||||||
|
test_temp = original_temp + 1
|
||||||
|
elif original_temp > low:
|
||||||
|
test_temp = original_temp - 1
|
||||||
|
elif original_temp < 31:
|
||||||
test_temp = original_temp + 1
|
test_temp = original_temp + 1
|
||||||
elif original_temp > low:
|
elif original_temp > 17:
|
||||||
test_temp = original_temp - 1
|
test_temp = original_temp - 1
|
||||||
if test_temp == original_temp:
|
if test_temp == original_temp:
|
||||||
results["writes"]["temperature"] = "skipped_no_room"
|
results["writes"]["temperature"] = "skipped_no_room"
|
||||||
|
|||||||
Reference in New Issue
Block a user