* Initial implementation of backend for frontend.

* Frontend fixes (#4205)

* Send data to frontend api withoud baseTopic preffix

* Send frontend api requests to mqtt

* Fix topic name sanitisation

* Fix base_topic trimming

* Update frontend.js

* Add zigbee2mqtt-frontend dependency

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>

* Dont' setup separate MQTT connection.

* Correctly stop

* Add frontend tests.

* [WIP] Bindings structure change (#4233)

* Change bindings location

* Bump frontend version

* Republish devices on bindings change

* Fix data structure

* Fix  payload double encoding

* Change endpoints structure

* Expose config to bridge/info

* Fix typo

* Updates

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>

* Resend states on ws reconnect

* Update deps

* Bump frontend (#4264)

Co-authored-by: John Doe <nurikk@users.noreply.github.com>
This commit is contained in:
Koen Kanters 2020-09-04 18:42:24 +02:00 committed by GitHub
parent 84029e2057
commit ba7a85bbb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 492 additions and 66 deletions

View File

@ -13,6 +13,7 @@ const path = require('path');
const assert = require('assert'); const assert = require('assert');
// Extensions // Extensions
const ExtensionFrontend = require('./extension/frontend');
const ExtensionPublish = require('./extension/publish'); const ExtensionPublish = require('./extension/publish');
const ExtensionReceive = require('./extension/receive'); const ExtensionReceive = require('./extension/receive');
const ExtensionNetworkMap = require('./extension/networkMap'); const ExtensionNetworkMap = require('./extension/networkMap');
@ -67,6 +68,10 @@ class Controller {
this.extensions.push(new ExtensionBridge(...args, this.enableDisableExtension)); this.extensions.push(new ExtensionBridge(...args, this.enableDisableExtension));
} }
if (settings.get().experimental.frontend) {
this.extensions.push(new ExtensionFrontend(...args));
}
if (settings.get().advanced.legacy_api) { if (settings.get().advanced.legacy_api) {
this.extensions.push(new ExtensionBridgeLegacy(...args)); this.extensions.push(new ExtensionBridgeLegacy(...args));
} }
@ -143,6 +148,7 @@ class Controller {
// MQTT // MQTT
this.mqtt.on('message', this.onMQTTMessage.bind(this)); this.mqtt.on('message', this.onMQTTMessage.bind(this));
await this.mqtt.connect(); await this.mqtt.connect();
this.mqtt.publish('bridge/state', 'online', {retain: true, qos: 0});
// Send all cached states. // Send all cached states.
if (settings.get().advanced.cache_state_send_on_startup && settings.get().advanced.cache_state) { if (settings.get().advanced.cache_state_send_on_startup && settings.get().advanced.cache_state) {

View File

@ -9,6 +9,7 @@ const allowedEvents = [
'stateChange', // Entity changes its state 'stateChange', // Entity changes its state
'groupMembersChanged', // Members of a group has been changed 'groupMembersChanged', // Members of a group has been changed
'reportingDisabled', // Reporting is disabled for a device 'reportingDisabled', // Reporting is disabled for a device
'deviceBindingsChanged', // Device bindings changed
]; ];
class EventBus extends events.EventEmitter { class EventBus extends events.EventEmitter {

View File

@ -157,6 +157,8 @@ class Bind extends Extension {
if (error) { if (error) {
logger.error(error); logger.error(error);
} else {
this.eventBus.emit(`deviceBindingsChanged`);
} }
} }
} }

View File

@ -5,6 +5,7 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const settings = require('../util/settings'); const settings = require('../util/settings');
const Transport = require('winston-transport'); const Transport = require('winston-transport');
const stringify = require('json-stable-stringify'); const stringify = require('json-stable-stringify');
const objectAssignDeep = require(`object-assign-deep`);
const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`); const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
@ -38,6 +39,7 @@ class Bridge extends Extension {
this.coordinatorVersion = await this.zigbee.getCoordinatorVersion(); this.coordinatorVersion = await this.zigbee.getCoordinatorVersion();
this.eventBus.on(`groupMembersChanged`, () => this.publishGroups()); this.eventBus.on(`groupMembersChanged`, () => this.publishGroups());
this.eventBus.on(`deviceBindingsChanged`, () => this.publishDevices());
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/#`); this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/#`);
await this.publishInfo(); await this.publishInfo();
await this.publishDevices(); await this.publishDevices();
@ -174,6 +176,7 @@ class Bridge extends Extension {
} }
settings.set(['advanced', 'last_seen'], message); settings.set(['advanced', 'last_seen'], message);
this.publishInfo();
return utils.getResponse(message, {value}, null); return utils.getResponse(message, {value}, null);
} }
@ -186,6 +189,7 @@ class Bridge extends Extension {
this.enableDisableExtension(value, 'HomeAssistant'); this.enableDisableExtension(value, 'HomeAssistant');
settings.set(['homeassistant'], value); settings.set(['homeassistant'], value);
this.publishInfo();
return utils.getResponse(message, {value}, null); return utils.getResponse(message, {value}, null);
} }
@ -197,6 +201,7 @@ class Bridge extends Extension {
} }
settings.set(['advanced', 'elapsed'], value); settings.set(['advanced', 'elapsed'], value);
this.publishInfo();
return utils.getResponse(message, {value}, null); return utils.getResponse(message, {value}, null);
} }
@ -362,6 +367,8 @@ class Bridge extends Extension {
} }
async publishInfo() { async publishInfo() {
const config = objectAssignDeep.noMutate({}, settings.get());
delete config.advanced.network_key;
const payload = { const payload = {
version: this.zigbee2mqttVersion.version, version: this.zigbee2mqttVersion.version,
commit: this.zigbee2mqttVersion.commitHash, commit: this.zigbee2mqttVersion.commitHash,
@ -369,6 +376,7 @@ class Bridge extends Extension {
network: utils.toSnakeCase(await this.zigbee.getNetworkParameters()), network: utils.toSnakeCase(await this.zigbee.getNetworkParameters()),
log_level: logger.getLevel(), log_level: logger.getLevel(),
permit_join: await this.zigbee.getPermitJoin(), permit_join: await this.zigbee.getPermitJoin(),
config,
}; };
await this.mqtt.publish('bridge/info', stringify(payload), {retain: true, qos: 0}); await this.mqtt.publish('bridge/info', stringify(payload), {retain: true, qos: 0});

116
lib/extension/frontend.js Normal file
View File

@ -0,0 +1,116 @@
const http = require('http');
const httpProxy = require('http-proxy');
const nStatic = require('node-static');
const Extension = require('./extension');
const logger = require('../util/logger');
const frontend = require('zigbee2mqtt-frontend');
const WebSocket = require('ws');
const url = require('url');
const settings = require('../util/settings');
const utils = require('../util/utils');
const stringify = require('json-stable-stringify');
/**
* This extension servers the frontend
*/
class Frontend extends Extension {
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
this.onRequest = this.onRequest.bind(this);
this.onUpgrade = this.onUpgrade.bind(this);
this.mqttBaseTopic = settings.get().mqtt.base_topic;
this.onMQTTPublishedMessage = this.onMQTTPublishedMessage.bind(this);
this.mqtt.on('publishedMessage', this.onMQTTPublishedMessage);
this.onWebSocketConnection = this.onWebSocketConnection.bind(this);
this.server = http.createServer(this.onRequest);
this.server.on('upgrade', this.onUpgrade);
this.developmentServer = settings.get().experimental.frontend.development_server;
this.development = !!this.developmentServer;
this.port = settings.get().experimental.frontend.port || 8080;
this.retainedMessages = new Map();
if (this.development) {
this.proxy = httpProxy.createProxyServer({ws: true});
} else {
this.fileServer = new nStatic.Server(frontend.getPath());
}
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}`);
}
async stop() {
for (const client of this.wss.clients) {
client.close();
}
return new Promise((resolve) => {
this.server.close(resolve);
});
}
onRequest(request, response) {
if (this.development) {
this.proxy.web(request, response, {target: `http://${this.developmentServer}`});
} else {
this.fileServer.serve(request, response);
}
}
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();
}
}
onWebSocketConnection(ws) {
ws.on('message', (message) => {
const {topic, payload} = utils.parseJSON(message, message);
this.mqtt.onMessage(`${this.mqttBaseTopic}/${topic}`, stringify(payload));
});
for (const [key, value] of this.retainedMessages) {
ws.send(stringify({topic: key, payload: value}));
}
for (const device of this.zigbee.getClients()) {
if (this.state.exists(device.ieeeAddr)) {
const resolvedEntity = this.zigbee.resolveEntity(device);
ws.send(stringify({topic: resolvedEntity.name, payload: this.state.get(device.ieeeAddr)}));
}
}
}
onMQTTPublishedMessage(data) {
let {topic, payload, options} = data;
// Send topic without base_topic
topic = topic.substring(this.mqttBaseTopic.length + 1);
payload = utils.parseJSON(payload, payload);
if (options.retain) {
this.retainedMessages.set(topic, payload);
}
for (const client of this.wss.clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(stringify({topic, payload}));
}
}
}
}
module.exports = Frontend;

View File

@ -71,7 +71,6 @@ class MQTT extends events.EventEmitter {
this.client.on('connect', () => { this.client.on('connect', () => {
logger.info('Connected to MQTT server'); logger.info('Connected to MQTT server');
this.publish('bridge/state', 'online', {retain: true, qos: 0});
resolve(); resolve();
}); });
@ -116,7 +115,10 @@ class MQTT extends events.EventEmitter {
} }
return new Promise((resolve) => { return new Promise((resolve) => {
this.client.publish(topic, payload, options, () => resolve()); this.client.publish(topic, payload, options, () => {
this.emit('publishedMessage', {topic, payload, options});
resolve();
});
}); });
} }
} }

196
npm-shrinkwrap.json generated
View File

@ -14,19 +14,19 @@
} }
}, },
"@babel/core": { "@babel/core": {
"version": "7.11.4", "version": "7.11.6",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.4.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz",
"integrity": "sha512-5deljj5HlqRXN+5oJTY7Zs37iH3z3b++KjiKtIsJy1NrjOOVSEaJHEetLBhyu0aQOSNNZ/0IuEAan9GzRuDXHg==", "integrity": "sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.10.4", "@babel/code-frame": "^7.10.4",
"@babel/generator": "^7.11.4", "@babel/generator": "^7.11.6",
"@babel/helper-module-transforms": "^7.11.0", "@babel/helper-module-transforms": "^7.11.0",
"@babel/helpers": "^7.10.4", "@babel/helpers": "^7.10.4",
"@babel/parser": "^7.11.4", "@babel/parser": "^7.11.5",
"@babel/template": "^7.10.4", "@babel/template": "^7.10.4",
"@babel/traverse": "^7.11.0", "@babel/traverse": "^7.11.5",
"@babel/types": "^7.11.0", "@babel/types": "^7.11.5",
"convert-source-map": "^1.7.0", "convert-source-map": "^1.7.0",
"debug": "^4.1.0", "debug": "^4.1.0",
"gensync": "^1.0.0-beta.1", "gensync": "^1.0.0-beta.1",
@ -52,12 +52,12 @@
} }
}, },
"@babel/generator": { "@babel/generator": {
"version": "7.11.4", "version": "7.11.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.4.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz",
"integrity": "sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g==", "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/types": "^7.11.0", "@babel/types": "^7.11.5",
"jsesc": "^2.5.1", "jsesc": "^2.5.1",
"source-map": "^0.5.0" "source-map": "^0.5.0"
}, },
@ -211,9 +211,9 @@
} }
}, },
"@babel/parser": { "@babel/parser": {
"version": "7.11.4", "version": "7.11.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz",
"integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==",
"dev": true "dev": true
}, },
"@babel/plugin-syntax-async-generators": { "@babel/plugin-syntax-async-generators": {
@ -327,17 +327,17 @@
} }
}, },
"@babel/traverse": { "@babel/traverse": {
"version": "7.11.0", "version": "7.11.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz",
"integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.10.4", "@babel/code-frame": "^7.10.4",
"@babel/generator": "^7.11.0", "@babel/generator": "^7.11.5",
"@babel/helper-function-name": "^7.10.4", "@babel/helper-function-name": "^7.10.4",
"@babel/helper-split-export-declaration": "^7.11.0", "@babel/helper-split-export-declaration": "^7.11.0",
"@babel/parser": "^7.11.0", "@babel/parser": "^7.11.5",
"@babel/types": "^7.11.0", "@babel/types": "^7.11.5",
"debug": "^4.1.0", "debug": "^4.1.0",
"globals": "^11.1.0", "globals": "^11.1.0",
"lodash": "^4.17.19" "lodash": "^4.17.19"
@ -352,9 +352,9 @@
} }
}, },
"@babel/types": { "@babel/types": {
"version": "7.11.0", "version": "7.11.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz",
"integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.10.4", "@babel/helper-validator-identifier": "^7.10.4",
@ -388,6 +388,24 @@
"kuler": "^2.0.0" "kuler": "^2.0.0"
} }
}, },
"@eslint/eslintrc": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz",
"integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.1.1",
"espree": "^7.3.0",
"globals": "^12.1.0",
"ignore": "^4.0.6",
"import-fresh": "^3.2.1",
"js-yaml": "^3.13.1",
"lodash": "^4.17.19",
"minimatch": "^3.0.4",
"strip-json-comments": "^3.1.1"
}
},
"@istanbuljs/load-nyc-config": { "@istanbuljs/load-nyc-config": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@ -706,15 +724,15 @@
} }
}, },
"@types/json-schema": { "@types/json-schema": {
"version": "7.0.5", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
"integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==", "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
"version": "14.6.2", "version": "14.6.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz",
"integrity": "sha512-onlIwbaeqvZyniGPfdw/TEhKIh79pz66L1q06WUQqJLnAb6wbjvOtepLYTGHTqzdXgBYIE3ZdmqHDGsRsbBz7A==", "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==",
"dev": true "dev": true
}, },
"@types/normalize-package-data": { "@types/normalize-package-data": {
@ -1883,12 +1901,13 @@
} }
}, },
"eslint": { "eslint": {
"version": "7.7.0", "version": "7.8.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.7.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.8.1.tgz",
"integrity": "sha512-1KUxLzos0ZVsyL81PnRN335nDtQ8/vZUD6uMtWbF+5zDtjKcsklIi78XoE0MVL93QvWTu+E5y44VyyCsOMBrIg==", "integrity": "sha512-/2rX2pfhyUG0y+A123d0ccXtMm7DV7sH1m3lk9nk2DZ2LReq39FXHueR9xZwshE5MdfSf0xunSaMWRqyIA6M1w==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.0.0", "@babel/code-frame": "^7.0.0",
"@eslint/eslintrc": "^0.1.3",
"ajv": "^6.10.0", "ajv": "^6.10.0",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.2",
@ -1898,7 +1917,7 @@
"eslint-scope": "^5.1.0", "eslint-scope": "^5.1.0",
"eslint-utils": "^2.1.0", "eslint-utils": "^2.1.0",
"eslint-visitor-keys": "^1.3.0", "eslint-visitor-keys": "^1.3.0",
"espree": "^7.2.0", "espree": "^7.3.0",
"esquery": "^1.2.0", "esquery": "^1.2.0",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"file-entry-cache": "^5.0.1", "file-entry-cache": "^5.0.1",
@ -2020,12 +2039,20 @@
} }
}, },
"esrecurse": { "esrecurse": {
"version": "4.2.1", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true, "dev": true,
"requires": { "requires": {
"estraverse": "^4.1.0" "estraverse": "^5.2.0"
},
"dependencies": {
"estraverse": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"dev": true
}
} }
}, },
"estraverse": { "estraverse": {
@ -2049,6 +2076,11 @@
"es5-ext": "~0.10.14" "es5-ext": "~0.10.14"
} }
}, },
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"exec-sh": { "exec-sh": {
"version": "0.3.4", "version": "0.3.4",
"resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz",
@ -2438,6 +2470,11 @@
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
}, },
"follow-redirects": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
"integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA=="
},
"for-in": { "for-in": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@ -2707,6 +2744,16 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true "dev": true
}, },
"http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"requires": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
}
},
"http-signature": { "http-signature": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@ -3641,9 +3688,9 @@
"dev": true "dev": true
}, },
"json-parse-even-better-errors": { "json-parse-even-better-errors": {
"version": "2.3.0", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.0.tgz", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-o3aP+RsWDJZayj1SbHNQAI8x0v3T3SKiGoZlNYfbUP1S3omJQ6i9CnqADqkSPaOAxwua4/1YWx5CM7oiChJt2Q==", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true "dev": true
}, },
"json-schema": { "json-schema": {
@ -3830,6 +3877,11 @@
"picomatch": "^2.0.5" "picomatch": "^2.0.5"
} }
}, },
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": { "mime-db": {
"version": "1.44.0", "version": "1.44.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
@ -3928,22 +3980,13 @@
} }
}, },
"mqtt-packet": { "mqtt-packet": {
"version": "6.4.0", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.4.0.tgz", "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.5.0.tgz",
"integrity": "sha512-dNd1RPyBolklOR27hgHhy3TxkDk31ZaDu4ljAgJoHlnVsdACH8guwEZhpk3ZMn6GAdH6ENDLgtE285FHIiXzxA==", "integrity": "sha512-Pzv0auCMip3D2JZId5n8q084ASyi3AvpjP1qJUf9d/gjuk2YkjD5xSUe/KPKgXIqfr5x99bRm5FdAm7ag96RBQ==",
"requires": { "requires": {
"bl": "^4.0.2", "bl": "^4.0.2",
"debug": "^4.1.1", "debug": "^4.1.1",
"inherits": "^2.0.4", "process-nextick-args": "^2.0.1"
"process-nextick-args": "^2.0.1",
"safe-buffer": "^5.2.1"
},
"dependencies": {
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
} }
}, },
"ms": { "ms": {
@ -4020,6 +4063,16 @@
"which": "^2.0.2" "which": "^2.0.2"
} }
}, },
"node-static": {
"version": "0.7.11",
"resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.11.tgz",
"integrity": "sha512-zfWC/gICcqb74D9ndyvxZWaI1jzcoHmf4UTHWQchBNuNMxdBLJMDiUgZ1tjGLEIe/BMhj2DxKD8HOuc2062pDQ==",
"requires": {
"colors": ">=0.6.0",
"mime": "^1.2.9",
"optimist": ">=0.3.4"
}
},
"normalize-package-data": { "normalize-package-data": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@ -4154,6 +4207,22 @@
"mimic-fn": "^2.1.0" "mimic-fn": "^2.1.0"
} }
}, },
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
"requires": {
"minimist": "~0.0.1",
"wordwrap": "~0.0.2"
},
"dependencies": {
"minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
}
}
},
"optionator": { "optionator": {
"version": "0.9.1", "version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@ -4601,6 +4670,11 @@
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true "dev": true
}, },
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"resolve": { "resolve": {
"version": "1.17.0", "version": "1.17.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
@ -5654,9 +5728,9 @@
} }
}, },
"uri-js": { "uri-js": {
"version": "4.2.2", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz",
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==",
"requires": { "requires": {
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
@ -5857,6 +5931,11 @@
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true "dev": true
}, },
"wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc="
},
"wrap-ansi": { "wrap-ansi": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@ -19913,6 +19992,11 @@
} }
} }
} }
},
"zigbee2mqtt-frontend": {
"version": "0.1.44",
"resolved": "https://registry.npmjs.org/zigbee2mqtt-frontend/-/zigbee2mqtt-frontend-0.1.44.tgz",
"integrity": "sha512-C7jU5v+rXQNpWOP7NjwWLjL1uACqk+u+saaHXhT1zlH2E2wGp0KMg+q0yAt2aYKRBK8pyhjSXDVo0szyQekVgQ=="
} }
} }
} }

View File

@ -37,19 +37,23 @@
"debounce": "^1.2.0", "debounce": "^1.2.0",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"git-last-commit": "^1.0.0", "git-last-commit": "^1.0.0",
"http-proxy": "^1.18.1",
"humanize-duration": "^3.23.1", "humanize-duration": "^3.23.1",
"js-yaml": "^3.14.0", "js-yaml": "^3.14.0",
"json-stable-stringify": "^1.0.1", "json-stable-stringify": "^1.0.1",
"mkdir-recursive": "^0.4.0", "mkdir-recursive": "^0.4.0",
"moment": "^2.27.0", "moment": "^2.27.0",
"mqtt": "^4.2.1", "mqtt": "^4.2.1",
"node-static": "^0.7.11",
"object-assign-deep": "^0.4.0", "object-assign-deep": "^0.4.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"semver": "^7.3.2", "semver": "^7.3.2",
"winston": "^3.3.3", "winston": "^3.3.3",
"winston-syslog": "^2.4.4", "winston-syslog": "^2.4.4",
"ws": "^7.3.1",
"zigbee-herdsman": "0.12.130", "zigbee-herdsman": "0.12.130",
"zigbee-herdsman-converters": "12.0.179" "zigbee-herdsman-converters": "12.0.179",
"zigbee2mqtt-frontend": "^0.1.44"
}, },
"devDependencies": { "devDependencies": {
"eslint": "*", "eslint": "*",

View File

@ -50,11 +50,19 @@ describe('Bind', () => {
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target);
expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target);
expect(MQTT.publish).toHaveBeenCalledTimes(5);
expect(MQTT.publish).toHaveBeenCalledWith( expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind', 'zigbee2mqtt/bridge/response/device/bind',
stringify({"data":{"from":"remote","to":"bulb_color","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}), stringify({"data":{"from":"remote","to":"bulb_color","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}),
{retain: false, qos: 0}, expect.any(Function) {retain: false, qos: 0}, expect.any(Function)
); );
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/devices',
expect.any(String),
{ retain: true, qos: 0 },
expect.any(Function)
);
}); });
it('Should bind only specifief clusters', async () => { it('Should bind only specifief clusters', async () => {
@ -266,7 +274,7 @@ describe('Bind', () => {
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target);
expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target);
expect(MQTT.publish).toHaveBeenCalledTimes(3); expect(MQTT.publish).toHaveBeenCalledTimes(4);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'}}); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'}});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');
@ -297,7 +305,7 @@ describe('Bind', () => {
expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", target); expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", target);
expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", target); expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", target);
expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", target); expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", target);
expect(MQTT.publish).toHaveBeenCalledTimes(3); expect(MQTT.publish).toHaveBeenCalledTimes(4);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'}}); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'}});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');
@ -318,7 +326,7 @@ describe('Bind', () => {
expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", target); expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", target);
expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", target); expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", target);
expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", target); expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", target);
expect(MQTT.publish).toHaveBeenCalledTimes(3); expect(MQTT.publish).toHaveBeenCalledTimes(4);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'Coordinator', cluster: 'genScenes'}}); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'Coordinator', cluster: 'genScenes'}});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');
@ -338,7 +346,7 @@ describe('Bind', () => {
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target);
expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target);
expect(MQTT.publish).toHaveBeenCalledTimes(3); expect(MQTT.publish).toHaveBeenCalledTimes(4);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genScenes'}}); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genScenes'}});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');
@ -358,7 +366,7 @@ describe('Bind', () => {
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target);
expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target);
expect(MQTT.publish).toHaveBeenCalledTimes(3); expect(MQTT.publish).toHaveBeenCalledTimes(4);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genScenes'}}); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genScenes'}});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');
@ -412,7 +420,7 @@ describe('Bind', () => {
expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", 901); expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", 901);
expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", 901); expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", 901);
expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", 901); expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", 901);
expect(MQTT.publish).toHaveBeenCalledTimes(3); expect(MQTT.publish).toHaveBeenCalledTimes(4);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'default_bind_group', cluster: 'genScenes'}}); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'default_bind_group', cluster: 'genScenes'}});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');

View File

@ -38,9 +38,10 @@ describe('Bridge', () => {
it('Should publish bridge info on startup', async () => { it('Should publish bridge info on startup', async () => {
const version = await require('../lib/util/utils').getZigbee2mqttVersion(); const version = await require('../lib/util/utils').getZigbee2mqttVersion();
const directory = settings.get().advanced.log_directory;
expect(MQTT.publish).toHaveBeenCalledWith( expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/info', 'zigbee2mqtt/bridge/info',
stringify({"version":version.version,"commit":version.commitHash,"coordinator":{"type":"z-Stack","meta":{"version":1,"revision":20190425}},"network":{"channel":15,"pan_id":5674,"extended_pan_id":[0,11,22]},"log_level":"info","permit_join":false}), stringify({"commit":version.commitHash,"config":{"advanced":{"adapter_concurrent":null,"availability_blacklist":[],"availability_blocklist":[],"availability_passlist":[],"availability_timeout":0,"availability_whitelist":[],"cache_state":true,"cache_state_persistent":true,"cache_state_send_on_startup":true,"channel":11,"elapsed":false,"ext_pan_id":[221,221,221,221,221,221,221,221],"homeassistant_discovery_topic":"homeassistant","homeassistant_legacy_triggers":true,"homeassistant_status_topic":"hass/status","last_seen":"disable","legacy_api":false,"log_directory":directory,"log_file":"log.txt","log_level":"info","log_output":["console","file"],"log_rotation":true,"log_syslog":{},"pan_id":6754,"report":false,"soft_reset_timeout":0,"timestamp_format":"YYYY-MM-DD HH:mm:ss"},"ban":[],"blocklist":[],"device_options":{},"devices":{"0x000b57fffec6a5b2":{"friendly_name":"bulb","retain":true},"0x000b57fffec6a5b3":{"friendly_name":"bulb_color","retain":false},"0x000b57fffec6a5b4":{"friendly_name":"bulb_color_2","retain":false},"0x000b57fffec6a5b7":{"friendly_name":"bulb_2","retain":false},"0x0017880104a44559":{"friendly_name":"J1_cover"},"0x0017880104e43559":{"friendly_name":"U202DST600ZB"},"0x0017880104e44559":{"friendly_name":"3157100_thermostat"},"0x0017880104e45517":{"friendly_name":"remote","retain":true},"0x0017880104e45518":{"friendly_name":"0x0017880104e45518"},"0x0017880104e45520":{"friendly_name":"button","retain":false},"0x0017880104e45521":{"friendly_name":"button_double_key","retain":false},"0x0017880104e45522":{"friendly_name":"weather_sensor","qos":1,"retain":false},"0x0017880104e45523":{"friendly_name":"occupancy_sensor","retain":false},"0x0017880104e45524":{"friendly_name":"power_plug","retain":false},"0x0017880104e45526":{"friendly_name":"GL-S-007ZS"},"0x0017880104e45529":{"friendly_name":"unsupported2","retain":false},"0x0017880104e45530":{"friendly_name":"button_double_key_interviewing","retain":false},"0x0017880104e45540":{"friendly_name":"ikea_onoff"},"0x0017880104e45541":{"friendly_name":"wall_switch","retain":false},"0x0017880104e45542":{"friendly_name":"wall_switch_double","retain":false},"0x0017880104e45543":{"friendly_name":"led_controller_1","retain":false},"0x0017880104e45544":{"friendly_name":"led_controller_2","retain":false},"0x0017880104e45545":{"friendly_name":"dimmer_wall_switch","retain":false},"0x0017880104e45547":{"friendly_name":"curtain","retain":false},"0x0017880104e45548":{"friendly_name":"fan","retain":false},"0x0017880104e45549":{"friendly_name":"siren","retain":false},"0x0017880104e45550":{"friendly_name":"thermostat","retain":false},"0x0017880104e45551":{"friendly_name":"smart vent","retain":false},"0x0017880104e45552":{"friendly_name":"j1","retain":false},"0x0017880104e45553":{"friendly_name":"bulb_enddevice","retain":false},"0x0017880104e45559":{"friendly_name":"cc2530_router","retain":false},"0x0017880104e45560":{"friendly_name":"livolo","retain":false},"0x0017882104a44559":{"friendly_name":"TS0601_thermostat"},"0x90fd9ffffe4b64aa":{"friendly_name":"SP600_OLD"},"0x90fd9ffffe4b64ab":{"friendly_name":"SP600_NEW"},"0x90fd9ffffe4b64ac":{"friendly_name":"MKS-CM-W5"},"0x90fd9ffffe4b64ae":{"friendly_name":"tradfri_remote","retain":false},"0x90fd9ffffe4b64af":{"friendly_name":"roller_shutter"},"0x90fd9ffffe4b64ax":{"friendly_name":"ZNLDP12LM"}},"experimental":{"new_api":true,"output":"json"},"external_converters":[],"groups":{"1":{"friendly_name":"group_1","retain":false},"11":{"devices":["bulb_2"],"friendly_name":"group_with_tradfri","retain":false},"12":{"devices":["TS0601_thermostat"],"friendly_name":"thermostat_group","retain":false},"15071":{"devices":["bulb_color_2","bulb_2"],"friendly_name":"group_tradfri_remote","retain":false},"2":{"friendly_name":"group_2","retain":false}},"homeassistant":false,"map_options":{"graphviz":{"colors":{"fill":{"coordinator":"#e04e5d","enddevice":"#fff8ce","router":"#4ea3e0"},"font":{"coordinator":"#ffffff","enddevice":"#000000","router":"#ffffff"},"line":{"active":"#009900","inactive":"#994444"}}}},"mqtt":{"base_topic":"zigbee2mqtt","include_device_information":false,"server":"mqtt://localhost"},"passlist":[],"permit_join":true,"serial":{"disable_led":false,"port":"/dev/dummy"},"whitelist":[]},"coordinator":{"meta":{"revision":20190425,"version":1},"type":"z-Stack"},"log_level":"info","network":{"channel":15,"extended_pan_id":[0,11,22],"pan_id":5674},"permit_join":false,"version":version.version}),
{ retain: true, qos: 0 }, { retain: true, qos: 0 },
expect.any(Function) expect.any(Function)
); );

194
test/frontend.test.js Normal file
View File

@ -0,0 +1,194 @@
const data = require('./stub/data');
require('./stub/logger');
require('./stub/zigbeeHerdsman');
const MQTT = require('./stub/mqtt');
const settings = require('../lib/util/settings');
const Controller = require('../lib/controller');
const stringify = require('json-stable-stringify');
const flushPromises = () => new Promise(setImmediate);
jest.spyOn(process, 'exit').mockImplementation(() => {});
const mockHTTP = {
implementation: {
listen: jest.fn(),
on: (event, handler) => {mockHTTP.events[event] = handler},
close: jest.fn().mockImplementation((cb) => cb()),
},
variables: {},
events: {},
};
const mockHTTPProxy = {
implementation: {
web: jest.fn(),
ws: jest.fn(),
},
variables: {},
events: {},
};
const mockWS = {
implementation: {
clients: [],
on: (event, handler) => {mockWS.events[event] = handler},
handleUpgrade: jest.fn(),
emit: jest.fn(),
},
variables: {},
events: {},
};
const mockNodeStatic = {
implementation: {
serve: jest.fn(),
},
variables: {},
events: {},
};
jest.mock('http', () => ({
createServer: jest.fn().mockImplementation((onRequest) => {
mockHTTP.variables.onRequest = onRequest;
return mockHTTP.implementation;
}),
}));
jest.mock('http-proxy', () => ({
createProxyServer: jest.fn().mockImplementation((initParameter) => {
mockHTTPProxy.variables.initParameter = initParameter;
return mockHTTPProxy.implementation;
}),
}));
jest.mock('node-static', () => ({
Server: jest.fn().mockImplementation((path) => {
mockNodeStatic.variables.path = path;
return mockNodeStatic.implementation;
}),
}));
jest.mock('zigbee2mqtt-frontend', () => ({
getPath: () => 'my/dummy/path',
}));
jest.mock('ws', () => ({
OPEN: 'open',
Server: jest.fn().mockImplementation(() => {
return mockWS.implementation;
}),
}));
describe('Frontend', () => {
let controller;
beforeEach(async () => {
mockWS.implementation.clients = [];
data.writeDefaultConfiguration();
data.writeDefaultState();
settings._reRead();
settings.set(['experimental'], {new_api: true, frontend: {port: 8081}});
});
it('Start/stop', async () => {
controller = new Controller();
await controller.start();
expect(mockNodeStatic.variables.path).toBe("my/dummy/path");
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081);
const mockWSClient = {
implementation: {
close: jest.fn(),
},
events: {},
};
mockWS.implementation.clients.push(mockWSClient.implementation);
await controller.stop();
expect(mockWSClient.implementation.close).toHaveBeenCalledTimes(1);
expect(mockHTTP.implementation.close).toHaveBeenCalledTimes(1);
});
it('Websocket interaction', async () => {
controller = new Controller();
await controller.start();
// Connect
const mockWSClient = {
implementation: {
on: (event, handler) => {mockWSClient.events[event] = handler},
send: jest.fn(),
readyState: 'open',
},
events: {},
};
mockWS.implementation.clients.push(mockWSClient.implementation);
await mockWS.events.connection(mockWSClient.implementation);
expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(9);
expect(JSON.parse(mockWSClient.implementation.send.mock.calls[0])).toStrictEqual({topic: 'bridge/state', payload: 'online'});
expect(JSON.parse(mockWSClient.implementation.send.mock.calls[8])).toStrictEqual({topic:"remote", payload:{brightness:255}});
// Message
MQTT.publish.mockClear();
mockWSClient.implementation.send.mockClear();
mockWSClient.events.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}))
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color',
stringify({state: 'ON'}),
{ retain: false, qos: 0 },
expect.any(Function)
);
// Received message on socket
expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(1);
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bulb_color', payload: {state: 'ON'}}));
// Shouldnt set when not ready
mockWSClient.implementation.send.mockClear();
mockWSClient.implementation.readyState = 'close';
mockWSClient.events.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}))
expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(0);
});
it('onReques/onUpgrade', async () => {
controller = new Controller();
await controller.start();
const mockSocket = {destroy: jest.fn()};
mockWS.implementation.handleUpgrade.mockClear();
mockHTTP.events.upgrade({url: 'http://localhost:8080/api'}, mockSocket, 3);
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(1);
expect(mockSocket.destroy).toHaveBeenCalledTimes(0);
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({"url": "http://localhost:8080/api"}, mockSocket, 3, expect.any(Function));
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.serve).toHaveBeenCalledTimes(1);
expect(mockNodeStatic.implementation.serve).toHaveBeenCalledWith(1, 2);
});
it('Development server', async () => {
settings.set(['experimental', 'frontend'], {development_server: 'localhost:3001'});
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"});
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);
expect(mockSocket.destroy).toHaveBeenCalledTimes(0);
expect(mockHTTPProxy.implementation.ws).toHaveBeenCalledWith({"url": "http://localhost:8080/sockjs-node"}, mockSocket, 3, {"target": "ws://localhost:3001"});
});
});