diff --git a/lib/controller.js b/lib/controller.js index 615d7682a..8fb9ce77e 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -7,14 +7,15 @@ const zigbee2mqtt = require('./converters/zigbee2mqtt'); const mqtt2zigbee = require('./converters/mqtt2zigbee'); const homeassistant = require('./homeassistant'); +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'); + class Controller { constructor() { this.zigbee = new Zigbee(); this.mqtt = new MQTT(); this.stateCache = {}; - this.hassDiscoveryCache = {}; - this.handleZigbeeMessage = this.handleZigbeeMessage.bind(this); this.handleMQTTMessage = this.handleMQTTMessage.bind(this); } @@ -24,9 +25,8 @@ class Controller { if (error) { logger.error('Failed to start'); } else { - this.mqtt.connect(this.handleMQTTMessage, () => { - this.handleStarted(); - }); + const subscriptions = [`${settings.get().mqtt.base_topic}/+/set`, `${settings.get().mqtt.base_topic}/bridge/config/+`]; + this.mqtt.connect(this.handleMQTTMessage, subscriptions, () => this.handleStarted()); } }); } @@ -35,24 +35,9 @@ class Controller { // Home Assistant MQTT discovery on startup. 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; - } + this.zigbee.getAllClients().forEach((device) => { + homeassistant.discover(device.ieeeAddr, deviceMapping[device.modelId].homeassistant, this.mqtt); }); - - // 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. @@ -72,13 +57,12 @@ class Controller { handleZigbeeMessage(message) { if (message.type == 'devInterview') { logger.info('Connecting with device, please wait...'); - } - if (message.type == 'devIncoming') { + } else if (message.type == 'devIncoming') { logger.info('New device joined the network!'); } + // We dont handle messages without endpoints. if (!message.endpoints) { - // We dont handle messages without endpoints. return; } @@ -103,7 +87,6 @@ class Controller { // Map Zigbee modelID to vendor modelID. const modelID = message.endpoints[0].device.modelId; const mappedModel = deviceMapping[modelID]; - const friendlyName = settings.getDevice(device.ieeeAddr).friendly_name; if (!mappedModel) { logger.warn(`Device with modelID '${modelID}' is not supported.`); @@ -112,10 +95,8 @@ class Controller { } // Home Assistant MQTT discovery - if (settings.get().homeassistant && mappedModel.homeassistant && - !this.hassDiscoveryCache[device.ieeeAddr]) { - this.homeassistantDiscover(mappedModel.homeassistant, device.ieeeAddr, friendlyName); - this.hassDiscoveryCache[device.ieeeAddr] = true; + if (settings.get().homeassistant) { + homeassistant.discover(device.ieeeAddr, mappedModel.homeassistant, this.mqtt); } // After this point we cant handle message withoud cid anymore. @@ -134,21 +115,12 @@ class Controller { } // Convert this Zigbee message to a MQTT message. - const retain = settings.getDevice(device.ieeeAddr).retain; - - const publish = (payload) => { - if (this.stateCache[device.ieeeAddr]) { - payload = {...this.stateCache[device.ieeeAddr], ...payload}; - } - - this.mqtt.publish(friendlyName, 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 publish = (payload) => this.mqttPublishDeviceState(device.ieeeAddr, payload); const payload = converter.convert(message, publish); if (payload) { @@ -162,8 +134,35 @@ class Controller { } handleMQTTMessage(topic, message) { + if (topic.match(mqttConfigRegex)) { + this.handleMQTTMessageConfig(topic, message); + } else if (topic.match(mqttDeviceRegex)) { + this.handleMQTTMessageDevice(topic, message); + } else { + logger.warn(`Cannot handle MQTT message with topic '${topic}' and message '${message}'`); + } + } + + 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}'`); + } + } + + handleMQTTMessageDevice(topic, message) { const friendlyName = topic.split('/')[1]; + // 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; + } + // Convert the MQTT message to a Zigbee message. let json = null; try { @@ -173,20 +172,6 @@ class Controller { json = {state: message.toString()}; } - // 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; - } - Object.keys(json).forEach((key) => { // Find converter for this key. const converter = mqtt2zigbee[key]; @@ -199,11 +184,7 @@ class Controller { const callback = (error) => { // 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]}), - settings.getDevice(deviceID).retain, - ); + this.mqttPublishDeviceState(deviceID, {state: json[key]}) } }; @@ -211,26 +192,16 @@ class Controller { }); } - homeassistantDiscover(configurations, deviceID, friendlyName) { - configurations.forEach((config) => { - const topic = `${config.type}/${deviceID}/${config.object_id}/config`; - const payload = config.discovery_payload; - payload.state_topic = `${settings.get().mqtt.base_topic}/${friendlyName}`; - payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`; + mqttPublishDeviceState(deviceID, payload) { + if (this.stateCache[deviceID]) { + payload = {...this.stateCache[deviceID], ...payload}; + } - // Set unique names in cases this device produces multiple entities in homeassistant. - if (configurations.length > 1) { - payload.name = `${friendlyName}_${config.object_id}`; - } else { - payload.name = friendlyName; - } + // Update state cache with new state. + this.stateCache[deviceID] = payload; - if (payload.command_topic) { - payload.command_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/set`; - } - - this.mqtt.publish(topic, JSON.stringify(payload), true, null, 'homeassistant'); - }); + const deviceSettings = settings.getDevice(deviceID); + this.mqtt.publish(deviceSettings.friendly_name, JSON.stringify(payload), deviceSettings.retain); } } diff --git a/lib/converters/zigbee2mqtt.js b/lib/converters/zigbee2mqtt.js index 6c672d488..13305d9af 100644 --- a/lib/converters/zigbee2mqtt.js +++ b/lib/converters/zigbee2mqtt.js @@ -141,7 +141,7 @@ const parsers = [ devices: ['WXKG11LM'], cid: 'genOnOff', type: 'attReport', - convert: (msg, publish) => { + convert: (msg) => { const data = msg.data.data; let clicks; @@ -178,7 +178,7 @@ const parsers = [ devices: ['WXKG03LM'], cid: 'genOnOff', type: 'attReport', - convert: (msg) => {return {click: 'single'}} + convert: () => {return {click: 'single'}} }, { devices: ['SJCGQ11LM'], @@ -246,7 +246,7 @@ const parsers = [ cid: 'msPressureMeasurement', type: 'devChange', convert: () => null - }, + }, { devices: ['ZNCZ02LM'], cid: 'genAnalogInput', diff --git a/lib/devices.js b/lib/devices.js index c3cb63e32..0a1ea282f 100644 --- a/lib/devices.js +++ b/lib/devices.js @@ -1,4 +1,4 @@ -const homeassistant = require('./homeassistant'); +const homeassistant = require('./homeassistant').configurations; const devices = { // Xiaomi diff --git a/lib/homeassistant.js b/lib/homeassistant.js index ac02d6669..350f5133a 100644 --- a/lib/homeassistant.js +++ b/lib/homeassistant.js @@ -1,4 +1,6 @@ -const homeassistant = { +const settings = require('./util/settings'); + +const configurations = { // Binary sensor 'binary_sensor_occupancy': { type: 'binary_sensor', @@ -104,7 +106,7 @@ const homeassistant = { xy: true, platform: 'mqtt_json', command_topic: true - } + } }, 'light_brightness_colortemp': { type: 'light', @@ -114,7 +116,7 @@ const homeassistant = { color_temp: true, platform: 'mqtt_json', command_topic: true - } + } }, // Switch @@ -126,21 +128,45 @@ const homeassistant = { payload_on: 'ON', value_template: '{{ value_json.state }}', command_topic: true - } - }, - - // zigbee2mqtt - 'zigbee2mqtt_permit_join': { - type: 'switch', - object_id: 'switch', - discovery_payload: { - payload_off: 'OFF', - payload_on: 'ON', - value_template: '{{ value_json.state }}', - command_topic: true, - icon: 'mdi:router-wireless', - } + } }, }; -module.exports = homeassistant; +// A map of all discoverd devices +const discovered = {}; + +function discover(deviceID, configs, mqtt) { + // Check if already discoverd and check if there are configs. + if (discovered[deviceID] || !configs) { + return; + } + + const friendlyName = settings.getDevice(deviceID).friendly_name; + + configs.forEach((config) => { + const topic = `${config.type}/${deviceID}/${config.object_id}/config`; + const payload = config.discovery_payload; + payload.state_topic = `${settings.get().mqtt.base_topic}/${friendlyName}`; + payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`; + + // Set unique names in cases this device produces multiple entities in homeassistant. + if (configs.length > 1) { + payload.name = `${friendlyName}_${config.object_id}`; + } else { + payload.name = friendlyName; + } + + if (payload.command_topic) { + payload.command_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/set`; + } + + mqtt.publish(topic, JSON.stringify(payload), true, null, 'homeassistant'); + }); + + discovered[deviceID] = true; +} + +module.exports = { + configurations: configurations, + discover: (deviceID, configs, mqtt) => discover(deviceID, configs, mqtt), +}; diff --git a/lib/mqtt.js b/lib/mqtt.js index c89ccfdb5..2feafd292 100644 --- a/lib/mqtt.js +++ b/lib/mqtt.js @@ -2,7 +2,6 @@ const mqtt = require('mqtt'); const logger = require('./util/logger'); const settings = require('./util/settings'); - class MQTT { constructor() { @@ -10,7 +9,7 @@ class MQTT { this.handleMessage = this.handleMessage.bind(this); } - connect(onMessage, callback) { + connect(onMessage, subscriptions, callback) { const mqttSettings = settings.get().mqtt; logger.info(`Connecting to MQTT server at ${mqttSettings.server}`); @@ -27,7 +26,7 @@ class MQTT { this.handleConnect(); callback(); }); - + this.client.on('message', this.handleMessage); // Set timer at interval to check if connected to MQTT server. @@ -39,6 +38,7 @@ class MQTT { }, interval); this.onMessage = onMessage; + this.subscriptions = subscriptions; } disconnect() { @@ -54,7 +54,7 @@ class MQTT { handleConnect() { logger.info('Connected to MQTT server'); this.publish('bridge/state', 'online', true); - this.client.subscribe(`${settings.get().mqtt.base_topic}/+/set`); + this.subscriptions.forEach((topic) => this.client.subscribe(topic)); } handleMessage(topic, message) { diff --git a/lib/util/settings.js b/lib/util/settings.js index 7d195b252..2934a425c 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -13,7 +13,7 @@ function read() { function addDevice(id) { if (!yawn.json.devices) { - const first = "\n" + + const first = "\n" + "# List of devices, automatically updates when new devices join, E.G.:\n" + "# '0x00158d0001d8e1d2':\n" + "# friendly_name: bedroom_switch # Friendly name to be used in MQTT topic\n" +