diff --git a/README.md b/README.md index e48a028953..ef3e524210 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ Zigbee2MQTT is made up of three modules, each developed in its own Github projec ### Developing Zigbee2MQTT uses TypeScript (partially for now). Therefore after making changes to files in the `lib/` directory you need to recompile Zigbee2MQTT. This can be done by executing `npm run build`. For faster development instead of running `npm run build` you can run `npm run build-watch` in another terminal session, this will recompile as you change files. +In first time before building you need to run `npm i --save-dev @types/node` ## Supported devices diff --git a/lib/extension/receive.ts b/lib/extension/receive.ts index 2292eb991a..97e32c8e6e 100755 --- a/lib/extension/receive.ts +++ b/lib/extension/receive.ts @@ -125,10 +125,40 @@ export default class Receive extends Extension { const options: KeyValue = data.device.options; zhc.postProcessConvertedFromZigbeeMessage(data.device.definition, payload, options); - if (settings.get().advanced.elapsed) { + if (settings.get().advanced.elapsed || data.device.options.min_elapsed) { const now = Date.now(); if (this.elapsed[data.device.ieeeAddr]) { payload.elapsed = now - this.elapsed[data.device.ieeeAddr]; + + // very simple and dirty anti-spamming https://github.com/Koenkk/zigbee2mqtt/issues/17984 + // as a proof of concept maybe Koenkk can find a better solution as the debounce does not help for my SPAMMER devices + // ambient sensor and water level that sometimes send mupliple messages on same second + // this will not help on zigbee network, but at least help on mqtt and homeassistant recorder and history + // this will not work for devices that have actions and specific events that are important + // this will only DISCARD messages that came to fast from device + // it solves the SPAMMING on sensor devices that does not change values too fast and messages can be ignored + // I dont know all the side effects of this code, but here is the ones that I found already + // - on web ui, the last-seen is only updated after a non ignored message + // - web ui are more responsive than before + // - my homeassistant does not have a lot of data from this devices that are not need + // - my homeassistant became more responsive + // - the CPU load are sensible lower + // using "SPAMMER" in description is an easy way to test without changing options on yaml + if (data.device.options.min_elapsed || + (data.device.options.description && data.device.options.description.includes("SPAMMER")) + ) { + let min_elapsed = 30000; + if (data.device.options.min_elapsed) { + min_elapsed = data.device.options.min_elapsed; + } + + if (payload.elapsed < min_elapsed) { + logger.debug(`Ignoring message from SPAMMER - ${data.device.ieeeAddr} - ${data.device.options.friendly_name} - elapsed=${payload.elapsed} - min_elapsed=${min_elapsed}`); + return; + } + } + // end of changes + } this.elapsed[data.device.ieeeAddr] = now; diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 49fc62600d..69c99628c7 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -234,6 +234,7 @@ declare global { retrieve_state?: boolean; debounce?: number; debounce_ignore?: string[]; + min_elapsed?: number; filtered_attributes?: string[]; filtered_cache?: string[]; filtered_optimistic?: string[]; diff --git a/test/receive.test.js b/test/receive.test.js index 283a4f2d47..43d8fd3578 100755 --- a/test/receive.test.js +++ b/test/receive.test.js @@ -355,6 +355,63 @@ describe('Receive', () => { expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2}); }); + it('Should ignore multiple messages from spamming devices', async () => { + const device = zigbeeHerdsman.devices.SPAMMER; + const start = Date.now(); + // Using low elapsed to dont fail the test by elapsed time + const min_elapsed_for_testing = 500; + settings.set(['device_options', 'min_elapsed'], min_elapsed_for_testing); + settings.set(['device_options', 'retain'], true); + const data1 = {measuredValue: 1}; + const payload1 = { + data: data1, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; + await zigbeeHerdsman.events.message(payload1); + const data2 = {measuredValue: 2}; + const payload2 = { + data: data2, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; + await zigbeeHerdsman.events.message(payload2); + const data3 = {measuredValue: 3}; + const payload3 = { + data: data3, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; + await zigbeeHerdsman.events.message(payload3); + await flushPromises(); + + expect(MQTT.publish).toHaveBeenCalledTimes(1); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/0x0017880104e455ff'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.01}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: true}); + + // Now we try after elapsed time to see if it publishes + const timeshift = min_elapsed_for_testing + 500; + jest.advanceTimersByTime(timeshift); + await zigbeeHerdsman.events.message(payload3); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/0x0017880104e455ff'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ elapsed: timeshift, temperature: 0.03}); + expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: true}); + }); + it('Shouldnt republish old state', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/3572 const device = zigbeeHerdsman.devices.bulb; @@ -670,4 +727,6 @@ describe('Receive', () => { await zigbeeHerdsman.events.message(payload); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/devices'); }); + + }); diff --git a/test/stub/zigbeeHerdsman.js b/test/stub/zigbeeHerdsman.js index f8f54f9319..3763cdaebc 100644 --- a/test/stub/zigbeeHerdsman.js +++ b/test/stub/zigbeeHerdsman.js @@ -503,6 +503,8 @@ const devices = { 'lumi.sensor_86sw2.es1', ), WSDCGQ11LM: new Device('EndDevice', '0x0017880104e45522', 6539, 4151, [new Endpoint(1, [0], [])], true, 'Battery', 'lumi.weather'), + // This is not a real spammer device, just copy of previous to test the spam filter + SPAMMER: new Device('EndDevice', '0x0017880104e455ff', 6539, 4151, [new Endpoint(1, [0], [])], true, 'Battery', 'lumi.weather'), RTCGQ11LM: new Device('EndDevice', '0x0017880104e45523', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Battery', 'lumi.sensor_motion.aq2'), ZNCZ02LM: ZNCZ02LM, E1743: new Device('Router', '0x0017880104e45540', 6540, 4476, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'TRADFRI on/off switch'),