mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-07-02 18:11:36 +00:00
Replace Livolo extension with DeviceEvent. https://github.com/Koenkk/zigbee2mqtt/issues/592
This commit is contained in:
+2
-9
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -36,7 +36,6 @@ const defaults = {
|
||||
},
|
||||
},
|
||||
experimental: {
|
||||
livolo: false,
|
||||
// json or attribute
|
||||
output: 'json',
|
||||
},
|
||||
|
||||
Generated
+6
-6
@@ -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
@@ -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": "*",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -113,6 +113,10 @@ function writeDefaultConfiguration() {
|
||||
'0x0017880104e45559': {
|
||||
retain: false,
|
||||
friendly_name: 'cc2530_router'
|
||||
},
|
||||
'0x0017880104e45560': {
|
||||
retain: false,
|
||||
friendly_name: 'livolo'
|
||||
}
|
||||
},
|
||||
groups: {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user