zigbee2mqtt/lib/controller.js

159 lines
5.9 KiB
JavaScript
Raw Normal View History

2018-04-18 09:25:40 -07:00
const MQTT = require('./mqtt');
const Zigbee = require('./zigbee');
const logger = require('./util/logger');
2018-04-18 10:09:59 -07:00
const settings = require('./util/settings');
2018-04-18 09:25:40 -07:00
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.hassDiscoveryCache = {};
2018-04-18 09:25:40 -07:00
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();
}
2018-04-19 14:15:09 -07:00
// We can't handle devices without modelId or cid.
if (!device.modelId || !message.data.cid) {
2018-04-18 09:25:40 -07:00
return;
}
// Map Zigbee modelID to vendor modelID.
2018-04-18 10:09:59 -07:00
const modelID = message.endpoints[0].device.modelId;
2018-04-18 09:25:40 -07:00
const mappedModel = deviceMapping[modelID];
const friendlyName = settings.get().devices[device.ieeeAddr].friendly_name;
2018-04-18 09:25:40 -07:00
if (!mappedModel) {
2018-04-18 11:53:22 -07:00
logger.warn(`Device with modelID '${modelID}' is not supported.`);
logger.warn('Please create an issue on https://github.com/Koenkk/zigbee2mqtt/issues to add support for your device');
2018-04-18 09:25:40 -07:00
return;
}
// Home assistant MQTT discovery
if (settings.get().homeassistant_discovery && mappedModel.homeassistant &&
!this.hassDiscoveryCache[device.ieeeAddr]) {
const topic = `${mappedModel.homeassistant.type}/${device.ieeeAddr}/config`;
const payload = mappedModel.homeassistant.discovery_payload;
payload.state_topic = `${settings.get().mqtt.base_topic}/${friendlyName}`;
payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`;
payload.name = friendlyName;
this.mqtt.publish(topic, JSON.stringify(payload), true, null, 'homeassistant');
this.hassDiscoveryCache[device.ieeeAddr] = true;
}
2018-04-18 09:25:40 -07:00
// Find a conveter for this message.
2018-04-18 10:09:59 -07:00
const cid = message.data.cid;
const converters = zigbee2mqtt.filter((c) => c.devices.includes(mappedModel.model) && c.cid === cid && c.type === message.type);
2018-04-18 09:25:40 -07:00
if (!converters.length) {
2018-04-18 11:53:22 -07:00
logger.warn(`No converter available for '${mappedModel.model}' with cid '${cid}' and type '${message.type}'`);
logger.warn('Please create an issue on https://github.com/Koenkk/zigbee2mqtt/issues with this message.');
2018-04-18 09:25:40 -07:00
return;
}
// Convert this Zigbee message to a MQTT message.
const retain = settings.get().devices[device.ieeeAddr].retain;
const publish = (payload) => {
if (this.stateCache[device.ieeeAddr]) {
payload = {...this.stateCache[device.ieeeAddr], ...payload};
}
2018-04-18 11:53:22 -07:00
this.mqtt.publish(friendlyName, JSON.stringify(payload), retain);
2018-04-18 09:25:40 -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.
converters.forEach((converter) => {
2018-04-18 10:09:59 -07:00
const payload = converter.convert(message, publish);
2018-04-18 09:25:40 -07:00
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`);
2018-04-18 12:55:00 -07:00
return;
2018-04-18 09:25:40 -07:00
}
// 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]);
2018-04-18 10:09:59 -07:00
const callback = (error) => {
2018-04-18 09:25:40 -07:00
// Devices do not report when they go off, this ensures state (on/off) is always in sync.
if (!error && key === 'state') {
2018-04-18 13:07:18 -07:00
this.mqtt.publish(
friendlyName,
JSON.stringify({state: json[key]}),
settings.get().devices[deviceID].retain,
);
2018-04-18 09:25:40 -07:00
}
};
this.zigbee.publish(deviceID, message.cId, message.cmd, message.zclData, callback);
});
}
}
module.exports = Controller;