mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 07:08:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04c15f7fe8 | ||
|
|
4ac8d0df95 | ||
|
|
e7bb0c3def | ||
|
|
15d04eda7c | ||
|
|
8b508971db | ||
|
|
b1b134fbf0 | ||
|
|
3cbed1dcd2 | ||
|
|
bda3548cd2 | ||
|
|
200b099de1 | ||
|
|
b8f5e67bee | ||
|
|
9238748978 | ||
|
|
9e0ea50da2 | ||
|
|
e0c2d69244 | ||
|
|
26841f53aa | ||
|
|
6548250eb4 | ||
|
|
ab8035d6bb | ||
|
|
99207358d5 | ||
|
|
7b98a00a24 | ||
|
|
749e6789b8 | ||
|
|
68f243221f | ||
|
|
4b1b1a4c3b | ||
|
|
9ead1a248e | ||
|
|
b5f62f15c7 | ||
|
|
cb9699e12a | ||
|
|
9b79620bde | ||
|
|
481cdefafc | ||
|
|
9ee35f6933 | ||
|
|
a5c2c483ad | ||
|
|
2fa70fea4d | ||
|
|
554f44e361 | ||
|
|
33a0a6a651 | ||
|
|
982e918a50 | ||
|
|
ebad3d673c | ||
|
|
aa6554c1b7 | ||
|
|
fc19f2e903 | ||
|
|
c1f7ce7389 | ||
|
|
5f7553f624 | ||
|
|
7a774ef3ed | ||
|
|
153b82ca77 | ||
|
|
4978d09eb1 | ||
|
|
350ec1776e | ||
|
|
8d01a20d6f | ||
|
|
1e9553fadc | ||
|
|
49f37acd18 | ||
|
|
bce2877641 | ||
|
|
02a945d84b | ||
|
|
1eaf14a861 | ||
|
|
aabb88fbf4 | ||
|
|
9c8323326d | ||
|
|
da3b3b565f | ||
|
|
c1e5c75473 | ||
|
|
6c22f285b4 | ||
|
|
4c9dd749b9 | ||
|
|
918064feb3 | ||
|
|
1c3ad9da72 | ||
|
|
033863925a | ||
|
|
b5436d4b93 | ||
|
|
cbf353e387 | ||
|
|
ca33e228e6 | ||
|
|
97215c9072 | ||
|
|
dd582a3f7e | ||
|
|
8034ea272c | ||
|
|
e6124c2631 | ||
|
|
35813ff951 | ||
|
|
1ff4be4600 | ||
|
|
7305960d53 | ||
|
|
d5d2b68e67 | ||
|
|
25b080a0f1 | ||
|
|
f8943011d4 | ||
|
|
21b8e01149 | ||
|
|
ee50fb40f2 | ||
|
|
3c218305fa | ||
|
|
f31961982d | ||
|
|
8c20fa5d83 | ||
|
|
6ef08e855c | ||
|
|
431c7f7327 | ||
|
|
ab7d507806 | ||
|
|
b1f129c6e8 | ||
|
|
5224f0f372 | ||
|
|
30cd101adf | ||
|
|
f99257ae83 | ||
|
|
0c6d4a0f05 | ||
|
|
752bb6c4d1 | ||
|
|
6b32f30654 | ||
|
|
ddddd88ef6 | ||
|
|
40619a5a53 | ||
|
|
c27e66b618 | ||
|
|
3c2eab00d0 | ||
|
|
dc89b92e2a | ||
|
|
828b4b3e4e | ||
|
|
bd7d0be07a | ||
|
|
2ee3b5f12b | ||
|
|
195d56d4ef | ||
|
|
ca53013661 |
@@ -76,4 +76,13 @@ jobs:
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{"definition": {"id": 1}}' \
|
||||
-u ${{ secrets.HASSIO_TRIGGER_SECRET }} \
|
||||
"https://dev.azure.com/danielwelch2101/hassio-zigbee2mqtt/_apis/build/builds?api-version=5.1"
|
||||
"https://dev.azure.com/danielwelch2101/hassio-zigbee2mqtt/_apis/build/builds?api-version=5.1"
|
||||
- name: Trigger zigbee2mqtt/hassio-zigbee2mqtt build
|
||||
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
|
||||
run: |
|
||||
curl -s \
|
||||
--request POST \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{"definition": {"id": 9}}' \
|
||||
-u ${{ secrets.HASSIO_TRIGGER_SECRET }} \
|
||||
"https://dev.azure.com/zigbee2mqtt/Zigbee2mqtt%20Add-on/_apis/build/builds?api-version=5.1"
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
- run: npm install
|
||||
- run: npm ls --depth 0 --prod --parseable | tail -n +2 | grep -v "zigbee" | xargs -I '{}' basename "{}" | xargs -I '{}' npm install '{}'@latest
|
||||
- run: rm -rf node_modules npm-shrinkwrap.json && npm install && npm shrinkwrap --dev
|
||||
- uses: peter-evans/create-pull-request@v2
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
commit-message: Update dependencies
|
||||
branch: "update_deps"
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
with:
|
||||
node-version: 12
|
||||
- run: npm install zigbee2mqtt-frontend@${{ github.event.client_payload.version }} --save-exact
|
||||
- uses: peter-evans/create-pull-request@v2
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
commit-message: Update zigbee2mqtt-frontend to ${{ github.event.client_payload.version }}
|
||||
branch: "update_frontend"
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
with:
|
||||
node-version: 12
|
||||
- run: npm install zigbee-herdsman@${{ github.event.client_payload.version }} --save-exact
|
||||
- uses: peter-evans/create-pull-request@v2
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
commit-message: Update zigbee-herdsman to ${{ github.event.client_payload.version }}
|
||||
branch: "update_zh"
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
with:
|
||||
node-version: 12
|
||||
- run: npm install zigbee-herdsman-converters@${{ github.event.client_payload.version }} --save-exact
|
||||
- uses: peter-evans/create-pull-request@v2
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
commit-message: Update zigbee-herdsman-converters to ${{ github.event.client_payload.version }}
|
||||
branch: "update_zhc"
|
||||
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
const path = require('path');
|
||||
process.env['ZIGBEE2MQTT_DATA'] = process.env['ZIGBEE2MQTT_DATA'] || path.join(process.env['HOME'], '.z2m');
|
||||
require('./index');
|
||||
+9
-3
@@ -149,7 +149,6 @@ class Controller {
|
||||
// MQTT
|
||||
this.mqtt.on('message', this.onMQTTMessage.bind(this));
|
||||
await this.mqtt.connect();
|
||||
this.mqtt.publish('bridge/state', 'online', {retain: true, qos: 0});
|
||||
|
||||
// Send all cached states.
|
||||
if (settings.get().advanced.cache_state_send_on_startup && settings.get().advanced.cache_state) {
|
||||
@@ -212,7 +211,12 @@ class Controller {
|
||||
|
||||
async onZigbeeEvent(type, data) {
|
||||
const resolvedEntity = this.zigbee.resolveEntity(data.device || data.ieeeAddr);
|
||||
if (data.device && !settings.getDevice(data.device.ieeeAddr) && data.device.type !== 'Coordinator') {
|
||||
if (data.device && data.device.type === 'Coordinator') {
|
||||
logger.debug('Ignoring message from coordinator');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.device && !settings.getDevice(data.device.ieeeAddr)) {
|
||||
// Only deviceLeave doesn't have a device (not interesting to add to settings)
|
||||
resolvedEntity.settings = {...settings.get().device_options, ...settings.addDevice(data.device.ieeeAddr)};
|
||||
}
|
||||
@@ -236,7 +240,8 @@ class Controller {
|
||||
logger.info(`Device '${name}' is supported, identified as: ${vendor} ${description} (${model})`);
|
||||
} else {
|
||||
logger.warn(
|
||||
`Device '${name}' with Zigbee model '${data.device.modelID}' is NOT supported, ` +
|
||||
`Device '${name}' with Zigbee model '${data.device.modelID}' and manufacturer name ` +
|
||||
`'${data.device.manufacturerName}' is NOT supported, ` +
|
||||
`please follow https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`,
|
||||
);
|
||||
}
|
||||
@@ -254,6 +259,7 @@ class Controller {
|
||||
/* istanbul ignore else */
|
||||
if (type === 'deviceLeave') {
|
||||
logger.warn(`Device '${name || data.ieeeAddr}' left the network`);
|
||||
this.state.remove(data.ieeeAddr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,20 +17,6 @@ class Bind extends Extension {
|
||||
this.legacyApi = settings.get().advanced.legacy_api;
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/bind/#`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/unbind/#`);
|
||||
}
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().experimental.new_api) {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/device/bind`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/device/unbind`);
|
||||
}
|
||||
}
|
||||
|
||||
parseMQTTMessage(topic, message) {
|
||||
let type = null;
|
||||
let sourceKey = null;
|
||||
|
||||
@@ -42,7 +42,6 @@ class Bridge extends Extension {
|
||||
|
||||
this.eventBus.on(`groupMembersChanged`, () => this.publishGroups());
|
||||
this.eventBus.on(`devicesChanged`, () => this.publishDevices());
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/#`);
|
||||
await this.publishInfo();
|
||||
await this.publishDevices();
|
||||
await this.publishGroups();
|
||||
@@ -440,6 +439,7 @@ class Bridge extends Extension {
|
||||
const config = objectAssignDeep.noMutate({}, settings.get());
|
||||
delete config.advanced.network_key;
|
||||
delete config.mqtt.password;
|
||||
config.frontend && delete config.frontend.auth_token;
|
||||
const payload = {
|
||||
version: this.zigbee2mqttVersion.version,
|
||||
commit: this.zigbee2mqttVersion.commitHash,
|
||||
@@ -508,7 +508,7 @@ class Bridge extends Extension {
|
||||
const resolved = this.zigbee.resolveEntity(group);
|
||||
return {
|
||||
id: group.groupID,
|
||||
friendly_name: resolved.name,
|
||||
friendly_name: group.groupID === 901 ? 'default_bind_group' : resolved.name,
|
||||
members: group.members.map((m) => {
|
||||
return {
|
||||
ieee_address: m.deviceIeeeAddress,
|
||||
@@ -527,8 +527,7 @@ class Bridge extends Extension {
|
||||
model: definition.model,
|
||||
vendor: definition.vendor,
|
||||
description: definition.description,
|
||||
[definition.hasOwnProperty('exposes') ? 'exposes' : 'supports']:
|
||||
definition.hasOwnProperty('exposes') ? definition.exposes : definition.supports,
|
||||
exposes: definition.exposes,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
|
||||
@@ -53,18 +53,6 @@ class Configure extends Extension {
|
||||
return true;
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
this.mqtt.subscribe(this.legacyTopic);
|
||||
}
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().experimental.new_api) {
|
||||
this.mqtt.subscribe(this.topic);
|
||||
}
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
if (topic === this.legacyTopic) {
|
||||
const resolvedEntity = this.zigbee.resolveEntity(message);
|
||||
|
||||
+29
-37
@@ -1,5 +1,4 @@
|
||||
const http = require('http');
|
||||
const httpProxy = require('http-proxy');
|
||||
const serveStatic = require('serve-static');
|
||||
const finalhandler = require('finalhandler');
|
||||
const Extension = require('./extension');
|
||||
@@ -25,34 +24,24 @@ class Frontend extends Extension {
|
||||
this.onWebSocketConnection = this.onWebSocketConnection.bind(this);
|
||||
this.server = http.createServer(this.onRequest);
|
||||
this.server.on('upgrade', this.onUpgrade);
|
||||
this.developmentServer = settings.get().frontend.development_server;
|
||||
this.development = !!this.developmentServer;
|
||||
this.host = settings.get().frontend.host || '0.0.0.0';
|
||||
this.port = settings.get().frontend.port || 8080;
|
||||
this.authToken = settings.get().frontend.auth_token || false;
|
||||
this.retainedMessages = new Map();
|
||||
|
||||
if (this.development) {
|
||||
this.proxy = httpProxy.createProxyServer({ws: true});
|
||||
} else {
|
||||
/* istanbul ignore next */
|
||||
const options = {setHeaders: (res, path) => {
|
||||
if (path.endsWith('index.html')) {
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
}
|
||||
}};
|
||||
this.fileServer = serveStatic(frontend.getPath(), options);
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
const options = {setHeaders: (res, path) => {
|
||||
if (path.endsWith('index.html')) {
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
}
|
||||
}};
|
||||
this.fileServer = serveStatic(frontend.getPath(), options);
|
||||
this.wss = new WebSocket.Server({noServer: true});
|
||||
this.wss.on('connection', this.onWebSocketConnection);
|
||||
}
|
||||
|
||||
onZigbeeStarted() {
|
||||
if (this.development) {
|
||||
logger.info(`Running frontend in development mode (${this.developmentServer})`);
|
||||
}
|
||||
|
||||
this.server.listen(this.port);
|
||||
logger.info(`Started frontend on port ${this.port}`);
|
||||
this.server.listen(this.port, this.host);
|
||||
logger.info(`Started frontend on port ${this.host}:${this.port}`);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
@@ -64,25 +53,24 @@ class Frontend extends Extension {
|
||||
this.server.close(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
onRequest(request, response) {
|
||||
if (this.development) {
|
||||
this.proxy.web(request, response, {target: `http://${this.developmentServer}`});
|
||||
} else {
|
||||
this.fileServer(request, response, finalhandler(request, response));
|
||||
}
|
||||
this.fileServer(request, response, finalhandler(request, response));
|
||||
}
|
||||
authenticate(request, cb) {
|
||||
const {query} = url.parse(request.url, true);
|
||||
cb(!this.authToken || this.authToken === query.token);
|
||||
}
|
||||
|
||||
onUpgrade(request, socket, head) {
|
||||
const pathname = url.parse(request.url).pathname;
|
||||
if (pathname === '/api') {
|
||||
const wss = this.wss;
|
||||
wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request));
|
||||
} else if (this.development && pathname === '/sockjs-node') {
|
||||
this.proxy.ws(request, socket, head, {target: `ws://${this.developmentServer}`});
|
||||
} else {
|
||||
socket.destroy();
|
||||
}
|
||||
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
this.authenticate(request, (isAuthentificated) => {
|
||||
if (isAuthentificated) {
|
||||
this.wss.emit('connection', ws, request);
|
||||
} else {
|
||||
ws.close(4401, 'Unauthorized');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onWebSocketConnection(ws) {
|
||||
@@ -109,6 +97,10 @@ class Frontend extends Extension {
|
||||
payload.last_seen = utils.formatDate(resolvedEntity.device.lastSeen, lastSeen);
|
||||
}
|
||||
|
||||
if (resolvedEntity.device.linkquality !== undefined) {
|
||||
payload.linkquality = resolvedEntity.device.linkquality;
|
||||
}
|
||||
|
||||
ws.send(stringify({topic: resolvedEntity.name, payload}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,27 +19,6 @@ class Groups extends Extension {
|
||||
this.lastOptimisticState = {};
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/remove_all`);
|
||||
|
||||
for (let step = 1; step < 20; step++) {
|
||||
const topic = `${settings.get().mqtt.base_topic}/bridge/group/${'+/'.repeat(step)}`;
|
||||
this.mqtt.subscribe(`${topic}remove`);
|
||||
this.mqtt.subscribe(`${topic}add`);
|
||||
this.mqtt.subscribe(`${topic}remove_all`);
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().experimental.new_api) {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/group/members/add`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/group/members/remove`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/group/members/remove_all`);
|
||||
}
|
||||
}
|
||||
|
||||
async onZigbeeStarted() {
|
||||
this.eventBus.on('stateChange', this.onStateChange);
|
||||
await this.syncGroupsWithSettings();
|
||||
|
||||
+146
-322
@@ -9,26 +9,6 @@ const assert = require('assert');
|
||||
|
||||
const cfg = {
|
||||
// Binary sensor
|
||||
'binary_sensor_water_leak': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'water_leak',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.water_leak }}',
|
||||
device_class: 'moisture',
|
||||
},
|
||||
},
|
||||
'binary_sensor_battery_low': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'battery_low',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.battery_low}}',
|
||||
device_class: 'battery',
|
||||
},
|
||||
},
|
||||
'binary_sensor_update_available': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'update_available',
|
||||
@@ -48,15 +28,6 @@ const cfg = {
|
||||
value_template: `{{ value_json['update']['state'] }}`,
|
||||
},
|
||||
},
|
||||
'sensor_local_temperature': {
|
||||
type: 'sensor',
|
||||
object_id: 'local_temperature',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: '°C',
|
||||
device_class: 'temperature',
|
||||
value_template: '{{ value_json.local_temperature }}',
|
||||
},
|
||||
},
|
||||
'sensor_click': {
|
||||
type: 'sensor',
|
||||
object_id: 'click',
|
||||
@@ -65,24 +36,6 @@ const cfg = {
|
||||
value_template: '{{ value_json.click }}',
|
||||
},
|
||||
},
|
||||
'sensor_power': {
|
||||
type: 'sensor',
|
||||
object_id: 'power',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'W',
|
||||
icon: 'mdi:flash',
|
||||
value_template: '{{ value_json.power }}',
|
||||
},
|
||||
},
|
||||
'sensor_energy': {
|
||||
type: 'sensor',
|
||||
object_id: 'energy',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'kWh',
|
||||
icon: 'mdi:power-plug',
|
||||
value_template: '{{ value_json.energy }}',
|
||||
},
|
||||
},
|
||||
'sensor_brightness': {
|
||||
type: 'sensor',
|
||||
object_id: 'brightness',
|
||||
@@ -92,15 +45,6 @@ const cfg = {
|
||||
value_template: '{{ value_json.brightness }}',
|
||||
},
|
||||
},
|
||||
'sensor_battery': {
|
||||
type: 'sensor',
|
||||
object_id: 'battery',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: '%',
|
||||
device_class: 'battery',
|
||||
value_template: '{{ value_json.battery }}',
|
||||
},
|
||||
},
|
||||
'sensor_linkquality': {
|
||||
type: 'sensor',
|
||||
object_id: 'linkquality',
|
||||
@@ -111,65 +55,6 @@ const cfg = {
|
||||
},
|
||||
},
|
||||
|
||||
// Switch
|
||||
'switch_window_detection': {
|
||||
type: 'switch',
|
||||
object_id: 'window_detection',
|
||||
discovery_payload: {
|
||||
state_topic: true,
|
||||
command_topic: true,
|
||||
command_topic_postfix: 'window_detection',
|
||||
payload_off: 'OFF',
|
||||
payload_on: 'ON',
|
||||
state_off: 'OFF',
|
||||
state_on: 'ON',
|
||||
value_template: '{{ value_json.window_detection }}',
|
||||
icon: 'mdi:window-open-variant',
|
||||
},
|
||||
},
|
||||
'switch_valve_detection': {
|
||||
type: 'switch',
|
||||
object_id: 'valve_detection',
|
||||
discovery_payload: {
|
||||
state_topic: true,
|
||||
command_topic: true,
|
||||
command_topic_postfix: 'valve_detection',
|
||||
payload_off: 'OFF',
|
||||
payload_on: 'ON',
|
||||
state_off: 'OFF',
|
||||
state_on: 'ON',
|
||||
value_template: '{{ value_json.valve_detection }}',
|
||||
},
|
||||
},
|
||||
|
||||
// Lock
|
||||
'lock_keypad_lockout': {
|
||||
type: 'lock',
|
||||
object_id: 'keypad_lock',
|
||||
discovery_payload: {
|
||||
state_topic: true,
|
||||
command_topic: true,
|
||||
command_topic_postfix: 'keypad_lockout',
|
||||
payload_unlock: '0',
|
||||
payload_lock: '1',
|
||||
value_template: '{{ value_json.keypad_lockout }}',
|
||||
},
|
||||
},
|
||||
'lock_child_lock': {
|
||||
type: 'lock',
|
||||
object_id: 'child_lock',
|
||||
discovery_payload: {
|
||||
state_topic: true,
|
||||
command_topic: true,
|
||||
command_topic_postfix: 'child_lock',
|
||||
payload_lock: 'LOCK',
|
||||
payload_unlock: 'UNLOCK',
|
||||
state_locked: 'LOCKED',
|
||||
state_unlocked: 'UNLOCKED',
|
||||
value_template: '{{ value_json.child_lock }}',
|
||||
},
|
||||
},
|
||||
|
||||
// Trigger
|
||||
'trigger_action': {
|
||||
type: 'device_automation',
|
||||
@@ -187,190 +72,6 @@ const cfg = {
|
||||
},
|
||||
};
|
||||
|
||||
const climate = (minTemp=7, maxTemp=30, temperatureStateProperty='occupied_heating_setpoint',
|
||||
tempStep=1, systemModes=['off', 'auto', 'heat'], fanModes=[], holdModes=[],
|
||||
temperatureLowStateTopic=false, temperatureHighStateTopic=false, endpoint=null ) => {
|
||||
const jsonProperty = (key) => `value_json.${key}${endpoint ? `_${endpoint}` : ''}`;
|
||||
const retVal = {
|
||||
type: 'climate',
|
||||
object_id: endpoint ? `climate_${endpoint}` : 'climate',
|
||||
discovery_payload: {
|
||||
state_topic: false,
|
||||
temperature_unit: 'C',
|
||||
min_temp: `${minTemp}`,
|
||||
max_temp: `${maxTemp}`,
|
||||
mode_state_topic: true,
|
||||
mode_state_template: `{{ ${jsonProperty('system_mode')} }}`,
|
||||
mode_command_topic: true,
|
||||
current_temperature_topic: true,
|
||||
current_temperature_template: `{{ ${jsonProperty('local_temperature')} }}`,
|
||||
temp_step: tempStep,
|
||||
action_topic: true,
|
||||
action_template:
|
||||
'{% set values = {\'idle\':\'off\',\'heat\':\'heating\',\'cool\':\'cooling\',\'fan only\':\'fan\'}'+
|
||||
` %}{{ values[${jsonProperty('running_state')}] }}`,
|
||||
},
|
||||
};
|
||||
|
||||
if (endpoint) {
|
||||
retVal.discovery_payload.state_topic_postfix = endpoint;
|
||||
}
|
||||
|
||||
// system_modes empty <=> use auto (in other case ha ui is showing all modes)
|
||||
if (systemModes.length > 0) {
|
||||
retVal.discovery_payload.modes = systemModes;
|
||||
} else {
|
||||
retVal.discovery_payload.modes = ['auto'];
|
||||
}
|
||||
// hold_modes empty <=> don't use presets
|
||||
if (holdModes.length > 0) {
|
||||
// NOTE: Preset 'none' will be added as first item on HA side `mqtt/climate.py preset_modes()`
|
||||
const indexOfNone = holdModes.indexOf('none');
|
||||
if (indexOfNone > -1) holdModes.splice(indexOfNone, 1);
|
||||
|
||||
// HA has special behaviour for the away mode
|
||||
// https://github.com/Koenkk/zigbee2mqtt/pull/4491#issuecomment-701550476
|
||||
const indexOfAway = holdModes.indexOf('away');
|
||||
/* istanbul ignore else */
|
||||
if (indexOfAway > -1) {
|
||||
holdModes.splice(indexOfAway, 1); // HA will add "Away" to modes by itself
|
||||
retVal.discovery_payload.away_mode_command_topic = true;
|
||||
retVal.discovery_payload.away_mode_state_topic = true;
|
||||
retVal.discovery_payload.away_mode_state_template =
|
||||
`{{ ${jsonProperty('away_mode')} }}`;
|
||||
}
|
||||
|
||||
if (holdModes.length > 0) { // || indexOfAway > -1) {
|
||||
retVal.discovery_payload.hold_modes = holdModes;
|
||||
retVal.discovery_payload.hold_command_topic = true;
|
||||
retVal.discovery_payload.hold_state_template = `{{ ${jsonProperty('preset')} }}`;
|
||||
retVal.discovery_payload.hold_state_topic = true;
|
||||
}
|
||||
}
|
||||
// fan_modes empty <=> don't use fan modes
|
||||
if (fanModes.length > 0) {
|
||||
retVal.discovery_payload.fan_modes = fanModes;
|
||||
retVal.discovery_payload.fan_mode_command_topic = true;
|
||||
retVal.discovery_payload.fan_mode_state_template = `{{ ${jsonProperty('fan_mode')} }}`;
|
||||
retVal.discovery_payload.fan_mode_state_topic = true;
|
||||
}
|
||||
// if no high and low temp used then use temperature_state_topic
|
||||
if (!temperatureHighStateTopic && !temperatureLowStateTopic) {
|
||||
retVal.discovery_payload.temperature_state_topic = true;
|
||||
retVal.discovery_payload.temperature_state_template = `{{ ${jsonProperty(temperatureStateProperty)} }}`;
|
||||
retVal.discovery_payload.temperature_command_topic = temperatureStateProperty;
|
||||
}
|
||||
// use low target temperature
|
||||
if (temperatureLowStateTopic) {
|
||||
retVal.discovery_payload.temperature_low_state_topic = temperatureLowStateTopic;
|
||||
retVal.discovery_payload.temperature_low_state_template = `{{ ${jsonProperty('occupied_heating_setpoint')} }}`;
|
||||
retVal.discovery_payload.temperature_low_command_topic = 'occupied_heating_setpoint';
|
||||
}
|
||||
// use high target temperature
|
||||
if (temperatureHighStateTopic) {
|
||||
retVal.discovery_payload.temperature_high_state_topic = temperatureHighStateTopic;
|
||||
retVal.discovery_payload.temperature_high_state_template = `{{ ${jsonProperty('occupied_cooling_setpoint')} }}`;
|
||||
retVal.discovery_payload.temperature_high_command_topic = 'occupied_cooling_setpoint';
|
||||
}
|
||||
return retVal;
|
||||
};
|
||||
|
||||
|
||||
// Map Home Assistant configurations to devices.
|
||||
const manualMaping = {
|
||||
'1TST-EU': [climate(), cfg.sensor_battery],
|
||||
'AV2010/32': [climate(7, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_battery],
|
||||
'SPZB0001': [climate(5, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_battery],
|
||||
'ST218': [
|
||||
climate(5, 30, 'occupied_heating_setpoint', 0.5),
|
||||
cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout,
|
||||
],
|
||||
'TH1123ZB': [
|
||||
climate(7, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout, cfg.sensor_power, cfg.sensor_energy,
|
||||
],
|
||||
'TH1124ZB': [
|
||||
climate(7, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout, cfg.sensor_power, cfg.sensor_energy,
|
||||
],
|
||||
'TH1300ZB': [
|
||||
climate(7, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout,
|
||||
],
|
||||
'TH1400ZB': [climate()],
|
||||
'TH1500ZB': [climate()],
|
||||
'Zen-01-W': [climate(10, 30, 'occupied_heating_setpoint', 0.5)],
|
||||
'UK7004240': [climate(), cfg.sensor_battery],
|
||||
'WV704R0A0902': [climate()],
|
||||
'STZB402': [
|
||||
climate(5, 30, 'occupied_heating_setpoint', 0.5),
|
||||
cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout,
|
||||
],
|
||||
'SMT402': [
|
||||
climate(5, 30, 'occupied_heating_setpoint', 0.5),
|
||||
cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout,
|
||||
],
|
||||
'SMT402AD': [
|
||||
climate(5, 30, 'occupied_heating_setpoint', 0.5),
|
||||
cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout,
|
||||
],
|
||||
'SLR1b': [climate()],
|
||||
'GS361A-H04': [
|
||||
cfg.lock_child_lock,
|
||||
cfg.switch_window_detection,
|
||||
cfg.switch_valve_detection,
|
||||
climate(5, 30, 'current_heating_setpoint', 0.5, ['off', 'auto', 'heat', 'manual']),
|
||||
cfg.sensor_battery,
|
||||
],
|
||||
'3157100': [climate(10, 30, 'occupied_heating_setpoint', 1, ['off', 'heat', 'cool'],
|
||||
['auto', 'on'], [], true, true), cfg.sensor_battery],
|
||||
'RC-2000WH': [climate(10, 30, 'occupied_heating_setpoint', 1, ['off', 'auto', 'heat', 'cool'],
|
||||
['auto', 'on', 'smart'], [], true, true)],
|
||||
'TS0601_thermostat': [
|
||||
cfg.lock_child_lock, cfg.switch_window_detection, cfg.switch_valve_detection, cfg.sensor_battery,
|
||||
climate(5, 30, 'current_heating_setpoint', 0.5, [], [],
|
||||
['schedule', 'manual', 'away', 'boost', 'complex', 'comfort', 'eco']),
|
||||
],
|
||||
'HT-08': [
|
||||
cfg.lock_child_lock,
|
||||
climate(5, 35, 'current_heating_setpoint', 0.5,
|
||||
['off', 'heat', 'auto'], [], ['none', 'away']),
|
||||
],
|
||||
'HT-10': [
|
||||
cfg.lock_child_lock, cfg.binary_sensor_battery_low,
|
||||
climate(5, 35, 'current_heating_setpoint', 0.5,
|
||||
['off', 'heat', 'auto'], [], ['none', 'away']),
|
||||
],
|
||||
'07703L': [
|
||||
cfg.lock_child_lock, cfg.binary_sensor_battery_low,
|
||||
climate(5, 35, 'current_heating_setpoint', 0.5,
|
||||
['off', 'heat', 'auto'], [], ['none', 'away']),
|
||||
],
|
||||
'ZK03840': [climate()],
|
||||
'U86KWF-ZPSJ': [climate(5, 30, 'current_heating_setpoint', 0.5)],
|
||||
'D3-DPWK-TY': [climate(5, 30, 'current_heating_setpoint', 0.5)],
|
||||
'BHT-002-GCLZB': [
|
||||
cfg.lock_child_lock, climate(5, 30, 'current_heating_setpoint', 1, ['off', 'heat'], [], ['hold', 'program']),
|
||||
],
|
||||
'SLR2': [
|
||||
climate(7, 30, 'occupied_heating_setpoint', 1, ['off', 'auto', 'heat'], [], [], false, false, 'heat'),
|
||||
climate(7, 30, 'occupied_heating_setpoint', 1, ['off', 'auto', 'heat'], [], [], false, false, 'cool'),
|
||||
],
|
||||
'SEA801-Zigbee': [
|
||||
cfg.binary_sensor_battery_low,
|
||||
climate(5, 30, 'current_heating_setpoint', 0.5, ['off', 'heat'], [], ['manual', 'auto']),
|
||||
],
|
||||
'SEA802-Zigbee': [
|
||||
cfg.binary_sensor_battery_low,
|
||||
climate(5, 30, 'current_heating_setpoint', 0.5, ['off', 'heat'], [], ['manual', 'auto']),
|
||||
],
|
||||
'HY08WE': [climate(5, 30, 'current_heating_setpoint', 0.5)],
|
||||
};
|
||||
|
||||
const defaultStatusTopic = 'homeassistant/status';
|
||||
|
||||
/**
|
||||
@@ -410,7 +111,6 @@ class HomeAssistant extends Extension {
|
||||
populateMapping() {
|
||||
for (const def of zigbeeHerdsmanConverters.definitions) {
|
||||
if (def.hasOwnProperty('exposes')) {
|
||||
assert(!manualMaping.hasOwnProperty(def.model), `'${def.model}' has manual mapping and exposes`);
|
||||
this.mapping[def.model] = [];
|
||||
|
||||
if (['WXKG01LM', 'HS1EB/HS1EB-E', 'ICZB-KPD14S', 'TERNCY-SD01', 'TERNCY-PP01', 'ICZB-KPD18S',
|
||||
@@ -452,29 +152,151 @@ class HomeAssistant extends Extension {
|
||||
discoveryEntry.discovery_payload.effect_list = effect.values;
|
||||
}
|
||||
} else if (expose.type === 'switch') {
|
||||
const state = expose.features.find((f) => f.name === 'state');
|
||||
discoveryEntry = {
|
||||
type: 'switch',
|
||||
object_id: expose.endpoint ? `switch_${expose.endpoint}` : 'switch',
|
||||
discovery_payload: {
|
||||
payload_off: 'OFF',
|
||||
payload_on: 'ON',
|
||||
value_template: `{{ value_json.state${expose.endpoint ? `_${expose.endpoint}` : ''} }}`,
|
||||
value_template: `{{ value_json.${state.property} }}`,
|
||||
command_topic: true,
|
||||
command_topic_prefix: expose.endpoint ? expose.endpoint : undefined,
|
||||
},
|
||||
};
|
||||
|
||||
if (state.property === 'valve_detection' || state.property === 'window_detection') {
|
||||
discoveryEntry.discovery_payload.command_topic_postfix = state.property;
|
||||
discoveryEntry.discovery_payload.state_off = 'OFF';
|
||||
discoveryEntry.discovery_payload.state_on = 'ON';
|
||||
discoveryEntry.discovery_payload.state_topic = true;
|
||||
discoveryEntry.object_id = state.property;
|
||||
|
||||
if (state.property === 'window_detection') {
|
||||
discoveryEntry.discovery_payload.icon = 'mdi:window-open-variant';
|
||||
}
|
||||
}
|
||||
} else if (expose.type === 'climate') {
|
||||
const setpointProperties = ['occupied_heating_setpoint', 'current_heating_setpoint'];
|
||||
const setpoint = expose.features.find((f) => setpointProperties.includes(f.name));
|
||||
assert(setpoint, 'No setpoint found');
|
||||
const temperature = expose.features.find((f) => f.name === 'local_temperature');
|
||||
assert(temperature, 'No temperature found');
|
||||
|
||||
discoveryEntry = {
|
||||
type: 'climate',
|
||||
object_id: expose.endpoint ? `climate_${expose.endpoint}` : 'climate',
|
||||
discovery_payload: {
|
||||
// Static
|
||||
state_topic: false,
|
||||
temperature_unit: 'C',
|
||||
// Setpoint
|
||||
temp_step: setpoint.value_step,
|
||||
min_temp: setpoint.value_min.toString(),
|
||||
max_temp: setpoint.value_max.toString(),
|
||||
// Temperature
|
||||
current_temperature_topic: true,
|
||||
current_temperature_template: `{{ value_json.${temperature.property} }}`,
|
||||
},
|
||||
};
|
||||
|
||||
const mode = expose.features.find((f) => f.name === 'system_mode');
|
||||
if (mode) {
|
||||
discoveryEntry.discovery_payload.mode_state_topic = true;
|
||||
discoveryEntry.discovery_payload.mode_state_template = `{{ value_json.${mode.property} }}`;
|
||||
discoveryEntry.discovery_payload.modes = mode.values;
|
||||
discoveryEntry.discovery_payload.mode_command_topic = true;
|
||||
}
|
||||
|
||||
const state = expose.features.find((f) => f.name === 'running_state');
|
||||
if (state) {
|
||||
discoveryEntry.discovery_payload.action_topic = true;
|
||||
discoveryEntry.discovery_payload.action_template = `{% set values = ` +
|
||||
`{'idle':'off','heat':'heating','cool':'cooling','fan only':'fan'}` +
|
||||
` %}{{ values[value_json.${state.property}] }}`;
|
||||
}
|
||||
|
||||
const coolingSetpoint = expose.features.find((f) => f.name === 'occupied_cooling_setpoint');
|
||||
if (coolingSetpoint) {
|
||||
discoveryEntry.discovery_payload.temperature_low_command_topic = setpoint.name;
|
||||
discoveryEntry.discovery_payload.temperature_low_state_template =
|
||||
`{{ value_json.${setpoint.property} }}`;
|
||||
discoveryEntry.discovery_payload.temperature_low_state_topic = true;
|
||||
discoveryEntry.discovery_payload.temperature_high_command_topic = coolingSetpoint.name;
|
||||
discoveryEntry.discovery_payload.temperature_high_state_template =
|
||||
`{{ value_json.${coolingSetpoint.property} }}`;
|
||||
discoveryEntry.discovery_payload.temperature_high_state_topic = true;
|
||||
} else {
|
||||
discoveryEntry.discovery_payload.temperature_command_topic = setpoint.name;
|
||||
discoveryEntry.discovery_payload.temperature_state_template =
|
||||
`{{ value_json.${setpoint.property} }}`;
|
||||
discoveryEntry.discovery_payload.temperature_state_topic = true;
|
||||
}
|
||||
|
||||
const fanMode = expose.features.find((f) => f.name === 'fan_mode');
|
||||
if (fanMode) {
|
||||
discoveryEntry.discovery_payload.fan_modes = fanMode.values;
|
||||
discoveryEntry.discovery_payload.fan_mode_command_topic = true;
|
||||
discoveryEntry.discovery_payload.fan_mode_state_template =
|
||||
`{{ value_json.${fanMode.property} }}`;
|
||||
discoveryEntry.discovery_payload.fan_mode_state_topic = true;
|
||||
}
|
||||
|
||||
const preset = expose.features.find((f) => f.name === 'preset');
|
||||
if (preset) {
|
||||
discoveryEntry.discovery_payload.hold_modes = preset.values;
|
||||
discoveryEntry.discovery_payload.hold_command_topic = true;
|
||||
discoveryEntry.discovery_payload.hold_state_template =
|
||||
`{{ value_json.${preset.property} }}`;
|
||||
discoveryEntry.discovery_payload.hold_state_topic = true;
|
||||
}
|
||||
|
||||
const awayMode = expose.features.find((f) => f.name === 'away_mode');
|
||||
if (awayMode) {
|
||||
discoveryEntry.discovery_payload.away_mode_command_topic = true;
|
||||
discoveryEntry.discovery_payload.away_mode_state_topic = true;
|
||||
discoveryEntry.discovery_payload.away_mode_state_template =
|
||||
`{{ value_json.${awayMode.property} }}`;
|
||||
}
|
||||
|
||||
if (expose.endpoint) {
|
||||
discoveryEntry.discovery_payload.state_topic_postfix = expose.endpoint;
|
||||
}
|
||||
} else if (expose.type === 'lock') {
|
||||
assert(!expose.endpoint, `Endpoint not supported for lock type`);
|
||||
const state = expose.features.find((f) => f.name === 'state');
|
||||
assert(state, 'No state found');
|
||||
discoveryEntry = {
|
||||
type: 'lock',
|
||||
object_id: 'lock',
|
||||
discovery_payload: {
|
||||
command_topic: true,
|
||||
value_template: '{{ value_json.state }}',
|
||||
state_locked: 'LOCK',
|
||||
state_unlocked: 'UNLOCK',
|
||||
value_template: `{{ value_json.${state.property} }}`,
|
||||
},
|
||||
};
|
||||
|
||||
if (state.property === 'keypad_lockout') {
|
||||
// deprecated: keypad_lockout is messy, but changing is breaking
|
||||
discoveryEntry.discovery_payload.payload_lock = state.value_on;
|
||||
discoveryEntry.discovery_payload.payload_unlock = state.value_off;
|
||||
discoveryEntry.discovery_payload.state_topic = true;
|
||||
discoveryEntry.object_id = 'keypad_lock';
|
||||
} else if (state.property === 'child_lock') {
|
||||
// deprecated: child_lock is messy, but changing is breaking
|
||||
discoveryEntry.discovery_payload.payload_lock = state.value_on;
|
||||
discoveryEntry.discovery_payload.payload_unlock = state.value_off;
|
||||
discoveryEntry.discovery_payload.state_locked = 'LOCKED';
|
||||
discoveryEntry.discovery_payload.state_unlocked = 'UNLOCKED';
|
||||
discoveryEntry.discovery_payload.state_topic = true;
|
||||
discoveryEntry.object_id = 'child_lock';
|
||||
} else {
|
||||
discoveryEntry.discovery_payload.state_locked = state.value_on;
|
||||
discoveryEntry.discovery_payload.state_unlocked = state.value_off;
|
||||
}
|
||||
|
||||
if (state.property !== 'state') {
|
||||
discoveryEntry.discovery_payload.command_topic_postfix = state.property;
|
||||
}
|
||||
} else if (expose.type === 'cover') {
|
||||
assert(!expose.endpoint, `Endpoint not supported for cover type`);
|
||||
const hasPosition = expose.features.find((e) => e.name === 'position');
|
||||
@@ -539,6 +361,7 @@ class HomeAssistant extends Extension {
|
||||
smoke: {device_class: 'smoke'},
|
||||
gas: {device_class: 'gas'},
|
||||
carbon_monoxide: {device_class: 'safety'},
|
||||
presence: {device_class: 'presence'},
|
||||
};
|
||||
|
||||
assert(!expose.endpoint, `Endpoint not supported for binary type`);
|
||||
@@ -561,6 +384,7 @@ class HomeAssistant extends Extension {
|
||||
illuminance_lux: {device_class: 'illuminance'},
|
||||
illuminance: {device_class: 'illuminance'},
|
||||
soil_moisture: {icon: 'mdi:water-percent'},
|
||||
position: {icon: 'mdi:valve'},
|
||||
pressure: {device_class: 'pressure'},
|
||||
power: {icon: 'mdi:flash'},
|
||||
linkquality: {icon: 'mdi:signal'},
|
||||
@@ -582,6 +406,7 @@ class HomeAssistant extends Extension {
|
||||
requested_brightness_percent: {icon: 'mdi:brightness-5'},
|
||||
eco2: {icon: 'mdi:molecule-co2'},
|
||||
co2: {icon: 'mdi:molecule-co2'},
|
||||
local_temperature: {device_class: 'temperature'},
|
||||
};
|
||||
|
||||
assert(!expose.endpoint, `Endpoint not supported for numeric type`);
|
||||
@@ -596,7 +421,8 @@ class HomeAssistant extends Extension {
|
||||
},
|
||||
};
|
||||
} else if (expose.type === 'enum' || expose.type === 'text') {
|
||||
if (expose.access === 'r' || expose.access === 'rw') {
|
||||
const ACCESS_STATE = 1;
|
||||
if (expose.access & ACCESS_STATE) {
|
||||
const lookup = {
|
||||
action: {icon: 'mdi:gesture-double-tap'},
|
||||
};
|
||||
@@ -618,8 +444,6 @@ class HomeAssistant extends Extension {
|
||||
this.mapping[def.model].push(discoveryEntry);
|
||||
}
|
||||
}
|
||||
} else if (manualMaping.hasOwnProperty(def.model)) {
|
||||
this.mapping[def.model] = manualMaping[def.model];
|
||||
} else {
|
||||
logger.warn(`Supported device '${def.model}' has no Home Assistant mapping`);
|
||||
}
|
||||
@@ -637,7 +461,7 @@ class HomeAssistant extends Extension {
|
||||
delete this.discovered[resolvedEntity.device.ieeeAddr];
|
||||
for (const config of this.getConfigs(resolvedEntity)) {
|
||||
const topic = this.getDiscoveryTopic(config, resolvedEntity.device);
|
||||
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic);
|
||||
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,7 +563,7 @@ class HomeAssistant extends Extension {
|
||||
if (homeAssisantRename) {
|
||||
for (const config of this.getConfigs(resolvedEntity)) {
|
||||
const topic = this.getDiscoveryTopic(config, device);
|
||||
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic);
|
||||
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,11 +594,6 @@ class HomeAssistant extends Extension {
|
||||
if (!resolvedEntity || !resolvedEntity.definition || !this.mapping[resolvedEntity.definition.model]) return [];
|
||||
|
||||
let configs = this.mapping[resolvedEntity.definition.model].slice();
|
||||
if (!resolvedEntity.definition.hasOwnProperty('exposes')) {
|
||||
// Exposes already has linkquality.
|
||||
configs.push(cfg.sensor_linkquality);
|
||||
}
|
||||
|
||||
if (resolvedEntity.definition.hasOwnProperty('ota')) {
|
||||
if (this.legacyApi) {
|
||||
configs.push(cfg.binary_sensor_update_available);
|
||||
@@ -848,7 +667,12 @@ class HomeAssistant extends Extension {
|
||||
|
||||
// Set (unique) name, separate by space if friendlyName contains space.
|
||||
const nameSeparator = friendlyName.includes(' ') ? ' ' : '_';
|
||||
payload.name = `${friendlyName}${nameSeparator}${config.object_id}`;
|
||||
payload.name = friendlyName;
|
||||
if (config.object_id.startsWith(config.type) && config.object_id.includes('_')) {
|
||||
payload.name += `${nameSeparator}${config.object_id.split(/_(.+)/)[1]}`;
|
||||
} else if (!config.object_id.startsWith(config.type)) {
|
||||
payload.name += `${nameSeparator}${config.object_id}`;
|
||||
}
|
||||
|
||||
// Set unique_id
|
||||
payload.unique_id = `${resolvedEntity.settings.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`;
|
||||
@@ -857,10 +681,10 @@ class HomeAssistant extends Extension {
|
||||
payload.device = this.getDevicePayload(resolvedEntity);
|
||||
|
||||
// Availability payload
|
||||
payload.availability = [
|
||||
{topic: `${settings.get().mqtt.base_topic}/bridge/state`},
|
||||
{topic: `${settings.get().mqtt.base_topic}/${friendlyName}/availability`},
|
||||
];
|
||||
payload.availability = [{topic: `${settings.get().mqtt.base_topic}/bridge/state`}];
|
||||
if (settings.get().advanced.availability_timeout) {
|
||||
payload.availability.push({topic: `${settings.get().mqtt.base_topic}/${friendlyName}/availability`});
|
||||
}
|
||||
|
||||
if (payload.command_topic) {
|
||||
payload.command_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/`;
|
||||
@@ -986,7 +810,7 @@ class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
const topic = this.getDiscoveryTopic(config, device);
|
||||
this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}, this.discoveryTopic);
|
||||
this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}, this.discoveryTopic, false, false);
|
||||
});
|
||||
|
||||
this.discovered[device.ieeeAddr] = true;
|
||||
@@ -1038,7 +862,7 @@ class HomeAssistant extends Extension {
|
||||
if (clear) {
|
||||
logger.debug(`Clearing Home Assistant config '${topic}'`);
|
||||
topic = topic.substring(this.discoveryTopic.length + 1);
|
||||
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic);
|
||||
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic, false, false);
|
||||
}
|
||||
} else if ((topic === this.statusTopic || topic === defaultStatusTopic) && message.toLowerCase() === 'online') {
|
||||
const timer = setTimeout(async () => {
|
||||
@@ -1096,7 +920,7 @@ class HomeAssistant extends Extension {
|
||||
device: this.getDevicePayload(entity),
|
||||
};
|
||||
|
||||
await this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}, this.discoveryTopic);
|
||||
await this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}, this.discoveryTopic, false, false);
|
||||
this.discoveredTriggers[device.ieeeAddr].add(discoveredKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -351,8 +351,6 @@ class BridgeLegacy extends Extension {
|
||||
}
|
||||
|
||||
async onMQTTConnected() {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/config/+`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/config/+/+`);
|
||||
await this.publish();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,6 @@ const Extension = require('../extension');
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/device/(.+)/get_group_membership$`);
|
||||
|
||||
class DeviceGroupMembership extends Extension {
|
||||
onMQTTConnected() {
|
||||
for (let step = 1; step < 20; step++) {
|
||||
const topic = `${settings.get().mqtt.base_topic}/bridge/device/${'+/'.repeat(step)}get_group_membership`;
|
||||
this.mqtt.subscribe(topic);
|
||||
}
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
const match = topic.match(topicRegex);
|
||||
if (!match) {
|
||||
|
||||
@@ -29,18 +29,6 @@ class NetworkMap extends Extension {
|
||||
};
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
this.mqtt.subscribe(this.legacyTopic);
|
||||
this.mqtt.subscribe(this.legacyTopicRoutes);
|
||||
}
|
||||
|
||||
if (settings.get().experimental.new_api) {
|
||||
this.mqtt.subscribe(this.topic);
|
||||
}
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
@@ -256,7 +244,11 @@ class NetworkMap extends Extension {
|
||||
model: resolvedEntity.definition.model,
|
||||
vendor: resolvedEntity.definition.vendor,
|
||||
description: resolvedEntity.definition.description,
|
||||
supports: resolvedEntity.definition.supports,
|
||||
supports: Array.from(new Set(resolvedEntity.definition.exposes.map((e) => {
|
||||
return e.hasOwnProperty('name') ?
|
||||
e.name :
|
||||
`${e.type} (${e.features.map((f) => f.name).join(', ')})`;
|
||||
}))).join(', '),
|
||||
} : null;
|
||||
|
||||
networkMap.nodes.push({
|
||||
|
||||
@@ -18,18 +18,6 @@ class OTAUpdate extends Extension {
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/check`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/update`);
|
||||
}
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().experimental.new_api) {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/check`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/update`);
|
||||
}
|
||||
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
// In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state.
|
||||
// remove them.
|
||||
|
||||
@@ -27,18 +27,6 @@ const groupConverters = [
|
||||
];
|
||||
|
||||
class EntityPublish extends Extension {
|
||||
onMQTTConnected() {
|
||||
// Subscribe to topics.
|
||||
const baseTopic = settings.get().mqtt.base_topic;
|
||||
for (let step = 1; step < 20; step++) {
|
||||
const topic = `${baseTopic}/${'+/'.repeat(step)}`;
|
||||
this.mqtt.subscribe(`${topic}set`);
|
||||
this.mqtt.subscribe(`${topic}set/+`);
|
||||
this.mqtt.subscribe(`${topic}get`);
|
||||
this.mqtt.subscribe(`${topic}get/+`);
|
||||
}
|
||||
}
|
||||
|
||||
parseTopic(topic) {
|
||||
const match = topic.match(topicRegex);
|
||||
if (!match) {
|
||||
@@ -178,14 +166,13 @@ class EntityPublish extends Extension {
|
||||
|
||||
// When the key has a endpointName included (e.g. state_right), this will override the target.
|
||||
if (resolvedEntity.type === 'device' && key.includes('_')) {
|
||||
const underscoreIndex = key.lastIndexOf('_');
|
||||
const possibleEndpointName = key.substring(underscoreIndex + 1, key.length);
|
||||
if (utils.getEndpointNames().includes(possibleEndpointName)) {
|
||||
endpointName = possibleEndpointName;
|
||||
key = key.substring(0, underscoreIndex);
|
||||
const endpointNameMatch = utils.getEndpointNames().find((n) => key.endsWith(`_${n}`));
|
||||
if (endpointNameMatch) {
|
||||
endpointName = endpointNameMatch;
|
||||
key = key.replace(`_${endpointNameMatch}`, '');
|
||||
|
||||
const device = target.getDevice();
|
||||
actualTarget = device.getEndpoint(definition.endpoint(device)[endpointName]);
|
||||
|
||||
if (!actualTarget) {
|
||||
logger.error(`Device '${resolvedEntity.name}' has no endpoint '${endpointName}'`);
|
||||
continue;
|
||||
|
||||
@@ -74,16 +74,13 @@ class Receive extends Extension {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.device.ieeeAddr === this.coordinator.ieeeAddr) {
|
||||
logger.debug('Ignoring message from coordinator');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!resolvedEntity.definition) {
|
||||
if (data.device.interviewing) {
|
||||
logger.debug(`Skipping message, definition is undefined and still interviewing`);
|
||||
} else {
|
||||
logger.warn(`Received message from unsupported device with Zigbee model '${data.device.modelID}'`);
|
||||
logger.warn(
|
||||
`Received message from unsupported device with Zigbee model '${data.device.modelID}' ` +
|
||||
`and manufacturer name '${data.device.manufacturerName}'`);
|
||||
logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`);
|
||||
}
|
||||
|
||||
|
||||
+14
-4
@@ -81,8 +81,13 @@ const pollOnMessage = [
|
||||
},
|
||||
// Read the following attributes
|
||||
read: {cluster: 'genLevelCtrl', attributes: ['currentLevel']},
|
||||
// When the bound devices/members of group have the following manufacturerID
|
||||
manufacturerID: ZigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
||||
// When the bound devices/members of group have the following manufacturerIDs
|
||||
manufacturerIDs: [
|
||||
ZigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
||||
ZigbeeHerdsman.Zcl.ManufacturerCode.ATMEL,
|
||||
ZigbeeHerdsman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
ZigbeeHerdsman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
@@ -98,7 +103,12 @@ const pollOnMessage = [
|
||||
],
|
||||
},
|
||||
read: {cluster: 'genOnOff', attributes: ['onOff']},
|
||||
manufacturerID: ZigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
||||
manufacturerIDs: [
|
||||
ZigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
||||
ZigbeeHerdsman.Zcl.ManufacturerCode.ATMEL,
|
||||
ZigbeeHerdsman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
ZigbeeHerdsman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -264,7 +274,7 @@ class Report extends Extension {
|
||||
|
||||
for (const endpoint of toPoll) {
|
||||
for (const poll of polls) {
|
||||
if (poll.manufacturerID !== endpoint.getDevice().manufacturerID) {
|
||||
if (!poll.manufacturerIDs.includes(endpoint.getDevice().manufacturerID)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
+23
-4
@@ -8,6 +8,7 @@ class MQTT extends events.EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
this.publishedTopics = new Set();
|
||||
}
|
||||
|
||||
async connect() {
|
||||
@@ -18,7 +19,7 @@ class MQTT extends events.EventEmitter {
|
||||
will: {
|
||||
topic: `${settings.get().mqtt.base_topic}/bridge/state`,
|
||||
payload: 'offline',
|
||||
retain: true,
|
||||
retain: settings.get().mqtt.force_disable_retain ? false : true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,8 +45,11 @@ class MQTT extends events.EventEmitter {
|
||||
}
|
||||
|
||||
if (mqttSettings.user && mqttSettings.password) {
|
||||
logger.debug(`Using MQTT login with username: ${mqttSettings.user}`);
|
||||
options.username = mqttSettings.user;
|
||||
options.password = mqttSettings.password;
|
||||
} else {
|
||||
logger.debug(`Using MQTT anonymous login`);
|
||||
}
|
||||
|
||||
if (mqttSettings.client_id) {
|
||||
@@ -69,8 +73,11 @@ class MQTT extends events.EventEmitter {
|
||||
return new Promise((resolve) => {
|
||||
this.client = mqtt.connect(mqttSettings.server, options);
|
||||
|
||||
const self = this;
|
||||
this.client.on('connect', () => {
|
||||
logger.info('Connected to MQTT server');
|
||||
self.subscribe(`${settings.get().mqtt.base_topic}/#`);
|
||||
self.publish('bridge/state', 'online', {retain: true, qos: 0});
|
||||
resolve();
|
||||
});
|
||||
|
||||
@@ -91,17 +98,24 @@ class MQTT extends events.EventEmitter {
|
||||
}
|
||||
|
||||
onMessage(topic, message) {
|
||||
this.emit('message', {topic, message: message + ''});
|
||||
// Since we subscribe to zigbee2mqtt/# we also receive the message we send ourselves, skip these.
|
||||
if (!this.publishedTopics.has(topic)) {
|
||||
this.emit('message', {topic, message: message + ''});
|
||||
}
|
||||
}
|
||||
|
||||
isConnected() {
|
||||
return this.client && !this.client.reconnecting;
|
||||
}
|
||||
|
||||
async publish(topic, payload, options, base=settings.get().mqtt.base_topic, skipLog=false) {
|
||||
async publish(topic, payload, options, base=settings.get().mqtt.base_topic, skipLog=false, skipReceive=true) {
|
||||
topic = `${base}/${topic}`;
|
||||
options = {qos: 0, retain: false, ...options};
|
||||
|
||||
if (skipReceive) {
|
||||
this.publishedTopics.add(topic);
|
||||
}
|
||||
|
||||
if (!this.isConnected()) {
|
||||
if (!skipLog) {
|
||||
logger.error(`Not connected to MQTT server!`);
|
||||
@@ -114,8 +128,13 @@ class MQTT extends events.EventEmitter {
|
||||
logger.info(`MQTT publish: topic '${topic}', payload '${payload}'`);
|
||||
}
|
||||
|
||||
const actualOptions = {...options};
|
||||
if (settings.get().mqtt.force_disable_retain) {
|
||||
actualOptions.retain = false;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.client.publish(topic, payload, options, () => {
|
||||
this.client.publish(topic, payload, actualOptions, () => {
|
||||
this.emit('publishedMessage', {topic, payload, options});
|
||||
resolve();
|
||||
});
|
||||
|
||||
+1
-2
@@ -3,7 +3,6 @@ const data = require('./util/data');
|
||||
const settings = require('./util/settings');
|
||||
const fs = require('fs');
|
||||
const objectAssignDeep = require('object-assign-deep');
|
||||
const stringify = require('json-stable-stringify-without-jsonify');
|
||||
|
||||
const saveInterval = 1000 * 60 * 5; // 5 minutes
|
||||
|
||||
@@ -57,7 +56,7 @@ class State {
|
||||
save() {
|
||||
if (settings.get().advanced.cache_state_persistent) {
|
||||
logger.debug(`Saving state to file ${this.file}`);
|
||||
const json = stringify(this.state, null, 4);
|
||||
const json = JSON.stringify(this.state, null, 4);
|
||||
try {
|
||||
fs.writeFileSync(this.file, json, 'utf8');
|
||||
} catch (e) {
|
||||
|
||||
+41
-17
@@ -17,6 +17,11 @@ const defaults = {
|
||||
permit_join: false,
|
||||
mqtt: {
|
||||
include_device_information: false,
|
||||
/**
|
||||
* Configurable force disable retain flag on mqtt publish.
|
||||
* https://github.com/Koenkk/zigbee2mqtt/pull/4948
|
||||
*/
|
||||
force_disable_retain: false,
|
||||
},
|
||||
serial: {
|
||||
disable_led: false,
|
||||
@@ -60,6 +65,7 @@ const defaults = {
|
||||
ext_pan_id: [0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD],
|
||||
channel: 11,
|
||||
adapter_concurrent: null,
|
||||
adapter_delay: null,
|
||||
|
||||
// Availability timeout in seconds, disabled by default.
|
||||
availability_timeout: 0,
|
||||
@@ -153,6 +159,7 @@ const schema = {
|
||||
reject_unauthorized: {type: 'boolean'},
|
||||
include_device_information: {type: 'boolean'},
|
||||
version: {type: 'number'},
|
||||
force_disable_retain: {type: 'boolean'},
|
||||
},
|
||||
required: ['base_topic', 'server'],
|
||||
},
|
||||
@@ -208,6 +215,7 @@ const schema = {
|
||||
homeassistant_status_topic: {type: 'string'},
|
||||
timestamp_format: {type: 'string'},
|
||||
adapter_concurrent: {type: 'number'},
|
||||
adapter_delay: {type: 'number'},
|
||||
},
|
||||
},
|
||||
map_options: {
|
||||
@@ -317,15 +325,27 @@ function write() {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof actual.devices === 'string') {
|
||||
yaml.writeIfChanged(data.joinPath(actual.devices), settings.devices);
|
||||
toWrite.devices = actual.devices;
|
||||
}
|
||||
// Write devices/groups to separate file if required.
|
||||
const writeDevicesOrGroups = (type) => {
|
||||
if (typeof actual[type] === 'string' || Array.isArray(actual[type])) {
|
||||
const fileToWrite = Array.isArray(actual[type]) ? actual[type][0] : actual[type];
|
||||
const content = objectAssignDeep.noMutate(settings[type]);
|
||||
|
||||
if (typeof actual.groups === 'string') {
|
||||
yaml.writeIfChanged(data.joinPath(actual.groups), settings.groups);
|
||||
toWrite.groups = actual.groups;
|
||||
}
|
||||
// If an array, only write to first file and only devices which are not in the other files.
|
||||
if (Array.isArray(actual[type])) {
|
||||
actual[type].filter((f, i) => i !== 0)
|
||||
.map((f) => yaml.readIfExists(data.joinPath(f), {}))
|
||||
.map((c) => Object.keys(c))
|
||||
.forEach((k) => delete content[k]);
|
||||
}
|
||||
|
||||
yaml.writeIfChanged(data.joinPath(fileToWrite), content);
|
||||
toWrite[type] = actual[type];
|
||||
}
|
||||
};
|
||||
|
||||
writeDevicesOrGroups('devices');
|
||||
writeDevicesOrGroups('groups');
|
||||
|
||||
yaml.writeIfChanged(file, toWrite);
|
||||
|
||||
@@ -420,16 +440,20 @@ function read() {
|
||||
s.advanced.network_key = interpetValue(s.advanced.network_key);
|
||||
}
|
||||
|
||||
// Read devices/groups configuration from separate file.
|
||||
if (typeof s.devices === 'string') {
|
||||
const file = data.joinPath(s.devices);
|
||||
s.devices = yaml.readIfExists(file) || {};
|
||||
}
|
||||
// Read devices/groups configuration from separate file if specified.
|
||||
const readDevicesOrGroups = (type) => {
|
||||
if (typeof s[type] === 'string' || Array.isArray(s[type])) {
|
||||
const files = Array.isArray(s[type]) ? s[type] : [s[type]];
|
||||
s[type] = {};
|
||||
for (const file of files) {
|
||||
const content = yaml.readIfExists(data.joinPath(file), {});
|
||||
s[type] = objectAssignDeep.noMutate(s[type], content);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof s.groups === 'string') {
|
||||
const file = data.joinPath(s.groups);
|
||||
s.groups = yaml.readIfExists(file) || {};
|
||||
}
|
||||
readDevicesOrGroups('devices');
|
||||
readDevicesOrGroups('groups');
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
+21
-3
@@ -1,6 +1,8 @@
|
||||
const equals = require('fast-deep-equal/es6');
|
||||
const humanizeDuration = require('humanize-duration');
|
||||
const data = require('./data');
|
||||
const vm = require('vm');
|
||||
const fs = require('fs');
|
||||
|
||||
// Xiaomi uses 4151 and 4447 (lumi.plug) as manufacturer ID.
|
||||
const xiaomiManufacturerID = [4151, 4447];
|
||||
@@ -69,6 +71,14 @@ async function getZigbee2mqttVersion() {
|
||||
});
|
||||
}
|
||||
|
||||
async function getDependencyVersion(depend) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const packageJSON = require('../../node_modules/'+depend+'/package.json');
|
||||
const version = packageJSON.version;
|
||||
resolve({version});
|
||||
});
|
||||
}
|
||||
|
||||
function formatDate(date, type) {
|
||||
let result;
|
||||
|
||||
@@ -138,13 +148,20 @@ function* getExternalConvertersDefinitions(settings) {
|
||||
const externalConverters = settings.get().external_converters;
|
||||
|
||||
for (const moduleName of externalConverters) {
|
||||
let converterModule = moduleName;
|
||||
let converter;
|
||||
|
||||
if (moduleName.endsWith('.js')) {
|
||||
converterModule = data.joinPath(moduleName.split('.')[0]);
|
||||
const sandbox = {
|
||||
require,
|
||||
module: {},
|
||||
};
|
||||
const converterCode = fs.readFileSync(data.joinPath(moduleName), {encoding: 'utf8'});
|
||||
vm.runInNewContext(converterCode, sandbox);
|
||||
converter = sandbox.module.exports;
|
||||
} else {
|
||||
converter = require(moduleName);
|
||||
}
|
||||
|
||||
const converter = require(converterModule);
|
||||
if (Array.isArray(converter)) {
|
||||
for (const item of converter) {
|
||||
yield item;
|
||||
@@ -184,6 +201,7 @@ module.exports = {
|
||||
millisecondsToSeconds: (milliseconds) => milliseconds / 1000,
|
||||
secondsToMilliseconds: (seconds) => seconds * 1000,
|
||||
getZigbee2mqttVersion,
|
||||
getDependencyVersion,
|
||||
objectHasProperties,
|
||||
toSnakeCase,
|
||||
getObjectProperty,
|
||||
|
||||
+2
-2
@@ -14,8 +14,8 @@ function read(file) {
|
||||
}
|
||||
}
|
||||
|
||||
function readIfExists(file) {
|
||||
return fs.existsSync(file) ? read(file) : null;
|
||||
function readIfExists(file, default_=null) {
|
||||
return fs.existsSync(file) ? read(file) : default_;
|
||||
}
|
||||
|
||||
function writeIfChanged(file, content) {
|
||||
|
||||
+3
-1
@@ -19,7 +19,8 @@ class Zigbee extends events.EventEmitter {
|
||||
}
|
||||
|
||||
async start() {
|
||||
logger.info(`Starting zigbee-herdsman...`);
|
||||
const infoHerdsman = await utils.getDependencyVersion('zigbee-herdsman');
|
||||
logger.info(`Starting zigbee-herdsman (${infoHerdsman.version})`);
|
||||
const herdsmanSettings = {
|
||||
network: {
|
||||
panID: settings.get().advanced.pan_id,
|
||||
@@ -38,6 +39,7 @@ class Zigbee extends events.EventEmitter {
|
||||
},
|
||||
adapter: {
|
||||
concurrent: settings.get().advanced.adapter_concurrent,
|
||||
delay: settings.get().advanced.adapter_delay,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Generated
+1512
-1539
File diff suppressed because it is too large
Load Diff
+9
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "1.16.0",
|
||||
"version": "1.16.2",
|
||||
"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 || ^14"
|
||||
"node": "^10 || ^12 || ^14 || ^15"
|
||||
},
|
||||
"keywords": [
|
||||
"xiaomi",
|
||||
@@ -38,13 +38,12 @@
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"finalhandler": "^1.1.2",
|
||||
"git-last-commit": "^1.0.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"humanize-duration": "^3.23.1",
|
||||
"js-yaml": "^3.14.0",
|
||||
"json-stable-stringify-without-jsonify": "=1.0.1",
|
||||
"mkdir-recursive": "^0.4.0",
|
||||
"moment": "^2.27.0",
|
||||
"mqtt": "^4.2.1",
|
||||
"mqtt": "4.2.1",
|
||||
"object-assign-deep": "^0.4.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.2",
|
||||
@@ -52,9 +51,9 @@
|
||||
"winston": "^3.3.3",
|
||||
"winston-syslog": "^2.4.4",
|
||||
"ws": "^7.3.1",
|
||||
"zigbee-herdsman": "0.13.26",
|
||||
"zigbee-herdsman-converters": "12.0.224",
|
||||
"zigbee2mqtt-frontend": "0.2.68"
|
||||
"zigbee-herdsman": "0.13.37",
|
||||
"zigbee-herdsman-converters": "13.0.7",
|
||||
"zigbee2mqtt-frontend": "0.2.100"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "*",
|
||||
@@ -75,5 +74,8 @@
|
||||
"collectCoverageFrom": [
|
||||
"lib/**"
|
||||
]
|
||||
},
|
||||
"bin": {
|
||||
"zigbee2mqtt": "./cli.js"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
const {ZnpCommandStatus} = require('zigbee-herdsman/dist/adapter/z-stack/constants/common');
|
||||
const {ZnpVersion} = require('zigbee-herdsman/dist/adapter/z-stack/adapter/tstype');
|
||||
const {Subsystem} = require('zigbee-herdsman/dist/adapter/z-stack/unpi/constants');
|
||||
const {Znp} = require('zigbee-herdsman/dist/adapter/z-stack/znp');
|
||||
|
||||
|
||||
class ZStackNvMemEraser {
|
||||
constructor(device) {
|
||||
this.znp = new Znp(device, 115200, false);
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this.znp.open();
|
||||
const attempts = 3;
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
try {
|
||||
await this.znp.request(Subsystem.SYS, 'ping', {capabilities: 1});
|
||||
break;
|
||||
} catch (e) {
|
||||
if (attempts - 1 === i) {
|
||||
throw new Error(`Failed to connect to the adapter (${e})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Old firmware did not support version, assume it's Z-Stack 1.2 for now.
|
||||
try {
|
||||
this.version = (await this.znp.request(Subsystem.SYS, 'version', {})).payload;
|
||||
} catch (e) {
|
||||
console.log(`Failed to get zStack version, assuming 1.2`);
|
||||
this.version = {'transportrev': 2, 'product': 0, 'majorrel': 2,
|
||||
'minorrel': 0, 'maintrel': 0, 'revision': ''};
|
||||
}
|
||||
|
||||
console.log(`Detected znp version '${ZnpVersion[this.version.product]}' (${JSON.stringify(this.version)})`);
|
||||
|
||||
await this.clearAllNvMemItems();
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async clearAllNvMemItems() {
|
||||
let maxNvMemId;
|
||||
switch (this.version.product) {
|
||||
case ZnpVersion.zStack12: maxNvMemId = 0x0302; break;
|
||||
case ZnpVersion.zStack30x: maxNvMemId = 0x033F; break;
|
||||
case ZnpVersion.zStack3x0: maxNvMemId = 0x032F; break;
|
||||
}
|
||||
|
||||
console.log(`Clearing all NVMEM items, from 0 to ${maxNvMemId}`);
|
||||
for (let id=0; id<=maxNvMemId; id++) {
|
||||
const lengthRes = await this.znp.request(Subsystem.SYS, 'osalNvLength',{id: id});
|
||||
if (lengthRes.payload['length']!=0) {
|
||||
console.log(`NVMEM item #${id} - need to delete, size: ${lengthRes.payload['length']}`);
|
||||
await this.znp.request(Subsystem.SYS, 'osalNvDelete',
|
||||
{id: id, len: lengthRes.payload['length']},
|
||||
null, [ZnpCommandStatus.SUCCESS, ZnpCommandStatus.NV_ITEM_INITIALIZED]);
|
||||
} else {
|
||||
console.log(`NVMEM item #${id} - is free`);
|
||||
}
|
||||
}
|
||||
console.log('Clearing all NVMEM items finished');
|
||||
}
|
||||
}
|
||||
const processArgs = process.argv.slice(2);
|
||||
if (processArgs.length != 1) {
|
||||
console.log('ZStack NVMEM eraser.');
|
||||
console.log('Usage:');
|
||||
console.log(' node zStackEraseAllNvMem.js <SERIAL_PORT>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const eraser = new ZStackNvMemEraser(processArgs[0]);
|
||||
|
||||
eraser.start();
|
||||
@@ -33,12 +33,6 @@ describe('Bind', () => {
|
||||
MQTT.publish.mockClear();
|
||||
});
|
||||
|
||||
it('Should subscribe to topics', async () => {
|
||||
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/request/device/bind');
|
||||
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/request/device/unbind');
|
||||
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/bind/#');
|
||||
});
|
||||
|
||||
it('Should bind', async () => {
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
|
||||
|
||||
+4
-4
File diff suppressed because one or more lines are too long
+21
-1
@@ -31,7 +31,7 @@ describe('Controller', () => {
|
||||
|
||||
it('Start controller', async () => {
|
||||
await controller.start();
|
||||
expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({"network":{"panID":6754,"extendedPanID":[221,221,221,221,221,221,221,221],"channelList":[11],"networkKey":[1,3,5,7,9,11,13,15,0,2,4,6,8,10,12,13]},"databasePath":path.join(data.mockDir, "database.db"), "databaseBackupPath":path.join(data.mockDir, "database.db.backup"),"backupPath":path.join(data.mockDir, "coordinator_backup.json"),"acceptJoiningDeviceHandler": expect.any(Function),adapter: {concurrent: null}, "serialPort":{"baudRate":undefined,"rtscts":undefined,"path":"/dev/dummy"}});
|
||||
expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({"network":{"panID":6754,"extendedPanID":[221,221,221,221,221,221,221,221],"channelList":[11],"networkKey":[1,3,5,7,9,11,13,15,0,2,4,6,8,10,12,13]},"databasePath":path.join(data.mockDir, "database.db"), "databaseBackupPath":path.join(data.mockDir, "database.db.backup"),"backupPath":path.join(data.mockDir, "coordinator_backup.json"),"acceptJoiningDeviceHandler": expect.any(Function),adapter: {concurrent: null, delay: null}, "serialPort":{"baudRate":undefined,"rtscts":undefined,"path":"/dev/dummy"}});
|
||||
expect(zigbeeHerdsman.start).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.setLED).toHaveBeenCalledTimes(0);
|
||||
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(0);
|
||||
@@ -598,4 +598,24 @@ describe('Controller', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', { retain: false, qos: 0 }, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Start controller with force_disable_retain', async () => {
|
||||
settings.set(['mqtt', 'force_disable_retain'], true);
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
expect(MQTT.connect).toHaveBeenCalledTimes(1);
|
||||
const expected = {
|
||||
"will": { "payload": "offline", "retain": false, "topic": "zigbee2mqtt/bridge/state" },
|
||||
}
|
||||
expect(MQTT.connect).toHaveBeenCalledWith("mqtt://localhost", expected);
|
||||
});
|
||||
|
||||
it('Should prevent any message being published with retain flag when force_disable_retain is set', async () => {
|
||||
settings.set(['mqtt', 'force_disable_retain'], true);
|
||||
await controller.mqtt.connect()
|
||||
MQTT.publish.mockClear();
|
||||
await controller.mqtt.publish('fo', 'bar', { retain: true })
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/fo', 'bar', { retain: false, qos: 0 }, expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
+41
-33
@@ -6,6 +6,7 @@ const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
const stringify = require('json-stable-stringify-without-jsonify');
|
||||
const flushPromises = () => new Promise(setImmediate);
|
||||
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
jest.spyOn(process, 'exit').mockImplementation(() => {});
|
||||
|
||||
const mockHTTP = {
|
||||
@@ -18,20 +19,17 @@ const mockHTTP = {
|
||||
events: {},
|
||||
};
|
||||
|
||||
const mockHTTPProxy = {
|
||||
implementation: {
|
||||
web: jest.fn(),
|
||||
ws: jest.fn(),
|
||||
},
|
||||
variables: {},
|
||||
events: {},
|
||||
const mockWSocket = {
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
const mockWS = {
|
||||
implementation: {
|
||||
clients: [],
|
||||
on: (event, handler) => {mockWS.events[event] = handler},
|
||||
handleUpgrade: jest.fn(),
|
||||
handleUpgrade: jest.fn().mockImplementation((request, socket, head, cb) => {
|
||||
cb(mockWSocket)
|
||||
}),
|
||||
emit: jest.fn(),
|
||||
},
|
||||
variables: {},
|
||||
@@ -51,13 +49,6 @@ jest.mock('http', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('http-proxy', () => ({
|
||||
createProxyServer: jest.fn().mockImplementation((initParameter) => {
|
||||
mockHTTPProxy.variables.initParameter = initParameter;
|
||||
return mockHTTPProxy.implementation;
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("serve-static", () =>
|
||||
jest.fn().mockImplementation((path) => {
|
||||
mockNodeStatic.variables.path = path
|
||||
@@ -85,15 +76,20 @@ describe('Frontend', () => {
|
||||
data.writeDefaultState();
|
||||
settings._reRead();
|
||||
settings.set(['experimental'], {new_api: true});
|
||||
settings.set(['frontend'], {port: 8081});
|
||||
settings.set(['frontend'], {port: 8081, host: "127.0.0.1"});
|
||||
settings.set(['homeassistant'], true);
|
||||
zigbeeHerdsman.devices.bulb.linkquality = 10;
|
||||
});
|
||||
|
||||
afterEach(async() => {
|
||||
delete zigbeeHerdsman.devices.bulb.linkquality;
|
||||
});
|
||||
|
||||
it('Start/stop', async () => {
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
expect(mockNodeStatic.variables.path).toBe("my/dummy/path");
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081);
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
|
||||
const mockWSClient = {
|
||||
implementation: {
|
||||
@@ -176,32 +172,44 @@ describe('Frontend', () => {
|
||||
mockWS.implementation.handleUpgrade.mock.calls[0][3](99);
|
||||
expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', 99, {"url": "http://localhost:8080/api"});
|
||||
|
||||
mockWS.implementation.handleUpgrade.mockClear();
|
||||
mockHTTP.events.upgrade({url: 'http://localhost:8080/unkown'}, mockSocket, 3);
|
||||
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(0);
|
||||
expect(mockSocket.destroy).toHaveBeenCalledTimes(1);
|
||||
|
||||
mockHTTP.variables.onRequest(1, 2);
|
||||
expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1);
|
||||
expect(mockNodeStatic.implementation).toHaveBeenCalledWith(1, 2, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Development server', async () => {
|
||||
settings.set(['frontend'], {development_server: 'localhost:3001'});
|
||||
it('Static server', async () => {
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
expect(mockHTTPProxy.variables.initParameter).toStrictEqual({ws: true});
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8080);
|
||||
|
||||
mockHTTP.variables.onRequest(1, 2);
|
||||
expect(mockHTTPProxy.implementation.web).toHaveBeenCalledTimes(1);
|
||||
expect(mockHTTPProxy.implementation.web).toHaveBeenCalledWith(1, 2, {"target": "http://localhost:3001"});
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
});
|
||||
|
||||
it('Authentification', async () => {
|
||||
const authToken = 'sample-secure-token'
|
||||
settings.set(['frontend'], {auth_token: authToken});
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
|
||||
const mockSocket = {destroy: jest.fn()};
|
||||
mockHTTPProxy.implementation.ws.mockClear();
|
||||
mockHTTP.events.upgrade({url: 'http://localhost:8080/sockjs-node'}, mockSocket, 3);
|
||||
expect(mockHTTPProxy.implementation.ws).toHaveBeenCalledTimes(1);
|
||||
mockWS.implementation.handleUpgrade.mockClear();
|
||||
mockHTTP.events.upgrade({url: '/api'}, mockSocket, mockWSocket);
|
||||
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(1);
|
||||
expect(mockSocket.destroy).toHaveBeenCalledTimes(0);
|
||||
expect(mockHTTPProxy.implementation.ws).toHaveBeenCalledWith({"url": "http://localhost:8080/sockjs-node"}, mockSocket, 3, {"target": "ws://localhost:3001"});
|
||||
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({"url": "/api"}, mockSocket, mockWSocket, expect.any(Function));
|
||||
expect(mockWSocket.close).toHaveBeenCalledWith(4401, "Unauthorized");
|
||||
|
||||
mockWSocket.close.mockClear();
|
||||
mockWS.implementation.emit.mockClear();
|
||||
|
||||
const url = `/api?token=${authToken}`;
|
||||
mockWS.implementation.handleUpgrade.mockClear();
|
||||
mockHTTP.events.upgrade({url: url}, mockSocket, 3);
|
||||
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(1);
|
||||
expect(mockSocket.destroy).toHaveBeenCalledTimes(0);
|
||||
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({url}, mockSocket, 3, expect.any(Function));
|
||||
expect(mockWSocket.close).toHaveBeenCalledTimes(0);
|
||||
mockWS.implementation.handleUpgrade.mock.calls[0][3](mockWSocket);
|
||||
expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', mockWSocket, {url});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,12 +127,6 @@ describe('Groups', () => {
|
||||
expect(group.members).toStrictEqual([endpoint]);
|
||||
expect(settings.getGroup('group/with/slashes').devices).toStrictEqual([`${device.ieeeAddr}/1`]);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bridge/log", stringify({"type":"device_group_add","message":{"friendly_name":"bulb_color","group":"group/with/slashes"}}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
|
||||
// Test if subscribed to topics with slashes
|
||||
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/group/+/remove');
|
||||
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/group/+/+/remove');
|
||||
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/group/+/+/+/+/+/remove');
|
||||
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/group/+/+/+/+/+/add');
|
||||
});
|
||||
|
||||
it('Legacy api: Add to group via MQTT with postfix', async () => {
|
||||
|
||||
+136
-41
@@ -22,19 +22,6 @@ describe('HomeAssistant extension', () => {
|
||||
settings.set(['homeassistant'], true);
|
||||
});
|
||||
|
||||
it('Should have mapping for all devices supported by zigbee-herdsman-converters', () => {
|
||||
const missing = [];
|
||||
const ha = new HomeAssistant(null, null, null, null, {on: () => {}});
|
||||
|
||||
require('zigbee-herdsman-converters').definitions.forEach((d) => {
|
||||
if (!d.hasOwnProperty('exposes') && !ha._getMapping()[d.model]) {
|
||||
missing.push(d.model);
|
||||
}
|
||||
});
|
||||
|
||||
expect(missing).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('Should not have duplicate type/object_ids in a mapping', () => {
|
||||
const duplicated = [];
|
||||
const ha = new HomeAssistant(null, null, null, null, {on: () => {}});
|
||||
@@ -77,7 +64,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -102,7 +89,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -127,7 +114,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -152,7 +139,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -177,7 +164,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -186,6 +173,114 @@ describe('HomeAssistant extension', () => {
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
payload = {
|
||||
"availability":[
|
||||
{
|
||||
"topic":"zigbee2mqtt/bridge/state"
|
||||
}
|
||||
],
|
||||
"command_topic":"zigbee2mqtt/wall_switch_double/left/set",
|
||||
"device":{
|
||||
"identifiers":[
|
||||
"zigbee2mqtt_0x0017880104e45542"
|
||||
],
|
||||
"manufacturer":"Xiaomi",
|
||||
"model":"Aqara double key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG03LM)",
|
||||
"name":"wall_switch_double",
|
||||
"sw_version":this.version
|
||||
},
|
||||
"json_attributes_topic":"zigbee2mqtt/wall_switch_double",
|
||||
"name":"wall_switch_double_left",
|
||||
"payload_off":"OFF",
|
||||
"payload_on":"ON",
|
||||
"state_topic":"zigbee2mqtt/wall_switch_double",
|
||||
"unique_id":"0x0017880104e45542_switch_left_zigbee2mqtt",
|
||||
"value_template":"{{ value_json.state_left }}"
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/switch/0x0017880104e45542/switch_left/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
payload = {
|
||||
"availability":[
|
||||
{
|
||||
"topic":"zigbee2mqtt/bridge/state"
|
||||
}
|
||||
],
|
||||
"command_topic":"zigbee2mqtt/wall_switch_double/right/set",
|
||||
"device":{
|
||||
"identifiers":[
|
||||
"zigbee2mqtt_0x0017880104e45542"
|
||||
],
|
||||
"manufacturer":"Xiaomi",
|
||||
"model":"Aqara double key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG03LM)",
|
||||
"name":"wall_switch_double",
|
||||
"sw_version":this.version
|
||||
},
|
||||
"json_attributes_topic":"zigbee2mqtt/wall_switch_double",
|
||||
"name":"wall_switch_double_right",
|
||||
"payload_off":"OFF",
|
||||
"payload_on":"ON",
|
||||
"state_topic":"zigbee2mqtt/wall_switch_double",
|
||||
"unique_id":"0x0017880104e45542_switch_right_zigbee2mqtt",
|
||||
"value_template":"{{ value_json.state_right }}"
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/switch/0x0017880104e45542/switch_right/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
payload = {
|
||||
"availability":[
|
||||
{
|
||||
"topic":"zigbee2mqtt/bridge/state"
|
||||
}
|
||||
],
|
||||
"brightness":true,
|
||||
"brightness_scale":254,
|
||||
"color_temp":true,
|
||||
"command_topic":"zigbee2mqtt/bulb/set",
|
||||
"device":{
|
||||
"identifiers":[
|
||||
"zigbee2mqtt_0x000b57fffec6a5b2"
|
||||
],
|
||||
"manufacturer":"IKEA",
|
||||
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
|
||||
"name":"bulb",
|
||||
"sw_version":this.version,
|
||||
},
|
||||
"effect":true,
|
||||
"effect_list":[
|
||||
"blink",
|
||||
"breathe",
|
||||
"okay",
|
||||
"channel_change",
|
||||
"finish_effect",
|
||||
"stop_effect"
|
||||
],
|
||||
"hs":false,
|
||||
"json_attributes_topic":"zigbee2mqtt/bulb",
|
||||
"name":"bulb",
|
||||
"schema":"json",
|
||||
"state_topic":"zigbee2mqtt/bulb",
|
||||
"unique_id":"0x000b57fffec6a5b2_light_zigbee2mqtt",
|
||||
"xy":false
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/light/0x000b57fffec6a5b2/light/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should discover devices with precision', async () => {
|
||||
@@ -218,7 +313,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -243,7 +338,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -268,7 +363,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -324,7 +419,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'custom model',
|
||||
'manufacturer': 'From Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
'expire_after': 90,
|
||||
'icon': 'mdi:test',
|
||||
};
|
||||
@@ -350,7 +445,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'custom model',
|
||||
'manufacturer': 'Not from Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
'expire_after': 30,
|
||||
'icon': 'mdi:test',
|
||||
};
|
||||
@@ -386,7 +481,7 @@ describe('HomeAssistant extension', () => {
|
||||
await flushPromises();
|
||||
|
||||
payload = {
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/my_switch/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
"command_topic": "zigbee2mqtt/my_switch/set",
|
||||
"device": {
|
||||
"identifiers": [
|
||||
@@ -456,7 +551,7 @@ describe('HomeAssistant extension', () => {
|
||||
"smart"
|
||||
],
|
||||
"json_attributes_topic":"zigbee2mqtt/fan",
|
||||
"name":"fan_fan",
|
||||
"name":"fan",
|
||||
"unique_id":"0x0017880104e45548_fan_zigbee2mqtt",
|
||||
"device":{
|
||||
"identifiers":[
|
||||
@@ -467,7 +562,7 @@ describe('HomeAssistant extension', () => {
|
||||
"model":"Universal wink enabled white ceiling fan premier remote control (99432)",
|
||||
"manufacturer":"Hampton Bay"
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/fan/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -488,7 +583,7 @@ describe('HomeAssistant extension', () => {
|
||||
payload = {
|
||||
"action_template":"{% set values = {'idle':'off','heat':'heating','cool':'cooling','fan only':'fan'} %}{{ values[value_json.running_state] }}",
|
||||
"action_topic":"zigbee2mqtt/TS0601_thermostat",
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/TS0601_thermostat/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
"away_mode_command_topic":"zigbee2mqtt/TS0601_thermostat/set/away_mode",
|
||||
"away_mode_state_template":"{{ value_json.away_mode }}",
|
||||
"away_mode_state_topic":"zigbee2mqtt/TS0601_thermostat",
|
||||
@@ -515,7 +610,7 @@ describe('HomeAssistant extension', () => {
|
||||
"hold_state_template":"{{ value_json.preset }}",
|
||||
"hold_state_topic":"zigbee2mqtt/TS0601_thermostat",
|
||||
"json_attributes_topic":"zigbee2mqtt/TS0601_thermostat",
|
||||
"max_temp":"30",
|
||||
"max_temp":"35",
|
||||
"min_temp":"5",
|
||||
"mode_command_topic":"zigbee2mqtt/TS0601_thermostat/set/system_mode",
|
||||
"mode_state_template":"{{ value_json.system_mode }}",
|
||||
@@ -523,7 +618,7 @@ describe('HomeAssistant extension', () => {
|
||||
"modes":[
|
||||
"auto"
|
||||
],
|
||||
"name":"TS0601_thermostat_climate",
|
||||
"name":"TS0601_thermostat",
|
||||
"temp_step":0.5,
|
||||
"temperature_command_topic":"zigbee2mqtt/TS0601_thermostat/set/current_heating_setpoint",
|
||||
"temperature_state_template":"{{ value_json.current_heating_setpoint }}",
|
||||
@@ -554,7 +649,7 @@ describe('HomeAssistant extension', () => {
|
||||
set_position_template: '{ "position": {{ position }} }',
|
||||
value_template: '{{ value_json.position }}',
|
||||
json_attributes_topic: 'zigbee2mqtt/smart vent',
|
||||
name: 'smart vent cover',
|
||||
name: 'smart vent',
|
||||
unique_id: '0x0017880104e45551_cover_zigbee2mqtt',
|
||||
device:
|
||||
{
|
||||
@@ -564,7 +659,7 @@ describe('HomeAssistant extension', () => {
|
||||
model: 'Smart vent (SV01)',
|
||||
manufacturer: 'Keen Home'
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/smart vent/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -598,7 +693,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -663,7 +758,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -878,7 +973,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor_renamed/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -949,7 +1044,7 @@ describe('HomeAssistant extension', () => {
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor_renamed/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -981,7 +1076,7 @@ describe('HomeAssistant extension', () => {
|
||||
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
|
||||
"manufacturer":"IKEA"
|
||||
},
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/bulb/availability'}],
|
||||
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -1290,7 +1385,7 @@ describe('HomeAssistant extension', () => {
|
||||
command_topic: true,
|
||||
},
|
||||
};
|
||||
expect(ha._getMapping()['external_converters_device']).toStrictEqual([homeassistantSwitch]);
|
||||
expect(ha._getMapping()['external_converters_device']).toEqual([homeassistantSwitch]);
|
||||
});
|
||||
|
||||
it('Should clear outdated configs', async () => {
|
||||
@@ -1300,26 +1395,26 @@ describe('HomeAssistant extension', () => {
|
||||
|
||||
// Non-existing device -> clear
|
||||
MQTT.publish.mockClear();
|
||||
await MQTT.events.message('homeassistant/sensor/0x123/temperature/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/0x123/availability'}]}));
|
||||
await MQTT.events.message('homeassistant/sensor/0x123/temperature/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/sensor/0x123/temperature/config', null, {qos: 0, retain: true}, expect.any(Function));
|
||||
|
||||
// Existing device -> don't clear
|
||||
MQTT.publish.mockClear();
|
||||
await MQTT.events.message('homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/0x000b57fffec6a5b2/availability'}]}));
|
||||
await MQTT.events.message('homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Non-existing device of different instance -> don't clear
|
||||
MQTT.publish.mockClear();
|
||||
await MQTT.events.message('homeassistant/sensor/0x123/temperature/config', stringify({availability: [{topic: 'zigbee2mqtt_different/bridge/state'}, {topic: 'zigbee2mqtt_different/0x123/availability'}]}));
|
||||
await MQTT.events.message('homeassistant/sensor/0x123/temperature/config', stringify({availability: [{topic: 'zigbee2mqtt_different/bridge/state'}]}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Existing device but non-existing config -> don't clear
|
||||
MQTT.publish.mockClear();
|
||||
await MQTT.events.message('homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/0x000b57fffec6a5b2/availability'}]}));
|
||||
await MQTT.events.message('homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', null, {qos: 0, retain: true}, expect.any(Function));
|
||||
|
||||
+2
-47
@@ -87,26 +87,7 @@ describe('Networkmap', () => {
|
||||
let call = MQTT.publish.mock.calls[0];
|
||||
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/raw');
|
||||
|
||||
const conv = (device) => {return {ieeeAddr: device.ieeeAddr, networkAddress: device.networkAddress}};
|
||||
|
||||
const expected = {
|
||||
"nodes":[
|
||||
{"definition":null,"lastSeen": 1000,"ieeeAddr":coordinator.ieeeAddr,"friendlyName":"Coordinator","type":"Coordinator","networkAddress":0,"modelID":null,"failed":[]},
|
||||
{"definition":{"description": "TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model": "LED1545G12","supports": "on/off, brightness, color temperature","vendor": "IKEA"},"lastSeen": 1000,"ieeeAddr":bulb.ieeeAddr,"friendlyName":"bulb","type":"Router","networkAddress":40369,"modelID":"TRADFRI bulb E27 WS opal 980lm","failed":[]},
|
||||
{"definition":{"description": "Hue Go","model": "7146060PH","supports": "on/off, brightness, color temperature, color xy, power-on behavior","vendor": "Philips"},"lastSeen": 1000,"ieeeAddr":bulb_color.ieeeAddr,"friendlyName":"bulb_color","type":"Router","networkAddress":40399,"modelID":"LLC020","failed":[]},
|
||||
{"definition":{"description": "Aqara double key wireless wall switch","model": "WXKG02LM","supports": "left, right, both click (and double, long click for left, right and both depending on model)","vendor": "Xiaomi"},"lastSeen": 1000,"ieeeAddr":WXKG02LM.ieeeAddr,"friendlyName":"button_double_key","type":"EndDevice","networkAddress":6538,"modelID":"lumi.sensor_86sw2.es1"},
|
||||
{"definition":null,"lastSeen": 1000,"ieeeAddr":unsupported_router.ieeeAddr,"friendlyName":"0x0017880104e45525","type":"Router","networkAddress":6536,"modelID":"notSupportedModelID","manufacturerName": "Boef","failed":['lqi', 'routingTable']},
|
||||
{"definition":{"description": "[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model": "CC2530.ROUTER","supports": "state, description, type, rssi","vendor": "Custom devices (DiY)"},"lastSeen": 1000,"ieeeAddr":CC2530_ROUTER.ieeeAddr,"friendlyName":"cc2530_router","type":"Router","networkAddress":6540,"modelID":"lumi.router","failed":[]},
|
||||
],
|
||||
"links":[
|
||||
{depth: 1, linkquality: 120, routes: [], source: conv(bulb_color), target: conv(coordinator), sourceIeeeAddr: bulb_color.ieeeAddr, sourceNwkAddr: bulb_color.networkAddress, targetIeeeAddr: coordinator.ieeeAddr, lqi: 120, relationship: 2},
|
||||
{depth: 1, linkquality: 92, routes: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}], source: conv(bulb), target: conv(coordinator), sourceIeeeAddr: bulb.ieeeAddr, sourceNwkAddr: bulb.networkAddress, targetIeeeAddr: coordinator.ieeeAddr, lqi: 92, relationship: 2},
|
||||
{depth: 2, linkquality: 110, routes: [], source: conv(bulb_color), target: conv(bulb), sourceIeeeAddr: bulb_color.ieeeAddr, sourceNwkAddr: bulb_color.networkAddress, targetIeeeAddr: bulb.ieeeAddr, lqi: 110, relationship: 1},
|
||||
{depth: 2, linkquality: 100, routes: [], source: conv(CC2530_ROUTER), target: conv(bulb), sourceIeeeAddr: CC2530_ROUTER.ieeeAddr, sourceNwkAddr: CC2530_ROUTER.networkAddress, targetIeeeAddr: bulb.ieeeAddr, lqi: 100, relationship: 1},
|
||||
{depth: 2, linkquality: 130, routes: [], source: conv(WXKG02LM), target: conv(CC2530_ROUTER), sourceIeeeAddr: WXKG02LM.ieeeAddr, sourceNwkAddr: WXKG02LM.networkAddress, targetIeeeAddr: CC2530_ROUTER.ieeeAddr, lqi: 130, relationship: 1}
|
||||
]
|
||||
};
|
||||
|
||||
const expected = {"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp), effect, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_xy), effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch","model":"WXKG02LM","supports":"battery, action, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"}]};
|
||||
expect(JSON.parse(call[1])).toStrictEqual(expected);
|
||||
|
||||
/**
|
||||
@@ -267,33 +248,7 @@ describe('Networkmap', () => {
|
||||
let call = MQTT.publish.mock.calls[0];
|
||||
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
|
||||
|
||||
const conv = (device) => {return {ieeeAddr: device.ieeeAddr, networkAddress: device.networkAddress}};
|
||||
|
||||
const expected = {
|
||||
status: 'ok',
|
||||
data: {
|
||||
type: 'raw',
|
||||
routes: true,
|
||||
value: {
|
||||
"nodes":[
|
||||
{"definition":null,"lastSeen": 1000,"ieeeAddr":coordinator.ieeeAddr,"friendlyName":"Coordinator","type":"Coordinator","networkAddress":0,"modelID":null,"failed":[]},
|
||||
{"definition":{"description": "TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model": "LED1545G12","supports": "on/off, brightness, color temperature","vendor": "IKEA"},"lastSeen": 1000,"ieeeAddr":bulb.ieeeAddr,"friendlyName":"bulb","type":"Router","networkAddress":40369,"modelID":"TRADFRI bulb E27 WS opal 980lm","failed":[]},
|
||||
{"definition":{"description": "Hue Go","model": "7146060PH","supports": "on/off, brightness, color temperature, color xy, power-on behavior","vendor": "Philips"},"lastSeen": 1000,"ieeeAddr":bulb_color.ieeeAddr,"friendlyName":"bulb_color","type":"Router","networkAddress":40399,"modelID":"LLC020","failed":[]},
|
||||
{"definition":{"description": "Aqara double key wireless wall switch","model": "WXKG02LM","supports": "left, right, both click (and double, long click for left, right and both depending on model)","vendor": "Xiaomi"},"lastSeen": 1000,"ieeeAddr":WXKG02LM.ieeeAddr,"friendlyName":"button_double_key","type":"EndDevice","networkAddress":6538,"modelID":"lumi.sensor_86sw2.es1"},
|
||||
{"definition":null,"lastSeen": 1000,"ieeeAddr":unsupported_router.ieeeAddr,"friendlyName":"0x0017880104e45525","type":"Router","networkAddress":6536,"modelID":"notSupportedModelID","manufacturerName": "Boef","failed":['lqi', 'routingTable']},
|
||||
{"definition":{"description": "[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model": "CC2530.ROUTER","supports": "state, description, type, rssi","vendor": "Custom devices (DiY)"},"lastSeen": 1000,"ieeeAddr":CC2530_ROUTER.ieeeAddr,"friendlyName":"cc2530_router","type":"Router","networkAddress":6540,"modelID":"lumi.router","failed":[]},
|
||||
],
|
||||
"links":[
|
||||
{depth: 1, linkquality: 120, routes: [], source: conv(bulb_color), target: conv(coordinator), sourceIeeeAddr: bulb_color.ieeeAddr, sourceNwkAddr: bulb_color.networkAddress, targetIeeeAddr: coordinator.ieeeAddr, lqi: 120, relationship: 2},
|
||||
{depth: 1, linkquality: 92, routes: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}], source: conv(bulb), target: conv(coordinator), sourceIeeeAddr: bulb.ieeeAddr, sourceNwkAddr: bulb.networkAddress, targetIeeeAddr: coordinator.ieeeAddr, lqi: 92, relationship: 2},
|
||||
{depth: 2, linkquality: 110, routes: [], source: conv(bulb_color), target: conv(bulb), sourceIeeeAddr: bulb_color.ieeeAddr, sourceNwkAddr: bulb_color.networkAddress, targetIeeeAddr: bulb.ieeeAddr, lqi: 110, relationship: 1},
|
||||
{depth: 2, linkquality: 100, routes: [], source: conv(CC2530_ROUTER), target: conv(bulb), sourceIeeeAddr: CC2530_ROUTER.ieeeAddr, sourceNwkAddr: CC2530_ROUTER.networkAddress, targetIeeeAddr: bulb.ieeeAddr, lqi: 100, relationship: 1},
|
||||
{depth: 2, linkquality: 130, routes: [], source: conv(WXKG02LM), target: conv(CC2530_ROUTER), sourceIeeeAddr: WXKG02LM.ieeeAddr, sourceNwkAddr: WXKG02LM.networkAddress, targetIeeeAddr: CC2530_ROUTER.ieeeAddr, lqi: 130, relationship: 1}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const expected = {"data":{"routes":true,"type":"raw","value":{"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp), effect, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_xy), effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch","model":"WXKG02LM","supports":"battery, action, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"}]}},"status":"ok"};
|
||||
const actual = JSON.parse(call[1]);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -27,13 +27,6 @@ describe('OTA update', () => {
|
||||
MQTT.publish.mockClear();
|
||||
});
|
||||
|
||||
it('Should subscribe to topics', async () => {
|
||||
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/ota_update/check');
|
||||
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/ota_update/update');
|
||||
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/request/device/ota_update/check');
|
||||
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/request/device/ota_update/update');
|
||||
});
|
||||
|
||||
it('Should OTA update a device', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const endpoint = device.endpoints[0];
|
||||
|
||||
@@ -339,7 +339,7 @@ describe('Publish', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/thermostat_group/set', stringify({child_lock: 'LOCK'}));
|
||||
await flushPromises();
|
||||
expect(group.command).toHaveBeenCalledTimes(1);
|
||||
expect(group.command).toHaveBeenCalledWith("manuSpecificTuyaDimmer", "setData", {data: [1,1], dp: 263, fn: 0, status: 0, transid: expect.any(Number)}, {disableDefaultResponse: true});
|
||||
expect(group.command).toHaveBeenCalledWith("manuSpecificTuya", "setData", {data: [1], datatype: 1, dp: 7, length_hi: 0, length_lo: 1, status: 0, transid: 0}, {disableDefaultResponse: true});
|
||||
});
|
||||
|
||||
it('Should publish messages to groups with on and brightness', async () => {
|
||||
@@ -388,10 +388,10 @@ describe('Publish', () => {
|
||||
|
||||
it('Should create and publish to group which is in configuration.yaml but not in zigbee-herdsman', async () => {
|
||||
delete zigbeeHerdsman.groups.group_2;
|
||||
expect(Object.values(zigbeeHerdsman.groups).length).toBe(6);
|
||||
expect(Object.values(zigbeeHerdsman.groups).length).toBe(7);
|
||||
await MQTT.events.message('zigbee2mqtt/group_2/set', stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
expect(Object.values(zigbeeHerdsman.groups).length).toBe(7);
|
||||
expect(Object.values(zigbeeHerdsman.groups).length).toBe(8);
|
||||
expect(zigbeeHerdsman.groups.group_2.command).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.groups.group_2.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
|
||||
});
|
||||
@@ -1203,7 +1203,7 @@ describe('Publish', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/MKS-CM-W5/l3/set', stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("manuSpecificTuyaDimmer", "setData", {data: [1,1], dp: 259, fn: 0, status: 0, transid: expect.any(Number)}, {disableDefaultResponse: true});
|
||||
expect(endpoint.command).toHaveBeenCalledWith("manuSpecificTuya", "setData", {data: [1], datatype: 1, dp: 3, length_hi: 0, length_lo: 1, status: 0, transid: 1}, {disableDefaultResponse: true});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/MKS-CM-W5');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state_l3: 'ON'});
|
||||
|
||||
@@ -418,7 +418,7 @@ describe('Receive', () => {
|
||||
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(logger.warn).toHaveBeenCalledWith(`Received message from unsupported device with Zigbee model 'notSupportedModelID'`);
|
||||
expect(logger.warn).toHaveBeenCalledWith(`Received message from unsupported device with Zigbee model 'notSupportedModelID' and manufacturer name 'notSupportedMfg'`);
|
||||
expect(logger.warn).toHaveBeenCalledWith(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
+1
-1
@@ -267,7 +267,7 @@ describe('Report', () => {
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Should only call Hue bulb, not e.g. tradfri
|
||||
expect(zigbeeHerdsman.devices.bulb.getEndpoint(1).read).toHaveBeenCalledTimes(0);
|
||||
expect(zigbeeHerdsman.devices.bulb_2.getEndpoint(1).read).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should not configure reporting for the ZNLDP12LM closuresWindowCovering as it is ignored', async () => {
|
||||
|
||||
+70
-1
@@ -6,6 +6,7 @@ const settings = require('../lib/util/settings.js');
|
||||
const fs = require('fs');
|
||||
const configurationFile = data.joinPath('configuration.yaml');
|
||||
const devicesFile = data.joinPath('devices.yaml');
|
||||
const devicesFile2 = data.joinPath('devices2.yaml');
|
||||
const groupsFile = data.joinPath('groups.yaml');
|
||||
const secretFile = data.joinPath('secret.yaml');
|
||||
const yaml = require('js-yaml');
|
||||
@@ -29,7 +30,7 @@ describe('Settings', () => {
|
||||
if (fs.existsSync(file)) fs.unlinkSync(file);
|
||||
}
|
||||
const clearEnvironmentVariables = () => {
|
||||
Object.keys(process.env).forEach((key) => {
|
||||
Object.keys(process.env).forEach((key) => {
|
||||
if(key.indexOf('ZIGBEE2MQTT_CONFIG_') >= 0) {
|
||||
delete process.env[key];
|
||||
}
|
||||
@@ -153,6 +154,7 @@ describe('Settings', () => {
|
||||
|
||||
const expected = {
|
||||
include_device_information: false,
|
||||
force_disable_retain: false,
|
||||
password: "mysecretpassword",
|
||||
server: "my.mqtt.server",
|
||||
user: "mysecretusername",
|
||||
@@ -196,6 +198,32 @@ describe('Settings', () => {
|
||||
expect(device).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should read devices form 2 separate files', () => {
|
||||
const contentConfiguration = {
|
||||
devices: ['devices.yaml', 'devices2.yaml']
|
||||
};
|
||||
|
||||
const contentDevices = {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
const contentDevices2 = {
|
||||
'0x87654321': {
|
||||
friendly_name: '0x87654321',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
write(configurationFile, contentConfiguration);
|
||||
write(devicesFile, contentDevices);
|
||||
write(devicesFile2, contentDevices2);
|
||||
expect(settings.getDevice('0x12345678').friendly_name).toStrictEqual('0x12345678');
|
||||
expect(settings.getDevice('0x87654321').friendly_name).toStrictEqual('0x87654321');
|
||||
});
|
||||
|
||||
it('Should add devices to a separate file', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
@@ -228,6 +256,47 @@ describe('Settings', () => {
|
||||
expect(read(devicesFile)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should add devices for first file when using 2 separates file', () => {
|
||||
const contentConfiguration = {
|
||||
devices: ['devices.yaml', 'devices2.yaml']
|
||||
};
|
||||
|
||||
const contentDevices = {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
const contentDevices2 = {
|
||||
'0x87654321': {
|
||||
friendly_name: '0x87654321',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
write(configurationFile, contentConfiguration);
|
||||
write(devicesFile, contentDevices);
|
||||
write(devicesFile2, contentDevices2);
|
||||
|
||||
settings.addDevice('0x1234');
|
||||
|
||||
expect(read(configurationFile)).toStrictEqual({devices: ['devices.yaml', 'devices2.yaml']});
|
||||
|
||||
const expected = {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
},
|
||||
'0x1234': {
|
||||
friendly_name: '0x1234',
|
||||
},
|
||||
};
|
||||
|
||||
expect(read(devicesFile)).toStrictEqual(expected);
|
||||
expect(read(devicesFile2)).toStrictEqual(contentDevices2);
|
||||
});
|
||||
|
||||
it('Should add devices to a separate file if devices.yaml doesnt exist', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
|
||||
@@ -131,7 +131,7 @@ const devices = {
|
||||
'bulb_2': bulb_2,
|
||||
'bulb_color_2': bulb_color_2,
|
||||
'remote': new Device('EndDevice', '0x0017880104e45517', 6535, 4107, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x0017880104e45517', [{target: bulb_color.endpoints[0], cluster: {ID: 8, name: 'genLevelCtrl'}}, {target: new Group(1, []), cluster: {ID: 6, name: 'genOnOff'}}]), new Endpoint(2, [0,1,3,15,64512], [25, 6])], true, "Battery", "RWL021"),
|
||||
'unsupported': new Device('EndDevice', '0x0017880104e45518', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "notSupportedModelID"),
|
||||
'unsupported': new Device('EndDevice', '0x0017880104e45518', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "notSupportedModelID", false, "notSupportedMfg"),
|
||||
'unsupported2': new Device('EndDevice', '0x0017880104e45529', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "notSupportedModelID"),
|
||||
'interviewing': new Device('EndDevice', '0x0017880104e45530', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", undefined, true),
|
||||
'notInSettings': new Device('EndDevice', '0x0017880104e45519', 6537, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "lumi.sensor_switch.aq2"),
|
||||
@@ -177,6 +177,7 @@ const groups = {
|
||||
'group_with_tradfri': new Group(11, [bulb_2.endpoints[0]]),
|
||||
'thermostat_group': new Group(12, [TS0601_thermostat.endpoints[0]]),
|
||||
'group_with_switch': new Group(14, [devices.ZNCZ02LM.endpoints[0]]),
|
||||
'default_bind_group': new Group(901, []),
|
||||
}
|
||||
|
||||
const mock = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const utils = require('../lib/util/utils.js');
|
||||
const version = require('../package.json').version;
|
||||
const versionHerdsman = require('../node_modules/zigbee-herdsman/package.json').version;
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('Is xiaomi device', () => {
|
||||
@@ -41,6 +42,11 @@ describe('Utils', () => {
|
||||
expect(await utils.getZigbee2mqttVersion()).toStrictEqual({"commitHash": "unknown", "version": version});
|
||||
})
|
||||
|
||||
it('Check dependency version', async () => {
|
||||
var dependency = 'zigbee-herdsman';
|
||||
expect(await utils.getDependencyVersion(dependency)).toStrictEqual({"version": versionHerdsman});
|
||||
})
|
||||
|
||||
it('To local iso string', async () => {
|
||||
var date = new Date('August 19, 1975 23:15:30 UTC+00:00');
|
||||
var getTimezoneOffset = Date.prototype.getTimezoneOffset;
|
||||
|
||||
Reference in New Issue
Block a user