Skip to content

Handle appliances without a command set (e.g. TVs) gracefully - #336

Open
gregoriohc wants to merge 1 commit into
gvigroux:masterfrom
gregoriohc:fix/appliances-without-command-set
Open

Handle appliances without a command set (e.g. TVs) gracefully#336
gregoriohc wants to merge 1 commit into
gvigroux:masterfrom
gregoriohc:fix/appliances-without-command-set

Conversation

@gregoriohc

Copy link
Copy Markdown
Contributor

Some hOn appliances — a Haier TV (applianceType: "TV") in my case — legitimately return an empty command-retrieve payload. On every startup and integration reload this produced a noisy, misleading error:

ERROR [custom_components.hon.device] Unable to load device commands. Please try to restart. Current value: [{}]

Root cause

  • hon.py load_commands() collapses both a real API failure and a legitimately empty payload into {}.
  • device.py load_commands() then does commands.pop("applianceModel") inside a bare except:, so the resulting KeyError is logged as an ERROR even though, for a TV, having no program/command set is a perfectly normal state.

Changes

  • device.py: guard for an empty command set → log at DEBUG and skip command setup; narrow the bare except: to except KeyError so genuine malformed payloads are still surfaced.
  • hon.py: distinguish a real API failure (HTTP != 200 / resultCode != "0", logged at WARNING) from a legitimately empty payload (returned quietly). No behavioural change for appliances that do return commands.

Verification (live, with debug logging)

Three linked appliances — dishwasher (DW), washing machine (WM) and a TV:

  • WM and DW still load their Commands and expose program entities (no regression).
  • The TV now logs DEBUG ... No command set returned for Living Room TV (type TV); skipping command setup. instead of the ERROR.
  • The Unable to load device commands ERROR no longer appears on startup or reload.

Some hOn appliances (a Haier TV, applianceType 'TV') legitimately return
an empty command-retrieve payload. load_commands() collapsed that into
{}, and device.load_commands() then did commands.pop('applianceModel')
inside a bare 'except:', logging a noisy ERROR ('Unable to load device
commands. Please try to restart.') for what is a perfectly normal state.

- device.py: guard for an empty command set -> log at DEBUG and skip
  command setup; narrow the bare 'except:' to 'except KeyError'.
- hon.py: distinguish a real API failure (HTTP != 200 / resultCode != 0,
  logged at WARNING) from a legitimately empty payload (returned quietly).

Washing machine / dishwasher keep loading their commands unchanged; the
TV no longer logs an ERROR on startup or reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
foXaCe added a commit to foXaCe/hon that referenced this pull request Aug 8, 2026
…mName, password-change error, program helpers

Adapt interesting upstream (gvigroux/hon) PRs to the refactored fork:
- gvigroux#336: appliances without a command set (e.g. TVs) handled gracefully (debug log, no error)
- gvigroux#335: optional 'series' field in load_commands (no KeyError)
- gvigroux#332: add programName to startProgram commands for WM/WD
- gvigroux#318: detect ChangePassword redirect -> HonPasswordChangeRequiredError, actionable config flow error (password_change_required) in user/reauth/reconfigure + translations
- gvigroux#317: limit 'Get programs details' button to oven/dishwasher (PROGRAM_HELPER_APPLIANCE_TYPES) + don't propagate 'program' parameter in start_command
- Ignored gvigroux#341 (induction hob) and gvigroux#342 (pt-PT) per decision
- 3 new tests (password change, missing series, button OV/DW), 410 total
foXaCe added a commit to foXaCe/hon that referenced this pull request Aug 8, 2026
* chore: overhaul state file

* docs: overhaul state (phase 0/0.5/1 done)

* refactor: architecture & coordinator

- New typed coordinator HonBaseCoordinator (DataUpdateCoordinator[HonDevice]) with _async_setup one-shot (commands+statistics), UpdateFailed, async_set
- Modern config entry lifecycle: entry.runtime_data, ConfigEntryAuthFailed (translatable), ConfigEntryNotReady, async_on_unload, add_update_listener
- Typed exceptions (HonAuthenticationError/HonConnectionError/HonRateLimitError)
- OptionsFlow with configurable update_interval (30-3600s)
- Cleaned reauth + reconfigure flows (removed dead commented code, modern selectors)
- All platforms migrated from hass.data[DOMAIN] to entry.runtime_data

* docs: overhaul state (phase 2 done)

* refactor: modular structure

- Split monoliths into api/ (client, exceptions, models) and devices/ (base, per-platform entity classes)
- Platforms reduced to async_setup_entry importing from devices/
- Added helpers.py (snake_case, get_key, minutes_until)
- Deduplicated HonBaseEntity (device_info/attributes shared, no more 3x copy)
- Implemented 3 orphan service handlers (turn_off_oven/washingmachine/purifier)
- Removed dead code (async_get_device_ids duplicate, commented blocks, TODO orphans)
- Fixed translation_key bug in HonBaseChildLockStatus
- Fixed get_setting log spam (warning -> debug)
- Replaced deprecated CONCENTRATION_* constants with UnitOfDensity/UnitOfRatio

* docs: overhaul state (phase 3 done)

* fix: api client robustness

- Shared aiohttp session via async_get_clientsession (drop ssl=False private session)
- ClientTimeout(total=30) on every request
- Exponential backoff retry on 429/5xx/ClientError/Timeout (2^n up to 8s)
- Automatic token refresh on 401 with re-entrancy guard (no infinite loop)
- HonRateLimitError for HTTP 429
- Responses closed properly (async with), return parsed JSON
- Eliminated _LOGGER f-strings (lazy %s logging), removed setup log spam
- Fixed non-existent async_get_state call in washing machine service (connectivity via coordinator)
- datetime.utcnow() -> datetime.now(UTC)

* docs: overhaul state (phase 4 done)

* feat: modernize entities

- has_entity_name=True everywhere (HA prefixes device name)
- Unified unique_id format {entry.unique_id}_{mac}_{key} via coordinator.unique_id_prefix
- ConfigEntry VERSION 1->2 + async_migrate_entry: prefix existing unique ids with {email}_ (54 UID stable, verified)
- Activate number & select platforms (were dead code); merged duplicate entity creation, fixed missing .send() on start_command, fixed native_unit_of_measurement typo, fixed HonSelect MRO (HonDevice, SelectEntity) -> (HonBaseEntity, SelectEntity)
- update_sensor matches new prefixed unique_id format

* docs: overhaul state (phase 5 done)

* fix: config flow & fr translations

- Create strings.json (single source of truth, 1329 keys, parity strict with en/fr)
- Full French translations (was 5% coverage) with vouvoiement, strict key parity verified
- Clean dirty en.json data (lockStatuslockStatus, newline keys, test/ending states, broken config error)
- Add entity.binary_sensor.lockstatus + exceptions.auth_failed translations
- Clean services.yaml: typos (Botton->Bottom, ºC->°C), inconsistent defaults (30°->30, 400 rpm->400, 15 min->15), simplify huge send_custom_request default
- Align all 28 YAML services with handlers
- hassfest: 0 invalid integrations

* docs: overhaul state (phase 6 done)

* chore: typing + docstrings + hassfest

- from __future__ import annotations on all 30 files
- mypy --strict: 0 errors (fixed async_get_options_flow to new 2026.8 @staticmethod @callback pattern)
- 237 docstrings added across 22 files
- Switch available log spam: warning -> debug
- manifest.json: add loggers
- hassfest: 0 invalid integrations

* docs: overhaul state (phase 7 done)

* test: exhaustive test suite, coverage >= 90%

- 402 tests, 96% coverage (100% on all critical modules)
- tests/: conftest, config_flow (20), init (9), coordinator (13), client (38), helpers (11), parameter (25), device (44), command (11), services (7), devices/* (sensor, binary_sensor, switch, number, select, button, climate, water_heater)
- Fixed 2 real bugs found by tests:
  * HonOptionsFlowHandler overwrote read-only config_entry (options flow crashed on HA >= 2026.7)
  * HonDevice.set wrote into a recreated dict (value never persisted)
- ruff check+format green on code and tests, mypy strict green
- pre-commit mypy: run on whole package (pass_filenames=false) to avoid namespace collision

* docs: overhaul state (phase 8 done)

* chore: final validation, changelog, architecture

- Remove .overhaul-state.md (overhaul complete)
- CHANGELOG.md updated with all phases
- ARCHITECTURE.md rewritten for the modular structure
- Parallelize appliance context loading at setup (6.91s vs 8.93s baseline, faster boot)
- Remove duplicate load_commands/load_statistics calls (already in coordinator _async_setup)

* feat: localize entities (FR) + config flow & options translations + diagnostics

- Entity names moved from hard-coded English _attr_name to translation_key (HA resolves FR/EN): 100 keys added (sensor, binary_sensor, switch, button), strict parity strings/en/fr (1432 keys)
- Fixed invalid auto translation keys (start_time, statistics_programs_counter, water_hardness, haier_detergent_weight...)
- Config flow + options flow fully translated to French
- Added diagnostics.py (entry/appliances/coordinators/devices/entities snapshot, async_redact_data)
- Fixed options flow 500 error (async_get_options_flow passed config_entry to constructor that takes none)
- Tests updated to assert translation_key; added test_diagnostics; 404 tests, 96% coverage
- hassfest green, mypy strict green, unique_id stability preserved (71/71)

* feat: system health, README/docs update

- system_health.py: cloud reachability, appliance count, coordinator update status
- Tests for system health (406 tests total, 96% coverage)
- README: document options flow and reauth
- CHANGELOG: record system health + translations

* feat: localize button notifications + mode/phase/error entity names

- Button notifications (programs/settings details) in French, program titles translated via entity translations (program: value kept raw for the service call)
- Add missing 'name' to mode/phase/error/program translations (were state-only, entities showed no name)
- Fix fr.json using English names for wash_program_phase/washingmachine_error
- Add test asserting French notification text + translated title
- hassfest green, 407 tests, parity strict (1449 keys)

* perf: cut config entry setup time ~2.5x

- Remove redundant per-platform refreshes at setup: sensor (1x), switch (9x), climate (re-first-refresh after __init__ already refreshed)
- Parallelize load_commands + load_statistics in coordinator _async_setup
- Config entry setup: ~3.5-4s vs 8.93s baseline (measured on HA dev, 2 appliances)
- authorize (~2.1s, 3 dependent CIAM calls) is the remaining dominant cost
- Update test_sensor_platform (no longer awaits per-appliance refresh)
- 407 tests green, mypy/ruff green, 71 entities stable

* fix: audit findings — branding, min HA version, parallel updates, docs

- Add brand/ assets (icon 256/512, logo) for Bronze 'brands' rule (local bundle, modern 2026.3 method)
- Fix hacs.json + README min HA version 2024.1 -> 2026.7 (code uses UnitOfDensity/UnitOfRatio from 2026.7)
- Add PARALLEL_UPDATES to all platforms (0 for read-only with coordinator, 1 for action platforms) — Silver 'parallel-updates'
- README: add Removal, Data updates, Use cases, Examples, Known limitations sections — Gold docs rules
- Audit against Quality Scale of 2026-08-09, HA 2026.8

* fix: sanitize sensitive payload logging (CodeQL)

- CodeQL: 2 high-severity alerts logging full command payloads (mac, parameters) in debug
- Sanitize all payload logs in api/client.py: log mac/type/command/resultCode only, never full payloads or appliance lists
- Sanitize appliance list log in __init__.py (MAC addresses only)

* feat: adapt upstream PRs — empty command set, optional series, programName, password-change error, program helpers

Adapt interesting upstream (gvigroux/hon) PRs to the refactored fork:
- gvigroux#336: appliances without a command set (e.g. TVs) handled gracefully (debug log, no error)
- gvigroux#335: optional 'series' field in load_commands (no KeyError)
- gvigroux#332: add programName to startProgram commands for WM/WD
- gvigroux#318: detect ChangePassword redirect -> HonPasswordChangeRequiredError, actionable config flow error (password_change_required) in user/reauth/reconfigure + translations
- gvigroux#317: limit 'Get programs details' button to oven/dishwasher (PROGRAM_HELPER_APPLIANCE_TYPES) + don't propagate 'program' parameter in start_command
- Ignored gvigroux#341 (induction hob) and gvigroux#342 (pt-PT) per decision
- 3 new tests (password change, missing series, button OV/DW), 410 total

* fix: remove MAC identifiers from logs (CodeQL)

- CodeQL flags MAC addresses and appliance identifiers as sensitive data even in debug logs
- Replace identifier-bearing logs with counts/result codes only (appliances loaded N, resultCode, command type)
- No MAC/email/serial identifiers logged anywhere anymore
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant