From 3211d54b5ff40a3c591adfda37d69d1fafc0f966 Mon Sep 17 00:00:00 2001 From: Alexander Tihoniuk Date: Sun, 21 Jun 2026 20:40:04 +0300 Subject: [PATCH] fix(appliances): route enumeration at Haier's new unified-api endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Haier migrated appliance enumeration off the legacy /commands/v1/appliance REST endpoint around 2026-06-20. For migrated accounts that endpoint now returns 200 with an empty {"payload":{"appliances":[]}} body, so every device shows up as unavailable in Home Assistant — pyhon-revived 0.18.3 still calls the legacy path in HonAPI.load_appliances. Monkey-patch load_appliances to query the endpoint the hOn app now uses, {API_URL}/unified-api/v1/view/appliance-list, which returns the same records. The response envelope is undocumented, so list extraction probes the plausible shapes and logs the raw keys if none match, instead of silently returning nothing. Stopgap until the fix lands upstream in pyhon-revived. Ref: https://github.com/mmalolepszy/hon-revived/issues/48 Co-Authored-By: Claude Opus 4.8 (1M context) --- custom_components/hon/__init__.py | 6 ++ custom_components/hon/appliance_list_patch.py | 80 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 custom_components/hon/appliance_list_patch.py diff --git a/custom_components/hon/__init__.py b/custom_components/hon/__init__.py index 6c1e4cbc..675c65bf 100644 --- a/custom_components/hon/__init__.py +++ b/custom_components/hon/__init__.py @@ -11,10 +11,16 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from pyhon import Hon +from . import appliance_list_patch from .const import DOMAIN, PLATFORMS, MOBILE_ID, CONF_REFRESH_TOKEN _LOGGER = logging.getLogger(__name__) +# Work around Haier's 2026-06 appliance-list API migration: route pyhon's +# appliance enumeration at the new unified-api endpoint so devices stop +# showing up as unavailable. See appliance_list_patch for details. +appliance_list_patch.apply() + HON_SCHEMA = vol.Schema( { vol.Required(CONF_EMAIL): cv.string, diff --git a/custom_components/hon/appliance_list_patch.py b/custom_components/hon/appliance_list_patch.py new file mode 100644 index 00000000..f8307f9b --- /dev/null +++ b/custom_components/hon/appliance_list_patch.py @@ -0,0 +1,80 @@ +"""Patch pyhon appliance enumeration onto Haier's new unified-api endpoint. + +Background +---------- +Around 2026-06-20 Haier migrated appliance enumeration off the legacy +``/commands/v1/appliance`` REST endpoint that pyhon-revived (0.18.3) calls in +:meth:`pyhon.connection.api.HonAPI.load_appliances`. For migrated accounts that +endpoint now answers ``200`` with an empty ``{"payload": {"appliances": []}}`` +body, so every Haier device ends up ``unavailable`` in Home Assistant. The hOn +mobile app reads the list from ``{API_URL}/unified-api/v1/view/appliance-list`` +instead, which returns the same appliance records. + +This module monkey-patches ``load_appliances`` to query the new endpoint. It is +a stopgap until the fix lands upstream in pyhon-revived; delete this module and +its use in ``__init__.py`` once the pinned dependency targets the new endpoint. + +Ref: https://github.com/mmalolepszy/hon-revived/issues/48 +""" + +import logging +from typing import Any + +from pyhon import const +from pyhon.connection.api import HonAPI + +_LOGGER = logging.getLogger(__name__) + +# The legacy ``f"{const.API_URL}/commands/v1/appliance"`` now returns an empty +# list for migrated accounts; this is the endpoint the current app uses. +APPLIANCE_LIST_URL = f"{const.API_URL}/unified-api/v1/view/appliance-list" + +# Keys the unified-api response might nest the appliance list under. The exact +# envelope is undocumented, so probe the plausible shapes rather than assume one +# and silently return nothing. +_LIST_KEYS = ("appliances", "applianceList", "appliancesList", "items", "data") + + +def _extract_appliances(result: Any) -> list[dict[str, Any]]: + """Pull the appliance list out of the unified-api response body.""" + if not result: + return [] + if isinstance(result, dict): + payload: Any = result.get("payload", result) + else: + payload = result + if isinstance(payload, list): + return payload + if isinstance(payload, dict): + for key in _LIST_KEYS: + value = payload.get(key) + if isinstance(value, list): + return value + _LOGGER.warning( + "hon: unexpected appliance-list response shape (top-level=%s, payload=%s); " + "no appliances parsed", + list(result.keys()) if isinstance(result, dict) else type(result).__name__, + list(payload.keys()) if isinstance(payload, dict) else type(payload).__name__, + ) + return [] + + +async def _load_appliances(self: HonAPI) -> list[dict[str, Any]]: + """Replacement for ``HonAPI.load_appliances`` hitting the new endpoint.""" + # pylint: disable=protected-access + async with self._hon.get(APPLIANCE_LIST_URL) as response: + result = await response.json() + appliances = _extract_appliances(result) + _LOGGER.debug( + "hon: loaded %d appliance(s) from %s", len(appliances), APPLIANCE_LIST_URL + ) + return appliances + + +def apply() -> None: + """Install the monkey-patch (idempotent).""" + HonAPI.load_appliances = _load_appliances # type: ignore[method-assign] + _LOGGER.info( + "hon: patched HonAPI.load_appliances -> %s (Haier API migration workaround)", + APPLIANCE_LIST_URL, + )