mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-07-03 18:41:39 +00:00
Refactor and rename. #8
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
# xiaomi-zb2mqtt
|
||||
# zigbee2mqtt
|
||||

|
||||
|
||||
Allows you to use your Xiaomi Zigbee sensors and switches **without** Xiaomi's gateway.
|
||||
Allows you to use your Zigbee devices **without** the vendors (Xiaomi/TRADFRI/Hue) bridge/gateway.
|
||||
|
||||
It bridges the events sent from the sensors and switches to MQTT. You can integrate the cheap and nice Zigbee sensors and switches with whatever smart home infrastructure you are using.
|
||||
It bridges the events sent from the sensors and switches to MQTT. In this way you can integrate your Zigbee devices with whatever smart home infrastructure you are using.
|
||||
|
||||
The [wiki](https://github.com/Koenkk/xiaomi-zb2mqtt/wiki) provides you all the information needed to get up and running!
|
||||
The [wiki](https://github.com/Koenkk/zigbee2mqtt/wiki) provides you all the information needed to get up and running!
|
||||
|
||||
### Contributors
|
||||
* [AndrewLinden](https://github.com/AndrewLinden)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker build -t koenkk/xiaomi-zb2mqtt:arm32v7 -f Dockerfile ../../
|
||||
docker push koenkk/xiaomi-zb2mqtt:arm32v7
|
||||
docker build -t koenkk/zigbee2mqtt:arm32v7 -f Dockerfile ../../
|
||||
docker push koenkk/zigbee2mqtt:arm32v7
|
||||
|
||||
@@ -1,273 +1,10 @@
|
||||
const util = require("util");
|
||||
const ZShepherd = require('zigbee-shepherd');
|
||||
const mqtt = require('mqtt')
|
||||
const fs = require('fs');
|
||||
const parsers = require('./parsers');
|
||||
const commands = require('./commands');
|
||||
const deviceMapping = require('./devices');
|
||||
const config = require('yaml-config');
|
||||
const configFile = `${__dirname}/data/configuration.yaml`;
|
||||
const winston = require('winston');
|
||||
let settings = config.readConfig(configFile, 'user');
|
||||
const stateCache = {};
|
||||
const Controller = require('./lib/controller');
|
||||
|
||||
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) : '' );
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
const controller = new Controller();
|
||||
controller.start();
|
||||
|
||||
// Create empty device array if not set yet.
|
||||
if (!settings.devices) {
|
||||
settings.devices = {};
|
||||
writeConfig();
|
||||
}
|
||||
|
||||
// Setup client
|
||||
logger.info(`Connecting to MQTT server at ${settings.mqtt.server}`)
|
||||
|
||||
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)
|
||||
const shepherd = new ZShepherd(
|
||||
settings.serial.port,
|
||||
{
|
||||
net: {panId: 0x1a62},
|
||||
dbPath: `${__dirname}/data/database.db`
|
||||
}
|
||||
);
|
||||
|
||||
// Register callbacks
|
||||
client.on('connect', handleConnect);
|
||||
client.on('message', handleMqttMessage);
|
||||
shepherd.on('ready', handleReady);
|
||||
shepherd.on('ind', handleMessage);
|
||||
process.on('SIGINT', handleQuit);
|
||||
|
||||
// Check every interval if connected to MQTT server.
|
||||
setInterval(() => {
|
||||
if (client.reconnecting) {
|
||||
logger.error('Not connected to MQTT server!');
|
||||
}
|
||||
}, 10 * 1000); // seconds * 1000.
|
||||
|
||||
// Start server
|
||||
logger.info(`Starting zigbee-shepherd with device ${settings.serial.port}`)
|
||||
shepherd.start((err) => {
|
||||
if (err) {
|
||||
logger.error('Error while starting zigbee-shepherd');
|
||||
logger.error(err);
|
||||
} else {
|
||||
logger.info('zigbee-shepherd started');
|
||||
}
|
||||
});
|
||||
|
||||
function handleReady() {
|
||||
logger.info('zigbee-shepherd ready');
|
||||
|
||||
const devices = shepherd.list().filter((device) => device.type !== 'Coordinator');
|
||||
|
||||
logger.info(`Currently ${devices.length} devices are joined:`);
|
||||
devices.forEach((device) => logger.info(getDeviceLogMessage(device)));
|
||||
|
||||
// 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)
|
||||
});
|
||||
});
|
||||
|
||||
// Allow or disallow new devices to join the network.
|
||||
if (settings.allowJoin) {
|
||||
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.');
|
||||
}
|
||||
|
||||
shepherd.permitJoin(settings.allowJoin ? 255 : 0, (err) => {
|
||||
if (err) {
|
||||
logger.info(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleConnect() {
|
||||
mqttPublish(`${settings.mqtt.base_topic}/bridge/state`, 'online', true);
|
||||
client.subscribe(`${settings.mqtt.base_topic}/+/set`)
|
||||
}
|
||||
|
||||
function handleMessage(msg) {
|
||||
if (!msg.endpoints) {
|
||||
return;
|
||||
}
|
||||
|
||||
const device = msg.endpoints[0].device;
|
||||
|
||||
// New device!
|
||||
if (!settings.devices[device.ieeeAddr]) {
|
||||
logger.info(`New device with address ${device.ieeeAddr} connected!`);
|
||||
|
||||
settings.devices[device.ieeeAddr] = {
|
||||
friendly_name: device.ieeeAddr,
|
||||
retain: false,
|
||||
};
|
||||
|
||||
writeConfig();
|
||||
}
|
||||
|
||||
// We can't handle devices without modelId.
|
||||
if (!device.modelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Map modelID to Xiaomi model.
|
||||
const modelID = msg.endpoints[0].device.modelId;
|
||||
const mappedModel = deviceMapping[modelID];
|
||||
|
||||
if (!mappedModel) {
|
||||
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');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find a parser for this modelID and cid.
|
||||
const cid = msg.data.cid;
|
||||
const _parsers = parsers.filter((p) => p.devices.includes(mappedModel.model) && p.cid === cid && p.type === msg.type);
|
||||
|
||||
if (!_parsers.length) {
|
||||
logger.error(`No parser available for '${mappedModel.model}' with cid '${cid}' and type '${msg.type}'`);
|
||||
logger.error('Please create an issue on https://github.com/Koenkk/xiaomi-zb2mqtt/issues with this message.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse generic information from message.
|
||||
const friendlyName = settings.devices[device.ieeeAddr].friendly_name;
|
||||
const retain = settings.devices[device.ieeeAddr].retain;
|
||||
const topic = `${settings.mqtt.base_topic}/${friendlyName}`;
|
||||
const publish = (payload) => {
|
||||
if (stateCache[device.ieeeAddr]) {
|
||||
payload = {...stateCache[device.ieeeAddr], ...payload};
|
||||
}
|
||||
|
||||
mqttPublish(topic, JSON.stringify(payload), retain);
|
||||
}
|
||||
|
||||
// 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.
|
||||
_parsers.forEach((parser) => {
|
||||
const payload = parser.parse(msg, publish);
|
||||
|
||||
if (payload) {
|
||||
stateCache[device.ieeeAddr] = {...stateCache[device.ieeeAddr], ...payload};
|
||||
|
||||
if (!parser.disablePublish) {
|
||||
publish(payload);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleQuit() {
|
||||
mqttPublish(`${settings.mqtt.base_topic}/bridge/state`, 'offline', true);
|
||||
|
||||
shepherd.stop((err) => {
|
||||
if (err) {
|
||||
logger.error('Error while stopping zigbee-shepherd');
|
||||
} else {
|
||||
logger.error('zigbee-shepherd stopped')
|
||||
}
|
||||
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
|
||||
function mqttPublish(topic, payload, retain) {
|
||||
if (client.reconnecting) {
|
||||
logger.error(`Not connected to MQTT server!`);
|
||||
logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`MQTT publish, topic: '${topic}', payload: '${payload}'`);
|
||||
client.publish(topic, payload, {retain: retain});
|
||||
}
|
||||
|
||||
function writeConfig() {
|
||||
config.updateConfig(settings, configFile, 'user');
|
||||
settings = config.readConfig(configFile, 'user');
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
controller.stop(() => process.exit());
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
const MQTT = require('./mqtt');
|
||||
const Zigbee = require('./zigbee');
|
||||
const logger = require('./util/logger');
|
||||
const deviceMapping = require('./devices');
|
||||
const zigbee2mqtt = require('./converters/zigbee2mqtt');
|
||||
const mqtt2zigbee = require('./converters/mqtt2zigbee');
|
||||
|
||||
class Controller {
|
||||
|
||||
constructor() {
|
||||
this.zigbee = new Zigbee();
|
||||
this.mqtt = new MQTT();
|
||||
this.stateCache = {};
|
||||
|
||||
this.handleZigbeeMessage = this.handleZigbeeMessage.bind(this);
|
||||
this.handleMQTTMessage = this.handleMQTTMessage.bind(this);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.zigbee.start(this.handleZigbeeMessage, (error) => {
|
||||
if (error) {
|
||||
logger.error('Failed to start');
|
||||
} else {
|
||||
this.mqtt.connect(this.handleMQTTMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stop(callback) {
|
||||
this.mqtt.disconnect();
|
||||
this.zigbee.stop(callback);
|
||||
}
|
||||
|
||||
handleZigbeeMessage(message) {
|
||||
if (!message.endpoints) {
|
||||
// We dont handle messages without endpoints.
|
||||
return;
|
||||
}
|
||||
|
||||
const device = message.endpoints[0].device;
|
||||
|
||||
// Check if this is a new device.
|
||||
if (!settings.get().devices[device.ieeeAddr]) {
|
||||
logger.info(`New device with address ${device.ieeeAddr} connected!`);
|
||||
|
||||
settings.get().devices[device.ieeeAddr] = {
|
||||
friendly_name: device.ieeeAddr,
|
||||
retain: false,
|
||||
};
|
||||
|
||||
settings.write();
|
||||
}
|
||||
|
||||
// We can't handle devices without modelId.
|
||||
if (!device.modelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Map Zigbee modelID to vendor modelID.
|
||||
const modelID = msg.endpoints[0].device.modelId;
|
||||
const mappedModel = deviceMapping[modelID];
|
||||
|
||||
if (!mappedModel) {
|
||||
logger.error(`Device with modelID '${modelID}' is not supported.`);
|
||||
logger.error('Please create an issue on https://github.com/Koenkk/zigbee2mqtt/issues to add support for your device');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find a conveter for this message.
|
||||
const cid = msg.data.cid;
|
||||
const converters = zigbee2mqtt.filter((c) => c.devices.includes(mappedModel.model) && c.cid === cid && c.type === msg.type);
|
||||
|
||||
if (!converters.length) {
|
||||
logger.error(`No converter available for '${mappedModel.model}' with cid '${cid}' and type '${msg.type}'`);
|
||||
logger.error('Please create an issue on https://github.com/Koenkk/zigbee2mqtt/issues with this message.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert this Zigbee message to a MQTT message.
|
||||
const friendlyName = settings.get().devices[device.ieeeAddr].friendly_name;
|
||||
const retain = settings.get().devices[device.ieeeAddr].retain;
|
||||
const topic = `${settings.get().mqtt.base_topic}/${friendlyName}`;
|
||||
|
||||
const publish = (payload) => {
|
||||
if (this.stateCache[device.ieeeAddr]) {
|
||||
payload = {...this.stateCache[device.ieeeAddr], ...payload};
|
||||
}
|
||||
|
||||
this.mqtt.publish(topic, JSON.stringify(payload), retain);
|
||||
}
|
||||
|
||||
// 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.
|
||||
converters.forEach((converter) => {
|
||||
const payload = converter.convert(msg, publish);
|
||||
|
||||
if (payload) {
|
||||
this.stateCache[device.ieeeAddr] = {...this.stateCache[device.ieeeAddr], ...payload};
|
||||
|
||||
if (!converter.disablePublish) {
|
||||
publish(payload);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleMQTTMessage(topic, message) {
|
||||
const friendlyName = topic.split('/')[1];
|
||||
|
||||
// Map friendlyName to deviceID.
|
||||
const deviceID = Object.keys(settings.get().devices).find((id) => settings.get().devices[id].friendly_name === friendlyName);
|
||||
if (!deviceID) {
|
||||
logger.error(`Cannot handle '${topic}' because deviceID of '${friendlyName}' cannot be found`);
|
||||
}
|
||||
|
||||
// Convert the MQTT message to a Zigbee message.
|
||||
const json = JSON.parse(message);
|
||||
Object.keys(json).forEach((key) => {
|
||||
// Find converter for this key.
|
||||
const converter = mqtt2zigbee[key];
|
||||
if (!converter) {
|
||||
logger.error(`No converter available for '${key}' (${json[key]})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = converter(json[key]);
|
||||
const callback = (error, response) => {
|
||||
// Devices do not report when they go off, this ensures state (on/off) is always in sync.
|
||||
if (!error && key === 'state') {
|
||||
this.mqtt.publish(friendlyName, JSON.stringify({state: json[key]}), true);
|
||||
}
|
||||
};
|
||||
|
||||
this.zigbee.publish(deviceID, message.cId, message.cmd, message.zclData, callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Controller;
|
||||
@@ -1,4 +1,4 @@
|
||||
const commands = {
|
||||
const converters = {
|
||||
"state": (value) => {
|
||||
return {
|
||||
cId: 'genOnOff',
|
||||
@@ -28,4 +28,4 @@ const commands = {
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = commands;
|
||||
module.exports = converters;
|
||||
@@ -29,7 +29,7 @@ const parsers = [
|
||||
cid: 'genBasic',
|
||||
type: 'attReport',
|
||||
disablePublish: true,
|
||||
parse: (msg, publish) => {
|
||||
convert: (msg, publish) => {
|
||||
let voltage = null;
|
||||
|
||||
if (msg.data.data['65281']) {
|
||||
@@ -47,7 +47,7 @@ const parsers = [
|
||||
devices: ['WXKG01LM'],
|
||||
cid: 'genOnOff',
|
||||
type: 'attReport',
|
||||
parse: (msg, publish) => {
|
||||
convert: (msg, publish) => {
|
||||
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
||||
const state = msg.data.data['onOff'];
|
||||
|
||||
@@ -74,19 +74,19 @@ const parsers = [
|
||||
devices: ['WSDCGQ01LM'],
|
||||
cid: 'msTemperatureMeasurement',
|
||||
type: 'attReport',
|
||||
parse: (msg) => {return {temperature: parseFloat(msg.data.data['measuredValue']) / 100.0}}
|
||||
convert: (msg) => {return {temperature: parseFloat(msg.data.data['measuredValue']) / 100.0}}
|
||||
},
|
||||
{
|
||||
devices: ['WSDCGQ01LM'],
|
||||
cid: 'msRelativeHumidity',
|
||||
type: 'attReport',
|
||||
parse: (msg) => {return {humidity: parseFloat(msg.data.data['measuredValue']) / 100.0}}
|
||||
convert: (msg) => {return {humidity: parseFloat(msg.data.data['measuredValue']) / 100.0}}
|
||||
},
|
||||
{
|
||||
devices: ['RTCGQ01LM'],
|
||||
cid: 'msOccupancySensing',
|
||||
type: 'attReport',
|
||||
parse: (msg, publish) => {
|
||||
convert: (msg, publish) => {
|
||||
// The occupancy sensor only sends a message when motion detected.
|
||||
// Therefore we need to publish the no_motion detected by ourselves.
|
||||
// no_motion is triggered after 3 minutes of no motion.
|
||||
@@ -102,7 +102,7 @@ const parsers = [
|
||||
store[deviceID] = setTimeout(() => {
|
||||
publish({occupancy: 'no_motion'})
|
||||
store[deviceID] = null;
|
||||
}, noMotionTimeout * 60 * 1000);
|
||||
}, noMotionTimeout * 60 * 1000);
|
||||
return {occupancy: 'motion'};
|
||||
}
|
||||
},
|
||||
@@ -110,19 +110,19 @@ const parsers = [
|
||||
devices: ['MCCGQ01LM'],
|
||||
cid: 'genOnOff',
|
||||
type: 'attReport',
|
||||
parse: (msg) => {return {state: msg.data.data['onOff'] ? 'open' : 'closed'}}
|
||||
convert: (msg) => {return {state: msg.data.data['onOff'] ? 'open' : 'closed'}}
|
||||
},
|
||||
{
|
||||
devices: ['LED1545G12'],
|
||||
cid: 'genLevelCtrl',
|
||||
type: 'devChange',
|
||||
parse: (msg) => {return {brightness: msg.data.data['currentLevel']}},
|
||||
convert: (msg) => {return {brightness: msg.data.data['currentLevel']}},
|
||||
},
|
||||
{
|
||||
devices: ['LED1545G12'],
|
||||
cid: 'lightingColorCtrl',
|
||||
type: 'devChange',
|
||||
parse: (msg) => {return {color_temp: msg.data.data['colorTemperature']}},
|
||||
convert: (msg) => {return {color_temp: msg.data.data['colorTemperature']}},
|
||||
},
|
||||
|
||||
// Ignore the following messages:
|
||||
@@ -130,13 +130,13 @@ const parsers = [
|
||||
devices: ['LED1545G12'],
|
||||
cid: 'genOnOff',
|
||||
type: 'devChange',
|
||||
parse: (msg) => null
|
||||
convert: (msg) => null
|
||||
},
|
||||
{
|
||||
devices: ['WXKG01LM'],
|
||||
cid: 'genOnOff',
|
||||
type: 'devChange',
|
||||
parse: (msg) => null
|
||||
convert: (msg) => null
|
||||
},
|
||||
];
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
const mqtt = require('mqtt');
|
||||
const logger = require('./util/logger');
|
||||
const settings = require('./util/settings');
|
||||
|
||||
|
||||
class MQTT {
|
||||
|
||||
constructor() {
|
||||
this.handleConnect = this.handleConnect.bind(this);
|
||||
this.handleMessage = this.handleMessage.bind(this);
|
||||
}
|
||||
|
||||
connect(onMessage) {
|
||||
const mqttSettings = settings.get().mqtt;
|
||||
logger.info(`Connecting to MQTT server at ${mqttSettings.server}`);
|
||||
|
||||
const options = {};
|
||||
if (mqttSettings.user && mqttSettings.password) {
|
||||
options.username = mqttSettings.user;
|
||||
options.password = mqttSettings.password;
|
||||
}
|
||||
|
||||
this.client = mqtt.connect(mqttSettings.server, options);
|
||||
|
||||
// Register callbacks.
|
||||
this.client.on('connect', this.handleConnect);
|
||||
this.client.on('message', this.handleMessage);
|
||||
|
||||
// Set timer at interval to check if connected to MQTT server.
|
||||
const interval = 10 * 1000; // seconds * 1000.
|
||||
this.connectionTimer = setInterval(() => {
|
||||
if (this.client.reconnecting) {
|
||||
logger.error('Not connected to MQTT server!');
|
||||
}
|
||||
}, interval);
|
||||
|
||||
this.onMessage = onMessage;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
clearTimeout(this.connectionTimer);
|
||||
this.connectionTimer = null;
|
||||
|
||||
this.publish('bridge/state', 'offline', true);
|
||||
|
||||
logger.info('Disconnecting from MQTT server');
|
||||
this.client.end();
|
||||
}
|
||||
|
||||
handleConnect() {
|
||||
logger.info('Connected to MQTT server');
|
||||
this.publish('bridge/state', 'online', true);
|
||||
this.client.subscribe(`${settings.get().mqtt.base_topic}/+/set`);
|
||||
}
|
||||
|
||||
handleMessage(topic, message) {
|
||||
if (this.onMessage) {
|
||||
this.onMessage(topic, message);
|
||||
}
|
||||
}
|
||||
|
||||
publish(topic, payload, retain) {
|
||||
topic = `${settings.get().mqtt.base_topic}/${topic}`;
|
||||
|
||||
if (!this.client || this.client.reconnecting) {
|
||||
logger.error(`Not connected to MQTT server!`);
|
||||
logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`MQTT publish, topic: '${topic}', payload: '${payload}'`);
|
||||
this.client.publish(topic, payload, {retain: retain});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MQTT;
|
||||
@@ -0,0 +1,17 @@
|
||||
const winston = require('winston');
|
||||
|
||||
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) : '' );
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = logger;
|
||||
@@ -0,0 +1,23 @@
|
||||
const yamlConfig = require('yaml-config');
|
||||
const file = `${__dirname}/../../data/configuration.yaml`;
|
||||
let settings = read();
|
||||
|
||||
// Create empty device array if not set yet.
|
||||
if (!settings.devices) {
|
||||
settings.devices = {};
|
||||
write();
|
||||
}
|
||||
|
||||
function write() {
|
||||
yamlConfig.updateConfig(settings, file, 'user');
|
||||
settings = read();
|
||||
}
|
||||
|
||||
function read() {
|
||||
return yamlConfig.readConfig(file, 'user');
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
get: () => settings,
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
const ZShepherd = require('zigbee-shepherd');
|
||||
const logger = require('./util/logger');
|
||||
const settings = require('./util/settings');
|
||||
const deviceMapping = require('./devices');
|
||||
|
||||
const shepherdSettings = {
|
||||
net: {panId: 0x1a62},
|
||||
dbPath: `${__dirname}/../data/database.db`
|
||||
};
|
||||
|
||||
class Zigbee {
|
||||
|
||||
constructor() {
|
||||
this.handleReady = this.handleReady.bind(this);
|
||||
this.handleMessage = this.handleMessage.bind(this);
|
||||
}
|
||||
|
||||
start(onMessage, callback) {
|
||||
logger.info(`Starting zigbee-shepherd`);
|
||||
|
||||
this.shepherd = new ZShepherd(settings.get().serial.port, shepherdSettings);
|
||||
|
||||
this.shepherd.start((error) => {
|
||||
if (error) {
|
||||
logger.error('Error while starting zigbee-shepherd!');
|
||||
} else {
|
||||
logger.info('zigbee-shepherd started');
|
||||
}
|
||||
|
||||
callback(error);
|
||||
});
|
||||
|
||||
// Register callbacks.
|
||||
this.shepherd.on('ready', this.handleReady);
|
||||
this.shepherd.on('ind', this.handleMessage);
|
||||
|
||||
this.onMessage = onMessage;
|
||||
}
|
||||
|
||||
stop(callback) {
|
||||
this.shepherd.stop((error) => {
|
||||
if (error) {
|
||||
logger.error('Error while stopping zigbee-shepherd');
|
||||
} else {
|
||||
logger.error('zigbee-shepherd stopped')
|
||||
}
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
handleReady() {
|
||||
logger.info('zigbee-shepherd ready');
|
||||
|
||||
const devices = this.shepherd.list().filter((device) => device.type !== 'Coordinator');
|
||||
|
||||
logger.info(`Currently ${devices.length} devices are joined:`);
|
||||
devices.forEach((device) => logger.info(getDeviceLogMessage(device)));
|
||||
|
||||
// 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).
|
||||
devices.forEach((device) => {
|
||||
if (device.manufId === 4151) {
|
||||
this.shepherd.find(device.ieeeAddr, 1).getDevice().update({
|
||||
status: 'online',
|
||||
joinTime: Math.floor(Date.now()/1000)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Allow or disallow new devices to join the network.
|
||||
if (settings.get().allowJoin) {
|
||||
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.');
|
||||
}
|
||||
|
||||
this.shepherd.permitJoin(settings.get().allowJoin ? 255 : 0, (error) => {
|
||||
if (error) {
|
||||
logger.info(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleMessage(message) {
|
||||
if (this.onMessage) {
|
||||
this.onMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
getDeviceLogMessage(device) {
|
||||
let friendlyName = 'unknown';
|
||||
let friendlyDevice = {model: 'unkown', description: 'unknown'};
|
||||
|
||||
if (deviceMapping[device.modelId]) {
|
||||
friendlyDevice = deviceMapping[device.modelId];
|
||||
}
|
||||
|
||||
if (settings.get().devices[device.ieeeAddr]) {
|
||||
friendlyName = settings.devices[device.ieeeAddr].friendly_name
|
||||
}
|
||||
|
||||
return `${friendlyName} (${device.ieeeAddr}): ${friendlyDevice.model} - ${friendlyDevice.description}`;
|
||||
}
|
||||
|
||||
publish(deviceID, cId, cmd, zclData, callback) {
|
||||
// Find device in zigbee-shepherd
|
||||
const device = this.shepherd.find(deviceID, 1);
|
||||
if (!device) {
|
||||
logger.error(`Zigbee cannot publish message to device because '${deviceID}' is not known by zigbee-shepherd`);
|
||||
}
|
||||
|
||||
logger.info(`Zigbee publish to '${deviceID}', ${cId} - ${cmd} - ${zclData}`);
|
||||
device.functional(cId, cmd, zclData, callback);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Zigbee;
|
||||
Generated
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xiaomi-zb2mqtt",
|
||||
"version": "1.0.0",
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+8
-5
@@ -1,21 +1,24 @@
|
||||
{
|
||||
"name": "xiaomi-zb2mqtt",
|
||||
"version": "1.0.0",
|
||||
"description": "Xiaomi Zigbee to MQTT bridge using zigbee-shepherd",
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "0.1.0",
|
||||
"description": "Zigbee to MQTT bridge using zigbee-shepherd",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Koenkk/xiaomi-zb2mqtt.git"
|
||||
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
|
||||
},
|
||||
"keywords": [
|
||||
"xiaomi",
|
||||
"tradfri",
|
||||
"hue",
|
||||
"bridge",
|
||||
"zigbee",
|
||||
"mqtt",
|
||||
"cc2531"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"docs": "node doc.js"
|
||||
"docgen": "node support/docgen.js"
|
||||
},
|
||||
"author": "Koen Kanters",
|
||||
"license": "GPL-3.0",
|
||||
|
||||
@@ -8,85 +8,100 @@ const plannedToSupport = [
|
||||
model: 'WXKG11LM',
|
||||
description: 'Aqara wireless switch',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'WSDCGQ11LM',
|
||||
description: 'Aqara temperature & humidity sensor',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'MCCGQ11LM',
|
||||
description: 'Aqara door & window contact sensor',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'RTCGQ11LM',
|
||||
description: 'Aqara human body movement and illuminance sensor',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'SJCGQ11LM',
|
||||
description: 'Aqara water leak sensor',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'MFKZQ01LM',
|
||||
description: 'Mi magic cube controller',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'WXKG03LM',
|
||||
description: 'Aqara single key wireless wall switch',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'WXKG02LM',
|
||||
description: 'Aqara double key wireless wall switch',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'QBKG11LM',
|
||||
description: 'Aqara single key wired wall switch',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'QBKG03LM',
|
||||
description: 'Aqara double key wired wall switch',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'ZNCZ02LM',
|
||||
description: 'Mi power plug ZigBee',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'QBCZ11LM',
|
||||
description: 'Aqara wall socket',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'JTYJ-GD-01LM/BW',
|
||||
description: 'MiJia Honeywell smoke detector',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'KTBL01LM',
|
||||
description: 'Aqara air conditioning companion',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
{
|
||||
model: 'KTBL02LM',
|
||||
description: 'Aqara air conditioning companion 2',
|
||||
supports: '-',
|
||||
vendor: 'Xiaomi',
|
||||
},
|
||||
];
|
||||
|
||||
const parsers = require('./parsers');
|
||||
const deviceMapping = require('./devices');
|
||||
const zigbee2mqtt = require('../lib/converters/zigbee2mqtt');
|
||||
const deviceMapping = require('../lib/devices');
|
||||
|
||||
// Sanity check if all supported devices are in deviceMapping
|
||||
const supportedDevices = new Set();
|
||||
parsers.forEach((p) => supportedDevices.add(...p.devices));
|
||||
zigbee2mqtt.forEach((p) => supportedDevices.add(...p.devices));
|
||||
|
||||
// Check if in deviceMapping.
|
||||
supportedDevices.forEach((s) => {
|
||||
@@ -99,15 +114,15 @@ const logDevices = (devices) => {
|
||||
console.log('| Model | Description | Picture |')
|
||||
console.log('| ------------- | ------------- | -------------------------- |')
|
||||
devices.forEach((device) => {
|
||||
console.log(`| ${device.model} | ${device.description} (${device.supports}) | }.jpg) |`);
|
||||
console.log(`| ${device.model} | ${device.vendor} ${device.description} (${device.supports}) | }.jpg) |`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('GENERATED')
|
||||
console.log('=============================')
|
||||
console.log('*NOTE: Automatically generated by `npm run docs`*')
|
||||
console.log('*NOTE: Automatically generated by `npm run docgen`*')
|
||||
console.log('')
|
||||
console.log('In case your device is **NOT** listed here, please create an issue at: https://github.com/Koenkk/xiaomi-zb2mqtt/issues');
|
||||
console.log('In case your device is **NOT** listed here, please create an issue at: https://github.com/Koenkk/zigbee2mqtt/issues');
|
||||
console.log('')
|
||||
logDevices(Object.values(deviceMapping));
|
||||
console.log('');
|
||||
Reference in New Issue
Block a user