Refactor router polling & only poll Xiaomi routers, refactor soft reset timeout and disable by default. #274

This commit is contained in:
Koenkk
2018-10-02 21:15:12 +02:00
parent da9ee71d80
commit 1db926173d
6 changed files with 142 additions and 73 deletions
+13 -60
View File
@@ -4,6 +4,8 @@ const State = require('./state');
const logger = require('./util/logger');
const settings = require('./util/settings');
const ExtensionNetworkMap = require('./extension/networkMap');
const ExtensionSoftReset = require('./extension/softReset');
const ExtensionRouterPollXiaomi = require('./extension/routerPollXiaomi');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
const homeassistant = require('./homeassistant');
const objectAssignDeep = require('object-assign-deep');
@@ -12,8 +14,6 @@ const mqttConfigRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/con
const mqttDeviceRegex = new RegExp(`${settings.get().mqtt.base_topic}/[\\w\\s\\d.-]+/set`, 'g');
const mqttDevicePrefixRegex = new RegExp(`${settings.get().mqtt.base_topic}/[\\w\\s\\d.-]+/[\\w\\s\\d.-]+/set`, 'g');
const pollInterval = 60 * 1000; // seconds * 1000.
const softResetTimeout = 3600 * 1000; // seconds * 1000.
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
@@ -32,17 +32,19 @@ if (settings.get().homeassistant && !cacheState) {
class Controller {
constructor() {
this.zigbee = new Zigbee();
this.handleZigbeeMessage = this.handleZigbeeMessage.bind(this);
this.handleMQTTMessage = this.handleMQTTMessage.bind(this);
this.zigbee = new Zigbee(this.handleZigbeeMessage);
this.mqtt = new MQTT();
this.state = new State();
this.configured = [];
this.handleZigbeeMessage = this.handleZigbeeMessage.bind(this);
this.handleMQTTMessage = this.handleMQTTMessage.bind(this);
this.extensions = [];
}
start() {
this.startupLogVersion(() => {
this.zigbee.start(this.handleZigbeeMessage, (error) => {
this.zigbee.start((error) => {
if (error) {
logger.error('Failed to start', error);
} else {
@@ -63,10 +65,6 @@ class Controller {
this.zigbee.permitJoin(settings.get().permit_join);
// Start timers.
this.pollTimer(true);
this.softResetTimeout(true);
// Connect to MQTT broker
const subscriptions = [
`${settings.get().mqtt.base_topic}/+/set`,
@@ -99,6 +97,8 @@ class Controller {
// Initialize extensions.
this.extensions = [
new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state),
new ExtensionSoftReset(this.zigbee, this.mqtt, this.state),
new ExtensionRouterPollXiaomi(this.zigbee, this.mqtt, this.state),
];
// Resend all cached states.
@@ -113,57 +113,10 @@ class Controller {
});
}
softResetTimeout(start) {
if (this._softResetTimer) {
clearTimeout(this._softResetTimer);
this._softResetTimer = null;
}
if (start) {
this._softResetTimer = setTimeout(() => {
this.zigbee.softReset((error) => {
if (error) {
logger.warn('Soft reset error', error);
this.zigbee.stop((error) => {
logger.warn('Zigbee stopped');
this.zigbee.start(this.handleZigbeeMessage, (error) => {
if (error) {
logger.error('Failed to restart!');
}
});
});
} else {
logger.warn('Soft resetted zigbee');
}
this.softResetTimeout(true);
});
}, softResetTimeout);
}
}
pollTimer(start) {
// Some routers need polling to prevent them from sleeping.
if (start && !this._pollTimer) {
this._pollTimer = setInterval(() => {
const devices = this.zigbee.getAllClients().filter((d) => {
const power = d.powerSource ? d.powerSource.toLowerCase().split(' ')[0] : 'unknown';
return power !== 'battery' && power !== 'unknown' && d.type === 'Router';
});
devices.forEach((d) => this.zigbee.ping(d.ieeeAddr));
}, pollInterval);
} else if (!start && this._pollTimer) {
clearTimeout(this._pollTimer);
this._pollTimer = null;
}
}
stop(callback) {
this.extensions.filter((e) => e.stop).forEach((e) => e.stop());
this.state.save();
this.mqtt.disconnect();
this.pollTimer(false);
this.softResetTimeout(false);
this.zigbee.stop(callback);
}
@@ -235,8 +188,8 @@ class Controller {
}
handleZigbeeMessage(message) {
// Zigbee message receieved, reset soft reset timeout.
this.softResetTimeout(true);
// Call extensions.
this.extensions.filter((e) => e.handleZigbeeMessage).forEach((e) => e.handleZigbeeMessage(message));
// Log the message.
let logMessage = `Recieved zigbee message of type '${message.type}' ` +
+40
View File
@@ -0,0 +1,40 @@
const utils = require('../util/utils');
const interval = utils.secondsToMilliseconds(60);
/**
* This extensions polls Xiaomi Zigbee routers to keep them awake.
*/
class RouterPollXiaomi {
constructor(zigbee, mqtt, state) {
this.zigbee = zigbee;
this.timer = null;
this.startTimer();
}
startTimer() {
this.clearTimer();
this.timer = setInterval(() => this.handleInterval(), interval);
}
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
stop() {
this.clearTimer();
}
handleInterval() {
this.zigbee.getAllClients()
.filter((d) => utils.isXiaomiDevice(d)) // Filter Xiaomi devices
.filter((d) => d.type === 'Router') // Filter routers
.filter((d) => d.powerSource && d.powerSource !== 'Battery') // Remove battery powered devices
.forEach((d) => this.zigbee.ping(d.ieeeAddr)); // Ping devices.
}
}
module.exports = RouterPollXiaomi;
+66
View File
@@ -0,0 +1,66 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
const utils = require('../util/utils');
/**
* This extensions soft resets the ZNP after a certain timeout.
*/
class SoftReset {
constructor(zigbee, mqtt, state) {
this.zigbee = zigbee;
this.timer = null;
this.timeout = utils.secondsToMilliseconds(settings.get().advanced.soft_reset_timeout);
if (this.timeout === 0) {
logger.debug(`Soft reset timeout disabled`);
} else {
logger.debug(`Soft reset timeout set to ${utils.millisecondsToSeconds(this.timeout)} seconds`);
}
this.resetTimer();
}
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
resetTimer() {
if (this.timeout === 0) {
return;
}
this.clearTimer();
this.timer = setTimeout(() => this.handleTimeout(), this.timeout);
}
handleTimeout() {
logger.warn('Soft reset timeout triggered');
this.zigbee.softReset((error) => {
if (error) {
logger.warn('Soft reset failed, trying stop/start');
this.zigbee.stop((error) => {
logger.warn('Zigbee stopped');
this.zigbee.start((error) => {
if (error) {
logger.error('Failed to restart!');
}
});
});
} else {
logger.warn('Soft resetted ZNP due to timeout');
}
this.resetTimer();
});
}
handleZigbeeMessage(message) {
this.resetTimer();
}
}
module.exports = SoftReset;
+1
View File
@@ -10,6 +10,7 @@ const defaults = {
advanced: {
log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'),
log_level: process.env.DEBUG ? 'debug' : 'info',
soft_reset_timeout: 0,
},
};
+8
View File
@@ -0,0 +1,8 @@
// Xiaomi uses 4151 and 4447 (lumi.plug) as manufacturer ID.
const xiaomiManufacturerID = [4151, 4447];
module.exports = {
millisecondsToSeconds: (milliseconds) => milliseconds / 1000,
secondsToMilliseconds: (seconds) => seconds * 1000,
isXiaomiDevice: (device) => xiaomiManufacturerID.includes(device.manufId),
};
+14 -13
View File
@@ -3,6 +3,7 @@ const logger = require('./util/logger');
const settings = require('./util/settings');
const data = require('./util/data');
const zclPacket = require('zcl-packet');
const utils = require('./util/utils');
const advancedSettings = settings.get().advanced;
@@ -21,13 +22,14 @@ const shepherdSettings = {
logger.debug(`Using zigbee-shepherd with settings: '${JSON.stringify(shepherdSettings)}'`);
class Zigbee {
constructor() {
constructor(onMessage) {
this.onMessage = onMessage;
this.handleReady = this.handleReady.bind(this);
this.handleMessage = this.handleMessage.bind(this);
this.handleError = this.handleError.bind(this);
}
start(onMessage, callback) {
start(callback) {
logger.info(`Starting zigbee-shepherd`);
this.shepherd = new ZShepherd(settings.get().serial.port, shepherdSettings);
@@ -62,8 +64,6 @@ class Zigbee {
this.shepherd.on('ready', this.handleReady);
this.shepherd.on('ind', this.handleMessage);
this.shepherd.on('error', this.handleError);
this.onMessage = onMessage;
}
_logStartupInfo() {
@@ -84,12 +84,11 @@ class Zigbee {
}
handleReady() {
// Set all Xiaomi devices (manufId === 4151) to be online, so shepherd won't try
// to query info from devices (which would fail because they go tosleep).
// Xiaomi lumi.plug has manufId === 4447 and can be in the sleep mode too
// Set all Xiaomi devices to be online, so shepherd won't try
// to query info from devices (which would fail because they go to sleep).
const devices = this.getAllClients();
devices.forEach((d) => {
if ((d.manufId === 4151) || (d.manufId === 4447)) {
if (utils.isXiaomiDevice(d)) {
const device = this.shepherd.find(d.ieeeAddr, 1);
if (device) {
device.getDevice().update({
@@ -100,7 +99,7 @@ class Zigbee {
}
});
// Check if we have to turn of the led
// Check if we have to turn off the led
if (settings.get().serial.disable_led) {
this.shepherd.controller.request('UTIL', 'ledControl', {ledid: 3, mode: 0});
}
@@ -190,14 +189,16 @@ class Zigbee {
return;
}
logger.info(`Zigbee publish to '${deviceID}', ${cid} - ${cmd}
- ${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${ep}`);
logger.info(
`Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ` +
`${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${ep}`
);
const callback_ = (error) => {
if (error) {
logger.error(
`Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)}
- ${JSON.stringify(cfg)} - ${ep} ` +
`Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} ` +
`- ${JSON.stringify(cfg)} - ${ep} ` +
`failed with error ${error}`);
}