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