Commit 7125284
* feat: component-backed bulk state reads and full blueprint bodies (#1813)
Adds two ha_mcp_tools capabilities to Phase 1 (issue #1813), each behind the
established capability gate with a byte-identical legacy fallback.
states (A1): ha_get_state resolves single- and bulk-mode reads through one
in-process ha_mcp_tools/states frame (State.as_dict() per hit) instead of up to
100 REST GETs. The component read is the shared fetch primitive for both modes;
MAX_ENTITIES=100 is enforced server-side regardless of backend; a component-
reported missing id is classified through the same path as a legacy 404, so the
per-id ENTITY_NOT_FOUND error and the ha_search() suggestion are preserved on
both paths. Falls back to the per-id REST fetch silently (logged) when the
component is absent, downlevel, or errors.
blueprint_get (A2): ha_get_blueprint now returns the full parsed blueprint body
under config (core's blueprint/list serves metadata only). The component reads
the on-disk file, path-jailed under <config>/blueprints/<domain>/ (symlink-safe
containment mirroring the file-tool jail) with the read+parse offloaded to the
executor via the async prep. !input markers are preserved as {"__input__": name}
and every other custom tag (!secret/!include/...) is neutralized to None at load
time, so no resolved secret plaintext can reach the body. The dead
if "blueprint" in blueprint_data branch is removed and the overpromising
docstring corrected for the capability-miss (metadata-only) path.
Component: adds _do_states + _do_blueprint_get (+ prep, jailed reader, blueprint
YAML loader), CAPABILITIES entries, _command_specs rows, schemas, and the module
docstring inventory. No SCHEMA_VERSION or component version bump (additive).
Tests: fake-hass component unit tests, server routing tests, and cross-seam
contract tests covering bulk parity, missing-id handling, MAX_ENTITIES,
blueprint path-traversal rejection, !secret neutralization, capability-miss and
unknown_command fallbacks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: component-backed device reads for get/remove and radio resolves (#1813)
Adds the device_get / device_list capabilities to Phase 1 (issue #1813) behind
the established capability gate, and routes the single-device consumers that
dumped the whole device registry for one lookup through them, each with a
byte-identical legacy fallback.
Component: _do_device_get returns one DeviceEntry.dict_repr by id
({device: dict_repr | None}); _do_device_list returns them all
({devices: [...]}). The body is core's DeviceEntry.dict_repr returned VERBATIM
(never _plainify'd) so the WS transport encodes it with the same JSON serializer
config/device_registry/list uses (it sends json_bytes(entry.dict_repr)) —
byte-identical to one legacy list element by construction, the device analog of
State.as_dict() for the states capability. Adds CAPABILITIES entries,
_command_specs rows, schemas, and the module-docstring inventory. No
SCHEMA_VERSION or component version bump (additive).
Server (shared component_devices helper owns the caps-gated fetch + error
taxonomy in one place):
- ha_get_device: a single lookup reads just the target via device_get and a
list read via device_list, so neither dumps the whole device registry; the
entity registry is still read for a device's entity list (no per-device entity
capability) and skipped entirely for a summary list. Single-device fallback
(no caps / unknown_command) goes straight to the legacy list, not device_list.
- ha_remove_device: its body reads the device via device_get; the shared
@with_auto_backup capture (backup_manager._fetch_device, also ha_set_device's
pre-write snapshot) routes through device_get too — the identical dump fetched
twice per remove is gone.
- ha_manage_radio: _resolve_ieee (all 7 ZHA call sites share it) resolves the
device via device_get.
zha/zwave/matter enrichment and config-entry removal stay on their dedicated
paths. resolve_update_entity is left on the legacy path deliberately: it reads
the ENTITY registry filtered by device_id, which a DeviceEntry-shaped capability
cannot serve (that needs an entities-for-device capability, outside item B).
Tests: fake-hass component unit tests for both _do_ functions (raw dict_repr
verbatim, missing/absent handling, drift-degrades-to-None), schema + admin-gate
+ info-shape coverage, per-tool routing tests (capability-hit, capability-miss
fallback, unknown_command fallback + invalidate_caps, and the auto-backup
capture read routing), and one cross-seam contract test that drives the REAL
_do_device_get through ha_get_device, ha_remove_device, and _resolve_ieee to pin
the raw DeviceEntry shape across every consumer site.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: entities-for-device join on device_get for ha_get_device and radio update resolve (#1813)
Extends the device_get capability (added in cce7fa0) with an opt-in
include_entities join so a single-device lookup lists that device's entities
without dumping the whole entity registry. With include_entities the component
returns the device's config/entity_registry/list-shaped rows
(RegistryEntry.as_partial_dict, disabled included via
er.async_entries_for_device(..., include_disabled_entities=True)) as a SIBLING
entities key; the raw DeviceEntry dict stays untouched, so the byte-parity
contract holds by construction (the join is a sibling, not a field).
Component (websocket_api.py): _device_get_schema gains
Optional("include_entities", default=False); _do_device_get adds the entities
key only when requested via _device_entities / _entries_for_device /
_entity_partial_dict (as_partial_dict verbatim, mirroring _device_dict_repr).
No SCHEMA_VERSION or component-version bump — additive under the pending 1.1.0
device_get already opened.
Server consumers:
- ha_get_device single lookup reads the device AND its entities from one
device_get(include_entities) frame, killing the full entity-registry dump;
entity_id mode now resolves the device via a single native
config/entity_registry/get instead of the full entity map; full-detail LIST
mode is the only path that still dumps the entity registry.
- resolve_update_entity (radio/base.py) reads the device's rows via the join,
filtering update.* / platform client-side exactly as before.
- device_list stays entity-free.
Backward-compat: the join is additive within schema_version 1, so its absence
degrades to legacy rather than depending on a version bump. An older device_get
predating the param rejects the extra field via the command's PREVENT_EXTRA
base schema (mapped to the None miss), and, belt-and-suspenders, a response that
carries the device but no entities key is treated the same —
fetch_device_entities_via_component and _single_device_and_entities both fall
back to config/entity_registry/list for the entity half rather than reporting
zero entities. Documented in the component_devices module docstring.
Tests: cross-seam contract test pins both raw shapes (DeviceEntry dict_repr +
entity as_partial_dict rows) through the real join; ha_get_device routing tests
updated for the killed entity dump (hit / entity_id-resolve / miss-fallback /
unknown_command+invalidate / entities-half-absent tolerance); new
resolve_update_entity routing suite with the same matrix; component unit tests
for the include_entities join (schema, disabled-included, omitted-when-not-
requested, unknown-device empty).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: entity enrichment fields and component-backed exposure reads (#1813)
ha_search gains opt-in result_fields= emitting area/floor/labels/aliases on
both the component and legacy paths (default shape unchanged); ha_get_entity
gains the same fields additively via the entity_enrich capability.
ha_get_entity_exposure routes through the new exposure capability with
names/areas joined in-process and a targeted single-entity mode, keeping the
legacy keys byte-identical and new fields strictly additive.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: mypy variable reuse in _all_exposable_entity_ids
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: reuse area-mode haystack registry entries for result_fields enrichment and restore legacy device-registry fetch order
The area+query search path fetched config/entity_registry/get_entries twice
when result_fields requested enrichment (once for the alias haystack, once
inside the enrichment join); the haystack entries map is now threaded through
as prefetched entries so the flow costs one call. Raised by Gemini Code
Assist review; its proposed fix (dropping the haystack fetch) would have
broken alias matching, so the consolidation went the other way.
Also restores device-registry-before-entity-registry fetch order in
ha_get_device's legacy fallback, which the #1297 error-contract test pins.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: surface degraded enrichment and blueprint reads, close review-found gaps (#1813)
Partial batch from the pr-review-toolkit round (completed in follow-up):
degraded-enrichment warnings, blueprint null-config warning, device-resolve
error branching, narrowed unknown-entity match, defensive-path signals,
dead KNOWN_ASSISTANTS removal, docstring corrections, device-seam
error-taxonomy tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump component to 1.1.1 (1.1.0 shipped stable)
Phase 1 capabilities now ride the new pending version; parity test and
module docstring inventory updated. MIN_COMPONENT_VERSION stays 1.1.0
(capability-gated, no new required service).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: complete device-seam and exposure error-taxonomy coverage (#1813)
Finish the pr-review-toolkit fix batch left partial by the interrupted agent.
The behavioral fixes (F1-F6) and doc corrections (D1-D5) landed in the prior
commit; this adds the remaining test items and formats that commit's files:
- device_get non-unknown error (timeout) falls back to the legacy registries
without invalidating caps -- a transient failure is not a downgrade (T1)
- fetch_device_list_via_component taxonomy mirrored from device_get:
unknown_command -> invalidate caps; non-unknown -> fallback keeps caps;
malformed devices shape -> fallback (T2)
- device_get "device" key absent shape guard -> None -> legacy, distinct from
the authoritative {"device": None} verdict (T4)
- exposure single-mode merge with missing entity_info (None) or a stateless
entity_info (registry keys only) omits the live-state keys without crashing (T6)
Also apply ruff format to the prior partial commit's files (tools_search.py and
the blueprint/search-fields tests), which were committed unformatted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address post-ready Codex review findings on PR #1895
Five findings from the Codex bot review of the Phase 1 read-capability
seam (issue #1813), each with a matching regression test:
- ha_get_state component read: also catch HomeAssistantConnectionError so a
WS transport failure falls back to the REST legacy path (a SEPARATE
transport) instead of surfacing a spurious connection error. Unlike
ha_search/ha_get_overview -- whose legacy paths also read the WS registry
and fail identically -- ha_get_state's legacy read is REST get_entity_state
(tools_search.py). [P2]
- result_fields legacy enrichment: drop the COMPUTED_NAME alias sentinel
(serialized null over the WS registry read) instead of str()-casting it to
the literal "None", matching the component join's isinstance filter
(tools_search.py). [P3]
- area+query search: _fetch_area_entity_entries now returns None (not an
empty map) on a FAILED prefetch, so a failed read no longer masquerades as
an empty-but-successful prefetch that bypasses the degraded-enrichment
warning; enrichment re-fetches and reports the degradation
(tools_search.py). [P2]
- ha_get_device entity resolver: the legacy fallback uses a STRICT
entity-registry list read, so a double transient failure surfaces
SERVICE_CALL_FAILED instead of misreporting a real entity as
ENTITY_NOT_FOUND (tools_registry.py). [P2]
- bulk entity_enrich: chunk the WS command to _GET_ENTRIES_CHUNK_SIZE (the
same bound the sibling get_entries read uses) so a large bulk request
cannot produce an over-cap WebSocket frame; stays all-or-nothing
(tools_entities.py). [P2]
Byte-parity contracts unchanged; existing contract/routing 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 b1c68d4 commit 7125284
26 files changed
Lines changed: 6074 additions & 170 deletions
File tree
- custom_components/ha_mcp_tools
- src/ha_mcp
- client
- tools
- radio
- tests/src/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
| 27 | + | |
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
| 22 | + | |
23 | 23 | | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1720 | 1720 | | |
1721 | 1721 | | |
1722 | 1722 | | |
1723 | | - | |
| 1723 | + | |
| 1724 | + | |
| 1725 | + | |
| 1726 | + | |
| 1727 | + | |
| 1728 | + | |
| 1729 | + | |
| 1730 | + | |
| 1731 | + | |
| 1732 | + | |
| 1733 | + | |
| 1734 | + | |
1724 | 1735 | | |
1725 | 1736 | | |
1726 | 1737 | | |
1727 | 1738 | | |
1728 | | - | |
| 1739 | + | |
1729 | 1740 | | |
1730 | 1741 | | |
1731 | 1742 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1445 | 1445 | | |
1446 | 1446 | | |
1447 | 1447 | | |
| 1448 | + | |
1448 | 1449 | | |
1449 | 1450 | | |
1450 | 1451 | | |
| |||
1457 | 1458 | | |
1458 | 1459 | | |
1459 | 1460 | | |
1460 | | - | |
| 1461 | + | |
| 1462 | + | |
| 1463 | + | |
| 1464 | + | |
| 1465 | + | |
| 1466 | + | |
| 1467 | + | |
| 1468 | + | |
1461 | 1469 | | |
1462 | 1470 | | |
1463 | 1471 | | |
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
| |||
107 | 108 | | |
108 | 109 | | |
109 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
110 | 117 | | |
111 | | - | |
112 | | - | |
113 | | - | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
114 | 123 | | |
115 | 124 | | |
116 | 125 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
| |||
123 | 124 | | |
124 | 125 | | |
125 | 126 | | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
126 | 147 | | |
127 | 148 | | |
128 | 149 | | |
129 | 150 | | |
130 | 151 | | |
131 | 152 | | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
132 | 158 | | |
133 | | - | |
134 | | - | |
135 | | - | |
136 | | - | |
| 159 | + | |
| 160 | + | |
137 | 161 | | |
138 | 162 | | |
139 | 163 | | |
| |||
0 commit comments