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');
|
2018-04-24 10:30:56 -07:00
|
|
|
const homeassistant = require('./homeassistant');
|
2018-04-18 09:25:40 -07:00
|
|
|
|
|
|
|
class Controller {
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.zigbee = new Zigbee();
|
|
|
|
this.mqtt = new MQTT();
|
|
|
|
this.stateCache = {};
|
2018-04-20 10:53:40 -07:00
|
|
|
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 {
|
2018-04-23 09:17:47 -07:00
|
|
|
this.mqtt.connect(this.handleMQTTMessage, () => {
|
2018-04-24 10:30:56 -07:00
|
|
|
this.handleStarted();
|
2018-04-23 09:17:47 -07:00
|
|
|
});
|
2018-04-18 09:25:40 -07:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-04-24 10:30:56 -07:00
|
|
|
handleStarted() {
|
2018-04-25 10:41:20 -07:00
|
|
|
// Home Assistant MQTT discovery on startup.
|
2018-04-24 10:30:56 -07:00
|
|
|
if (settings.get().homeassistant) {
|
|
|
|
// MQTT discovery of all paired devices on startup.
|
|
|
|
const devices = this.zigbee.getAllClients();
|
|
|
|
devices.forEach((device) => {
|
|
|
|
const mappedModel = deviceMapping[device.modelId];
|
|
|
|
|
|
|
|
if (mappedModel && mappedModel.homeassistant && !this.hassDiscoveryCache[device.ieeeAddr]) {
|
|
|
|
this.homeassistantDiscover(
|
|
|
|
mappedModel.homeassistant,
|
|
|
|
device.ieeeAddr,
|
|
|
|
settings.getDevice(device.ieeeAddr).friendly_name
|
|
|
|
);
|
|
|
|
|
|
|
|
this.hassDiscoveryCache[device.ieeeAddr] = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// MQTT discovery of zigbee2mqtt permit join switch.
|
|
|
|
this.homeassistantDiscover([homeassistant.zigbee2mqtt_permit_join], 'zigbee2mqtt_permit_join', 'zigbee2mqtt_permit_join');
|
|
|
|
this.mqtt.publish('permit_join', JSON.stringify({state: settings.get().permit_join ? '"ON"' : '"OFF"'}), true);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Enable zigbee join.
|
|
|
|
if (settings.get().permit_join) {
|
|
|
|
logger.warn('`permit_join` set to `true` in configuration.yaml.')
|
|
|
|
logger.warn('Allowing new devices to join.');
|
|
|
|
logger.warn('Set `permit_join` to `false` once you joined all devices.');
|
|
|
|
this.zigbee.permitJoin(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-18 09:25:40 -07:00
|
|
|
stop(callback) {
|
|
|
|
this.mqtt.disconnect();
|
|
|
|
this.zigbee.stop(callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleZigbeeMessage(message) {
|
2018-04-21 03:45:22 -07:00
|
|
|
if (message.type == 'devInterview') {
|
|
|
|
logger.info('Connecting with device, please wait...');
|
|
|
|
}
|
|
|
|
if (message.type == 'devIncoming') {
|
|
|
|
logger.info('New device joined the network!');
|
|
|
|
}
|
|
|
|
|
2018-04-18 09:25:40 -07:00
|
|
|
if (!message.endpoints) {
|
|
|
|
// We dont handle messages without endpoints.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const device = message.endpoints[0].device;
|
|
|
|
|
2018-04-23 12:44:06 -07:00
|
|
|
if (!device) {
|
|
|
|
logger.warn('Message without device!');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-18 09:25:40 -07:00
|
|
|
// Check if this is a new device.
|
2018-04-23 09:17:47 -07:00
|
|
|
if (!settings.getDevice(device.ieeeAddr)) {
|
2018-04-18 09:25:40 -07:00
|
|
|
logger.info(`New device with address ${device.ieeeAddr} connected!`);
|
2018-04-25 10:29:03 -07:00
|
|
|
settings.addDevice(device.ieeeAddr);
|
2018-04-18 09:25:40 -07:00
|
|
|
}
|
|
|
|
|
2018-04-20 14:39:20 -07:00
|
|
|
// We can't handle devices without modelId.
|
|
|
|
if (!device.modelId) {
|
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];
|
2018-04-23 09:17:47 -07:00
|
|
|
const friendlyName = settings.getDevice(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;
|
|
|
|
}
|
|
|
|
|
2018-04-25 10:41:20 -07:00
|
|
|
// Home Assistant MQTT discovery
|
2018-04-24 09:04:36 -07:00
|
|
|
if (settings.get().homeassistant && mappedModel.homeassistant &&
|
2018-04-20 10:53:40 -07:00
|
|
|
!this.hassDiscoveryCache[device.ieeeAddr]) {
|
2018-04-24 10:30:56 -07:00
|
|
|
this.homeassistantDiscover(mappedModel.homeassistant, device.ieeeAddr, friendlyName);
|
2018-04-20 10:53:40 -07:00
|
|
|
this.hassDiscoveryCache[device.ieeeAddr] = true;
|
|
|
|
}
|
|
|
|
|
2018-04-20 14:39:20 -07:00
|
|
|
// After this point we cant handle message withoud cid anymore.
|
|
|
|
if (!message.data.cid) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
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.
|
2018-04-23 09:17:47 -07:00
|
|
|
const retain = settings.getDevice(device.ieeeAddr).retain;
|
2018-04-18 09:25:40 -07:00
|
|
|
|
|
|
|
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];
|
|
|
|
|
|
|
|
// Convert the MQTT message to a Zigbee message.
|
2018-04-24 10:03:09 -07:00
|
|
|
let json = null;
|
|
|
|
try {
|
|
|
|
json = JSON.parse(message);
|
|
|
|
} catch (e) {
|
|
|
|
// Cannot be parsed to JSON, assume state message.
|
|
|
|
json = {state: message.toString()};
|
|
|
|
}
|
|
|
|
|
2018-04-24 10:30:56 -07:00
|
|
|
// Check if permit_join
|
|
|
|
if (friendlyName === 'zigbee2mqtt_permit_join') {
|
|
|
|
this.zigbee.permitJoin(json.state === 'ON' ? true : false);
|
|
|
|
this.mqtt.publish(friendlyName, JSON.stringify({state: json.state}), true);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Map friendlyName to deviceID.
|
|
|
|
const deviceID = Object.keys(settings.get().devices).find((id) => settings.getDevice(id).friendly_name === friendlyName);
|
|
|
|
if (!deviceID) {
|
|
|
|
logger.error(`Cannot handle '${topic}' because deviceID of '${friendlyName}' cannot be found`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-18 09:25:40 -07:00
|
|
|
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]}),
|
2018-04-23 09:17:47 -07:00
|
|
|
settings.getDevice(deviceID).retain,
|
2018-04-18 13:07:18 -07:00
|
|
|
);
|
2018-04-18 09:25:40 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
this.zigbee.publish(deviceID, message.cId, message.cmd, message.zclData, callback);
|
|
|
|
});
|
|
|
|
}
|
2018-04-21 00:13:14 -07:00
|
|
|
|
2018-04-24 10:30:56 -07:00
|
|
|
homeassistantDiscover(configurations, deviceID, friendlyName) {
|
|
|
|
configurations.forEach((config) => {
|
|
|
|
const topic = `${config.type}/${deviceID}/${config.object_id}/config`;
|
|
|
|
const payload = config.discovery_payload;
|
2018-04-21 00:13:14 -07:00
|
|
|
payload.state_topic = `${settings.get().mqtt.base_topic}/${friendlyName}`;
|
|
|
|
payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`;
|
2018-04-21 03:15:00 -07:00
|
|
|
|
|
|
|
// Set unique names in cases this device produces multiple entities in homeassistant.
|
2018-04-24 10:30:56 -07:00
|
|
|
if (configurations.length > 1) {
|
|
|
|
payload.name = `${friendlyName}_${config.object_id}`;
|
2018-04-21 03:15:00 -07:00
|
|
|
} else {
|
|
|
|
payload.name = friendlyName;
|
|
|
|
}
|
2018-04-21 00:13:14 -07:00
|
|
|
|
|
|
|
if (payload.command_topic) {
|
|
|
|
payload.command_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/set`;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.mqtt.publish(topic, JSON.stringify(payload), true, null, 'homeassistant');
|
|
|
|
});
|
|
|
|
}
|
2018-04-18 09:25:40 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Controller;
|