2018-10-23 11:39:48 -07:00
|
|
|
|
|
|
|
const settings = require('../util/settings');
|
|
|
|
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
|
|
|
const logger = require('../util/logger');
|
2018-12-21 16:07:53 -07:00
|
|
|
const utils = require('../util/utils');
|
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)$`);
|
2019-02-18 09:57:57 -07:00
|
|
|
const postfixes = ['left', 'right', 'center', 'bottom_left', 'bottom_right',
|
|
|
|
'top_left', 'top_right', 'white', 'rgb', 'system'];
|
2018-11-16 12:23:11 -07:00
|
|
|
const maxDepth = 20;
|
2018-10-23 11:39:48 -07:00
|
|
|
|
2018-12-21 16:07:53 -07:00
|
|
|
const groupConverters = [
|
2019-02-14 10:13:51 -07:00
|
|
|
{
|
|
|
|
from: (converted) => {
|
2019-03-02 08:35:00 -07:00
|
|
|
if (converted.cid === 'genOnOff') {
|
|
|
|
return {state: converted.cmd.toUpperCase()};
|
|
|
|
} else if (converted.cid === 'genLevelCtrl') {
|
|
|
|
return {state: 'ON', brightness: Number(converted.zclData.level)};
|
|
|
|
}
|
2019-02-14 10:13:51 -07:00
|
|
|
},
|
2019-03-02 08:35:00 -07:00
|
|
|
to: zigbeeShepherdConverters.toZigbeeConverters.light_onoff_brightness,
|
2019-02-14 10:13:51 -07:00
|
|
|
},
|
|
|
|
{
|
|
|
|
from: (converted) => {
|
|
|
|
return {color_temp: Number(converted.zclData.colortemp)};
|
|
|
|
},
|
|
|
|
to: zigbeeShepherdConverters.toZigbeeConverters.light_colortemp,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
from: (converted) => {
|
|
|
|
if (converted.zclData.hasOwnProperty('colorx') && converted.zclData.hasOwnProperty('colory')) {
|
|
|
|
return {
|
|
|
|
color: {
|
|
|
|
x: converted.zclData.colorx / 65535,
|
|
|
|
y: converted.zclData.colory / 65535,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
},
|
|
|
|
to: zigbeeShepherdConverters.toZigbeeConverters.light_color,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
from: () => null,
|
|
|
|
to: zigbeeShepherdConverters.toZigbeeConverters.ignore_transition,
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
2018-11-05 13:55:30 -07:00
|
|
|
class DevicePublish {
|
2019-02-04 10:36:49 -07:00
|
|
|
constructor(zigbee, mqtt, state, publishEntityState) {
|
2018-10-23 11:39:48 -07:00
|
|
|
this.zigbee = zigbee;
|
|
|
|
this.mqtt = mqtt;
|
|
|
|
this.state = state;
|
2019-02-04 10:36:49 -07:00
|
|
|
this.publishEntityState = publishEntityState;
|
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
|
|
|
}
|
|
|
|
|
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
|
2018-12-27 10:43:34 -07:00
|
|
|
const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
|
2018-11-07 11:40:58 -07:00
|
|
|
|
|
|
|
// Remove type from topic
|
2018-12-27 10:43:34 -07:00
|
|
|
topic = topic.replace(`/${type}`, '');
|
2018-11-07 11:40:58 -07:00
|
|
|
|
|
|
|
// 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}`, '');
|
|
|
|
}
|
|
|
|
|
2018-12-21 16:07:53 -07:00
|
|
|
const ID = topic;
|
2018-11-07 11:40:58 -07:00
|
|
|
|
2018-12-27 10:43:34 -07:00
|
|
|
return {type: type, ID: ID, postfix: postfix};
|
2018-11-07 11:40:58 -07:00
|
|
|
}
|
|
|
|
|
2019-02-20 09:57:28 -07:00
|
|
|
handlePublishError(entity, message, error) {
|
|
|
|
const meta = {
|
|
|
|
entity,
|
|
|
|
message: message.toString(),
|
|
|
|
};
|
|
|
|
|
|
|
|
this.mqtt.log('zigbee_publish_error', error.toString(), meta);
|
|
|
|
}
|
|
|
|
|
2019-02-14 10:13:51 -07:00
|
|
|
handlePublished(entity, topic, converter, converted) {
|
|
|
|
if (entity.type === 'device' && topic.type === 'set') {
|
|
|
|
// Devices do not report when they go off, this ensures state (on/off) is always in sync.
|
2019-03-02 08:35:00 -07:00
|
|
|
// Brightness onoff converters also control the state. (do a moveToLevelWithOnOff)
|
|
|
|
const msg = {};
|
|
|
|
const _key = topic.postfix ? `state_${topic.postfix}` : 'state';
|
|
|
|
|
|
|
|
if (converted.cid === 'genOnOff') {
|
|
|
|
msg[_key] = converted.cmd.toUpperCase();
|
|
|
|
} else if (converted.cid === 'genLevelCtrl' && converted.cmd === 'moveToLevelWithOnOff') {
|
|
|
|
msg[_key] = 'ON';
|
|
|
|
msg['brightness'] = Number(converted.zclData.level);
|
|
|
|
} else if (converted.cid === 'genLevelCtrl' && converted.cmd === 'moveToLevel') {
|
|
|
|
msg['brightness'] = Number(converted.zclData.level);
|
|
|
|
}
|
2019-02-14 10:13:51 -07:00
|
|
|
|
2019-03-02 08:35:00 -07:00
|
|
|
if (Object.keys(msg).length > 0) {
|
2019-02-14 10:13:51 -07:00
|
|
|
this.publishEntityState(entity.ID, msg, true);
|
|
|
|
}
|
|
|
|
} else if (entity.type === 'group' && topic.type === 'set') {
|
|
|
|
// As a group doesn't confirm it's state, we mock the state here.
|
|
|
|
const payload = groupConverters.find((g) => g.to === converter).from(converted);
|
|
|
|
if (payload) {
|
|
|
|
this.publishEntityState(entity.ID, payload, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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-12-27 10:43:34 -07:00
|
|
|
// Resolve the entity
|
|
|
|
const entity = utils.resolveEntity(topic.ID);
|
2018-10-23 11:39:48 -07:00
|
|
|
|
2018-12-21 16:07:53 -07:00
|
|
|
// Get entity details
|
|
|
|
let endpoint = null;
|
|
|
|
let converters = null;
|
|
|
|
let device = null;
|
|
|
|
|
2018-12-27 10:43:34 -07:00
|
|
|
if (entity.type === 'device') {
|
|
|
|
device = this.zigbee.getDevice(entity.ID);
|
2018-12-21 16:07:53 -07:00
|
|
|
if (!device) {
|
2018-12-27 10:43:34 -07:00
|
|
|
logger.error(`Failed to find device with ieeAddr: '${entity.ID}'`);
|
2019-02-22 04:49:08 -07:00
|
|
|
this.mqtt.log('entity_not_found', entity.ID);
|
2018-12-21 16:07:53 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Map device to a model
|
|
|
|
const model = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
|
|
|
if (!model) {
|
|
|
|
logger.warn(`Device with modelID '${device.modelId}' is not supported.`);
|
2019-02-12 13:39:37 -07:00
|
|
|
logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`);
|
2018-12-21 16:07:53 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine endpoint to publish to.
|
|
|
|
if (model.hasOwnProperty('ep')) {
|
|
|
|
const eps = model.ep(device);
|
|
|
|
endpoint = eps.hasOwnProperty(topic.postfix) ? eps[topic.postfix] : null;
|
2019-02-18 09:57:57 -07:00
|
|
|
if (endpoint === null && eps.hasOwnProperty('default')) {
|
|
|
|
endpoint = eps['default'];
|
|
|
|
}
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
converters = model.toZigbee;
|
2018-12-27 10:43:34 -07:00
|
|
|
} else if (entity.type === 'group') {
|
2019-02-14 10:13:51 -07:00
|
|
|
converters = groupConverters.map((g) => g.to);
|
2018-10-23 11:39:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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()};
|
|
|
|
}
|
|
|
|
|
2019-01-21 09:38:06 -07:00
|
|
|
// Ensure that state and brightness are executed before other commands.
|
|
|
|
const keys = Object.keys(json);
|
|
|
|
keys.sort((a, b) => (['state', 'brightness'].includes(a) ? -1 : 1));
|
|
|
|
|
2018-10-23 11:39:48 -07:00
|
|
|
// For each key in the JSON message find the matching converter.
|
2019-03-02 08:35:00 -07:00
|
|
|
const usedConverters = [];
|
2019-01-21 09:38:06 -07:00
|
|
|
keys.forEach((key) => {
|
2018-12-21 16:07:53 -07:00
|
|
|
const converter = converters.find((c) => c.key.includes(key));
|
2019-03-02 08:35:00 -07:00
|
|
|
|
|
|
|
if (usedConverters.includes(converter)) {
|
|
|
|
// Use a converter only once (e.g. light_onoff_brightness converters can convert state and brightness)
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-10-23 11:39:48 -07:00
|
|
|
if (!converter) {
|
|
|
|
logger.error(`No converter available for '${key}' (${json[key]})`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Converter didn't return a result, skip
|
2019-02-02 12:09:20 -07:00
|
|
|
const converted = converter.convert(key, json[key], json, topic.type, topic.postfix);
|
2018-10-23 11:39:48 -07:00
|
|
|
if (!converted) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-02-01 17:41:05 -07:00
|
|
|
this.zigbee.publish(
|
|
|
|
entity.ID,
|
|
|
|
entity.type,
|
|
|
|
converted.cid,
|
|
|
|
converted.cmd,
|
|
|
|
converted.cmdType,
|
|
|
|
converted.zclData,
|
|
|
|
converted.cfg,
|
|
|
|
endpoint,
|
|
|
|
(error, rsp) => {
|
2019-02-14 10:13:51 -07:00
|
|
|
if (!error) {
|
|
|
|
this.handlePublished(entity, topic, converter, converted);
|
2019-02-20 09:57:28 -07:00
|
|
|
} else {
|
|
|
|
this.handlePublishError(entity, message, error);
|
2018-10-23 11:39:48 -07:00
|
|
|
}
|
2019-02-01 17:41:05 -07:00
|
|
|
}
|
|
|
|
);
|
2018-12-03 09:48:21 -07:00
|
|
|
|
2019-01-22 12:08:57 -07:00
|
|
|
// It's possible for devices to get out of sync when writing an attribute that's not reportable.
|
|
|
|
// So here we re-read the value after a specified timeout, this timeout could for example be the
|
2019-02-03 06:39:00 -07:00
|
|
|
// transition time of a color change or for forcing a state read for devices that don't
|
|
|
|
// automatically report a new state when set.
|
|
|
|
// When reporting is requested for a device (report: true in device-specific settings) we won't
|
|
|
|
// ever issue a read here, as we assume the device will properly report changes.
|
|
|
|
const deviceSettings = settings.getDevice(entity.ID);
|
2019-01-22 12:08:57 -07:00
|
|
|
if (topic.type === 'set' && entity.type === 'device'
|
2019-02-03 06:39:00 -07:00
|
|
|
&& converted.hasOwnProperty('readAfterWriteTime')
|
|
|
|
&& !(deviceSettings && deviceSettings.report)) {
|
2018-12-17 13:27:13 -07:00
|
|
|
const getConverted = converter.convert(key, json[key], json, 'get');
|
2018-12-03 09:48:21 -07:00
|
|
|
setTimeout(() => {
|
2019-02-01 17:41:05 -07:00
|
|
|
this.zigbee.publish(
|
|
|
|
entity.ID, entity.type, getConverted.cid, getConverted.cmd, getConverted.cmdType,
|
|
|
|
getConverted.zclData, getConverted.cfg, endpoint, () => {}
|
|
|
|
);
|
2019-01-22 12:08:57 -07:00
|
|
|
}, converted.readAfterWriteTime);
|
2018-12-03 09:48:21 -07:00
|
|
|
}
|
2019-03-02 08:35:00 -07:00
|
|
|
|
|
|
|
usedConverters.push(converter);
|
2018-10-23 11:39:48 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-05 13:55:30 -07:00
|
|
|
module.exports = DevicePublish;
|