From 627421ca6d4f2b3047f64a667097858b766b160a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:32:47 +0000 Subject: [PATCH 1/2] docs: add ZHA and Z-Wave setup guides and README section Agent-Logs-Url: https://github.com/TheLarsinator/zigbee-floorplan-card/sessions/da4f536e-ec2b-41c5-9838-3e6340e35115 Co-authored-by: TheLarsinator <6304557+TheLarsinator@users.noreply.github.com> --- README.md | 29 +++++ docs/ZHA-SETUP.md | 154 ++++++++++++++++++++++ docs/ZWAVE-SETUP.md | 306 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 489 insertions(+) create mode 100644 docs/ZHA-SETUP.md create mode 100644 docs/ZWAVE-SETUP.md diff --git a/README.md b/README.md index d92b732..4fec7d6 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,35 @@ This card uses MQTT to get the complete network map from Zigbee2MQTT, including - ✅ Single source of truth (Zigbee2MQTT) - ✅ Accurate device types and network topology +## Alternative Integrations + +The card is not limited to Zigbee2MQTT. Any integration that can provide a sensor with `nodes` and `links` attributes in the expected format is supported. The following integrations are documented: + +### ZHA (Zigbee Home Automation) + +ZHA is the built-in Zigbee integration in Home Assistant. To use the card with ZHA you need: + +1. The [zha_toolkit](https://github.com/mdeweerd/zha-toolkit) HACS integration to retrieve device topology +2. A template sensor that listens for the `zha_device_list` event +3. An automation that fires the event on a schedule + +Device identifiers in `device_coordinates` use **IEEE addresses** (e.g., `"0x00158d0001a2b3c4"`), the same format as Zigbee2MQTT. + +See **[docs/ZHA-SETUP.md](docs/ZHA-SETUP.md)** for the full setup guide, including the template sensor and automation configuration. + +### Z-Wave (Z-Wave JS) + +The card can also visualize Z-Wave mesh networks by reading routing topology from the Z-Wave JS integration. To use the card with Z-Wave you need: + +1. The [pyscript](https://github.com/custom-components/pyscript) HACS integration to read Z-Wave JS controller data +2. A pyscript service that fires a `zwave_device_list` event with nodes and links +3. A template sensor that listens for the event +4. An automation that calls the service on startup and on a schedule + +Device identifiers in `device_coordinates` use **node IDs as strings** (e.g., `"1"`, `"5"`, `"12"`). + +See **[docs/ZWAVE-SETUP.md](docs/ZWAVE-SETUP.md)** for the full setup guide, including the pyscript service, template sensor, and automation configuration. + ## Quick Start See [QUICKSTART.md](docs/QUICKSTART.md) for a step-by-step guide to get started! diff --git a/docs/ZHA-SETUP.md b/docs/ZHA-SETUP.md new file mode 100644 index 0000000..b74db7c --- /dev/null +++ b/docs/ZHA-SETUP.md @@ -0,0 +1,154 @@ +# ZHA (Zigbee Home Automation) Setup Guide + +This guide explains how to use the Zigbee Floorplan Card with the **ZHA (Zigbee Home Automation)** integration instead of Zigbee2MQTT. + +## Overview + +The card works with ZHA by using: + +1. A **template sensor** that builds the network map from ZHA device data +2. An **automation** that periodically fires a ZHA device list event using [zha_toolkit](https://github.com/mdeweerd/zha-toolkit) + +## Prerequisites + +- ✅ Home Assistant running with ZHA integration +- ✅ [zha_toolkit](https://github.com/mdeweerd/zha-toolkit) installed (available via HACS) +- ✅ A floorplan image ready + +## Step 1: Install zha_toolkit + +The automation relies on `zha_toolkit` to retrieve the ZHA device list and fire the event that the template sensor listens for. + +1. Open HACS in Home Assistant +2. Search for **"ZHA Toolkit"** and install it +3. Restart Home Assistant + +## Step 2: Add the Template Sensor + +Add this sensor to your `configuration.yaml`. It listens for a `zha_device_list` event fired by the automation and builds the `nodes` and `links` attributes that the card requires. + +```yaml +template: + - sensor: + - name: "ZHA Devices List" + state: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}" + attributes: + nodes: > + {% set ns = namespace(output=[], ieee_names={}) %} + {% for entity_id in integration_entities('zha') %} + {% set did = device_id(entity_id) %} + {% if did %} + {% for id_pair in device_attr(did, 'identifiers') | default([], true) %} + {% if id_pair[0] == 'zha' and id_pair[1] not in ns.ieee_names %} + {% set ns.ieee_names = dict(ns.ieee_names, **{id_pair[1]: device_attr(did, 'name_by_user') or device_attr(did, 'name')}) %} + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + {% for device in trigger.event.data.devices %} + {% set current = { + 'ieeeAddr': device.ieee, + 'friendly_name': ns.ieee_names.get(device.ieee, device.friendly_name), + 'type': device.device_type, + 'networkAddress': device.nwk + } %} + {% set ns.output = ns.output + [current] %} + {% endfor %} + {{ ns.output }} + links: > + {% set ns = namespace(output=[]) %} + {% set relationships = { + 'None': 0, + 'Child': 1, + 'Sibling': 2, + 'Parent': 3, + 'Neighbor': 4 + } %} + {% for device in trigger.event.data.devices %} + {% for neighbor in device.neighbors %} + {% set current = { + 'source': {'ieeeAddr': device.ieee }, + 'target': {'ieeeAddr': neighbor.ieee}, + 'lqi': int(neighbor.lqi), + 'relationship': relationships.get(neighbor.relationship) + } %} + {% set ns.output = ns.output + [current] %} + {% endfor %} + {% endfor %} + {{ ns.output }} + trigger: + - trigger: event + event_type: "zha_device_list" +``` + +## Step 3: Add the Automation + +This automation calls `zha_toolkit.zha_devices` on a schedule to fire the `zha_device_list` event that the sensor listens for. + +```yaml +alias: ZHA Device List Refresh +description: "" +triggers: + - trigger: time_pattern + hours: "*" + minutes: "10" + seconds: "0" +conditions: [] +actions: + - action: zha_toolkit.zha_devices + data: + event_success: zha_device_list +mode: single +``` + +> **Tip:** The automation runs every hour at the 10-minute mark. You can adjust the schedule as needed. + +## Step 4: Restart Home Assistant + +Restart Home Assistant to load the new sensor and automation. + +After restart, you can manually trigger the automation to populate the sensor immediately: + +- **Developer Tools → Services → `zha_toolkit.zha_devices`** + - Data: `event_success: zha_device_list` + +## Step 5: Add the Card + +The card configuration for ZHA is the same as for Zigbee2MQTT, except you reference the new sensor entity. Device identifiers use **IEEE addresses** (e.g., `0x00158d0001a2b3c4`). + +```yaml +type: custom:zigbee-floorplan-card +entity: sensor.zha_devices_list +image: /local/floorplan.png +device_coordinates: + "0xbc026efffe29c7de": { x: 500, y: 400 } # Coordinator + "0x00158d0001a2b3c4": { x: 200, y: 200 } # Living Room Light + # Add more devices... +``` + +## Finding IEEE Addresses for ZHA Devices + +1. Go to **Settings → Devices & Services → Zigbee Home Automation** +2. Click on any device +3. The IEEE address is shown in the device information panel (format: `0x...`) + +Alternatively, after the sensor is populated: + +1. Go to **Developer Tools → States** +2. Find `sensor.zha_devices_list` +3. Expand the `nodes` attribute to see all IEEE addresses + +## Troubleshooting + +**Sensor state is unavailable or empty** +- Verify `zha_toolkit` is installed and restart Home Assistant +- Manually trigger the `zha_toolkit.zha_devices` service call +- Check Developer Tools → States for `sensor.zha_devices_list` + +**No links showing** +- ZHA must have scanned neighbors for your devices — this data is collected during normal operation +- Run a **scan** from the ZHA interface to refresh neighbor tables + +**Device names showing as codes** +- Ensure devices have friendly names set in the ZHA interface (**Settings → Devices & Services → Zigbee Home Automation → device → rename**) +- The template sensor resolves names from `name_by_user` first, then falls back to `name` diff --git a/docs/ZWAVE-SETUP.md b/docs/ZWAVE-SETUP.md new file mode 100644 index 0000000..f82c23a --- /dev/null +++ b/docs/ZWAVE-SETUP.md @@ -0,0 +1,306 @@ +# Z-Wave (Z-Wave JS) Setup Guide + +This guide explains how to use the Zigbee Floorplan Card with the **Z-Wave JS** integration to visualize your Z-Wave mesh network. + +## Overview + +The card works with Z-Wave JS by using: + +1. A **pyscript** service that reads the Z-Wave JS controller data and fires an event containing nodes and routing topology +2. An **automation** that calls the pyscript on a schedule or when routes are rebuilt + +## Prerequisites + +- ✅ Home Assistant running with Z-Wave JS integration +- ✅ [pyscript](https://github.com/custom-components/pyscript) installed (available via HACS) +- ✅ A floorplan image ready + +## Step 1: Install pyscript + +1. Open HACS in Home Assistant +2. Search for **"Pyscript"** and install it +3. Restart Home Assistant +4. Create the pyscript directory if it does not exist: `/config/pyscript/` + +## Step 2: Add the Pyscript Service + +Create a new file `/config/pyscript/update_zwave_device_list.py` with the following content: + +```python +"""Fetch Z-Wave device list with routing topology data.""" + +import datetime + + +@service +def update_zwave_device_list(): + """Collect Z-Wave device data including neighbor/routing information and fire event.""" + + from homeassistant.helpers import device_registry as dr + + devices = [] + node_map = {} + controller_id = None + + # Build node_id -> friendly_name map from device registry + dev_reg = dr.async_get(hass) + node_names = {} + for device in dev_reg.devices.values(): + for identifier in device.identifiers: + if identifier[0] == "zwave_js": + id_str = str(identifier[1]) + parts = id_str.split("-") + try: + nid = int(parts[-1]) + node_names[nid] = device.name_by_user or device.name or f"Node {nid}" + except (ValueError, IndexError): + pass + + # Find Z-Wave JS controller via config entries (HA 2024+ pattern) + controller = None + config_entries = hass.config_entries.async_entries("zwave_js") + + if not config_entries: + log.error("No Z-Wave JS config entries found") + return + + for entry in config_entries: + try: + runtime_data = getattr(entry, "runtime_data", None) + if runtime_data is None: + continue + + if hasattr(runtime_data, "client"): + client = runtime_data.client + elif isinstance(runtime_data, dict) and "client" in runtime_data: + client = runtime_data["client"] + else: + continue + + if client and hasattr(client, "driver") and client.driver: + controller = client.driver.controller + if controller and hasattr(controller, "nodes"): + break + except Exception as ex: + log.warning(f"Could not read entry {entry.entry_id}: {ex}") + + if not controller: + log.error("No Z-Wave JS controller found – check integration status") + return + + # First pass: collect node data and find controller + node_stats = {} # Store stats for link building + for node_id, node in controller.nodes.items(): + friendly_name = node_names.get(node_id, f"Node {node_id}") + + is_controller = getattr(node, "is_controller_node", False) + if is_controller: + controller_id = node_id + + # Device type + if is_controller: + device_type = "Coordinator" + elif getattr(node, "is_listening", False) and getattr(node, "is_routing", False): + device_type = "Router" + else: + device_type = "EndDevice" + + # Get statistics for RSSI and routing info + stats = getattr(node, "statistics", None) + rssi = None + if stats: + rssi = getattr(stats, "rssi", None) + node_stats[node_id] = stats + + device_info = { + "node_id": node_id, + "friendly_name": friendly_name, + "device_type": device_type, + "is_listening": getattr(node, "is_listening", False), + "is_routing": getattr(node, "is_routing", False), + "is_controller": is_controller, + "neighbors": [], # Will be populated from LWR + "rssi": rssi, + } + devices.append(device_info) + node_map[node_id] = device_info + + devices.sort(key=lambda d: d["node_id"]) + + if controller_id is None: + controller_id = 1 # Default assumption + + # Build links from LWR (Last Working Route) data + links = [] + seen_links = set() # Avoid duplicate links + + for node_id, stats in node_stats.items(): + if node_id == controller_id: + continue # Controller doesn't have routes to itself + + lwr = getattr(stats, "lwr", None) + if not lwr: + continue + + # Get RSSI for link quality + rssi = getattr(lwr, "rssi", None) + if rssi is not None and isinstance(rssi, (int, float)): + lqi = max(0, min(255, int((rssi + 100) * 3.64))) + else: + lqi = 128 + + # Get repeaters in the route + repeaters = getattr(lwr, "repeaters", []) or [] + repeater_ids = [] + for r in repeaters: + if hasattr(r, "node_id"): + repeater_ids.append(r.node_id) + + # Build the route chain: node -> repeaters -> controller + if repeater_ids: + route = [node_id] + repeater_ids + [controller_id] + else: + route = [node_id, controller_id] + + # Create links for each hop in the route + for i in range(len(route) - 1): + src = route[i] + tgt = route[i + 1] + + # Create canonical link key to avoid duplicates + link_key = (min(src, tgt), max(src, tgt)) + if link_key in seen_links: + continue + seen_links.add(link_key) + + # Determine relationship + if tgt == controller_id: + relationship = 3 # Parent (connecting to controller) + elif src == controller_id: + relationship = 1 # Child + else: + relationship = 2 # Sibling (repeater connection) + + links.append({ + "source": {"ieeeAddr": str(src)}, + "target": {"ieeeAddr": str(tgt)}, + "lqi": lqi, + "relationship": relationship, + }) + + # Update neighbors list for the device + if src in node_map and tgt not in node_map[src]["neighbors"]: + node_map[src]["neighbors"].append(tgt) + if tgt in node_map and src not in node_map[tgt]["neighbors"]: + node_map[tgt]["neighbors"].append(src) + + # Fire event consumed by the template sensor + event.fire( + "zwave_device_list", + devices=devices, + links=links, + timestamp=datetime.datetime.now().isoformat(), + ) + + log.info(f"Z-Wave device list updated: {len(devices)} devices, {len(links)} links") +``` + +## Step 3: Add the Template Sensor + +Add this to your `configuration.yaml`. It listens for the `zwave_device_list` event and builds the attributes the card requires. + +```yaml +template: + - sensor: + - name: "ZWave Devices List" + state: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}" + attributes: + nodes: "{{ trigger.event.data.devices }}" + links: "{{ trigger.event.data.links }}" + trigger: + - trigger: event + event_type: "zwave_device_list" +``` + +## Step 4: Add the Automation + +This automation calls the pyscript service on startup, on a schedule, and whenever Z-Wave routes are rebuilt. + +```yaml +- id: zwave_device_list_update + alias: "Z-Wave Device List Update" + description: "Collects Z-Wave device data and fires zwave_device_list event" + trigger: + - trigger: homeassistant + event: start + id: startup + - trigger: time_pattern + hours: "/6" + id: scheduled + - trigger: event + event_type: zwave_js_rebuild_node_routes_done + id: routes_updated + action: + - delay: + seconds: "{{ 30 if trigger.id == 'startup' else 0 }}" + - service: pyscript.update_zwave_device_list + mode: single +``` + +> **Tip:** The automation runs every 6 hours and also fires when routes are rebuilt. You can adjust the `hours: "/6"` schedule as needed. + +## Step 5: Restart Home Assistant + +Restart Home Assistant to load the pyscript, sensor, and automation. + +After restart, you can manually trigger the service: + +- **Developer Tools → Services → `pyscript.update_zwave_device_list`** + +## Step 6: Add the Card + +For Z-Wave, device identifiers are **node IDs** (as strings, e.g., `"1"`, `"5"`, `"12"`), not IEEE addresses. Use the numeric node ID reported by Z-Wave JS. + +```yaml +type: custom:zigbee-floorplan-card +entity: sensor.zwave_devices_list +image: /local/floorplan.png +device_coordinates: + "1": { x: 500, y: 400 } # Controller (node 1) + "5": { x: 200, y: 200 } # Living Room Switch + "12": { x: 700, y: 300 } # Bedroom Sensor + # Add more devices... +``` + +## Finding Node IDs for Z-Wave Devices + +1. Go to **Settings → Devices & Services → Z-Wave JS** +2. Click on any device +3. The **Node ID** is shown in the device information panel + +Alternatively, after the sensor is populated: + +1. Go to **Developer Tools → States** +2. Find `sensor.zwave_devices_list` +3. Expand the `nodes` attribute — each node has a `node_id` field + +## Troubleshooting + +**Sensor state is unavailable or empty** +- Verify `pyscript` is installed and the script file is in `/config/pyscript/` +- Restart Home Assistant and manually call `pyscript.update_zwave_device_list` +- Check Home Assistant logs for pyscript errors + +**"No Z-Wave JS controller found" in logs** +- Ensure the Z-Wave JS integration is running and connected to the Z-Wave stick +- Try restarting Home Assistant and re-running the service + +**No links showing** +- Links are derived from the **Last Working Route (LWR)** data in Z-Wave JS +- After initial setup, rebuild routes in Z-Wave JS and re-run the service +- Not all nodes have LWR data until they have communicated with the controller + +**Device names showing as "Node X"** +- Set friendly names for devices in Home Assistant: + **Settings → Devices & Services → Z-Wave JS → device → rename** +- Re-run the `pyscript.update_zwave_device_list` service after renaming From d56488a3e268dd0635834ba038cebf471db45622 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:33:34 +0000 Subject: [PATCH 2/2] docs: improve controller_id fallback comment in ZWAVE-SETUP.md Agent-Logs-Url: https://github.com/TheLarsinator/zigbee-floorplan-card/sessions/da4f536e-ec2b-41c5-9838-3e6340e35115 Co-authored-by: TheLarsinator <6304557+TheLarsinator@users.noreply.github.com> --- docs/ZWAVE-SETUP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ZWAVE-SETUP.md b/docs/ZWAVE-SETUP.md index f82c23a..2d55e00 100644 --- a/docs/ZWAVE-SETUP.md +++ b/docs/ZWAVE-SETUP.md @@ -128,7 +128,7 @@ def update_zwave_device_list(): devices.sort(key=lambda d: d["node_id"]) if controller_id is None: - controller_id = 1 # Default assumption + controller_id = 1 # Z-Wave JS convention: the controller is always node 1 # Build links from LWR (Last Working Route) data links = []