2019-09-09 10:48:09 -07:00
|
|
|
const ZigbeeHerdsman = require('zigbee-herdsman');
|
2018-04-18 09:25:40 -07:00
|
|
|
const logger = require('./util/logger');
|
|
|
|
const settings = require('./util/settings');
|
2018-05-11 20:04:15 -07:00
|
|
|
const data = require('./util/data');
|
2019-09-09 10:48:09 -07:00
|
|
|
const assert = require('assert');
|
2018-10-02 12:15:12 -07:00
|
|
|
const utils = require('./util/utils');
|
2019-09-09 10:48:09 -07:00
|
|
|
const events = require('events');
|
2019-03-02 06:04:52 -07:00
|
|
|
const objectAssignDeep = require('object-assign-deep');
|
2019-09-09 10:48:09 -07:00
|
|
|
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
2018-04-18 09:25:40 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const postfixes = utils.getPostfixes();
|
2019-11-11 09:34:51 -07:00
|
|
|
const keyEndpointByNumber = new RegExp(`.*/([0-9]*)$`);
|
2019-02-13 12:06:14 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const herdsmanSettings = {
|
|
|
|
network: {
|
|
|
|
panID: settings.get().advanced.pan_id,
|
2019-11-06 11:43:12 -07:00
|
|
|
extendedPanID: settings.get().advanced.ext_pan_id,
|
2019-09-09 10:48:09 -07:00
|
|
|
channelList: [settings.get().advanced.channel],
|
|
|
|
networkKey: settings.get().advanced.network_key,
|
2018-05-17 00:52:28 -07:00
|
|
|
},
|
2019-09-09 10:48:09 -07:00
|
|
|
databasePath: data.joinPath('database.db'),
|
2019-11-05 12:42:32 -07:00
|
|
|
databaseBackupPath: data.joinPath('database.db.backup'),
|
2019-09-09 10:48:09 -07:00
|
|
|
backupPath: data.joinPath('coordinator_backup.json'),
|
|
|
|
serialPort: {
|
|
|
|
baudRate: settings.get().advanced.baudrate,
|
|
|
|
rtscts: settings.get().advanced.rtscts,
|
|
|
|
path: settings.get().serial.port,
|
2018-06-14 12:37:19 -07:00
|
|
|
},
|
2018-04-18 09:25:40 -07:00
|
|
|
};
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
class Zigbee extends events.EventEmitter {
|
2018-11-16 12:23:11 -07:00
|
|
|
constructor() {
|
2019-09-09 10:48:09 -07:00
|
|
|
super();
|
2019-10-17 13:01:39 -07:00
|
|
|
this.acceptJoiningDeviceHandler = this.acceptJoiningDeviceHandler.bind(this);
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
async start() {
|
|
|
|
logger.info(`Starting zigbee-herdsman...`);
|
|
|
|
const herdsmanSettingsLog = objectAssignDeep.noMutate(herdsmanSettings);
|
|
|
|
herdsmanSettingsLog.network.networkKey = 'HIDDEN';
|
|
|
|
logger.debug(`Using zigbee-herdsman with settings: '${JSON.stringify(herdsmanSettingsLog)}'`);
|
|
|
|
|
|
|
|
try {
|
2019-10-17 13:01:39 -07:00
|
|
|
herdsmanSettings.acceptJoiningDeviceHandler = this.acceptJoiningDeviceHandler;
|
2019-09-09 10:48:09 -07:00
|
|
|
this.herdsman = new ZigbeeHerdsman.Controller(herdsmanSettings);
|
|
|
|
await this.herdsman.start();
|
|
|
|
} catch (error) {
|
|
|
|
logger.error(`Error while starting zigbee-herdsman`);
|
|
|
|
throw error;
|
|
|
|
}
|
2018-04-18 09:25:40 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
this.herdsman.on('adapterDisconnected', () => this.emit('adapterDisconnected'));
|
|
|
|
this.herdsman.on('deviceAnnounce', (data) => this.emit('event', 'deviceAnnounce', data));
|
|
|
|
this.herdsman.on('deviceInterview', (data) => this.emit('event', 'deviceInterview', data));
|
|
|
|
this.herdsman.on('deviceJoined', (data) => this.emit('event', 'deviceJoined', data));
|
|
|
|
this.herdsman.on('deviceLeave', (data) => this.emit('event', 'deviceLeave', data));
|
|
|
|
this.herdsman.on('message', (data) => this.emit('event', 'message', data));
|
2018-06-01 12:04:48 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
logger.info('zigbee-herdsman started');
|
|
|
|
logger.info(`Coordinator firmware version: '${JSON.stringify(await this.getCoordinatorVersion())}'`);
|
|
|
|
logger.debug(`Zigbee network parameters: ${JSON.stringify(await this.herdsman.getNetworkParameters())}`);
|
2019-03-26 13:34:58 -07:00
|
|
|
|
2019-09-23 13:21:27 -07:00
|
|
|
for (const device of this.getClients()) {
|
2019-09-09 10:48:09 -07:00
|
|
|
// If a whitelist is used, all other device will be removed from the network.
|
|
|
|
if (settings.get().whitelist.length > 0) {
|
2019-06-25 10:38:36 -07:00
|
|
|
if (!settings.get().whitelist.includes(device.ieeeAddr)) {
|
2019-09-09 10:48:09 -07:00
|
|
|
logger.warn(`Blacklisted device is connected (${device.ieeeAddr}), removing...`);
|
|
|
|
device.removeFromNetwork();
|
2019-06-25 10:38:36 -07:00
|
|
|
}
|
2019-09-09 10:48:09 -07:00
|
|
|
} else if (settings.get().ban.includes(device.ieeeAddr)) {
|
|
|
|
logger.warn(`Banned device is connected (${device.ieeeAddr}), removing...`);
|
|
|
|
device.removeFromNetwork();
|
2019-03-26 13:34:58 -07:00
|
|
|
}
|
2019-05-07 12:00:17 -07:00
|
|
|
}
|
|
|
|
|
2018-10-02 12:15:12 -07:00
|
|
|
// Check if we have to turn off the led
|
2018-07-21 12:13:28 -07:00
|
|
|
if (settings.get().serial.disable_led) {
|
2019-09-30 12:16:00 -07:00
|
|
|
this.herdsman.setLED(false);
|
2019-05-07 12:00:17 -07:00
|
|
|
}
|
2018-04-23 09:17:47 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
async getCoordinatorVersion() {
|
|
|
|
return this.herdsman.getCoordinatorVersion();
|
2018-06-06 12:13:32 -07:00
|
|
|
}
|
|
|
|
|
2019-10-07 10:34:10 -07:00
|
|
|
async reset(type) {
|
|
|
|
await this.herdsman.reset(type);
|
2018-06-04 01:55:00 -07:00
|
|
|
}
|
2018-06-06 12:19:50 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
async stop() {
|
|
|
|
await this.herdsman.stop();
|
|
|
|
logger.info('zigbee-herdsman stopped');
|
2018-04-18 09:25:40 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
async permitJoin(permit) {
|
|
|
|
permit ?
|
|
|
|
logger.info('Zigbee: allowing new devices to join.') :
|
|
|
|
logger.info('Zigbee: disabling joining new devices.');
|
2018-04-27 14:58:46 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
await this.herdsman.permitJoin(permit);
|
2019-05-14 08:51:43 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
async getPermitJoin() {
|
|
|
|
return this.herdsman.getPermitJoin();
|
2018-06-04 12:36:51 -07:00
|
|
|
}
|
|
|
|
|
2019-09-23 13:21:27 -07:00
|
|
|
getClients() {
|
|
|
|
return this.herdsman.getDevices().filter((device) => device.type !== 'Coordinator');
|
2019-05-19 10:33:30 -07:00
|
|
|
}
|
|
|
|
|
2019-09-23 13:21:27 -07:00
|
|
|
getDevices() {
|
|
|
|
return this.herdsman.getDevices();
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
2019-09-23 13:21:27 -07:00
|
|
|
getDeviceByIeeeAddr(ieeeAddr) {
|
|
|
|
return this.herdsman.getDeviceByIeeeAddr(ieeeAddr);
|
2019-05-14 08:51:43 -07:00
|
|
|
}
|
|
|
|
|
2019-09-23 13:21:27 -07:00
|
|
|
getDevicesByType(type) {
|
|
|
|
return this.herdsman.getDevicesByType(type);
|
|
|
|
}
|
|
|
|
|
|
|
|
resolveEntity(key) {
|
2019-09-12 13:48:23 -07:00
|
|
|
assert(
|
|
|
|
typeof key === 'string' || typeof key === 'number' ||
|
2019-10-26 09:25:51 -07:00
|
|
|
key.constructor.name === 'Device', `Wrong type '${typeof key}'`,
|
2019-09-12 13:48:23 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
if (typeof key === 'string' || typeof key === 'number') {
|
|
|
|
if (typeof key === 'number') {
|
|
|
|
key = key.toString();
|
|
|
|
}
|
2019-06-16 04:49:15 -07:00
|
|
|
|
2019-10-09 10:40:46 -07:00
|
|
|
if (typeof key === 'string' && key.toLowerCase() === 'coordinator') {
|
2019-09-23 13:21:27 -07:00
|
|
|
const coordinator = this.getDevicesByType('Coordinator')[0];
|
2019-09-09 10:48:09 -07:00
|
|
|
return {
|
|
|
|
type: 'device',
|
|
|
|
device: coordinator,
|
|
|
|
endpoint: coordinator.getEndpoint(1),
|
|
|
|
settings: {friendlyName: 'Coordinator'},
|
|
|
|
name: 'Coordinator',
|
|
|
|
};
|
|
|
|
}
|
2019-05-19 10:33:30 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
let postfix = postfixes.find((p) => key.endsWith(`/${p}`));
|
|
|
|
const postfixByNumber = key.match(keyEndpointByNumber);
|
|
|
|
if (!postfix && postfixByNumber) {
|
|
|
|
postfix = Number(postfixByNumber[1]);
|
|
|
|
}
|
|
|
|
if (postfix) {
|
|
|
|
key = key.replace(`/${postfix}`, '');
|
2019-06-16 04:49:15 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const entity = settings.getEntity(key);
|
|
|
|
if (!entity) {
|
|
|
|
return null;
|
|
|
|
} else if (entity.type === 'device') {
|
2019-09-23 13:21:27 -07:00
|
|
|
const device = this.getDeviceByIeeeAddr(entity.ID);
|
2019-09-25 16:14:58 -07:00
|
|
|
if (!device) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
|
|
|
const endpoints = mapped && mapped.endpoint ? mapped.endpoint(device) : null;
|
|
|
|
let isDefaultEndpoint = true;
|
|
|
|
let endpoint;
|
|
|
|
if (postfix) {
|
|
|
|
isDefaultEndpoint = false;
|
|
|
|
if (postfixByNumber) {
|
|
|
|
endpoint = device.getEndpoint(postfix);
|
2019-06-13 12:43:12 -07:00
|
|
|
} else {
|
2019-09-09 10:48:09 -07:00
|
|
|
assert(mapped != null, `Postfix '${postfix}' is given but device is unsupported`);
|
|
|
|
assert(endpoints != null, `Postfix '${postfix}' is given but device defines no endpoints`);
|
|
|
|
const endpointID = endpoints[postfix];
|
|
|
|
assert(endpointID, `Postfix '${postfix}' is given but device has no such endpoint`);
|
|
|
|
endpoint = device.getEndpoint(endpointID);
|
2019-05-24 09:21:53 -07:00
|
|
|
}
|
2019-09-09 10:48:09 -07:00
|
|
|
} else if (endpoints && endpoints['default']) {
|
|
|
|
endpoint = device.getEndpoint(endpoints['default']);
|
2019-06-13 12:43:12 -07:00
|
|
|
} else {
|
2019-09-09 10:48:09 -07:00
|
|
|
endpoint = device.endpoints[0];
|
2019-06-13 12:43:12 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const endpointName = endpoints ? Object.entries(endpoints).find((e) => e[1] === endpoint.ID)[0] : null;
|
|
|
|
return {
|
|
|
|
type: 'device', device, settings: entity, mapped, endpoint, name: entity.friendlyName,
|
|
|
|
isDefaultEndpoint, endpointName,
|
|
|
|
};
|
2019-06-13 12:43:12 -07:00
|
|
|
} else {
|
2019-09-23 13:21:27 -07:00
|
|
|
let group = this.getGroupByID(entity.ID);
|
|
|
|
if (!group) group = this.createGroup(entity.ID);
|
2019-09-09 10:48:09 -07:00
|
|
|
return {type: 'group', group, settings: entity, name: entity.friendlyName};
|
2019-06-13 12:43:12 -07:00
|
|
|
}
|
2019-09-09 10:48:09 -07:00
|
|
|
} else {
|
|
|
|
const setting = settings.getEntity(key.ieeeAddr);
|
|
|
|
return {
|
|
|
|
type: 'device',
|
|
|
|
device: key,
|
|
|
|
settings: setting,
|
|
|
|
mapped: zigbeeHerdsmanConverters.findByZigbeeModel(key.modelID),
|
|
|
|
name: setting ? setting.friendlyName : (key.type === 'Coordinator' ? 'Coordinator' : key.ieeeAddr),
|
2019-02-01 17:41:05 -07:00
|
|
|
};
|
2019-02-01 16:57:51 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-23 13:21:27 -07:00
|
|
|
getGroupByID(ID) {
|
|
|
|
return this.herdsman.getGroupByID(ID);
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
|
|
|
|
2019-09-23 13:21:27 -07:00
|
|
|
getGroups() {
|
|
|
|
return this.herdsman.getGroups();
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
|
|
|
|
2019-09-23 13:21:27 -07:00
|
|
|
createGroup(groupID) {
|
|
|
|
return this.herdsman.createGroup(groupID);
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
|
|
|
|
2019-10-17 13:01:39 -07:00
|
|
|
acceptJoiningDeviceHandler(ieeeAddr) {
|
|
|
|
// If set whitelist devices, all other device will be rejected to join the network
|
|
|
|
if (settings.get().whitelist.length > 0) {
|
|
|
|
if (settings.get().whitelist.includes(ieeeAddr)) {
|
|
|
|
logger.info(`Accepting joining whitelisted device '${ieeeAddr}'`);
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
logger.info(`Rejecting joining non-whitelisted device '${ieeeAddr}'`);
|
|
|
|
return false;
|
|
|
|
}
|
2019-10-26 09:05:40 -07:00
|
|
|
} else if (settings.get().ban.length > 0) {
|
2019-10-17 13:01:39 -07:00
|
|
|
if (settings.get().ban.includes(ieeeAddr)) {
|
|
|
|
logger.info(`Rejecting joining banned device '${ieeeAddr}'`);
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
logger.info(`Accepting joining non-banned device '${ieeeAddr}'`);
|
|
|
|
return true;
|
|
|
|
}
|
2019-10-26 09:05:40 -07:00
|
|
|
} else {
|
|
|
|
return true;
|
2019-10-17 13:01:39 -07:00
|
|
|
}
|
|
|
|
}
|
2018-04-18 09:25:40 -07:00
|
|
|
}
|
|
|
|
|
2019-02-23 07:39:24 -07:00
|
|
|
module.exports = Zigbee;
|