zigbee2mqtt/lib/zigbee.js

360 lines
12 KiB
JavaScript

const ZShepherd = require('zigbee-shepherd');
const logger = require('./util/logger');
const settings = require('./util/settings');
const data = require('./util/data');
const utils = require('./util/utils');
const cieApp = require('./zapp/cie');
const Queue = require('queue');
const zclId = require('zcl-id');
const advancedSettings = settings.get().advanced;
if (advancedSettings.channel < 11 || advancedSettings.channel > 26) {
throw new Error(`'${advancedSettings.channel}' is an invalid channel, use a channel between 11 - 26.`);
}
const shepherdSettings = {
net: {
panId: advancedSettings.pan_id,
extPanId: advancedSettings.ext_pan_id,
channelList: [advancedSettings.channel],
precfgkey: settings.get().advanced.network_key,
},
dbPath: data.joinPath('database.db'),
sp: {
baudRate: advancedSettings.baudrate,
rtscts: advancedSettings.rtscts,
},
};
const defaultCfg = {
manufSpec: 0,
disDefaultRsp: 0,
};
const delay = 170;
logger.debug(`Using zigbee-shepherd with settings: '${JSON.stringify(shepherdSettings)}'`);
class Zigbee {
constructor() {
this.onReady = this.onReady.bind(this);
this.onMessage = this.onMessage.bind(this);
this.onError = this.onError.bind(this);
this.messageHandler = null;
this.queue = new Queue();
this.queue.concurrency = 1;
}
start(messageHandler, callback) {
logger.info(`Starting zigbee-shepherd`);
this.messageHandler = messageHandler;
this.shepherd = new ZShepherd(settings.get().serial.port, shepherdSettings);
this.shepherd.start((error) => {
if (error) {
logger.info('Error while starting zigbee-shepherd, attemping to fix... (takes 60 seconds)');
this.shepherd.controller._znp.close((() => null));
setTimeout(() => {
logger.info(`Starting zigbee-shepherd`);
this.shepherd.start((error) => {
if (error) {
logger.error('Error while starting zigbee-shepherd!');
logger.error(
'Press the reset button on the stick (the one closest to the USB) and start again'
);
callback(error);
} else {
this.logStartupInfo();
callback(null);
}
});
}, utils.secondsToMilliseconds(60));
} else {
this.logStartupInfo();
callback(null);
}
});
// Register callbacks.
this.shepherd.on('ready', this.onReady);
this.shepherd.on('ind', this.onMessage);
this.shepherd.on('error', this.onError);
}
logStartupInfo() {
logger.info('zigbee-shepherd started');
logger.info(`Coordinator firmware version: '${this.shepherd.info().firmware.revision}'`);
logger.debug(`zigbee-shepherd info: ${JSON.stringify(this.shepherd.info())}`);
}
softReset(callback) {
this.shepherd.reset('soft', callback);
}
stop(callback) {
this.queue.stop();
this.shepherd.stop((error) => {
logger.info('zigbee-shepherd stopped');
callback(error);
});
}
onReady() {
// Mount cieApp
this.shepherd.mount(cieApp, (err, epId) => {
if (!err) {
logger.debug(`Mounted the cieApp (epId ${epId})`);
} else {
logger.error(`Failed to mount the cieApp`);
}
});
// 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});
}
// Add the coordinator to group 99
this.addCoordinatorToGroup(99);
// Wait some time before we start the queue, many calls skip this queue which hangs the stick
setTimeout(() => {
this.queue.autostart = true;
this.queue.start();
}, 2000);
logger.info('zigbee-shepherd ready');
}
addCoordinatorToGroup(groupID, callback=() => {}) {
this.shepherd.controller.request('ZDO', 'extFindGroup', {groupid: groupID, endpoint: 1}, (err) => {
// Error means that the coordinator is not in the group yet.
if (err) {
const payload = {groupid: groupID, endpoint: 1, namelen: 0, groupname: ''};
this.shepherd.controller.request('ZDO', 'extAddGroup', payload, (err) => {
if (err) {
logger.info(`Successfully added coordinator to group '${groupID}'`);
callback(false);
} else {
logger.error(`Failed to add coordinator to group '${groupID}'`);
callback(true);
}
});
} else {
logger.debug(`Coordinator is already in group '${groupID}'`);
callback(false);
}
});
}
onError(message) {
// This event may appear if zigbee-shepherd cannot decode bad packets (invalid checksum).
logger.error(message);
}
permitJoin(permit, callback) {
if (permit) {
logger.info('Zigbee: allowing new devices to join.');
} else {
logger.info('Zigbee: disabling joining new devices.');
}
this.shepherd.permitJoin(permit ? 255 : 0, (error) => {
if (error) {
logger.info(error);
}
if (callback) {
callback();
}
});
}
getPermitJoin() {
return this.shepherd.controller._permitJoinTime === 255;
}
getAllClients() {
return this.getDevices().filter((device) => device.type !== 'Coordinator');
}
removeDevice(deviceID, ban, callback) {
this.shepherd.remove(deviceID, {reJoin: !ban}, (error) => {
if (error) {
logger.warn(`Failed to remove '${deviceID}', trying force remove...`);
this.forceRemove(deviceID, callback);
} else {
callback(null);
}
});
}
forceRemove(deviceID, callback) {
const device = this.shepherd._findDevByAddr(deviceID);
if (device) {
return this.shepherd._unregisterDev(device, (error) => callback(error));
} else {
logger.warn(`Could not find ${deviceID} for force removal`);
callback(true);
}
}
onMessage(message) {
if (this.messageHandler) {
this.messageHandler(message);
}
}
getDevices() {
return this.shepherd.list();
}
getDevice(ieeeAddr) {
return this.getDevices().find((d) => d.ieeeAddr === ieeeAddr);
}
getCoordinator() {
const device = this.getDevices().find((d) => d.type === 'Coordinator');
return this.shepherd.find(device.ieeeAddr, 1);
}
getGroup(ID) {
return this.shepherd.getGroup(ID);
}
networkScan(callback) {
logger.info('Starting network scan...');
this.shepherd.lqiScan().then((result) => {
logger.info('Network scan completed');
callback(result);
});
}
getEndpoint(ieeeAddr, ep) {
// If no ep is given, the first endpoint will be returned
// Find device in zigbee-shepherd
const device = this.getDevice(ieeeAddr);
if (!device || !device.epList || !device.epList.length) {
logger.error(`Zigbee cannot determine endpoint for '${ieeeAddr}'`);
return null;
}
ep = ep ? ep : device.epList[0];
const endpoint = this.shepherd.find(ieeeAddr, ep);
return endpoint;
}
publish(entityID, entityType, cid, cmd, cmdType, zclData, cfg=defaultCfg, ep, callback) {
let entity = null;
if (entityType === 'device') {
entity = this.getEndpoint(entityID, ep);
} else if (entityType === 'group') {
entity = this.getGroup(entityID);
}
if (!entity) {
logger.error(
`Cannot publish message to ${entityType} because '${entityID}' is not known by zigbee-shepherd`
);
return;
}
this.queue.push((queueCallback) => {
logger.info(
`Zigbee publish to ${entityType} '${entityID}', ${cid} - ${cmd} - ` +
`${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${ep}`
);
const callback_ = (error, rsp) => {
if (error) {
logger.error(
`Zigbee publish to ${entityType} '${entityID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} ` +
`- ${JSON.stringify(cfg)} - ${ep} ` +
`failed with error ${error}`);
}
if (callback) {
callback(error, rsp);
}
};
if (cmdType === 'functional' && entity.functional) {
entity.functional(cid, cmd, zclData, cfg, callback_);
} else if (cmdType === 'foundation' && entity.foundation) {
entity.foundation(cid, cmd, zclData, cfg, callback_);
} else {
logger.error(`Unknown zigbee publish cmdType ${cmdType}`);
}
setTimeout(() => queueCallback(), delay);
});
}
ping(ieeeAddr, errorLogLevel='error', cb) {
const device = this.shepherd._findDevByAddr(ieeeAddr);
if (device) {
this.queue.push((queueCallback) => {
logger.debug(`Ping ${ieeeAddr}`);
this.shepherd.controller.checkOnline(device, (error) => {
if (error) {
logger[errorLogLevel](`Failed to ping ${ieeeAddr}`);
} else {
logger.debug(`Successfully pinged ${ieeeAddr}`);
}
if (cb) {
cb(error);
}
});
setTimeout(() => queueCallback(), delay);
});
}
}
bind(ep, cluster, target=this.getCoordinator()) {
const log = `for ${ep.device.ieeeAddr} - ${cluster}`;
this.queue.push((queueCallback) => {
logger.debug(`Setup binding ${log}`);
ep.bind(cluster, target, (error) => {
if (error) {
logger.error(`Failed to setup binding ${log} - (${error})`);
} else {
logger.debug(`Successfully setup binding ${log}`);
}
});
setTimeout(() => queueCallback(), delay);
});
}
report(ep, cluster, attribute, min, max, change) {
const attrId = zclId.attr(cluster, attribute).value;
const dataType = zclId.attrType(cluster, attribute).value;
const cfg = {direction: 0, attrId, dataType, minRepIntval: min, maxRepIntval: max, repChange: change};
const log = `for ${ep.device.ieeeAddr} - ${cluster} - ${attribute}`;
this.queue.push((queueCallback) => {
logger.debug(`Setup reporting ${log}`);
ep.foundation(cluster, 'configReport', [cfg], defaultCfg, (error) => {
if (error) {
logger.error(`Failed to setup reporting ${log} - (${error})`);
} else {
logger.debug(`Successfully setup reporting ${log}`);
}
});
setTimeout(() => queueCallback(), delay);
});
}
}
module.exports = Zigbee;