Skip to content

Commit

Permalink
Implement publishing to zigbee groups. #15
Browse files Browse the repository at this point in the history
  • Loading branch information
Koenkk committed Dec 21, 2018
1 parent 968460a commit e3d79a4
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 158 deletions.
94 changes: 62 additions & 32 deletions lib/extension/devicePublish.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
const Queue = require('queue');
const logger = require('../util/logger');

const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/((?!group).+)/(set|get)$`);
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/.+/(set|get)$`);
const postfixes = ['left', 'right', 'center', 'bottom_left', 'bottom_right', 'top_left', 'top_right'];
const maxDepth = 20;

const groupConverters = [
zigbeeShepherdConverters.toZigbeeConverters.on_off,
zigbeeShepherdConverters.toZigbeeConverters.light_brightness,
zigbeeShepherdConverters.toZigbeeConverters.light_colortemp,
zigbeeShepherdConverters.toZigbeeConverters.light_color,
zigbeeShepherdConverters.toZigbeeConverters.ignore_transition,
];

class DevicePublish {
constructor(zigbee, mqtt, state, publishDeviceState) {
this.zigbee = zigbee;
Expand Down Expand Up @@ -47,10 +55,10 @@ class DevicePublish {
topic = topic.replace(`${settings.get().mqtt.base_topic}/`, '');

// Parse type from topic
const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
const cmdType = topic.substr(topic.lastIndexOf('/') + 1, topic.length);

// Remove type from topic
topic = topic.replace(`/${type}`, '');
topic = topic.replace(`/${cmdType}`, '');

// Check if we have to deal with a postfix.
let postfix = '';
Expand All @@ -61,9 +69,15 @@ class DevicePublish {
topic = topic.replace(`/${postfix}`, '');
}

const deviceID = topic;
let entityType = 'device';
if (topic.startsWith('group/')) {
topic = topic.replace('group/', '');
entityType = 'group';
}

const ID = topic;

return {type: type, deviceID: deviceID, postfix: postfix};
return {cmdType: cmdType, ID: ID, postfix: postfix, entityType: entityType};
}

onMQTTMessage(topic, message) {
Expand All @@ -73,22 +87,43 @@ class DevicePublish {
return false;
}

// Map friendlyName to ieeeAddr if possible.
const ieeeAddr = settings.getIeeeAddrByFriendlyName(topic.deviceID) || topic.deviceID;

// Get device
const device = this.zigbee.getDevice(ieeeAddr);
if (!device) {
logger.error(`Failed to find device with ieeAddr: '${ieeeAddr}'`);
return;
// Map friendlyName (ID) to entityID if possible.
let entityID = null;
if (topic.entityType === 'group') {
entityID = settings.getGroupIDByFriendlyName(topic.ID) || topic.ID;
} else if (topic.entityType === 'device') {
entityID = settings.getIeeeAddrByFriendlyName(topic.ID) || topic.ID;
}

// 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://koenkk.github.io/zigbee2mqtt/how_tos/how_to_support_new_devices.html`);
return;
// Get entity details
let endpoint = null;
let converters = null;
let device = null;

if (topic.entityType === 'device') {
device = this.zigbee.getDevice(entityID);
if (!device) {
logger.error(`Failed to find device with ieeAddr: '${entityID}'`);
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://koenkk.github.io/zigbee2mqtt/how_tos/how_to_support_new_devices.html`);
return;
}

// Determine endpoint to publish to.
if (model.hasOwnProperty('ep')) {
const eps = model.ep(device);
endpoint = eps.hasOwnProperty(topic.postfix) ? eps[topic.postfix] : null;
}

converters = model.toZigbee;
} else if (topic.entityType === 'group') {
converters = groupConverters;
}

// Convert the MQTT message to a Zigbee message.
Expand All @@ -100,13 +135,6 @@ class DevicePublish {
json = {state: message.toString()};
}

// Determine endpoint to publish to.
let endpoint = null;
if (model.hasOwnProperty('ep')) {
const eps = model.ep(device);
endpoint = eps.hasOwnProperty(topic.postfix) ? eps[topic.postfix] : null;
}

// When brightness is present skip state; brightness also handles state.
if (json.hasOwnProperty('brightness') && json.hasOwnProperty('state')) {
logger.debug(`Skipping 'state' because of 'brightness'`);
Expand All @@ -115,22 +143,23 @@ class DevicePublish {

// For each key in the JSON message find the matching converter.
Object.keys(json).forEach((key) => {
const converter = model.toZigbee.find((c) => c.key.includes(key));
const converter = converters.find((c) => c.key.includes(key));
if (!converter) {
logger.error(`No converter available for '${key}' (${json[key]})`);
return;
}

// Converter didn't return a result, skip
const converted = converter.convert(key, json[key], json, topic.type);
const converted = converter.convert(key, json[key], json, topic.cmdType);
if (!converted) {
return;
}

// Add job to queue
this.queue.push((queueCallback) => {
this.zigbee.publish(
ieeeAddr,
entityID,
topic.entityType,
converted.cid,
converted.cmd,
converted.cmdType,
Expand All @@ -139,7 +168,8 @@ class DevicePublish {
endpoint,
(error, rsp) => {
// Devices do not report when they go off, this ensures state (on/off) is always in sync.
if (topic.type === 'set' && !error && (key.startsWith('state') || key === 'brightness')) {
if (topic.entityType === 'device' && topic.cmdType === 'set' &&
!error && (key.startsWith('state') || key === 'brightness')) {
const msg = {};
const _key = topic.postfix ? `state_${topic.postfix}` : 'state';
msg[_key] = key === 'brightness' ? 'ON' : json['state'];
Expand All @@ -153,14 +183,14 @@ class DevicePublish {

// When there is a transition in the message the state of the device gets out of sync.
// Therefore; at the end of the transition, read the new state from the device.
if (topic.type === 'set' && converted.zclData.transtime) {
if (topic.cmdType === 'set' && converted.zclData.transtime && topic.entityType === 'device') {
const time = converted.zclData.transtime * 100;
const getConverted = converter.convert(key, json[key], json, 'get');
setTimeout(() => {
// Add job to queue
this.queue.push((queueCallback) => {
this.zigbee.publish(
ieeeAddr, getConverted.cid, getConverted.cmd, getConverted.cmdType,
entityID, topic.entityType, getConverted.cid, getConverted.cmd, getConverted.cmdType,
getConverted.zclData, getConverted.cfg, endpoint, () => queueCallback()
);
});
Expand Down
9 changes: 9 additions & 0 deletions lib/util/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ function getIeeeAddrByFriendlyName(friendlyName) {
);
}

function getGroupIDByFriendlyName(friendlyName) {
if (!settings.groups || !settings.groups.hasOwnProperty(friendlyName)) {
return null;
}

return settings.groups[friendlyName].ID;
}

function changeFriendlyName(old, new_) {
const ieeeAddr = getIeeeAddrByFriendlyName(old);

Expand All @@ -95,5 +103,6 @@ module.exports = {
removeDevice: (ieeeAddr) => removeDevice(ieeeAddr),

getIeeeAddrByFriendlyName: (friendlyName) => getIeeeAddrByFriendlyName(friendlyName),
getGroupIDByFriendlyName: (friendlyName) => getGroupIDByFriendlyName(friendlyName),
changeFriendlyName: (old, new_) => changeFriendlyName(old, new_),
};
32 changes: 22 additions & 10 deletions lib/zigbee.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,33 +192,45 @@ class Zigbee {
return this.shepherd.find(device.ieeeAddr, 1);
}

publish(ieeAddr, cid, cmd, cmdType, zclData, cfg=defaultCfg, ep, callback) {
const device = this.findDevice(ieeAddr, ep);
if (!device) {
logger.error(`Zigbee cannot publish message to device because '${ieeAddr}' not known by zigbee-shepherd`);
getGroup(ID) {
return this.shepherd.getGroup(ID);
}

publish(entityID, entityType, cid, cmd, cmdType, zclData, cfg=defaultCfg, ep, callback) {
let entity = null;
if (entityType === 'device') {
entity = this.findDevice(entityID, ep);
} else if (entityType === 'group') {
entity = this.getGroup(entityID);
}

if (!entity) {
logger.error(
`Zigbee cannot publish message to ${entityType} because '${entityID}' not known by zigbee-shepherd`
);
return;
}

logger.info(
`Zigbee publish to '${ieeAddr}', ${cid} - ${cmd} - ` +
`Zigbee publish to ${entityType} '${entityID}', ${cid} - ${cmd} - ` +
`${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${ep}`
);

const callback_ = (error, rsp) => {
if (error) {
logger.error(
`Zigbee publish to '${ieeAddr}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} ` +
`Zigbee publish to ${entityType} '${entityID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} ` +
`- ${JSON.stringify(cfg)} - ${ep} ` +
`failed with error ${error}`);
}

callback(error, rsp);
};

if (cmdType === 'functional') {
device.functional(cid, cmd, zclData, cfg, callback_);
} else if (cmdType === 'foundation') {
device.foundation(cid, cmd, zclData, cfg, callback_);
if (cmdType === 'functional' && entity.functional) {
entity.functional(cid, cmd, zclData, cfg, callback_);
} else if (cmdType === 'foundation' && entity.foundation) {
entity.foundation(cid, cmd, zclData, cfg, callback_);
} else {
logger.error(`Unknown zigbee publish cmdType ${cmdType}`);
}
Expand Down
26 changes: 13 additions & 13 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
"semver": "*",
"winston": "2.4.2",
"ziee": "*",
"zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#ce52ac4131e2a505af6197b4a26d2b5360e4eb80",
"zigbee-shepherd-converters": "7.0.4",
"zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#081227b1789a6b416a5ef4a61162d805272f1ef3",
"zigbee-shepherd-converters": "7.0.5",
"zive": "*"
},
"devDependencies": {
Expand Down
Loading

0 comments on commit e3d79a4

Please sign in to comment.