Commit 6f63f68
feat: Matter & Thread support + unified ha_manage_radio management tool (#1696)
* feat: Matter node diagnostics + ha_manage_radio scaffold (#1693)
ha_get_device now enriches Matter devices with node_diagnostics (network
type wifi/thread, reachability, IPs, joined fabrics), mirroring the existing
Z-Wave node_status / ZHA radio-metrics enrichers.
New ha_manage_radio multi-modal tool with a per-radio handler package
(radio/). Matter handler is complete: diagnostics, ping, network_status,
commission, commission_on_network, share_out (open_commissioning_window),
interview, remove_fabric (auto-resolves this HA's fabric), and Thread/WiFi
credential provisioning. zwave/zigbee/thread handlers are scaffolded behind
the shared dispatch contract (base.py) pending implementation.
Unit tests cover the Matter enricher (incl. graceful degradation) and the
dispatcher contract (action validation, required-param + destructive-confirm
gates, entity_id resolution).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: implement zwave/zigbee/thread radio handlers + matter firmware + network-health includes (#1693)
Completes ha_manage_radio across all four radios:
- Z-Wave (10 actions): diagnostics, network_status, ping, add (SmartStart/QR
non-interactive; interactive S2 PIN pairing -> HA UI), remove_device
(failed/exclusion), reinterview, rebuild_routes, set_config_param,
firmware_update (update.install), hard_reset
- Zigbee/ZHA (18 actions): diagnostics, network_status, permit_join,
remove_device, reconfigure, group + member CRUD, bind/unbind, cluster
read/write/command, network backup/restore, change_channel, firmware_update
- Thread/OTBR (7 actions): network_status, list_datasets, discover_routers,
create_network, set_network, set_channel, add_dataset
- Matter: add firmware_update for parity (update.install on the node's
update entity)
ha_get_system_health gains matter_network + thread_network include sections
(otbr/info + matter config-entry summary), mirroring zwave_network/zha_network.
WS command strings verified against HA core; service-only ZHA ops and
update.install go through client.call_service. Destructive actions gated on
confirm=True; long-running ops return operation handles. 84 unit tests across
the dispatcher and all four handlers (mock-WS; pytest runs in CI per the
on-device bionic-wheel limitation).
Also fixes a base.py import depth (..errors -> ...errors) that would have
broken radio-package import in CI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(e2e): graceful-degradation + validation e2e for ha_manage_radio (#1693)
Bare-container e2e (no real radios): asserts ha_manage_radio is registered,
the validation gates fire (unknown action lists supported, missing required
param, destructive-without-confirm), zwave/thread network_status degrade to
available=False, and ha_get_system_health exposes the thread_network/
matter_network includes without crashing. Deep behavior stays unit-tested
(mock-WS) + user live-test post-merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: mypy strictness + CodeQL empty-except in radio handlers (#1696)
- base.resolve_entry_id returns str|None (str-cast the registry value)
- base.confirm_required typed -> None (drop unused type:ignore)
- zigbee/matter id-resolve helpers accept Any (args are dynamically typed);
rename optional permit_join ieee to avoid a str/str|None reassignment
- tools_radio dispatcher returns a typed dict
- annotate the best-effort zha/topology/update except (CodeQL py/empty-except)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: drop now-unused ToolError import in radio/base.py (#1696)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(radio): address PR review findings (#1696)
- BUG: Matter node_diagnostics dropped IPs — upstream NodeDiagnostics misspells
the field 'ip_adresses' (one d); read the real key, surface it as
ip_addresses. The test mock hid this; mock + assertion corrected.
- Surface each action's summary in the unknown-action error (ActionSpec.summary
was dead and its docstring over-claimed); makes the field load-bearing.
- ActionSpec.long_running is now the single source of truth — the dispatcher
fills it in, covering branches that omitted it (e.g. force-remove).
- Drop the dispatcher's manual logger.error: exception_to_structured_error owns
logging and was being double-logged.
- Gate thread set_network with confirm=True (it replaces the active Thread
network and can drop devices), matching create_network/set_channel.
- Align the 'no update entity' error to ENTITY_NOT_FOUND across zwave/zigbee/
matter (was three different codes).
- Log the best-effort zha/topology/update failure at debug instead of bare pass.
- Remove dead RADIOS constant; fix docstrings that over-claimed (no operation
'handle'; ping is not in the read tools; service calls are not ZHA-only;
config_param is not a real param; thread summary is a list not keyed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(hacs): retry repo registration once on timeout in ha_install_mcp_tools
A freshly-added HACS repo can miss the first registration budget on a loaded
runner or slow GitHub (intermittently flakes test_install_mcp_tools_*). On that
timeout, re-add to re-nudge HACS and wait once more before surfacing the
failure. Best-effort — a genuinely stuck add still fails on the retry; a repo
that already had an ID is re-raised immediately as a real failure. Happy path
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(radio): cover untested handler bodies + SUPPORTED↔handle round-trip (#1696)
Adds success-path record-and-assert tests for the Matter write actions
(commission, commission_on_network, interview, set_thread, set_wifi_credentials,
network_status both branches, firmware no-entity) and the Zigbee
group/restore/member/cluster actions, plus a dispatcher-contract suite: a
ws_call failure surfaces ToolError, every SUPPORTED action across all four
radios is reachable by handle() (drift guard), and entity_id-not-found resolves
to ENTITY_NOT_FOUND.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(radio): keep unknown-action 'supported' a list, summaries in suggestions (#1696)
The summary-surfacing change made context['supported'] a dict, breaking the e2e
assertion that it is a list. Keep 'supported' as the sorted action-name list
(stable shape) and surface each action's summary via 'suggestions' instead, so
the summary field is still load-bearing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(radio): pass confirm=True for now-destructive set_network + add its gate test (#1696)
Making set_network destructive (review fix) required confirm=True; the two
existing set_network success tests omitted it and started failing the confirm
gate. Add confirm=True to both and a dedicated set_network_requires_confirm
test. Verified locally: 211 radio unit tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(radio): normalize matter diagnostics ip_adresses->ip_addresses in handler (#1696)
Live testing showed ha_manage_radio(matter, diagnostics) returned the raw
upstream 'ip_adresses' typo while ha_get_device surfaced the corrected
'ip_addresses' — inconsistent. Normalize in the handler too (copying the dict
so the upstream result isn't mutated). Verified live + 40 unit tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(radio): remove_fabric requires explicit other-controller index, not HA's own (#1696)
Live testing revealed that defaulting remove_fabric to HA's own active fabric
makes the Matter server return 'Operation aborted' — removing the active fabric
severs HA's session before the confirmation returns (the device DOES detach,
but reports as an error). The clean detach-from-HA path is deleting the device.
remove_fabric now requires params.fabric_index for a DIFFERENT controller and
refuses HA's own/active index with actionable guidance to use ha_remove_device,
listing active_fabrics so the caller can pick. Verified locally (26 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(radio): narrow zigbee topology-refresh except to ToolError (Gemini review, #1696)
Gemini flagged the best-effort zha/topology/update catching broad Exception.
Narrow to ToolError: ws_call normalizes transport failures to a ToolError, so
catching just that swallows a command-level topology failure (debug-logged)
while letting unexpected programmer errors propagate. The other Gemini point
(redundant dispatcher logger.error) was already removed in f5d3826.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(radio): address Patch76 review — write-action legibility + DRY + guards (#1696)
- Write/management actions now RAISE (integration_required) when the integration
isn't configured, instead of returning the graceful success:true/available:false
payload — an agent keying on success could otherwise read a no-op management
action as done. Read actions (network_status) still degrade gracefully.
Affects zwave add/remove_device(exclusion)/rebuild_routes(network)/hard_reset
and thread create/set_network/set_channel.
- DRY: fold the three near-duplicate update.* firmware-entity resolvers into
base.resolve_update_entity(client, device_id, platform=...).
- zwave add: enforce exactly-one of the mutually-exclusive inclusion credentials
(HA add_node uses vol.Exclusive) with a clear conflict error.
- Remove 4 dead 'raise AssertionError' lines sitting after NoReturn
raise_tool_error calls (kept the intentional end-of-handler unhandled guards).
Verified locally: 207 radio unit tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: restore py/mixed-returns terminals after NoReturn raise_tool_error (#1696)
CI CodeQL (py/mixed-returns) failed at zigbee.py and tools_radio.py: removing
the trailing 'raise AssertionError' (a Patch76 nit, applied last commit) left an
implicit None fall-through mixed with explicit str returns. Those terminals are
NOT dead code — they exist to satisfy the gate (same as the documented
'return None # py/mixed-returns' terminals elsewhere in the repo). Restored in
all three resolvers with an explanatory comment so they aren't stripped again.
My local CodeQL check only covers implicit-string-concat-in-list, so it missed
this; the CI gate is authoritative.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(radio): address Patch76 follow-up — zwave write coverage, firmware parity, cluster_command docs (#1696)
- Add per-action integration-absent tests for the zwave write actions (add,
remove_device exclusion path, rebuild_routes network scope, hard_reset) that
raise integration_required when zwave_js is unconfigured — mirrors thread's
test_set_network_no_otbr_raises; the read path was the only one covered.
- Align zwave/zigbee firmware_update TO matter (drop the captured result),
not the reverse. update.install registers with no supports_response, so
call_service returns only the transient affected-states (the entity flipping
to in_progress), not the async install outcome; surfacing it on a long_running
install misreads as the result. All three now return entity_id + long_running.
- cluster_command ActionSpec now steers callers to params (HA deprecated args in
favor of params; both still accepted), forwarding unchanged for back-compat.
Verified locally: 88 radio unit tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: kingpanther13 <kingpanther13@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent 27c804b commit 6f63f68
16 files changed
Lines changed: 4094 additions & 6 deletions
File tree
- src/ha_mcp/tools
- radio
- tests/src
- e2e/tools
- unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
0 commit comments