Compare commits

..
50 Commits
Author SHA1 Message Date
Koen Kanters 9009de24ac 1.14.0 2020-06-10 16:42:27 +02:00
Koen Kanters a5b0c80a2e Undev 2020-06-10 16:42:24 +02:00
Koen Kanters d9391ac685 Update herdsman 2020-06-10 16:41:08 +02:00
Koen Kanters 2bdb0f9ae1 Merge branch 'master' into dev 2020-06-10 16:36:37 +02:00
Koen Kanters 56714d7c5a Update converters. 2020-06-09 19:52:48 +02:00
Koen Kanters 7dee99df52 Add colortemp_move for group. #3705 2020-06-09 19:37:45 +02:00
Koen Kanters e5f9a969ac Discovery Home Assistant battery sensor for Danalock V3. #3702 2020-06-09 19:24:47 +02:00
Koen Kanters 206a45d29a Update converters. 2020-06-07 13:58:28 +02:00
sprockersandKoen Kanters c14e18dabb Adding homeassistant mapping for Hive DWS003 contact sensor (#3682)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-06-07 13:25:52 +02:00
sprockers 3566af1b08 Removing illuminance from Hive MOT003 (#3685) 2020-06-07 13:23:45 +02:00
Mark BergsmaandKoen Kanters a7adfebcb3 Add discovery support for the Plugwise Plug 160-01 (#3679)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-06-07 11:36:04 +02:00
Koen Kanters f8ea6cb574 Update converters. 2020-06-06 14:45:43 +02:00
Mattia480andKoen Kanters d66e1c322b Add EchoStar SAGE by Hughes Doorbell Sensor (SAGE206612) (#3675)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-06-06 14:12:41 +02:00
Koen Kanters 43127e95a9 Fix lint 2020-06-06 13:13:09 +02:00
Koen Kanters 976e802af8 Update converters. 2020-06-06 10:31:50 +02:00
Dominik Röttsches 32b96d1d4c Add homeassistant mapping for Ledvance Z3 Tunable White Panel (#3666)
See https://github.com/Koenkk/zigbee-herdsman-converters/pull/1279
2020-06-06 09:52:58 +02:00
Koen Kanters 7f68fd3fc4 Initial syslog logging. #3647 2020-06-03 20:44:11 +02:00
Koen Kanters 6d9dff8832 Mark node 14 as supported. #3660 2020-06-03 19:37:11 +02:00
Koen Kanters 8dc535e3e3 Update converters 2020-06-02 18:45:29 +02:00
Koen Kanters fda1246fca Attempt to fix lastSeen incorrect leading to incorrect offline state. #3650 2020-06-01 16:38:24 +02:00
Koen Kanters a9b54ad668 Update converters 2020-06-01 16:37:16 +02:00
Koen Kanters c0d4aea5bd Fix lint 2020-05-31 15:02:25 +02:00
Koen Kanters d440de8069 Fix networkmap error when using Gledopto GL-C-008-2ID. #3623 2020-05-30 18:27:25 +02:00
Koen Kanters e4c3ffe38d Update converters 2020-05-30 17:11:54 +02:00
Will UtherandKoen Kanters 7c045af987 Add a brief internal architecture description. (#3639)
* Add Stitchy Dim to list of devices.

* Add an internal architecture paragraph.

Add a README paragraph with a basic architecture description.  This doesn't have a lot of detail, but links to docs with (much) more detail.

* Try to line up with upstream.

* Update README.md

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-05-30 16:03:57 +02:00
Koen Kanters 2cb234c934 Update dependencies. #3616 2020-05-30 16:01:47 +02:00
Crowbar ZandKoen Kanters 51a6b9226d Remove precision rounding from HA MQTT autodiscovery template (#3636)
* Remove precision rounding from HA MQTT autodiscovery template

Precision rounding should be handled in converters and made available to
all MQTT consumers (not just HA)

* Update homeassistant.test.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-05-29 23:13:09 +02:00
Koen Kanters 7d1a97841c Update herdsman and converters. 2020-05-29 23:09:09 +02:00
JaydenandKoen Kanters 9c71ba8754 Support E1E-G7F (#3630)
I couldn't see battery data in the output so I have only added the sensor_action type. This works and home assistant can see button presses + link quality.

This is linked to: https://github.com/Koenkk/zigbee-herdsman-converters/pull/1265

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-05-29 19:39:32 +02:00
Koen Kanters 5f63dcb0f2 Fix old state being published due to debounce. #3572 2020-05-29 19:24:59 +02:00
Koen Kanters 89a6c976cb Update converters 2020-05-28 19:49:38 +02:00
Koen Kanters f793eab4ab Republish availability on MQTT connected. #3625 2020-05-28 17:44:50 +02:00
Jonas Bergler d5604ab5ec Make thermostat more like thermostatHeatCool (#3613)
* Make thermostat more like thermostatHeatCool

This change tries to unify the two thermostat templates a little.
I'm making this change because I wanted to enable tracking the current
state of the Zen thermostat and noticed that the logic was very specific
to a subset of thermostats.

The matching change in zigbee-herdsman-converters ensures that the
thermostats that were sending the "operation" field use the new format
and should effectively behave the same after the change.

* 0.5 degree steps for Zen thermostat

Because it supports it.
2020-05-27 16:26:55 +02:00
Koen Kanters 780246fe74 Fix typo 2020-05-26 17:36:04 +02:00
Koen Kanters b706233604 Fix lint. 2020-05-25 22:05:34 +02:00
Koen Kanters c4ce339942 Update converters. 2020-05-25 22:04:30 +02:00
Koen Kanters 409fb2407a #3281 Add tests for new bridge API 2020-05-24 18:16:39 +02:00
Koen Kanters 1638b81723 Update converters 2020-05-24 15:57:29 +02:00
Koen Kanters 396bdf5592 Update converters 2020-05-24 14:41:40 +02:00
sswongandKoen Kanters 89b9b84f94 add support for QBKG22LM-aqara-double-key-wall-switch-without-neutral (#3600)
* add support for QBKG22LM-aqara-double-key-wall-switch-without-neutral (updated model variant)

* removed tab

* Update homeassistant.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-05-24 14:19:51 +02:00
Koen Kanters 770a7e77e7 Update converters 2020-05-23 20:53:20 +02:00
Koen Kanters 49df5751b2 Log friendly error when endpoint does not exist on device. https://github.com/Koenkk/zigbee-herdsman-converters/issues/125 2020-05-23 20:48:07 +02:00
f594664f55 add support for WXKG06LM-aqara-single-key-wireless-wall-switch (#3593)
* add support for WXKG06LM-aqara-single-key-wireless-wall-switch

* Update homeassistant.js

Co-authored-by: sswong <saiseng.wong@wizvision.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2020-05-22 18:33:05 +02:00
Koen Kanters d9b8e1fc18 Fix messages being skipped for Xiaomi devices with Deconz adapter. #3592 2020-05-22 18:25:32 +02:00
Koen Kanters 94dbf9c505 Fix not able to read state from multiple endpoints at once. https://github.com/Koenkk/zigbee-herdsman-converters/issues/1252 2020-05-22 18:16:53 +02:00
Koen Kanters 83215c3290 Update converters. 2020-05-21 15:47:01 +02:00
Andrew N Golovkov adcec74f23 added Aqara Opple MX650 aka XDD12LM (#3579) 2020-05-21 14:55:37 +02:00
Koen Kanters 7e6c508263 Fix number action causing exception in Home Assistant extesion. #3578 2020-05-21 14:52:02 +02:00
Koen Kanters b2034943a5 Update converters. 2020-05-21 12:21:15 +02:00
Koen Kanters 6e382516cb Dev 2020-05-20 16:52:49 +02:00
20 changed files with 2213 additions and 2335 deletions
+3
View File
@@ -66,6 +66,9 @@ Zigbee2mqtt integrates well with (almost) every home automation solution because
## Architecture
![Architecture](images/architecture.png)
### Internal Architecture
Zigbee2mqtt is made up of three modules, each developed in its own Github project. Starting from the hardware (adapter) and moving up; [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) connects to your Zigbee adapter an makes an API available to the higher levels of the stack. For e.g. Texas Instruments hardware, zigbee-herdsman uses the [TI zStack monitoring and test API](https://github.com/koenkk/zigbee-herdsman/raw/master/docs/Z-Stack%20Monitor%20and%20Test%20API.pdf) to communicate with the adapter. Zigbee-herdsman handles the core Zigbee communication. The module [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters) handles the mapping from individual device models to the Zigbee clusters they support. [Zigbee clusters](https://github.com/Koenkk/zigbee-herdsman/raw/master/docs/07-5123-06-zigbee-cluster-library-specification.pdf) are the layers of the Zigbee protocol on top of the base protocol that define things like how lights, sensors and switches talk to each other over the Zigbee network. Finally, the zigbee2mqtt module drives zigbee-herdsman and maps the zigbee messages to MQTT messages. Zigbee2mqtt also keeps track of the state of the system. It uses a `database.db` file to store this state; a text file with a JSON database of connected devices and their capabilities.
## Supported devices
See [Supported devices](https://www.zigbee2mqtt.io/information/supported_devices.html) to check whether your device is supported. There is quite an extensive list, including devices from vendors like Xiaomi, Ikea, Philips, OSRAM and more.
+6 -4
View File
@@ -19,7 +19,7 @@ const ExtensionHomeAssistant = require('./extension/homeassistant');
const ExtensionConfigure = require('./extension/configure');
const ExtensionDeviceGroupMembership = require('./extension/legacy/deviceGroupMembership');
const ExtensionBridgeLegacy = require('./extension/legacy/bridgeLegacy');
// const ExtensionBridge = require('./extension/bridge');
const ExtensionBridge = require('./extension/bridge');
const ExtensionGroups = require('./extension/groups');
const ExtensionAvailability = require('./extension/availability');
const ExtensionBind = require('./extension/bind');
@@ -49,10 +49,12 @@ class Controller {
new ExtensionBind(...args),
new ExtensionOnEvent(...args),
new ExtensionOTAUpdate(...args),
// new ExtensionBridge(...args),
];
/* istanbul ignore else */
if (settings.get().experimental.new_api) {
this.extensions.push(new ExtensionBridge(...args));
}
if (settings.get().advanced.legacy_api) {
this.extensions.push(new ExtensionBridgeLegacy(...args));
}
@@ -317,7 +319,7 @@ class Controller {
}
}
this.eventBus.emit('publishEntityState', {payload: messagePayload, entity: resolvedEntity});
this.eventBus.emit('publishEntityState', {payload: messagePayload, entity: resolvedEntity, stateChangeReason});
}
async iteratePayloadAttributeOutput(topicRoot, payload, options) {
+6 -5
View File
@@ -58,13 +58,14 @@ class Availability extends Extension {
onMQTTConnected() {
for (const device of this.zigbee.getClients()) {
// Mark all devices as online on start
this.publishAvailability(device, true);
const ieeeAddr = device.ieeeAddr;
this.publishAvailability(device, this.state.hasOwnProperty(ieeeAddr) ? this.state[ieeeAddr] : true, true);
if (this.inWhitelistOrNotInBlacklist(device)) {
if (this.isPingable(device)) {
this.setTimerPingable(device);
} else {
this.timers[device.ieeeAddr] = setInterval(() => {
this.timers[ieeeAddr] = setInterval(() => {
this.handleIntervalNotPingable(device);
}, utils.secondsToMilliseconds(300));
}
@@ -94,12 +95,12 @@ class Availability extends Extension {
}
async handleIntervalNotPingable(device) {
const ago = Date.now() - device.lastSeen;
const resolvedEntity = this.zigbee.resolveEntity(device.ieeeAddr);
if (!resolvedEntity || !device.lastSeen) {
return;
}
const ago = Date.now() - resolvedEntity.device.lastSeen;
logger.debug(`Non-pingable device '${resolvedEntity.name}' was last seen '${ago / 1000}' seconds ago.`);
if (ago > Hours25) {
@@ -143,7 +144,7 @@ class Availability extends Extension {
}
}
publishAvailability(device, available) {
publishAvailability(device, available, force=false) {
const ieeeAddr = device.ieeeAddr;
if (this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] && available) {
this.onReconnect(device);
@@ -153,7 +154,7 @@ class Availability extends Extension {
const name = deviceSettings ? deviceSettings.friendlyName : ieeeAddr;
const topic = `${name}/availability`;
const payload = available ? 'online' : 'offline';
if (this.state[ieeeAddr] !== available) {
if (this.state[ieeeAddr] !== available || force) {
this.state[ieeeAddr] = available;
this.mqtt.publish(topic, payload, {retain: true, qos: 0});
}
+67 -68
View File
@@ -1,5 +1,3 @@
/* istanbul ignore file newApi */
const logger = require('../util/logger');
const utils = require('../util/utils');
const Extension = require('./extension');
@@ -8,22 +6,25 @@ const settings = require('../util/settings');
const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
class BridgeLegacy extends Extension {
class Bridge extends Extension {
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
this.requestLookup = {
'permitjoin': this.requestPermitJoin.bind(this),
'device/remove': this.deviceRemove.bind(this),
'device/forceremove': this.deviceForceRemove.bind(this),
'device/ban': this.deviceBan.bind(this),
'group/remove': this.groupRemove.bind(this),
'permitjoin': this.permitJoin.bind(this),
// 'device/remove': this.deviceRemove.bind(this),
// 'device/forceremove': this.deviceForceRemove.bind(this),
// 'device/ban': this.deviceBan.bind(this),
// 'group/remove': this.groupRemove.bind(this),
};
}
async onMQTTConnected() {
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/+`);
this.zigbee2mqttVersion = await utils.getZigbee2mqttVersion();
this.coordinatorVersion = await this.zigbee.getCoordinatorVersion();
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/#`);
await this.publishInfo();
await this.publishDevices();
await this.publishGroups();
@@ -74,23 +75,23 @@ class BridgeLegacy extends Extension {
* Requests
*/
async deviceRemove(message) {
return this.removeForceRemoveOrBanEntity('remove', 'device', message);
}
// async deviceRemove(message) {
// return this.removeForceRemoveOrBanEntity('remove', 'device', message);
// }
async deviceForceRemove(message) {
return this.removeForceRemoveOrBanEntity('force_remove', 'device', message);
}
// async deviceForceRemove(message) {
// return this.removeForceRemoveOrBanEntity('force_remove', 'device', message);
// }
async deviceBan(message) {
return this.removeForceRemoveOrBanEntity('ban', 'device', message);
}
// async deviceBan(message) {
// return this.removeForceRemoveOrBanEntity('ban', 'device', message);
// }
async groupRemove(message) {
return this.removeForceRemoveOrBanEntity('remove', 'group', message);
}
// async groupRemove(message) {
// return this.removeForceRemoveOrBanEntity('remove', 'group', message);
// }
async requestPermitJoin(message) {
async permitJoin(message) {
const value = typeof message === 'object' ? message.value : message;
await this.zigbee.permitJoin(value);
await this.publishInfo();
@@ -101,59 +102,57 @@ class BridgeLegacy extends Extension {
* Utils
*/
async removeForceRemoveOrBanEntity(action, entityType, message) {
const ID = typeof message === 'object' ? message.ID : message.trim();
const entity = this.zigbee.resolveEntity(ID);
if (!entity || entity.type !== entityType) {
throw new Error(`${ID} is not a ${entityType}`);
}
// async removeForceRemoveOrBanEntity(action, entityType, message) {
// const ID = typeof message === 'object' ? message.ID : message.trim();
// const entity = this.zigbee.resolveEntity(ID);
// if (!entity || entity.type !== entityType) {
// throw new Error(`${ID} is not a ${entityType}`);
// }
const lookup = {
ban: ['banned', 'Banning', 'ban'],
force_remove: ['force_removed', 'Force removing', 'force remove'],
remove: ['removed', 'Removing', 'remove'],
};
// const lookup = {
// ban: ['banned', 'Banning', 'ban'],
// force_remove: ['force_removed', 'Force removing', 'force remove'],
// remove: ['removed', 'Removing', 'remove'],
// };
try {
logger.info(`${lookup[action][1]} '${entity.settings.friendlyName}'`);
if (entity.type === 'device') {
if (action === 'ban') {
settings.banDevice(entity.settings.ID);
}
// try {
// logger.info(`${lookup[action][1]} '${entity.settings.friendlyName}'`);
// if (entity.type === 'device') {
// if (action === 'ban') {
// settings.banDevice(entity.settings.ID);
// }
action === 'force_remove' ?
await entity.device.removeFromDatabase() : await entity.device.removeFromNetwork();
} else {
await entity.group.removeFromDatabase();
}
// action === 'force_remove' ?
// await entity.device.removeFromDatabase() : await entity.device.removeFromNetwork();
// } else {
// await entity.group.removeFromDatabase();
// }
// Fire event
if (entity.type === 'device') {
this.eventBus.emit('deviceRemoved', {device: entity.device});
}
// // Fire event
// if (entity.type === 'device') {
// this.eventBus.emit('deviceRemoved', {device: entity.device});
// }
// Remove from configuration.yaml
entity.type === 'device' ?
settings.removeDevice(entity.settings.ID) : settings.removeGroup(entity.settings.ID);
// // Remove from configuration.yaml
// entity.type === 'device' ?
// settings.removeDevice(entity.settings.ID) : settings.removeGroup(entity.settings.ID);
// Remove from state
this.state.remove(entity.settings.ID);
// // Remove from state
// this.state.remove(entity.settings.ID);
logger.info(`Successfully ${lookup[action][0]} ${entity.settings.friendlyName}`);
entity.type === 'device' ? this.publishDevices() : this.publishGroups();
return utils.getResponse(message, {ID}, null);
} catch (error) {
throw new Error(`Failed to ${lookup[action][2]} ${entity.settings.friendlyName} (${error})`);
}
}
// logger.info(`Successfully ${lookup[action][0]} ${entity.settings.friendlyName}`);
// entity.type === 'device' ? this.publishDevices() : this.publishGroups();
// return utils.getResponse(message, {ID}, null);
// } catch (error) {
// throw new Error(`Failed to ${lookup[action][2]} ${entity.settings.friendlyName} (${error})`);
// }
// }
async publishInfo() {
const info = await utils.getZigbee2mqttVersion();
const coordinator = await this.zigbee.getCoordinatorVersion();
const payload = {
version: info.version,
commit: info.commitHash,
coordinator,
version: this.zigbee2mqttVersion.version,
commit: this.zigbee2mqttVersion.commitHash,
coordinator: this.coordinatorVersion,
logLevel: logger.getLevel(),
permitJoin: await this.zigbee.getPermitJoin(),
};
@@ -177,7 +176,7 @@ class BridgeLegacy extends Extension {
type: device.type,
networkAddress: device.networkAddress,
supported: !!definition,
friendlyName: resolved.settings.friendlyName,
friendlyName: resolved.name,
definition: definitionPayload,
powerSource: device.powerSource,
softwareBuildID: device.softwareBuildID,
@@ -195,7 +194,7 @@ class BridgeLegacy extends Extension {
const resolved = this.zigbee.resolveEntity(group);
return {
ID: group.groupID,
friendlyName: resolved.settings.friendlyName,
friendlyName: resolved.name,
members: group.members.map((m) => {
return {
ieeeAddress: m.deviceIeeeAddress,
@@ -209,4 +208,4 @@ class BridgeLegacy extends Extension {
}
}
module.exports = BridgeLegacy;
module.exports = Bridge;
+65 -23
View File
@@ -422,6 +422,20 @@ const cfg = {
icon: 'mdi:brightness-5',
},
},
'sensor_radioactive_events_per_minute': {
type: 'sensor',
object_id: 'radioactive_events_per_minute',
discovery_payload: {
value_template: '{{ value_json.radioactive_events_per_minute }}',
},
},
'sensor_radiation_dose_per_hour': {
type: 'sensor',
object_id: 'radiation_dose_per_hour',
discovery_payload: {
value_template: '{{ value_json.radiation_dose_per_hour }}',
},
},
// Light
'light_brightness_colorxy_white': {
@@ -696,7 +710,9 @@ const thermostat = (minTemp=7, maxTemp=30, temperatureStateProperty='occupied_he
temperature_command_topic: temperatureStateProperty,
temp_step: tempStep,
action_topic: true,
action_template: '{{ value_json.operation }}',
action_template:
'{% set values = {\'idle\':\'off\',\'heat\':\'heating\',\'cool\':\'cooling\',\'fan only\':\'fan\'}'+
' %}{{ values[value_json.running_state] }}',
},
};
};
@@ -742,6 +758,7 @@ const mapping = {
'WXKG12LM': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action],
// DEPRECATED; BREAKING_IMPROVEMENT: only use sensor_click for WXKG03LM (action hold -> click hold)
'WXKG03LM': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action],
'WXKG06LM': [cfg.sensor_battery, cfg.sensor_action],
'WXKG02LM': [cfg.sensor_click, cfg.sensor_battery],
'QBKG04LM': [cfg.switch, cfg.sensor_click, cfg.sensor_action],
'QBKG03LM': [switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_click, cfg.sensor_temperature],
@@ -758,7 +775,7 @@ const mapping = {
'LED1545G12': [cfg.light_brightness_colortemp],
'LED1623G12': [cfg.light_brightness],
'LED1622G12': [cfg.light_brightness],
'LED1537R6': [cfg.light_brightness_colortemp],
'LED1537R6/LED1739R5': [cfg.light_brightness_colortemp],
'LED1650R5': [cfg.light_brightness],
'LED1536G5': [cfg.light_brightness_colortemp],
'7299760PH': [cfg.light_brightness_colorxy],
@@ -819,6 +836,7 @@ const mapping = {
'AE 260': [cfg.light_brightness],
'AA68199': [cfg.light_brightness_colortemp],
'QBKG11LM': [cfg.switch, cfg.sensor_power, cfg.sensor_click, cfg.sensor_temperature],
'QBKG22LM': [cfg.switch, cfg.sensor_click, cfg.sensor_temperature],
'QBKG12LM': [
switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_power, cfg.sensor_click,
cfg.sensor_temperature,
@@ -933,9 +951,9 @@ const mapping = {
'ZPIR-8000': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
'ZCTS-808': [cfg.binary_sensor_contact, cfg.sensor_battery],
'ZNLDP12LM': [cfg.light_brightness_colortemp],
'XDD12LM': [cfg.light_brightness_colortemp],
'D1821': [cfg.light_brightness_colortemp_colorxy],
'ZNCLDJ11LM': [cfg.cover_position, cfg.sensor_cover],
'owvfni3': [cfg.cover_position],
'TS0601': [cfg.cover_position],
'LTFY004': [cfg.light_brightness_colorxy],
'GL-S-007Z': [cfg.light_brightness_colortemp_colorxy],
@@ -1000,6 +1018,7 @@ const mapping = {
'E1746': [],
'LED1836G9': [cfg.light_brightness],
'YRD426NRSC': [cfg.lock, cfg.sensor_battery],
'BE468': [cfg.lock, cfg.sensor_battery],
'YRD246HA20BP': [cfg.lock, cfg.sensor_battery],
'E1743': [cfg.sensor_click, cfg.sensor_battery],
'LED1732G11': [cfg.light_brightness_colortemp],
@@ -1137,7 +1156,7 @@ const mapping = {
'SCM-5ZBS': [cfg.cover_position],
'YRD226HA2619': [cfg.sensor_battery, cfg.lock],
'YMF40/YDM4109+': [cfg.lock, cfg.sensor_battery],
'V3-BTZB': [cfg.lock],
'V3-BTZB': [cfg.lock, cfg.sensor_battery],
'3RSS008Z': [cfg.switch, cfg.sensor_battery],
'3RSS007Z': [cfg.switch],
'99432': [cfg.fan, cfg.light_brightness],
@@ -1243,7 +1262,7 @@ const mapping = {
'TH1124ZB': [thermostat()],
'TH1400ZB': [thermostat()],
'TH1500ZB': [thermostat()],
'Zen-01-W': [thermostat()],
'Zen-01-W': [thermostat(10, 30, 'occupied_heating_setpoint', 0.5)],
'9290022166': [cfg.light_brightness_colortemp_colorxy],
'PM-C140-ZB': [cfg.sensor_power, cfg.switch],
'PM-B530-ZB': [cfg.sensor_power, cfg.switch],
@@ -1418,9 +1437,9 @@ const mapping = {
'WV704R0A0902': [thermostat()],
'067776': [cfg.cover_position],
'067773': [cfg.sensor_action, cfg.sensor_battery],
'067771': [cfg.switch],
'067771': [cfg.light_brightness],
'064873': [cfg.sensor_action],
'K4003C': [cfg.switch],
'K4003C': [cfg.switch, cfg.sensor_action],
'STZB402': [
thermostat(5, 30, 'occupied_heating_setpoint', 0.5),
cfg.sensor_local_temperature,
@@ -1485,7 +1504,7 @@ const mapping = {
'10011725': [cfg.light_brightness_colortemp_colorxy],
'929002277501': [cfg.light_brightness],
'RS 230 C': [cfg.light_brightness_colortemp_colorxy],
'LED1903C5': [cfg.light_brightness_colortemp],
'LED1903C5/LED1835C6': [cfg.light_brightness_colortemp],
'1402755': [cfg.light_brightness],
'4503848C5': [cfg.light_brightness_colortemp],
'500.48': [cfg.light_brightness],
@@ -1556,6 +1575,7 @@ const mapping = {
'4080248P9': [cfg.light_brightness_colortemp_colorxy],
'4080148P9': [cfg.light_brightness_colortemp_colorxy],
'4058075148338': [cfg.light_brightness_colortemp],
'4058075181472': [cfg.light_brightness_colortemp],
'484719': [cfg.light_brightness],
'SEB01ZB': [cfg.binary_sensor_sos, cfg.sensor_battery],
'SBM01ZB': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
@@ -1586,10 +1606,8 @@ const mapping = {
'511.040': [cfg.light_brightness_colortemp_colorxy],
'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],
'MOT003': [
cfg.sensor_temperature, cfg.binary_sensor_occupancy, cfg.sensor_illuminance,
cfg.sensor_illuminance_lux, cfg.binary_sensor_battery_low, cfg.sensor_battery,
],
'DWS003': [cfg.binary_sensor_contact, cfg.sensor_battery, cfg.binary_sensor_battery_low, cfg.sensor_temperature],
'MOT003': [cfg.binary_sensor_occupancy, cfg.sensor_temperature, cfg.sensor_battery, cfg.binary_sensor_battery_low],
'HALIGHTDIMWWE14': [cfg.light_brightness],
'GreenPower_On_Off_Switch': [cfg.sensor_action],
'GreenPower_7': [cfg.sensor_action],
@@ -1624,7 +1642,7 @@ const mapping = {
'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],
'AU-A1ZBPIRS': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low, cfg.sensor_illuminance_lux],
'TS0121': [cfg.switch],
'ZK03840': [thermostat()],
'ZS1100400-IN-V1A02': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
@@ -1661,6 +1679,39 @@ const mapping = {
'LH-990ZB': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
'HO-09ZB': [cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
'500.67': [cfg.sensor_action],
'E1E-G7F': [cfg.sensor_action],
'LH-990F': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
'QBKG25LM': [
switchEndpoint('left'), switchEndpoint('center'), switchEndpoint('right'), cfg.sensor_action,
cfg.sensor_power,
],
'QBKG24LM': [switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_power],
'WS-USC01': [cfg.switch],
'WS-USC02': [switchEndpoint('top'), switchEndpoint('bottom')],
'WS-USC04': [switchEndpoint('top'), switchEndpoint('bottom')],
'100.462.31': [cfg.sensor_action],
'SN10ZW': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
'AU-A1ZBPIAB': [cfg.switch, cfg.sensor_voltage, cfg.sensor_current, cfg.sensor_power],
'AU-A1ZBDWS': [cfg.binary_sensor_contact, cfg.sensor_battery],
'4058075816459': [cfg.sensor_action],
'14592.0': [cfg.switch],
'73699': [cfg.light_brightness_colorxy],
'SAGE206612': [cfg.sensor_action, cfg.sensor_battery],
'TI0001-switch': [cfg.switch],
'TI0001-socket': [cfg.switch],
'9290022891': [cfg.light_brightness_colortemp_colorxy],
'160-01': [cfg.switch, cfg.sensor_power],
'ZS232000178': [cfg.sensor_action],
'mcdj3aq': [cfg.cover_position],
'DIYRuZ_Geiger': [cfg.sensor_radioactive_events_per_minute, cfg.sensor_radiation_dose_per_hour, cfg.sensor_action],
'8718696170557': [cfg.light_brightness_colortemp_colorxy],
'12127': [switchEndpoint('l1'), switchEndpoint('l2')],
'SWO-MOS1PA': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
'STS-IRM-251': [cfg.sensor_temperature, cfg.binary_sensor_occupancy, cfg.sensor_battery],
'WSDCGQ12LM': [cfg.sensor_temperature, cfg.sensor_pressure, cfg.sensor_humidity, cfg.sensor_battery],
'SJCGQ12LM': [cfg.sensor_battery, cfg.binary_sensor_water_leak],
'DJT12LM': [cfg.sensor_action],
'AV2010/22A': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
};
/**
@@ -1757,7 +1808,7 @@ class HomeAssistant extends Extension {
this.discoveredTriggers[device.ieeeAddr] = new Set();
}
const value = data.payload[key];
const value = data.payload[key].toString();
const discoveredKey = `${key}_${value}`;
if (!this.discoveredTriggers[device.ieeeAddr].has(discoveredKey)) {
@@ -1893,15 +1944,6 @@ class HomeAssistant extends Extension {
payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`;
}
// Add precision to value_template
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}) }}`;
payload.value_template = template;
}
if (payload.command_topic) {
payload.command_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/`;
+14 -15
View File
@@ -1,6 +1,5 @@
const settings = require('../util/settings');
const utils = require('../util/utils');
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const logger = require('../util/logger');
const Extension = require('./extension');
@@ -57,32 +56,32 @@ class NetworkMap extends Extension {
let text = 'digraph G {\nnode[shape=record];\n';
let style = '';
topology.nodes.forEach((device) => {
topology.nodes.forEach((node) => {
const labels = [];
// Add friendly name
labels.push(`${device.friendlyName}`);
labels.push(`${node.friendlyName}`);
// Add the device short network address, ieeaddr and scan note (if any)
labels.push(
`${device.ieeeAddr} (${device.networkAddress})` +
((device.failed && device.failed.length) ? `failed: ${device.failed.join(',')}` : ''),
`${node.ieeeAddr} (${node.networkAddress})` +
((node.failed && node.failed.length) ? `failed: ${node.failed.join(',')}` : ''),
);
// Add the device model
if (device.type !== 'Coordinator') {
const definition = zigbeeHerdsmanConverters.findByDevice(device);
if (node.type !== 'Coordinator') {
const definition = this.zigbee.resolveEntity(node.ieeeAddr).definition;
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}`);
labels.push(`${node.manufacturerName} ${node.modelID}`);
}
}
// Add the device last_seen timestamp
let lastSeen = 'unknown';
const date = device.type === 'Coordinator' ? Date.now() : device.lastSeen;
const date = node.type === 'Coordinator' ? Date.now() : node.lastSeen;
if (date) {
lastSeen = utils.formatDate(date, 'ISO_8601_local');
}
@@ -90,10 +89,10 @@ class NetworkMap extends Extension {
labels.push(lastSeen);
// Shape the record according to device type
if (device.type == 'Coordinator') {
if (node.type == 'Coordinator') {
style = `style="bold, filled", fillcolor="${colors.fill.coordinator}", ` +
`fontcolor="${colors.font.coordinator}"`;
} else if (device.type == 'Router') {
} else if (node.type == 'Router') {
style = `style="rounded, filled", fillcolor="${colors.fill.router}", ` +
`fontcolor="${colors.font.router}"`;
} else {
@@ -102,22 +101,22 @@ class NetworkMap extends Extension {
}
// Add the device with its labels to the graph as a node.
text += ` "${device.ieeeAddr}" [`+style+`, label="{${labels.join('|')}}"];\n`;
text += ` "${node.ieeeAddr}" [`+style+`, label="{${labels.join('|')}}"];\n`;
/**
* Add an edge between the device and its child to the graph
* NOTE: There are situations where a device is NOT in the topology, this can be e.g.
* due to not responded to the lqi scan. In that case we do not add an edge for this device.
*/
topology.links.filter((e) => (e.source.ieeeAddr === device.ieeeAddr)).forEach((e) => {
const lineStyle = (device.type=='EndDevice') ? 'penwidth=1, ' :
topology.links.filter((e) => (e.source.ieeeAddr === node.ieeeAddr)).forEach((e) => {
const lineStyle = (node.type=='EndDevice') ? 'penwidth=1, ' :
(!e.routes.length) ? 'penwidth=0.5, ' : 'penwidth=2, ';
const lineWeight = (!e.routes.length) ? `weight=0, color="${colors.line.inactive}", ` :
`weight=1, color="${colors.line.active}", `;
const textRoutes = e.routes.map((r) => r.destinationAddress);
const lineLabels = (!e.routes.length) ? `label="${e.linkquality}"` :
`label="${e.linkquality} (routes: ${textRoutes.join(',')})"`;
text += ` "${device.ieeeAddr}" -> "${e.target.ieeeAddr}"`;
text += ` "${node.ieeeAddr}" -> "${e.target.ieeeAddr}"`;
text += ` [${lineStyle}${lineWeight}${lineLabels}]\n`;
});
});
+11 -3
View File
@@ -18,6 +18,7 @@ const groupConverters = [
zigbeeHerdsmanConverters.toZigbeeConverters.thermostat_occupied_heating_setpoint,
zigbeeHerdsmanConverters.toZigbeeConverters.tint_scene,
zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_move,
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_move,
];
class EntityPublish extends Extension {
@@ -143,7 +144,7 @@ class EntityPublish extends Extension {
entries.sort((a, b) => (['state', 'brightness', 'brightness_percent'].includes(a[0]) ? sorter : sorter * -1));
// For each attribute call the corresponding converter
const usedConverters = [];
const usedConverters = {};
for (let [key, value] of entries) {
let endpointName = topic.endpointName;
let actualTarget = target;
@@ -157,12 +158,19 @@ class EntityPublish extends Extension {
key = key.substring(0, underscoreIndex);
const device = target.getDevice();
actualTarget = device.getEndpoint(definition.endpoint(device)[endpointName]);
if (!actualTarget) {
logger.error(`Device '${resolvedEntity.name}' has no endpoint '${endpointName}'`);
continue;
}
}
}
const endpointOrGroupID = actualTarget.constructor.name == 'Group' ? actualTarget.groupID : actualTarget.ID;
if (!usedConverters.hasOwnProperty(endpointOrGroupID)) usedConverters[endpointOrGroupID] = [];
const converter = converters.find((c) => c.key.includes(key));
if (usedConverters.includes(converter)) {
if (usedConverters[endpointOrGroupID].includes(converter)) {
// Use a converter only once (e.g. light_onoff_brightness converters can convert state and brightness)
continue;
}
@@ -238,7 +246,7 @@ class EntityPublish extends Extension {
}
}
usedConverters.push(converter);
usedConverters[endpointOrGroupID].push(converter);
}
return true;
+17 -2
View File
@@ -9,18 +9,33 @@ class Receive extends Extension {
super(zigbee, mqtt, state, publishEntityState, eventBus);
this.elapsed = {};
this.debouncers = {};
this.eventBus.on('publishEntityState', (data) => this.onPublishEntityState(data));
}
async onZigbeeStarted() {
this.coordinator = this.zigbee.getDevicesByType('Coordinator')[0];
}
async onPublishEntityState(data) {
/**
* Prevent that outdated properties are being published.
* In case that e.g. the state is currently held back by a debounce and a new state is published
* remove it from the to be send debounced message.
*/
if (data.entity.type === 'device' && this.debouncers[data.entity.device.ieeeAddr] &&
data.stateChangeReason !== 'publishDebounce') {
for (const key of Object.keys(data.payload)) {
delete this.debouncers[data.entity.device.ieeeAddr].payload[key];
}
}
}
publishDebounce(ieeeAddr, payload, time, debounceIgnore) {
if (!this.debouncers[ieeeAddr]) {
this.debouncers[ieeeAddr] = {
payload: {},
publish: debounce(() => {
this.publishEntityState(ieeeAddr, this.debouncers[ieeeAddr].payload);
this.publishEntityState(ieeeAddr, this.debouncers[ieeeAddr].payload, 'publishDebounce');
this.debouncers[ieeeAddr].payload = {};
}, time * 1000),
};
@@ -76,7 +91,7 @@ class Receive extends Extension {
* As the same message is also received directly from the end device, it makes no sense
* to handle these messages.
*/
const hasGroupID = data.hasOwnProperty('groupID') && data.groupID != 0;
const hasGroupID = data.hasOwnProperty('groupID') && !!data.groupID;
if (utils.isXiaomiDevice(data.device) && utils.isRouter(data.device) && hasGroupID) {
logger.debug('Skipping re-transmitted Xiaomi message');
return false;
+16 -3
View File
@@ -44,7 +44,8 @@ const transportsToUse = [
format: winston.format.combine(
winston.format.timestamp({format: timestampFormat}),
winston.format.printf(/* istanbul ignore next */(info) => {
const {timestamp, level, message} = info;
let {timestamp, level, message} = info;
level = level === 'warning' ? 'warn' : level;
const prefix = colorizer.colorize(level, `zigbee2mqtt:${levelWithCompensatedLength[level]}`);
return `${prefix} ${timestamp.split('.')[0]}: ${message}`;
}),
@@ -61,7 +62,8 @@ const transportFileOptions = {
format: winston.format.combine(
winston.format.timestamp({format: timestampFormat}),
winston.format.printf(/* istanbul ignore next */(info) => {
const {timestamp, level, message} = info;
let {timestamp, level, message} = info;
level = level === 'warning' ? 'warn' : level;
return `${levelWithCompensatedLength[level]} ${timestamp.split('.')[0]}: ${message}`;
}),
),
@@ -77,8 +79,15 @@ if (output.includes('file')) {
transportsToUse.push(new winston.transports.File(transportFileOptions));
}
/* istanbul ignore next */
if (output.includes('syslog')) {
require('winston-syslog').Syslog;
const options = {app_name: 'zigbee2mqtt', ...settings.get().advanced.log_syslog};
transportsToUse.push(new winston.transports.Syslog(options));
}
// Create logger
const logger = winston.createLogger({transports: transportsToUse});
const logger = winston.createLogger({transports: transportsToUse, levels: winston.config.syslog.levels});
// Cleanup any old log directory.
function cleanup() {
@@ -117,4 +126,8 @@ if (output.includes('file')) {
logger.info(`Logging to console only'`);
}
// winston.config.syslog.levels doesnt have warn, but is required for syslog.
/* istanbul ignore next */
logger.warn = (message) => logger.warning(message);
module.exports = logger;
+2
View File
@@ -41,6 +41,7 @@ const defaults = {
experimental: {
// json or attribute or attribute_and_json
output: 'json',
new_api: false,
},
advanced: {
legacy_api: true,
@@ -49,6 +50,7 @@ const defaults = {
log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'),
log_file: 'log.txt',
log_level: /* istanbul ignore next */ process.env.DEBUG ? 'debug' : 'info',
log_syslog: {},
soft_reset_timeout: 0,
pan_id: 0x1a62,
ext_pan_id: [0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD],
-1
View File
@@ -124,7 +124,6 @@ function getObjectsProperty(objects, key, defaultValue) {
return defaultValue;
}
/* istanbul ignore next newApi */
function getResponse(request, data, error) {
const response = {data, status: error ? 'error' : 'ok'};
if (error) response.error = error;
+2 -3
View File
@@ -149,7 +149,6 @@ class Zigbee extends events.EventEmitter {
* }
*/
resolveEntity(key) {
/* istanbul ignore next newApi */
assert(
typeof key === 'string' || typeof key === 'number' ||
key.constructor.name === 'Device' || key.constructor.name === 'Group',
@@ -217,7 +216,7 @@ class Zigbee extends events.EventEmitter {
if (!group) group = this.createGroup(entity.ID);
return {type: 'group', group, settings: entity, name: entity.friendlyName};
}
} /* istanbul ignore else newApi */ else if (key.constructor.name === 'Device') {
} else if (key.constructor.name === 'Device') {
const setting = settings.getEntity(key.ieeeAddr);
return {
type: 'device',
@@ -233,7 +232,7 @@ class Zigbee extends events.EventEmitter {
type: 'group',
group: key,
settings: setting,
name: setting.friendlyName,
name: setting ? setting.friendlyName : key.groupID,
};
}
}
+1679 -2174
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "1.13.1",
"version": "1.14.0",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"repository": {
@@ -8,7 +8,7 @@
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
},
"engines": {
"node": "^10 || ^12 || ^13"
"node": "^10 || ^12 || ^13 || ^14"
},
"keywords": [
"xiaomi",
@@ -45,8 +45,9 @@
"rimraf": "*",
"semver": "*",
"winston": "*",
"zigbee-herdsman": "0.12.90",
"zigbee-herdsman-converters": "12.0.92"
"winston-syslog": "*",
"zigbee-herdsman": "0.12.94",
"zigbee-herdsman-converters": "12.0.110"
},
"devDependencies": {
"eslint": "*",
+17
View File
@@ -352,4 +352,21 @@ describe('Availability', () => {
device.lastSeen = defaultLastSeen;
});
it('Should republish existing state on MQTT connected', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
await controller.stop();
await flushPromises();
MQTT.publish.mockClear();
controller = new Controller();
getExtension().state[device.ieeeAddr] = false;
await controller.start();
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color/availability',
'offline',
{ retain: true, qos: 0 },
expect.any(Function)
);
});
});
+208
View File
@@ -0,0 +1,208 @@
const data = require('./stub/data');
const logger = require('./stub/logger');
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
const MQTT = require('./stub/mqtt');
const settings = require('../lib/util/settings');
const Controller = require('../lib/controller');
const flushPromises = () => new Promise(setImmediate);
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
const {coordinator, bulb, unsupported} = zigbeeHerdsman.devices;
zigbeeHerdsman.returnDevices.push(coordinator.ieeeAddr);
zigbeeHerdsman.returnDevices.push(bulb.ieeeAddr);
zigbeeHerdsman.returnDevices.push(unsupported.ieeeAddr);
describe('Bridge', () => {
let controller;
beforeEach(async () => {
data.writeDefaultConfiguration();
settings._reRead();
settings.set(['advanced', 'legacy_api'], false);
settings.set(['experimental', 'new_api'], true);
data.writeDefaultState();
logger.info.mockClear();
logger.warn.mockClear();
MQTT.publish.mockClear();
controller = new Controller();
await controller.start();
await flushPromises();
});
it('Should publish bridge info on startup', async () => {
const version = await require('../lib/util/utils').getZigbee2mqttVersion();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/info',
JSON.stringify({"version":version.version,"commit":version.commitHash,"coordinator":{"type":"z-Stack","meta":{"version":1,"revision":20190425}},"logLevel":"info","permitJoin":false}),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should publish devices on startup', async () => {
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/devices',
JSON.stringify([{"ieeeAddress":"0x000b57fffec6a5b2","type":"Router","networkAddress":40369,"supported":true,"friendlyName":"bulb","definition":{"model":"LED1545G12","vendor":"IKEA","description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","supports":"on/off, brightness, color temperature"},"powerSource":"Mains (single phase)","dateCode":null,"interviewing":false,"interviewCompleted":true},{"ieeeAddress":"0x0017880104e45518","type":"EndDevice","networkAddress":6536,"supported":false,"friendlyName":"0x0017880104e45518","definition":null,"powerSource":"Battery","dateCode":null,"interviewing":false,"interviewCompleted":true}]),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should publish devices on startup', async () => {
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/groups',
JSON.stringify([{"ID":1,"friendlyName":"group_1","members":[]},{"ID":15071,"friendlyName":"group_tradfri_remote","members":[]},{"ID":99,"friendlyName":99,"members":[]},{"ID":11,"friendlyName":"group_with_tradfri","members":[]},{"ID":2,"friendlyName":"group_2","members":[]}]),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should publish event when device joined', async () => {
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
JSON.stringify({"type":"deviceJoined","data":{"friendlyName":"bulb","ieeeAddress":"0x000b57fffec6a5b2"}}),
{ retain: false, qos: 0 },
expect.any(Function)
);
});
it('Should publish event when device interview started', async () => {
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'started'});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
JSON.stringify({"type":"deviceInterview","data":{"friendlyName":"bulb","status":"started","ieeeAddress":"0x000b57fffec6a5b2"}}),
{ retain: false, qos: 0 },
expect.any(Function)
);
});
it('Should publish event and devices when device interview failed', async () => {
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'failed'});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
JSON.stringify({"type":"deviceInterview","data":{"friendlyName":"bulb","status":"failed","ieeeAddress":"0x000b57fffec6a5b2"}}),
{ retain: false, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/devices',
expect.any(String),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should publish event and devices when device interview successful', async () => {
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'successful'});
await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.unsupported, status: 'successful'});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(4);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
JSON.stringify({"type":"deviceInterview","data":{"friendlyName":"bulb","status":"successful","ieeeAddress":"0x000b57fffec6a5b2","supported":true,"definition":{"model":"LED1545G12","vendor":"IKEA","description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","supports":"on/off, brightness, color temperature"}}}),
{ retain: false, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
JSON.stringify({"type":"deviceInterview","data":{"friendlyName":"0x0017880104e45518","status":"successful","ieeeAddress":"0x0017880104e45518","supported":false,"definition":null}}),
{ retain: false, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/devices',
expect.any(String),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should publish event and devices when device leaves', async () => {
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceLeave({ieeeAddr: zigbeeHerdsman.devices.bulb.ieeeAddr});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
JSON.stringify({"type":"deviceLeave","data":{"ieeeAddress":"0x000b57fffec6a5b2"}}),
{ retain: false, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/devices',
expect.any(String),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should allow permit join', async () => {
zigbeeHerdsman.permitJoin.mockClear();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/permitJoin', 'true');
await flushPromises();
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true);
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/permitJoin',
JSON.stringify({"data":{"value":true},"status":"ok"}),
{retain: false, qos: 0}, expect.any(Function)
);
zigbeeHerdsman.permitJoin.mockClear();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/permitJoin', JSON.stringify({"value": false}));
await flushPromises();
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false);
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/permitJoin',
JSON.stringify({"data":{"value":false},"status":"ok"}),
{retain: false, qos: 0}, expect.any(Function)
);
});
it('Should put transaction in response when request is done with transaction', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/permitJoin', JSON.stringify({"value": false, "transaction": 22}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/permitJoin',
JSON.stringify({"data":{"value":false},"status":"ok", "transaction": 22}),
{retain: false, qos: 0}, expect.any(Function)
);
});
it('Should put error in response when request fails', async () => {
zigbeeHerdsman.permitJoin.mockImplementationOnce(() => {throw new Error('Failed to connect to adapter')});
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/permitJoin', JSON.stringify({"value": false}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/permitJoin',
JSON.stringify({"data":{},"status":"error","error": "Failed to connect to adapter"}),
{retain: false, qos: 0}, expect.any(Function)
);
});
it('Coverage satisfaction', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/random', JSON.stringify({"value": false}));
const device = zigbeeHerdsman.devices.bulb;
await zigbeeHerdsman.events.message({data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
await flushPromises();
});
});
+10 -8
View File
@@ -1,6 +1,7 @@
const data = require('./stub/data');
const logger = require('./stub/logger');
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
zigbeeHerdsman.returnDevices.push('0x00124b00120144ae');
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b3');
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b2');
@@ -20,6 +21,7 @@ describe('Groups', () => {
data.writeDefaultConfiguration();
settings._reRead();
MQTT.publish.mockClear();
zigbeeHerdsmanConverters.toZigbeeConverters.__clearStore__();
})
it('Apply group updates add', async () => {
@@ -375,7 +377,7 @@ describe('Groups', () => {
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'OFF'}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", '{"state":"OFF"}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", '{"state":"OFF","brightness":0}', {"retain": false, qos: 0}, expect.any(Function));
});
it('Should not publish state change off if any lights within are still on when changed via shared group', async () => {
@@ -400,8 +402,8 @@ describe('Groups', () => {
await MQTT.events.message('zigbee2mqtt/group_2/set', JSON.stringify({state: 'OFF'}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_2", '{"state":"OFF"}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", '{"state":"OFF"}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_2", '{"state":"OFF","brightness":0}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", '{"state":"OFF","brightness":0}', {"retain": false, qos: 0}, expect.any(Function));
});
it('Should publish state change off if all lights within turn off', async () => {
@@ -426,9 +428,9 @@ describe('Groups', () => {
await MQTT.events.message('zigbee2mqtt/bulb/set', JSON.stringify({state: 'OFF'}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(3);
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", '{"state":"OFF"}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", '{"state":"OFF"}', {"retain": true, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", '{"state":"OFF"}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", '{"state":"OFF","brightness":0}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", '{"state":"OFF","brightness":0}', {"retain": true, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", '{"state":"OFF","brightness":0}', {"retain": false, qos: 0}, expect.any(Function));
});
it('Should publish state change off even when missing current state', async () => {
@@ -454,7 +456,7 @@ describe('Groups', () => {
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", '{"state":"OFF"}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", '{"state":"OFF"}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", '{"state":"OFF","brightness":0}', {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", '{"state":"OFF","brightness":0}', {"retain": false, qos: 0}, expect.any(Function));
});
});
+3 -3
View File
@@ -204,7 +204,7 @@ describe('HomeAssistant extension', () => {
payload = {
'unit_of_measurement': '°C',
'device_class': 'temperature',
'value_template': "{{ (value_json.temperature | float) | round(1) }}",
'value_template': "{{ value_json.temperature }}",
'state_topic': 'zigbee2mqtt/weather_sensor',
'json_attributes_topic': 'zigbee2mqtt/weather_sensor',
'name': 'weather_sensor_temperature',
@@ -229,7 +229,7 @@ describe('HomeAssistant extension', () => {
payload = {
'unit_of_measurement': '%',
'device_class': 'humidity',
'value_template': '{{ (value_json.humidity | float) | round(0) }}',
'value_template': '{{ value_json.humidity }}',
'state_topic': 'zigbee2mqtt/weather_sensor',
'json_attributes_topic': 'zigbee2mqtt/weather_sensor',
'name': 'weather_sensor_humidity',
@@ -254,7 +254,7 @@ describe('HomeAssistant extension', () => {
payload = {
'unit_of_measurement': 'hPa',
'device_class': 'pressure',
'value_template': '{{ (value_json.pressure | float) | round(2) }}',
'value_template': '{{ value_json.pressure }}',
'state_topic': 'zigbee2mqtt/weather_sensor',
'json_attributes_topic': 'zigbee2mqtt/weather_sensor',
'name': 'weather_sensor_pressure',
+68 -19
View File
@@ -259,7 +259,7 @@ describe('Publish', () => {
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'});
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 100, y: 50}, color_temp: 62, state: 'OFF', brightness: 0});
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
});
@@ -269,10 +269,10 @@ describe('Publish', () => {
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({color: {r: 100, g: 200, b: 10}}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 16764, colory: 40979, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 156, color: {x: 0.2717, y: 0.6585}});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 162, color: {x: 0.2558, y: 0.6253}});
});
it('Should publish messages to zigbee devices with color rgb', async () => {
@@ -281,10 +281,10 @@ describe('Publish', () => {
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({color: {rgb: '100,200,10'}}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 16764, colory: 40979, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 156, color: {x: 0.2717, y: 0.6585}});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 162, color: {x: 0.2558, y: 0.6253}});
});
it('Should publish messages to zigbee devices with color rgb', async () => {
@@ -340,7 +340,7 @@ describe('Publish', () => {
expect(group.command).toHaveBeenCalledWith("genOnOff", "off", {}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF', brightness: 0});
});
it('Should publish messages to groups color', async () => {
@@ -414,6 +414,31 @@ describe('Publish', () => {
expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']);
});
it('Should handle get with multiple endpoints', async () => {
const device = zigbeeHerdsman.devices.QBKG03LM;
const endpoint2 = device.getEndpoint(2);
const endpoint3 = device.getEndpoint(3);
await MQTT.events.message('zigbee2mqtt/0x0017880104e45542/get', JSON.stringify({state_left: '', state_right: ''}));
await flushPromises();
expect(endpoint2.read).toHaveBeenCalledTimes(1);
expect(endpoint2.read).toHaveBeenCalledWith('genOnOff', ['onOff']);
expect(endpoint3.read).toHaveBeenCalledTimes(1);
expect(endpoint3.read).toHaveBeenCalledWith('genOnOff', ['onOff']);
});
it('Should log error when device has no such endpoint', async () => {
const device = zigbeeHerdsman.devices.QBKG03LM;
const endpoint2 = device.getEndpoint(2);
const endpoint3 = device.getEndpoint(3);
logger.error.mockClear();
await MQTT.events.message('zigbee2mqtt/0x0017880104e45542/get', JSON.stringify({state_center: '', state_right: ''}));
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(`Device 'wall_switch_double' has no endpoint 'center'`);
expect(endpoint2.read).toHaveBeenCalledTimes(0);
expect(endpoint3.read).toHaveBeenCalledTimes(1);
expect(endpoint3.read).toHaveBeenCalledWith('genOnOff', ['onOff']);
});
it('Should not respond to bridge/config/devices/get', async () => {
await MQTT.events.message('zigbee2mqtt/bridge/config/devices/get', JSON.stringify({state: 'ON'}));
await flushPromises();
@@ -478,10 +503,10 @@ describe('Publish', () => {
await MQTT.events.message('zigbee2mqtt/bulb_color/set/color', '#64C80A');
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 16764, colory: 40979, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 156, color: {x: 0.2717, y: 0.6585}});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 162, color: {x: 0.2558, y: 0.6253}});
});
it('Should parse set with ieeeAddr topic', async () => {
@@ -499,6 +524,21 @@ describe('Publish', () => {
expectNothingPublished();
});
it('Should send state update on toggle specific endpoint', async () => {
const device = zigbeeHerdsman.devices.QBKG03LM;
const endpoint = device.getEndpoint(2);
await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', 'ON');
await flushPromises();
await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', 'TOGGLE');
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "toggle", {}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/wall_switch_double", JSON.stringify({state_left: 'ON'}), {"qos": 0, "retain": false}, expect.any(Function)]);
expect(MQTT.publish.mock.calls[1]).toEqual(["zigbee2mqtt/wall_switch_double", JSON.stringify({state_left: 'OFF'}), {"qos": 0, "retain": false}, expect.any(Function)]);
});
it('Should parse set with postfix topic and attribute', async () => {
const device = zigbeeHerdsman.devices.QBKG03LM;
const endpoint = device.getEndpoint(2);
@@ -661,11 +701,20 @@ describe('Publish', () => {
it('Should turn device off when brightness 0 is send', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const payload = {'brightness': 0};
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload));
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({brightness: 50, state: 'ON'}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "off", {}, {}]);
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({brightness: 0}));
await flushPromises();
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'ON'}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(3);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["genOnOff", "off", {}, {}]);
expect(endpoint.command.mock.calls[2]).toEqual(["genOnOff", "on", {}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(3);
expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'ON', brightness: 50}), {"qos": 0, "retain": false}, expect.any(Function)]);
expect(MQTT.publish.mock.calls[1]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'OFF', brightness: 0}), {"qos": 0, "retain": false}, expect.any(Function)]);
expect(MQTT.publish.mock.calls[2]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'ON', brightness: 50}), {"qos": 0, "retain": false}, expect.any(Function)]);
});
it('Should turn device off when brightness 0 is send with light_brightness converter', async () => {
@@ -685,7 +734,7 @@ describe('Publish', () => {
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "enhancedMoveToHueAndSaturation", {"direction": 0, "enhancehue": 45510.416666666664, "saturation": 127, "transtime": 0,}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "enhancedMoveToHueAndSaturation", {"direction": 0, "enhancehue": 44891.475, "saturation": 199.21474, "transtime": 0,}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({"color":{"hue":250,"saturation":50}});
@@ -761,7 +810,7 @@ describe('Publish', () => {
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 10}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
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({state: 'OFF', brightness: 0});
});
it('When device is turned off and on with transition with report enabled it should restore correct brightness', async () => {
@@ -779,7 +828,7 @@ describe('Publish', () => {
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'OFF', brightness: 200}), {"qos": 0, "retain": false}, expect.any(Function)]);
expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'OFF', brightness: 0}), {"qos": 0, "retain": false}, expect.any(Function)]);
// Bulb reports brightness while decreasing brightness
await zigbeeHerdsman.events.message({data: {currentLevel: 1}, cluster: 'genLevelCtrl', device, endpoint, type: 'attributeReport', linkquality: 10});
@@ -811,7 +860,7 @@ describe('Publish', () => {
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'OFF', brightness: 200}), {"qos": 0, "retain": false}, expect.any(Function)]);
expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'OFF', brightness: 0}), {"qos": 0, "retain": false}, expect.any(Function)]);
// Turn on again
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'ON'}));
@@ -904,7 +953,7 @@ describe('Publish', () => {
expect(endpoint.command).toHaveBeenCalledTimes(3);
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON'});
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 0});
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
});
@@ -917,7 +966,7 @@ describe('Publish', () => {
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}, {disableDefaultResponse: true});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/led_controller_1');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF', brightness: 0});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
});
@@ -1094,7 +1143,7 @@ describe('Publish', () => {
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[2]).toEqual([ 'zigbee2mqtt/bulb_color', '{"state":"OFF","brightness":0}', { 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)]);
});
});
+14
View File
@@ -142,6 +142,20 @@ describe('Receive', () => {
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.07, pressure: 2, humidity: 0.03, linkquality: 13});
});
it('Shouldnt republish old state', async () => {
// https://github.com/Koenkk/zigbee2mqtt/issues/3572
jest.useFakeTimers();
const device = zigbeeHerdsman.devices.bulb;
settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1);
await zigbeeHerdsman.events.message({data: {onOff: 0}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
await MQTT.events.message('zigbee2mqtt/bulb/set', JSON.stringify({state: 'ON'}));
await flushPromises();
jest.runAllTimers();
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'});
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', linkquality: 10});
});
it('Should handle a zigbee message with 1 precision', async () => {
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 1);