Skip to content

Commit

Permalink
basic spam control
Browse files Browse the repository at this point in the history
  • Loading branch information
ivanfmartinez committed Sep 27, 2024
1 parent 403d3c0 commit a2016d0
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
32 changes: 31 additions & 1 deletion lib/extension/receive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions lib/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
59 changes: 59 additions & 0 deletions test/receive.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -670,4 +727,6 @@ describe('Receive', () => {
await zigbeeHerdsman.events.message(payload);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/devices');
});


});
2 changes: 2 additions & 0 deletions test/stub/zigbeeHerdsman.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down

0 comments on commit a2016d0

Please sign in to comment.