From c2de5ba85bc44ef3d5a45f52e7489f5d23358e92 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Wed, 1 Jul 2026 01:22:56 -0700 Subject: [PATCH] zigbee2mqtt: patch Bridge TypeError on BLZ coordinator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Pine64 BLZ fork's zigbee2mqtt image crashes on start because Bridge.start() calls firstCoordinatorEndpoint().deviceIeeeAddress before the onMQTTMessage listener is registered. The BLZ coordinator does not register its endpoint (the coordinator is the dongle itself, no ZDO loopback), so firstCoordinatorEndpoint() returns undefined. The crash happens in publishInfo() BEFORE onMQTTMessage is wired up, which means every bridge MQTT request (permit_join, device rename, group ops, etc.) silently no-ops — even though the message is received. Add infra/zigbee2mqtt/scripts/blz-bridge-patch.js that applies two surgical fixes (idempotent): 1. Optional chain on the .deviceIeeeAddress access so the missing endpoint doesn't throw. 2. Move onMQTTMessage registration BEFORE publishInfo/devices/ groups/definitions, and wrap each publish in try/catch so Bridge.start() completes even if a single publish fails. Add 'just z2m-patch-bridge' to apply the patch on the live container after fresh image pulls. --- infra/zigbee2mqtt/scripts/blz-bridge-patch.js | 91 +++++++++++++++++++ justfile | 19 +++- 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 infra/zigbee2mqtt/scripts/blz-bridge-patch.js diff --git a/infra/zigbee2mqtt/scripts/blz-bridge-patch.js b/infra/zigbee2mqtt/scripts/blz-bridge-patch.js new file mode 100644 index 0000000..83b536c --- /dev/null +++ b/infra/zigbee2mqtt/scripts/blz-bridge-patch.js @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/* Patch zigbee2mqtt bridge.js to tolerate BLZ adapter quirks. + +The Pine64 `latest-dev` image of zigbee2mqtt crashes on start when used with +the BLZ (BL702) dongle because the Bridge extension calls +`firstCoordinatorEndpoint().deviceIeeeAddress` and the BLZ coordinator never +registers its endpoint — `firstCoordinatorEndpoint()` returns undefined. + +The crash happens inside `Bridge.start()` BEFORE the `onMQTTMessage` listener +is registered, so permit_join (and every other bridge MQTT request) silently +no-ops. + +This script applies two surgical patches: + +1. Optional-chaining on the `deviceIeeeAddress` access so the missing endpoint + doesn't throw. +2. Move `onMQTTMessage` registration to BEFORE the publishInfo/devices/groups/ + definitions calls AND wrap each publish in try/catch — so Bridge.start() + completes even if a single publish throws, and permit_join still works. + +Run inside the zigbee2mqtt container after image pull: + + docker exec zigbee2mqtt node /app/blz-bridge-patch.js + +Idempotent: re-running is safe (sed -i replaces whole matched block). +*/ + +const fs = require("fs"); + +const BRIDGE_PATH = process.argv[2] || "/app/dist/extension/bridge.js"; + +function patch(content) { + let changed = false; + + // Patch 1: optional chain on firstCoordinatorEndpoint().deviceIeeeAddress + const oldAccess = "ieee_address: this.zigbee.firstCoordinatorEndpoint().deviceIeeeAddress,"; + const newAccess = "ieee_address: this.zigbee.firstCoordinatorEndpoint()?.deviceIeeeAddress,"; + if (content.includes(oldAccess) && !content.includes(newAccess)) { + content = content.replace(oldAccess, newAccess); + changed = true; + console.log("[patch 1] added optional chaining on deviceIeeeAddress"); + } else if (content.includes(newAccess)) { + console.log("[patch 1] already applied"); + } else { + console.warn("[patch 1] target line not found, skipping"); + } + + // Patch 2: move onMQTTMessage registration to BEFORE publishInfo/devices/groups/definitions + const oldStartTail = ` await this.publishInfo(); + await this.publishDevices(); + await this.publishGroups(); + await this.publishDefinitions(); + this.eventBus.onMQTTMessage(this, this.onMQTTMessage);`; + + const newStartTail = ` this.eventBus.onMQTTMessage(this, this.onMQTTMessage); + // BLZ adapter workaround: each publish wrapped so Bridge.start completes + // even if a single publish fails (e.g. BLZ coordinator endpoint missing). + for (const [name, fn] of [["publishInfo", this.publishInfo], ["publishDevices", this.publishDevices], ["publishGroups", this.publishGroups], ["publishDefinitions", this.publishDefinitions]]) { + try { await fn.call(this); } catch (err) { logger_1.default.error("Bridge " + name + " failed: " + err.message); } + }`; + + if (content.includes(oldStartTail)) { + content = content.replace(oldStartTail, newStartTail); + changed = true; + console.log("[patch 2] moved onMQTTMessage before publish calls + added try/catch"); + } else if (content.includes(newStartTail)) { + console.log("[patch 2] already applied"); + } else { + console.warn("[patch 2] target block not found, skipping"); + } + + return { content, changed }; +} + +const original = fs.readFileSync(BRIDGE_PATH, "utf8"); +const { content, changed } = patch(original); + +if (!changed) { + console.log("no changes needed"); + process.exit(0); +} + +// Backup original once (only if .bak does not exist) +const bak = BRIDGE_PATH + ".bak"; +if (!fs.existsSync(bak)) { + fs.writeFileSync(bak, original); + console.log("backup written to " + bak); +} + +fs.writeFileSync(BRIDGE_PATH, content); +console.log("patched " + BRIDGE_PATH); diff --git a/justfile b/justfile index f640038..73806cb 100644 --- a/justfile +++ b/justfile @@ -155,9 +155,26 @@ doorbell-publish TITLE BODY="Someone is at the door": doorbell-rebuild: ssh {{RPI_HOST}} 'cd {{RPI_DIR}}/nr-flow-validator && docker compose build doorbell-listener && docker compose up -d doorbell-listener' +# Patch zigbee2mqtt's bridge.js for BLZ adapter compatibility. +# The Pine64 latest-dev image crashes on start because Bridge.start() calls +# firstCoordinatorEndpoint().deviceIeeeAddress BEFORE registering the +# onMQTTMessage listener, so permit_join (and every other bridge MQTT +# request) silently no-ops. +# +# Run this after every fresh image pull OR after the zigbee2mqtt container +# is recreated (the patch is lost on image rebuild). +z2m-patch-bridge: + @echo "Copying patch script into zigbee2mqtt container..." + ssh {{RPI_HOST}} 'docker cp {{justfile_directory()}}/infra/zigbee2mqtt/scripts/blz-bridge-patch.js zigbee2mqtt:/tmp/blz-bridge-patch.js' + @echo "Applying patch (idempotent)..." + ssh {{RPI_HOST}} 'docker exec zigbee2mqtt node /tmp/blz-bridge-patch.js' + @echo "Restarting zigbee2mqtt..." + ssh {{RPI_HOST}} 'docker restart zigbee2mqtt' + @echo "Done. Verify with: just z2m-logs" + # Show paired zigbee devices and their friendly names z2m-devices: - ssh {{RPI_HOST}} 'cat {{RPI_DIR}}/zigbee2mqtt/configuration.yaml' > /tmp/z2m.yaml + ssh {{RPI_HOST}} 'cat {{RPI_DIR}}/nr-flow-validator/infra/zigbee2mqtt/configuration.yaml' > /tmp/z2m.yaml python3 scripts/diag.py z2m-devices /tmp/z2m.yaml # Show per-device persisted state from z2m