|
26 | 26 | _LOGGER = logging.getLogger(__name__) |
27 | 27 |
|
28 | 28 |
|
| 29 | +def _raise_for_error_payload(response, *, auth_only: bool = False) -> None: |
| 30 | + """Surface U-Tec error envelopes returned with an HTTP 2xx status. |
| 31 | +
|
| 32 | + U-Tec replies HTTP 200 even on failure, carrying the error under |
| 33 | + ``payload.error`` (e.g. ``{"code": "INVALID_TOKEN", "message": ...}``). |
| 34 | + Left unraised, a revoked/expired token is swallowed and the coordinator |
| 35 | + serves stale state indefinitely with no reauth prompt. ``INVALID_TOKEN`` |
| 36 | + becomes ``ConfigEntryAuthFailed`` (so HA triggers reauth and marks entities |
| 37 | + unavailable); other error codes become ``UpdateFailed`` unless ``auth_only`` |
| 38 | + is set — discovery only needs to surface auth failures and lets other |
| 39 | + errors fall through to its existing graceful handling. |
| 40 | + """ |
| 41 | + if not isinstance(response, dict): |
| 42 | + return |
| 43 | + payload = response.get("payload") |
| 44 | + if not isinstance(payload, dict): |
| 45 | + return |
| 46 | + error = payload.get("error") |
| 47 | + if not isinstance(error, dict): |
| 48 | + return |
| 49 | + code = error.get("code") |
| 50 | + message = error.get("message", "") |
| 51 | + if code == "INVALID_TOKEN": |
| 52 | + raise ConfigEntryAuthFailed(f"U-Tec rejected access token: {message}") |
| 53 | + if not auth_only: |
| 54 | + raise UpdateFailed(f"U-Tec API error {code}: {message}") |
| 55 | + |
| 56 | + |
29 | 57 | class UhomeDataUpdateCoordinator(DataUpdateCoordinator): |
30 | 58 | """Class to manage fetching Uhome data.""" |
31 | 59 |
|
@@ -88,6 +116,11 @@ async def async_discover_devices(self) -> None: |
88 | 116 | _LOGGER.error("Invalid discovery data received: %s", discovery_data) |
89 | 117 | return |
90 | 118 |
|
| 119 | + # A revoked token returns an INVALID_TOKEN envelope here; surface it so |
| 120 | + # setup/reload fails into reauth instead of silently building 0 devices |
| 121 | + # (which wipes every entity to unavailable on reload). |
| 122 | + _raise_for_error_payload(discovery_data, auth_only=True) |
| 123 | + |
91 | 124 | devices_data = discovery_data.get("payload", {}).get("devices", []) |
92 | 125 | _LOGGER.debug("Found %s devices in discovery data", len(devices_data)) |
93 | 126 |
|
@@ -152,6 +185,11 @@ async def _async_update_data(self) -> dict[str, dict]: |
152 | 185 | except ApiError as err: |
153 | 186 | raise UpdateFailed(f"Error communicating with API: {err}") from err |
154 | 187 |
|
| 188 | + # U-Tec returns HTTP 200 with an error envelope (e.g. INVALID_TOKEN) that |
| 189 | + # get_device_state does not raise on — surface it instead of treating an |
| 190 | + # error response as an empty-but-successful poll. |
| 191 | + _raise_for_error_payload(response) |
| 192 | + |
155 | 193 | if response and "payload" in response: |
156 | 194 | for device_data in response["payload"].get("devices", []): |
157 | 195 | device_id = device_data.get("id") |
|
0 commit comments