Compare commits

..
91 Commits
Author SHA1 Message Date
Koen Kanters ac3b924065 1.5.1 2019-07-16 18:31:32 +02:00
Koen Kanters dd8a55cf08 Update zigbee-herdsman to 0.1.17. https://github.com/Koenkk/zigbee2mqtt/issues/1734#issuecomment-511403606 2019-07-15 20:04:39 +02:00
clockbrain 3ae2d2b946 Network map routes selectable by mqtt topic (#1736) 2019-07-15 20:02:56 +02:00
Koen Kanters 31b8352f5a 1.5.0 2019-07-13 19:39:20 +02:00
Koen Kanters 503fef807c Add changelog generator tool for new supported devices page. 2019-07-13 19:38:23 +02:00
Koen Kanters 4aab81d9e3 Remove HS1WD (not supported anymore). 2019-07-13 19:38:00 +02:00
Koen Kanters 8d705ade39 Update zigbee-shepherd-converters. 2019-07-13 16:36:24 +02:00
Koen Kanters d3e97953c3 Update zigbee-shepherd-converters. 2019-07-11 20:22:43 +02:00
wixoff 1b859c4dc2 Support for PP-WHT-US new sensors (#1718)
* Support for PP-WHT-US new sensors

* Update homeassistant.js
2019-07-11 19:36:50 +02:00
Koen Kanters 225919bb23 Update zigbee-herdsman to 0.1.6. https://github.com/Koenkk/zigbee2mqtt/issues/1710 2019-07-10 19:04:54 +02:00
Andreas Brett 74b8340afb add TLS/SSL options for connecting to MQTT server (#1706)
* add TLS/SSL options for connecting to MQTT server

* Update mqtt.js

* gotta love travis

* Update mqtt.js

separated processing of mqtt.ca and mqtt.key/mqtt.cert in order to allow for "server-authentication only" as well as "server+client authentication"

* Update mqtt.js
2019-07-10 17:57:41 +02:00
Koen Kanters 08c0f2b614 Lint. 2019-07-08 22:04:38 +02:00
Koen Kanters 61b54c50a5 Update zigbee-shepherd-converters and zigbee-herdsman. 2019-07-08 21:42:07 +02:00
Rick 6400f52a28 Added "alert" for groups (#1704) 2019-07-08 21:06:08 +02:00
clockbrain 97f9f83181 Use penwidth instead of dotted/dashed lines (#1694) 2019-07-06 10:09:44 +02:00
Koen Kanters 90c3fc0320 Add device rename test case. https://github.com/Koenkk/zigbee2mqtt/issues/1687 2019-07-04 20:50:54 +02:00
Koen Kanters 94a7cd8ec9 Update zigbee-shepherd-converters to 10.1.1 2019-07-04 20:16:20 +02:00
Craig Stephen 1df43bdbc4 Add support for UK Hive Bulb E27 & B22 - Warm to cool white (#1683) 2019-07-03 17:38:43 +02:00
Koen Kanters 3daf97283c Fix queue settings. 2019-07-02 19:13:06 +02:00
Koen Kanters aca0c39291 Move queue from advanced. 2019-07-02 19:00:42 +02:00
Koen Kanters 9624384c3f Add queue settings. https://github.com/Koenkk/Z-Stack-firmware/issues/93 2019-07-02 18:56:03 +02:00
Koen Kanters 3b4ee3bc2e Update zigbee-shepherd-converters to 10.1.0. 2019-07-01 16:48:54 +02:00
clockbrain 7a19c8876a Network map fixes (#1676)
* Include scan failed note in network map

* Include relationship in map structure

* Clearer source and destination node naming

* eslint fixes

* More eslint fixes

* Removed empty brackets for no routes

* Reposition short address in device node

* eslint fixes
2019-07-01 16:10:50 +02:00
Koen Kanters 51df6b7e8a Correctly call convert. https://github.com/Koenkk/zigbee2mqtt/issues/1639#issuecomment-506016224 2019-06-27 19:11:21 +02:00
Andreas Brett 7cabe3a523 colors for network map (#1666)
* Update settings.js

added default color options for network map

* Update networkMap.js

- add timestamp node to indicate when the map was created
- colorize nodes by device type
- colorize edges by activity

* Update networkMap.js

make travis happy

* Update networkMap.js

* Update networkMap.js

* Update settings.js

* Refactor

* Move to graphviz.
2019-06-26 19:30:38 +02:00
Viet Dzung 7b2522f712 Support whitelist devices feature #1643 (#1654)
* Support whitelist devices feature #1643

* Control via MQTT

* Log
2019-06-25 19:38:36 +02:00
Koenkk 5a132ad34b Retrieve default ep from mapping. #1662 2019-06-25 19:13:59 +02:00
Koen Kanters 362b910e48 Lint. 2019-06-24 21:10:06 +02:00
Dennis Keitzel 241b300f7b Allow custom home-assistant status topic (#1667) 2019-06-24 20:52:47 +02:00
Koen Kanters 0e9931af6c Improve bind logging. https://github.com/Koenkk/zigbee2mqtt/issues/1662 2019-06-24 20:25:37 +02:00
Koen Kanters f4f297e8a2 Update zigbee-herdsman. 2019-06-24 20:17:07 +02:00
Koen Kanters 5934efa41e Add last_seen when device publish succeeds. https://github.com/Koenkk/zigbee2mqtt/issues/1651 2019-06-24 20:16:03 +02:00
clockbrain 38a1aec823 Network map styling improvements (#1653)
* Weight and style lines based on routes

* Refactor to make End Device case more obvious
2019-06-23 21:22:13 +02:00
Koen Kanters 3c398df658 Lint. 2019-06-23 16:10:44 +02:00
Koen Kanters 91d415404c Update zigbee-shepherd-converters and zigbee-herdsman. 2019-06-23 16:02:43 +02:00
Koen Kanters ef01346776 Update zigbee-shepherd-converters to 10.0.0 and zigbee-herdsman to 0.1.11. 2019-06-19 21:44:44 +02:00
Koen Kanters 801e97143c Fix adding groups with duplicate names. #764 2019-06-19 18:35:57 +02:00
Koen Kanters 304b2efa48 Don’t publish emtpy messages. #1599 2019-06-19 18:11:35 +02:00
Koen Kanters a36ed2032d Update zigbee-herdsman to 0.1.10. 2019-06-19 18:07:13 +02:00
Koen Kanters 17d01e937e Update zigbee-herdsman to 0.1.9. 2019-06-18 22:07:07 +02:00
Koen Kanters 110f80ec84 Lint. 2019-06-17 21:41:48 +02:00
clockbrain b28da0265e Include last seen in device boxes on graphviz map (#1633)
* Include last seen in device boxes on graphviz map

* Refactor

* Fix test cases

* Bind

* Refactor last seen label construction

* style fix
2019-06-17 21:28:27 +02:00
Koenkk 1c529c4873 Add group remove_all without group #764 2019-06-17 21:17:29 +02:00
clockbrain 3981c1c6e4 Include network routes on the map (#1641)
* Initial inclusion of routes on map

* Forgot to include zigbee.js

* Hex string for route address

* Rebaseline to dev

* Cleaner merging of scan arrays

* style fixes
2019-06-16 13:49:15 +02:00
Koen Kanters c48f73e36b Discover action sensor for WXKG03LM. https://github.com/Koenkk/zigbee2mqtt/issues/1640 2019-06-16 13:13:03 +02:00
Koen Kanters 94e03d4189 Update zigbee-herdsman 2019-06-15 22:57:35 +02:00
Koen Kanters e30ff9e722 Move to zigbee-herdsman. 2019-06-15 22:20:54 +02:00
pali 1f63a32170 [graphviz] show network address in hexastring format (#1638)
* [graphviz] show network address in hexastring format

Wireshark shows network address in hexadecimal format, so it's easier to follow messages in Wireshark if both shows address in same format.

* Update networkMap.js

* Update networkMap.js
2019-06-15 17:04:35 +02:00
Jevgeni Kiski e9b5a588cc #1588 Openhab2 compatibility (#1589) 2019-06-15 16:55:33 +02:00
Koen Kanters 4357064203 Update zigbee-shepherd-converters to 9.0.22. 2019-06-15 14:02:58 +02:00
Koen Kanters 86a65ebec0 Merge branch 'master' into dev 2019-06-13 22:10:52 +02:00
Koen Kanters 259ba9d658 Update zigbee-shepherd-converters to 9.0.21. 2019-06-13 22:01:39 +02:00
Dan Chen 9fe76c29ef Add power usage metering to GE Plug-in Smart Switch 45853GE (#1629)
* Create FUNDING.yml

* Add power usage metering to GE Plug-in Smart Switch 45853GE

* Delete FUNDING.yml
2019-06-13 21:48:04 +02:00
clockbrain dd49a76e5c Network map rewrite using queue (#1626)
* Initial rewrite of network map scan using queue

* Rename routerList to scanList

* eslist fixes

* More eslint fixes

* Timeout now dynamic based on number of scanable devices
2019-06-13 21:43:12 +02:00
Anne Meijer 6976f8e70a Added support for HEIMAN HS1CA-E (#1622)
* Create FUNDING.yml

* Adde support for HEIMAN HS1CA-E

* Fixed eslint issues

* Delete FUNDING.yml
2019-06-11 20:07:32 +02:00
Koen Kanters 82dc8437a1 Get rid of logging during test cases. 2019-06-10 00:01:48 +02:00
Koen Kanters fb12d15d9d Fix lint error. 2019-06-09 23:46:19 +02:00
Koen Kanters 852950fc09 Update zigbee-shepherd-converters to 9.0.20. 2019-06-09 23:43:40 +02:00
clockbrain b663e37fd9 Also match network map link by short address (#1611) 2019-06-09 08:16:38 +02:00
Koen Kanters 70fbe714e3 Add trace logging and add trace logging to zigbeeQueue. https://github.com/Koenkk/zigbee2mqtt/issues/1566 2019-06-08 19:56:35 +02:00
Koen Kanters ee8161b807 Don’t add state_topic to payload for thermostat. #1599 2019-06-07 23:55:20 +02:00
Koen Kanters 8ad6aee43a Identify QBKG04LM and QBKG03LM as end device. https://github.com/Koenkk/zigbee2mqtt/issues/1594 2019-06-04 19:23:58 +02:00
clockbrain 4497214588 Network map fixes (#1590)
* Remove device status from network scan topology

* Random start for each lqi scan then sort resultant link map

* Fix log statement order

* Sort network map links by composite key

* Fix typo in link sorting

* Fix eslint formatting
2019-06-02 10:05:44 +02:00
Koen Kanters 08e55e469f Update zigbee-shepherd-converters to 9.0.19. 2019-05-29 20:26:40 +02:00
Koen Kanters c29bfe1455 Allow to specify endpoint when adding device to group. (#1515)
* uu

* Update
2019-05-29 20:11:40 +02:00
Koen Kanters 82ed260d47 Update zigbee-shepherd-converters to 9.0.18. 2019-05-29 19:33:15 +02:00
rachetfoot caff94559e Iterate objects recursively and support array output (#1573)
* Iterate objects recursively and support array output

* Fix eslint errors

* Improve array handling and expand related test

* Improve tests and array support, add special case for color attributes
2019-05-28 20:01:46 +02:00
Koen Kanters c2b4d63e12 Don’t cache elapsed. https://github.com/Koenkk/zigbee2mqtt/issues/1576 2019-05-26 20:03:43 +02:00
Koen Kanters 825f096434 Update zigbee-shepherd-converters to 9.0.17. 2019-05-24 18:32:37 +02:00
clockbrain 4b14222bda Network map error handling (#1565)
* Catch errors from failing network scans

* Tidier code for process result

* Fix eslint format errors
2019-05-24 18:21:53 +02:00
Koen Kanters 927c4dbc05 Create FUNDING.yml 2019-05-24 08:27:35 +02:00
clockbrain c4e9ea208a Add debugging for network scan (#1559)
* Add debugging for network scan

* Fix eslint warning
2019-05-23 18:53:47 +02:00
Koen Kanters af09624cff Update zigbee-shepherd-converters to 9.0.16. 2019-05-22 22:09:13 +02:00
Koen Kanters e4a50b662d Update zigbee-shepherd-converters to 9.0.15. 2019-05-21 21:15:16 +02:00
clockbrain 22b64d09f2 Network map (#1546)
* Initial iterative rewrite of network scan

* Remove unused linkKey

* Restore original log message

* Cleaner array flattening

* Fix various travis warnings

* Add a timeout so partial map can be generated

* Fix further eslint issues

* Add a timeout so partial map can be generated

* Fix further eslint issues
2019-05-21 21:04:14 +02:00
Koen Kanters dc028d5046 Update zigbee-shepherd. 2019-05-21 20:54:49 +02:00
Koen Kanters 0352a738a7 Update zigbee-shepherd-converters to 9.0.14. 2019-05-20 20:53:51 +02:00
Koen Kanters 6adfdacb3c Don’t quit twice. https://github.com/Koenkk/zigbee2mqtt/issues/1550 2019-05-20 20:43:25 +02:00
Koen Kanters 18913a1577 Update zigbee-shepherd-converters to 9.0.12. 2019-05-19 19:51:27 +02:00
Koen Kanters 1e04c229dd Merge remote-tracking branch 'origin/master' into dev 2019-05-19 19:34:02 +02:00
clockbrain 31d21546b9 Simpler network map scan (#1543)
* Initial iterative rewrite of network scan

* Remove unused linkKey

* Restore original log message

* Cleaner array flattening

* Fix various travis warnings
2019-05-19 19:33:30 +02:00
Koen Kanters 7f9104367b Reduce permitJoin interval to 160 seconds to fix pairing devices after pairing E1603. https://github.com/Koenkk/zigbee2mqtt/issues/1099 2019-05-19 19:10:51 +02:00
Koen Kanters 92d0002bbb Update zigbee-shepherd-converters to 9.0.11. 2019-05-19 18:51:00 +02:00
Koen Kanters 0074c32136 Update zigbee-shepherd-converters to 9.0.10. 2019-05-14 21:57:47 +02:00
Koen Kanters faa6aa41a9 Fix AC0251100NJ discovery. 2019-05-14 21:46:55 +02:00
Koen Kanters 3ed9bbde1a Update zigbee-shepherd-converters to 9.0.9 2019-05-14 21:36:07 +02:00
Koen Kanters 8f39ae9d63 Update zigbee-shepherd-converters to 9.0.8. 2019-05-14 21:18:48 +02:00
Koen Kanters a09da4fe10 Update zigbee-shepherd-converters to 9.0.7. 2019-05-14 21:12:13 +02:00
Koen Kanters 9cc82701c1 Fix crash when device not in configuration.yaml 2019-05-14 19:25:13 +02:00
Koen Kanters 190dda5104 Update zigbee-shepherd-converters to 9.0.6 2019-05-14 18:04:47 +02:00
Andreas Brett 3d74a6e031 log friendly name when possible (#1524)
* log friendly name when possible

* make travis happy
2019-05-14 17:51:43 +02:00
35 changed files with 7956 additions and 1487 deletions
+2 -1
View File
@@ -12,7 +12,8 @@
"rules": {
"require-jsdoc": "off",
"indent": ["error", 4],
"max-len": ["error", { "code": 120 }]
"max-len": ["error", { "code": 120 }],
"no-prototype-builtins": "off"
},
"plugins": [
"jest"
+2
View File
@@ -0,0 +1,2 @@
github: [koenkk]
custom: https://www.paypal.me/koenkk
+6 -1
View File
@@ -13,6 +13,11 @@ controller.start();
process.on('SIGINT', handleQuit);
process.on('SIGTERM', handleQuit);
let stopping = false;
function handleQuit() {
controller.stop(() => process.exit());
if (!stopping) {
stopping = true;
controller.stop(() => process.exit());
}
}
+37 -14
View File
@@ -157,6 +157,10 @@ class Controller {
}
start() {
logger.info(`Logging to directory: '${logger.directory}'`);
logger.cleanup();
this.state.start();
this.startupLogVersion(() => {
@@ -204,7 +208,7 @@ class Controller {
}
if (device.type) {
type = device.type;
type = utils.correctDeviceType(device);
}
return `${friendlyName} (${device.ieeeAddr}): ${friendlyDevice.model} - ` +
@@ -245,22 +249,41 @@ class Controller {
messagePayload.device = this.getDeviceInfoForMqtt(entityID);
}
if (settings.get().experimental.output === 'json') {
this.mqtt.publish(entity.friendlyName, JSON.stringify(messagePayload), options);
} else if (settings.get().experimental.output === 'attribute') {
Object.keys(messagePayload).forEach((key) => {
if (typeof messagePayload[key] == 'object') {
Object.keys(messagePayload[key]).forEach((subKey) => {
this.mqtt.publish(`${entity.friendlyName}/${key}-${subKey}`,
`${messagePayload[key][subKey]}`, options);
});
} else {
this.mqtt.publish(`${entity.friendlyName}/${key}`, `${messagePayload[key]}`, options);
}
});
if (Object.entries(messagePayload).length) {
if (settings.get().experimental.output === 'json') {
this.mqtt.publish(entity.friendlyName, JSON.stringify(messagePayload), options);
} else if (settings.get().experimental.output === 'attribute') {
this.iteratePayloadForAttrOutput(entity.friendlyName+'/', messagePayload, options);
}
}
}
iteratePayloadForAttrOutput(topicRoot, payload, options) {
Object.keys(payload).forEach((key) => {
let subPayload = payload[key];
let message;
// Special cases
if (key === 'color' &&
subPayload.r !== undefined &&
subPayload.g !== undefined &&
subPayload.b !== undefined) {
subPayload = [subPayload.r, subPayload.g, subPayload.b];
}
// Check Array first, since it is also an Object
if (Array.isArray(subPayload)) {
message = subPayload.map((x) => `${x}`).join(',');
} else if (typeof subPayload === 'object') {
return this.iteratePayloadForAttrOutput(topicRoot+key+'-', subPayload, options);
} else {
message = typeof subPayload === 'string' ? subPayload : JSON.stringify(subPayload);
}
this.mqtt.publish(`${topicRoot}${key}`, message, options);
});
}
getDeviceInfoForMqtt(ieeeAddr) {
const device = this.zigbee.getDevice(ieeeAddr);
const {
+11
View File
@@ -27,6 +27,7 @@ class BridgeConfig {
this.deviceOptions = this.deviceOptions.bind(this);
this.addGroup = this.addGroup.bind(this);
this.removeGroup = this.removeGroup.bind(this);
this.whitelist= this.whitelist.bind(this);
// Set supported options
this.supportedOptions = {
@@ -44,9 +45,19 @@ class BridgeConfig {
'device_options': this.deviceOptions,
'add_group': this.addGroup,
'remove_group': this.removeGroup,
'whitelist': this.whitelist,
};
}
whitelist(topic, message) {
message = message.toString();
const IDByFriendlyName = settings.getIeeeAddrByFriendlyName(message);
const deviceID = IDByFriendlyName ? IDByFriendlyName : message;
const whitelisted = settings.whitelistDevice(deviceID);
whitelisted ? logger.info(`Whitelisted '${deviceID}'`) : logger.error(`Failed to whitelist '${deviceID}'`);
this.mqtt.log('device_whitelisted', deviceID);
}
deviceOptions(topic, message) {
let json = null;
try {
+4 -4
View File
@@ -30,20 +30,20 @@ class DeviceAvailability {
}
isPingable(device) {
logger.debug(`Checking if ${device.ieeeAddr} is pingable`);
logger.trace(`Checking if ${device.ieeeAddr} is pingable`);
if (this.blacklist.includes(device.ieeeAddr)) {
logger.debug(`${device.ieeeAddr} is not pingable because of blacklist`);
logger.trace(`${device.ieeeAddr} is not pingable because of blacklist`);
return false;
}
if (pingableDevices.find((d) => d.zigbeeModel.includes(device.modelId))) {
logger.debug(`${device.ieeeAddr} is pingable because in pingable devices`);
logger.trace(`${device.ieeeAddr} is pingable because in pingable devices`);
return true;
}
const result = utils.isRouter(device) && !utils.isBatteryPowered(device);
logger.debug(`${device.ieeeAddr} is pingable (${result}) not router or battery powered`);
logger.trace(`${device.ieeeAddr} is pingable (${result}) not router or battery powered`);
return result;
}
+6 -3
View File
@@ -96,18 +96,21 @@ class DeviceBind {
// Bind
clusters.forEach((cluster) => {
logger.debug(`${topic.type}ing cluster '${cluster}' from ${sourceEntity.ID}' to '${targetEntity.ID}'`);
logger.debug(
`${topic.type}ing cluster '${cluster}' from '${sourceEntity.ID}' ` +
`(${source.epId}) to '${targetEntity.ID}'`
);
this.zigbee[topic.type](source, cluster, target, (error) => {
if (error) {
logger.error(
`Failed to ${topic.type} cluster '${cluster}' from ${sourceEntity.ID}' to ` +
`Failed to ${topic.type} cluster '${cluster}' from '${sourceEntity.ID}' to ` +
`'${targetEntity.ID}' (${error})`
);
} else {
logger.info(
`Successfully ${topic.type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from ` +
`${sourceEntity.ID}' to '${targetEntity.ID}'`
`'${sourceEntity.ID}' to '${targetEntity.ID}'`
);
this.mqtt.log(
+9 -2
View File
@@ -13,6 +13,7 @@ const groupConverters = [
zigbeeShepherdConverters.toZigbeeConverters.light_onoff_brightness,
zigbeeShepherdConverters.toZigbeeConverters.light_colortemp,
zigbeeShepherdConverters.toZigbeeConverters.light_color,
zigbeeShepherdConverters.toZigbeeConverters.light_alert,
zigbeeShepherdConverters.toZigbeeConverters.ignore_transition,
];
@@ -69,6 +70,10 @@ class DevicePublish {
delete msg.state;
}
if (settings.get().advanced.last_seen !== 'disable') {
msg.last_seen = utils.formatDate(Date.now(), settings.get().advanced.last_seen);
}
this.publishEntityState(entity.ID, msg);
}
}
@@ -87,6 +92,7 @@ class DevicePublish {
let endpoint = null;
let converters = null;
let device = null;
let options = {};
if (entity.type === 'device') {
device = this.zigbee.getDevice(entity.ID);
@@ -114,6 +120,7 @@ class DevicePublish {
}
converters = model.toZigbee;
options = model.options || {};
} else if (entity.type === 'group') {
converters = groupConverters;
}
@@ -168,7 +175,7 @@ class DevicePublish {
}
// Converter didn't return a result, skip
const convertedResults = converter.convert(key, json[key], json, topic.type, topic.postfix);
const convertedResults = converter.convert(key, json[key], json, topic.type, topic.postfix, options);
if (!convertedResults || !convertedResults.length) {
return;
}
@@ -202,7 +209,7 @@ class DevicePublish {
const deviceSettings = settings.getDevice(entity.ID);
if (topic.type === 'set' && entity.type === 'device' &&
converted.hasOwnProperty('readAfterWriteTime') && deviceSettings && deviceSettings.retrieve_state) {
const getConvertedResults = converter.convert(key, json[key], json, 'get');
const getConvertedResults = converter.convert(key, json[key], json, 'get', '', options);
getConvertedResults.forEach((getConverted) => {
setTimeout(() => {
this.zigbee.publish(
+2 -10
View File
@@ -158,16 +158,8 @@ class DeviceReceive {
// Add last seen timestamp
const now = Date.now();
switch (settings.get().advanced.last_seen) {
case 'ISO_8601':
payload.last_seen = new Date(now).toISOString();
break;
case 'ISO_8601_local':
payload.last_seen = utils.toLocalISOString(new Date(now));
break;
case 'epoch':
payload.last_seen = now;
break;
if (settings.get().advanced.last_seen !== 'disable') {
payload.last_seen = utils.formatDate(now, settings.get().advanced.last_seen);
}
if (settings.get().advanced.elapsed) {
+44 -27
View File
@@ -1,10 +1,12 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
const data = require('../util/data');
const utils = require('../util/utils');
const fs = require('fs');
const diff = require('deep-diff');
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/.+/(remove|add|remove_all)$`);
const topicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(remove|add|remove_all)$`);
class Groups {
constructor(zigbee, mqtt, state, publishEntityState) {
@@ -30,6 +32,7 @@ class Groups {
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/+/remove`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/+/add`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/+/remove_all`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/remove_all`);
}
apply(from, to) {
@@ -44,7 +47,7 @@ class Groups {
const groupID = diff.path[0];
if (diff.kind === 'N') {
diff.rhs.forEach((ieeeAddr) => this.updateDeviceGroup(ieeeAddr, 'add', groupID));
diff.rhs.forEach((ID) => this.updateDeviceGroup(ID, 'add', groupID));
} else if (diff.kind === 'A') {
if (diff.item.lhs) {
this.updateDeviceGroup(diff.item.lhs, 'remove', groupID);
@@ -52,7 +55,7 @@ class Groups {
this.updateDeviceGroup(diff.item.rhs, 'add', groupID);
}
} else if (diff.kind === 'D') {
diff.lhs.forEach((ieeeAddr) => this.updateDeviceGroup(ieeeAddr, 'remove', groupID));
diff.lhs.forEach((ID) => this.updateDeviceGroup(ID, 'remove', groupID));
} else if (diff.kind === 'E') {
this.updateDeviceGroup(diff.rhs, 'add', groupID);
this.updateDeviceGroup(diff.lhs, 'remove', groupID);
@@ -61,9 +64,28 @@ class Groups {
}
}
getGroupsOfDevice(ieeeAddr) {
parseID(ID) {
let entityID = ID;
let endpointID = null;
const postfix = utils.getPostfixes().find((p) => entityID.endsWith(`/${p}`));
if (postfix) {
// Found a postfix, retrieve the endpoint which correspodns to the postfix
entityID = entityID.substring(0, entityID.length - (postfix.length + 1));
const endpoint = utils.getEndpointByEntityID(this.zigbee, entityID, postfix);
if (!endpoint) {
return;
}
endpointID = endpoint.epId;
}
return {endpointID, entityID};
}
getGroupsOfDevice(entityID) {
return Object.keys(settings.getGroups()).filter((groupID) => {
return settings.getGroup(groupID).devices.includes(ieeeAddr);
return settings.getGroup(groupID).devices.includes(entityID);
});
}
@@ -97,7 +119,7 @@ class Groups {
}
parseTopic(topic) {
if (!topic.match(topicRegex)) {
if (!topic.match(topicRegex) && !topic.match(topicRegexRemoveAll)) {
return null;
}
@@ -110,10 +132,10 @@ class Groups {
// Remove type from topic
topic = topic.replace(`/${type}`, '');
return {friendly_name: topic, type};
return {friendly_name: type === 'remove_all' ? null : topic, type};
}
updateDeviceGroup(ieeeAddr, cmd, groupID) {
updateDeviceGroup(ID, cmd, groupID) {
let payload = null;
const orignalCmd = cmd;
if (cmd === 'add') {
@@ -127,6 +149,9 @@ class Groups {
cmd = 'removeAll';
}
const {entityID, endpointID} = this.parseID(ID);
const ieeeAddr = settings.resolveEntity(entityID).ID;
const cb = (error, rsp) => {
if (error) {
logger.error(`Failed to ${cmd} ${ieeeAddr} from ${groupID}`);
@@ -167,8 +192,8 @@ class Groups {
} else if (cmd === 'remove') {
settings.removeDeviceFromGroup(groupID, ieeeAddr);
} else if (cmd === 'removeAll') {
Object.keys(settings.get().groups).forEach((groupID) => {
settings.removeDeviceFromGroup(groupID, ieeeAddr);
Object.keys(settings.get().groups).forEach((groupID_) => {
settings.removeDeviceFromGroup(groupID_, ieeeAddr);
});
}
}
@@ -176,7 +201,7 @@ class Groups {
this.zigbee.publish(
ieeeAddr, 'device', 'genGroups', cmd, 'functional',
payload, null, null, cb,
payload, null, endpointID, cb,
);
}
@@ -188,27 +213,19 @@ class Groups {
}
// Find ID of this group.
const groupID = settings.getGroupIDByFriendlyName(topic.friendly_name);
if (!groupID) {
logger.error(`Group with friendly_name '${topic.friendly_name}' doesn't exist`);
return;
}
let groupID = null;
if (topic.type !== 'remove_all') {
groupID = settings.getGroupIDByFriendlyName(topic.friendly_name);
if (!groupID) {
logger.error(`Group with friendly_name '${topic.friendly_name}' doesn't exist`);
return;
}
if (groupID === 99) {
logger.error('Group 99 is reserved, please use a different groupID');
return;
}
// Map message to ieeeAddr and check if device exist.
message = message.toString();
const ieeeAddr = settings.getIeeeAddrByFriendlyName(message) || message;
if (!this.zigbee.getDevice(ieeeAddr)) {
logger.error(`Failed to find device '${message}'`);
return;
groupID = groupID.toString();
}
// Send command to the device.
this.updateDeviceGroup(ieeeAddr, topic.type, groupID);
this.updateDeviceGroup(message.toString(), topic.type, groupID);
return true;
}
+122 -13
View File
@@ -95,6 +95,26 @@ const configurations = {
device_class: 'battery',
},
},
'binary_sensor_lock': {
type: 'binary_sensor',
object_id: 'lock',
discovery_payload: {
payload_on: 'UNLOCK',
payload_off: 'LOCK',
value_template: '{{ value_json.state}}',
device_class: 'lock',
},
},
'binary_sensor_lock_reverse': {
type: 'binary_sensor',
object_id: 'lock_reverse',
discovery_payload: {
payload_on: 'UNLOCK',
payload_off: 'LOCK',
value_template: '{{ value_json.reverse}}',
device_class: 'lock',
},
},
// Sensor
'sensor_illuminance': {
@@ -145,11 +165,29 @@ const configurations = {
type: 'sensor',
object_id: 'power',
discovery_payload: {
unit_of_measurement: 'Watt',
icon: 'mdi:flash',
unit_of_measurement: 'W',
icon: 'mdi:factory',
value_template: '{{ value_json.power }}',
},
},
'sensor_current': {
type: 'sensor',
object_id: 'current',
discovery_payload: {
unit_of_measurement: 'A',
icon: 'mdi:power-plug',
value_template: '{{ value_json.current }}',
},
},
'sensor_voltage': {
type: 'sensor',
object_id: 'voltage',
discovery_payload: {
unit_of_measurement: 'V',
icon: 'mdi:flash',
value_template: '{{ value_json.voltage }}',
},
},
'sensor_action': {
type: 'sensor',
object_id: 'action',
@@ -300,6 +338,7 @@ const configurations = {
type: 'climate',
object_id: 'climate',
discovery_payload: {
state_topic: false,
min_temp: 7,
max_temp: 30,
modes: ['off', 'auto', 'heat'],
@@ -350,9 +389,10 @@ const mapping = {
'WXKG01LM': [configurations.sensor_click, configurations.sensor_battery],
'WXKG11LM': [configurations.sensor_click, configurations.sensor_battery],
'WXKG12LM': [configurations.sensor_click, configurations.sensor_battery],
'WXKG03LM': [configurations.sensor_click, configurations.sensor_battery],
// BREAKING_IMPROVEMENT: only use sensor_click for WXKG03LM (action hold -> click hold)
'WXKG03LM': [configurations.sensor_click, configurations.sensor_battery, configurations.sensor_action],
'WXKG02LM': [configurations.sensor_click, configurations.sensor_battery],
'QBKG04LM': [configurations.switch, configurations.sensor_click],
'QBKG04LM': [configurations.switch, configurations.sensor_click, configurations.sensor_action],
'QBKG03LM': [switchWithPostfix('left'), switchWithPostfix('right'), configurations.sensor_click],
'WSDCGQ01LM': [configurations.sensor_temperature, configurations.sensor_humidity, configurations.sensor_battery],
'WSDCGQ11LM': [
@@ -415,6 +455,7 @@ const mapping = {
'RS 125': [configurations.light_brightness],
'RS 225': [configurations.light_brightness],
'RB 145': [configurations.light_brightness],
'RB 245': [configurations.light_brightness],
'PL 110': [configurations.light_brightness],
'ST 110': [configurations.light_brightness],
'UC 110': [configurations.light_brightness],
@@ -485,7 +526,7 @@ const mapping = {
'3261030P7': [configurations.light_brightness_colortemp],
'3216431P5': [configurations.light_brightness_colortemp],
'DJT11LM': [configurations.sensor_action, configurations.sensor_battery],
'E1603': [configurations.switch],
'E1603/E1702': [configurations.switch],
'7199960PH': [configurations.light_brightness_colorxy],
'74696': [configurations.light_brightness],
'AB35996': [configurations.light_brightness_colortemp_colorxy],
@@ -516,8 +557,8 @@ const mapping = {
'SP 120': [configurations.switch, configurations.sensor_power],
'RB 248 T': [configurations.light_brightness_colortemp],
'HS3SA': [configurations.binary_sensor_smoke, configurations.binary_sensor_battery_low],
'HS1DS': [configurations.binary_sensor_contact],
'HS1WL': [configurations.binary_sensor_water_leak],
'HS1DS/HS3DS': [configurations.binary_sensor_contact],
'HS1WL/HS3WL': [configurations.binary_sensor_water_leak],
'HS1-WL-E': [configurations.binary_sensor_water_leak],
'421786': [configurations.light_brightness],
'ICZB-IW11D': [configurations.light_brightness],
@@ -552,7 +593,7 @@ const mapping = {
'E1524': [configurations.sensor_action],
'GL-C-006/GL-C-009': [configurations.light_brightness_colortemp],
'100.424.11': [configurations.light_brightness_colortemp],
'AC0251100NJ': [configurations.sensor_click, configurations.sensor_brightness, configurations.sensor_battery],
'AC0251100NJ': [configurations.sensor_action, configurations.sensor_battery],
'71831': [configurations.light_brightness_colortemp],
'404000/404005/404012': [configurations.light_brightness_colortemp_colorxy],
'404006/404008/404004': [configurations.light_brightness_colortemp],
@@ -586,12 +627,17 @@ const mapping = {
configurations.sensor_illuminance, configurations.sensor_battery,
],
'HGZB-042': [switchWithPostfix('top'), switchWithPostfix('bottom')],
'HGZB-42': [switchWithPostfix('top'), switchWithPostfix('bottom')],
'GL-FL-004TZ': [configurations.light_brightness_colortemp_colorxy],
'IM6001-OTP05': [configurations.switch],
'SV01': [
configurations.cover_position, configurations.sensor_temperature, configurations.sensor_pressure,
configurations.sensor_battery,
],
'SV02': [
configurations.cover_position, configurations.sensor_temperature, configurations.sensor_pressure,
configurations.sensor_battery,
],
'316GLEDRF': [configurations.light_brightness],
'LVS-ZB500D': [configurations.light_brightness],
'ST218': [],
@@ -612,6 +658,14 @@ const mapping = {
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
configurations.sensor_battery, configurations.binary_sensor_battery_low,
],
'NCZ-3041-HA': [
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
configurations.sensor_battery, configurations.binary_sensor_battery_low,
],
'NCZ-3045-HA': [
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
configurations.sensor_battery, configurations.binary_sensor_battery_low,
],
'STS-IRM-250': [
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
configurations.sensor_battery, configurations.binary_sensor_battery_low,
@@ -626,7 +680,10 @@ const mapping = {
],
'IM6001-BTP01': [configurations.sensor_click, configurations.sensor_temperature],
'AV2010/34': [configurations.sensor_click],
'PP-WHT-US': [configurations.switch, configurations.sensor_power],
'PP-WHT-US': [
configurations.switch, configurations.sensor_power,
configurations.sensor_current, configurations.sensor_voltage,
],
'CR701-YZ': [
configurations.binary_sensor_battery_low, configurations.binary_sensor_carbon_monoxide,
configurations.binary_sensor_gas,
@@ -664,7 +721,7 @@ const mapping = {
'LLKZMK11LM': [switchWithPostfix('l1'), switchWithPostfix('l2')],
'LVS-SM10ZW': [configurations.binary_sensor_contact, configurations.binary_sensor_battery_low],
'HS2SK': [configurations.switch, configurations.sensor_power],
'45853GE': [configurations.switch],
'45853GE': [configurations.switch, configurations.sensor_power],
'50064': [configurations.light_brightness_colortemp],
'9290011998B': [configurations.light_brightness_colortemp],
'4096730U7': [configurations.light_brightness_colortemp],
@@ -692,6 +749,57 @@ const mapping = {
'2430-100': [configurations.sensor_action],
'100.425.90': [configurations.switch],
'74580': [configurations.light_brightness],
'HS1CA-E': [
configurations.binary_sensor_carbon_monoxide, configurations.binary_sensor_battery_low,
configurations.sensor_battery,
],
'MCT-340 E': [configurations.binary_sensor_contact],
'D1542': [configurations.light_brightness_colortemp],
'ZGRC-KEY-013': [configurations.sensor_click],
'ZigUP': [configurations.switch],
'YRD256HA20BP': [configurations.sensor_battery, configurations.lock],
'SZ-ESW01-AU': [configurations.sensor_power, configurations.switch],
'PSM-29ZBSR': [configurations.switch],
'ZM350STW1TCF': [configurations.light_brightness_colortemp],
'M350STW1': [configurations.light_brightness],
'A806S-Q1R': [configurations.light_brightness],
'XY12S-15': [configurations.light_brightness_colortemp_colorxy],
'B07KG5KF5R': [configurations.light_brightness_colortemp],
'SCM-S1': [configurations.cover_position],
'HEIMAN-M1': [configurations.binary_sensor_contact],
'3216131P5': [configurations.light_brightness_colortemp],
'ST8AU-CON': [configurations.light_brightness],
'HS3MS': [configurations.binary_sensor_occupancy],
'DIYRUZ_R4_5': [
switchWithPostfix('bottom_left'), switchWithPostfix('bottom_right'), switchWithPostfix('center'),
switchWithPostfix('top_left'), switchWithPostfix('top_right'),
],
'NCZ-3011-HA': [
configurations.binary_sensor_occupancy, configurations.sensor_humidity, configurations.sensor_temperature,
],
'MEAZON_BIZY_PLUG': [configurations.sensor_power, configurations.switch, configurations.sensor_temperature],
'MEAZON_DINRAIL': [configurations.sensor_power, configurations.switch, configurations.sensor_temperature],
'HS1CA-M': [configurations.binary_sensor_carbon_monoxide, configurations.binary_sensor_battery_low],
'7099860PH': [configurations.light_brightness_colorxy],
'HV-GSCXZB269': [configurations.light_brightness_colortemp],
'3216231P5': [configurations.light_brightness_colortemp],
'AC03647': [configurations.light_brightness_colortemp_colorxy],
'12031': [configurations.cover_position],
'421792': [configurations.light_brightness_colortemp_colorxy],
'HGZB-06A': [configurations.light_brightness_colortemp_colorxy],
'LED1733G7': [configurations.light_brightness_colortemp],
'9290011370B': [configurations.light_brightness],
'RB 250 C': [configurations.light_brightness_colortemp_colorxy],
'8718696170625': [configurations.light_brightness],
'GL-G-001Z': [configurations.light_brightness_colortemp_colorxy],
'HV-GSCXZB279_HV-GSCXZB229': [configurations.light_brightness_colortemp],
'HS2WD-E': [configurations.sensor_battery],
'ZNMS12LM': [
configurations.sensor_action, configurations.binary_sensor_lock, configurations.binary_sensor_lock_reverse,
],
'12050': [configurations.switch, configurations.sensor_power],
'ROB_200-004-0': [configurations.light_brightness],
'RH3040': [configurations.sensor_battery, configurations.binary_sensor_occupancy],
};
Object.keys(mapping).forEach((key) => {
@@ -721,10 +829,11 @@ class HomeAssistant {
}
this.discoveryTopic = settings.get().advanced.homeassistant_discovery_topic;
this.statusTopic = settings.get().advanced.homeassistant_status_topic;
}
onMQTTConnected() {
this.mqtt.subscribe('hass/status');
this.mqtt.subscribe(this.statusTopic);
// MQTT discovery of all paired devices on startup.
this.zigbee.getAllClients().forEach((device) => {
@@ -778,7 +887,7 @@ class HomeAssistant {
// Attributes for device registry
payload.device = {
identifiers: `zigbee2mqtt_${entityID}`,
identifiers: [`zigbee2mqtt_${entityID}`],
name: entity.friendlyName,
sw_version: `Zigbee2mqtt ${this.zigbee2mqttVersion}`,
model: `${mappedModel.description} (${mappedModel.model})`,
@@ -880,7 +989,7 @@ class HomeAssistant {
}
onMQTTMessage(topic, message) {
if (!topic === 'hass/status') {
if (!topic === this.statusTopic) {
return false;
}
+64 -19
View File
@@ -1,4 +1,5 @@
const settings = require('../util/settings');
const utils = require('../util/utils');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
class NetworkMap {
@@ -6,9 +7,15 @@ class NetworkMap {
this.zigbee = zigbee;
this.mqtt = mqtt;
this.state = state;
this.lastSeenMap = {};
// Subscribe to topic.
this.topic = `${settings.get().mqtt.base_topic}/bridge/networkmap`;
this.topicRoutes = `${settings.get().mqtt.base_topic}/bridge/networkmap/routes`;
// Bind
this.raw = this.raw.bind(this);
this.graphviz = this.graphviz.bind(this);
// Set supported formats
this.supportedFormats = {
@@ -17,15 +24,23 @@ class NetworkMap {
};
}
onZigbeeMessage(message, device, mappedDevice) {
if (device) {
this.lastSeenMap[device.ieeeAddr] = Date.now();
}
}
onMQTTConnected() {
this.mqtt.subscribe(this.topic);
this.mqtt.subscribe(this.topicRoutes);
}
onMQTTMessage(topic, message) {
message = message.toString();
if (topic === this.topic && this.supportedFormats.hasOwnProperty(message)) {
this.zigbee.networkScan((result)=> {
const includeRoutes = (topic === this.topicRoutes) ? 1: 0;
if ((topic === this.topic || topic === this.topicRoutes)
&& this.supportedFormats.hasOwnProperty(message)) {
this.zigbee.networkScan(includeRoutes, (result)=> {
const converted = this.supportedFormats[message](this.zigbee, result);
this.mqtt.publish(`bridge/networkmap/${message}`, converted, {});
});
@@ -41,19 +56,26 @@ class NetworkMap {
}
graphviz(zigbee, topology) {
const colors = settings.get().map_options.graphviz.colors;
let text = 'digraph G {\nnode[shape=record];\n';
let devStyle = '';
zigbee.getDevices().forEach((device) => {
topology.nodes.forEach((device) => {
const labels = [];
const friendlyDevice = settings.getDevice(device.ieeeAddr);
const friendlyName = friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr;
// Add friendly name
labels.push(friendlyName);
labels.push(`${device.friendlyName}`);
// Add the device type
labels.push(device.type);
// Add the device short network address and scan note (if any)
let scanNote = '';
if (device.scanfailed.includes('lqi')) {
scanNote += ' no lqi';
}
if (device.scanfailed.includes('rtg')) {
scanNote += ' no routes';
}
labels.push(`0x${device.nwkAddr.toString(16)} ${scanNote}`);
// Add the device model
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
@@ -65,30 +87,53 @@ class NetworkMap {
labels.push(zigbeeModel ? zigbeeModel : 'No model information available');
}
// Add the device status (online/offline)
labels.push(device.status);
// Add the device status (online/offline) and last_seen timestamp
let lastSeen = 'unknown';
if (device.type == 'Coordinator') {
lastSeen = utils.formatDate(Date.now(), settings.get().advanced.last_seen,
utils.toLocalISOString(new Date(Date.now())));
} else {
if (this.lastSeenMap[device.ieeeAddr]) {
const lastSeenAgo =
`${new Date(Date.now() - this.lastSeenMap[device.ieeeAddr]).toISOString().substr(11, 8)}s ago`;
lastSeen = utils.formatDate(this.lastSeenMap[device.ieeeAddr],
settings.get().advanced.last_seen, lastSeenAgo);
}
}
labels.push(`${device.status} (${lastSeen})`);
// Shape the record according to device type
if (device.type == 'Coordinator') {
devStyle = 'style="bold"';
devStyle = `style="bold, filled", fillcolor="${colors.fill.coordinator}", ` +
`fontcolor="${colors.font.coordinator}"`;
} else if (device.type == 'Router') {
devStyle = 'style="rounded"';
devStyle = `style="rounded, filled", fillcolor="${colors.fill.router}", ` +
`fontcolor="${colors.font.router}"`;
} else {
devStyle = 'style="rounded, dashed"';
devStyle = `style="rounded, dashed, filled", fillcolor="${colors.fill.enddevice}", `
+ `fontcolor="${colors.font.enddevice}"`;
}
// Add the device with its labels to the graph as a node.
text += ` "${device.ieeeAddr}" [`+devStyle+`, label="{${labels.join('|')}}"];\n`;
/**
* Add an edge between the device and its parent to the graph
* Add an edge between the device and its child to the graph
* NOTE: There are situations where a device is NOT in the topology, this can be e.g.
* due to not responded to the lqi scan. In that case we do not add an edge for this device.
*/
topology.filter((e) => e.ieeeAddr === device.ieeeAddr).forEach((e) => {
const lineStyle = (e.lqi==0) ? `style="dashed", ` : ``;
text += ` "${device.ieeeAddr}" -> "${e.parent}" [`+lineStyle+`label="${e.lqi}"]\n`;
});
topology.links.filter((e) => (e.sourceIeeeAddr === device.ieeeAddr) || (e.SourceNwkAddr === device.nwkAddr))
.forEach((e) => {
const lineStyle = (device.type=='EndDevice') ? 'penwidth=1, '
: (!e.routes.length) ? 'penwidth=0.5, ' : 'penwidth=2, ';
const lineWeight = (!e.routes.length) ? `weight=0, color="${colors.line.inactive}", `
: `weight=1, color="${colors.line.active}", `;
const textRoutes = e.routes.map((r) => `0x${r.toString(16)}`);
const lineLabels = (!e.routes.length) ? `label="${e.lqi}"`
: `label="${e.lqi}\\n[${textRoutes.join(']\\n[')}]"`;
text += ` "${device.ieeeAddr}" -> "${e.targetIeeeAddr}"`;
text += ` [${lineStyle}${lineWeight}${lineLabels}]\n`;
});
});
text += '}';
+1 -1
View File
@@ -1,4 +1,4 @@
const zclId = require('zcl-id');
const zclId = require('zigbee-herdsman/dist/zcl-id');
const OneJanuary2000 = new Date('January 01, 2000 00:00:00').getTime();
+27
View File
@@ -1,6 +1,7 @@
const mqtt = require('mqtt');
const logger = require('./util/logger');
const settings = require('./util/settings');
const fs = require('fs');
class MQTT {
constructor() {
@@ -21,6 +22,32 @@ class MQTT {
},
};
if (mqttSettings.ca) {
logger.debug(`MQTT SSL/TLS: Path to CA certificate = ${mqttSettings.ca}`);
const ca = fs.readFileSync(mqttSettings.ca);
if (ca) {
options.ca = ca;
} else {
logger.error(`Error loading CA certificate for MQTT SSL/TLS configuration.`);
}
}
if (mqttSettings.key && mqttSettings.cert) {
logger.debug(`MQTT SSL/TLS: Path to client key = ${mqttSettings.key}`);
logger.debug(`MQTT SSL/TLS: Path to client certificate = ${mqttSettings.cert}`);
const key = fs.readFileSync(mqttSettings.key);
const cert = fs.readFileSync(mqttSettings.cert);
if (key && cert) {
options.key = key;
options.cert = cert;
} else {
logger.error(`Error loading key and/or certificate for MQTT SSL/TLS client authentication.`);
}
}
if (mqttSettings.user && mqttSettings.password) {
options.username = mqttSettings.user;
options.password = mqttSettings.password;
+1 -1
View File
@@ -10,7 +10,7 @@ const dontCacheProperties = [
'action', 'button', 'button_left', 'button_right', 'click', 'forgotten', 'keyerror',
'step_size', 'transition_time', 'action_color_temperature', 'action_color',
'action_group', 'group_list', 'group_capacity', 'no_occupancy_since',
'step_mode', 'transition_time', 'duration',
'step_mode', 'transition_time', 'duration', 'elapsed',
];
class State {
+38 -13
View File
@@ -16,8 +16,27 @@ const directory = settings.get().advanced.log_directory.replace('%TIMESTAMP%', t
// Make sure that log directoy exsists
fx.mkdirSync(directory);
// Custom level
const levels = {
levels: {
error: 0,
warn: 1,
info: 2,
debug: 3,
trace: 4,
},
colors: {
error: 'red',
warn: 'yellow',
info: 'green',
debug: 'blue',
trace: 'magenta',
},
};
// Create logger
const logger = new winston.Logger({
levels: levels.levels,
transports: [
new winston.transports.File({
filename: path.join(directory, 'log.txt'),
@@ -37,25 +56,31 @@ const logger = new winston.Logger({
],
});
logger.info(`Logging to directory: '${directory}'`);
// Add colors
winston.addColors(levels.colors);
logger.transports.console.level = level;
// Cleanup any old log directory.
if (settings.get().advanced.log_directory.includes('%TIMESTAMP%')) {
const rootDirectory = path.join(directory, '..');
function cleanup() {
if (settings.get().advanced.log_directory.includes('%TIMESTAMP%')) {
const rootDirectory = path.join(directory, '..');
let directories = fs.readdirSync(rootDirectory).map((d) => {
d = path.join(rootDirectory, d);
return {path: d, birth: fs.statSync(d).birthtimeMs};
});
let directories = fs.readdirSync(rootDirectory).map((d) => {
d = path.join(rootDirectory, d);
return {path: d, birth: fs.statSync(d).birthtimeMs};
});
directories.sort((a, b) => b.birth - a.birth);
directories = directories.slice(10, directories.length);
directories.forEach((dir) => {
logger.debug(`Removing old log directory '${dir.path}'`);
rimraf.sync(dir.path);
});
directories.sort((a, b) => b.birth - a.birth);
directories = directories.slice(10, directories.length);
directories.forEach((dir) => {
logger.debug(`Removing old log directory '${dir.path}'`);
rimraf.sync(dir.path);
});
}
}
logger.cleanup = cleanup;
logger.directory = directory;
module.exports = logger;
+46
View File
@@ -6,6 +6,7 @@ const fs = require('./fs');
const onChangeHandlers = [];
const defaults = {
whitelist: [],
ban: [],
permit_join: false,
mqtt: {
@@ -13,6 +14,30 @@ const defaults = {
},
groups: {},
device_options: {},
map_options: {
graphviz: {
colors: {
fill: {
enddevice: '#fff8ce',
coordinator: '#e04e5d',
router: '#4ea3e0',
},
font: {
coordinator: '#ffffff',
router: '#ffffff',
enddevice: '#000000',
},
line: {
active: '#009900',
inactive: '#994444',
},
},
},
},
queue: {
delay: 250,
simultaneously: 5,
},
experimental: {
livolo: false,
// json or attribute
@@ -71,6 +96,11 @@ const defaults = {
* Home Assistant discovery topic
*/
homeassistant_discovery_topic: 'homeassistant',
/**
* Home Assistant status topic
*/
homeassistant_status_topic: 'hass/status',
},
};
@@ -176,6 +206,17 @@ function addDevice(ieeeAddr) {
writeRead();
}
function whitelistDevice(ieeeAddr) {
const settings = getSettings();
if (!settings.whitelist) {
settings.whitelist = [];
}
settings.whitelist.push(ieeeAddr);
writeRead();
}
function banDevice(ieeeAddr) {
const settings = getSettings();
if (!settings.ban) {
@@ -206,6 +247,10 @@ function addGroup(groupName) {
ID = (Number.parseInt(ID) + 1).toString();
}
if (Object.values(settings.groups).find((g) => g.friendly_name === groupName)) {
return false; // Duplicate group names are not allowed
}
settings.groups[ID] = {friendly_name: groupName};
writeRead();
@@ -340,6 +385,7 @@ module.exports = {
getGroup,
getGroups,
getDevices,
whitelistDevice: (ieeeAddr) => whitelistDevice(ieeeAddr),
banDevice: (ieeeAddr) => banDevice(ieeeAddr),
addDevice: (ieeeAddr) => addDevice(ieeeAddr),
removeDevice: (ieeeAddr) => removeDevice(ieeeAddr),
+46 -1
View File
@@ -32,6 +32,17 @@ const postfixes = [
'top_left', 'top_right', 'white', 'rgb', 'system', 'top', 'bottom',
];
function flatten(arr) {
return arr.reduce((flat, toFlatten) => {
return flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten);
}, []);
}
const forceEndDevice = flatten(
['QBKG03LM', 'QBKG04LM']
.map((model) => zigbeeShepherdConverters.devices.find((d) => d.model === model))
.map((mappedModel) => mappedModel.zigbeeModel));
function getEndpointByEntityID(zigbee, entityID, epName) {
const device = zigbee.getDevice(entityID);
if (!device) {
@@ -57,6 +68,9 @@ function getEndpointByEntityID(zigbee, entityID, epName) {
logger.error(`Device ${mappedDevice.model} doesn't have ep named '${epName}'`);
return;
}
} else if (mappedDevice.hasOwnProperty('ep')) {
const eps = mappedDevice.ep(device);
epID = eps[''] || null;
}
const endpoint = zigbee.getEndpoint(entityID, epID);
@@ -89,6 +103,27 @@ function getZigbee2mqttVersion(callback) {
});
}
function formatDate(date, type, _default) {
let result;
switch (type) {
case 'ISO_8601':
result = new Date(date).toISOString();
break;
case 'ISO_8601_local':
result = toLocalISOString(new Date(date));
break;
case 'epoch':
result = date;
break;
default:
result = _default;
break;
}
return result;
}
module.exports = {
millisecondsToSeconds: (milliseconds) => milliseconds / 1000,
secondsToMilliseconds: (seconds) => seconds * 1000,
@@ -97,11 +132,21 @@ module.exports = {
(!device.manufName || !device.manufName.startsWith('Trust'));
},
isIkeaTradfriDevice: (device) => ikeaTradfriManufacturerID.includes(device.manufId),
isRouter: (device) => device.type === 'Router',
isRouter: (device) => device.type === 'Router' && !forceEndDevice.includes(device.modelId),
isBatteryPowered: (device) => device.powerSource && device.powerSource === 'Battery',
isNumeric: (string) => /^\d+$/.test(string),
toLocalISOString: (dDate) => toLocalISOString(dDate),
getPostfixes: () => postfixes,
formatDate: (date, type, _default=null) => formatDate(date, type, _default),
correctDeviceType: (device) => {
if (device) {
if (forceEndDevice.includes(device.modelId)) {
return 'EndDevice';
}
}
return device.type;
},
getEndpointByEntityID,
getZigbee2mqttVersion,
};
+16 -3
View File
@@ -1,6 +1,9 @@
const maxSimultaneouslyRunning = 5;
const delay = 250;
const settings = require('./settings');
const maxSimultaneouslyRunning = settings.get().queue.simultaneously;
const delay = settings.get().queue.delay;
const error17Retries = 3;
const logger = require('./logger');
class ZigbeeQueue {
constructor() {
@@ -8,6 +11,7 @@ class ZigbeeQueue {
this.active = [];
this.timer = null;
this.running = false;
this.ID = 0;
}
start() {
@@ -16,7 +20,13 @@ class ZigbeeQueue {
}
push(entityID, func) {
this.queue.push({entityID, func, attempts: 0});
this.ID++;
this.queue.push({entityID, func, attempts: 0, ID: this.ID});
this.log(`Added new job with ID ${this.ID} for ${entityID}`);
}
log(message) {
logger.trace(`zigbeeQueue: ${message}`);
}
stopTimer() {
@@ -40,6 +50,8 @@ class ZigbeeQueue {
}
handleJobComplete(job, error) {
this.log(`Completed job with ID ${job.ID} for ${job.entityID}${error ? ` with error ${error}` : ''}`);
if (error && error.message === 'rsp error: 17' && job.attempts < error17Retries) {
// Error 17 means that the buffer of the ZNP was full,
// retry this for a maximum of 3 times.
@@ -60,6 +72,7 @@ class ZigbeeQueue {
if (next) {
this.active.push(next);
this.log(`Executing job with ID ${next.ID} for ${next.entityID}`);
next.func((error) => this.handleJobComplete(next, error));
}
}
+2 -2
View File
@@ -3,8 +3,8 @@
* Based on: https://github.com/zigbeer/zapp-cie
*/
const Ziee = require('ziee');
const Zive = require('zive');
const Ziee = require('zigbee-herdsman/dist/ziee');
const Zive = require('zigbee-herdsman/dist/zive');
const cieClusters = new Ziee();
// Direction-Value 1: input, 2: output, 3: input/output
+219 -30
View File
@@ -1,4 +1,4 @@
const ZShepherd = require('zigbee-shepherd');
const ZShepherd = require('zigbee-herdsman');
const logger = require('./util/logger');
const settings = require('./util/settings');
const data = require('./util/data');
@@ -6,7 +6,7 @@ const utils = require('./util/utils');
const ZigbeeQueue = require('./util/zigbeeQueue');
const cieApp = require('./zapp/cie');
const objectAssignDeep = require('object-assign-deep');
const zclId = require('zcl-id');
const zclId = require('zigbee-herdsman/dist/zcl-id');
const advancedSettings = settings.get().advanced;
@@ -37,7 +37,6 @@ const defaultCfg = {
// Don't print network key.
const shepherdSettingsLog = objectAssignDeep.noMutate(shepherdSettings);
shepherdSettingsLog.net.precfgkey = 'HIDDEN';
logger.debug(`Using zigbee-shepherd with settings: '${JSON.stringify(shepherdSettingsLog)}'`);
class Zigbee {
constructor() {
@@ -52,6 +51,8 @@ class Zigbee {
start(messageHandler, callback) {
logger.info(`Starting zigbee-shepherd`);
logger.debug(`Using zigbee-shepherd with settings: '${JSON.stringify(shepherdSettingsLog)}'`);
this.messageHandler = messageHandler;
this.shepherd = new ZShepherd(settings.get().serial.port, shepherdSettings);
@@ -93,9 +94,17 @@ class Zigbee {
this.logStartupInfo();
this.getAllClients().forEach((device) => {
if (settings.get().ban.includes(device.ieeeAddr)) {
logger.warn(`Banned device is connected (${device.ieeeAddr}), removing..`);
this.removeDevice(device.ieeeAddr, false, () => {});
// If set whitelist devices, all other device will be ban or reject to join the network
if (settings.get().whitelist.size>0) {
if (!settings.get().whitelist.includes(device.ieeeAddr)) {
logger.warn(`Blacklist device is connected (${device.ieeeAddr}), removing...`);
this.removeDevice(device.ieeeAddr, true, () => {});
}
} else {
if (settings.get().ban.includes(device.ieeeAddr)) {
logger.warn(`Banned device is connected (${device.ieeeAddr}), removing...`);
this.removeDevice(device.ieeeAddr, false, () => {});
}
}
});
@@ -106,13 +115,23 @@ class Zigbee {
logger.debug(
`Accept device incoming with ieeeAddr '${devInfo.ieeeAddr}' permit join is '${this.getPermitJoin()}'`
);
if (settings.get().ban.includes(devInfo.ieeeAddr)) {
logger.info(`Banned device tried to connect (${devInfo.ieeeAddr})`);
callback(null, false);
// If set whitelist devices, all other device will be ban or reject to join the network
if (settings.get().whitelist.size>0) {
if (settings.get().whitelist.includes(devInfo.ieeeAddr)) {
logger.info(`whitelist device tried to connect (${devInfo.ieeeAddr})`);
callback(null, true);
} else {
logger.debug(`Not allowing device '${devInfo.ieeeAddr}' to join`);
callback(null, false);
}
} else {
logger.debug(`Allowing device '${devInfo.ieeeAddr}' to join`);
callback(null, true);
if (settings.get().ban.includes(devInfo.ieeeAddr)) {
logger.info(`Banned device tried to connect (${devInfo.ieeeAddr})`);
callback(null, false);
} else {
logger.debug(`Allowing device '${devInfo.ieeeAddr}' to join`);
callback(null, true);
}
}
}
@@ -193,10 +212,10 @@ class Zigbee {
if (error) {
logger.error('Failed to reenable joining');
} else {
logger.info('Succesfully reenabled joining');
logger.info('Successfully reenabled joining');
}
});
}, utils.secondsToMilliseconds(200));
}, utils.secondsToMilliseconds(160));
}
this.shepherd.permitJoin(permit ? 255 : 0, (error) => {
@@ -223,12 +242,14 @@ class Zigbee {
settings.banDevice(deviceID);
}
const friendlyName = this.getDeviceFriendlyName(deviceID);
this.shepherd.remove(deviceID, {reJoin: true}, (error) => {
if (error) {
logger.warn(`Failed to remove '${deviceID}', trying force remove...`);
logger.warn(`Failed to remove '${friendlyName}', trying force remove...`);
this.forceRemove(deviceID, callback);
} else {
logger.info(`Removed ${deviceID}`);
logger.info(`Removed ${friendlyName}`);
callback(null);
}
});
@@ -238,8 +259,9 @@ class Zigbee {
const device = this.shepherd._findDevByAddr(deviceID);
if (device) {
const friendlyName = this.getDeviceFriendlyName(deviceID);
return this.shepherd._unregisterDev(device, (error) => {
logger.info(`Force removed ${deviceID}`);
logger.info(`Force removed ${friendlyName}`);
callback(error);
});
} else {
@@ -262,21 +284,180 @@ class Zigbee {
return this.getDevices().find((d) => d.ieeeAddr === ieeeAddr);
}
getDeviceFriendlyName(ieeeAddr) {
const device = settings.getDevice(ieeeAddr);
return device ? device.friendly_name || ieeeAddr : ieeeAddr;
}
getCoordinator() {
const device = this.getDevices().find((d) => d.type === 'Coordinator');
return this.shepherd.find(device.ieeeAddr, 1);
}
getScanable() {
return this.getDevices().filter((d) => d.type != 'EndDevice');
}
getGroup(ID) {
return this.shepherd.getGroup(ID);
}
networkScan(callback) {
logger.info('Starting network scan...');
this.shepherd.lqiScan().then((result) => {
logger.info('Network scan completed');
callback(result);
getGroupFriendlyName(ID) {
let friendlyName = null;
friendlyName = settings.getGroup(ID).friendly_name;
return (friendlyName ? friendlyName : ID);
}
networkScan(includeRoutes, callback) {
logger.info(`Starting network scan includeRoutes='${includeRoutes}'...`);
const lqiScanList = new Set();
const rtgScanList = new Set();
const linkMap = [];
const routeMap = [];
// Gather the lqi and the route info into separate lists and only collate them when done.
const collateMap = () => {
linkMap.sort();
logger.debug(`Link map: %j`, linkMap);
logger.debug(`Route map: %j`, routeMap);
// Merge the routes into the linkMap by matching on 'source|target' link short addresses
linkMap.forEach((link) => {
routeMap.filter((e) => e.key === link.key).forEach((e) => {
link.routes.push(e.destAddr);
});
link.routes.sort(function(a, b) {
return a-b;
});
delete link.key;
});
const networkMap = {nodes: [], links: linkMap};
this.getDevices().forEach((device) => {
const friendlyDevice = settings.getDevice(device.ieeeAddr);
const friendlyName = friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr;
const deviceType = utils.correctDeviceType(device);
const scanfailed = [];
if (lqiScanList.has(device.ieeeAddr)) {
scanfailed.push('lqi');
}
if (rtgScanList.has(device.ieeeAddr)) {
scanfailed.push('rtg');
}
networkMap.nodes.push({ieeeAddr: device.ieeeAddr, friendlyName: friendlyName, type: deviceType,
nwkAddr: device.nwkAddr, manufName: device.manufName, modelId: device.modelId,
status: device.status, scanfailed: scanfailed});
});
// Clear remaining devices so they don't process when/if they eventually complete
lqiScanList.clear();
rtgScanList.clear();
logger.debug(`Merged map: %j`, networkMap);
callback(networkMap);
};
const processLqiResponse = (error, rsp, targetIeeeAddr, targetNwkAddr) => {
if (error) {
logger.warn(`Failed network lqi scan for device: '${targetIeeeAddr}' with error: '${error}'`);
} else {
if (lqiScanList.has(targetIeeeAddr)) {
// Haven't processed this one yet
if (rsp && rsp.status === 0 && rsp.neighborlqilist) {
logger.debug(`lqi scan: '${targetIeeeAddr}' with '${rsp.neighborlqilistcount}' neighbors`);
rsp.neighborlqilist.forEach(function(neighbor) {
// only include active relationships
if (neighbor.relationship <= 3) {
// lqi is measured at receiver so link is from neighbor (source) to scanned router
const key = neighbor.nwkAddr + '|' + targetNwkAddr;
linkMap.push({
key: key, sourceIeeeAddr: neighbor.extAddr, targetIeeeAddr: targetIeeeAddr,
sourceNwkAddr: neighbor.nwkAddr, lqi: neighbor.lqi, depth: neighbor.depth,
relationship: neighbor.relationship, routes: []});
}
});
// Remove from scan list and if both lists are done return the completed network map
lqiScanList.delete(targetIeeeAddr);
if (lqiScanList.size === 0 && rtgScanList.size === 0) {
logger.info('Network scan completed');
collateMap();
} else {
logger.debug(`Outstanding network lqi scans for devices: '${[...lqiScanList].join(' ')}'`);
logger.debug(`Outstanding network rtg scans for devices: '${[...rtgScanList].join(' ')}'`);
}
} else {
logger.warn(`Empty network lqi scan result for: '${targetIeeeAddr}'`);
}
} else {
// This target has already had timeout so don't add to result network map
logger.warn(`Ignoring late network lqi scan result for: '${targetIeeeAddr}'`);
}
}
};
const processRtgResponse = (error, rsp, sourceIeeeAddr, sourceNwkAddr) => {
if (error) {
logger.warn(`Failed network rtg scan for device: '${sourceIeeeAddr}' with error: '${error}'`);
} else {
if (rtgScanList.has(sourceIeeeAddr)) {
// Haven't processed this one yet
if (rsp && rsp.status === 0 && rsp.routingtablelist) {
logger.debug(`rtg scan: '${sourceIeeeAddr}' with '${rsp.routingtablelistcount}' entries`);
rsp.routingtablelist.forEach(function(route) {
if (route.routeStatus === 0) {
const key = sourceNwkAddr + '|' + route.nextHopNwkAddr;
routeMap.push({
key: key, destAddr: route.destNwkAddr});
}
});
// Remove from scan list and if both lists are done return the completed network map
rtgScanList.delete(sourceIeeeAddr);
if (lqiScanList.size === 0 && rtgScanList.size === 0) {
logger.info('Network scan completed');
collateMap();
} else {
logger.debug(`Outstanding network lqi scans for devices: '${[...lqiScanList].join(' ')}'`);
logger.debug(`Outstanding network rtg scans for devices: '${[...rtgScanList].join(' ')}'`);
}
} else {
logger.warn(`Empty network rtg scan result for: '${sourceIeeeAddr}'`);
}
} else {
// This source has already had timeout so don't add to result network map
logger.warn(`Ignoring late network rtg scan result for: '${sourceIeeeAddr}'`);
}
}
};
// Queue up an lqi scan and an rtg scan for coordinator and each router
this.getScanable().forEach((dev) => {
logger.debug(`Queing network scans for device: '${dev.ieeeAddr}'`);
lqiScanList.add(dev.ieeeAddr);
this.queue.push(dev.ieeeAddr, (queueCallback) => {
this.shepherd.controller.request('ZDO', 'mgmtLqiReq', {dstaddr: dev.nwkAddr, startindex: 0},
(error, rsp, ieeeAddr, nwkAddr) => {
processLqiResponse(error, rsp, dev.ieeeAddr, dev.nwkAddr);
queueCallback(error);
});
});
if (includeRoutes) {
rtgScanList.add(dev.ieeeAddr);
this.queue.push(dev.ieeeAddr, (queueCallback) => {
this.shepherd.controller.request('ZDO', 'mgmtRtgReq', {dstaddr: dev.nwkAddr, startindex: 0},
(error, rsp, ieeeAddr, nwkAddr) => {
processRtgResponse(error, rsp, dev.ieeeAddr, dev.nwkAddr);
queueCallback(error);
});
});
}
});
// Wait for all device scans before forcing map with whatever results are already in
setTimeout(() => {
if (lqiScanList.size === 0 && rtgScanList.size === 0) {
logger.info('Network scan timeout no outstanding requests');
} else {
logger.warn(`Network scan timeout, skipping outstanding lqi scans for '${[...lqiScanList].join(' ')}'`);
logger.warn(`Network scan timeout, skipping outstanding rtg scans for '${[...rtgScanList].join(' ')}'`);
collateMap();
}
}, lqiScanList.size * 1000);
}
getEndpoint(ieeeAddr, ep) {
@@ -295,10 +476,13 @@ class Zigbee {
publish(entityID, entityType, cid, cmd, cmdType, zclData, cfg=defaultCfg, ep, callback) {
let entity = null;
let friendlyName = null;
if (entityType === 'device') {
entity = this.getEndpoint(entityID, ep);
friendlyName = this.getDeviceFriendlyName(entityID);
} else if (entityType === 'group') {
entity = this.getGroup(entityID);
friendlyName = this.getGroupFriendlyName(entityID);
}
if (!entity) {
@@ -310,14 +494,15 @@ class Zigbee {
this.queue.push(entityID, (queueCallback) => {
logger.info(
`Zigbee publish to ${entityType} '${entityID}', ${cid} - ${cmd} - ` +
`${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${ep}`
`Zigbee publish to ${entityType} '${friendlyName}', ${cid} - ${cmd} - ` +
`${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${entity.epId}`
);
const callback_ = (error, rsp) => {
if (error) {
logger.error(
`Zigbee publish to ${entityType} '${entityID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} ` +
`Zigbee publish to ${entityType} '${friendlyName}', ${cid} ` +
`- ${cmd} - ${JSON.stringify(zclData)} ` +
`- ${JSON.stringify(cfg)} - ${ep} ` +
`failed with error ${error}`);
}
@@ -340,11 +525,12 @@ class Zigbee {
}
ping(ieeeAddr, errorLogLevel='error', cb, mechanism='default') {
const friendlyName = this.getDeviceFriendlyName(ieeeAddr);
const callback = (error) => {
if (error) {
logger[errorLogLevel](`Failed to ping ${ieeeAddr}`);
logger[errorLogLevel](`Failed to ping '${friendlyName}'`);
} else {
logger.debug(`Successfully pinged ${ieeeAddr}`);
logger.debug(`Successfully pinged '${friendlyName}'`);
}
if (cb) {
@@ -378,7 +564,8 @@ class Zigbee {
}
bind(ep, cluster, target, callback) {
const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
const friendlyName = this.getDeviceFriendlyName(ep.device.ieeeAddr);
const log = ` '${friendlyName}' - ${cluster}`;
target = !target ? this.getCoordinator() : target;
this.queue.push(ep.device.ieeeAddr, (queueCallback) => {
@@ -397,7 +584,8 @@ class Zigbee {
}
unbind(ep, cluster, target, callback) {
const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
const friendlyName = this.getDeviceFriendlyName(ep.device.ieeeAddr);
const log = ` '${friendlyName}' - ${cluster}`;
target = !target ? this.getCoordinator() : target;
this.queue.push(ep.device.ieeeAddr, (queueCallback) => {
@@ -425,6 +613,7 @@ class Zigbee {
* change the minimum amount of change before sending a report
*/
report(ep, cluster, attributes) {
const friendlyName = this.getDeviceFriendlyName(ep.device.ieeeAddr);
const cfgArr = attributes.map((attribute) => {
const attrId = zclId.attr(cluster, attribute.attr).value;
const dataType = zclId.attrType(cluster, attribute.attr).value;
@@ -438,7 +627,7 @@ class Zigbee {
};
});
const log=`for ${ep.device.ieeeAddr} - ${cluster} - ${attributes.length}`;
const log=`for '${friendlyName}' - ${cluster} - ${attributes.length}`;
const configReport = () => {
this.queue.push(ep.device.ieeeAddr, (queueCallback) => {
+6737 -1296
View File
File diff suppressed because it is too large Load Diff
+4 -7
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "1.4.0",
"version": "1.5.1",
"description": "Zigbee to MQTT bridge using zigbee-shepherd",
"main": "index.js",
"repository": {
@@ -43,12 +43,9 @@
"rimraf": "*",
"semver": "*",
"winston": "2.4.2",
"zcl-id": "git+https://github.com/Koenkk/zcl-id.git#4fa75c92424cd070a3bf6e1f4640b1e3ad8f802d",
"ziee": "*",
"zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#566cba295119cb334a7a7d939b4741e59415ed80",
"zigbee-shepherd-converters": "9.0.5",
"deep-diff": "*",
"zive": "*"
"zigbee-herdsman": "0.1.17",
"zigbee-shepherd-converters": "10.1.4",
"deep-diff": "*"
},
"devDependencies": {
"@types/jest": "^24.0.11",
+10
View File
@@ -0,0 +1,10 @@
const fs = require('fs');
const filename = process.argv[2];
const text = fs.readFileSync(filename, 'utf8');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
for (const line of text.split('\n')) {
const model = zigbeeShepherdConverters.devices.find((d) => d.model === line);
console.log(`- \`${line}\` ${model.vendor} ${model.description}`);
}
+34
View File
@@ -4,6 +4,7 @@ const fs = require('../lib/util/fs');
const objectAssignDeep = require('object-assign-deep');
const data = require('../lib/util/data');
const configurationFile = data.joinPath('configuration.yaml');
const utils = require('./utils');
const mqtt = {
subscribe: (topic) => {},
@@ -17,6 +18,7 @@ describe('BridgeConfig', () => {
const files = new Map();
beforeAll(() => {
utils.stubLogger(jest);
jest.spyOn(fs, 'readYaml').mockImplementation((file) => {
if (files.has(file)) return objectAssignDeep.noMutate(files.get(file));
throw new Error(`Fake file not found: ${file}`);
@@ -137,4 +139,36 @@ describe('BridgeConfig', () => {
bridgeConfig.onMQTTMessage('zigbee2mqtt/bridge/config/groups', 'whatever');
});
it('Rename device with é', async () => {
jest.spyOn(mqtt, 'log').mockImplementation((type, message) => {
expect(type).toBe('device_renamed');
});
write(configurationFile, {
devices: {
'0x12345678': {
friendly_name: 'test123',
retain: false,
},
},
});
const payload = {
old: 'test123',
new: 'tést123',
};
bridgeConfig.onMQTTMessage('zigbee2mqtt/bridge/config/rename', JSON.stringify(payload));
const expected = {
devices: {
'0x12345678': {
friendly_name: 'tést123',
retain: false,
},
},
};
expect(read(configurationFile)).toStrictEqual(expected);
});
});
+38 -6
View File
@@ -45,6 +45,7 @@ describe('Controller', () => {
},
advanced: {
cache_state: false,
last_seen: 'disable',
},
experimental: {
output: 'json',
@@ -91,13 +92,27 @@ describe('Controller', () => {
},
});
const payload = {temperature: 1, humidity: 2};
const payload = {
temperature: 1,
humidity: 2,
state: 'ON',
allowedStates: ['ON', 'OFF'],
color: {r: 100, g: 0, b: 102, a: 0},
nested: {
state: 'OFF',
color: {r: 1, g: 0, b: 2},
},
};
controller.publishEntityState('0x12345678', payload);
expect(mqttPublish).toHaveBeenCalledTimes(2);
expect(mqttPublish.mock.calls[0][0]).toBe('test/temperature');
expect(mqttPublish.mock.calls[0][1]).toBe('1');
expect(mqttPublish.mock.calls[1][0]).toBe('test/humidity');
expect(mqttPublish.mock.calls[1][1]).toBe('2');
expect(mqttPublish.mock.calls.map((x) => [x[0], x[1]])).toEqual([
['test/temperature', '1'],
['test/humidity', '2'],
['test/state', 'ON'],
['test/allowedStates', 'ON,OFF'],
['test/color', '100,0,102'],
['test/nested-state', 'OFF'],
['test/nested-color', '1,0,2'],
]);
});
it('Should cache state', () => {
@@ -147,5 +162,22 @@ describe('Controller', () => {
expect(JSON.parse(mqttPublish.mock.calls[1][1])).toStrictEqual({humidity: 2});
expect(JSON.parse(mqttPublish.mock.calls[2][1])).toStrictEqual({temperature: 3});
});
it('Should not send empty messages', () => {
jest.spyOn(settings, 'get').mockReturnValue({
mqtt: {
include_device_information: false,
},
advanced: {
cache_state: true,
},
experimental: {
output: 'json',
},
});
controller.publishEntityState('0x12345678', {});
expect(mqttPublish).toHaveBeenCalledTimes(0);
});
});
});
+5
View File
@@ -1,8 +1,13 @@
const data = require('../lib/util/data.js');
const path = require('path');
const utils = require('./utils');
describe('Data', () => {
describe('Get path', () => {
beforeEach(() => {
utils.stubLogger(jest);
});
it('Should return correct path', () => {
const expected = path.normalize(path.join(__dirname, '..', 'data'));
const actual = data.getPath();
+19
View File
@@ -28,6 +28,11 @@ const devices = {
return {inClusterList: [5, 6]};
},
},
occupancy_sensor: {
getSimpleDesc: () => {
return {outClusterList: [5, 6]};
},
},
};
const zigbee = {
@@ -46,12 +51,15 @@ const zigbee = {
return devices.switch_ep2;
} else if (ep == 3 && ID === 'switch_ep3') {
return devices.switch_ep3;
} else if (ep == 2 && ID == 'occupancy_sensor') {
return devices.occupancy_sensor;
}
throw new Error(`No mock for ${ID} and ep ${ep}`);
},
getDevice: (ID) => {
const lookup = {
'occupancy_sensor': 'SML002',
'switch_ep2': 'lumi.sensor_86sw2.es1',
'switch_ep3': 'DNCKAT_S003',
'bulb': 'TRADFRI bulb E27 WS opal 980lm',
@@ -154,6 +162,17 @@ describe('DeviceBind', () => {
expect.any(Function)
);
});
it('Bind default ep when mapped', async () => {
deviceBind.onMQTTMessage('zigbee2mqtt/bridge/bind/occupancy_sensor', 'bulb');
expect(zigbee.bind).toHaveBeenCalledTimes(1);
expect(zigbee.bind).toHaveBeenNthCalledWith(1,
devices.occupancy_sensor,
6,
devices.bulb,
expect.any(Function)
);
});
});
describe('Unbind devices', () => {
+148 -8
View File
@@ -26,6 +26,11 @@ const cfg = {
manufSpec: 0,
disDefaultRsp: 0,
},
disFeedbackRsp: {
manufSpec: 0,
disDefaultRsp: 0,
disFeedbackRsp: true,
},
};
describe('DevicePublish', () => {
@@ -481,7 +486,7 @@ describe('DevicePublish', () => {
'lightingColorCtrl',
'moveToColor',
'functional',
{colorx: 17085, colory: 44000, transtime: 0},
{colorx: 17806, colory: 43155, transtime: 0},
cfg.default,
null,
expect.any(Function));
@@ -490,7 +495,7 @@ describe('DevicePublish', () => {
expect(publishEntityState).toHaveBeenCalledTimes(1);
expect(publishEntityState).toHaveBeenNthCalledWith(1,
'0x00000016',
{color: {x: 0.2607, y: 0.6714}});
{color: {x: 0.2717, y: 0.6585}});
});
it('Should publish messages to zigbee devices with color rgb string', async () => {
@@ -505,7 +510,7 @@ describe('DevicePublish', () => {
'lightingColorCtrl',
'moveToColor',
'functional',
{colorx: 17085, colory: 44000, transtime: 0},
{colorx: 17806, colory: 43155, transtime: 0},
cfg.default,
null,
expect.any(Function));
@@ -514,7 +519,7 @@ describe('DevicePublish', () => {
expect(publishEntityState).toHaveBeenCalledTimes(1);
expect(publishEntityState).toHaveBeenNthCalledWith(1,
'0x00000017',
{color: {x: 0.2607, y: 0.6714}});
{color: {x: 0.2717, y: 0.6585}});
}
);
@@ -1081,7 +1086,11 @@ describe('DevicePublish', () => {
it('Home Assistant: should set state', async () => {
zigbee.publish.mockClear();
publishEntityState.mockClear();
jest.spyOn(settings, 'get').mockReturnValue({homeassistant: true, mqtt: {base_topic: 'zigbee2mqtt'}});
jest.spyOn(settings, 'get').mockReturnValue({
homeassistant: true,
mqtt: {base_topic: 'zigbee2mqtt'},
advanced: {last_seen: 'disable'},
});
zigbee.getDevice = () => ({modelId: 'RB 185 C'});
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({state: 'ON'}));
expect(zigbee.publish).toHaveBeenCalledTimes(1);
@@ -1105,7 +1114,11 @@ describe('DevicePublish', () => {
zigbee.publish.mockClear();
publishEntityState.mockClear();
jest.spyOn(state, 'get').mockReturnValue({state: 'ON'});
jest.spyOn(settings, 'get').mockReturnValue({homeassistant: true, mqtt: {base_topic: 'zigbee2mqtt'}});
jest.spyOn(settings, 'get').mockReturnValue({
homeassistant: true,
mqtt: {base_topic: 'zigbee2mqtt'},
advanced: {last_seen: 'disable'},
});
zigbee.getDevice = () => ({modelId: 'RB 185 C'});
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({state: 'ON', color_temp: 100}));
expect(zigbee.publish).toHaveBeenCalledTimes(1);
@@ -1130,7 +1143,11 @@ describe('DevicePublish', () => {
zigbee.publish.mockClear();
publishEntityState.mockClear();
jest.spyOn(state, 'get').mockReturnValue({state: 'ON'});
jest.spyOn(settings, 'get').mockReturnValue({homeassistant: true, mqtt: {base_topic: 'zigbee2mqtt'}});
jest.spyOn(settings, 'get').mockReturnValue({
homeassistant: true,
mqtt: {base_topic: 'zigbee2mqtt'},
advanced: {last_seen: 'disable'},
});
zigbee.getDevice = () => ({modelId: 'RB 185 C'});
devicePublish.onMQTTMessage(
'zigbee2mqtt/0x00000001/set',
@@ -1158,7 +1175,11 @@ describe('DevicePublish', () => {
zigbee.publish.mockClear();
publishEntityState.mockClear();
jest.spyOn(state, 'get').mockReturnValue({state: 'OFF'});
jest.spyOn(settings, 'get').mockReturnValue({homeassistant: true, mqtt: {base_topic: 'zigbee2mqtt'}});
jest.spyOn(settings, 'get').mockReturnValue({
homeassistant: true,
mqtt: {base_topic: 'zigbee2mqtt'},
advanced: {last_seen: 'disable'},
});
zigbee.getDevice = () => ({modelId: 'RB 185 C'});
devicePublish.onMQTTMessage(
'zigbee2mqtt/0x00000001/set',
@@ -1194,4 +1215,123 @@ describe('DevicePublish', () => {
'0x00000001',
{color: {x: 0.41, y: 0.25}});
});
it('Should publish message with disFeedbackRsp when set', async () => {
zigbee.publish.mockClear();
publishEntityState.mockClear();
zigbee.getDevice = () => ({modelId: 'HDC52EastwindFan'});
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify({brightness: '92'}));
expect(zigbee.publish).toHaveBeenCalledTimes(1);
expect(zigbee.publish).toHaveBeenNthCalledWith(1,
'0x00000003',
'device',
'genLevelCtrl',
'moveToLevelWithOnOff',
'functional',
{level: 92, transtime: 0},
cfg.disFeedbackRsp,
null,
expect.any(Function));
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify({state: 'OFF'}));
expect(zigbee.publish).toHaveBeenCalledTimes(2);
expect(zigbee.publish).toHaveBeenNthCalledWith(2,
'0x00000003',
'device',
'genOnOff',
'off',
'functional',
{},
cfg.disFeedbackRsp,
null,
expect.any(Function));
});
it('Should publish messages to zigbee devices', async () => {
jest.spyOn(settings, 'get').mockReturnValue({
mqtt: {base_topic: 'zigbee2mqtt'},
advanced: {last_seen: 'ISO_8601'},
});
publishEntityState.mockClear();
zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'});
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({brightness: '200'}));
expect(publishEntityState).toHaveBeenCalledTimes(1);
expect(typeof publishEntityState.mock.calls[0][1].last_seen).toBe('string');
});
it('HS2WD-E burglar warning', async () => {
zigbee.publish.mockClear();
publishEntityState.mockClear();
zigbee.getDevice = () => ({modelId: 'WarningDevice'});
const payload = {warning: {duration: 100, mode: 'burglar', strobe: true, level: 'high'}};
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify(payload));
expect(zigbee.publish).toHaveBeenCalledTimes(1);
expect(zigbee.publish).toHaveBeenNthCalledWith(1,
'0x00000003',
'device',
'ssIasWd',
'startWarning',
'functional',
{startwarninginfo: 22, warningduration: 100},
cfg.default,
null,
expect.any(Function));
});
it('HS2WD-E emergency warning', async () => {
zigbee.publish.mockClear();
publishEntityState.mockClear();
zigbee.getDevice = () => ({modelId: 'WarningDevice'});
const payload = {warning: {duration: 10, mode: 'emergency', strobe: false, level: 'very_high'}};
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify(payload));
expect(zigbee.publish).toHaveBeenCalledTimes(1);
expect(zigbee.publish).toHaveBeenNthCalledWith(1,
'0x00000003',
'device',
'ssIasWd',
'startWarning',
'functional',
{startwarninginfo: 51, warningduration: 10},
cfg.default,
null,
expect.any(Function));
});
it('HS2WD-E emergency without level', async () => {
zigbee.publish.mockClear();
publishEntityState.mockClear();
zigbee.getDevice = () => ({modelId: 'WarningDevice'});
const payload = {warning: {duration: 10, mode: 'emergency', strobe: false}};
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify(payload));
expect(zigbee.publish).toHaveBeenCalledTimes(1);
expect(zigbee.publish).toHaveBeenNthCalledWith(1,
'0x00000003',
'device',
'ssIasWd',
'startWarning',
'functional',
{startwarninginfo: 49, warningduration: 10},
cfg.default,
null,
expect.any(Function));
});
it('HS2WD-E wrong payload (should use defaults)', async () => {
zigbee.publish.mockClear();
publishEntityState.mockClear();
zigbee.getDevice = () => ({modelId: 'WarningDevice'});
const payload = {warning: 'wrong'};
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify(payload));
expect(zigbee.publish).toHaveBeenCalledTimes(1);
expect(zigbee.publish).toHaveBeenNthCalledWith(1,
'0x00000003',
'device',
'ssIasWd',
'startWarning',
'functional',
{startwarninginfo: 53, warningduration: 10},
cfg.default,
null,
expect.any(Function));
});
});
+1 -1
View File
@@ -41,7 +41,7 @@ describe('DeviceReceive', () => {
});
it('Should handle a zigbee message and counter it when Home Assistant integration is enabled', () => {
jest.spyOn(settings, 'get').mockReturnValue({homeassistant: true, advanced: {last_seen: 'disabled'}});
jest.spyOn(settings, 'get').mockReturnValue({homeassistant: true, advanced: {last_seen: 'disable'}});
const device = {ieeeAddr: '0x12345678'};
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1);
deviceReceive.onZigbeeMessage(message, device, WXKG11LM);
+109
View File
@@ -1,10 +1,14 @@
const Groups = require('../lib/extension/groups');
const settings = require('../lib/util/settings');
const utils = require('./utils');
let groupExtension = null;
let zigbee = null;
describe('Groups', () => {
beforeEach(() => {
utils.stubLogger(jest);
zigbee = {
publish: jest.fn(),
};
@@ -175,4 +179,109 @@ describe('Groups', () => {
{groupid: '2', groupname: ''}, null, null, expect.any(Function)
);
});
it('Apply group updates add with postfix', async () => {
zigbee.publish.mockClear();
zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'});
zigbee.getEndpoint = (entityID, ep) => ({epId: ep});
const from = {};
const to = {'1': ['0x12345689/right']};
groupExtension.apply(from, to);
expect(zigbee.publish).toHaveBeenCalledTimes(1);
expect(zigbee.publish).toHaveBeenCalledWith(
'0x12345689', 'device', 'genGroups', 'add', 'functional',
{groupid: '1', groupname: ''}, null, 3, expect.any(Function)
);
});
it('Apply group updates add and remove with postfix', async () => {
zigbee.publish.mockClear();
zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'});
zigbee.getEndpoint = (entityID, ep) => ({epId: ep});
const from = {'1': ['0x12345689/right']};
const to = {'1': ['0x12345689'], '2': ['0x12345689/left']};
groupExtension.apply(from, to);
expect(zigbee.publish).toHaveBeenCalledTimes(3);
expect(zigbee.publish).toHaveBeenCalledWith(
'0x12345689', 'device', 'genGroups', 'add', 'functional',
{groupid: '2', groupname: ''}, null, 2, expect.any(Function)
);
expect(zigbee.publish).toHaveBeenCalledWith(
'0x12345689', 'device', 'genGroups', 'remove', 'functional',
{groupid: '1'}, null, 3, expect.any(Function)
);
expect(zigbee.publish).toHaveBeenCalledWith(
'0x12345689', 'device', 'genGroups', 'add', 'functional',
{groupid: '1', groupname: ''}, null, null, expect.any(Function)
);
});
it('Add to group via MQTT', async () => {
zigbee.publish.mockClear();
zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'});
zigbee.getEndpoint = (entityID, ep) => ({epId: ep});
jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue(1);
jest.spyOn(settings, 'getIeeeAddrByFriendlyName').mockReturnValue('0x12345689');
groupExtension.onMQTTMessage('zigbee2mqtt/bridge/group/my_group/add', 'my_switch');
expect(zigbee.publish).toHaveBeenCalledTimes(1);
expect(zigbee.publish).toHaveBeenCalledWith(
'0x12345689', 'device', 'genGroups', 'add', 'functional',
{groupid: '1', groupname: ''}, null, null, expect.any(Function)
);
});
it('Add to group via MQTT with postfix', async () => {
zigbee.publish.mockClear();
zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'});
zigbee.getEndpoint = (entityID, ep) => ({epId: ep});
jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue(1);
jest.spyOn(settings, 'getIeeeAddrByFriendlyName').mockReturnValue('0x12345689');
groupExtension.onMQTTMessage('zigbee2mqtt/bridge/group/my_group/add', 'my_switch/right');
expect(zigbee.publish).toHaveBeenCalledTimes(1);
expect(zigbee.publish).toHaveBeenCalledWith(
'0x12345689', 'device', 'genGroups', 'add', 'functional',
{groupid: '1', groupname: ''}, null, 3, expect.any(Function)
);
});
it('Remove from group via MQTT with postfix', async () => {
zigbee.publish.mockClear();
zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'});
zigbee.getEndpoint = (entityID, ep) => ({epId: ep});
jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue(1);
jest.spyOn(settings, 'getIeeeAddrByFriendlyName').mockReturnValue('0x12345689');
groupExtension.onMQTTMessage('zigbee2mqtt/bridge/group/my_group/remove', 'my_switch/left');
expect(zigbee.publish).toHaveBeenCalledTimes(1);
expect(zigbee.publish).toHaveBeenCalledWith(
'0x12345689', 'device', 'genGroups', 'remove', 'functional',
{groupid: '1'}, null, 2, expect.any(Function)
);
});
it('Remove all group via MQTT', async () => {
zigbee.publish.mockClear();
zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'});
zigbee.getEndpoint = (entityID, ep) => ({epId: ep});
jest.spyOn(settings, 'getIeeeAddrByFriendlyName').mockReturnValue('0x12345689');
groupExtension.onMQTTMessage('zigbee2mqtt/bridge/group/remove_all', 'my_switch');
expect(zigbee.publish).toHaveBeenCalledTimes(1);
expect(zigbee.publish).toHaveBeenCalledWith(
'0x12345689', 'device', 'genGroups', 'removeAll', 'functional',
{}, null, null, expect.any(Function)
);
});
it('Remove all group via MQTT deprecated', async () => {
zigbee.publish.mockClear();
zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'});
zigbee.getEndpoint = (entityID, ep) => ({epId: ep});
jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue(1);
jest.spyOn(settings, 'getIeeeAddrByFriendlyName').mockReturnValue('0x12345689');
groupExtension.onMQTTMessage('zigbee2mqtt/bridge/group/my_group/remove_all', 'my_switch');
expect(zigbee.publish).toHaveBeenCalledTimes(1);
expect(zigbee.publish).toHaveBeenCalledWith(
'0x12345689', 'device', 'genGroups', 'removeAll', 'functional',
{}, null, null, expect.any(Function)
);
});
});
+54 -24
View File
@@ -1,6 +1,7 @@
const devices = require('zigbee-shepherd-converters').devices;
const HomeassistantExtension = require('../lib/extension/homeassistant');
const settings = require('../lib/util/settings');
const utils = require('./utils');
const WSDCGQ11LM = devices.find((d) => d.model === 'WSDCGQ11LM');
const SV01 = devices.find((d) => d.model === 'SV01');
@@ -11,6 +12,8 @@ describe('HomeAssistant extension', () => {
let mqtt = null;
beforeEach(() => {
utils.stubLogger(jest);
mqtt = {
publish: jest.fn(),
};
@@ -53,7 +56,7 @@ describe('HomeAssistant extension', () => {
'name': 'my_device_temperature',
'unique_id': '0x12345678_temperature_zigbee2mqtt',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -77,7 +80,7 @@ describe('HomeAssistant extension', () => {
'name': 'my_device_humidity',
'unique_id': '0x12345678_humidity_zigbee2mqtt',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -101,7 +104,7 @@ describe('HomeAssistant extension', () => {
'name': 'my_device_pressure',
'unique_id': '0x12345678_pressure_zigbee2mqtt',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -125,7 +128,7 @@ describe('HomeAssistant extension', () => {
'name': 'my_device_battery',
'unique_id': '0x12345678_battery_zigbee2mqtt',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -148,7 +151,7 @@ describe('HomeAssistant extension', () => {
'name': 'my_device_linkquality',
'unique_id': '0x12345678_linkquality_zigbee2mqtt',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -185,7 +188,7 @@ describe('HomeAssistant extension', () => {
'name': 'my_device_temperature',
'unique_id': '0x12345678_temperature_zigbee2mqtt',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -209,7 +212,7 @@ describe('HomeAssistant extension', () => {
'name': 'my_device_humidity',
'unique_id': '0x12345678_humidity_zigbee2mqtt',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -233,7 +236,7 @@ describe('HomeAssistant extension', () => {
'name': 'my_device_pressure',
'unique_id': '0x12345678_pressure_zigbee2mqtt',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -257,7 +260,7 @@ describe('HomeAssistant extension', () => {
'name': 'my_device_battery',
'unique_id': '0x12345678_battery_zigbee2mqtt',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -280,7 +283,7 @@ describe('HomeAssistant extension', () => {
'name': 'my_device_linkquality',
'unique_id': '0x12345678_linkquality_zigbee2mqtt',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -323,7 +326,7 @@ describe('HomeAssistant extension', () => {
'expire_after': 90,
'icon': 'mdi:test',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -349,7 +352,7 @@ describe('HomeAssistant extension', () => {
'expire_after': 30,
'icon': 'mdi:test',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -375,7 +378,7 @@ describe('HomeAssistant extension', () => {
'expire_after': 30,
'icon': 'mdi:test',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -401,7 +404,7 @@ describe('HomeAssistant extension', () => {
'expire_after': 30,
'icon': 'mdi:test',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -426,7 +429,7 @@ describe('HomeAssistant extension', () => {
'expire_after': 30,
'icon': 'mdi:test',
'device': {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -460,7 +463,7 @@ describe('HomeAssistant extension', () => {
unique_id: '0x12345678_fan_zigbee2mqtt',
speeds: ['off', 'low', 'medium', 'high', 'on', 'auto', 'smart'],
device: {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'manufacturer': 'Hampton Bay',
@@ -493,7 +496,7 @@ describe('HomeAssistant extension', () => {
value_template: '{{ value_json.position }}',
unique_id: '0x12345678_cover_zigbee2mqtt',
device: {
'identifiers': 'zigbee2mqtt_0x12345678',
'identifiers': ['zigbee2mqtt_0x12345678'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Smart vent (SV01)',
@@ -514,7 +517,7 @@ describe('HomeAssistant extension', () => {
friendly_name: 'my_device',
homeassistant: {
device: {
identifiers: 'test123',
identifiers: ['test123'],
},
temperature: {
expire_after: 90,
@@ -536,7 +539,7 @@ describe('HomeAssistant extension', () => {
'unique_id': '0x12345678_temperature_zigbee2mqtt',
'expire_after': 90,
'device': {
'identifiers': 'test123',
'identifiers': ['test123'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -559,7 +562,7 @@ describe('HomeAssistant extension', () => {
temperature: {
expire_after: 90,
device: {
identifiers: 'test',
identifiers: ['test'],
},
},
},
@@ -579,7 +582,7 @@ describe('HomeAssistant extension', () => {
'unique_id': '0x12345678_temperature_zigbee2mqtt',
'expire_after': 90,
'device': {
'identifiers': 'test',
'identifiers': ['test'],
'name': 'my_device',
'sw_version': 'Zigbee2mqtt test',
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
@@ -603,7 +606,7 @@ describe('HomeAssistant extension', () => {
output: 'json',
},
advanced: {
homeassistant_discovery_topic: 'my_custom_topic',
homeassistant_discovery_topic: 'my_custom_discovery_topic',
},
});
@@ -615,7 +618,7 @@ describe('HomeAssistant extension', () => {
temperature: {
expire_after: 90,
device: {
identifiers: 'test',
identifiers: ['test'],
},
},
},
@@ -624,6 +627,33 @@ describe('HomeAssistant extension', () => {
homeassistant.discover('0x12345678', WSDCGQ11LM, false);
expect(mqtt.publish).toHaveBeenCalledTimes(5);
expect(mqtt.publish.mock.calls[0][4]).toBe('my_custom_topic');
expect(mqtt.publish.mock.calls[0][4]).toBe('my_custom_discovery_topic');
});
it('Should subscribe to custom status topic', () => {
jest.spyOn(settings, 'get').mockReturnValue({
experimental: {
output: 'json',
},
advanced: {
homeassistant_status_topic: 'my_custom_status_topic',
},
});
const zigbee = {
getAllClients: jest.fn().mockReturnValue([]),
};
mqtt = {
subscribe: jest.fn(),
};
homeassistant = new HomeassistantExtension(zigbee, mqtt, null, null);
homeassistant.onMQTTConnected();
expect(mqtt.subscribe).toHaveBeenCalledTimes(1);
expect(mqtt.subscribe.mock.calls[0][0]).toBe('my_custom_status_topic');
});
});
+33
View File
@@ -3,6 +3,7 @@ const objectAssignDeep = require('object-assign-deep');
const data = require('../lib/util/data');
const settings = require('../lib/util/settings.js');
const fs = require('../lib/util/fs');
const utils = require('./utils');
const configurationFile = data.joinPath('configuration.yaml');
const devicesFile = data.joinPath('devices.yaml');
@@ -16,6 +17,8 @@ describe('Settings', () => {
const files = new Map();
beforeAll(() => {
utils.stubLogger(jest);
jest.spyOn(fs, 'readYaml').mockImplementation((file) => {
if (files.has(file)) return objectAssignDeep.noMutate(files.get(file));
throw new Error(`Fake file not found: ${file}`);
@@ -285,5 +288,35 @@ describe('Settings', () => {
expect(settings.getDevice('0x1234')).toStrictEqual(expectedDevice2);
});
it('Should add groups', () => {
write(configurationFile, {});
const added = settings.addGroup('test123');
const expected = {
'1': {
friendly_name: 'test123',
},
};
expect(added).toStrictEqual(true);
expect(settings.get().groups).toStrictEqual(expected);
});
it('Should not add duplicate groups', () => {
write(configurationFile, {});
const added = settings.addGroup('test123');
const added2 = settings.addGroup('test123');
const expected = {
'1': {
friendly_name: 'test123',
},
};
expect(added).toStrictEqual(true);
expect(added2).toStrictEqual(false);
expect(settings.get().groups).toStrictEqual(expected);
});
});
});
+1
View File
@@ -6,6 +6,7 @@ module.exports = {
jest.spyOn(logger, 'warn').mockReturnValue(undefined);
jest.spyOn(logger, 'debug').mockReturnValue(undefined);
jest.spyOn(logger, 'error').mockReturnValue(undefined);
jest.spyOn(logger, 'trace').mockReturnValue(undefined);
},
zigbeeMessage: (device, cid, type, data, epId, groupid=0) => {
return {data: {cid, data}, type, groupid, endpoints: [{device, epId}]};
+58
View File
@@ -1,6 +1,11 @@
const utils = require('../lib/util/utils.js');
const testUtils = require('./utils');
describe('Utils', () => {
beforeAll(() => {
testUtils.stubLogger(jest);
});
describe('Is xiaomi device', () => {
it('Identify xiaomi device', () => {
const device = {type: 'Router', manufId: 4151, manufName: 'Xiaomi'};
@@ -16,5 +21,58 @@ describe('Utils', () => {
const device = {type: 'Router', manufId: 4151, manufName: 'Trust International B.V.\u0000'};
expect(false).toBe(utils.isXiaomiDevice(device));
});
it('Identify QBKG03LM as enddevice', () => {
const device = {type: 'Router', manufId: 4447, modelId: 'lumi.ctrl_neutral1'};
expect(false).toBe(utils.isRouter(device));
expect('EndDevice').toBe(utils.correctDeviceType(device));
});
it('Identify QBKG04LM as enddevice', () => {
const device = {type: 'Router', manufId: 4447, modelId: 'lumi.ctrl_neutral2'};
expect(false).toBe(utils.isRouter(device));
expect('EndDevice').toBe(utils.correctDeviceType(device));
});
});
describe('Get endpoint by id', () => {
it('Pick default ep', () => {
const zigbee = {
getDevice: (entityID) => {
return {modelId: 'TRADFRI on/off switch'};
},
getEndpoint: (entityID, epId) => {
return {epId: epId == null ? 1 : 0};
},
};
const endpoint = utils.getEndpointByEntityID(zigbee, '0x12345678', null);
expect(endpoint.epId).toBe(1);
});
it('Pick default ep from mapping when default defined', () => {
const zigbee = {
getDevice: (entityID) => {
return {modelId: 'SML002'};
},
getEndpoint: (entityID, epId) => {
return {epId};
},
};
const endpoint = utils.getEndpointByEntityID(zigbee, '0x12345678', null);
expect(endpoint.epId).toBe(2);
});
it('Pick default ep from mapping when not defined', () => {
const zigbee = {
getDevice: (entityID) => {
return {modelId: 'lumi.sensor_86sw2.es1'};
},
getEndpoint: (entityID, epId) => {
return {epId: epId == null ? 1 : 0};
},
};
const endpoint = utils.getEndpointByEntityID(zigbee, '0x12345678', null);
expect(endpoint.epId).toBe(1);
});
});
});