mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 23:28:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31b8352f5a | ||
|
|
503fef807c | ||
|
|
4aab81d9e3 | ||
|
|
8d705ade39 | ||
|
|
d3e97953c3 | ||
|
|
1b859c4dc2 | ||
|
|
225919bb23 | ||
|
|
74b8340afb | ||
|
|
08c0f2b614 | ||
|
|
61b54c50a5 | ||
|
|
6400f52a28 | ||
|
|
97f9f83181 | ||
|
|
90c3fc0320 | ||
|
|
94a7cd8ec9 | ||
|
|
1df43bdbc4 | ||
|
|
3daf97283c | ||
|
|
aca0c39291 | ||
|
|
9624384c3f | ||
|
|
3b4ee3bc2e | ||
|
|
7a19c8876a | ||
|
|
51df6b7e8a | ||
|
|
7cabe3a523 | ||
|
|
7b2522f712 | ||
|
|
5a132ad34b | ||
|
|
362b910e48 | ||
|
|
241b300f7b | ||
|
|
0e9931af6c | ||
|
|
f4f297e8a2 | ||
|
|
5934efa41e | ||
|
|
38a1aec823 | ||
|
|
3c398df658 | ||
|
|
91d415404c | ||
|
|
ef01346776 | ||
|
|
801e97143c | ||
|
|
304b2efa48 | ||
|
|
a36ed2032d | ||
|
|
17d01e937e | ||
|
|
110f80ec84 | ||
|
|
b28da0265e | ||
|
|
1c529c4873 | ||
|
|
3981c1c6e4 | ||
|
|
c48f73e36b | ||
|
|
94e03d4189 | ||
|
|
e30ff9e722 | ||
|
|
1f63a32170 | ||
|
|
e9b5a588cc | ||
|
|
4357064203 | ||
|
|
86a65ebec0 | ||
|
|
259ba9d658 | ||
|
|
9fe76c29ef | ||
|
|
dd49a76e5c | ||
|
|
6976f8e70a | ||
|
|
82dc8437a1 | ||
|
|
fb12d15d9d | ||
|
|
852950fc09 | ||
|
|
b663e37fd9 | ||
|
|
70fbe714e3 | ||
|
|
ee8161b807 | ||
|
|
8ad6aee43a | ||
|
|
4497214588 | ||
|
|
08e55e469f | ||
|
|
c29bfe1455 | ||
|
|
82ed260d47 | ||
|
|
caff94559e | ||
|
|
c2b4d63e12 | ||
|
|
825f096434 | ||
|
|
4b14222bda | ||
|
|
927c4dbc05 | ||
|
|
c4e9ea208a | ||
|
|
af09624cff | ||
|
|
e4a50b662d | ||
|
|
22b64d09f2 | ||
|
|
dc028d5046 | ||
|
|
0352a738a7 | ||
|
|
6adfdacb3c | ||
|
|
18913a1577 | ||
|
|
1e04c229dd | ||
|
|
31d21546b9 | ||
|
|
7f9104367b | ||
|
|
92d0002bbb | ||
|
|
0074c32136 | ||
|
|
faa6aa41a9 | ||
|
|
3ed9bbde1a | ||
|
|
8f39ae9d63 | ||
|
|
a09da4fe10 | ||
|
|
9cc82701c1 | ||
|
|
190dda5104 | ||
|
|
3d74a6e031 |
+2
-1
@@ -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"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
github: [koenkk]
|
||||
custom: https://www.paypal.me/koenkk
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+58
-16
@@ -1,4 +1,5 @@
|
||||
const settings = require('../util/settings');
|
||||
const utils = require('../util/utils');
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
|
||||
class NetworkMap {
|
||||
@@ -6,10 +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`;
|
||||
|
||||
// Bind
|
||||
this.raw = this.raw.bind(this);
|
||||
this.graphviz = this.graphviz.bind(this);
|
||||
|
||||
// Set supported formats
|
||||
this.supportedFormats = {
|
||||
'raw': this.raw,
|
||||
@@ -17,6 +23,12 @@ class NetworkMap {
|
||||
};
|
||||
}
|
||||
|
||||
onZigbeeMessage(message, device, mappedDevice) {
|
||||
if (device) {
|
||||
this.lastSeenMap[device.ieeeAddr] = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(this.topic);
|
||||
}
|
||||
@@ -41,19 +53,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 +84,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,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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
+215
-28
@@ -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,178 @@ 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);
|
||||
}
|
||||
|
||||
getGroupFriendlyName(ID) {
|
||||
let friendlyName = null;
|
||||
friendlyName = settings.getGroup(ID).friendly_name;
|
||||
return (friendlyName ? friendlyName : ID);
|
||||
}
|
||||
|
||||
networkScan(callback) {
|
||||
logger.info('Starting network scan...');
|
||||
this.shepherd.lqiScan().then((result) => {
|
||||
logger.info('Network scan completed');
|
||||
callback(result);
|
||||
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 === 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);
|
||||
});
|
||||
});
|
||||
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 +474,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 +492,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 +523,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 +562,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 +582,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 +611,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 +625,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) => {
|
||||
|
||||
Generated
+6730
-1292
File diff suppressed because it is too large
Load Diff
+4
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.0",
|
||||
"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.16",
|
||||
"zigbee-shepherd-converters": "10.1.4",
|
||||
"deep-diff": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^24.0.11",
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}]};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user