2018-04-08 05:46:20 -07:00
|
|
|
const util = require("util");
|
|
|
|
const ZShepherd = require('zigbee-shepherd');
|
|
|
|
const mqtt = require('mqtt')
|
2018-04-08 08:25:28 -07:00
|
|
|
const fs = require('fs');
|
2018-04-09 08:09:52 -07:00
|
|
|
const parsers = require('./parsers');
|
2018-04-16 12:41:24 -07:00
|
|
|
const commands = require('./commands');
|
2018-04-11 10:55:54 -07:00
|
|
|
const deviceMapping = require('./devices');
|
2018-04-09 08:37:21 -07:00
|
|
|
const config = require('yaml-config');
|
2018-04-14 16:11:37 -07:00
|
|
|
const configFile = `${__dirname}/data/configuration.yaml`;
|
2018-04-14 07:17:25 -07:00
|
|
|
const winston = require('winston');
|
2018-04-11 11:00:18 -07:00
|
|
|
let settings = config.readConfig(configFile, 'user');
|
2018-04-15 12:52:58 -07:00
|
|
|
const stateCache = {};
|
2018-04-09 08:37:21 -07:00
|
|
|
|
2018-04-14 07:17:25 -07:00
|
|
|
const logger = new (winston.Logger)({
|
|
|
|
transports: [
|
|
|
|
new (winston.transports.Console)({
|
|
|
|
timestamp: () => new Date().toLocaleString(),
|
|
|
|
formatter: function(options) {
|
|
|
|
return options.timestamp() + ' ' +
|
|
|
|
winston.config.colorize(options.level, options.level.toUpperCase()) + ' ' +
|
|
|
|
(options.message ? options.message : '') +
|
|
|
|
(options.meta && Object.keys(options.meta).length ? '\n\t'+ JSON.stringify(options.meta) : '' );
|
|
|
|
}
|
|
|
|
})
|
|
|
|
]
|
|
|
|
});
|
|
|
|
|
2018-04-09 08:37:21 -07:00
|
|
|
// Create empty device array if not set yet.
|
|
|
|
if (!settings.devices) {
|
|
|
|
settings.devices = {};
|
|
|
|
writeConfig();
|
2018-04-08 08:25:28 -07:00
|
|
|
}
|
|
|
|
|
2018-04-08 05:46:20 -07:00
|
|
|
// Setup client
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.info(`Connecting to MQTT server at ${settings.mqtt.server}`)
|
2018-04-11 12:09:19 -07:00
|
|
|
|
|
|
|
const options = {};
|
|
|
|
if (settings.mqtt.user && settings.mqtt.password) {
|
|
|
|
options.username = settings.mqtt.user;
|
|
|
|
options.password = settings.mqtt.password;
|
|
|
|
}
|
|
|
|
|
|
|
|
const client = mqtt.connect(settings.mqtt.server, options)
|
2018-04-08 07:41:42 -07:00
|
|
|
const shepherd = new ZShepherd(
|
2018-04-09 08:37:21 -07:00
|
|
|
settings.serial.port,
|
2018-04-08 07:41:42 -07:00
|
|
|
{
|
|
|
|
net: {panId: 0x1a62},
|
|
|
|
dbPath: `${__dirname}/data/database.db`
|
|
|
|
}
|
|
|
|
);
|
2017-09-27 09:39:10 -07:00
|
|
|
|
2018-04-08 06:00:36 -07:00
|
|
|
// Register callbacks
|
|
|
|
client.on('connect', handleConnect);
|
2018-04-16 12:41:24 -07:00
|
|
|
client.on('message', handleMqttMessage);
|
2018-04-08 07:16:36 -07:00
|
|
|
shepherd.on('ready', handleReady);
|
|
|
|
shepherd.on('ind', handleMessage);
|
2018-04-08 06:33:47 -07:00
|
|
|
process.on('SIGINT', handleQuit);
|
2018-04-08 06:00:36 -07:00
|
|
|
|
2018-04-11 11:14:12 -07:00
|
|
|
// Check every interval if connected to MQTT server.
|
|
|
|
setInterval(() => {
|
|
|
|
if (client.reconnecting) {
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.error('Not connected to MQTT server!');
|
2018-04-11 11:14:12 -07:00
|
|
|
}
|
|
|
|
}, 10 * 1000); // seconds * 1000.
|
|
|
|
|
2018-04-08 06:00:36 -07:00
|
|
|
// Start server
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.info(`Starting zigbee-shepherd with device ${settings.serial.port}`)
|
2018-04-08 06:00:36 -07:00
|
|
|
shepherd.start((err) => {
|
|
|
|
if (err) {
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.error('Error while starting zigbee-shepherd');
|
|
|
|
logger.error(err);
|
2018-04-08 06:28:59 -07:00
|
|
|
} else {
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.info('zigbee-shepherd started');
|
2018-04-08 06:00:36 -07:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
function handleReady() {
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.info('zigbee-shepherd ready');
|
2018-04-08 07:16:36 -07:00
|
|
|
|
2018-04-16 12:41:24 -07:00
|
|
|
const devices = shepherd.list().filter((device) => device.type !== 'Coordinator');
|
2018-04-08 07:16:36 -07:00
|
|
|
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.info(`Currently ${devices.length} devices are joined:`);
|
|
|
|
devices.forEach((device) => logger.info(getDeviceLogMessage(device)));
|
2018-04-08 07:16:36 -07:00
|
|
|
|
|
|
|
// Set all Xiaomi devices to be online, so shepherd won't try
|
|
|
|
// to query info from devices (which would fail because they go tosleep).
|
|
|
|
devices.forEach((device) => {
|
|
|
|
shepherd.find(device.ieeeAddr, 1).getDevice().update({
|
|
|
|
status: 'online',
|
|
|
|
joinTime: Math.floor(Date.now()/1000)
|
|
|
|
});
|
2017-12-24 13:41:01 -07:00
|
|
|
});
|
2018-04-08 06:51:33 -07:00
|
|
|
|
|
|
|
// Allow or disallow new devices to join the network.
|
2018-04-09 08:45:02 -07:00
|
|
|
if (settings.allowJoin) {
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.warn('allowJoin set to true in configuration.yaml.')
|
|
|
|
logger.warn('Allowing new devices to join.');
|
|
|
|
logger.warn('Remove this parameter once you joined all devices.');
|
2018-04-08 06:51:33 -07:00
|
|
|
}
|
2018-04-08 07:16:36 -07:00
|
|
|
|
2018-04-09 08:45:02 -07:00
|
|
|
shepherd.permitJoin(settings.allowJoin ? 255 : 0, (err) => {
|
2018-04-08 06:51:33 -07:00
|
|
|
if (err) {
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.info(err);
|
2018-04-08 06:51:33 -07:00
|
|
|
}
|
2017-09-27 14:56:21 -07:00
|
|
|
});
|
2018-04-08 06:00:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
function handleConnect() {
|
2018-04-14 16:14:43 -07:00
|
|
|
mqttPublish(`${settings.mqtt.base_topic}/bridge/state`, 'online', true);
|
2018-04-16 12:41:24 -07:00
|
|
|
client.subscribe(`${settings.mqtt.base_topic}/+/set`)
|
2018-04-08 06:00:36 -07:00
|
|
|
}
|
|
|
|
|
2018-04-08 07:16:36 -07:00
|
|
|
function handleMessage(msg) {
|
2018-04-16 12:41:24 -07:00
|
|
|
if (!msg.endpoints) {
|
2018-04-09 08:09:52 -07:00
|
|
|
return;
|
2017-09-27 09:39:10 -07:00
|
|
|
}
|
|
|
|
|
2018-04-09 08:09:52 -07:00
|
|
|
const device = msg.endpoints[0].device;
|
2018-04-11 10:55:54 -07:00
|
|
|
|
|
|
|
// New device!
|
2018-04-09 08:37:21 -07:00
|
|
|
if (!settings.devices[device.ieeeAddr]) {
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.info(`New device with address ${device.ieeeAddr} connected!`);
|
2018-04-09 08:37:21 -07:00
|
|
|
|
|
|
|
settings.devices[device.ieeeAddr] = {
|
2018-04-14 16:14:43 -07:00
|
|
|
friendly_name: device.ieeeAddr,
|
|
|
|
retain: false,
|
2018-04-09 08:37:21 -07:00
|
|
|
};
|
2018-04-11 10:55:54 -07:00
|
|
|
|
2018-04-09 08:37:21 -07:00
|
|
|
writeConfig();
|
2017-09-27 14:56:21 -07:00
|
|
|
}
|
2018-04-09 08:09:52 -07:00
|
|
|
|
2018-04-11 10:55:54 -07:00
|
|
|
// We can't handle devices without modelId.
|
|
|
|
if (!device.modelId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Map modelID to Xiaomi model.
|
2018-04-09 11:01:49 -07:00
|
|
|
const modelID = msg.endpoints[0].device.modelId;
|
2018-04-11 10:55:54 -07:00
|
|
|
const mappedModel = deviceMapping[modelID];
|
|
|
|
|
|
|
|
if (!mappedModel) {
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.error(`Device with modelID '${modelID}' is not supported.`);
|
|
|
|
logger.error('Please create an issue on https://github.com/Koenkk/xiaomi-zb2mqtt/issues to add support for your device');
|
2018-04-16 11:08:38 -07:00
|
|
|
return;
|
2018-04-11 10:55:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Find a parser for this modelID and cid.
|
2018-04-09 11:01:49 -07:00
|
|
|
const cid = msg.data.cid;
|
2018-04-16 12:49:25 -07:00
|
|
|
const _parsers = parsers.filter((p) => p.devices.includes(mappedModel.model) && p.cid === cid && p.type === msg.type);
|
2018-04-09 11:21:23 -07:00
|
|
|
|
2018-04-14 06:29:07 -07:00
|
|
|
if (!_parsers.length) {
|
2018-04-16 12:49:25 -07:00
|
|
|
logger.error(`No parser available for '${mappedModel.model}' with cid '${cid}' and type '${msg.type}'`);
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.error('Please create an issue on https://github.com/Koenkk/xiaomi-zb2mqtt/issues with this message.');
|
2018-04-09 08:09:52 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-09 09:36:32 -07:00
|
|
|
// Parse generic information from message.
|
2018-04-09 08:37:21 -07:00
|
|
|
const friendlyName = settings.devices[device.ieeeAddr].friendly_name;
|
2018-04-14 16:14:43 -07:00
|
|
|
const retain = settings.devices[device.ieeeAddr].retain;
|
2018-04-14 16:11:37 -07:00
|
|
|
const topic = `${settings.mqtt.base_topic}/${friendlyName}`;
|
|
|
|
const publish = (payload) => {
|
2018-04-15 12:52:58 -07:00
|
|
|
if (stateCache[device.ieeeAddr]) {
|
|
|
|
payload = {...stateCache[device.ieeeAddr], ...payload};
|
2018-04-14 16:11:37 -07:00
|
|
|
}
|
|
|
|
|
2018-04-14 16:14:43 -07:00
|
|
|
mqttPublish(topic, JSON.stringify(payload), retain);
|
2018-04-14 16:11:37 -07:00
|
|
|
}
|
2018-04-09 09:36:32 -07:00
|
|
|
|
|
|
|
// Get payload for the message.
|
|
|
|
// - If a payload is returned publish it to the MQTT broker
|
|
|
|
// - If NO payload is returned do nothing. This is for non-standard behaviour
|
|
|
|
// for e.g. click switches where we need to count number of clicks and detect long presses.
|
2018-04-14 06:29:07 -07:00
|
|
|
_parsers.forEach((parser) => {
|
|
|
|
const payload = parser.parse(msg, publish);
|
2018-04-14 07:17:25 -07:00
|
|
|
|
2018-04-15 12:52:58 -07:00
|
|
|
if (payload) {
|
|
|
|
stateCache[device.ieeeAddr] = {...stateCache[device.ieeeAddr], ...payload};
|
|
|
|
|
|
|
|
if (!parser.disablePublish) {
|
|
|
|
publish(payload);
|
|
|
|
}
|
2018-04-14 06:29:07 -07:00
|
|
|
}
|
|
|
|
});
|
2018-04-08 06:33:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
function handleQuit() {
|
2018-04-15 12:23:49 -07:00
|
|
|
mqttPublish(`${settings.mqtt.base_topic}/bridge/state`, 'offline', true);
|
|
|
|
|
2018-04-08 06:33:47 -07:00
|
|
|
shepherd.stop((err) => {
|
|
|
|
if (err) {
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.error('Error while stopping zigbee-shepherd');
|
2018-04-08 06:33:47 -07:00
|
|
|
} else {
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.error('zigbee-shepherd stopped')
|
2018-04-08 06:33:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
process.exit();
|
|
|
|
});
|
|
|
|
}
|
2018-04-08 08:25:28 -07:00
|
|
|
|
2018-04-14 16:14:43 -07:00
|
|
|
function mqttPublish(topic, payload, retain) {
|
2018-04-11 11:14:12 -07:00
|
|
|
if (client.reconnecting) {
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.error(`Not connected to MQTT server!`);
|
|
|
|
logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`);
|
2018-04-11 11:14:12 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-14 07:17:25 -07:00
|
|
|
logger.info(`MQTT publish, topic: '${topic}', payload: '${payload}'`);
|
2018-04-14 16:14:43 -07:00
|
|
|
client.publish(topic, payload, {retain: retain});
|
2018-04-11 11:14:12 -07:00
|
|
|
}
|
2018-04-08 08:25:28 -07:00
|
|
|
|
2018-04-09 08:37:21 -07:00
|
|
|
function writeConfig() {
|
|
|
|
config.updateConfig(settings, configFile, 'user');
|
2018-04-11 11:00:18 -07:00
|
|
|
settings = config.readConfig(configFile, 'user');
|
2018-04-08 08:25:28 -07:00
|
|
|
}
|
2018-04-11 10:55:54 -07:00
|
|
|
|
|
|
|
function getDeviceLogMessage(device) {
|
|
|
|
let friendlyName = 'unknown';
|
|
|
|
let friendlyDevice = {model: 'unkown', description: 'unknown'};
|
|
|
|
|
|
|
|
if (deviceMapping[device.modelId]) {
|
|
|
|
friendlyDevice = deviceMapping[device.modelId];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (settings.devices[device.ieeeAddr]) {
|
|
|
|
friendlyName = settings.devices[device.ieeeAddr].friendly_name
|
|
|
|
}
|
|
|
|
|
|
|
|
return `${friendlyName} (${device.ieeeAddr}): ${friendlyDevice.model} - ${friendlyDevice.description}`;
|
|
|
|
}
|
2018-04-16 12:41:24 -07:00
|
|
|
|
|
|
|
function handleMqttMessage(topic, message) {
|
|
|
|
const friendlyName = topic.split('/')[1];
|
|
|
|
|
|
|
|
// Find device id of this friendlyName
|
|
|
|
const deviceID = Object.keys(settings.devices).find((id) => settings.devices[id].friendly_name === friendlyName);
|
|
|
|
if (!deviceID) {
|
|
|
|
logger.error(`Cannot handle '${topic}' because ID of '${friendlyName}' cannot be found`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find device in zigbee-shepherd
|
|
|
|
const device = shepherd.find(deviceID, 1);
|
|
|
|
if (!device) {
|
|
|
|
logger.error(`Cannot handle '${topic}' because '${deviceID}' is not known by zigbee-shepherd`);
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.info(`Executing '${topic}' '${message.toString()}' `)
|
|
|
|
|
|
|
|
const json = JSON.parse(message);
|
|
|
|
|
|
|
|
// Iterate over all keys in the json.
|
|
|
|
Object.keys(json).forEach((key) => {
|
|
|
|
// Find parser for this key
|
|
|
|
const parser = commands[key];
|
|
|
|
if (!parser) {
|
|
|
|
logger.error(`No parser available for '${key}' (${json[key]})`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const parsed = parser(json[key]);
|
|
|
|
const callback = (err, rsp) => {
|
|
|
|
// Devices do not report when they go off.
|
|
|
|
// Ensure state (on/off) is always in sync.
|
|
|
|
if (!err && key === 'state') {
|
|
|
|
mqttPublish(
|
|
|
|
`${settings.mqtt.base_topic}/${friendlyName}`,
|
|
|
|
JSON.stringify({state: json[key]}),
|
|
|
|
true
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
device.functional(parsed.cId, parsed.cmd, parsed.zclData, callback);
|
|
|
|
});
|
|
|
|
}
|