2020-09-04 09:42:24 -07:00
|
|
|
const http = require('http');
|
2020-09-24 09:06:43 -07:00
|
|
|
const serveStatic = require('serve-static');
|
|
|
|
const finalhandler = require('finalhandler');
|
2020-09-04 09:42:24 -07:00
|
|
|
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');
|
2020-09-24 09:06:43 -07:00
|
|
|
const stringify = require('json-stable-stringify-without-jsonify');
|
2020-09-04 09:42:24 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
2020-11-23 11:09:47 -07:00
|
|
|
this.host = settings.get().frontend.host || '0.0.0.0';
|
2020-09-27 13:14:29 -07:00
|
|
|
this.port = settings.get().frontend.port || 8080;
|
2020-11-25 08:28:02 -07:00
|
|
|
this.authToken = settings.get().frontend.auth_token || false;
|
2020-09-04 09:42:24 -07:00
|
|
|
this.retainedMessages = new Map();
|
2020-11-25 08:28:02 -07:00
|
|
|
/* 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);
|
2020-09-04 09:42:24 -07:00
|
|
|
this.wss = new WebSocket.Server({noServer: true});
|
|
|
|
this.wss.on('connection', this.onWebSocketConnection);
|
|
|
|
}
|
|
|
|
|
|
|
|
onZigbeeStarted() {
|
2020-11-23 11:09:47 -07:00
|
|
|
this.server.listen(this.port, this.host);
|
|
|
|
logger.info(`Started frontend on port ${this.host}:${this.port}`);
|
2020-09-04 09:42:24 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
async stop() {
|
|
|
|
for (const client of this.wss.clients) {
|
2021-02-06 09:50:44 -07:00
|
|
|
client.send(stringify({topic: 'bridge/state', payload: 'offline'}));
|
2020-09-04 09:42:24 -07:00
|
|
|
client.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
this.server.close(resolve);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
onRequest(request, response) {
|
2020-11-25 08:28:02 -07:00
|
|
|
this.fileServer(request, response, finalhandler(request, response));
|
|
|
|
}
|
|
|
|
authenticate(request, cb) {
|
|
|
|
const {query} = url.parse(request.url, true);
|
|
|
|
cb(!this.authToken || this.authToken === query.token);
|
2020-09-04 09:42:24 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
onUpgrade(request, socket, head) {
|
2020-11-25 08:28:02 -07:00
|
|
|
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
|
|
this.authenticate(request, (isAuthentificated) => {
|
|
|
|
if (isAuthentificated) {
|
|
|
|
this.wss.emit('connection', ws, request);
|
|
|
|
} else {
|
|
|
|
ws.close(4401, 'Unauthorized');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2020-09-04 09:42:24 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
onWebSocketConnection(ws) {
|
|
|
|
ws.on('message', (message) => {
|
2020-09-13 06:38:10 -07:00
|
|
|
if (message) {
|
|
|
|
const {topic, payload} = utils.parseJSON(message, message);
|
|
|
|
this.mqtt.onMessage(`${this.mqttBaseTopic}/${topic}`, stringify(payload));
|
|
|
|
}
|
2020-09-04 09:42:24 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
for (const [key, value] of this.retainedMessages) {
|
|
|
|
ws.send(stringify({topic: key, payload: value}));
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const device of this.zigbee.getClients()) {
|
2020-10-07 08:05:46 -07:00
|
|
|
let payload = {};
|
|
|
|
const resolvedEntity = this.zigbee.resolveEntity(device);
|
2020-09-04 09:42:24 -07:00
|
|
|
if (this.state.exists(device.ieeeAddr)) {
|
2020-10-07 08:05:46 -07:00
|
|
|
payload = {...payload, ...this.state.get(device.ieeeAddr)};
|
2020-09-04 09:42:24 -07:00
|
|
|
}
|
2020-10-07 08:05:46 -07:00
|
|
|
|
|
|
|
const lastSeen = settings.get().advanced.last_seen;
|
|
|
|
if (lastSeen !== 'disable') {
|
|
|
|
payload.last_seen = utils.formatDate(resolvedEntity.device.lastSeen, lastSeen);
|
|
|
|
}
|
|
|
|
|
2020-11-13 09:29:23 -07:00
|
|
|
if (resolvedEntity.device.linkquality !== undefined) {
|
|
|
|
payload.linkquality = resolvedEntity.device.linkquality;
|
|
|
|
}
|
|
|
|
|
2020-10-07 08:05:46 -07:00
|
|
|
ws.send(stringify({topic: resolvedEntity.name, payload}));
|
2020-09-04 09:42:24 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onMQTTPublishedMessage(data) {
|
|
|
|
let {topic, payload, options} = data;
|
2020-09-10 05:54:03 -07:00
|
|
|
if (topic.startsWith(`${this.mqttBaseTopic}/`)) {
|
|
|
|
// 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);
|
|
|
|
}
|
2020-09-04 09:42:24 -07:00
|
|
|
|
2020-09-10 05:54:03 -07:00
|
|
|
for (const client of this.wss.clients) {
|
|
|
|
if (client.readyState === WebSocket.OPEN) {
|
|
|
|
client.send(stringify({topic, payload}));
|
|
|
|
}
|
2020-09-04 09:42:24 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Frontend;
|