zigbee2mqtt/lib/extension/entityPublish.js
2019-09-23 22:21:27 +02:00

201 lines
8.1 KiB
JavaScript

const settings = require('../util/settings');
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const logger = require('../util/logger');
const utils = require('../util/utils');
const assert = require('assert');
const BaseExtension = require('./baseExtension');
const postfixes = utils.getPostfixes();
const topicRegex = new RegExp(`^(.+?)(?:/(${postfixes.join('|')}))?/(get|set)(?:/(.+))?`);
const groupConverters = [
zigbeeHerdsmanConverters.toZigbeeConverters.light_onoff_brightness,
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp,
zigbeeHerdsmanConverters.toZigbeeConverters.light_color,
zigbeeHerdsmanConverters.toZigbeeConverters.light_alert,
zigbeeHerdsmanConverters.toZigbeeConverters.ignore_transition,
];
class EntityPublish extends BaseExtension {
onMQTTConnected() {
// Subscribe to topics.
const baseTopic = settings.get().mqtt.base_topic;
for (let step = 1; step < 20; step++) {
const topic = `${baseTopic}/${'+/'.repeat(step)}`;
this.mqtt.subscribe(`${topic}set`);
this.mqtt.subscribe(`${topic}set/+`);
this.mqtt.subscribe(`${topic}get`);
this.mqtt.subscribe(`${topic}get/+`);
}
}
parseTopic(topic) {
const match = topic.match(topicRegex);
if (!match) {
return null;
}
const ID = match[1].replace(`${settings.get().mqtt.base_topic}/`, '');
// If we didn't repalce base_topic we received something we don't care about
if (ID === match[1] || ID.match(/bridge/)) {
return null;
}
return {ID: ID, postfix: match[2] || '', type: match[3], attribute: match[4]};
}
async onMQTTMessage(topic, message) {
topic = this.parseTopic(topic);
if (!topic) {
return false;
}
const entityName = `${topic.ID}` + (topic.postfix ? `/${topic.postfix}` : '');
const entity = this.zigbee.resolveEntity(entityName);
if (!entity) {
this.mqtt.log('entity_not_found', {friendly_name: entityName});
logger.error(`Entity '${entityName}' is unknown`);
return;
}
// Get entity details
let converters = null;
let target = null;
let options = {};
let device = null;
let mapped = null;
assert(entity.type === 'device' || entity.type === 'group');
if (entity.type === 'device') {
// Map device to a model
if (!entity.mapped) {
logger.warn(`Device with modelID '${entity.device.modelID}' is not supported.`);
logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`);
return;
}
device = entity.device;
mapped = entity.mapped;
target = entity.endpoint;
converters = entity.mapped.toZigbee;
options = entity.mapped.meta && entity.mapped.meta.options ? entity.mapped.meta.options : {};
options = {...options, ...entity.settings};
} else {
converters = groupConverters;
target = entity.group;
options = entity.settings;
}
// Convert the MQTT message to a Zigbee message.
let json = {};
if (topic.hasOwnProperty('attribute') && topic.attribute) {
json[topic.attribute] = message;
} else {
try {
json = JSON.parse(message);
} catch (e) {
// Cannot be parsed to JSON, assume state message.
json = {state: message};
}
}
/**
* Home Assistant always publishes 'state', even when e.g. only setting
* the color temperature. This would lead to 2 zigbee publishes, where the first one
* (state) is probably unecessary.
*/
const deviceState = this.state.get(entity.settings.ID);
if (settings.get().homeassistant) {
const hasColorTemp = json.hasOwnProperty('color_temp');
const hasColor = json.hasOwnProperty('color');
const hasBrightness = json.hasOwnProperty('brightness');
const isOn = deviceState && deviceState.state === 'ON' ? true : false;
if (isOn && (hasColorTemp || hasColor) && !hasBrightness) {
delete json.state;
logger.debug('Skipping state because of Home Assistant');
}
}
// Ensure that state and brightness are executed before other commands.
const entries = Object.entries(json);
entries.sort((a, b) => (['state', 'brightness'].includes(a[0]) ? -1 : 1));
// For each attribute call the corresponding converter
const usedConverters = [];
for (const [key, value] of entries) {
const converter = converters.find((c) => c.key.includes(key));
if (usedConverters.includes(converter)) {
// Use a converter only once (e.g. light_onoff_brightness converters can convert state and brightness)
continue;
}
if (!converter) {
logger.error(`No converter available for '${key}' (${json[key]})`);
continue;
}
// Converter didn't return a result, skip
const meta = {
endpoint_name: topic.postfix,
options,
message: json,
logger,
device,
state: deviceState,
mapped,
};
try {
if (topic.type === 'set' && converter && converter.convertSet) {
logger.debug(`Publishing '${topic.type}' '${key}' to '${entity.name}'`);
const result = await converter.convertSet(target, key, value, meta);
if (result && result.hasOwnProperty('state')) {
const msg = result.state;
if (topic.postfix) {
msg[`state_${topic.postfix}`] = msg.state;
delete msg.state;
}
this.publishEntityState(entity.settings.ID, msg);
}
// 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
// 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.
// Only do this when the retrieve_state option is enabled for this device.
if (
entity.type === 'device' && result && result.hasOwnProperty('readAfterWriteTime') &&
entity.settings.retrieve_state
) {
setTimeout(() => converter.convertGet(target, key, meta), result.readAfterWriteTime);
}
} else if (topic.type === 'get' && converter && converter.convertGet) {
logger.debug(`Publishing get '${topic.type}' '${key}' to '${entity.name}'`);
await converter.convertGet(target, key, meta);
} else {
logger.error(`No converter available for '${topic.type}' '${key}' (${json[key]})`);
continue;
}
} catch (error) {
const message =
`Publish '${topic.type}' '${key}' to '${entity.name}' failed: '${error}'`;
logger.error(message);
this.mqtt.log('zigbee_publish_error', message, {friendly_name: entity.name});
}
usedConverters.push(converter);
}
return true;
}
}
module.exports = EntityPublish;