Repository navigation
refactor(repairs): track device faults per gateway and raise them as self-clearing repair issues - #593
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Thanks for the refactor. Moving device health tracking to the gateway makes sense, especially for switches and addresses without a configured entity. Keeping WHO 1001 masks as raw evidence without interpreting undocumented bits is also the right approach. Before merging, I would address two points:
Two smaller documentation points:
The overall architecture looks good; these changes would make the repair lifecycle and evidence association more reliable. |
Interstellar0verdrive
left a comment
There was a problem hiding this comment.
Thanks for reworking this. Moving fault tracking to one tracker per gateway is the right architecture: it covers switches and unconfigured addresses, removes the per-platform duplication, and keeping the WHO 1001 mask as raw evidence is exactly how undocumented bits should be handled. I checked that WHO 2 handling and cover calibration are unaffected, and that the dispatcher still delivers frames to entities in the same order.
I agree with all four points @xtimmy86x raised (failover/failback ownership, the one-directional evidence window, the entity-removal wording, and the "shows its state as unknown" text). I reproduced the failback case: once the primary reconnects, the standby stops observing WHO 1 and its issue never clears until a reload. I won't repeat those. These are the additional points I found, in order of importance.
1. The corpus test fails on the current base (blocking).
CI ran on ae94a1a before #596 was merged. With this branch merged onto the current v2-phase1-architecture (3ac21ff, merges without conflicts), two tests fail:
test_device_health_corpus.py::test_the_corpus_is_not_emptytest_device_health_corpus.py::test_capture_raises_exactly_its_known_faults[config_entry-myhome_F455.json]
_replay() calls OWNEvent.parse() on every frame of every capture, and the new F455 fixture contains *#13**0##, which OWNd 2.0.0b9 rejects with ValueError: Gateway dimension 0 requires 3 values. I suggest filtering WHO 1/1001 with the regex before parsing, so the replay does not depend on how other WHOs are parsed. Please rebase and re-run the suite.
2. Renaming an entity_id drops the device's faults.
MyHOMEEntity.async_will_remove_from_hass treats a missing registry entry for self.entity_id as "the owner deleted the entity". On an entity_id rename, Home Assistant updates the registry first and then calls async_remove(force_remove=True) while self.entity_id still holds the old id. The lookup therefore returns None and forget_address() withdraws an issue whose fault is still present. This pattern was already in the climate entity; this PR extends it to every platform that calls the base. Home Assistant has a dedicated hook for this, async_removed_from_registry(), which runs only on a real registry removal. Note that once the hook applies to sensors too, forget_address() needs to check that no other live entity still uses the same (WHO, WHERE): today removing one of two entities on an address drops the other's issue. That only doesn't happen for sensors/binary sensors because they override async_will_remove_from_hass without calling super().
3. Issues the tracker doesn't hold can no longer clear.
clear() only deletes an issue that is in _active. Any issue in the registry that the current tracker doesn't know about can't be withdrawn by the next normal status. That covers an issue raised by a frame processed between clear_all() and the listener task being cancelled during unload, and also the standby case above. ir.async_delete_issue on a missing id is already a cheap no-op, so I'd keep the edge trigger on create (that is where the savings are) and always delete on a normal status.
4. A keypad command clears the issue.
On the bus, *1*1*74## from a push button (or from Home Assistant itself) looks the same as the actuator's status. On a faulty actuator every command withdraws the issue and the next poll raises it again (three cycles give three creates and three deletes). The bus can't tell the two apart, so I'm not asking for a different rule. I am asking that the text and the docs say "clears when an on/off/level frame for this address is seen" rather than "when the device reports a normal state".
5. Re-raise after reload only works for configured addresses.
The docs say the issue "is raised again after a restart or reload if the actuator still reports the code". initial_discovery() only queries WHO 2, 4 and 16, so for an address with no entity nothing asks again. The issue comes back only if the actuator sends a frame by itself. Please qualify the sentence, or document that a bus sweep re-raises it.
6. Hot path.
health.observe() runs inside process_message without isolation. An exception there would skip the rest of the WHO 1 handling for that frame (area/group events, resync). I'd guard it with who_int in (1, 1001) before calling _is_active_for_who(), since on a standby that call scans the config entries for every frame of every WHO. I'd also wrap it in try/except with LOGGER.exception.
Questions
test_a_status_outside_the_published_table_is_a_faultasserts WHAT 32 and 33 as faults. Is there a source saying they are never used, or does the test pin a behaviour that would raise issues on real plants?- There is no real capture of lighting-management sensors in the corpus (OWNd handles WHAT 34). Do they emit other WHATs, for example end of movement? 33 of the 63 captures feed no WHO 1/1001 frame to the tracker, so "replays all 63 captures" overstates the coverage a bit.
unresponsive_zonechanges its placeholder from{zone}to{device}. fr/it/nl have no translation today, but if Crowdin holds strings with{zone}, the next sync would bring back an unfilled placeholder. Could you check the source string there?
Smaller things
- The
EVIDENCE_WINDOWcomment says the mask "followed the status reply by about 40 ms"; in the 2026-09-26 trace it arrived 3.45 s before. The PR body says so too. name_address()stores_device_nameandreport()stores_display_name, so the same device can be named two ways.- Italian strings: the rest of
it.jsonuses the second person ("Riconfigura la password", "Controlla il cablaggio"), while the new strings use the infinitive. Please align them. Also, "un attuatore luci in guasto" reads better as "un attuatore luci guasto", and "è mostrato così come ricevuto" as "viene mostrato così come è stato ricevuto".
I ran the suite on Python 3.14 with the latest pytest-homeassistant-custom-component. The head of this branch passes (3179 passed, 1 skipped, 100% coverage); merged onto the current base, the two tests above fail. I'm requesting changes for point 1 and the lifecycle points (2, 3, and the failover case already raised). The rest can be follow-ups if you prefer.
…epair issues When an actuator reports an anomaly status (such as WHAT 19 on WHO 1), raise a Home Assistant repair issue (actuator_hardware_fault) linking to diagnostic guidance and auto-clear it when normal status returns.
…apped WHAT copy - Slug '#' in WHERE addresses to maintain URL-safe issue identifiers - Edge-trigger repair issue creation and deletion on state change - Generalize copy to unmapped lighting WHAT codes, citing F414 WHAT 19 as the known hardware case - Point learn_more_url at the live repair issues documentation anchor - Add localized translations in fr.json, it.json, and nl.json - Add comprehensive edge-triggering, slugging, and non-19 tests
…vice health tracker Replace the light-only actuator_hardware_fault repair with DeviceHealth (device_health.py): one tracker per gateway, the only code that creates or deletes per-device fault issues. Issues are written once per change and withdrawn on the next normal status, entity removal or entry unload. - Frames are classified in GatewayEventDispatcher.process_message, so lights and switches with no entity still raise their issue; on a shared bus only the owner of WHO 1 raises lighting faults (WHO 1001 follows WHO 1). - A WHO 1 status outside the published table raises unmapped_device_status. A WHO 1001 DIMENSION 7/11 mask within 10 s is attached raw as evidence (unmapped_device_status_autodiag); its bits are undocumented, so it is never decoded and never a trigger on its own. - The entity base class names the device in its issues, drops them when the owner deletes the entity, and offers _report_fault/_clear_fault; the climate unresponsive_zone issue now goes through the tracker. - Neutral issue text in en/fr/it/nl: no F414, fuse or open-load claims the evidence (EVID-MH200-WHAT19-FAULT) does not support. Diagnostics list the active faults. - test_device_health_corpus replays all 63 captures: only the two MH200 address 74 captures may raise a fault; the Encyclopedia golden corpus and every published WHO 1 WHAT must raise none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le, evidence window) - corpus replay parses only WHO 1/1001 frames, so other WHOs' decoders cannot break it - a warm standby carrying an offline primary files faults under the primary's tracker - entity removal uses async_removed_from_registry (not a rename or reload) and the tracker keeps issues while another entity uses the address - a normal status always asks the registry to delete the issue - the 10 s evidence window holds in both directions - guard and isolate health.observe on the WHO 1/1001 hot path - repair text and docs: what clears the issue, what re-raises it, state wording - it/fr/nl strings aligned Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ecovery
WHAT 32 (Toggle), 34 (movement) and 39 (end of movement) are defined by the
ZigBee OpenWebNet spec 4.0 and say nothing about the on/off state; OWNd
reports 32 and 39 as unknown_state, which raised a false repair. WHAT 33 is
in no source and stays a fault. Also give unresponsive_zone a zone
placeholder, so a translation that still uses {zone} renders. Document what
to do with an actuator that will not be repaired soon.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A disabled entity's status is not requested at startup, so the repair does not come back on every restart. Also fixes a missing space in device_health. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6804784 to
bc418db
Compare
The failover tests only checked what health_owner() returns. Two new tests send the fault and the recovery through the dispatchers of a primary and its warm standby: a recovery seen by the standby during the failover, and one seen by the primary after the failback, both delete the primary's issue. Also calls super() in async_removed_from_registry, fixes the stale "~40 ms" mask timing in the test docstring, and points the docs' "clears itself" sentence at the on/off/level rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review, all four blocking points are addressed as of
On the other questions:
Docs: |
|
Follow-up: opened #611 for permanent device silencing via the |
Interstellar0verdrive
left a comment
There was a problem hiding this comment.
Thanks for the thorough follow-up. I re-ran everything against 9e860f8. Every point I raised is addressed, and so are @xtimmy86x's:
- the corpus replay now filters WHO 1/1001 before parsing, and the suite passes on the head (3205 passed, 1 skipped, 100% coverage) and merged onto the current base
fb3cce9(3254 passed, 1 skipped). I don't see the 4 alarm failures you mention, either on the head or merged with the current base, so #608 has probably fixed them; async_removed_from_registry()fixes the entity_id rename. My repro of HA's rename path now keeps the issue;health_owner()keeps one owner across failover and failback. My failback repro now clears on the primary and leaves nothing on the standby;- the evidence window holds in both directions,
clear()always deletes from the registry,observe()is guarded and isolated, and the per-unique-id owners keep an address's issue while another entity still uses it; - 32/34/39 as documented events, the
{zone}placeholder, the keypad and reload caveats in the docs, and the Italian strings all look good to me.
One small regression from the naming change, not blocking. name_address() now receives _display_name from every entity, sensors included. A zone's temperature sensor shares the climate's address (4, "71"), so when it is added after the climate it renames the unresponsive_zone issue to "Heating zone Zone 71 Temperature no longer answers" (I reproduced it). The issue describes the device, so I'd pass _device_name to name_address() and keep _display_name for the entity that calls report().
Two Italian nits, also not blocking: "lo stato ricevuto non consente a Home Assistant di determinare lo stato attuale" repeats "stato" ("il valore ricevuto…" reads better), and "un pulsante a muro" is more natural as "un pulsante a parete".
Approving. The naming fix can go in here or in a follow-up, as you prefer.
5519ddf
…nto rfc/actuator-hardware-fault-handling
abea209
into
OpenWebNet-HA:v2-phase1-architecture
Summary
Hardware faults on the bus show up as self-clearing Home Assistant repair issues, raised by one tracker per gateway instead of code inside each entity. Discussion: #594.
The first version of this PR raised
actuator_hardware_faultfrom insideMyHOMELight.handle_event. That missed switches and addresses with no entity, left the issue behind when the entity was deleted, and would have been copied into every platform. Its text also stated causes (F414, open load, T5H fuse) that the evidence does not show. This revision replaces it:device_health.py:DeviceHealth, one per gateway (handler.device_health). It is the only code that creates or deletes per-device fault issues. It writes an issue once per change, so a stuck actuator answering every poll does not rewrite the registry.GatewayEventDispatcher.process_messagepasses every WHO 1/1001 frame to it, so lights and switches with no entity still raise their issue. On a shared bus only the gateway owning WHO 1 raises lighting faults; WHO 1001 is gated as WHO 1.unmapped_device_status. The next normal status (on, off or a level) clears it.DOCUMENTED_EVENTS).unmapped_device_status_autodiag). It is not decoded and does not raise an issue on its own (see Evidence below).MyHOMEEntity):WHO x WHERE yuntil an entity exists);async_removed_from_registry), so neither a reload nor an entity_id rename clears them; an address shared by several entities keeps its issues until the last one is removed;_report_fault/_clear_faultfor faults only an entity can see.health_owner()), so the primary's entry owns every issue and a recovery seen during the failover or after the failback clears it.clear()always asks the registry: an issue raised before a reload (unknown to the new tracker) still clears.unresponsive_zonemoved onto the tracker; same text,{zone}→{device}. The issue still passeszonetoo, so a translation that kept{zone}(e.g. in Crowdin) renders.device_fault_<entry_id>_<kind>_<who>_<where>, so the existing_<entry_id>_sweep inasync_remove_entrycovers them.gateway.device_faults, without device names.docs/diagnostics/repair-issues.md, including what to do with an actuator that will not be repaired soon (disable the entity, don't delete it; "Ignore" on the repair is temporary).Evidence and protocol checks
EVID-MH200-WHAT19-FAULT, fix(light): keep the last state when a light reports an unknown WHAT #456):parse_and_validate_frameflags "Dimension 11 is not officially documented for WHO=1001". OPEN.db describes a 24-bit autodiagnostic mask but no per-bit meanings. The Encyclopedia says the WHO 1004 active-low fault labels must not be copied to WHO 1001.*#1001*0*7*111111111111111101101111##. Zero bits do not mean a fault, which is why the mask is evidence only.Tests
tests/test_device_health.pycovers:clear_all, clearing an issue the tracker does not hold;process_message.tests/test_device_health_corpus.py:tests/fixtures/traces(MH200, MH200N, MH201, MH202, F454, F455, F461, H4890, MyHomeServer1), 30 of which carry WHO 1/1001 frames. Only the two MH200 address 74 captures may raise a fault, with the expected evidence;test_poll_healthandtest_climate_central_discoverymoved to the new issue ids.test_where_usage_guardlists the reviewed WHERE read inmessage_where.test_component_alarm,test_issue_311,test_issue_466_h4890_trace,test_issue_564_mh202_alarm_trace) also fail on the base branch and are unrelated (alarm decoding);alarm_control_panel.py);scripts/verify_ha_standards.pypasses.Not covered yet
One fault example: the only positive is one actuator on one MH200 firmware. Bench captures with the load removed (F411/F414/F418U2 on MH200, MH200N, F454, MyHomeServer1;
*#1*W##and*#1001*W*7##before and after) would show whether WHAT 19 is universal and which mask bits change against a healthy baseline.Permanent silence: a faulty actuator that will not be repaired cannot yet be silenced for good. An
ignored_addressesoption (skip discovery, entities and health tracking for listed WHO/WHERE pairs) would be a separate PR.Next classifiers: WHO 1004 heating faults and WHO 18 DIMENSION 250 Stop&Go breakers are specified in the published docs. The corpus has no frames of either yet, so each waits for a capture.
V2 Architecture: New entities implement
handle_event()and do not manually subscribe viaasync_dispatcher_connect. (No new entities.)🤖 Generated with Claude Code