mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-10-23 22:32:40 +00:00
fix: Apply throttle and debounce to last seen updates from non-emitted frames (#33423)
This commit is contained in:
1 parent
c588eca20e
commit
7234d5bdbc
3 files changed
+77
-1
No files matched your search
@@ -21,6 +21,29 @@ export default class Receive extends Extension {
|
||||
override async start(): Promise<void> {
|
||||
this.eventBus.onPublishEntityState(this, this.onPublishEntityState);
|
||||
this.eventBus.onDeviceMessage(this, this.onDeviceMessage);
|
||||
this.eventBus.onLastSeenChanged(this, this.onLastSeenChanged);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frames that zigbee-herdsman does not emit as a message (e.g. default responses) only fire a
|
||||
* lastSeenChanged event. For devices with debounce or throttle set, publish those through the same
|
||||
* per-device debounce/throttle as regular messages instead of publishing the full state right away.
|
||||
* See `utils.publishLastSeen()`, which skips them for these devices.
|
||||
*/
|
||||
@bind async onLastSeenChanged(data: eventdata.LastSeenChanged): Promise<void> {
|
||||
const lastSeen = settings.get().advanced.last_seen;
|
||||
|
||||
if (data.reason !== "messageNonEmitted" || !lastSeen || lastSeen === "disable") {
|
||||
return;
|
||||
}
|
||||
|
||||
const {debounce, debounce_ignore, throttle} = data.device.options;
|
||||
|
||||
if (debounce) {
|
||||
this.publishDebounce(data.device, {}, debounce, debounce_ignore);
|
||||
} else if (throttle) {
|
||||
await this.publishThrottle(data.device, {}, throttle);
|
||||
}
|
||||
}
|
||||
|
||||
@bind onPublishEntityState(data: eventdata.PublishEntityState): void {
|
||||
|
||||
+9
-1
@@ -288,6 +288,10 @@ export const hours = (hours: number): number => 1000 * 60 * 60 * hours;
|
||||
export const minutes = (minutes: number): number => 1000 * 60 * minutes;
|
||||
export const seconds = (seconds: number): number => 1000 * seconds;
|
||||
|
||||
function isDebouncedOrThrottled(device: Device): boolean {
|
||||
return Boolean(device.options.debounce || device.options.throttle);
|
||||
}
|
||||
|
||||
async function publishLastSeen(
|
||||
data: eventdata.LastSeenChanged,
|
||||
settings: Settings,
|
||||
@@ -300,8 +304,12 @@ async function publishLastSeen(
|
||||
* message based on the received zigbee message. In this case allowMessageEmitted has to be true.
|
||||
* - In case reason !== messageEmitted, controller.ts will call this based on the zigbee-herdsman
|
||||
* lastSeenChanged event.
|
||||
* - In case reason == messageNonEmitted and the device has debounce or throttle set, receive.ts publishes
|
||||
* it through the per-device debounce/throttle, so skip it here.
|
||||
*/
|
||||
const allow = data.reason !== "messageEmitted" || (data.reason === "messageEmitted" && allowMessageEmitted);
|
||||
const allow =
|
||||
(data.reason !== "messageEmitted" || (data.reason === "messageEmitted" && allowMessageEmitted)) &&
|
||||
!(data.reason === "messageNonEmitted" && isDebouncedOrThrottled(data.device));
|
||||
if (settings.advanced.last_seen && settings.advanced.last_seen !== "disable" && allow) {
|
||||
await publishEntityState(data.device, {}, "lastSeenChanged");
|
||||
}
|
||||
|
||||
@@ -214,6 +214,51 @@ describe("Extension: Receive", () => {
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/weather_sensor");
|
||||
});
|
||||
|
||||
it("Should throttle last seen updates from frames that are not emitted as a message", async () => {
|
||||
const device = devices.WSDCGQ11LM;
|
||||
settings.set(["devices", device.ieeeAddr, "throttle"], 1);
|
||||
settings.set(["advanced", "last_seen"], "ISO_8601");
|
||||
const devicePublishes = (): unknown[] => mockMQTTPublishAsync.mock.calls.filter((c) => c[0] === "zigbee2mqtt/weather_sensor");
|
||||
|
||||
await mockZHEvents.lastSeenChanged({device, reason: "messageNonEmitted"});
|
||||
await mockZHEvents.lastSeenChanged({device, reason: "messageNonEmitted"});
|
||||
await mockZHEvents.lastSeenChanged({device, reason: "messageNonEmitted"});
|
||||
await flushPromises();
|
||||
expect(devicePublishes()).toHaveLength(1);
|
||||
|
||||
vi.advanceTimersByTime(2000);
|
||||
await flushPromises();
|
||||
expect(devicePublishes()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("Should debounce last seen updates from frames that are not emitted as a message", async () => {
|
||||
const device = devices.WSDCGQ11LM;
|
||||
settings.set(["devices", device.ieeeAddr, "debounce"], 0.1);
|
||||
settings.set(["advanced", "last_seen"], "ISO_8601");
|
||||
const devicePublishes = (): unknown[] => mockMQTTPublishAsync.mock.calls.filter((c) => c[0] === "zigbee2mqtt/weather_sensor");
|
||||
|
||||
await mockZHEvents.lastSeenChanged({device, reason: "messageNonEmitted"});
|
||||
await mockZHEvents.lastSeenChanged({device, reason: "messageNonEmitted"});
|
||||
await flushPromises();
|
||||
vi.advanceTimersByTime(50);
|
||||
expect(devicePublishes()).toHaveLength(0);
|
||||
|
||||
vi.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
expect(devicePublishes()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("Should publish last seen updates from frames that are not emitted as a message right away without debounce or throttle", async () => {
|
||||
const device = devices.WSDCGQ11LM;
|
||||
settings.set(["advanced", "last_seen"], "ISO_8601");
|
||||
const devicePublishes = (): unknown[] => mockMQTTPublishAsync.mock.calls.filter((c) => c[0] === "zigbee2mqtt/weather_sensor");
|
||||
|
||||
await mockZHEvents.lastSeenChanged({device, reason: "messageNonEmitted"});
|
||||
await mockZHEvents.lastSeenChanged({device, reason: "messageNonEmitted"});
|
||||
await flushPromises();
|
||||
expect(devicePublishes()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("Should debounce and retain messages when set via device_options", async () => {
|
||||
const device = devices.WSDCGQ11LM;
|
||||
settings.set(["device_options", "debounce"], 0.1);
|
||||
|
||||
Reference in new issue
Block a user