Replace Livolo extension with DeviceEvent. https://github.com/Koenkk/zigbee2mqtt/issues/592

This commit is contained in:
Koen Kanters
2019-09-19 20:36:05 +02:00
parent e9a336fd16
commit d4f01e6893
10 changed files with 116 additions and 207 deletions
+2 -9
View File
@@ -19,7 +19,7 @@ const ExtensionGroups = require('./extension/groups');
const ExtensionDeviceAvailability = require('./extension/deviceAvailability');
const ExtensionDeviceBind = require('./extension/deviceBind');
const ExtensionDeviceReport = require('./extension/deviceReport');
// const ExtensionLivolo = require('./extension/livolo');
const ExtensionDeviceEvent = require('./extension/deviceEvent');
class Controller {
constructor() {
@@ -40,6 +40,7 @@ class Controller {
new ExtensionBridgeConfig(this.zigbee, this.mqtt, this.state, this.publishEntityState),
new ExtensionGroups(this.zigbee, this.mqtt, this.state, this.publishEntityState),
new ExtensionDeviceBind(this.zigbee, this.mqtt, this.state, this.publishEntityState),
new ExtensionDeviceEvent(this.zigbee, this.mqtt, this.state, this.publishEntityState),
];
if (settings.get().advanced.report) {
@@ -66,14 +67,6 @@ class Controller {
this.zigbee, this.mqtt, this.state, this.publishEntityState
));
}
// TODO
// if (settings.get().experimental.livolo) {
// // https://github.com/Koenkk/zigbee2mqtt/issues/592
// this.extensions.push(new ExtensionLivolo(
// this.zigbee, this.mqtt, this.state, this.publishEntityState
// ));
// }
}
async start() {
+5 -4
View File
@@ -26,11 +26,12 @@ class BaseExtension {
/**
* Is called when a Zigbee message from a device is received.
* @param {Object?} message The received message (can be null)
* @param {Object?} device The device of the message (can be null)
* @param {Object?} mappedDevice The mapped device (can be null)
* @param {string} type Type of the message
* @param {Object} data Data of the message
* @param {Object?} mappedDevice The mapped device
* @param {Object?} settingsDevice Device settings
*/
// onZigbeeMessage(message, device, mappedDevice) {}
// onZigbeeEvent(type, data, mappedDevice, settingsDevice) {}
/**
* Is called when a MQTT message is received
+34
View File
@@ -0,0 +1,34 @@
const BaseExtension = require('./baseExtension');
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
class DeviceEvent extends BaseExtension {
async onZigbeeStarted() {
for (const device of await this.zigbee.getClients()) {
this.callOnEvent(device, 'start', {});
}
}
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
if (data.device) {
this.callOnEvent(data.device, type, data, mappedDevice);
}
}
async stop() {
for (const device of await this.zigbee.getClients()) {
this.callOnEvent(device, 'stop', {});
}
}
callOnEvent(device, type, data, mappedDevice) {
if (!mappedDevice) {
mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
}
if (mappedDevice && mappedDevice.onEvent) {
mappedDevice.onEvent(type, data, device);
}
}
}
module.exports = DeviceEvent;
-185
View File
@@ -1,185 +0,0 @@
/* istanbul ignore file */
// todo
const utils = require('../util/utils');
const interval = utils.secondsToMilliseconds(1);
const logger = require('../util/logger');
const BaseExtension = require('./baseExtension');
const foundationCfg = {manufSpec: 0, disDefaultRsp: 0};
/**
* Extension required for Livolo device support.
*/
class Livolo extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState) {
super(zigbee, mqtt, state, publishEntityState);
this.timer = null;
this.configured = {};
}
onZigbeeStarted() {
this.startTimer();
}
_resetDeviceState(ieeeAddr) {
this.configured[ieeeAddr] = {
stage: 0,
retry: 0,
waitresp: false,
};
}
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
if (!data.device) {
return;
}
if ((type == 'deviceInterview') ||
(type == 'deviceJoined') ||
(type == 'deviceAnnounce')) {
if (this.configured.hasOwnProperty(data.device.ieeeAddr)) {
logger.info(`LIVOLO ${data.device.ieeeAddr}. (Re)joins in the network (after power off?)`);
this._resetDeviceState(data.device.ieeeAddr);
}
return;
}
}
startTimer() {
this.clearTimer();
this.timer = setInterval(() => this.handleInterval(), interval);
}
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
stop() {
this.clearTimer();
}
_handleCommandRespSimple(err, rsp) {
this.ext.configured[this.ieeeAddr].waitresp = false;
if (err) {
if (this.ctype === 'toggle') {
logger.debug(`LIVOLO ${this.ieeeAddr}. Toggle command error:`, err.message);
}
} else {
logger.debug(`LIVOLO ${this.ieeeAddr}. Sucessfully configured`, rsp);
this.ext.configured[this.ieeeAddr].stage = 1; // sucessfully send command
this.ext.configured[this.ieeeAddr].retry = 0;
this.device.status = 'online';
}
}
_handleCommandRespWithData(err, rsp) {
this.ext.configured[this.ieeeAddr].waitresp = false;
if (err) {
logger.info(`LIVOLO ${this.ieeeAddr}. ${this.cid}.${this.ctype} response error:`, err.message);
if (this.ext.configured[this.ieeeAddr].retry >= 3) {
// errors in three sequental reads, stop polling, wait for a device message
this.device.status = 'offline';
logger.info(`LIVOLO ${this.ieeeAddr}. Stopped polling after 3 unsuccessful attempts`);
}
} else {
this.ext.configured[this.ieeeAddr].retry = 0;
this.device.status = 'online';
if (this.ext.zigbee) {
this.ext.zigbee.shepherd.emit('ind:reported', this.ep, this.cid, rsp, this.ep.last_af_msg);
}
}
}
// msg: { groupid, clusterid, srcaddr, srcendpoint, dstendpoint, wasbroadcast,
// linkquality, securityuse, timestamp, transseqnumber, len, data }
_handleAfMessage(msg, ep) {
ep.linkquality = msg.linkquality;
ep.last_af_msg = msg;
}
_sendToggle(zdev, ieeeAddr, ep, retry) {
this.zigbee.queue.push(ieeeAddr, (queueCallback) => {
const cfg = {};
logger.debug(`LIVOLO ${ieeeAddr}. Sending the 'toggle' command. Retry: ${retry}`);
ep.functional('genOnOff', 'toggle', [cfg], foundationCfg,
this._handleCommandRespSimple.bind({
device: zdev,
ieeeAddr,
cid: 'genOnOff',
ctype: 'toggle',
ext: this,
}));
queueCallback();
});
}
_sendPoll(zdev, ieeeAddr, ep, retry) {
this.zigbee.queue.push(ieeeAddr, (queueCallback) => {
ep.foundation('genOnOff', 'read', [{
attrId: 0, // onOff
}], this._handleCommandRespWithData.bind({
device: zdev,
ieeeAddr,
ep,
cid: 'genOnOff',
ctype: 'read',
ext: this,
}));
queueCallback();
});
}
async handleInterval() {
(await this.zigbee.getAllClients())
.filter((d) => d.manufacturerName && d.manufacturerName.startsWith('LIVOLO')) // LIVOLO
.filter((d) => d.type === 'EndDevice') // Filter end devices
.filter((d) => d.powerSource && d.powerSource !== 'Battery') // Remove battery powered devices
.forEach((d) => {
const zdev = this.zigbee.shepherd._findDevByAddr(d.ieeeAddr);
if (zdev && zdev.endpoints) {
const eplist = Object.keys(zdev.endpoints).filter((epId) => {
const ep2 = zdev.getEndpoint(epId);
const clist = ep2.getClusterList();
return clist && clist.includes(6); // 6 - genOnOff
});
if (eplist.length > 0) {
const ep = zdev.getEndpoint(eplist[0]);
ep.onAfIncomingMsg = this._handleAfMessage;
if (!this.configured.hasOwnProperty(d.ieeeAddr)) {
this._resetDeviceState(d.ieeeAddr);
}
const state = this.configured[d.ieeeAddr];
if (state.waitresp) {
return;
}
if (state.retry < 3) {
if (state.stage === 0) {
state.retry += 1;
state.waitresp = true;
this._sendToggle(zdev, d.ieeeAddr, ep, state.retry);
} else if (state.stage === 1) {
state.retry += 1;
state.waitresp = true;
this._sendPoll(zdev, d.ieeeAddr, ep, state.retry);
}
}
}
}
});
return true;
}
}
module.exports = Livolo;
-1
View File
@@ -36,7 +36,6 @@ const defaults = {
},
},
experimental: {
livolo: false,
// json or attribute
output: 'json',
},
+6 -6
View File
@@ -5938,9 +5938,9 @@
}
},
"zigbee-herdsman": {
"version": "0.6.13",
"resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-0.6.13.tgz",
"integrity": "sha512-Tcy+yaJ25+ocOadXOicUIUQLZFGDmRf9Y+yUfG5fYtap738l0BDaKd/hdVjhanOB5AQXYtQvAvMNhdn7szfOEg==",
"version": "0.6.14",
"resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-0.6.14.tgz",
"integrity": "sha512-QQNqSf+TkAVxdy6fxcqPbd7Jx+if03GTyjMjUK7ajZotYhweZQQaXMI3uATXggrBAd88YpbnCaUxaKkGMsC1Dw==",
"requires": {
"debug": "^4.1.1",
"fast-deep-equal": "^2.0.1",
@@ -12974,9 +12974,9 @@
}
},
"zigbee-herdsman-converters": {
"version": "11.0.10",
"resolved": "https://registry.npmjs.org/zigbee-herdsman-converters/-/zigbee-herdsman-converters-11.0.10.tgz",
"integrity": "sha512-ENNqj5crUHvLCV3AJ/Azqn/+nZ+uqEvKmJ1x0LV9+Cv1X/7hdzhoKYC/HGrr9DLpuh1TavvP4VHU5COpUTZJRg==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/zigbee-herdsman-converters/-/zigbee-herdsman-converters-11.1.0.tgz",
"integrity": "sha512-yNW2IUjKmCH3186onxfshSiUDC2Ye9dwmxnJo0/09bix+U86/7SyfJGphVvV9dWwjwjbGPwm/wH942PLOchrAQ==",
"requires": {
"debounce": "^1.2.0"
}
+2 -2
View File
@@ -43,8 +43,8 @@
"rimraf": "*",
"semver": "*",
"winston": "2.4.2",
"zigbee-herdsman": "0.6.13",
"zigbee-herdsman-converters": "11.0.10"
"zigbee-herdsman": "0.6.14",
"zigbee-herdsman-converters": "11.1.0"
},
"devDependencies": {
"eslint": "*",
+62
View File
@@ -0,0 +1,62 @@
const data = require('./stub/data');
const logger = require('./stub/logger');
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
zigbeeHerdsman.returnDevices.push('0x00124b00120144ae');
zigbeeHerdsman.returnDevices.push('0x0017880104e45560');
const MQTT = require('./stub/mqtt');
const settings = require('../lib/util/settings');
const Controller = require('../lib/controller');
const flushPromises = () => new Promise(setImmediate);
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
const mocksClear = [MQTT.publish, logger.warn, logger.debug];
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const mockOnEvent = jest.fn();
const mappedLivolo = zigbeeHerdsmanConverters.findByZigbeeModel(zigbeeHerdsman.devices.LIVOLO.modelID);
mappedLivolo.onEvent = mockOnEvent;
describe('onlythis Device event', () => {
let controller;
const device = zigbeeHerdsman.devices.LIVOLO;
beforeEach(async () => {
data.writeDefaultConfiguration();
settings._reRead();
data.writeEmptyState();
controller = new Controller();
await controller.start();
mocksClear.forEach((m) => m.mockClear());
await flushPromises();
});
it('Should call with start event', async () => {
expect(mockOnEvent).toHaveBeenCalledTimes(1);
const call = mockOnEvent.mock.calls[0];
expect(call[0]).toBe('start')
expect(call[1]).toStrictEqual({})
expect(call[2]).toBe(device);
});
it('Should call with stop event', async () => {
mockOnEvent.mockClear();
await controller.stop();
await flushPromises();
expect(mockOnEvent).toHaveBeenCalledTimes(1);
const call = mockOnEvent.mock.calls[0];
expect(call[0]).toBe('stop')
expect(call[1]).toStrictEqual({})
expect(call[2]).toBe(device);
});
it('Should call with zigbee event', async () => {
mockOnEvent.mockClear();
await zigbeeHerdsman.events.deviceAnnounce({device});
await flushPromises();
expect(mockOnEvent).toHaveBeenCalledTimes(1);
const call = mockOnEvent.mock.calls[0];
expect(call[0]).toBe('deviceAnnounce')
expect(call[1]).toStrictEqual({device})
expect(call[2]).toBe(device);
});
});
+4
View File
@@ -113,6 +113,10 @@ function writeDefaultConfiguration() {
'0x0017880104e45559': {
retain: false,
friendly_name: 'cc2530_router'
},
'0x0017880104e45560': {
retain: false,
friendly_name: 'livolo'
}
},
groups: {
+1
View File
@@ -123,6 +123,7 @@ const devices = {
'nomodel': new Device('Router', '0x0017880104e45535', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Mains (single phase)", undefined, true),
'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(1, [0, 6], [])], true, "Mains (single phase)", 'TI0001 '),
}
const groups = {