Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

basic spam control / throttle #24122

Merged
merged 13 commits into from
Oct 2, 2024
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ Zigbee2MQTT is made up of three modules, each developed in its own Github projec

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`
ivanfmartinez marked this conversation as resolved.
Show resolved Hide resolved
Before submitting changes run `npm run test-with-coverage` and `npm run pretty:check`
ivanfmartinez marked this conversation as resolved.
Show resolved Hide resolved

## Supported devices

Expand Down
7 changes: 5 additions & 2 deletions lib/extension/receive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ export default class Receive extends Extension {
const options: KeyValue = data.device.options;
zhc.postProcessConvertedFromZigbeeMessage(data.device.definition, payload, options);

if (settings.get().advanced.elapsed || data.device.options.min_elapsed) {
const checkElapsedTime =
data.device.options.min_elapsed || (data.device.options.description && data.device.options.description.includes('SPAMMER'));

if (settings.get().advanced.elapsed || checkElapsedTime) {
const now = Date.now();
if (this.elapsed[data.device.ieeeAddr]) {
payload.elapsed = now - this.elapsed[data.device.ieeeAddr];
Expand All @@ -144,7 +147,7 @@ export default class Receive extends Extension {
// - 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'))) {
if (checkElapsedTime) {
let min_elapsed = 30000;
if (data.device.options.min_elapsed) {
min_elapsed = data.device.options.min_elapsed;
Expand Down
63 changes: 59 additions & 4 deletions test/receive.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,8 @@ describe('Receive', () => {
});

it('Should ignore multiple messages from spamming devices', async () => {
const device = zigbeeHerdsman.devices.SPAMMER;
const device = zigbeeHerdsman.devices.SPAMMER1;
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);
Expand Down Expand Up @@ -397,7 +396,7 @@ describe('Receive', () => {
expect(MQTT.publish).toHaveBeenCalledTimes(1);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/0x0017880104e455ff');
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/' + device.ieeeAddr);
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});

Expand All @@ -407,7 +406,63 @@ describe('Receive', () => {
await zigbeeHerdsman.events.message(payload3);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/0x0017880104e455ff');
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/' + device.ieeeAddr);
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('Should ignore multiple messages from spamming devices defined by description', async () => {
const device = zigbeeHerdsman.devices.SPAMMER2;
const start = Date.now();
const min_elapsed_for_testing = 50000;
settings.set(['device_options', 'retain'], true);
settings.set(['devices', device.ieeeAddr, 'description'], 'this is a SPAMMER device');
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/' + device.ieeeAddr);
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/' + device.ieeeAddr);
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});
});
Expand Down
5 changes: 3 additions & 2 deletions test/stub/zigbeeHerdsman.js
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,9 @@ 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'),
// These are not a real spammer devices, just copy of previous to test the spam filter
SPAMMER1: new Device('EndDevice', '0x0017880104e455fe', 6539, 4151, [new Endpoint(1, [0], [])], true, 'Battery', 'lumi.weather'),
SPAMMER2: 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
Loading