From 0216bb7e524b2e7bfedcd0b22fb09e4bc5219d19 Mon Sep 17 00:00:00 2001 From: Adrien Bertrand Date: Fri, 27 Mar 2026 19:25:10 +0100 Subject: [PATCH] feat: emberAdapter: add SEND_MULTICASTS_TO_SLEEPY_ADDRESS stack config. Note: several commercial Zigbee stacks/coordinators use this option. P.S. This happens to be a "solution" to this zigbee2mqtt issue: https://github.com/koenkk/zigbee2mqtt/issues/30247 --- src/adapter/ember/adapter/emberAdapter.ts | 15 +++++++++++++++ test/adapter/ember/emberAdapter.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index a6c722e45a..905f55342c 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -172,6 +172,8 @@ type StackConfig = { TRANSIENT_KEY_TIMEOUT_S: number; /**@see Ezsp.ezspSetRadioIeee802154CcaMode */ CCA_MODE?: keyof typeof IEEE802154CcaMode; + /** (Default: undefined) @see EzspConfigId.SEND_MULTICASTS_TO_SLEEPY_ADDRESS */ + SEND_MULTICASTS_TO_SLEEPY_ADDRESS?: boolean; }; /** @@ -194,6 +196,7 @@ export const DEFAULT_STACK_CONFIG: Readonly = { END_DEVICE_POLL_TIMEOUT: 8, // zigpc: 8 TRANSIENT_KEY_TIMEOUT_S: 300, // zigpc: 65535 CCA_MODE: undefined, // not set by default + SEND_MULTICASTS_TO_SLEEPY_ADDRESS: undefined, // not set by default }; /** Default behavior is to disable app key requests */ const ALLOW_APP_KEY_REQUESTS = false; @@ -357,6 +360,11 @@ export class EmberAdapter extends Adapter { logger.error("[STACK CONFIG] Invalid CCA_MODE, ignoring.", NS); } + if (config.SEND_MULTICASTS_TO_SLEEPY_ADDRESS != null && typeof config.SEND_MULTICASTS_TO_SLEEPY_ADDRESS !== "boolean") { + config.SEND_MULTICASTS_TO_SLEEPY_ADDRESS = undefined; + logger.error("[STACK CONFIG] Invalid SEND_MULTICASTS_TO_SLEEPY_ADDRESS, ignoring.", NS); + } + logger.info(`Using stack config ${JSON.stringify(config)}.`, NS); return config; } catch { @@ -700,6 +708,13 @@ export class EmberAdapter extends Adapter { await this.ezsp.ezspSetRadioIeee802154CcaMode(IEEE802154CcaMode[this.stackConfig.CCA_MODE]); } + // Whether group commands (multicast messages) will be sent to sleepy end-devices too + // (we don't override what the stack was compiled with, if the stackConfig here does not explicitly have a value) + if (this.stackConfig.SEND_MULTICASTS_TO_SLEEPY_ADDRESS != null) { + // validated in `loadStackConfig` + await this.emberSetEzspConfigValue(EzspConfigId.SEND_MULTICASTS_TO_SLEEPY_ADDRESS, +this.stackConfig.SEND_MULTICASTS_TO_SLEEPY_ADDRESS); + } + // WARNING: From here on EZSP commands that affect memory allocation on the NCP should no longer be called (like resizing tables) await this.registerFixedEndpoints(); diff --git a/test/adapter/ember/emberAdapter.test.ts b/test/adapter/ember/emberAdapter.test.ts index 09a4b2fb7e..d44a9e88ea 100644 --- a/test/adapter/ember/emberAdapter.test.ts +++ b/test/adapter/ember/emberAdapter.test.ts @@ -513,6 +513,7 @@ describe("Ember Adapter Layer", () => { END_DEVICE_POLL_TIMEOUT: 12, TRANSIENT_KEY_TIMEOUT_S: 500, CCA_MODE: "SIGNAL_AND_RSSI", + SEND_MULTICASTS_TO_SLEEPY_ADDRESS: true, }; writeFileSync(STACK_CONFIG_PATH, JSON.stringify(config, undefined, 2)); @@ -612,6 +613,7 @@ describe("Ember Adapter Layer", () => { END_DEVICE_POLL_TIMEOUT: 12, TRANSIENT_KEY_TIMEOUT_S: 500, CCA_MODE: "SIGNAL_AND_RSSI", + SEND_MULTICASTS_TO_SLEEPY_ADDRESS: true, }; writeFileSync(STACK_CONFIG_PATH, JSON.stringify(config, undefined, 2)); @@ -625,6 +627,7 @@ describe("Ember Adapter Layer", () => { expect(mockEzspSetConfigurationValue).toHaveBeenCalledWith(EzspConfigId.MAX_END_DEVICE_CHILDREN, config.MAX_END_DEVICE_CHILDREN); expect(mockEzspSetConfigurationValue).toHaveBeenCalledWith(EzspConfigId.END_DEVICE_POLL_TIMEOUT, config.END_DEVICE_POLL_TIMEOUT); expect(mockEzspSetConfigurationValue).toHaveBeenCalledWith(EzspConfigId.TRANSIENT_KEY_TIMEOUT_S, config.TRANSIENT_KEY_TIMEOUT_S); + expect(mockEzspSetConfigurationValue).toHaveBeenCalledWith(EzspConfigId.SEND_MULTICASTS_TO_SLEEPY_ADDRESS, 1); expect(mockEzspSetConcentrator).toHaveBeenCalledWith( true, EMBER_LOW_RAM_CONCENTRATOR, @@ -658,6 +661,26 @@ describe("Ember Adapter Layer", () => { unlinkSync(STACK_CONFIG_PATH); }); + it("Starts with custom stack config invalid SEND_MULTICASTS_TO_SLEEPY_ADDRESS", async () => { + const config = { + SEND_MULTICASTS_TO_SLEEPY_ADDRESS: 42, + }; + + writeFileSync(STACK_CONFIG_PATH, JSON.stringify(config, undefined, 2)); + + adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); + const result = adapter.start(); + + await vi.advanceTimersByTimeAsync(5000); + await expect(result).resolves.toStrictEqual("resumed"); + expect(mockEzspSetConfigurationValue).not.toHaveBeenCalledWith(EzspConfigId.SEND_MULTICASTS_TO_SLEEPY_ADDRESS, 0); + expect(mockEzspSetConfigurationValue).not.toHaveBeenCalledWith(EzspConfigId.SEND_MULTICASTS_TO_SLEEPY_ADDRESS, 1); + expect(loggerSpies.error).toHaveBeenCalledWith("[STACK CONFIG] Invalid SEND_MULTICASTS_TO_SLEEPY_ADDRESS, ignoring.", "zh:ember"); + + // cleanup + unlinkSync(STACK_CONFIG_PATH); + }); + it("Starts with restored when no network in adapter", async () => { adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const expectedNetParams: EmberNetworkParameters = {