mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 23:28:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9009de24ac | ||
|
|
a5b0c80a2e | ||
|
|
d9391ac685 | ||
|
|
2bdb0f9ae1 | ||
|
|
56714d7c5a | ||
|
|
7dee99df52 | ||
|
|
e5f9a969ac | ||
|
|
206a45d29a | ||
|
|
c14e18dabb | ||
|
|
3566af1b08 | ||
|
|
a7adfebcb3 | ||
|
|
f8ea6cb574 | ||
|
|
d66e1c322b | ||
|
|
43127e95a9 | ||
|
|
976e802af8 | ||
|
|
32b96d1d4c | ||
|
|
7f68fd3fc4 | ||
|
|
6d9dff8832 | ||
|
|
8dc535e3e3 | ||
|
|
fda1246fca | ||
|
|
a9b54ad668 | ||
|
|
c0d4aea5bd | ||
|
|
d440de8069 | ||
|
|
e4c3ffe38d | ||
|
|
7c045af987 | ||
|
|
2cb234c934 | ||
|
|
51a6b9226d | ||
|
|
7d1a97841c | ||
|
|
9c71ba8754 | ||
|
|
5f63dcb0f2 | ||
|
|
89a6c976cb | ||
|
|
f793eab4ab | ||
|
|
d5604ab5ec | ||
|
|
780246fe74 | ||
|
|
b706233604 | ||
|
|
c4ce339942 | ||
|
|
409fb2407a | ||
|
|
1638b81723 | ||
|
|
396bdf5592 | ||
|
|
89b9b84f94 | ||
|
|
770a7e77e7 | ||
|
|
49df5751b2 | ||
|
|
f594664f55 | ||
|
|
d9b8e1fc18 | ||
|
|
94dbf9c505 | ||
|
|
83215c3290 | ||
|
|
adcec74f23 | ||
|
|
7e6c508263 | ||
|
|
b2034943a5 | ||
|
|
6e382516cb |
@@ -66,6 +66,9 @@ Zigbee2mqtt integrates well with (almost) every home automation solution because
|
||||
## Architecture
|
||||

|
||||
|
||||
### 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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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`;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1679
-2174
File diff suppressed because it is too large
Load Diff
+5
-4
@@ -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": "*",
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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));
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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)]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user