zigbee2mqtt: patch Bridge TypeError on BLZ coordinator
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.
This commit is contained in:
@@ -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);
|
||||||
@@ -155,9 +155,26 @@ doorbell-publish TITLE BODY="Someone is at the door":
|
|||||||
doorbell-rebuild:
|
doorbell-rebuild:
|
||||||
ssh {{RPI_HOST}} 'cd {{RPI_DIR}}/nr-flow-validator && docker compose build doorbell-listener && docker compose up -d doorbell-listener'
|
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
|
# Show paired zigbee devices and their friendly names
|
||||||
z2m-devices:
|
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
|
python3 scripts/diag.py z2m-devices /tmp/z2m.yaml
|
||||||
|
|
||||||
# Show per-device persisted state from z2m
|
# Show per-device persisted state from z2m
|
||||||
|
|||||||
Reference in New Issue
Block a user