Poll device when device does not support reporting. (#2122)

* Support emulation of attReport

Some device do not support attReport for some keys, this will emulate
it.

You can configure which keys that should be read when another device
send a message and the configured device a bind target or in a group the
message was send to.

```yaml
devices:
  '0x0017880104259333':
    friendly_name: bedroom/desk_lamp
    retain: true
    debounce: 0.5
    report_emulate:
      - brightness
      - color
```

Will have the brightness and color queried for example when a hue dimmer
sends commands to the bulb.

* Refactor polling.

* Finish poll tests.

* Update herdsman.

* Improve test stability.
This commit is contained in:
Jorge Schrauwen authored and Koen Kanters committed 2019-10-24 22:15:40 +02:00
1 parent ec5064bc4d
commit a813663de5
8 files changed
+167 -31

No files matched your search

+1 -2
View File
@@ -170,7 +170,7 @@ describe('Bridge config', () => {
await flushPromises();
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
const payload = JSON.parse(MQTT.publish.mock.calls[0][1]);
expect(payload).toStrictEqual({"message": [{"ID": 1, "friendly_name": "group_1", "retain": false, 'devices': [], optimistic: true}, {"ID": 2, "friendly_name": "group_2", "retain": false, "devices": [], optimistic: true}], "type": "groups"});
expect(payload).toStrictEqual({"message": [{"ID": 1, "friendly_name": "group_1", "retain": false, 'devices': [], optimistic: true}, {"ID": 2, "friendly_name": "group_2", "retain": false, "devices": [], optimistic: true}, {"ID": 15071, "friendly_name": "group_tradfri_remote", "retain": false, "devices": ['bulb_color_2', 'bulb_2'], optimistic: true}], "type": "groups"});
});
it('Should allow rename devices', async () => {
@@ -237,7 +237,6 @@ describe('Bridge config', () => {
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(controller.state[device.ieeeAddr]).toBeUndefined();
expect(settings.getDevice('bulb_color')).toBeNull();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
JSON.stringify({type: 'device_removed', message: 'bulb_color'}),
+35 -1
View File
@@ -6,13 +6,17 @@ zigbeeHerdsman.returnDevices.push('0x00124b00120144ae');
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b2');
zigbeeHerdsman.returnDevices.push('0x0017880104e45553');
zigbeeHerdsman.returnDevices.push('0x0017880104e45559');
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b4');
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b7');
const MQTT = require('./stub/mqtt');
const settings = require('../lib/util/settings');
const Controller = require('../lib/controller');
const flushPromises = () => new Promise(setImmediate);
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
jest.mock('debounce', () => jest.fn(fn => fn));
const debounce = require('debounce');
const mocksClear = [MQTT.publish, logger.warn, logger.debug];
const mocksClear = [MQTT.publish, logger.warn, logger.debug, debounce];
describe('Device report', () => {
let controller;
@@ -163,4 +167,34 @@ describe('Device report', () => {
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(1);
});
it('Should poll bounded Hue bulb when receiving message from Hue dimmer', async () => {
const remote = zigbeeHerdsman.devices.remote;
const data = {"button":3,"unknown1":3145728,"type":2,"unknown2":0,"time":1};
const payload = {data, cluster: 'manuSpecificPhilips', device: remote, endpoint: remote.getEndpoint(2), type: 'commandHueNotification', linkquality: 10, groupID: 0};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(debounce).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.devices.bulb_color.getEndpoint(1).read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]);
});
it('Should poll grouped Hue bulb when receiving message from TRADFRI remote and should', async () => {
const remote = zigbeeHerdsman.devices.tradfri_remote;
const data = {"stepmode":0,"stepsize":43,"transtime":5};
const payload = {data, cluster: 'genLevelCtrl', device: remote, endpoint: remote.getEndpoint(1), type: 'commandStepWithOnOff', linkquality: 10, groupID: 15071};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(debounce).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]);
// Should also only debounce once
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(debounce).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledTimes(2);
// Should only call Hue bulb, not e.g. tradfri
expect(zigbeeHerdsman.devices.bulb.getEndpoint(1).read).toHaveBeenCalledTimes(0);
});
});
+2 -2
View File
@@ -353,10 +353,10 @@ describe('Entity publish', () => {
it('Should create and publish to group which is in configuration.yaml but not in zigbee-herdsman', async () => {
delete zigbeeHerdsman.groups.group_2;
expect(Object.values(zigbeeHerdsman.groups).length).toBe(1);
expect(Object.values(zigbeeHerdsman.groups).length).toBe(2);
await MQTT.events.message('zigbee2mqtt/group_2/set', JSON.stringify({state: 'ON'}));
await flushPromises();
expect(Object.values(zigbeeHerdsman.groups).length).toBe(2);
expect(Object.values(zigbeeHerdsman.groups).length).toBe(3);
expect(zigbeeHerdsman.groups.group_2.command).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.groups.group_2.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
});
+17
View File
@@ -55,10 +55,18 @@ function writeDefaultConfiguration() {
retain: false,
friendly_name: "ikea_onoff"
},
'0x000b57fffec6a5b7': {
retain: false,
friendly_name: "bulb_2"
},
"0x000b57fffec6a5b3": {
retain: false,
friendly_name: "bulb_color"
},
'0x000b57fffec6a5b4': {
retain: false,
friendly_name: "bulb_color_2"
},
"0x0017880104e45541": {
retain: false,
friendly_name: "wall_switch"
@@ -118,6 +126,10 @@ function writeDefaultConfiguration() {
'0x0017880104e45560': {
retain: false,
friendly_name: 'livolo'
},
'0x90fd9ffffe4b64ae': {
retain: false,
friendly_name: 'tradfri_remote',
}
},
groups: {
@@ -128,6 +140,11 @@ function writeDefaultConfiguration() {
'2': {
friendly_name: 'group_2',
retain: false,
},
'15071': {
friendly_name: 'group_tradfri_remote',
retain: false,
devices: ['bulb_color_2', 'bulb_2']
}
}
};
+17 -7
View File
@@ -2,11 +2,11 @@ const events = {};
const assert = require('assert');
class Group {
constructor(groupID) {
constructor(groupID, members) {
this.groupID = groupID;
this.command = jest.fn();
this.meta = {};
this.members = [];
this.members = members;
this.hasMember = (endpoint) => this.members.includes(endpoint);
}
}
@@ -20,7 +20,7 @@ const clusters = {
}
class Endpoint {
constructor(ID, inputClusters, outputClusters, deviceIeeeAddress) {
constructor(ID, inputClusters, outputClusters, deviceIeeeAddress, binds=[]) {
this.deviceIeeeAddress = deviceIeeeAddress;
this.ID = ID;
this.inputClusters = inputClusters;
@@ -31,6 +31,7 @@ class Endpoint {
this.bind = jest.fn();
this.unbind = jest.fn();
this.configureReporting = jest.fn();
this.binds = binds;
this.supportsInputCluster = (cluster) => {
assert(clusters[cluster], `Undefined '${cluster}'`);
return this.inputClusters.includes(clusters[cluster]);
@@ -88,11 +89,17 @@ class Device {
const returnDevices = [];
const bulb_color = new Device('Router', '0x000b57fffec6a5b3', 40399, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b3')], true, "Mains (single phase)", "LLC020");
const bulb_color_2 = new Device('Router', '0x000b57fffec6a5b4', 401292, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b4')], true, "Mains (single phase)", "LLC020");
const bulb_2 = new Device('Router', '0x000b57fffec6a5b7', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b7')], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm");
const devices = {
'coordinator': new Device('Coordinator', '0x00124b00120144ae', 0, 0, [new Endpoint(1, [], [])], false),
'bulb': new Device('Router', '0x000b57fffec6a5b2', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b2')], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm"),
'bulb_color': new Device('Router', '0x000b57fffec6a5b3', 40399, 6535, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b3')], true, "Mains (single phase)", "LLC020"),
'remote': new Device('EndDevice', '0x0017880104e45517', 6535, 4107, [new Endpoint(1, [0], [0,3,4,6,8,5]), new Endpoint(2, [0,1,3,15,64512], [25, 6])], true, "Battery", "RWL021"),
'bulb_color': bulb_color,
'bulb_2': bulb_2,
'bulb_color_2': bulb_color_2,
'remote': new Device('EndDevice', '0x0017880104e45517', 6535, 4107, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x0017880104e45517', [{target: bulb_color.endpoints[0]}]), new Endpoint(2, [0,1,3,15,64512], [25, 6])], true, "Battery", "RWL021"),
'unsupported': new Device('EndDevice', '0x0017880104e45518', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "notSupportedModelID"),
'unsupported2': new Device('EndDevice', '0x0017880104e45529', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "notSupportedModelID"),
'interviewing': new Device('EndDevice', '0x0017880104e45530', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", undefined, true),
@@ -119,10 +126,12 @@ const devices = {
'unsupported_router': new Device('Router', '0x0017880104e45525', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Mains (single phase)", "notSupportedModelID", false, "Boef"),
'CC2530_ROUTER': new Device('Router', '0x0017880104e45559', 6540,4151, [new Endpoint(1, [0, 6], [])], true, "Mains (single phase)", 'lumi.router'),
'LIVOLO': new Device('Router', '0x0017880104e45560', 6541,4152, [new Endpoint(6, [0, 6], [])], true, "Mains (single phase)", 'TI0001 '),
'tradfri_remote': new Device('EndDevice', '0x90fd9ffffe4b64ae', 33906, 4476, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x90fd9ffffe4b64ae')], true, "Battery", "TRADFRI remote control"),
}
const groups = {
'group_1': new Group(1),
'group_1': new Group(1, []),
'group_tradfri_remote': new Group(15071, [bulb_color_2.endpoints[0], bulb_2.endpoints[0]]),
}
const mock = {
@@ -153,7 +162,7 @@ const mock = {
getPermitJoin: jest.fn().mockReturnValue(false),
reset: jest.fn(),
createGroup: jest.fn().mockImplementation((groupID) => {
const group = new Group(groupID);
const group = new Group(groupID, []);
groups[`group_${groupID}`] = group
return group;
})
@@ -163,6 +172,7 @@ const mockConstructor = jest.fn().mockImplementation(() => mock);
jest.mock('zigbee-herdsman', () => ({
Controller: mockConstructor,
Zcl: {ManufacturerCode: {Philips: 4107}},
}));
module.exports = {