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:
2026-07-01 01:22:56 -07:00
parent 758edc0a9a
commit c2de5ba85b
2 changed files with 109 additions and 1 deletions
@@ -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);