zigbee2mqtt/lib/controller.js

320 lines
12 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');
const homeassistant = require('./homeassistant');
2018-05-21 02:49:02 -07:00
const debug = require('debug')('zigbee2mqtt:controller');
2018-04-18 09:25:40 -07:00
const mqttConfigRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/\\w+`, 'g');
const mqttDeviceRegex = new RegExp(`${settings.get().mqtt.base_topic}/\\w+/set`, 'g');
2018-04-29 05:09:49 -07:00
const mqttDevicePrefixRegex = new RegExp(`${settings.get().mqtt.base_topic}/\\w+/\\w+/set`, 'g');
2018-05-17 08:20:46 -07:00
const issueLink = 'https://github.com/Koenkk/zigbee2mqtt/issues';
2018-05-21 02:49:02 -07:00
const pollInterval = 60 * 1000; // seconds * 1000.
const softResetTimeout = 3600 * 1000; // seconds * 1000.
2018-05-17 08:20:46 -07:00
class Controller {
2018-04-18 09:25:40 -07:00
constructor() {
this.zigbee = new Zigbee();
this.mqtt = new MQTT();
2018-05-21 02:51:53 -07:00
this.stateCache = {};
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 {
// Log zigbee clients on startup.
const devices = this.zigbee.getAllClients();
logger.info(`Currently ${devices.length} devices are joined:`);
2018-05-21 02:49:02 -07:00
devices.forEach((device) => logger.info(this.getDeviceStartupLogMessage(device)));
// Connect to MQTT broker
2018-04-29 05:09:49 -07:00
const subscriptions = [
`${settings.get().mqtt.base_topic}/+/set`,
`${settings.get().mqtt.base_topic}/+/+/set`,
2018-05-17 08:20:46 -07:00
`${settings.get().mqtt.base_topic}/bridge/config/+`,
2018-04-29 05:09:49 -07:00
];
this.mqtt.connect(this.handleMQTTMessage, subscriptions, () => this.handleStarted());
2018-04-18 09:25:40 -07:00
}
});
}
handleStarted() {
2018-04-25 10:41:20 -07:00
// Home Assistant MQTT discovery on startup.
if (settings.get().homeassistant) {
// MQTT discovery of all paired devices on startup.
this.zigbee.getAllClients().forEach((device) => {
if (deviceMapping[device.modelId]) {
homeassistant.discover(device.ieeeAddr, deviceMapping[device.modelId].model, this.mqtt);
}
});
}
// Enable zigbee join.
if (settings.get().permit_join) {
2018-05-17 08:20:46 -07:00
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-05-21 03:00:35 -07:00
// Start timers.
2018-05-21 02:49:02 -07:00
this.pollTimer(true);
2018-05-21 02:59:01 -07:00
this.softResetTimeout(true);
2018-05-21 02:49:02 -07:00
}
2018-05-21 02:59:01 -07:00
softResetTimeout(start) {
2018-05-21 02:49:02 -07:00
if (this._softResetTimer) {
clearTimeout(this._softResetTimer);
this._softResetTimer = null;
}
2018-05-21 02:59:01 -07:00
if (start) {
this._softResetTimer = setTimeout(() => {
this.zigbee.softReset((error) => {
if (error) {
logger.warn('Soft reset error', error);
this.zigbee.stop((error) => {
logger.warn('Zigbee stopped');
this.zigbee.start(this.handleZigbeeMessage, (error) => {
if (error) {
logger.error('Failed to restart!');
}
});
2018-05-21 02:49:02 -07:00
});
2018-05-21 02:59:01 -07:00
} else {
logger.warn('Soft resetted zigbee');
}
2018-05-21 02:49:02 -07:00
2018-05-21 02:59:01 -07:00
this.softResetTimeout(true);
});
}, softResetTimeout);
}
2018-05-21 02:49:02 -07:00
}
pollTimer(start) {
// Some routers need polling to prevent them from sleeping.
if (start && !this._pollTimer) {
this._pollTimer = setInterval(() => {
const devices = this.zigbee.getAllClients().filter((d) => {
const power = d.powerSource ? d.powerSource.toLowerCase().split(' ')[0] : 'unknown';
return power !== 'battery' && power !== 'unknown' && d.type === 'Router';
});
devices.forEach((d) => this.zigbee.ping(d.ieeeAddr));
}, pollInterval);
} else if (!start && this._pollTimer) {
clearTimeout(this._pollTimer);
this._pollTimer = null;
}
}
2018-04-18 09:25:40 -07:00
stop(callback) {
this.mqtt.disconnect();
2018-05-21 02:49:02 -07:00
this.pollTimer(false);
2018-05-21 02:59:01 -07:00
this.softResetTimeout(false);
2018-04-18 09:25:40 -07:00
this.zigbee.stop(callback);
}
getDeviceStartupLogMessage(device) {
let friendlyName = 'unknown';
let friendlyDevice = {model: 'unkown', description: 'unknown'};
if (deviceMapping[device.modelId]) {
friendlyDevice = deviceMapping[device.modelId];
}
if (settings.getDevice(device.ieeeAddr)) {
2018-05-17 08:20:46 -07:00
friendlyName = settings.getDevice(device.ieeeAddr).friendly_name;
}
2018-05-17 08:20:46 -07:00
return `${friendlyName} (${device.ieeeAddr}): ${friendlyDevice.model} - ` +
`${friendlyDevice.vendor} ${friendlyDevice.description}`;
}
2018-04-18 09:25:40 -07:00
handleZigbeeMessage(message) {
2018-05-21 02:49:02 -07:00
// Zigbee message receieved, reset soft reset timeout.
2018-05-21 02:59:01 -07:00
this.softResetTimeout(true);
2018-05-21 02:49:02 -07:00
debug('Recieved zigbee message with data', message.data);
2018-04-21 03:45:22 -07:00
if (message.type == 'devInterview') {
logger.info('Connecting with device, please wait...');
} else if (message.type == 'devIncoming') {
2018-04-21 03:45:22 -07:00
logger.info('New device joined the network!');
}
// We dont handle messages without endpoints.
2018-04-18 09:25:40 -07:00
if (!message.endpoints) {
return;
}
const device = message.endpoints[0].device;
if (!device) {
logger.warn('Message without device!');
return;
}
2018-04-18 09:25:40 -07:00
// Check if this is a new device.
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];
if (!mappedModel) {
2018-04-18 11:53:22 -07:00
logger.warn(`Device with modelID '${modelID}' is not supported.`);
2018-05-17 08:20:46 -07:00
logger.warn(`Please create an issue on ${issueLink} 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
if (settings.get().homeassistant) {
homeassistant.discover(device.ieeeAddr, mappedModel.model, this.mqtt);
}
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;
2018-05-17 08:20:46 -07:00
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-05-17 08:20:46 -07:00
logger.warn(
`No converter available for '${mappedModel.model}' with cid '${cid}' and type '${message.type}'`
);
logger.warn(`Please create an issue on ${issueLink} with this message.`);
2018-04-18 09:25:40 -07:00
return;
}
// Convert this Zigbee message to a MQTT message.
// 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-05-17 08:20:46 -07:00
const publish = (payload) => this.mqttPublishDeviceState(
device.ieeeAddr, payload, converter.disableCache !== true
);
const payload = converter.convert(message, publish, settings.getDevice(device.ieeeAddr));
2018-04-18 09:25:40 -07:00
if (payload) {
publish(payload);
2018-04-18 09:25:40 -07:00
}
});
}
handleMQTTMessage(topic, message) {
if (topic.match(mqttConfigRegex)) {
this.handleMQTTMessageConfig(topic, message);
2018-04-29 05:09:49 -07:00
} else if (topic.match(mqttDeviceRegex) || topic.match(mqttDevicePrefixRegex)) {
this.handleMQTTMessageDevice(topic, message, topic.match(mqttDevicePrefixRegex));
} else {
logger.warn(`Cannot handle MQTT message with topic '${topic}' and message '${message}'`);
2018-04-24 10:03:09 -07:00
}
}
2018-04-24 10:03:09 -07:00
handleMQTTMessageConfig(topic, message) {
const option = topic.split('/')[3];
if (option === 'permit_join') {
this.zigbee.permitJoin(message.toString().toLowerCase() === 'true');
} else {
logger.warn(`Cannot handle MQTT config option '${option}' with message '${message}'`);
}
}
2018-04-29 05:09:49 -07:00
handleMQTTMessageDevice(topic, message, withPrefix) {
const friendlyName = topic.split('/')[1];
2018-04-29 05:09:49 -07:00
const topicPrefix = withPrefix ? topic.split('/')[2] : '';
// Map friendlyName to deviceID.
2018-05-17 08:20:46 -07:00
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;
}
// Convert the MQTT message to a Zigbee message.
let json = null;
try {
json = JSON.parse(message);
} catch (e) {
// Cannot be parsed to JSON, assume state message.
json = {state: message.toString()};
}
2018-04-27 14:58:46 -07:00
// Find ep for this device
const mappedModel = deviceMapping[this.zigbee.getDevice(deviceID).modelId];
2018-04-29 05:09:49 -07:00
const ep = mappedModel.ep && mappedModel.ep[topicPrefix] ? mappedModel.ep[topicPrefix] : null;
2018-04-27 14:58:46 -07:00
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.
2018-04-29 05:09:49 -07:00
if (!error && key.startsWith('state')) {
const msg = {};
msg[key] = json[key];
this.mqttPublishDeviceState(deviceID, msg, true);
2018-04-18 09:25:40 -07:00
}
};
2018-05-16 10:22:47 -07:00
this.zigbee.publish(deviceID, message.cid, message.cmd, message.zclData, ep, callback);
2018-04-18 09:25:40 -07:00
});
}
mqttPublishDeviceState(deviceID, payload, cache) {
// Add cached state to payload
if (this.stateCache[deviceID]) {
payload = {...this.stateCache[deviceID], ...payload};
}
// Update state cache with new state.
if (cache) {
this.stateCache[deviceID] = payload;
}
const deviceSettings = settings.getDevice(deviceID);
const options = {
retain: deviceSettings.retain,
qos: deviceSettings.qos ? deviceSettings.qos : 0,
};
this.mqtt.publish(deviceSettings.friendly_name, JSON.stringify(payload), options);
}
2018-04-18 09:25:40 -07:00
}
2018-05-17 08:20:46 -07:00
module.exports = Controller;