mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 07:08:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55e628398b | ||
|
|
835bba5414 | ||
|
|
ce62f43edf | ||
|
|
7c0e1b0c65 | ||
|
|
4a6a88e0e8 | ||
|
|
89f1c709aa | ||
|
|
785601476e | ||
|
|
134712322f | ||
|
|
d8ec59bd9f | ||
|
|
48641f3028 | ||
|
|
265d5c4877 | ||
|
|
63ecc1501d | ||
|
|
f0f7905782 | ||
|
|
d073a7fc7a | ||
|
|
4b0e427e6c | ||
|
|
8d70ebd91a | ||
|
|
99acd3b50f | ||
|
|
f0ca71648b | ||
|
|
d22e73ebb4 | ||
|
|
e1f9a3910d | ||
|
|
c0ed4aa9cc | ||
|
|
a8793ab60b | ||
|
|
11fd5ffde6 | ||
|
|
d00a1fed30 | ||
|
|
9d6c7b2614 | ||
|
|
2177b33712 | ||
|
|
4a771a75df | ||
|
|
c9298c5606 | ||
|
|
07dc837b76 | ||
|
|
67151fba04 | ||
|
|
a56c5cfe50 | ||
|
|
383561cbc4 | ||
|
|
d193673917 | ||
|
|
62254bb67a | ||
|
|
85b57cd639 | ||
|
|
838cb6e702 | ||
|
|
38acbb0234 | ||
|
|
74aefcca76 | ||
|
|
5c3cd99288 | ||
|
|
121eaf3dcb | ||
|
|
22126a3e5d | ||
|
|
eab9886402 | ||
|
|
bab71fb5ea | ||
|
|
29762b2720 | ||
|
|
edab607f7d | ||
|
|
0aedbe0db3 | ||
|
|
bd5cb7790a | ||
|
|
a2d09b08ea | ||
|
|
0cc834b4b6 | ||
|
|
10a47df33b | ||
|
|
9da1a35bbd | ||
|
|
cc008db2ad | ||
|
|
ba0d930ddd | ||
|
|
b8b4ff9ee7 | ||
|
|
1573687979 | ||
|
|
2ce2876f04 | ||
|
|
e1ec16042f | ||
|
|
911cd58ef2 | ||
|
|
4cc0e5d88e | ||
|
|
2dcc74f9c6 | ||
|
|
4f81b7aa0b | ||
|
|
2d66145c6e | ||
|
|
7824f102c8 | ||
|
|
4b6add19d5 | ||
|
|
637ad8d2e6 | ||
|
|
6857f2ad5e | ||
|
|
272892b112 | ||
|
|
2c2af0603b | ||
|
|
3f058c317c |
+28
-12
@@ -1,23 +1,38 @@
|
||||
<!--
|
||||
Before submitting an issue make sure you've searched for a similar issue and read the documentation: https://www.zigbee2mqtt.io/.
|
||||
|
||||
Rules (don't ignore these, your issue will be closed without further notice):
|
||||
- English only
|
||||
- Make sure you are on the latest Zigbee2mqtt version
|
||||
- Provide a clear description of the problem
|
||||
- DON'T copy logs directly here, post a link to https://hastebin.com/ or https://pastebin.com/.
|
||||
- Make sure you are running the latest firmware https://github.com/koenkk/z-stack-firmware.
|
||||
|
||||
Did you read the FAQ?
|
||||
https://www.zigbee2mqtt.io/information/FAQ.html
|
||||
- https://www.zigbee2mqtt.io/information/FAQ.html
|
||||
|
||||
Zigbee2mqtt fails to start?
|
||||
- https://www.zigbee2mqtt.io/information/FAQ.html#help-zigbee2mqtt-fails-to-start
|
||||
|
||||
Having issues when using a CC2531?
|
||||
- Make sure the CC2531 is connected through a USB extension cable
|
||||
- Try the source routing firmware: https://github.com/Koenkk/Z-Stack-firmware/tree/master/coordinator/Z-Stack_Home_1.2/bin/source_routing
|
||||
- With larger networks (30/40+) devices, your CC2531 might not be powerful enough. This will cause weird issues, in this case it's advised to use a more powerful adapter: https://www.zigbee2mqtt.io/information/supported_adapters.html#texas-instruments-cc26x2r1
|
||||
|
||||
Unsupported device?
|
||||
https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html
|
||||
- https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html
|
||||
|
||||
A question?
|
||||
Read the documentation: https://www.zigbee2mqtt.io/
|
||||
Device does not pair or interview fails?
|
||||
- https://www.zigbee2mqtt.io/information/FAQ.html#why-does-my-device-not-or-fail-to-pair
|
||||
|
||||
Bug report:
|
||||
- First make sure you are on the latest version of zigbee2mqtt.
|
||||
- Make sure you are running the latest CC253X firmware from https://github.com/koenkk/z-stack-firmware.
|
||||
- Provide a clear description of the problem.
|
||||
Bug report?
|
||||
- If applicable, provide steps how to reproduce the problem.
|
||||
- Provide a log with `log_level` set to `debug`, see https://www.zigbee2mqtt.io/configuration/configuration.html
|
||||
- DON'T copy logs directly here, post a link to https://hastebin.com/ or https://pastebin.com/.
|
||||
- Provide the herdsman debug logging: https://www.zigbee2mqtt.io/information/debug.html#zigbee-herdsman-debug-logging
|
||||
|
||||
-->
|
||||
|
||||
<!--Start your bug report below this line-->
|
||||
|
||||
# Bug Report
|
||||
## What happened
|
||||
|
||||
@@ -26,5 +41,6 @@ Bug report:
|
||||
## How to reproduce it (minimal and precise)
|
||||
|
||||
## Debug Info
|
||||
zigbee2mqtt version:
|
||||
CC253X firmware version:
|
||||
Zigbee2mqtt version:
|
||||
Adapter hardware: CC2531, CC2530, CC26X2R1 or CC1352P-2
|
||||
Adapter firmware version:
|
||||
|
||||
@@ -67,6 +67,7 @@ data-backup/
|
||||
data/coordinator_backup.json
|
||||
data/.storage
|
||||
data/database.db.backup
|
||||
data/extension
|
||||
|
||||
# MacOS indexing file
|
||||
.DS_Store
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
}
|
||||
}
|
||||
+32
-20
@@ -6,21 +6,24 @@ const logger = require('./util/logger');
|
||||
const settings = require('./util/settings');
|
||||
const objectAssignDeep = require('object-assign-deep');
|
||||
const utils = require('./util/utils');
|
||||
const fs = require('fs');
|
||||
const data = require('./util/data');
|
||||
const path = require('path');
|
||||
|
||||
// Extensions
|
||||
const ExtensionEntityPublish = require('./extension/entityPublish');
|
||||
const ExtensionDeviceReceive = require('./extension/deviceReceive');
|
||||
const ExtensionPublish = require('./extension/publish');
|
||||
const ExtensionReceive = require('./extension/receive');
|
||||
const ExtensionNetworkMap = require('./extension/networkMap');
|
||||
const ExtensionSoftReset = require('./extension/softReset');
|
||||
const ExtensionSoftReset = require('./extension/legacy/softReset');
|
||||
const ExtensionHomeAssistant = require('./extension/homeassistant');
|
||||
const ExtensionDeviceConfigure = require('./extension/deviceConfigure');
|
||||
const ExtensionDeviceGroupMembership = require('./extension/deviceGroupMembership');
|
||||
const ExtensionBridgeLegacy = require('./extension/bridgeLegacy');
|
||||
const ExtensionConfigure = require('./extension/configure');
|
||||
const ExtensionDeviceGroupMembership = require('./extension/legacy/deviceGroupMembership');
|
||||
const ExtensionBridgeLegacy = require('./extension/legacy/bridgeLegacy');
|
||||
const ExtensionGroups = require('./extension/groups');
|
||||
const ExtensionDeviceAvailability = require('./extension/deviceAvailability');
|
||||
const ExtensionDeviceBind = require('./extension/deviceBind');
|
||||
const ExtensionDeviceReport = require('./extension/deviceReport');
|
||||
const ExtensionDeviceEvent = require('./extension/deviceEvent');
|
||||
const ExtensionAvailability = require('./extension/availability');
|
||||
const ExtensionBind = require('./extension/bind');
|
||||
const ExtensionReport = require('./extension/report');
|
||||
const ExtensionOnEvent = require('./extension/onEvent');
|
||||
const ExtensionOTAUpdate = require('./extension/otaUpdate');
|
||||
|
||||
class Controller {
|
||||
@@ -36,14 +39,14 @@ class Controller {
|
||||
// Initialize extensions.
|
||||
const args = [this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus];
|
||||
this.extensions = [
|
||||
new ExtensionEntityPublish(...args),
|
||||
new ExtensionDeviceReceive(...args),
|
||||
new ExtensionPublish(...args),
|
||||
new ExtensionReceive(...args),
|
||||
new ExtensionDeviceGroupMembership(...args),
|
||||
new ExtensionDeviceConfigure(...args),
|
||||
new ExtensionConfigure(...args),
|
||||
new ExtensionNetworkMap(...args),
|
||||
new ExtensionGroups(...args),
|
||||
new ExtensionDeviceBind(...args),
|
||||
new ExtensionDeviceEvent(...args),
|
||||
new ExtensionBind(...args),
|
||||
new ExtensionOnEvent(...args),
|
||||
new ExtensionOTAUpdate(...args),
|
||||
];
|
||||
|
||||
@@ -53,7 +56,7 @@ class Controller {
|
||||
}
|
||||
|
||||
if (settings.get().advanced.report) {
|
||||
this.extensions.push(new ExtensionDeviceReport(...args));
|
||||
this.extensions.push(new ExtensionReport(...args));
|
||||
}
|
||||
|
||||
if (settings.get().homeassistant) {
|
||||
@@ -66,7 +69,16 @@ class Controller {
|
||||
}
|
||||
|
||||
if (settings.get().advanced.availability_timeout) {
|
||||
this.extensions.push(new ExtensionDeviceAvailability(...args));
|
||||
this.extensions.push(new ExtensionAvailability(...args));
|
||||
}
|
||||
|
||||
const extensionPath = data.joinPath('extension');
|
||||
if (fs.existsSync(extensionPath)) {
|
||||
const extensions = fs.readdirSync(extensionPath).filter((f) => f.endsWith('.js'));
|
||||
for (const extension of extensions) {
|
||||
const Extension = require(path.join(extensionPath, extension.split('.')[0]));
|
||||
this.extensions.push(new Extension(...args, settings, logger));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +185,7 @@ class Controller {
|
||||
|
||||
async onZigbeeEvent(type, data) {
|
||||
const resolvedEntity = this.zigbee.resolveEntity(data.device || data.ieeeAddr);
|
||||
if (data.device && !resolvedEntity.settings) {
|
||||
if (data.device && !resolvedEntity.settings && data.device.type !== 'Coordinator') {
|
||||
// Only deviceLeave doesn't have a device (not interesting to add to settings)
|
||||
resolvedEntity.settings = settings.addDevice(data.device.ieeeAddr);
|
||||
}
|
||||
@@ -289,8 +301,6 @@ class Controller {
|
||||
resolvedEntity.settings.filtered_attributes.forEach((a) => delete messagePayload[a]);
|
||||
}
|
||||
|
||||
this.eventBus.emit('publishEntityState', {payload: messagePayload, entity: resolvedEntity});
|
||||
|
||||
if (Object.entries(messagePayload).length) {
|
||||
if (settings.get().experimental.output === 'attribute_and_json') {
|
||||
await this.mqtt.publish(resolvedEntity.name, JSON.stringify(messagePayload), options);
|
||||
@@ -304,6 +314,8 @@ class Controller {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.eventBus.emit('publishEntityState', {payload: messagePayload, entity: resolvedEntity});
|
||||
}
|
||||
|
||||
async iteratePayloadAttributeOutput(topicRoot, payload, options) {
|
||||
|
||||
@@ -2,26 +2,21 @@ const logger = require('../util/logger');
|
||||
const settings = require('../util/settings');
|
||||
const utils = require('../util/utils');
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const Extension = require('./extension');
|
||||
|
||||
// Some EndDevices should be pinged
|
||||
// Pingable end devices, some end devices should be pinged
|
||||
// e.g. E11-G13 https://github.com/Koenkk/zigbee2mqtt/issues/775#issuecomment-453683846
|
||||
const pingableModels = [
|
||||
'E11-G13',
|
||||
'53170161',
|
||||
const pingableEndDevices = [
|
||||
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'E11-G13'),
|
||||
zigbeeHerdsmanConverters.devices.find((d) => d.model === '53170161'),
|
||||
];
|
||||
|
||||
const forcedPingable =
|
||||
zigbeeHerdsmanConverters.devices.filter((d) => pingableModels.includes(d.model));
|
||||
|
||||
const toZigbeeCandidates = ['state', 'brightness', 'color', 'color_temp'];
|
||||
|
||||
const Hours25 = 1000 * 60 * 60 * 25;
|
||||
|
||||
/**
|
||||
* This extensions pings devices to check if they are online.
|
||||
*/
|
||||
class DeviceAvailability extends BaseExtension {
|
||||
class Availability extends Extension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
@@ -29,22 +24,14 @@ class DeviceAvailability extends BaseExtension {
|
||||
this.timers = {};
|
||||
this.state = {};
|
||||
|
||||
// Initialize blacklist
|
||||
this.blacklist = settings.get().advanced.availability_blacklist.map((e) => {
|
||||
return settings.getEntity(e).ID;
|
||||
});
|
||||
|
||||
// Initialize whitelist
|
||||
this.whitelist = settings.get().advanced.availability_whitelist.map((e) => {
|
||||
return settings.getEntity(e).ID;
|
||||
});
|
||||
this.blacklist = settings.get().advanced.availability_blacklist.map((e) => settings.getEntity(e).ID);
|
||||
this.whitelist = settings.get().advanced.availability_whitelist.map((e) => settings.getEntity(e).ID);
|
||||
}
|
||||
|
||||
isAllowed(device) {
|
||||
inWhitelistOrNotInBlacklist(device) {
|
||||
const ieeeAddr = device.ieeeAddr;
|
||||
|
||||
const deviceSettings = settings.getDevice(ieeeAddr);
|
||||
const name = deviceSettings.friendly_name;
|
||||
const name = deviceSettings.friendlyName;
|
||||
|
||||
// Whitelist is not empty and device is in it, enable availability
|
||||
if (this.whitelist.length > 0) {
|
||||
@@ -60,15 +47,12 @@ class DeviceAvailability extends BaseExtension {
|
||||
}
|
||||
|
||||
isPingable(device) {
|
||||
// Device is on forcedPingable-list, enable availability
|
||||
if (forcedPingable.find((d) => d.zigbeeModel.includes(device.modelID))) {
|
||||
if (pingableEndDevices.find((d) => d.hasOwnProperty('zigbeeModel') && d.zigbeeModel.includes(device.modelID))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Device is a mains powered router
|
||||
const result = utils.isRouter(device) && !utils.isBatteryPowered(device);
|
||||
|
||||
return result;
|
||||
return utils.isRouter(device) && !utils.isBatteryPowered(device);
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
@@ -76,7 +60,7 @@ class DeviceAvailability extends BaseExtension {
|
||||
// Mark all devices as online on start
|
||||
this.publishAvailability(device, true);
|
||||
|
||||
if (this.isAllowed(device)) {
|
||||
if (this.inWhitelistOrNotInBlacklist(device)) {
|
||||
if (this.isPingable(device)) {
|
||||
this.setTimerPingable(device);
|
||||
} else {
|
||||
@@ -90,21 +74,20 @@ class DeviceAvailability extends BaseExtension {
|
||||
|
||||
async handleIntervalPingable(device) {
|
||||
// When a device is already unavailable, log the ping failed on 'debug' instead of 'error'.
|
||||
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
|
||||
if (!entity) {
|
||||
const resolvedEntity = this.zigbee.resolveEntity(device.ieeeAddr);
|
||||
if (!resolvedEntity) {
|
||||
logger.debug(`Stop pinging '${device.ieeeAddr}', device is not known anymore`);
|
||||
return;
|
||||
}
|
||||
|
||||
const ieeeAddr = device.ieeeAddr;
|
||||
const level = this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] ? 'debug' : 'error';
|
||||
const level = this.state.hasOwnProperty(device.ieeeAddr) && !this.state[device.ieeeAddr] ? 'debug' : 'error';
|
||||
try {
|
||||
await device.ping();
|
||||
this.publishAvailability(device, true);
|
||||
logger.debug(`Successfully pinged '${entity.name}'`);
|
||||
logger.debug(`Successfully pinged '${resolvedEntity.name}'`);
|
||||
} catch (error) {
|
||||
this.publishAvailability(device, false);
|
||||
logger[level](`Failed to ping '${entity.name}'`);
|
||||
logger[level](`Failed to ping '${resolvedEntity.name}'`);
|
||||
} finally {
|
||||
this.setTimerPingable(device);
|
||||
}
|
||||
@@ -112,12 +95,12 @@ class DeviceAvailability extends BaseExtension {
|
||||
|
||||
async handleIntervalNotPingable(device) {
|
||||
const ago = Date.now() - device.lastSeen;
|
||||
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
|
||||
if (!entity || !device.lastSeen) {
|
||||
const resolvedEntity = this.zigbee.resolveEntity(device.ieeeAddr);
|
||||
if (!resolvedEntity || !device.lastSeen) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Non-pingable device '${entity.name}' was last seen '${ago / 1000}' seconds ago.`);
|
||||
logger.debug(`Non-pingable device '${resolvedEntity.name}' was last seen '${ago / 1000}' seconds ago.`);
|
||||
|
||||
if (ago > Hours25) {
|
||||
this.publishAvailability(device, false);
|
||||
@@ -143,23 +126,19 @@ class DeviceAvailability extends BaseExtension {
|
||||
}
|
||||
|
||||
async onReconnect(device) {
|
||||
if (device && device.modelID) {
|
||||
const definition = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
|
||||
if (definition) {
|
||||
const used = [];
|
||||
try {
|
||||
for (const key of toZigbeeCandidates) {
|
||||
const converter = definition.toZigbee.find((tz) => tz.key.includes(key));
|
||||
if (converter && !used.includes(converter)) {
|
||||
await converter.convertGet(device.endpoints[0], key, {});
|
||||
used.push(converter);
|
||||
}
|
||||
const resolvedEntity = this.zigbee.resolveEntity(device);
|
||||
if (resolvedEntity && resolvedEntity.definition) {
|
||||
const used = [];
|
||||
try {
|
||||
for (const key of ['state', 'brightness', 'color', 'color_temp']) {
|
||||
const converter = resolvedEntity.definition.toZigbee.find((tz) => tz.key.includes(key));
|
||||
if (converter && !used.includes(converter)) {
|
||||
await converter.convertGet(device.endpoints[0], key, {});
|
||||
used.push(converter);
|
||||
}
|
||||
} catch (error) {
|
||||
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
|
||||
logger.error(`Failed to read state of '${entity.name}' after reconnect`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to read state of '${resolvedEntity.name}' after reconnect`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,7 +150,7 @@ class DeviceAvailability extends BaseExtension {
|
||||
}
|
||||
|
||||
const deviceSettings = settings.getDevice(ieeeAddr);
|
||||
const name = deviceSettings ? deviceSettings.friendly_name : ieeeAddr;
|
||||
const name = deviceSettings ? deviceSettings.friendlyName : ieeeAddr;
|
||||
const topic = `${name}/availability`;
|
||||
const payload = available ? 'online' : 'offline';
|
||||
if (this.state[ieeeAddr] !== available) {
|
||||
@@ -186,7 +165,7 @@ class DeviceAvailability extends BaseExtension {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isAllowed(device)) {
|
||||
if (this.inWhitelistOrNotInBlacklist(device)) {
|
||||
this.publishAvailability(data.device, true);
|
||||
|
||||
if (this.isPingable(device)) {
|
||||
@@ -212,4 +191,4 @@ class DeviceAvailability extends BaseExtension {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceAvailability;
|
||||
module.exports = Availability;
|
||||
@@ -1,30 +1,46 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const assert = require('assert');
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(bind|unbind)/.+$`);
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(bind|unbind)/.+$`);
|
||||
const Extension = require('./extension');
|
||||
|
||||
const clusters = ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl', 'closuresWindowCovering'];
|
||||
|
||||
// See zigbee-herdsman-converters devices.js
|
||||
const defaultBindGroup = {type: 'group_number', ID: 901};
|
||||
|
||||
class DeviceBind extends BaseExtension {
|
||||
class Bind extends Extension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.legacyApi = settings.get().advanced.legacy_api;
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/bind/#`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/unbind/#`);
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/bind/#`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/unbind/#`);
|
||||
}
|
||||
}
|
||||
|
||||
parseMQTTMessage(topic, message) {
|
||||
let type = null;
|
||||
let sourceKey = null;
|
||||
let targetKey = null;
|
||||
|
||||
if (this.legacyApi && topic.match(legacyTopicRegex)) {
|
||||
topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/`, '');
|
||||
type = topic.split('/')[0];
|
||||
sourceKey = topic.replace(`${type}/`, '');
|
||||
targetKey = message;
|
||||
}
|
||||
|
||||
return {type, sourceKey, targetKey};
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
if (!topic.match(topicRegex)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse topic, retrieve type (bind or unbind) and source
|
||||
topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/`, '');
|
||||
const type = topic.split('/')[0];
|
||||
const sourceKey = topic.replace(`${type}/`, '');
|
||||
const targetKey = message;
|
||||
const {type, sourceKey, targetKey} = this.parseMQTTMessage(topic, message);
|
||||
if (!type) return null;
|
||||
|
||||
// Find source; can only be a device and target
|
||||
const source = this.zigbee.resolveEntity(sourceKey);
|
||||
@@ -64,10 +80,10 @@ class DeviceBind extends BaseExtension {
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const message = {from: sourceName, to: targetName, cluster};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `device_${type}`, message}),
|
||||
JSON.stringify({type: `device_${type}`,
|
||||
message: {from: sourceName, to: targetName, cluster}}),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -78,10 +94,10 @@ class DeviceBind extends BaseExtension {
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const message = {from: sourceName, to: targetName, cluster};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `device_${type}_failed`, message}),
|
||||
JSON.stringify({type: `device_${type}_failed`,
|
||||
message: {from: sourceName, to: targetName, cluster}}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -93,14 +109,13 @@ class DeviceBind extends BaseExtension {
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const message = {from: sourceName, to: targetName};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `device_${type}_failed`, message}),
|
||||
JSON.stringify({type: `device_${type}_failed`, message: {from: sourceName, to: targetName}}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceBind;
|
||||
module.exports = Bind;
|
||||
@@ -1,14 +1,19 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const Extension = require('./extension');
|
||||
|
||||
class DeviceConfigure extends BaseExtension {
|
||||
/**
|
||||
* This extension calls the zigbee-herdsman-converters definition configure() method
|
||||
*/
|
||||
class Configure extends Extension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
this.configuring = new Set();
|
||||
this.attempts = {};
|
||||
this.topic = `${settings.get().mqtt.base_topic}/bridge/configure`;
|
||||
|
||||
this.legacyApi = settings.get().advanced.legacy_api;
|
||||
this.legacyTopic = `${settings.get().mqtt.base_topic}/bridge/configure`;
|
||||
}
|
||||
|
||||
shouldConfigure(resolvedEntity) {
|
||||
@@ -30,26 +35,32 @@ class DeviceConfigure extends BaseExtension {
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(this.topic);
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
this.mqtt.subscribe(this.legacyTopic);
|
||||
}
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
if (topic !== this.topic) {
|
||||
return;
|
||||
}
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
if (topic !== this.legacyTopic) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedEntity = this.zigbee.resolveEntity(message);
|
||||
if (!resolvedEntity || resolvedEntity.type !== 'device') {
|
||||
logger.error(`Device '${message}' does not exist`);
|
||||
return;
|
||||
}
|
||||
const resolvedEntity = this.zigbee.resolveEntity(message);
|
||||
if (!resolvedEntity || resolvedEntity.type !== 'device') {
|
||||
logger.error(`Device '${message}' does not exist`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resolvedEntity.definition || !resolvedEntity.definition.configure) {
|
||||
logger.warn(`Skipping configure of '${resolvedEntity.name}', device does not require this.`);
|
||||
return;
|
||||
}
|
||||
if (!resolvedEntity.definition || !resolvedEntity.definition.configure) {
|
||||
logger.warn(`Skipping configure of '${resolvedEntity.name}', device does not require this.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.configure(resolvedEntity, true);
|
||||
this.configure(resolvedEntity, true);
|
||||
}
|
||||
}
|
||||
|
||||
async onZigbeeStarted() {
|
||||
@@ -106,4 +117,4 @@ class DeviceConfigure extends BaseExtension {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceConfigure;
|
||||
module.exports = Configure;
|
||||
@@ -1,36 +0,0 @@
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
|
||||
class DeviceEvent extends BaseExtension {
|
||||
async onZigbeeStarted() {
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
this.callOnEvent(device, 'start', {});
|
||||
}
|
||||
}
|
||||
|
||||
onZigbeeEvent(type, data, resolvedEntity) {
|
||||
if (data.device && resolvedEntity.definition) {
|
||||
this.callOnEvent(data.device, type, data, resolvedEntity.definition);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
this.callOnEvent(device, 'stop', {});
|
||||
}
|
||||
}
|
||||
|
||||
callOnEvent(device, type, data, definition) {
|
||||
zigbeeHerdsmanConverters.onEvent(type, data, device);
|
||||
|
||||
if (!definition) {
|
||||
definition = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
}
|
||||
|
||||
if (definition && definition.onEvent) {
|
||||
definition.onEvent(type, data, device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceEvent;
|
||||
@@ -1,4 +1,4 @@
|
||||
class BaseExtension {
|
||||
class Extension {
|
||||
/**
|
||||
* Besides intializing variables, the constructor should do nothing!
|
||||
*
|
||||
@@ -49,4 +49,4 @@ class BaseExtension {
|
||||
// stop() {}
|
||||
}
|
||||
|
||||
module.exports = BaseExtension;
|
||||
module.exports = Extension;
|
||||
+89
-69
@@ -1,26 +1,30 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const Extension = require('./extension');
|
||||
const utils = require('../util/utils');
|
||||
const postfixes = utils.getEndpointNames();
|
||||
|
||||
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_all$`);
|
||||
const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(.+)/(remove|add|remove_all)$`);
|
||||
const legacyTopicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/remove_all$`);
|
||||
|
||||
class Groups extends BaseExtension {
|
||||
class Groups extends Extension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.onStateChange = this.onStateChange.bind(this);
|
||||
this.legacyApi = settings.get().advanced.legacy_api;
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/remove_all`);
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/remove_all`);
|
||||
|
||||
for (let step = 1; step < 20; step++) {
|
||||
const topic = `${settings.get().mqtt.base_topic}/bridge/group/${'+/'.repeat(step)}`;
|
||||
this.mqtt.subscribe(`${topic}remove`);
|
||||
this.mqtt.subscribe(`${topic}add`);
|
||||
this.mqtt.subscribe(`${topic}remove_all`); // DEPRECATED
|
||||
for (let step = 1; step < 20; step++) {
|
||||
const topic = `${settings.get().mqtt.base_topic}/bridge/group/${'+/'.repeat(step)}`;
|
||||
this.mqtt.subscribe(`${topic}remove`);
|
||||
this.mqtt.subscribe(`${topic}add`);
|
||||
this.mqtt.subscribe(`${topic}remove_all`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,33 +91,33 @@ class Groups extends BaseExtension {
|
||||
});
|
||||
|
||||
if (Object.keys(payload).length) {
|
||||
const entity = this.zigbee.resolveEntity(data.ID);
|
||||
const resolvedEntity = this.zigbee.resolveEntity(data.ID);
|
||||
const zigbeeGroups = this.zigbee.getGroups().filter((zigbeeGroup) => {
|
||||
const settingsGroup = settings.getGroup(zigbeeGroup.groupID);
|
||||
return settingsGroup && settingsGroup.optimistic;
|
||||
});
|
||||
|
||||
if (entity.type === 'device') {
|
||||
if (resolvedEntity.type === 'device') {
|
||||
for (const zigbeeGroup of zigbeeGroups) {
|
||||
if (zigbeeGroup.hasMember(entity.endpoint)) {
|
||||
if (!payload || payload.state !== 'OFF' || this.allMembersOff(zigbeeGroup)) {
|
||||
if (zigbeeGroup.hasMember(resolvedEntity.endpoint)) {
|
||||
if (!payload || payload.state !== 'OFF' || this.areAllMembersOff(zigbeeGroup)) {
|
||||
await this.publishEntityState(zigbeeGroup.groupID, payload, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const groupIDsToPublish = new Set();
|
||||
for (const member of entity.group.members) {
|
||||
for (const member of resolvedEntity.group.members) {
|
||||
await this.publishEntityState(member.getDevice().ieeeAddr, payload, reason);
|
||||
for (const zigbeeGroup of zigbeeGroups) {
|
||||
if (zigbeeGroup.hasMember(member)) {
|
||||
if (!payload || payload.state !== 'OFF' || this.allMembersOff(zigbeeGroup)) {
|
||||
if (!payload || payload.state !== 'OFF' || this.areAllMembersOff(zigbeeGroup)) {
|
||||
groupIDsToPublish.add(zigbeeGroup.groupID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
groupIDsToPublish.delete(entity.group.groupID);
|
||||
groupIDsToPublish.delete(resolvedEntity.group.groupID);
|
||||
for (const groupID of groupIDsToPublish) {
|
||||
await this.publishEntityState(groupID, payload, reason);
|
||||
}
|
||||
@@ -121,7 +125,7 @@ class Groups extends BaseExtension {
|
||||
}
|
||||
}
|
||||
|
||||
allMembersOff(zigbeeGroup) {
|
||||
areAllMembersOff(zigbeeGroup) {
|
||||
for (const member of zigbeeGroup.members) {
|
||||
const device = member.getDevice();
|
||||
if (this.state.exists(device.ieeeAddr)) {
|
||||
@@ -134,105 +138,121 @@ class Groups extends BaseExtension {
|
||||
return true;
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
let type;
|
||||
let group;
|
||||
const topicMatch = topic.match(topicRegex);
|
||||
if (topicMatch) {
|
||||
group = this.zigbee.resolveEntity(topicMatch[1]);
|
||||
type = topicMatch[2];
|
||||
parseMQTTMessage(topic, message) {
|
||||
let type = null;
|
||||
let resolvedEntityGroup = null;
|
||||
let resolvedEntityDevice = null;
|
||||
let hasEndpointName = null;
|
||||
|
||||
if (!group || group.type !== 'group') {
|
||||
logger.error(`Group '${topicMatch[1]}' does not exist`);
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
const topicMatch = topic.match(legacyTopicRegex);
|
||||
if (topicMatch) {
|
||||
resolvedEntityGroup = this.zigbee.resolveEntity(topicMatch[1]);
|
||||
type = topicMatch[2];
|
||||
|
||||
if (!resolvedEntityGroup || resolvedEntityGroup.type !== 'group') {
|
||||
logger.error(`Group '${topicMatch[1]}' does not exist`);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const payload = {friendly_name: message, group: topicMatch[1], error: 'group doesn\'t exists'};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `device_group_${type}_failed`, message: payload}),
|
||||
);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
} else if (topic.match(legacyTopicRegexRemoveAll)) {
|
||||
type = 'remove_all';
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
|
||||
resolvedEntityDevice = this.zigbee.resolveEntity(message);
|
||||
if (!resolvedEntityDevice || !resolvedEntityDevice.type === 'device') {
|
||||
logger.error(`Device '${message}' does not exist`);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const payload = {friendly_name: message, group: topicMatch[1], error: 'group doesn\'t exists'};
|
||||
const payload = {friendly_name: message, group: topicMatch[1], error: 'entity doesn\'t exists'};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `device_group_${type}_failed`, message: payload}),
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
} else if (topic.match(topicRegexRemoveAll)) {
|
||||
type = 'remove_all';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const entity = this.zigbee.resolveEntity(message);
|
||||
if (!entity || !entity.type === 'device') {
|
||||
logger.error(`Device '${message}' does not exist`);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const payload = {friendly_name: message, group: topicMatch[1], error: 'entity doesn\'t exists'};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `device_group_${type}_failed`, message: payload}),
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
return;
|
||||
hasEndpointName = postfixes.find((p) => message.endsWith(`/${p}`));
|
||||
}
|
||||
|
||||
return {resolvedEntityGroup, resolvedEntityDevice, type, hasEndpointName};
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
const {resolvedEntityGroup, resolvedEntityDevice, type, hasEndpointName} =
|
||||
this.parseMQTTMessage(topic, message);
|
||||
if (!type) return;
|
||||
|
||||
const keys = [
|
||||
`${entity.device.ieeeAddr}/${entity.endpoint.ID}`,
|
||||
`${entity.name}/${entity.endpoint.ID}`,
|
||||
`${resolvedEntityDevice.device.ieeeAddr}/${resolvedEntityDevice.endpoint.ID}`,
|
||||
`${resolvedEntityDevice.name}/${resolvedEntityDevice.endpoint.ID}`,
|
||||
];
|
||||
|
||||
const definition = entity.definition;
|
||||
const endpoints = definition && definition.endpoint ? definition.endpoint(entity.device) : null;
|
||||
const endpointName = endpoints ? Object.entries(endpoints).find((e) => e[1] === entity.endpoint.ID)[0] : null;
|
||||
const definition = resolvedEntityDevice.definition;
|
||||
const endpoints = definition && definition.endpoint ? definition.endpoint(resolvedEntityDevice.device) : null;
|
||||
const endpointName = endpoints ?
|
||||
Object.entries(endpoints).find((e) => e[1] === resolvedEntityDevice.endpoint.ID)[0] : null;
|
||||
|
||||
if (endpointName) {
|
||||
keys.push(`${entity.device.ieeeAddr}/${endpointName}`);
|
||||
keys.push(`${entity.name}/${endpointName}`);
|
||||
keys.push(`${resolvedEntityDevice.device.ieeeAddr}/${endpointName}`);
|
||||
keys.push(`${resolvedEntityDevice.name}/${endpointName}`);
|
||||
}
|
||||
|
||||
const hasEndpointName = postfixes.find((p) => message.endsWith(`/${p}`));
|
||||
if (!hasEndpointName) {
|
||||
keys.push(entity.name);
|
||||
keys.push(entity.device.ieeeAddr);
|
||||
keys.push(resolvedEntityDevice.name);
|
||||
keys.push(resolvedEntityDevice.device.ieeeAddr);
|
||||
}
|
||||
|
||||
if (type === 'add') {
|
||||
logger.info(`Adding '${entity.name}' to '${group.name}'`);
|
||||
await entity.endpoint.addToGroup(group.group);
|
||||
settings.addDeviceToGroup(group.settings.ID, keys);
|
||||
logger.info(`Adding '${resolvedEntityDevice.name}' to '${resolvedEntityGroup.name}'`);
|
||||
await resolvedEntityDevice.endpoint.addToGroup(resolvedEntityGroup.group);
|
||||
settings.addDeviceToGroup(resolvedEntityGroup.settings.ID, keys);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const payload = {friendly_name: entity.name, group: group.name};
|
||||
const payload = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `device_group_add`, message: payload}),
|
||||
);
|
||||
}
|
||||
} else if (type === 'remove') {
|
||||
logger.info(`Removing '${entity.name}' from '${group.name}'`);
|
||||
await entity.endpoint.removeFromGroup(group.group);
|
||||
settings.removeDeviceFromGroup(group.settings.ID, keys);
|
||||
logger.info(`Removing '${resolvedEntityDevice.name}' from '${resolvedEntityGroup.name}'`);
|
||||
await resolvedEntityDevice.endpoint.removeFromGroup(resolvedEntityGroup.group);
|
||||
settings.removeDeviceFromGroup(resolvedEntityGroup.settings.ID, keys);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const payload = {friendly_name: entity.name, group: group.name};
|
||||
const payload = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `device_group_remove`, message: payload}),
|
||||
);
|
||||
}
|
||||
} else { // remove_all
|
||||
logger.info(`Removing '${entity.name}' from all groups`);
|
||||
await entity.endpoint.removeFromAllGroups();
|
||||
logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`);
|
||||
await resolvedEntityDevice.endpoint.removeFromAllGroups();
|
||||
for (const settingsGroup of settings.getGroups()) {
|
||||
settings.removeDeviceFromGroup(settingsGroup.ID, keys);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const payload = {friendly_name: entity.name};
|
||||
const payload = {friendly_name: resolvedEntityDevice.name};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `device_group_remove_all`, message: payload}),
|
||||
|
||||
+277
-191
@@ -2,7 +2,7 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const zigbee2mqttVersion = require('../../package.json').version;
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const Extension = require('./extension');
|
||||
const objectAssignDeep = require(`object-assign-deep`);
|
||||
|
||||
const cfg = {
|
||||
@@ -173,6 +173,24 @@ const cfg = {
|
||||
value_template: '{{ value_json.humidity }}',
|
||||
},
|
||||
},
|
||||
'sensor_eco2': {
|
||||
type: 'sensor',
|
||||
object_id: 'eco2',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'ppm',
|
||||
icon: 'mdi:air-filter',
|
||||
value_template: '{{ value_json.eco2 }}',
|
||||
},
|
||||
},
|
||||
'sensor_voc': {
|
||||
type: 'sensor',
|
||||
object_id: 'voc',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'ppb',
|
||||
icon: 'mdi:air-filter',
|
||||
value_template: '{{ value_json.voc }}',
|
||||
},
|
||||
},
|
||||
'sensor_temperature': {
|
||||
type: 'sensor',
|
||||
object_id: 'temperature',
|
||||
@@ -296,6 +314,14 @@ const cfg = {
|
||||
icon: 'mdi:palette',
|
||||
},
|
||||
},
|
||||
'sensor_action_color_temperature': {
|
||||
type: 'sensor',
|
||||
object_id: 'action_color_temperature',
|
||||
discovery_payload: {
|
||||
value_template: '{{ value_json.action_color_temperature }}',
|
||||
icon: 'hass:thermometer',
|
||||
},
|
||||
},
|
||||
'sensor_brightness': {
|
||||
type: 'sensor',
|
||||
object_id: 'brightness',
|
||||
@@ -573,64 +599,6 @@ const cfg = {
|
||||
},
|
||||
},
|
||||
|
||||
// Thermostat/HVAC
|
||||
'thermostat': (minTemp=7, maxTemp=30, temperatureStateProperty='occupied_heating_setpoint', tempStep=1) => {
|
||||
return {
|
||||
type: 'climate',
|
||||
object_id: 'climate',
|
||||
discovery_payload: {
|
||||
state_topic: false,
|
||||
min_temp: `${minTemp}`,
|
||||
max_temp: `${maxTemp}`,
|
||||
modes: ['off', 'auto', 'heat'],
|
||||
mode_state_topic: true,
|
||||
mode_state_template: '{{ value_json.system_mode }}',
|
||||
mode_command_topic: true,
|
||||
current_temperature_topic: true,
|
||||
current_temperature_template: '{{ value_json.local_temperature }}',
|
||||
temperature_state_topic: true,
|
||||
temperature_state_template: `{{ value_json.${temperatureStateProperty} }}`,
|
||||
temperature_command_topic: temperatureStateProperty,
|
||||
temp_step: tempStep,
|
||||
action_topic: true,
|
||||
action_template: '{{ value_json.operation }}',
|
||||
},
|
||||
};
|
||||
},
|
||||
'thermostat_heatcool': /* istanbul ignore next */ (minTemp=7, maxTemp=30, tempStep=0.5, fanModes=[]) => {
|
||||
return {
|
||||
type: 'climate',
|
||||
object_id: 'climate',
|
||||
discovery_payload: {
|
||||
state_topic: false,
|
||||
unit_of_measurement: '°C',
|
||||
min_temp: `${minTemp}`,
|
||||
max_temp: `${maxTemp}`,
|
||||
modes: ['off', 'auto', 'heat', 'cool'],
|
||||
mode_state_topic: true,
|
||||
mode_state_template: '{{ value_json.system_mode }}',
|
||||
mode_command_topic: true,
|
||||
current_temperature_topic: true,
|
||||
current_temperature_template: '{{ value_json.local_temperature }}',
|
||||
temperature_low_state_topic: true,
|
||||
temperature_low_state_template: `{{ value_json.occupied_heating_setpoint }}`,
|
||||
temperature_high_state_topic: true,
|
||||
temperature_high_state_template: `{{ value_json.occupied_cooling_setpoint }}`,
|
||||
temperature_low_command_topic: 'occupied_heating_setpoint',
|
||||
temperature_high_command_topic: 'occupied_cooling_setpoint',
|
||||
temp_step: tempStep,
|
||||
action_topic: true,
|
||||
action_template:
|
||||
'{% set values = {\'idle\':\'off\',\'heat\':\'heating\',\'cool\':\'cooling\',\'fan only\':\'fan\'}'+
|
||||
' %}{{ values[value_json.running_state] }}',
|
||||
fan_modes: fanModes,
|
||||
fan_mode_command_topic: true,
|
||||
fan_mode_state_topic: true,
|
||||
fan_mode_state_template: '{{ value_json.fan_mode }}',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// Fan
|
||||
'fan': {
|
||||
type: 'fan',
|
||||
@@ -664,29 +632,87 @@ const cfg = {
|
||||
},
|
||||
};
|
||||
|
||||
const switchWithPostfix = (postfix) => {
|
||||
const switchEndpoint = (endpointName) => {
|
||||
return {
|
||||
type: 'switch',
|
||||
object_id: `switch_${postfix}`,
|
||||
object_id: `switch_${endpointName}`,
|
||||
discovery_payload: {
|
||||
payload_off: 'OFF',
|
||||
payload_on: 'ON',
|
||||
value_template: `{{ value_json.state_${postfix} }}`,
|
||||
value_template: `{{ value_json.state_${endpointName} }}`,
|
||||
command_topic: true,
|
||||
command_topic_prefix: postfix,
|
||||
command_topic_prefix: endpointName,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const lightWithPostfix = (configType, postfix) => {
|
||||
const lightEndpoint = (configType, endpointName) => {
|
||||
const config = objectAssignDeep.noMutate(cfg[configType]);
|
||||
config['object_id'] = `light_${postfix}`;
|
||||
config['discovery_payload']['command_topic_prefix'] = postfix;
|
||||
config['discovery_payload']['state_topic_postfix'] = postfix;
|
||||
config['object_id'] = `light_${endpointName}`;
|
||||
config['discovery_payload']['command_topic_prefix'] = endpointName;
|
||||
config['discovery_payload']['state_topic_postfix'] = endpointName;
|
||||
return config;
|
||||
};
|
||||
|
||||
// Map homeassitant configurations to devices.
|
||||
const thermostat = (minTemp=7, maxTemp=30, temperatureStateProperty='occupied_heating_setpoint', tempStep=1) => {
|
||||
return {
|
||||
type: 'climate',
|
||||
object_id: 'climate',
|
||||
discovery_payload: {
|
||||
state_topic: false,
|
||||
min_temp: `${minTemp}`,
|
||||
max_temp: `${maxTemp}`,
|
||||
modes: ['off', 'auto', 'heat'],
|
||||
mode_state_topic: true,
|
||||
mode_state_template: '{{ value_json.system_mode }}',
|
||||
mode_command_topic: true,
|
||||
current_temperature_topic: true,
|
||||
current_temperature_template: '{{ value_json.local_temperature }}',
|
||||
temperature_state_topic: true,
|
||||
temperature_state_template: `{{ value_json.${temperatureStateProperty} }}`,
|
||||
temperature_command_topic: temperatureStateProperty,
|
||||
temp_step: tempStep,
|
||||
action_topic: true,
|
||||
action_template: '{{ value_json.operation }}',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const thermostatHeatCool = /* istanbul ignore next */ (minTemp=7, maxTemp=30, tempStep=0.5, fanModes=[]) => {
|
||||
return {
|
||||
type: 'climate',
|
||||
object_id: 'climate',
|
||||
discovery_payload: {
|
||||
state_topic: false,
|
||||
unit_of_measurement: '°C',
|
||||
min_temp: `${minTemp}`,
|
||||
max_temp: `${maxTemp}`,
|
||||
modes: ['off', 'auto', 'heat', 'cool'],
|
||||
mode_state_topic: true,
|
||||
mode_state_template: '{{ value_json.system_mode }}',
|
||||
mode_command_topic: true,
|
||||
current_temperature_topic: true,
|
||||
current_temperature_template: '{{ value_json.local_temperature }}',
|
||||
temperature_low_state_topic: true,
|
||||
temperature_low_state_template: `{{ value_json.occupied_heating_setpoint }}`,
|
||||
temperature_high_state_topic: true,
|
||||
temperature_high_state_template: `{{ value_json.occupied_cooling_setpoint }}`,
|
||||
temperature_low_command_topic: 'occupied_heating_setpoint',
|
||||
temperature_high_command_topic: 'occupied_cooling_setpoint',
|
||||
temp_step: tempStep,
|
||||
action_topic: true,
|
||||
action_template:
|
||||
'{% set values = {\'idle\':\'off\',\'heat\':\'heating\',\'cool\':\'cooling\',\'fan only\':\'fan\'}'+
|
||||
' %}{{ values[value_json.running_state] }}',
|
||||
fan_modes: fanModes,
|
||||
fan_mode_command_topic: true,
|
||||
fan_mode_state_topic: true,
|
||||
fan_mode_state_template: '{{ value_json.fan_mode }}',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Map Home Assistant configurations to devices.
|
||||
const mapping = {
|
||||
'WXKG01LM': [cfg.sensor_click, cfg.sensor_battery],
|
||||
'WXKG11LM': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action],
|
||||
@@ -695,7 +721,7 @@ const mapping = {
|
||||
'WXKG03LM': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action],
|
||||
'WXKG02LM': [cfg.sensor_click, cfg.sensor_battery],
|
||||
'QBKG04LM': [cfg.switch, cfg.sensor_click, cfg.sensor_action],
|
||||
'QBKG03LM': [switchWithPostfix('left'), switchWithPostfix('right'), cfg.sensor_click, cfg.sensor_temperature],
|
||||
'QBKG03LM': [switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_click, cfg.sensor_temperature],
|
||||
'WSDCGQ01LM': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery],
|
||||
'WSDCGQ11LM': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_pressure, cfg.sensor_battery],
|
||||
'RTCGQ01LM': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
|
||||
@@ -770,12 +796,11 @@ const mapping = {
|
||||
'AA68199': [cfg.light_brightness_colortemp],
|
||||
'QBKG11LM': [cfg.switch, cfg.sensor_power, cfg.sensor_click],
|
||||
'QBKG12LM': [
|
||||
switchWithPostfix('left'), switchWithPostfix('right'), cfg.sensor_power, cfg.sensor_click,
|
||||
switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_power, cfg.sensor_click,
|
||||
cfg.sensor_temperature,
|
||||
],
|
||||
'K2RGBW01': [cfg.light_brightness_colortemp_colorxy],
|
||||
'9290011370': [cfg.light_brightness],
|
||||
'DNCKATSW001': [cfg.switch],
|
||||
'Z809A': [cfg.switch, cfg.sensor_power],
|
||||
'NL08-0800': [cfg.light_brightness],
|
||||
'98425031': [cfg.light_brightness],
|
||||
@@ -795,6 +820,7 @@ const mapping = {
|
||||
'GL-C-009': [cfg.light_brightness],
|
||||
'STSS-MULT-001': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'E11-G23/E11-G33': [cfg.light_brightness],
|
||||
'E11-N13/E11-N13A/E11-N14/E11-N14A': [cfg.light_brightness],
|
||||
'E1ACA4ABE38A': [cfg.light_brightness],
|
||||
'AC03645': [cfg.light_brightness_colortemp_colorhs],
|
||||
'AC03641': [cfg.light_brightness],
|
||||
@@ -804,11 +830,13 @@ const mapping = {
|
||||
'AC03642': [cfg.light_brightness_colortemp],
|
||||
'AC08560': [cfg.light_brightness],
|
||||
'AC10786-DIM': [cfg.light_brightness],
|
||||
'DNCKATSW002': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'DNCKATSW003': [switchWithPostfix('left'), switchWithPostfix('right'), switchWithPostfix('center')],
|
||||
'DNCKATSD001': [cfg.light_brightness],
|
||||
'DNCKATSW001': [cfg.switch],
|
||||
'DNCKATSW002': [switchEndpoint('left'), switchEndpoint('right')],
|
||||
'DNCKATSW003': [switchEndpoint('left'), switchEndpoint('right'), switchEndpoint('center')],
|
||||
'DNCKATSW004': [
|
||||
switchWithPostfix('bottom_left'), switchWithPostfix('bottom_right'),
|
||||
switchWithPostfix('top_left'), switchWithPostfix('top_right'),
|
||||
switchEndpoint('bottom_left'), switchEndpoint('bottom_right'),
|
||||
switchEndpoint('top_left'), switchEndpoint('top_right'),
|
||||
],
|
||||
'BY 165': [cfg.light_brightness],
|
||||
'ZLED-2709': [cfg.light_brightness],
|
||||
@@ -844,7 +872,7 @@ const mapping = {
|
||||
'53170161': [cfg.light_brightness_colortemp],
|
||||
'4058075036147': [cfg.light_brightness_colortemp_colorxy],
|
||||
'KS-SM001': [cfg.switch],
|
||||
'MG-AUWS01': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'MG-AUWS01': [switchEndpoint('left'), switchEndpoint('right')],
|
||||
'9290002579A': [cfg.light_brightness_colortemp_colorxy],
|
||||
'4256251-RZHAC': [cfg.switch, cfg.sensor_power],
|
||||
'4257050-ZHAC': [cfg.light_brightness, cfg.sensor_power, cfg.sensor_current, cfg.sensor_voltage],
|
||||
@@ -882,6 +910,7 @@ const mapping = {
|
||||
'ZNLDP12LM': [cfg.light_brightness_colortemp],
|
||||
'D1821': [cfg.light_brightness_colortemp_colorxy],
|
||||
'ZNCLDJ11LM': [cfg.cover_position, cfg.sensor_cover],
|
||||
'owvfni3': [cfg.cover_position],
|
||||
'LTFY004': [cfg.light_brightness_colorxy],
|
||||
'GL-S-007Z': [cfg.light_brightness_colortemp_colorxy],
|
||||
'3325-S': [cfg.sensor_temperature, cfg.binary_sensor_occupancy],
|
||||
@@ -893,7 +922,7 @@ const mapping = {
|
||||
'GL-B-008Z': [cfg.light_brightness_colortemp_colorxy],
|
||||
'AV2010/25': [cfg.switch, cfg.sensor_power],
|
||||
'E12-N14': [cfg.light_brightness],
|
||||
'1TST-EU': [cfg.thermostat(), cfg.sensor_battery],
|
||||
'1TST-EU': [thermostat(), cfg.sensor_battery],
|
||||
'RB 178 T': [cfg.light_brightness_colortemp],
|
||||
'45856GE': [cfg.switch],
|
||||
'GL-D-003Z': [cfg.light_brightness_colortemp_colorxy],
|
||||
@@ -904,7 +933,7 @@ const mapping = {
|
||||
'D1531': [cfg.light_brightness],
|
||||
'D1532': [cfg.light_brightness],
|
||||
'D1533': [cfg.light_brightness],
|
||||
'AV2010/32': [cfg.thermostat(7, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_battery],
|
||||
'AV2010/32': [thermostat(7, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_battery],
|
||||
'HGZB-07A': [cfg.light_brightness_colortemp_colorxy],
|
||||
'E1524/E1810': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'GL-C-006': [cfg.light_brightness_colortemp],
|
||||
@@ -928,8 +957,8 @@ const mapping = {
|
||||
'81809': [cfg.light_brightness_colortemp_colorxy],
|
||||
'4090130P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'100.110.39': [cfg.light_brightness_colortemp_colorxy],
|
||||
'TI0001': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'SPZB0001': [cfg.thermostat(5, 30, 'current_heating_setpoint', 0.5), cfg.sensor_battery],
|
||||
'TI0001': [switchEndpoint('left'), switchEndpoint('right')],
|
||||
'SPZB0001': [thermostat(5, 30, 'current_heating_setpoint', 0.5), cfg.sensor_battery],
|
||||
'HS3CG': [cfg.binary_sensor_gas],
|
||||
'81825': [cfg.sensor_action],
|
||||
'Z809AF': [cfg.switch, cfg.sensor_power],
|
||||
@@ -950,8 +979,8 @@ const mapping = {
|
||||
cfg.binary_sensor_occupancy, cfg.sensor_temperature,
|
||||
cfg.sensor_illuminance, cfg.sensor_illuminance_lux, cfg.sensor_battery,
|
||||
],
|
||||
'HGZB-042': [switchWithPostfix('top'), switchWithPostfix('bottom')],
|
||||
'HGZB-42': [switchWithPostfix('top'), switchWithPostfix('bottom')],
|
||||
'HGZB-042': [switchEndpoint('top'), switchEndpoint('bottom')],
|
||||
'HGZB-42': [switchEndpoint('top'), switchEndpoint('bottom')],
|
||||
'GL-FL-004TZ': [cfg.light_brightness_colortemp_colorxy],
|
||||
'IM6001-OTP05': [cfg.switch],
|
||||
'SV01': [
|
||||
@@ -964,7 +993,11 @@ const mapping = {
|
||||
],
|
||||
'316GLEDRF': [cfg.light_brightness],
|
||||
'LVS-ZB500D': [cfg.light_brightness],
|
||||
'ST218': [],
|
||||
'ST218': [
|
||||
thermostat(5, 30, 'occupied_heating_setpoint', 0.5),
|
||||
cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout,
|
||||
],
|
||||
'E1525/E1745': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
|
||||
'ZYCT-202': [cfg.sensor_action],
|
||||
'GR-ZB01-W': [cfg.cover_position],
|
||||
@@ -976,7 +1009,7 @@ const mapping = {
|
||||
],
|
||||
'9290018195': [cfg.light_brightness],
|
||||
'HGZB-04D / HGZB-4D-UK': [cfg.light_brightness],
|
||||
'HGZB-043': [switchWithPostfix('top'), switchWithPostfix('bottom'), switchWithPostfix('center')],
|
||||
'HGZB-043': [switchEndpoint('top'), switchEndpoint('bottom'), switchEndpoint('center')],
|
||||
'NCZ-3043-HA': [
|
||||
cfg.binary_sensor_occupancy, cfg.sensor_temperature,
|
||||
cfg.sensor_battery, cfg.binary_sensor_battery_low,
|
||||
@@ -1013,7 +1046,7 @@ const mapping = {
|
||||
],
|
||||
'HGZB-1S': [cfg.switch, cfg.sensor_click],
|
||||
'HGZB-045': [cfg.switch, cfg.sensor_click],
|
||||
'HGZB-43': [switchWithPostfix('top'), switchWithPostfix('bottom'), switchWithPostfix('center')],
|
||||
'HGZB-43': [switchEndpoint('top'), switchEndpoint('bottom'), switchEndpoint('center')],
|
||||
'HGZB-01A': [cfg.switch],
|
||||
'HGZB-02A': [cfg.light_brightness],
|
||||
'MCT-350 SMA': [cfg.binary_sensor_contact],
|
||||
@@ -1041,17 +1074,17 @@ const mapping = {
|
||||
'RS 122': [cfg.light_brightness],
|
||||
'GL-B-001Z': [cfg.light_brightness_colortemp_colorxy],
|
||||
'IM6001-MTP01': [cfg.sensor_temperature, cfg.sensor_battery, cfg.binary_sensor_occupancy],
|
||||
'U86K31ND6': [switchWithPostfix('left'), switchWithPostfix('right'), switchWithPostfix('center')],
|
||||
'U86K31ND6': [switchEndpoint('left'), switchEndpoint('right'), switchEndpoint('center')],
|
||||
'HLD812-Z-SC': [cfg.light_brightness],
|
||||
'HLC610-Z': [cfg.light_brightness],
|
||||
'BY 285 C': [cfg.light_brightness_colortemp_colorxy],
|
||||
'HS1RC-M': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'SWO-WDS1PA': [cfg.binary_sensor_contact],
|
||||
'LLKZMK11LM': [
|
||||
switchWithPostfix('l1'), switchWithPostfix('l2'),
|
||||
switchEndpoint('l1'), switchEndpoint('l2'),
|
||||
cfg.sensor_power, cfg.sensor_temperature, cfg.sensor_consumption,
|
||||
],
|
||||
'T18W3Z': [switchWithPostfix('l1'), switchWithPostfix('l2'), switchWithPostfix('l3')],
|
||||
'T18W3Z': [switchEndpoint('l1'), switchEndpoint('l2'), switchEndpoint('l3')],
|
||||
'LVS-SM10ZW': [cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
|
||||
'HS2SK': [cfg.switch, cfg.sensor_power],
|
||||
'45853GE': [cfg.switch, cfg.sensor_power],
|
||||
@@ -1069,7 +1102,7 @@ const mapping = {
|
||||
'GL-S-004Z': [cfg.light_brightness_colortemp],
|
||||
'SCM-5ZBS': [cfg.cover_position],
|
||||
'YRD226HA2619': [cfg.sensor_battery, cfg.lock],
|
||||
'YMF40': [cfg.lock, cfg.sensor_battery],
|
||||
'YMF40/YDM4109+': [cfg.lock, cfg.sensor_battery],
|
||||
'V3-BTZB': [cfg.lock],
|
||||
'3RSS008Z': [cfg.switch, cfg.sensor_battery],
|
||||
'3RSS007Z': [cfg.switch],
|
||||
@@ -1107,8 +1140,8 @@ const mapping = {
|
||||
'ST8AU-CON': [cfg.light_brightness],
|
||||
'HS3MS': [cfg.binary_sensor_occupancy],
|
||||
'DIYRUZ_R4_5': [
|
||||
switchWithPostfix('bottom_left'), switchWithPostfix('bottom_right'), switchWithPostfix('center'),
|
||||
switchWithPostfix('top_left'), switchWithPostfix('top_right'),
|
||||
switchEndpoint('bottom_left'), switchEndpoint('bottom_right'), switchEndpoint('center'),
|
||||
switchEndpoint('top_left'), switchEndpoint('top_right'),
|
||||
],
|
||||
'NCZ-3011-HA': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'MEAZON_BIZY_PLUG': [cfg.sensor_power, cfg.switch, cfg.sensor_temperature],
|
||||
@@ -1144,19 +1177,19 @@ const mapping = {
|
||||
'RH3040': [cfg.sensor_battery, cfg.binary_sensor_occupancy],
|
||||
'DZ4743-00B': [cfg.switch],
|
||||
'GLSK3ZB-1711': [cfg.switch],
|
||||
'GLSK3ZB-1712': [switchWithPostfix('top'), switchWithPostfix('bottom')],
|
||||
'GLSK3ZB-1713': [switchWithPostfix('top'), switchWithPostfix('center'), switchWithPostfix('bottom')],
|
||||
'GLSK3ZB-1712': [switchEndpoint('top'), switchEndpoint('bottom')],
|
||||
'GLSK3ZB-1713': [switchEndpoint('top'), switchEndpoint('center'), switchEndpoint('bottom')],
|
||||
'GLSK6ZB-1714': [
|
||||
switchWithPostfix('top_left'), switchWithPostfix('bottom_left'),
|
||||
switchWithPostfix('top_right'), switchWithPostfix('bottom_right'),
|
||||
switchEndpoint('top_left'), switchEndpoint('bottom_left'),
|
||||
switchEndpoint('top_right'), switchEndpoint('bottom_right'),
|
||||
],
|
||||
'GLSK6ZB-1715': [
|
||||
switchWithPostfix('top_left'), switchWithPostfix('center_left'), switchWithPostfix('bottom_left'),
|
||||
switchWithPostfix('top_right'), switchWithPostfix('bottom_right'),
|
||||
switchEndpoint('top_left'), switchEndpoint('center_left'), switchEndpoint('bottom_left'),
|
||||
switchEndpoint('top_right'), switchEndpoint('bottom_right'),
|
||||
],
|
||||
'GLSK6ZB-1716': [
|
||||
switchWithPostfix('top_left'), switchWithPostfix('center_left'), switchWithPostfix('bottom_left'),
|
||||
switchWithPostfix('top_right'), switchWithPostfix('center_right'), switchWithPostfix('bottom_right'),
|
||||
switchEndpoint('top_left'), switchEndpoint('center_left'), switchEndpoint('bottom_left'),
|
||||
switchEndpoint('top_right'), switchEndpoint('center_right'), switchEndpoint('bottom_right'),
|
||||
],
|
||||
'3306431P7': [cfg.light_brightness_colortemp],
|
||||
'AC08559': [cfg.light_brightness_colortemp_colorxy],
|
||||
@@ -1165,28 +1198,28 @@ const mapping = {
|
||||
'2AJZ4KPKEY': [cfg.sensor_click, cfg.sensor_battery],
|
||||
'2AJZ4KPFT': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery],
|
||||
'TT001ZAV20': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery],
|
||||
'TS0002': [switchWithPostfix('l1'), switchWithPostfix('l2')],
|
||||
'TS0002': [switchEndpoint('l1'), switchEndpoint('l2')],
|
||||
'LVS-SN10ZW': [cfg.sensor_battery, cfg.binary_sensor_occupancy],
|
||||
'LVS-ZB15R': [cfg.switch],
|
||||
'TH1123ZB': [
|
||||
cfg.thermostat(7, 30, 'occupied_heating_setpoint', 1.0), cfg.sensor_local_temperature,
|
||||
thermostat(7, 30, 'occupied_heating_setpoint', 1.0), cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout, cfg.sensor_power,
|
||||
],
|
||||
'TH1124ZB': [cfg.thermostat()],
|
||||
'TH1400ZB': [cfg.thermostat()],
|
||||
'TH1500ZB': [cfg.thermostat()],
|
||||
'Zen-01-W': [cfg.thermostat()],
|
||||
'TH1124ZB': [thermostat()],
|
||||
'TH1400ZB': [thermostat()],
|
||||
'TH1500ZB': [thermostat()],
|
||||
'Zen-01-W': [thermostat()],
|
||||
'9290022166': [cfg.light_brightness_colortemp_colorxy],
|
||||
'PM-C140-ZB': [cfg.sensor_power, cfg.switch],
|
||||
'PM-B530-ZB': [cfg.sensor_power, cfg.switch],
|
||||
'PM-B430-ZB': [cfg.sensor_power, cfg.switch],
|
||||
'ptvo.switch': [
|
||||
switchWithPostfix('bottom_left'), switchWithPostfix('bottom_right'), switchWithPostfix('top_left'),
|
||||
switchWithPostfix('top_right'), switchWithPostfix('center'), cfg.sensor_click,
|
||||
switchEndpoint('bottom_left'), switchEndpoint('bottom_right'), switchEndpoint('top_left'),
|
||||
switchEndpoint('top_right'), switchEndpoint('center'), cfg.sensor_click,
|
||||
],
|
||||
'DIYRuZ_R4_5': [
|
||||
switchWithPostfix('bottom_left'), switchWithPostfix('bottom_right'), switchWithPostfix('top_left'),
|
||||
switchWithPostfix('top_right'), switchWithPostfix('center'),
|
||||
switchEndpoint('bottom_left'), switchEndpoint('bottom_right'), switchEndpoint('top_left'),
|
||||
switchEndpoint('top_right'), switchEndpoint('center'),
|
||||
],
|
||||
'DIYRuZ_KEYPAD20': [],
|
||||
'DTB190502A1': [],
|
||||
@@ -1199,7 +1232,7 @@ const mapping = {
|
||||
'B00TN589ZG': [cfg.light_brightness],
|
||||
'PSB19-SW27': [cfg.light_brightness],
|
||||
'S1': [cfg.switch, cfg.sensor_power],
|
||||
'S2': [switchWithPostfix('l1'), switchWithPostfix('l2'), cfg.sensor_power],
|
||||
'S2': [switchEndpoint('l1'), switchEndpoint('l2'), cfg.sensor_power],
|
||||
'ZWallRemote0': [cfg.sensor_click],
|
||||
'D1': [cfg.light_brightness, cfg.sensor_power],
|
||||
'J1': [cfg.cover_position_tilt, cfg.sensor_power],
|
||||
@@ -1214,7 +1247,7 @@ const mapping = {
|
||||
'Z3-1BRL': [cfg.sensor_action, cfg.sensor_brightness],
|
||||
'HS1CG-E': [cfg.binary_sensor_gas],
|
||||
'LED1842G3': [cfg.light_brightness],
|
||||
'SR-ZG9001K4-DIM2': [cfg.sensor_battery, cfg.sensor_click],
|
||||
'ROB_200-008-0': [cfg.sensor_battery, cfg.sensor_click],
|
||||
'ICZB-IW11SW': [cfg.switch],
|
||||
'HV-GUCXZB5': [cfg.light_brightness_colortemp],
|
||||
'HGZB-20A': [cfg.switch],
|
||||
@@ -1276,7 +1309,7 @@ const mapping = {
|
||||
'CR11S8UZ': [cfg.sensor_action],
|
||||
'RB 148 T': [cfg.light_brightness_colortemp],
|
||||
'STS-OUT-US-2': [cfg.switch, cfg.sensor_power],
|
||||
'UK7004240': [cfg.thermostat(), cfg.sensor_battery],
|
||||
'UK7004240': [thermostat(), cfg.sensor_battery],
|
||||
'S31ZB': [cfg.switch],
|
||||
'SA-003-Zigbee': [cfg.switch],
|
||||
'SZ-DWS04': [cfg.binary_sensor_contact, cfg.sensor_temperature, cfg.sensor_battery],
|
||||
@@ -1315,10 +1348,10 @@ const mapping = {
|
||||
'SP-EUC01': [cfg.switch, cfg.sensor_power],
|
||||
'511.012': [cfg.light_brightness],
|
||||
'GL-S-008Z': [cfg.light_brightness_colortemp_colorxy],
|
||||
'TZSW22FW-L4': [switchWithPostfix('top'), switchWithPostfix('bottom')],
|
||||
'TZSW22FW-L4': [switchEndpoint('top'), switchEndpoint('bottom')],
|
||||
'GDKES-01TZXD': [cfg.switch],
|
||||
'GDKES-02TZXD': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'GDKES-03TZXD': [switchWithPostfix('left'), switchWithPostfix('center'), switchWithPostfix('right')],
|
||||
'GDKES-02TZXD': [switchEndpoint('left'), switchEndpoint('right')],
|
||||
'GDKES-03TZXD': [switchEndpoint('left'), switchEndpoint('center'), switchEndpoint('right')],
|
||||
'6735/6736/6737': [cfg.switch, cfg.sensor_action],
|
||||
'4034031P7': [cfg.light_brightness_colortemp],
|
||||
'5900131C5': [cfg.light_brightness_colortemp],
|
||||
@@ -1344,22 +1377,27 @@ const mapping = {
|
||||
'E1C-NB6': [cfg.switch],
|
||||
'LVS-SC7': [cfg.sensor_action],
|
||||
'1742930P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'ZM-L03E-Z': [switchWithPostfix('left'), switchWithPostfix('center'), switchWithPostfix('right')],
|
||||
'ZM-L03E-Z': [switchEndpoint('left'), switchEndpoint('center'), switchEndpoint('right')],
|
||||
'DL15S-1BZ': [cfg.switch],
|
||||
'E1D-G73WNA': [cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
|
||||
'WV704R0A0902': [cfg.thermostat()],
|
||||
'WV704R0A0902': [thermostat()],
|
||||
'067776': [cfg.cover_position],
|
||||
'067773': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'067771': [cfg.switch],
|
||||
'064873': [cfg.sensor_action],
|
||||
'K4003C': [cfg.switch],
|
||||
'STZB402': [
|
||||
cfg.thermostat(5, 30, 'occupied_heating_setpoint', 0.5),
|
||||
thermostat(5, 30, 'occupied_heating_setpoint', 0.5),
|
||||
cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout,
|
||||
],
|
||||
'SMT402': [
|
||||
cfg.thermostat(5, 30, 'occupied_heating_setpoint', 0.5),
|
||||
thermostat(5, 30, 'occupied_heating_setpoint', 0.5),
|
||||
cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout,
|
||||
],
|
||||
'SMT402AD': [
|
||||
thermostat(5, 30, 'occupied_heating_setpoint', 0.5),
|
||||
cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout,
|
||||
],
|
||||
@@ -1385,14 +1423,14 @@ const mapping = {
|
||||
'AV2010/21A': [cfg.binary_sensor_battery_low, cfg.binary_sensor_contact, cfg.binary_sensor_tamper],
|
||||
'AL-PIR02': [cfg.binary_sensor_occupancy, cfg.binary_sensor_tamper, cfg.sensor_battery],
|
||||
'MKS-CM-W5': [
|
||||
switchWithPostfix('l1'), switchWithPostfix('l2'),
|
||||
switchWithPostfix('l3'), switchWithPostfix('l4'),
|
||||
switchEndpoint('l1'), switchEndpoint('l2'),
|
||||
switchEndpoint('l3'), switchEndpoint('l4'),
|
||||
],
|
||||
'STS-WTR-250': [cfg.binary_sensor_water_leak, cfg.sensor_battery, cfg.sensor_temperature],
|
||||
'ZG2835RAC': [cfg.light_brightness],
|
||||
'BW-IS2': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'BW-IS3': [cfg.binary_sensor_occupancy],
|
||||
'SLR1b': [cfg.thermostat()],
|
||||
'SLR1b': [thermostat()],
|
||||
'WPT1': [],
|
||||
'4058075047853': [cfg.light_brightness_colortemp_colorxy],
|
||||
'ROB_200-003-0': [cfg.switch],
|
||||
@@ -1431,7 +1469,7 @@ const mapping = {
|
||||
'GL-D-003ZS': [cfg.light_brightness_colortemp_colorxy],
|
||||
'66492-001': [cfg.lock, cfg.sensor_battery],
|
||||
'798.09': [cfg.light_brightness_colortemp_colorxy],
|
||||
'U202DST600ZB': [lightWithPostfix('light_brightness', 'l1'), lightWithPostfix('light_brightness', 'l2')],
|
||||
'U202DST600ZB': [lightEndpoint('light_brightness', 'l1'), lightEndpoint('light_brightness', 'l2')],
|
||||
'SM10ZW': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'ICZB-R11D': [cfg.light_brightness],
|
||||
'SW2500ZB': [cfg.switch],
|
||||
@@ -1440,29 +1478,30 @@ const mapping = {
|
||||
'4090331P9': [cfg.light_brightness_colortemp_colorxy],
|
||||
'HS1EB': [cfg.sensor_click],
|
||||
'HS2SW1A-N': [cfg.switch],
|
||||
'HS2SW2A-N': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'HS2SW3A-N': [switchWithPostfix('left'), switchWithPostfix('right'), switchWithPostfix('center')],
|
||||
'LifeControl_Leak_Sensor': [cfg.binary_sensor_water_leak, cfg.sensor_battery],
|
||||
'LifeControl_Door_Sensor': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'LifeControl_RGB_Led': [cfg.light_brightness_colortemp_colorxy],
|
||||
'HS2SW2A-N': [switchEndpoint('left'), switchEndpoint('right')],
|
||||
'HS2SW3A-N': [switchEndpoint('left'), switchEndpoint('right'), switchEndpoint('center')],
|
||||
'MCLH-07': [cfg.binary_sensor_water_leak, cfg.sensor_battery],
|
||||
'MCLH-04': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'MCLH-02': [cfg.light_brightness_colortemp_colorxy],
|
||||
'3323-G': [cfg.binary_sensor_contact, cfg.sensor_temperature, cfg.binary_sensor_battery_low],
|
||||
'ZL1000400-CCT-EU-2-V1A02': [cfg.light_brightness_colortemp],
|
||||
'ROB_200-007-0': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'PM-S140-ZB': [cfg.switch],
|
||||
'PM-S240-ZB': [switchWithPostfix('top'), switchWithPostfix('bottom')],
|
||||
'PM-S340-ZB': [switchWithPostfix('top'), switchWithPostfix('center'), switchWithPostfix('bottom')],
|
||||
'PM-S240-ZB': [switchEndpoint('top'), switchEndpoint('bottom')],
|
||||
'PM-S340-ZB': [switchEndpoint('top'), switchEndpoint('center'), switchEndpoint('bottom')],
|
||||
'U201DST600ZB': [cfg.light_brightness],
|
||||
'U201SRY2KWZB': [cfg.switch],
|
||||
'U202SRY2KWZB': [switchWithPostfix('l1'), switchWithPostfix('l2')],
|
||||
'U202SRY2KWZB': [switchEndpoint('l1'), switchEndpoint('l2')],
|
||||
'93999': [cfg.light_brightness],
|
||||
'ZHS-15': [cfg.switch, cfg.sensor_power, cfg.sensor_current, cfg.sensor_voltage],
|
||||
'GS361A-H04': [
|
||||
cfg.lock_child_lock,
|
||||
cfg.switch_window_detection,
|
||||
cfg.switch_valve_detection,
|
||||
cfg.thermostat(5, 30, 'current_heating_setpoint', 0.5),
|
||||
thermostat(5, 30, 'current_heating_setpoint', 0.5),
|
||||
cfg.sensor_battery,
|
||||
],
|
||||
'HLC614-ZLL': [switchWithPostfix('l1'), switchWithPostfix('l2'), switchWithPostfix('l3')],
|
||||
'HLC614-ZLL': [switchEndpoint('l1'), switchEndpoint('l2'), switchEndpoint('l3')],
|
||||
'EMIZB-132': [
|
||||
cfg.sensor_power, cfg.sensor_voltage, cfg.sensor_current, cfg.sensor_energy, cfg.sensor_current_phase_b,
|
||||
cfg.sensor_current_phase_c, cfg.sensor_voltage_phase_b, cfg.sensor_voltage_phase_c,
|
||||
@@ -1488,36 +1527,74 @@ const mapping = {
|
||||
'SWA01ZB': [cfg.binary_sensor_water_leak, cfg.sensor_battery],
|
||||
'SDM01ZB': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'SFS01ZB': [cfg.switch],
|
||||
'SLS301ZB_2': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'SLS301ZB_3': [switchWithPostfix('left'), switchWithPostfix('right'), switchWithPostfix('center')],
|
||||
'SLS301ZB_2': [switchEndpoint('left'), switchEndpoint('right')],
|
||||
'SLS301ZB_3': [switchEndpoint('left'), switchEndpoint('right'), switchEndpoint('center')],
|
||||
'SSS401ZB': [cfg.switch, cfg.sensor_action],
|
||||
'E12-N1E': [cfg.light_brightness_colortemp_colorxy],
|
||||
'4040B': [cfg.sensor_power, switchWithPostfix('l1'), switchWithPostfix('l2')],
|
||||
'4040B': [cfg.sensor_power, switchEndpoint('l1'), switchEndpoint('l2')],
|
||||
'3460-L': [cfg.sensor_action, cfg.sensor_temperature, cfg.sensor_battery],
|
||||
'3157100': [cfg.thermostat_heatcool(10, 30, 1, ['auto', 'on']), cfg.sensor_battery],
|
||||
'3157100': [thermostatHeatCool(10, 30, 1, ['auto', 'on']), cfg.sensor_battery],
|
||||
'4257050-RZHAC': [cfg.switch, cfg.sensor_power],
|
||||
'27087-03': [cfg.switch, cfg.sensor_battery],
|
||||
'99140-002': [cfg.lock, cfg.sensor_battery],
|
||||
'4512706': [cfg.sensor_battery, cfg.sensor_action],
|
||||
'GL-S-004ZS': [cfg.light_brightness_colortemp_colorxy],
|
||||
'7121131PU': [cfg.light_brightness_colortemp_colorxy],
|
||||
'RL804QZB': [switchWithPostfix('l1'), switchWithPostfix('l2'), switchWithPostfix('l3')],
|
||||
'RC-2000WH': [cfg.thermostat_heatcool(10, 30, 1, ['auto', 'on', 'smart'])],
|
||||
'TH1300ZB': [cfg.thermostat()],
|
||||
'RL804QZB': [switchEndpoint('l1'), switchEndpoint('l2'), switchEndpoint('l3')],
|
||||
'RC-2000WH': [thermostatHeatCool(10, 30, 1, ['auto', 'on', 'smart'])],
|
||||
'TH1300ZB': [thermostat()],
|
||||
'SP 220': [cfg.switch],
|
||||
'511.040': [cfg.light_brightness_colortemp_colorxy],
|
||||
'511.344': [cfg.sensor_battery, cfg.sensor_action, cfg.sensor_action_color],
|
||||
'511.344': [cfg.sensor_battery, cfg.sensor_action, cfg.sensor_action_color, cfg.sensor_action_color_temperature],
|
||||
'SMSZB-120': [cfg.binary_sensor_smoke, cfg.sensor_temperature, cfg.sensor_battery],
|
||||
'HALIGHTDIMWWE14': [cfg.light_brightness],
|
||||
'GreenPower_On_Off_Switch': [cfg.sensor_action],
|
||||
'GreenPower_7': [cfg.sensor_action],
|
||||
'3RSL011Z': [cfg.light_brightness_colortemp],
|
||||
'3RSL012Z': [cfg.light_brightness_colortemp],
|
||||
'1746130P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'6xy-M350ST-W1Z': [cfg.light_brightness_colortemp],
|
||||
'AU-A1GUZBCX5': [cfg.light_brightness_colortemp],
|
||||
'AU-A1GUZB5/30': [cfg.light_brightness],
|
||||
'AU-A1GUZBRGBW': [cfg.light_brightness_colortemp_colorxy],
|
||||
'AU-A1GSZ9RGBW': [cfg.light_brightness_colortemp_colorxy],
|
||||
'RF 261': [cfg.light_brightness],
|
||||
'RF 264': [cfg.light_brightness],
|
||||
'HY369RT': [
|
||||
cfg.lock_child_lock,
|
||||
cfg.switch_window_detection,
|
||||
cfg.switch_valve_detection,
|
||||
thermostat(5, 30, 'current_heating_setpoint', 0.5),
|
||||
cfg.sensor_battery,
|
||||
],
|
||||
'WXKG07LM': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'MCLH-03': [cfg.switch, cfg.sensor_voltage, cfg.sensor_power, cfg.sensor_current],
|
||||
'752189': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'676-00301024955Z': [cfg.light_brightness],
|
||||
'1743030P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'XBee': [],
|
||||
'ZL1000701-27-EU-V1A02': [cfg.light_brightness],
|
||||
'rgbw2.zbee27': [cfg.light_brightness_colortemp_colorxy],
|
||||
'MCLH-05': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
|
||||
'5045148P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'GL-FL-006TZ': [cfg.light_brightness_colortemp_colorxy],
|
||||
'5AA-SS-ZA-H0': [cfg.binary_sensor_occupancy, cfg.sensor_illuminance, cfg.sensor_illuminance_lux],
|
||||
'MOSZB-130': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
|
||||
'AU-A1ZBRC': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'AU-A1ZBPIRS': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
|
||||
'TS0121': [cfg.switch],
|
||||
'ZK03840': [thermostat()],
|
||||
'ZS1100400-IN-V1A02': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
|
||||
'MCLH-08': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_eco2, cfg.sensor_voc],
|
||||
'GL-FL-005TZ': [cfg.light_brightness_colortemp_colorxy],
|
||||
'ED2004-012': [],
|
||||
'ZG192910-4': [cfg.light_brightness_colortemp],
|
||||
};
|
||||
|
||||
Object.keys(mapping).forEach((key) => {
|
||||
mapping[key].push(cfg.sensor_linkquality);
|
||||
});
|
||||
|
||||
/**
|
||||
* This extensions handles integration with HomeAssistant
|
||||
*/
|
||||
class HomeAssistant extends BaseExtension {
|
||||
class HomeAssistant extends Extension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
@@ -1542,10 +1619,10 @@ class HomeAssistant extends BaseExtension {
|
||||
}
|
||||
|
||||
onDeviceRemoved(device) {
|
||||
const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
if (mappedModel) {
|
||||
const definition = zigbeeHerdsmanConverters.findByDevice(device);
|
||||
if (definition) {
|
||||
logger.info(`Clearing Home Assistant discovery topic for '${device.ieeeAddr}'`);
|
||||
this.getConfigs(mappedModel).forEach((config) => {
|
||||
this.getConfigs(definition).forEach((config) => {
|
||||
const topic = this.getDiscoveryTopic(config, device);
|
||||
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic);
|
||||
});
|
||||
@@ -1554,7 +1631,7 @@ class HomeAssistant extends BaseExtension {
|
||||
|
||||
async onPublishEntityState(data) {
|
||||
/**
|
||||
* In case we deal with a lightWithPostfix configuration Zigbee2mqtt publishes
|
||||
* In case we deal with a lightEndpoint configuration Zigbee2mqtt publishes
|
||||
* e.g. {state_l1: ON, brightness_l1: 250} to zigbee2mqtt/mydevice.
|
||||
* As the Home Assistant MQTT JSON light cannot be configured to use state_l1/brightness_l1
|
||||
* as the state variables, the state topic is set to zigbee2mqtt/mydevice/l1.
|
||||
@@ -1565,23 +1642,35 @@ class HomeAssistant extends BaseExtension {
|
||||
for (const config of mapping[data.entity.definition.model]) {
|
||||
const match = /light_(.*)/.exec(config['object_id']);
|
||||
if (match) {
|
||||
const postfix = match[1];
|
||||
const posfixRegExp = new RegExp(`(.*)_${postfix}`);
|
||||
const endpoint = match[1];
|
||||
const endpointRegExp = new RegExp(`(.*)_${endpoint}`);
|
||||
const payload = {};
|
||||
for (const key of Object.keys(data.payload)) {
|
||||
const keyMatch = posfixRegExp.exec(key);
|
||||
const keyMatch = endpointRegExp.exec(key);
|
||||
if (keyMatch) {
|
||||
payload[keyMatch[1]] = data.payload[key];
|
||||
}
|
||||
}
|
||||
|
||||
await this.mqtt.publish(
|
||||
`${data.entity.name}/${postfix}`, JSON.stringify(payload), {},
|
||||
`${data.entity.name}/${endpoint}`, JSON.stringify(payload), {},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an empty value for click and action payload, in this way Home Assistant
|
||||
* can use Home Assistant entities in automations.
|
||||
* https://github.com/Koenkk/zigbee2mqtt/issues/959#issuecomment-480341347
|
||||
*/
|
||||
if (settings.get().advanced.homeassistant_legacy_triggers) {
|
||||
const key = ['action', 'click'].find((k) => data.payload.hasOwnProperty(k) && data.payload[k] !== '');
|
||||
if (key) {
|
||||
this.publishEntityState(data.entity.device.ieeeAddr, {[key]: ''});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the MQTT device trigger (https://www.home-assistant.io/integrations/device_trigger.mqtt/)
|
||||
* The MQTT device trigger does not support JSON parsing, so it cannot listen to zigbee2mqtt/my_device
|
||||
@@ -1607,7 +1696,7 @@ class HomeAssistant extends BaseExtension {
|
||||
subtype: value,
|
||||
payload: value,
|
||||
topic: `${settings.get().mqtt.base_topic}/${data.entity.name}/${key}`,
|
||||
device: this.getDevicePayload(data.entity.settings, data.entity.definition),
|
||||
device: this.getDevicePayload(data.entity),
|
||||
};
|
||||
|
||||
await this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, this.discoveryTopic);
|
||||
@@ -1629,7 +1718,7 @@ class HomeAssistant extends BaseExtension {
|
||||
},
|
||||
{
|
||||
property: 'water_leak',
|
||||
condition: data.entity.device && data.entity.definition &&
|
||||
condition: data.entity.device && data.entity.definition && mapping[data.entity.definition.model] &&
|
||||
mapping[data.entity.definition.model].includes(cfg.binary_sensor_water_leak),
|
||||
value: false,
|
||||
},
|
||||
@@ -1644,9 +1733,9 @@ class HomeAssistant extends BaseExtension {
|
||||
}
|
||||
|
||||
onDeviceRenamed(device) {
|
||||
const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
const resolvedEntity = this.zigbee.resolveEntity(device);
|
||||
logger.info(`Refreshing Home Assistant discovery topic for '${device.ieeeAddr}'`);
|
||||
this.discover(device, mappedModel, true);
|
||||
this.discover(resolvedEntity, true);
|
||||
}
|
||||
|
||||
async onMQTTConnected() {
|
||||
@@ -1654,15 +1743,15 @@ class HomeAssistant extends BaseExtension {
|
||||
|
||||
// MQTT discovery of all paired devices on startup.
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
if (mappedModel) {
|
||||
this.discover(device, mappedModel, true);
|
||||
}
|
||||
const resolvedEntity = this.zigbee.resolveEntity(device);
|
||||
this.discover(resolvedEntity, true);
|
||||
}
|
||||
}
|
||||
|
||||
getConfigs(mappedModel) {
|
||||
let configs = mapping[mappedModel.model].slice();
|
||||
configs.push(cfg.sensor_linkquality);
|
||||
|
||||
if (mappedModel.hasOwnProperty('ota')) {
|
||||
configs.push(cfg.binary_sensor_update_available);
|
||||
}
|
||||
@@ -1674,23 +1763,21 @@ class HomeAssistant extends BaseExtension {
|
||||
return configs;
|
||||
}
|
||||
|
||||
discover(device, mappedModel, force=false) {
|
||||
discover(resolvedEntity, force=false) {
|
||||
// Check if already discoverd and check if there are configs.
|
||||
const {device, definition} = resolvedEntity;
|
||||
const deviceSettings = resolvedEntity.settings;
|
||||
const discover = force || !this.discovered[device.ieeeAddr];
|
||||
if (!discover) {
|
||||
if (!discover || !device || !definition || !mapping[definition.model] || !deviceSettings ||
|
||||
(deviceSettings.hasOwnProperty('homeassistant') && !deviceSettings.homeassistant)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entity = settings.getEntity(device.ieeeAddr);
|
||||
if (!entity || (entity.type === 'device' && !mapping[mappedModel.model]) ||
|
||||
(entity.hasOwnProperty('homeassistant') && !entity.homeassistant)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getConfigs(mappedModel).forEach((config) => {
|
||||
const friendlyName = deviceSettings.friendlyName;
|
||||
this.getConfigs(definition).forEach((config) => {
|
||||
const topic = this.getDiscoveryTopic(config, device);
|
||||
const payload = {...config.discovery_payload};
|
||||
let stateTopic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}`;
|
||||
let stateTopic = `${settings.get().mqtt.base_topic}/${deviceSettings.friendlyName}`;
|
||||
if (payload.state_topic_postfix) {
|
||||
stateTopic += `/${payload.state_topic_postfix}`;
|
||||
delete payload.state_topic_postfix;
|
||||
@@ -1716,26 +1803,26 @@ class HomeAssistant extends BaseExtension {
|
||||
payload.json_attributes_topic = stateTopic;
|
||||
|
||||
// Set (unique) name
|
||||
payload.name = `${entity.friendlyName}_${config.object_id}`;
|
||||
payload.name = `${friendlyName}_${config.object_id}`;
|
||||
|
||||
// Set unique_id
|
||||
payload.unique_id = `${entity.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`;
|
||||
payload.unique_id = `${deviceSettings.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`;
|
||||
|
||||
// Attributes for device registry
|
||||
payload.device = this.getDevicePayload(entity, mappedModel);
|
||||
payload.device = this.getDevicePayload(resolvedEntity);
|
||||
|
||||
// Set availability payload
|
||||
// When using availability_timeout each device has it's own availability topic.
|
||||
// If not, use the availability topic of zigbee2mqtt.
|
||||
if (settings.get().advanced.availability_timeout) {
|
||||
payload.availability_topic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}/availability`;
|
||||
payload.availability_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/availability`;
|
||||
} else {
|
||||
payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`;
|
||||
}
|
||||
|
||||
// Add precision to value_template
|
||||
if (entity.hasOwnProperty(`${config.object_id}_precision`)) {
|
||||
const precision = entity[`${config.object_id}_precision`];
|
||||
if (deviceSettings.hasOwnProperty(`${config.object_id}_precision`)) {
|
||||
const precision = deviceSettings[`${config.object_id}_precision`];
|
||||
let template = payload.value_template;
|
||||
template = template.replace('{{ ', '').replace(' }}', '');
|
||||
template = `{{ (${template} | float) | round(${precision}) }}`;
|
||||
@@ -1743,7 +1830,7 @@ class HomeAssistant extends BaseExtension {
|
||||
}
|
||||
|
||||
if (payload.command_topic) {
|
||||
payload.command_topic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}/`;
|
||||
payload.command_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/`;
|
||||
|
||||
if (payload.command_topic_prefix) {
|
||||
payload.command_topic += `${payload.command_topic_prefix}/`;
|
||||
@@ -1825,7 +1912,7 @@ class HomeAssistant extends BaseExtension {
|
||||
}
|
||||
|
||||
// Override configuration with user settings.
|
||||
if (entity.hasOwnProperty('homeassistant')) {
|
||||
if (deviceSettings.hasOwnProperty('homeassistant')) {
|
||||
const add = (obj) => {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (['number', 'string', 'boolean'].includes(typeof obj[key])) {
|
||||
@@ -1840,10 +1927,10 @@ class HomeAssistant extends BaseExtension {
|
||||
});
|
||||
};
|
||||
|
||||
add(entity.homeassistant);
|
||||
add(deviceSettings.homeassistant);
|
||||
|
||||
if (entity.homeassistant.hasOwnProperty(config.object_id)) {
|
||||
add(entity.homeassistant[config.object_id]);
|
||||
if (deviceSettings.homeassistant.hasOwnProperty(config.object_id)) {
|
||||
add(deviceSettings.homeassistant[config.object_id]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1873,19 +1960,18 @@ class HomeAssistant extends BaseExtension {
|
||||
}
|
||||
|
||||
onZigbeeEvent(type, data, resolvedEntity) {
|
||||
const device = data.device;
|
||||
if (device && resolvedEntity && resolvedEntity.definition) {
|
||||
this.discover(device, resolvedEntity.definition);
|
||||
if (resolvedEntity && type !== 'deviceLeave') {
|
||||
this.discover(resolvedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
getDevicePayload(entity, mappedModel) {
|
||||
getDevicePayload(resolvedEntity) {
|
||||
return {
|
||||
identifiers: [`zigbee2mqtt_${entity.ID}`],
|
||||
name: entity.friendlyName,
|
||||
identifiers: [`zigbee2mqtt_${resolvedEntity.settings.ID}`],
|
||||
name: resolvedEntity.settings.friendlyName,
|
||||
sw_version: `Zigbee2mqtt ${zigbee2mqttVersion}`,
|
||||
model: `${mappedModel.description} (${mappedModel.model})`,
|
||||
manufacturer: mappedModel.vendor,
|
||||
model: `${resolvedEntity.definition.description} (${resolvedEntity.definition.model})`,
|
||||
manufacturer: resolvedEntity.definition.vendor,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const settings = require('../../util/settings');
|
||||
const logger = require('../../util/logger');
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const utils = require('../util/utils');
|
||||
const utils = require('../../util/utils');
|
||||
const assert = require('assert');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const Extension = require('../extension');
|
||||
|
||||
const configRegex =
|
||||
new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+/factory_reset)|(?:\\w+))`);
|
||||
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
|
||||
|
||||
class BridgeLegacy extends BaseExtension {
|
||||
class BridgeLegacy extends Extension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
@@ -150,7 +150,7 @@ class BridgeLegacy extends BaseExtension {
|
||||
};
|
||||
|
||||
if (device.type !== 'Coordinator') {
|
||||
const definition = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
const definition = zigbeeHerdsmanConverters.findByDevice(device);
|
||||
const friendlyDevice = settings.getDevice(device.ieeeAddr);
|
||||
payload.model = definition ? definition.model : device.modelID;
|
||||
payload.vendor = definition ? definition.vendor : '-';
|
||||
@@ -192,7 +192,8 @@ class BridgeLegacy extends BaseExtension {
|
||||
}
|
||||
|
||||
rename(topic, message) {
|
||||
const invalid = `Invalid rename message format expected {old: 'friendly_name', new: 'new_name} got ${message}`;
|
||||
const invalid =
|
||||
`Invalid rename message format expected {"old": "friendly_name", "new": "new_name"} got ${message}`;
|
||||
|
||||
let json = null;
|
||||
try {
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
/* istanbul ignore file */
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const settings = require('../../util/settings');
|
||||
const logger = require('../../util/logger');
|
||||
const Extension = require('../extension');
|
||||
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/device/(.+)/get_group_membership$`);
|
||||
|
||||
class DeviceGroupMembership extends BaseExtension {
|
||||
class DeviceGroupMembership extends Extension {
|
||||
onMQTTConnected() {
|
||||
for (let step = 1; step < 20; step++) {
|
||||
const topic = `${settings.get().mqtt.base_topic}/bridge/device/${'+/'.repeat(step)}get_group_membership`;
|
||||
@@ -1,14 +1,14 @@
|
||||
/* istanbul ignore file */
|
||||
// DEPRECATED
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const utils = require('../util/utils');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const settings = require('../../util/settings');
|
||||
const logger = require('../../util/logger');
|
||||
const utils = require('../../util/utils');
|
||||
const Extension = require('../extension');
|
||||
|
||||
/**
|
||||
* This extensions soft resets the ZNP after a certain timeout.
|
||||
*/
|
||||
class SoftReset extends BaseExtension {
|
||||
class SoftReset extends Extension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.timer = null;
|
||||
+33
-23
@@ -2,15 +2,18 @@ const settings = require('../util/settings');
|
||||
const utils = require('../util/utils');
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const logger = require('../util/logger');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const Extension = require('./extension');
|
||||
|
||||
class NetworkMap extends BaseExtension {
|
||||
/**
|
||||
* This extension creates a network map
|
||||
*/
|
||||
class NetworkMap extends Extension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
// Subscribe to topic.
|
||||
this.topic = `${settings.get().mqtt.base_topic}/bridge/networkmap`;
|
||||
this.topicRoutes = `${settings.get().mqtt.base_topic}/bridge/networkmap/routes`;
|
||||
this.legacyApi = settings.get().advanced.legacy_api;
|
||||
this.legacyTopic = `${settings.get().mqtt.base_topic}/bridge/networkmap`;
|
||||
this.legacyTopicRoutes = `${settings.get().mqtt.base_topic}/bridge/networkmap/routes`;
|
||||
|
||||
// Bind
|
||||
this.raw = this.raw.bind(this);
|
||||
@@ -24,16 +27,23 @@ class NetworkMap extends BaseExtension {
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(this.topic);
|
||||
this.mqtt.subscribe(this.topicRoutes);
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
this.mqtt.subscribe(this.legacyTopic);
|
||||
this.mqtt.subscribe(this.legacyTopicRoutes);
|
||||
}
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
if ((topic === this.topic || topic === this.topicRoutes) && this.supportedFormats.hasOwnProperty(message)) {
|
||||
const includeRoutes = topic === this.topicRoutes;
|
||||
const topology = await this.networkScan(includeRoutes);
|
||||
const converted = this.supportedFormats[message](topology);
|
||||
this.mqtt.publish(`bridge/networkmap/${message}`, converted, {});
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
if ((topic === this.legacyTopic || topic === this.legacyTopicRoutes) &&
|
||||
this.supportedFormats.hasOwnProperty(message)) {
|
||||
const includeRoutes = topic === this.legacyTopicRoutes;
|
||||
const topology = await this.networkScan(includeRoutes);
|
||||
const converted = this.supportedFormats[message](topology);
|
||||
this.mqtt.publish(`bridge/networkmap/${message}`, converted, {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,9 +71,9 @@ class NetworkMap extends BaseExtension {
|
||||
|
||||
// Add the device model
|
||||
if (device.type !== 'Coordinator') {
|
||||
const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
if (mappedModel) {
|
||||
labels.push(`${mappedModel.vendor} ${mappedModel.description} (${mappedModel.model})`);
|
||||
const definition = zigbeeHerdsmanConverters.findByDevice(device);
|
||||
if (definition) {
|
||||
labels.push(`${definition.vendor} ${definition.description} (${definition.model})`);
|
||||
} else {
|
||||
// This model is not supported by zigbee-herdsman-converters, add zigbee model information
|
||||
labels.push(`${device.manufacturerName} ${device.modelID}`);
|
||||
@@ -119,31 +129,31 @@ class NetworkMap extends BaseExtension {
|
||||
|
||||
async networkScan(includeRoutes) {
|
||||
logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`);
|
||||
const devices = this.zigbee.getDevices();
|
||||
const devices = this.zigbee.getDevices().filter((d) => d.type !== 'GreenPower');
|
||||
const lqis = new Map();
|
||||
const routingTables = new Map();
|
||||
const failed = new Map();
|
||||
|
||||
for (const device of devices.filter((d) => d.type != 'EndDevice')) {
|
||||
failed.set(device, []);
|
||||
const resolved = this.zigbee.resolveEntity(device);
|
||||
const resolvedEntity = this.zigbee.resolveEntity(device);
|
||||
try {
|
||||
const result = await device.lqi();
|
||||
lqis.set(device, result);
|
||||
logger.debug(`LQI succeeded for '${resolved.name}'`);
|
||||
logger.debug(`LQI succeeded for '${resolvedEntity.name}'`);
|
||||
} catch (error) {
|
||||
failed.get(device).push('lqi');
|
||||
logger.error(`Failed to execute LQI for '${resolved.name}'`);
|
||||
logger.error(`Failed to execute LQI for '${resolvedEntity.name}'`);
|
||||
}
|
||||
|
||||
if (includeRoutes) {
|
||||
try {
|
||||
const result = await device.routingTable();
|
||||
routingTables.set(device, result);
|
||||
logger.debug(`Routing table succeeded for '${resolved.name}'`);
|
||||
logger.debug(`Routing table succeeded for '${resolvedEntity.name}'`);
|
||||
} catch (error) {
|
||||
failed.get(device).push('routingTable');
|
||||
logger.error(`Failed to execute routing table for '${resolved.name}'`);
|
||||
logger.error(`Failed to execute routing table for '${resolvedEntity.name}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,9 +163,9 @@ class NetworkMap extends BaseExtension {
|
||||
const networkMap = {nodes: [], links: []};
|
||||
// Add nodes
|
||||
for (const device of devices) {
|
||||
const resolved = this.zigbee.resolveEntity(device);
|
||||
const resolvedEntity = this.zigbee.resolveEntity(device);
|
||||
networkMap.nodes.push({
|
||||
ieeeAddr: device.ieeeAddr, friendlyName: resolved.name, type: device.type,
|
||||
ieeeAddr: device.ieeeAddr, friendlyName: resolvedEntity.name, type: device.type,
|
||||
networkAddress: device.networkAddress, manufacturerName: device.manufacturerName,
|
||||
modelID: device.modelID, failed: failed.get(device), lastSeen: device.lastSeen,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const Extension = require('./extension');
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
|
||||
/**
|
||||
* This extension calls the zigbee-herdsman-converters onEvent.
|
||||
*/
|
||||
class OnEvent extends Extension {
|
||||
async onZigbeeStarted() {
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
const resolvedEntity = this.zigbee.resolveEntity(device);
|
||||
this.callOnEvent(resolvedEntity, 'start', {});
|
||||
}
|
||||
}
|
||||
|
||||
onZigbeeEvent(type, data, resolvedEntity) {
|
||||
if (resolvedEntity && resolvedEntity.type === 'device') {
|
||||
this.callOnEvent(resolvedEntity, type, data);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
const resolvedEntity = this.zigbee.resolveEntity(device);
|
||||
this.callOnEvent(resolvedEntity, 'stop', {});
|
||||
}
|
||||
}
|
||||
|
||||
callOnEvent(resolvedEntity, type, data) {
|
||||
zigbeeHerdsmanConverters.onEvent(type, data, resolvedEntity.device);
|
||||
|
||||
if (resolvedEntity.definition && resolvedEntity.definition.onEvent) {
|
||||
resolvedEntity.definition.onEvent(type, data, resolvedEntity.device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OnEvent;
|
||||
+52
-38
@@ -1,20 +1,24 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const assert = require('assert');
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/ota_update/.+$`);
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/ota_update/.+$`);
|
||||
const Extension = require('./extension');
|
||||
const MINUTES_10 = 1000 * 60 * 10;
|
||||
|
||||
class OTAUpdate extends BaseExtension {
|
||||
class OTAUpdate extends Extension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.inProgress = new Set();
|
||||
this.lastChecked = {};
|
||||
this.legacyApi = settings.get().advanced.legacy_api;
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/check`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/update`);
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/check`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/update`);
|
||||
}
|
||||
}
|
||||
|
||||
async onZigbeeEvent(type, data, resolvedEntity) {
|
||||
@@ -52,7 +56,10 @@ class OTAUpdate extends BaseExtension {
|
||||
// - In case we don't support OTA: respond with NO_IMAGE_AVAILABLE (0x98) (so the client stops requesting OTAs)
|
||||
// - In case we do support OTA: respond with ABORT (0x95) as we don't want to update now.
|
||||
const endpoint = data.device.endpoints.find((e) => e.supportsOutputCluster('genOta'));
|
||||
await endpoint.commandResponse('genOta', 'queryNextImageResponse', {status: supportsOTA ? 0x95 : 0x98});
|
||||
if (endpoint) {
|
||||
// Some devices send OTA requests without defining OTA cluster as input cluster.
|
||||
await endpoint.commandResponse('genOta', 'queryNextImageResponse', {status: supportsOTA ? 0x95 : 0x98});
|
||||
}
|
||||
}
|
||||
|
||||
async readSoftwareBuildIDAndDateCode(device, update) {
|
||||
@@ -73,19 +80,26 @@ class OTAUpdate extends BaseExtension {
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
if (!topic.match(topicRegex)) {
|
||||
let resolvedEntity = null;
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
if (!topic.match(legacyTopicRegex)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
resolvedEntity = this.zigbee.resolveEntity(message);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const device = this.zigbee.resolveEntity(message);
|
||||
assert(device != null && device.type === 'device', 'Device not found or not a device');
|
||||
if (!device.definition || !device.definition.ota) {
|
||||
const message = `Device '${device.name}' does not support OTA updates`;
|
||||
assert(resolvedEntity != null && resolvedEntity.type === 'device', 'Device not found or not a device');
|
||||
if (!resolvedEntity.definition || !resolvedEntity.definition.ota) {
|
||||
const message = `Device '${resolvedEntity.name}' does not support OTA updates`;
|
||||
logger.error(message);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const meta = {status: `not_supported`, device: device.name};
|
||||
const meta = {status: `not_supported`, device: resolvedEntity.name};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `ota_update`, message, meta}),
|
||||
@@ -95,20 +109,20 @@ class OTAUpdate extends BaseExtension {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inProgress.has(device.device.ieeeAddr)) {
|
||||
logger.error(`Update or check already in progress for '${device.name}', skipping...`);
|
||||
if (this.inProgress.has(resolvedEntity.device.ieeeAddr)) {
|
||||
logger.error(`Update or check already in progress for '${resolvedEntity.name}', skipping...`);
|
||||
return;
|
||||
}
|
||||
this.inProgress.add(device.device.ieeeAddr);
|
||||
this.inProgress.add(resolvedEntity.device.ieeeAddr);
|
||||
|
||||
const type = topic.substring(settings.get().mqtt.base_topic.length).split('/')[3];
|
||||
if (type === 'check') {
|
||||
const message = `Checking if update available for '${device.name}'`;
|
||||
const message = `Checking if update available for '${resolvedEntity.name}'`;
|
||||
logger.info(message);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const meta = {status: `checking_if_available`, device: device.name};
|
||||
const meta = {status: `checking_if_available`, device: resolvedEntity.name};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `ota_update`, message, meta}),
|
||||
@@ -116,29 +130,28 @@ class OTAUpdate extends BaseExtension {
|
||||
}
|
||||
|
||||
try {
|
||||
const available = await device.definition.ota.isUpdateAvailable(device.device, logger);
|
||||
const message=(available ?
|
||||
`Update available for '${device.name}'` : `No update available for '${device.name}'`);
|
||||
const available = await resolvedEntity.definition.ota.isUpdateAvailable(resolvedEntity.device, logger);
|
||||
const message = `${available ? 'Update' : 'No update'} available for '${resolvedEntity.name}'`;
|
||||
logger.info(message);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const meta = {status: available ? 'available' : 'not_available', device: device.name};
|
||||
const meta = {status: available ? 'available' : 'not_available', device: resolvedEntity.name};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `ota_update`, message, meta}),
|
||||
);
|
||||
}
|
||||
|
||||
this.publishEntityState(device.device.ieeeAddr, {update_available: available});
|
||||
this.lastChecked[device.device.ieeeAddr] = Date.now();
|
||||
this.publishEntityState(resolvedEntity.device.ieeeAddr, {update_available: available});
|
||||
this.lastChecked[resolvedEntity.device.ieeeAddr] = Date.now();
|
||||
} catch (error) {
|
||||
const message = `Failed to check if update available for '${device.name}' (${error.message})`;
|
||||
const message = `Failed to check if update available for '${resolvedEntity.name}' (${error.message})`;
|
||||
logger.error(message);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const meta = {status: `check_failed`, device: device.name};
|
||||
const meta = {status: `check_failed`, device: resolvedEntity.name};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `ota_update`, message, meta}),
|
||||
@@ -146,12 +159,12 @@ class OTAUpdate extends BaseExtension {
|
||||
}
|
||||
}
|
||||
} else { // type === 'update'
|
||||
const message = `Updating '${device.name}' to latest firmware`;
|
||||
const message = `Updating '${resolvedEntity.name}' to latest firmware`;
|
||||
logger.info(message);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const meta = {status: `update_in_progress`, device: device.name};
|
||||
const meta = {status: `update_in_progress`, device: resolvedEntity.name};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `ota_update`, message, meta}),
|
||||
@@ -160,7 +173,7 @@ class OTAUpdate extends BaseExtension {
|
||||
|
||||
try {
|
||||
const onProgress = (progress, remaining) => {
|
||||
let message = `Update of '${device.name}' at ${progress}%`;
|
||||
let message = `Update of '${resolvedEntity.name}' at ${progress.toFixed(2)}%`;
|
||||
if (remaining) {
|
||||
message += `, +- ${Math.round(remaining / 60)} minutes remaining`;
|
||||
}
|
||||
@@ -169,37 +182,38 @@ class OTAUpdate extends BaseExtension {
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const meta = {status: `update_progress`, device: device.name, progress};
|
||||
const meta = {status: `update_progress`, device: resolvedEntity.name, progress};
|
||||
this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message, meta}));
|
||||
}
|
||||
};
|
||||
|
||||
const from_ = await this.readSoftwareBuildIDAndDateCode(device.device, false);
|
||||
await device.definition.ota.updateToLatest(device.device, logger, onProgress);
|
||||
const to = await this.readSoftwareBuildIDAndDateCode(device.device, true);
|
||||
const from_ = await this.readSoftwareBuildIDAndDateCode(resolvedEntity.device, false);
|
||||
await resolvedEntity.definition.ota.updateToLatest(resolvedEntity.device, logger, onProgress);
|
||||
const to = await this.readSoftwareBuildIDAndDateCode(resolvedEntity.device, true);
|
||||
const [fromS, toS] = [JSON.stringify(from_), JSON.stringify(to)];
|
||||
const message = `Finished update of '${device.name}'` + (to ? `, from '${fromS}' to '${toS}'` : ``);
|
||||
const message = `Finished update of '${resolvedEntity.name}'` +
|
||||
(to ? `, from '${fromS}' to '${toS}'` : ``);
|
||||
logger.info(message);
|
||||
this.publishEntityState(device.device.ieeeAddr, {update_available: false});
|
||||
this.publishEntityState(resolvedEntity.device.ieeeAddr, {update_available: false});
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const meta = {status: `update_succeeded`, device: device.name, from: from_, to};
|
||||
const meta = {status: `update_succeeded`, device: resolvedEntity.name, from: from_, to};
|
||||
this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message, meta}));
|
||||
}
|
||||
} catch (error) {
|
||||
const message = `Update of '${device.name}' failed (${error.message})`;
|
||||
const message = `Update of '${resolvedEntity.name}' failed (${error.message})`;
|
||||
logger.error(message);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const meta = {status: `update_failed`, device: device.name};
|
||||
const meta = {status: `update_failed`, device: resolvedEntity.name};
|
||||
this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message, meta}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.inProgress.delete(device.device.ieeeAddr);
|
||||
this.inProgress.delete(resolvedEntity.device.ieeeAddr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const logger = require('../util/logger');
|
||||
const utils = require('../util/utils');
|
||||
const assert = require('assert');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const Extension = require('./extension');
|
||||
|
||||
const postfixes = utils.getEndpointNames();
|
||||
const topicRegex = new RegExp(`^(.+?)(?:/(${postfixes.join('|')}))?/(get|set)(?:/(.+))?`);
|
||||
const topicRegex = new RegExp(`^(.+?)(?:/(${utils.getEndpointNames().join('|')}))?/(get|set)(?:/(.+))?`);
|
||||
|
||||
const groupConverters = [
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_onoff_brightness,
|
||||
@@ -21,7 +20,7 @@ const groupConverters = [
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_move,
|
||||
];
|
||||
|
||||
class EntityPublish extends BaseExtension {
|
||||
class EntityPublish extends Extension {
|
||||
onMQTTConnected() {
|
||||
// Subscribe to topics.
|
||||
const baseTopic = settings.get().mqtt.base_topic;
|
||||
@@ -46,7 +45,7 @@ class EntityPublish extends BaseExtension {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {ID: ID, postfix: match[2] || '', type: match[3], attribute: match[4]};
|
||||
return {ID: ID, endpointName: match[2] || '', type: match[3], attribute: match[4]};
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
@@ -55,20 +54,20 @@ class EntityPublish extends BaseExtension {
|
||||
return false;
|
||||
}
|
||||
|
||||
const entityName = `${topic.ID}` + (topic.postfix ? `/${topic.postfix}` : '');
|
||||
const entity = this.zigbee.resolveEntity(entityName);
|
||||
const entityKey = `${topic.ID}` + (topic.endpointName ? `/${topic.endpointName}` : '');
|
||||
const resolvedEntity = this.zigbee.resolveEntity(entityKey);
|
||||
|
||||
if (!entity) {
|
||||
if (!resolvedEntity) {
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const message = {friendly_name: entityName};
|
||||
const message = {friendly_name: entityKey};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `entity_not_found`, message}),
|
||||
);
|
||||
}
|
||||
|
||||
logger.error(`Entity '${entityName}' is unknown`);
|
||||
logger.error(`Entity '${entityKey}' is unknown`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,27 +76,26 @@ class EntityPublish extends BaseExtension {
|
||||
let target = null;
|
||||
let options = {};
|
||||
let device = null;
|
||||
let mapped = null;
|
||||
let definition = null;
|
||||
|
||||
assert(entity.type === 'device' || entity.type === 'group');
|
||||
if (entity.type === 'device') {
|
||||
// Map device to a model
|
||||
if (!entity.definition) {
|
||||
logger.warn(`Device with modelID '${entity.device.modelID}' is not supported.`);
|
||||
assert(resolvedEntity.type === 'device' || resolvedEntity.type === 'group');
|
||||
if (resolvedEntity.type === 'device') {
|
||||
if (!resolvedEntity.definition) {
|
||||
logger.warn(`Device with modelID '${resolvedEntity.device.modelID}' is not supported.`);
|
||||
logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`);
|
||||
return;
|
||||
}
|
||||
|
||||
device = entity.device;
|
||||
mapped = entity.definition;
|
||||
target = entity.endpoint;
|
||||
converters = entity.definition.toZigbee;
|
||||
options = entity.settings;
|
||||
device = resolvedEntity.device;
|
||||
definition = resolvedEntity.definition;
|
||||
target = resolvedEntity.endpoint;
|
||||
converters = resolvedEntity.definition.toZigbee;
|
||||
options = resolvedEntity.settings;
|
||||
} else {
|
||||
converters = groupConverters;
|
||||
target = entity.group;
|
||||
options = entity.settings;
|
||||
mapped = entity.group.members.map((e) => zigbeeHerdsmanConverters.findByZigbeeModel(e.getDevice().modelID));
|
||||
target = resolvedEntity.group;
|
||||
options = resolvedEntity.settings;
|
||||
definition = resolvedEntity.group.members.map((e) => zigbeeHerdsmanConverters.findByDevice(e.getDevice()));
|
||||
}
|
||||
|
||||
// Convert the MQTT message to a Zigbee message.
|
||||
@@ -113,12 +111,13 @@ class EntityPublish extends BaseExtension {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Home Assistant always publishes 'state', even when e.g. only setting
|
||||
* the color temperature. This would lead to 2 zigbee publishes, where the first one
|
||||
* (state) is probably unecessary.
|
||||
*/
|
||||
const deviceState = this.state.get(entity.settings.ID) || {};
|
||||
const deviceState = this.state.get(resolvedEntity.settings.ID) || {};
|
||||
if (settings.get().homeassistant) {
|
||||
const hasColorTemp = json.hasOwnProperty('color_temp');
|
||||
const hasColor = json.hasOwnProperty('color');
|
||||
@@ -130,25 +129,34 @@ class EntityPublish extends BaseExtension {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that state and brightness are executed before other commands.
|
||||
/**
|
||||
* Order state & brightness based on current bulb state
|
||||
*
|
||||
* Not all bulbs support setting the color/color_temp while it is off
|
||||
* this results in inconsistant behavior between different vendors.
|
||||
*
|
||||
* bulb on => move state & brightness to the back
|
||||
* bulb off => move state & brightness to the front
|
||||
*/
|
||||
const entries = Object.entries(json);
|
||||
entries.sort((a, b) => (['state', 'brightness'].includes(a[0]) ? -1 : 1));
|
||||
const sorter = typeof json.state === 'string' && json.state.toLowerCase() === 'off' ? 1 : -1;
|
||||
entries.sort((a, b) => (['state', 'brightness', 'brightness_percent'].includes(a[0]) ? sorter : sorter * -1));
|
||||
|
||||
// For each attribute call the corresponding converter
|
||||
const usedConverters = [];
|
||||
for (let [key, value] of entries) {
|
||||
let postfix = topic.postfix;
|
||||
let endpointName = topic.endpointName;
|
||||
let actualTarget = target;
|
||||
|
||||
// When the key has a postfix included (e.g. state_right), this will override the target.
|
||||
if (entity.type === 'device' && key.includes('_')) {
|
||||
// When the key has a endpointName included (e.g. state_right), this will override the target.
|
||||
if (resolvedEntity.type === 'device' && key.includes('_')) {
|
||||
const underscoreIndex = key.lastIndexOf('_');
|
||||
const possiblePostfix = key.substring(underscoreIndex + 1, key.length);
|
||||
if (utils.getEndpointNames().includes(possiblePostfix)) {
|
||||
postfix = possiblePostfix;
|
||||
const possibleEndpointName = key.substring(underscoreIndex + 1, key.length);
|
||||
if (utils.getEndpointNames().includes(possibleEndpointName)) {
|
||||
endpointName = possibleEndpointName;
|
||||
key = key.substring(0, underscoreIndex);
|
||||
const device = target.getDevice();
|
||||
actualTarget = device.getEndpoint(mapped.endpoint(device)[postfix]);
|
||||
actualTarget = device.getEndpoint(definition.endpoint(device)[endpointName]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,32 +174,32 @@ class EntityPublish extends BaseExtension {
|
||||
|
||||
// Converter didn't return a result, skip
|
||||
const meta = {
|
||||
endpoint_name: postfix,
|
||||
endpoint_name: endpointName,
|
||||
options,
|
||||
message: json,
|
||||
logger,
|
||||
device,
|
||||
state: deviceState,
|
||||
mapped,
|
||||
mapped: definition,
|
||||
};
|
||||
|
||||
try {
|
||||
if (topic.type === 'set' && converter.convertSet) {
|
||||
logger.debug(`Publishing '${topic.type}' '${key}' to '${entity.name}'`);
|
||||
logger.debug(`Publishing '${topic.type}' '${key}' to '${resolvedEntity.name}'`);
|
||||
const result = await converter.convertSet(actualTarget, key, value, meta);
|
||||
if (result && result.state) {
|
||||
const msg = result.state;
|
||||
|
||||
if (postfix) {
|
||||
if (endpointName) {
|
||||
for (const key of ['state', 'brightness']) {
|
||||
if (msg.hasOwnProperty(key)) {
|
||||
msg[`${key}_${postfix}`] = msg[key];
|
||||
msg[`${key}_${endpointName}`] = msg[key];
|
||||
delete msg[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.publishEntityState(entity.settings.ID, msg);
|
||||
this.publishEntityState(resolvedEntity.settings.ID, msg);
|
||||
}
|
||||
|
||||
// It's possible for devices to get out of sync when writing an attribute that's not reportable.
|
||||
@@ -202,13 +210,13 @@ class EntityPublish extends BaseExtension {
|
||||
// ever issue a read here, as we assume the device will properly report changes.
|
||||
// Only do this when the retrieve_state option is enabled for this device.
|
||||
if (
|
||||
entity.type === 'device' && result && result.hasOwnProperty('readAfterWriteTime') &&
|
||||
entity.settings.retrieve_state
|
||||
resolvedEntity.type === 'device' && result && result.hasOwnProperty('readAfterWriteTime') &&
|
||||
resolvedEntity.settings.retrieve_state
|
||||
) {
|
||||
setTimeout(() => converter.convertGet(actualTarget, key, meta), result.readAfterWriteTime);
|
||||
}
|
||||
} else if (topic.type === 'get' && converter.convertGet) {
|
||||
logger.debug(`Publishing get '${topic.type}' '${key}' to '${entity.name}'`);
|
||||
logger.debug(`Publishing get '${topic.type}' '${key}' to '${resolvedEntity.name}'`);
|
||||
await converter.convertGet(actualTarget, key, meta);
|
||||
} else {
|
||||
logger.error(`No converter available for '${topic.type}' '${key}' (${json[key]})`);
|
||||
@@ -216,13 +224,13 @@ class EntityPublish extends BaseExtension {
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
`Publish '${topic.type}' '${key}' to '${entity.name}' failed: '${error}'`;
|
||||
`Publish '${topic.type}' '${key}' to '${resolvedEntity.name}' failed: '${error}'`;
|
||||
logger.error(message);
|
||||
logger.debug(error.stack);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const meta = {friendly_name: entity.name};
|
||||
const meta = {friendly_name: resolvedEntity.name};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
JSON.stringify({type: `zigbee_publish_error`, message, meta}),
|
||||
@@ -2,12 +2,11 @@ const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const utils = require('../util/utils');
|
||||
const debounce = require('debounce');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const Extension = require('./extension');
|
||||
|
||||
class DeviceReceive extends BaseExtension {
|
||||
class Receive extends Extension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.coordinator = null;
|
||||
this.elapsed = {};
|
||||
this.debouncers = {};
|
||||
}
|
||||
@@ -54,7 +53,7 @@ class DeviceReceive extends BaseExtension {
|
||||
return result;
|
||||
}
|
||||
|
||||
canHandleEvent(type, data, resolvedEntity) {
|
||||
shouldProcess(type, data, resolvedEntity) {
|
||||
if (type !== 'message' || !resolvedEntity) {
|
||||
return false;
|
||||
}
|
||||
@@ -98,7 +97,7 @@ class DeviceReceive extends BaseExtension {
|
||||
}
|
||||
|
||||
onZigbeeEvent(type, data, resolvedEntity) {
|
||||
if (!this.canHandleEvent(type, data, resolvedEntity)) {
|
||||
if (!this.shouldProcess(type, data, resolvedEntity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -107,8 +106,8 @@ class DeviceReceive extends BaseExtension {
|
||||
return c.cluster === data.cluster && type;
|
||||
});
|
||||
|
||||
// Check if there is an available converter
|
||||
if (!converters.length) {
|
||||
// Check if there is an available converter, genOta messages are not interesting.
|
||||
if (!converters.length && data.cluster !== 'genOta') {
|
||||
logger.debug(
|
||||
`No converter available for '${resolvedEntity.definition.model}' with cluster '${data.cluster}' ` +
|
||||
`and type '${data.type}' and data '${JSON.stringify(data.data)}'`,
|
||||
@@ -144,21 +143,6 @@ class DeviceReceive extends BaseExtension {
|
||||
this.publishDebounce(data.device.ieeeAddr, payload, debounce, debounceIgnore);
|
||||
} else {
|
||||
this.publishEntityState(data.device.ieeeAddr, payload);
|
||||
|
||||
if (settings.get().homeassistant && settings.get().advanced.homeassistant_legacy_triggers) {
|
||||
/**
|
||||
* Publish an empty value for click and action payload, in this way Home Assistant
|
||||
* can use Home Assistant entities in automations.
|
||||
* https://github.com/Koenkk/zigbee2mqtt/issues/959#issuecomment-480341347
|
||||
*/
|
||||
Object.keys(payload).forEach((key) => {
|
||||
if (['action', 'click'].includes(key)) {
|
||||
const counterPayload = {};
|
||||
counterPayload[key] = '';
|
||||
this.publishEntityState(data.device.ieeeAddr, counterPayload);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -178,4 +162,4 @@ class DeviceReceive extends BaseExtension {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceReceive;
|
||||
module.exports = Receive;
|
||||
@@ -2,7 +2,7 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const logger = require('../util/logger');
|
||||
const ZNLDP12LM = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'ZNLDP12LM');
|
||||
const utils = require('../util/utils');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const Extension = require('./extension');
|
||||
const debounce = require('debounce');
|
||||
const ZigbeeHerdsman = require('zigbee-herdsman');
|
||||
|
||||
@@ -14,6 +14,7 @@ const devicesNotSupportingReporting = [
|
||||
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'CC2530.ROUTER'),
|
||||
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'BASICZBR3'),
|
||||
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'ZM-CSW032-D'),
|
||||
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'TS0001'),
|
||||
];
|
||||
|
||||
const reportKey = 1;
|
||||
@@ -99,10 +100,10 @@ const pollOnMessage = [
|
||||
},
|
||||
];
|
||||
|
||||
class DeviceReport extends BaseExtension {
|
||||
class Report extends Extension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.configuring = new Set();
|
||||
this.queue = new Set();
|
||||
this.failed = new Set();
|
||||
this.pollDebouncers = {};
|
||||
}
|
||||
@@ -120,8 +121,8 @@ class DeviceReport extends BaseExtension {
|
||||
async setupReporting(resolvedEntity) {
|
||||
const {device, definition} = resolvedEntity;
|
||||
|
||||
if (this.configuring.has(device.ieeeAddr) || this.failed.has(device.ieeeAddr)) return;
|
||||
this.configuring.add(device.ieeeAddr);
|
||||
if (this.queue.has(device.ieeeAddr) || this.failed.has(device.ieeeAddr)) return;
|
||||
this.queue.add(device.ieeeAddr);
|
||||
|
||||
try {
|
||||
for (const ep of device.endpoints) {
|
||||
@@ -157,7 +158,7 @@ class DeviceReport extends BaseExtension {
|
||||
}
|
||||
|
||||
device.save();
|
||||
this.configuring.delete(device.ieeeAddr);
|
||||
this.queue.delete(device.ieeeAddr);
|
||||
}
|
||||
|
||||
shouldSetupReporting(resolvedEntity, messageType) {
|
||||
@@ -252,4 +253,4 @@ class DeviceReport extends BaseExtension {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceReport;
|
||||
module.exports = Report;
|
||||
+15
-7
@@ -178,7 +178,7 @@ const schema = {
|
||||
baudrate: {type: 'number'},
|
||||
rtscts: {type: 'boolean'},
|
||||
soft_reset_timeout: {type: 'number', minimum: 0},
|
||||
network_key: {type: 'array', items: {type: 'number'}},
|
||||
network_key: {type: ['array', 'string'], items: {type: 'number'}},
|
||||
last_seen: {type: 'string', enum: ['disable', 'ISO_8601', 'ISO_8601_local', 'epoch']},
|
||||
elapsed: {type: 'boolean'},
|
||||
availability_timeout: {type: 'number', minimum: 0},
|
||||
@@ -282,13 +282,16 @@ function write() {
|
||||
|
||||
// Read settings to check if we have to split devices/groups into separate file.
|
||||
const actual = yaml.read(file);
|
||||
if (actual.mqtt && actual.mqtt.password && actual.mqtt.user) {
|
||||
toWrite.mqtt.user = actual.mqtt.user;
|
||||
toWrite.mqtt.password = actual.mqtt.password;
|
||||
}
|
||||
|
||||
if (actual.advanced && actual.advanced.network_key) {
|
||||
toWrite.advanced.network_key = actual.advanced.network_key;
|
||||
// In case the setting is defined in a separte file (e.g. !secret network_key) update it there.
|
||||
for (const path of [['mqtt', 'user'], ['mqtt', 'password'], ['advanced', 'network_key']]) {
|
||||
if (actual[path[0]] && actual[path[0]][path[1]]) {
|
||||
const match = /!(.*) (.*)/g.exec(actual[path[0]][path[1]]);
|
||||
if (match) {
|
||||
yaml.updateIfChanged(data.joinPath(`${match[1]}.yaml`), match[2], toWrite[path[0]][path[1]]);
|
||||
toWrite[path[0]][path[1]] = actual[path[0]][path[1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof actual.devices === 'string') {
|
||||
@@ -310,6 +313,11 @@ function write() {
|
||||
function validate() {
|
||||
const validate = ajv.compile(schema);
|
||||
const valid = validate(_settings);
|
||||
if (_settings.advanced && _settings.advanced.network_key && typeof _settings.advanced.network_key === 'string' &&
|
||||
_settings.advanced.network_key !== 'GENERATE') {
|
||||
throw new Error(`advanced.network_key: should be array or 'GENERATE' (is '${_settings.advanced.network_key}')`);
|
||||
}
|
||||
|
||||
const postfixes = utils.getEndpointNames();
|
||||
|
||||
// Verify that all friendly names are unique
|
||||
|
||||
+2
-1
@@ -43,7 +43,8 @@ function flatten(arr) {
|
||||
const forceEndDevice = flatten(
|
||||
['QBKG03LM', 'QBKG04LM', 'ZNMS13LM', 'ZNMS12LM']
|
||||
.map((model) => zigbeeHerdsmanConverters.devices.find((d) => d.model === model))
|
||||
.map((mappedModel) => mappedModel.zigbeeModel));
|
||||
.filter((definition) => definition.hasOwnProperty('zigbeeModel'))
|
||||
.map((definition) => definition.zigbeeModel));
|
||||
|
||||
async function getZigbee2mqttVersion() {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
+9
-1
@@ -33,4 +33,12 @@ function writeIfChanged(file, content) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {read, readIfExists, writeIfChanged};
|
||||
function updateIfChanged(file, key, value) {
|
||||
const content = read(file);
|
||||
if (content[key] !== value) {
|
||||
content[key] = value;
|
||||
writeIfChanged(file, content);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {read, readIfExists, writeIfChanged, updateIfChanged};
|
||||
|
||||
+29
-23
@@ -11,27 +11,6 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const endpointNames = utils.getEndpointNames();
|
||||
const keyEndpointByNumber = new RegExp(`.*/([0-9]*)$`);
|
||||
|
||||
const herdsmanSettings = {
|
||||
network: {
|
||||
panID: settings.get().advanced.pan_id,
|
||||
extendedPanID: settings.get().advanced.ext_pan_id,
|
||||
channelList: [settings.get().advanced.channel],
|
||||
networkKey: settings.get().advanced.network_key,
|
||||
},
|
||||
databasePath: data.joinPath('database.db'),
|
||||
databaseBackupPath: data.joinPath('database.db.backup'),
|
||||
backupPath: data.joinPath('coordinator_backup.json'),
|
||||
serialPort: {
|
||||
baudRate: settings.get().advanced.baudrate,
|
||||
rtscts: settings.get().advanced.rtscts,
|
||||
path: settings.get().serial.port,
|
||||
adapter: settings.get().serial.adapter,
|
||||
},
|
||||
adapter: {
|
||||
concurrent: settings.get().advanced.adapter_concurrent,
|
||||
},
|
||||
};
|
||||
|
||||
class Zigbee extends events.EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -40,10 +19,37 @@ class Zigbee extends events.EventEmitter {
|
||||
|
||||
async start() {
|
||||
logger.info(`Starting zigbee-herdsman...`);
|
||||
const herdsmanSettings = {
|
||||
network: {
|
||||
panID: settings.get().advanced.pan_id,
|
||||
extendedPanID: settings.get().advanced.ext_pan_id,
|
||||
channelList: [settings.get().advanced.channel],
|
||||
networkKey: settings.get().advanced.network_key,
|
||||
},
|
||||
databasePath: data.joinPath('database.db'),
|
||||
databaseBackupPath: data.joinPath('database.db.backup'),
|
||||
backupPath: data.joinPath('coordinator_backup.json'),
|
||||
serialPort: {
|
||||
baudRate: settings.get().advanced.baudrate,
|
||||
rtscts: settings.get().advanced.rtscts,
|
||||
path: settings.get().serial.port,
|
||||
adapter: settings.get().serial.adapter,
|
||||
},
|
||||
adapter: {
|
||||
concurrent: settings.get().advanced.adapter_concurrent,
|
||||
},
|
||||
};
|
||||
|
||||
const herdsmanSettingsLog = objectAssignDeep.noMutate(herdsmanSettings);
|
||||
herdsmanSettingsLog.network.networkKey = 'HIDDEN';
|
||||
logger.debug(`Using zigbee-herdsman with settings: '${JSON.stringify(herdsmanSettingsLog)}'`);
|
||||
|
||||
if (herdsmanSettings.network.networkKey === 'GENERATE') {
|
||||
const newKey = Array.from({length: 16}, () => Math.floor(Math.random() * 255));
|
||||
settings.set(['advanced', 'network_key'], newKey);
|
||||
herdsmanSettings.network.networkKey = newKey;
|
||||
}
|
||||
|
||||
try {
|
||||
herdsmanSettings.acceptJoiningDeviceHandler = this.acceptJoiningDeviceHandler;
|
||||
this.herdsman = new ZigbeeHerdsman.Controller(herdsmanSettings);
|
||||
@@ -182,7 +188,7 @@ class Zigbee extends events.EventEmitter {
|
||||
return null;
|
||||
}
|
||||
|
||||
const definition = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
const definition = zigbeeHerdsmanConverters.findByDevice(device);
|
||||
const endpoints = definition && definition.endpoint ? definition.endpoint(device) : null;
|
||||
let endpoint;
|
||||
if (endpointKey) {
|
||||
@@ -217,7 +223,7 @@ class Zigbee extends events.EventEmitter {
|
||||
endpoint: key.endpoints[0],
|
||||
settings: setting,
|
||||
name: setting ? setting.friendlyName : (key.type === 'Coordinator' ? 'Coordinator' : key.ieeeAddr),
|
||||
definition: zigbeeHerdsmanConverters.findByZigbeeModel(key.modelID),
|
||||
definition: zigbeeHerdsmanConverters.findByDevice(key),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+521
-366
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "1.12.2",
|
||||
"version": "1.13.0",
|
||||
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -45,8 +45,8 @@
|
||||
"rimraf": "*",
|
||||
"semver": "*",
|
||||
"winston": "*",
|
||||
"zigbee-herdsman": "0.12.78",
|
||||
"zigbee-herdsman-converters": "12.0.69"
|
||||
"zigbee-herdsman": "0.12.83",
|
||||
"zigbee-herdsman-converters": "12.0.81"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "*",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
class Example {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
this.mqtt = mqtt;
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.publish('example/extension', 'test')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Example;
|
||||
@@ -15,11 +15,11 @@ const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
||||
|
||||
const mocksClear = [MQTT.publish, logger.warn, logger.debug];
|
||||
|
||||
describe('Device availability', () => {
|
||||
describe('Availability', () => {
|
||||
let controller;
|
||||
|
||||
function getExtension() {
|
||||
return controller.extensions.find((e) => e.constructor.name === 'DeviceAvailability');
|
||||
return controller.extensions.find((e) => e.constructor.name === 'Availability');
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -6,7 +6,7 @@ const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
const flushPromises = () => new Promise(setImmediate);
|
||||
|
||||
describe('Device bind', () => {
|
||||
describe('Bind', () => {
|
||||
let controller;
|
||||
|
||||
mockClear = (device) => {
|
||||
@@ -9,7 +9,7 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const mocksClear = [MQTT.publish, logger.warn, logger.debug];
|
||||
|
||||
describe('Device configure', () => {
|
||||
describe('Configure', () => {
|
||||
let controller;
|
||||
|
||||
expectRemoteConfigured = () => {
|
||||
@@ -153,7 +153,7 @@ describe('Device configure', () => {
|
||||
});
|
||||
|
||||
it('Should configure max 3 times when fails', async () => {
|
||||
controller.extensions.find((e) => e.constructor.name === 'DeviceConfigure').attempts = {};
|
||||
controller.extensions.find((e) => e.constructor.name === 'Configure').attempts = {};
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
delete device.meta.configured;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
@@ -85,6 +85,14 @@ describe('Controller', () => {
|
||||
expect(MQTT.connect).toHaveBeenCalledWith("mqtt://localhost", expected);
|
||||
});
|
||||
|
||||
it('Should generate network_key when set to GENERATE', async () => {
|
||||
settings.set(['advanced', 'network_key'], 'GENERATE');
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
expect(zigbeeHerdsman.constructor.mock.calls[0][0].network.networkKey.length).toStrictEqual(16);
|
||||
expect(data.read().advanced.network_key.length).toStrictEqual(16);
|
||||
});
|
||||
|
||||
it('Start controller should publish cached states', async () => {
|
||||
data.writeDefaultState();
|
||||
await controller.start();
|
||||
@@ -529,4 +537,15 @@ describe('Controller', () => {
|
||||
await flushPromises();
|
||||
expect(controller.state.state).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('Load user extension', async () => {
|
||||
const extensionPath = path.join(data.mockDir, 'extension');
|
||||
fs.mkdirSync(extensionPath);
|
||||
fs.copyFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), path.join(extensionPath, 'exampleExtension.js'))
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', { retain: false, qos: 0 }, expect.any(Function));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -517,7 +517,7 @@ describe('HomeAssistant extension', () => {
|
||||
MQTT.publish.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
// 1 publish is the publish from deviceReceive
|
||||
// 1 publish is the publish from receive
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -558,7 +558,7 @@ describe('HomeAssistant extension', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Shouldnt discover when message has no device yet', async () => {
|
||||
it('Shouldnt discover when device leaves', async () => {
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
@@ -939,4 +939,25 @@ describe('HomeAssistant extension', () => {
|
||||
await flushPromises();
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should counter an action/click payload with an empty payload', async () => {
|
||||
controller = new Controller(false);
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
MQTT.publish.mockClear();
|
||||
const device = zigbeeHerdsman.devices.WXKG11LM;
|
||||
const data = {onOff: 1}
|
||||
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(4);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'single', linkquality: 10});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/button');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({click: '', linkquality: 10});
|
||||
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('homeassistant/device_automation/0x0017880104e45520/click_single/config');
|
||||
expect(MQTT.publish.mock.calls[3][0]).toStrictEqual('zigbee2mqtt/button/click');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const data = require('./stub/data');
|
||||
const logger = require('./stub/logger');
|
||||
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
const MQTT = require('./stub/mqtt');
|
||||
const data = require('../stub/data');
|
||||
const logger = require('../stub/logger');
|
||||
const zigbeeHerdsman = require('../stub/zigbeeHerdsman');
|
||||
const MQTT = require('../stub/mqtt');
|
||||
const path = require('path');
|
||||
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
||||
const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
const settings = require('../../lib/util/settings');
|
||||
const Controller = require('../../lib/controller');
|
||||
const flushPromises = () => new Promise(setImmediate);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('Bridge legacy', () => {
|
||||
let controller;
|
||||
|
||||
beforeAll(async () => {
|
||||
this.version = await require('../lib/util/utils').getZigbee2mqttVersion();
|
||||
this.version = await require('../../lib/util/utils').getZigbee2mqttVersion();
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
})
|
||||
@@ -13,11 +13,11 @@ const mocksClear = [MQTT.publish, logger.warn, logger.debug];
|
||||
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const mockOnEvent = jest.fn();
|
||||
const mappedLivolo = zigbeeHerdsmanConverters.findByZigbeeModel(zigbeeHerdsman.devices.LIVOLO.modelID);
|
||||
const mappedLivolo = zigbeeHerdsmanConverters.findByDevice(zigbeeHerdsman.devices.LIVOLO);
|
||||
mappedLivolo.onEvent = mockOnEvent;
|
||||
zigbeeHerdsmanConverters.onEvent = jest.fn();
|
||||
|
||||
describe('Device event', () => {
|
||||
describe('On event', () => {
|
||||
let controller;
|
||||
const device = zigbeeHerdsman.devices.LIVOLO;
|
||||
|
||||
+20
-9
@@ -38,7 +38,7 @@ describe('OTA update', () => {
|
||||
count++;
|
||||
return {swBuildId: count, dateCode: '2019010' + count}
|
||||
});
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
logger.error.mockClear();
|
||||
@@ -54,8 +54,8 @@ describe('OTA update', () => {
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(0);
|
||||
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(1);
|
||||
expect(mapped.ota.updateToLatest).toHaveBeenCalledWith(device, logger, expect.any(Function));
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0%`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10%, +- 60 minutes remaining`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, +- 60 minutes remaining`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb', from '{"softwareBuildID":1,"dateCode":"20190101"}' to '{"softwareBuildID":2,"dateCode":"20190102"}'`);
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
expect(device.save).toHaveBeenCalledTimes(1);
|
||||
@@ -67,7 +67,7 @@ describe('OTA update', () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const endpoint = device.endpoints[0];
|
||||
endpoint.read.mockImplementation(() => {return {swBuildId: 1, dateCode: '2019010'}});
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
logger.error.mockClear();
|
||||
@@ -84,7 +84,7 @@ describe('OTA update', () => {
|
||||
|
||||
it('Should be able to check if OTA update is available', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
|
||||
logger.info.mockClear();
|
||||
@@ -106,7 +106,7 @@ describe('OTA update', () => {
|
||||
|
||||
it('Should handle if OTA update check fails', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
logger.error.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('RF singals disturbed because of dogs barking')});
|
||||
@@ -127,7 +127,7 @@ describe('OTA update', () => {
|
||||
it('Should refuse to check/update when already in progress', async () => {
|
||||
jest.useFakeTimers();
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
|
||||
logger.info.mockClear();
|
||||
@@ -154,7 +154,7 @@ describe('OTA update', () => {
|
||||
return {swBuildId: 1, dateCode: '2019010'}
|
||||
});
|
||||
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
|
||||
@@ -165,7 +165,7 @@ describe('OTA update', () => {
|
||||
it('Should check for update when device requests it', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(true);
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
|
||||
@@ -201,4 +201,15 @@ describe('OTA update', () => {
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 152});
|
||||
});
|
||||
|
||||
it('Shouldnt respond with NO_IMAGE_AVAILABLE when not supporting OTA and device has no OTA endpoint', async () => {
|
||||
const device = zigbeeHerdsman.devices.QBKG03LM;
|
||||
const data = {imageType: 12382};
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
|
||||
logger.error.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(0);
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ const expectNothingPublished = () => {
|
||||
})
|
||||
}
|
||||
|
||||
describe('Entity publish', () => {
|
||||
describe('Publish', () => {
|
||||
let controller;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -249,11 +249,11 @@ describe('Entity publish', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({color: {x: 100, y: 50}, brightness: 20, state: 'OFF'}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(2);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}, {});
|
||||
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {});
|
||||
expect(endpoint.command).toHaveBeenNthCalledWith(1, "lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {});
|
||||
expect(endpoint.command).toHaveBeenNthCalledWith(2, "genOnOff", "off", {}, {});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 100, y: 50}, color_temp: 62});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 100, y: 50}, color_temp: 62, state: 'OFF'});
|
||||
@@ -1051,4 +1051,31 @@ describe('Entity publish', () => {
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(4);
|
||||
expect(endpoint.command.mock.calls[3]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 123, transtime: 10}, {}]);
|
||||
});
|
||||
|
||||
it('Shouldnt use moveToLevelWithOnOff on turn on when no transition has been used as some devices do not turn on in that case', async () => {
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/3332
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({'state': 'ON'}));
|
||||
await flushPromises();
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({'brightness': 150}));
|
||||
await flushPromises();
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({'state': 'OFF'}));
|
||||
await flushPromises();
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({'state': 'ON'}));
|
||||
await flushPromises();
|
||||
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(4);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]);
|
||||
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 150, transtime: 0}, {}]);
|
||||
expect(endpoint.command.mock.calls[2]).toEqual(["genOnOff", "off", {}, {}]);
|
||||
expect(endpoint.command.mock.calls[3]).toEqual(["genOnOff", "on", {}, {}]);
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(4);
|
||||
expect(MQTT.publish.mock.calls[0]).toEqual([ 'zigbee2mqtt/bulb_color', '{"state":"ON"}', { qos: 0, retain: false }, expect.any(Function)]);
|
||||
expect(MQTT.publish.mock.calls[1]).toEqual([ 'zigbee2mqtt/bulb_color', '{"state":"ON","brightness":150}', { qos: 0, retain: false }, expect.any(Function)]);
|
||||
expect(MQTT.publish.mock.calls[2]).toEqual([ 'zigbee2mqtt/bulb_color', '{"state":"OFF","brightness":150}', { qos: 0, retain: false }, expect.any(Function)]);
|
||||
expect(MQTT.publish.mock.calls[3]).toEqual([ 'zigbee2mqtt/bulb_color', '{"state":"ON","brightness":150}', { qos: 0, retain: false }, expect.any(Function)]);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ const flushPromises = () => new Promise(setImmediate);
|
||||
|
||||
const mocksClear = [MQTT.publish, logger.warn, logger.debug];
|
||||
|
||||
describe('Device receive', () => {
|
||||
describe('Receive', () => {
|
||||
let controller;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -33,23 +33,6 @@ describe('Device receive', () => {
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
|
||||
it('Should handle a zigbee message and counter it when Home Assistant integration is enabled', async () => {
|
||||
settings.set(['homeassistant'], true);
|
||||
const device = zigbeeHerdsman.devices.WXKG11LM;
|
||||
const data = {onOff: 1}
|
||||
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'single', linkquality: 10});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/button');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({click: '', linkquality: 10});
|
||||
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message which uses ep (left)', async () => {
|
||||
const device = zigbeeHerdsman.devices.WXKG02LM;
|
||||
const data = {onOff: 1}
|
||||
@@ -19,7 +19,7 @@ const debounce = require('debounce');
|
||||
|
||||
const mocksClear = [MQTT.publish, logger.warn, logger.debug, debounce];
|
||||
|
||||
describe('Device report', () => {
|
||||
describe('Report', () => {
|
||||
let controller;
|
||||
|
||||
function expectOnOffBrightnessColorReport(endpoint, colorXY) {
|
||||
@@ -125,6 +125,12 @@ describe('Settings', () => {
|
||||
|
||||
settings._write();
|
||||
expect(read(configurationFile)).toStrictEqual(contentConfiguration);
|
||||
expect(read(secretFile)).toStrictEqual(contentSecret);
|
||||
|
||||
settings.set(['mqtt', 'user'], 'test123');
|
||||
settings.set(['advanced', 'network_key'], [1,2,3, 4]);
|
||||
expect(read(configurationFile)).toStrictEqual(contentConfiguration);
|
||||
expect(read(secretFile)).toStrictEqual({...contentSecret, username: 'test123', network_key: [1,2,3,4]});
|
||||
});
|
||||
|
||||
it('Should read devices form a separate file', () => {
|
||||
@@ -489,6 +495,19 @@ describe('Settings', () => {
|
||||
}).toThrow(new Error("Device '0x123' already exists"));
|
||||
});
|
||||
|
||||
it('Should not allow any string values for network_key', () => {
|
||||
write(configurationFile, {
|
||||
advanced: {network_key: 'NOT_GENERATE'},
|
||||
});
|
||||
|
||||
settings._reRead();
|
||||
|
||||
expect(() => {
|
||||
settings.validate();
|
||||
}).toThrowError(`advanced.network_key: should be array or 'GENERATE' (is 'NOT_GENERATE')`);
|
||||
});
|
||||
|
||||
|
||||
it('Should not allow retention configuration without MQTT v5', () => {
|
||||
write(configurationFile, {
|
||||
devices: {'0x0017880104e45519': {friendly_name: 'tain', retention: 900}},
|
||||
|
||||
@@ -214,6 +214,7 @@ writeDefaultState();
|
||||
|
||||
module.exports = {
|
||||
mockDir,
|
||||
read: () => yaml.read(path.join(mockDir, 'configuration.yaml')),
|
||||
writeDefaultConfiguration,
|
||||
writeDefaultState,
|
||||
removeState,
|
||||
|
||||
Reference in New Issue
Block a user