homelab-codex-ws/services/ha-mcp/tests/fixtures/states.json

112 lines
3.2 KiB
JSON
Raw Normal View History

feat(ha-mcp): read-only MCP server (faza 2a) Own minimal MCP server exposing the live state of the HA instances in services/home-assistant/instances.yaml to Claude Code over stdio — the phase-2 "MCP read-only" gate in services/home-assistant/DESIGN.md. Operator decision 2026-07-30: build our own rather than adopt hass-mcp, so the tools reuse scripts/ha/lib/{ha_api,ha_ws}.py (one token-handling story for the whole HA toolchain) and can answer from the repo and from instances.yaml, which a generic server cannot. Seven tools, all read-only, default instance `ken`: list_entities, get_state, get_areas, find_entities_by_description, read_automation, list_automations, instance_status. Read-only by construction, not by policy: REST goes through ha_api.Client (get/get_raw_text only — no POST method exists), WebSocket commands are checked against a three-entry *_list allowlist before being sent, and read_automation reads services/home-assistant/config/<instance>/ rather than /api/config. Tests assert all three, including a grep guard that fails if requests.post/call_service ever appears in the package. The write path stays repo + scripts/ha/deploy.sh. Details that follow from how this instance actually behaves: - unavailable is never silent — every entity view carries unavailable + unavailable_since, every list a count. The 2026-07-23 audit traced ~15 silently dead automations to conditions sitting on dead sensors. - chelsty-ha (status: offline in instances.yaml) is answered from the file, never dialed — no 5s timeout for a known-offline LTE site. - areas come from the WS registries (entity area_id > device area_id) with a storage-export fallback; area_source/area_note say which was used and what the offline export cannot resolve. - PL->EN fuzzy matching, since the house is Polish and the entity_ids are transliterated English: "czujnik temperatury salon" -> sensor.thsalon_temperature, each hit explaining why it matched. - 5s timeouts and errors returned as {"error": ...} inside a normal tool result — a missing token or an unreachable instance never crashes the server or hangs the agent. Registered for Claude Code in the repo-root .mcp.json (new file) as `ha`, via services/ha-mcp/run.sh (prefers the venv, falls back to system python3). The mcp SDK lives in services/ha-mcp/.venv — rationale for venv over --break-system-packages is in the README. Tests: 42 offline (no network, no HA, no token) + a live read-only smoke against ken — HA 2026.7.2, 1647 entities, 377 unavailable, 115 automations, 13 areas.
2026-07-30 16:37:38 +02:00
[
{
"entity_id": "sensor.thsalon_temperature",
"state": "24.3",
"attributes": {
"friendly_name": "thSalon Temperature",
"device_class": "temperature",
"unit_of_measurement": "°C"
},
"last_changed": "2026-07-27T09:12:03.101000+00:00",
"last_updated": "2026-07-27T09:12:03.101000+00:00"
},
{
"entity_id": "sensor.thsalon_humidity",
"state": "49.5",
"attributes": {
"friendly_name": "thSalon Humidity",
"device_class": "humidity",
"unit_of_measurement": "%"
},
"last_changed": "2026-07-27T09:12:03.102000+00:00",
"last_updated": "2026-07-27T09:12:03.102000+00:00"
},
{
"entity_id": "select.thsalon_temperature_unit",
"state": "celsius",
"attributes": {
"friendly_name": "thSalon Temperature unit",
"options": ["celsius", "fahrenheit"]
},
"last_changed": "2026-07-26T21:14:41.396310+00:00",
"last_updated": "2026-07-26T21:14:41.396310+00:00"
},
{
"entity_id": "sensor.thsypialnia_temperature",
"state": "unavailable",
"attributes": {
"friendly_name": "thSypialnia Temperature",
"device_class": "temperature"
},
"last_changed": "2026-07-17T15:07:11.000000+00:00",
"last_updated": "2026-07-17T15:07:11.000000+00:00"
},
{
"entity_id": "binary_sensor.0x00124b00251472ef_occupancy",
"state": "unavailable",
"attributes": {
"friendly_name": "mdWejscie Occupancy",
"device_class": "occupancy"
},
"last_changed": "2026-07-17T15:07:12.000000+00:00",
"last_updated": "2026-07-17T15:07:12.000000+00:00"
},
{
"entity_id": "light.salon_lampa",
"state": "on",
"attributes": {
"friendly_name": "Lampa salon",
"brightness": 180
},
"last_changed": "2026-07-27T18:40:00.000000+00:00",
"last_updated": "2026-07-27T18:40:00.000000+00:00"
},
{
"entity_id": "switch.tasmota_14",
"state": "on",
"attributes": {
"friendly_name": "Tasmota 14"
},
"last_changed": "2026-03-26T04:00:00.000000+00:00",
"last_updated": "2026-03-26T04:00:00.000000+00:00"
},
{
"entity_id": "automation.klima_salon_off",
"state": "on",
"attributes": {
"id": "1784804668795",
"friendly_name": "Klima salon: wyłącz chłodzenie i osusz parownik",
"last_triggered": "2026-07-27T19:31:00.000000+00:00",
"mode": "single",
"current": 0
},
"last_changed": "2026-07-26T21:15:19.374224+00:00",
"last_updated": "2026-07-27T19:31:00.000000+00:00"
},
{
"entity_id": "automation.mirror_on",
"state": "on",
"attributes": {
"id": "1636927813132",
"friendly_name": "Mirror ON",
"last_triggered": "2026-07-24T04:30:00.102503+00:00",
"mode": "single",
"current": 0
},
"last_changed": "2026-07-26T21:15:19.375401+00:00",
"last_updated": "2026-07-26T21:15:19.375401+00:00"
},
{
"entity_id": "automation.only_on_instance",
"state": "off",
"attributes": {
"id": "9999999999999",
"friendly_name": "Created in the UI, never imported",
"mode": "single",
"current": 0
},
"last_changed": "2026-07-26T21:15:19.376000+00:00",
"last_updated": "2026-07-26T21:15:19.376000+00:00"
}
]