2018-10-23 11:39:48 -07:00
|
|
|
|
|
|
|
const settings = require('../util/settings');
|
|
|
|
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
|
|
|
const Queue = require('queue');
|
|
|
|
const logger = require('../util/logger');
|
2018-11-16 12:23:11 -07:00
|
|
|
|
2018-11-07 11:40:58 -07:00
|
|
|
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/.+/(set|get)$`);
|
|
|
|
const postfixes = ['left', 'right', 'center', 'bottom_left', 'bottom_right', 'top_left', 'top_right'];
|
2018-11-16 12:23:11 -07:00
|
|
|
const maxDepth = 20;
|
2018-10-23 11:39:48 -07:00
|
|
|
|
2018-11-05 13:55:30 -07:00
|
|
|
class DevicePublish {
|
2018-11-16 12:23:11 -07:00
|
|
|
constructor(zigbee, mqtt, state, publishDeviceState) {
|
2018-10-23 11:39:48 -07:00
|
|
|
this.zigbee = zigbee;
|
|
|
|
this.mqtt = mqtt;
|
|
|
|
this.state = state;
|
2018-11-16 12:23:11 -07:00
|
|
|
this.publishDeviceState = publishDeviceState;
|
2018-10-23 11:39:48 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Setup command queue.
|
|
|
|
* The command queue ensures that only 1 command is executed at a time.
|
|
|
|
* When executing multiple commands at the same time, some commands may fail.
|
|
|
|
*/
|
|
|
|
this.queue = new Queue();
|
|
|
|
this.queue.concurrency = 1;
|
|
|
|
this.queue.autostart = true;
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
2018-10-23 11:39:48 -07:00
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
onMQTTConnected() {
|
2018-10-23 11:39:48 -07:00
|
|
|
// Subscribe to topics.
|
2018-11-07 11:40:58 -07:00
|
|
|
for (let step = 1; step < maxDepth; step++) {
|
|
|
|
const topic = `${settings.get().mqtt.base_topic}/${'+/'.repeat(step)}`;
|
|
|
|
this.mqtt.subscribe(`${topic}set`);
|
|
|
|
this.mqtt.subscribe(`${topic}get`);
|
|
|
|
}
|
2018-10-23 11:39:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
this.queue.stop();
|
|
|
|
}
|
|
|
|
|
2018-11-07 11:40:58 -07:00
|
|
|
parseTopic(topic) {
|
|
|
|
if (!topic.match(topicRegex)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove base from topic
|
|
|
|
topic = topic.replace(`${settings.get().mqtt.base_topic}/`, '');
|
|
|
|
|
|
|
|
// Parse type from topic
|
|
|
|
const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
|
|
|
|
|
|
|
|
// Remove type from topic
|
|
|
|
topic = topic.replace(`/${type}`, '');
|
|
|
|
|
|
|
|
// Check if we have to deal with a postfix.
|
2018-11-19 12:29:35 -07:00
|
|
|
let postfix = '';
|
2018-11-07 11:40:58 -07:00
|
|
|
if (postfixes.find((p) => topic.endsWith(p))) {
|
|
|
|
postfix = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
|
|
|
|
|
|
|
|
// Remove postfix from topic
|
|
|
|
topic = topic.replace(`/${postfix}`, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
const deviceID = topic;
|
|
|
|
|
|
|
|
return {type: type, deviceID: deviceID, postfix: postfix};
|
|
|
|
}
|
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
onMQTTMessage(topic, message) {
|
2018-11-07 11:40:58 -07:00
|
|
|
topic = this.parseTopic(topic);
|
|
|
|
|
|
|
|
if (!topic) {
|
2018-10-23 11:39:48 -07:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-10-30 13:23:34 -07:00
|
|
|
// Map friendlyName to ieeeAddr if possible.
|
2018-11-16 12:23:11 -07:00
|
|
|
const ieeeAddr = settings.getIeeeAddrByFriendlyName(topic.deviceID) || topic.deviceID;
|
2018-10-23 11:39:48 -07:00
|
|
|
|
|
|
|
// Get device
|
|
|
|
const device = this.zigbee.getDevice(ieeeAddr);
|
|
|
|
if (!device) {
|
|
|
|
logger.error(`Failed to find device with ieeAddr: '${ieeeAddr}'`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Map device to a model
|
|
|
|
const model = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
|
|
|
if (!model) {
|
|
|
|
logger.warn(`Device with modelID '${device.modelId}' is not supported.`);
|
|
|
|
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices`);
|
|
|
|
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()};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine endpoint to publish to.
|
2018-11-27 13:50:55 -07:00
|
|
|
let endpoint = null;
|
|
|
|
if (model.hasOwnProperty('ep')) {
|
|
|
|
const eps = model.ep(device);
|
|
|
|
endpoint = eps.hasOwnProperty(topic.postfix) ? eps[topic.postfix] : null;
|
|
|
|
}
|
2018-10-23 11:39:48 -07:00
|
|
|
|
2018-11-29 12:35:06 -07:00
|
|
|
// When brightness is present skip state; brightness also handles state.
|
|
|
|
if (json.hasOwnProperty('brightness') && json.hasOwnProperty('state')) {
|
|
|
|
logger.debug(`Skipping 'state' because of 'brightness'`);
|
|
|
|
delete json.state;
|
|
|
|
}
|
|
|
|
|
2018-10-23 11:39:48 -07:00
|
|
|
// For each key in the JSON message find the matching converter.
|
|
|
|
Object.keys(json).forEach((key) => {
|
|
|
|
const converter = model.toZigbee.find((c) => c.key === key);
|
|
|
|
if (!converter) {
|
|
|
|
logger.error(`No converter available for '${key}' (${json[key]})`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Converter didn't return a result, skip
|
2018-11-07 11:40:58 -07:00
|
|
|
const converted = converter.convert(json[key], json, topic.type);
|
2018-10-23 11:39:48 -07:00
|
|
|
if (!converted) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add job to queue
|
|
|
|
this.queue.push((queueCallback) => {
|
|
|
|
this.zigbee.publish(
|
|
|
|
ieeeAddr,
|
|
|
|
converted.cid,
|
|
|
|
converted.cmd,
|
2018-11-05 13:55:30 -07:00
|
|
|
converted.cmdType,
|
2018-10-23 11:39:48 -07:00
|
|
|
converted.zclData,
|
|
|
|
converted.cfg,
|
|
|
|
endpoint,
|
2018-11-05 13:55:30 -07:00
|
|
|
(error, rsp) => {
|
2018-10-23 11:39:48 -07:00
|
|
|
// Devices do not report when they go off, this ensures state (on/off) is always in sync.
|
2018-11-07 11:40:58 -07:00
|
|
|
if (topic.type === 'set' && !error && (key.startsWith('state') || key === 'brightness')) {
|
2018-10-23 11:39:48 -07:00
|
|
|
const msg = {};
|
2018-11-07 11:40:58 -07:00
|
|
|
const _key = topic.postfix ? `state_${topic.postfix}` : 'state';
|
2018-10-23 11:39:48 -07:00
|
|
|
msg[_key] = key === 'brightness' ? 'ON' : json['state'];
|
2018-11-16 12:23:11 -07:00
|
|
|
this.publishDeviceState(device, msg, true);
|
2018-10-23 11:39:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
queueCallback();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-05 13:55:30 -07:00
|
|
|
module.exports = DevicePublish;
|