homebridge-smartsystem 7.1.23 → 7.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/duotecno/types.js +25 -9
- package/package.json +1 -1
- package/server/dserver.js +175 -0
- package/server/proxy.js +15 -24
- package/server/smartapp.js +98 -1
- package/server/views/proxy.ejs +64 -0
package/duotecno/types.js
CHANGED
|
@@ -14,8 +14,22 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.makeInt = exports.now = exports.datetime = exports.time = exports.date = exports.watt = exports.hex = exports.two = exports.cap = exports.char = exports.ascii = exports.wait = exports.Sanitizers = exports.actionValueStr = exports.actionValue = exports.kEmptyBinding = exports.kEmptyDevice = exports.PowerSource = exports.kEmptyLink = exports.LinkType = exports.kEmptySwitch = exports.SwitchType = exports.kEmptyRule = exports.kEmptyAction = exports.RuleType = exports.Boundaries = exports.kEmptyCommRecord = exports.WriteError = exports.kEmptySettings = exports.kEmptyNetworkConfig = exports.kEmptyGroup = exports.kEmptyScene = exports.kEmptyUnitScene = exports.kEmptyUnit = exports.UnitExtendedType = exports.UnitType = exports.UnitMotorCmd = exports.UnitState = exports.NodeType = void 0;
|
|
18
|
-
|
|
17
|
+
exports.makeInt = exports.now = exports.datetime = exports.time = exports.date = exports.watt = exports.hex = exports.two = exports.cap = exports.char = exports.ascii = exports.wait = exports.Sanitizers = exports.actionValueStr = exports.actionValue = exports.kEmptyBinding = exports.kEmptyDevice = exports.PowerSource = exports.kEmptyLink = exports.LinkType = exports.kEmptySwitch = exports.SwitchType = exports.kEmptyRule = exports.kEmptyAction = exports.RuleType = exports.Boundaries = exports.kEmptyCommRecord = exports.WriteError = exports.kEmptySettings = exports.kEmptyNetworkConfig = exports.kEmptyGroup = exports.kEmptyScene = exports.kEmptyUnitScene = exports.kEmptyUnit = exports.UnitExtendedType = exports.UnitType = exports.UnitMotorCmd = exports.UnitState = exports.NodeType = exports.kEmptyProxy = void 0;
|
|
18
|
+
exports.kEmptyProxy = {
|
|
19
|
+
cloudServer: "masters.duotecno.eu",
|
|
20
|
+
cloudPort: 5097,
|
|
21
|
+
masterAddress: "",
|
|
22
|
+
masterPort: 5001,
|
|
23
|
+
masterConfigPort: 8080,
|
|
24
|
+
installationId: "",
|
|
25
|
+
nodeNumber: 0xff,
|
|
26
|
+
httpEnabled: false,
|
|
27
|
+
gatewayPhysicalId: "",
|
|
28
|
+
debug: false,
|
|
29
|
+
kind: "gw",
|
|
30
|
+
dserverEmail: "",
|
|
31
|
+
dserverPassword: "",
|
|
32
|
+
};
|
|
19
33
|
// Node types
|
|
20
34
|
var NodeType;
|
|
21
35
|
(function (NodeType) {
|
|
@@ -419,17 +433,19 @@ exports.Sanitizers = {
|
|
|
419
433
|
///////////
|
|
420
434
|
proxy: function (config, system) {
|
|
421
435
|
var _a, _b;
|
|
422
|
-
const c = Object.assign({},
|
|
423
|
-
c.cloudServer = (config === null || config === void 0 ? void 0 : config.cloudServer) ||
|
|
424
|
-
c.cloudPort = (config === null || config === void 0 ? void 0 : config.cloudPort) ||
|
|
436
|
+
const c = Object.assign({}, exports.kEmptyProxy);
|
|
437
|
+
c.cloudServer = (config === null || config === void 0 ? void 0 : config.cloudServer) || exports.kEmptyProxy.cloudServer;
|
|
438
|
+
c.cloudPort = (config === null || config === void 0 ? void 0 : config.cloudPort) || exports.kEmptyProxy.cloudPort;
|
|
425
439
|
c.masterAddress = (config === null || config === void 0 ? void 0 : config.masterAddress) || ((_a = system === null || system === void 0 ? void 0 : system.cmasters[0]) === null || _a === void 0 ? void 0 : _a.address) || "";
|
|
426
440
|
c.masterPort = (config === null || config === void 0 ? void 0 : config.masterPort) || ((_b = system === null || system === void 0 ? void 0 : system.cmasters[0]) === null || _b === void 0 ? void 0 : _b.port) || 5001;
|
|
427
|
-
c.masterConfigPort = (config === null || config === void 0 ? void 0 : config.masterConfigPort) ||
|
|
428
|
-
c.installationId = (config === null || config === void 0 ? void 0 : config.installationId) ||
|
|
429
|
-
c.nodeNumber = (typeof (config === null || config === void 0 ? void 0 : config.nodeNumber) === "number") ? config.nodeNumber :
|
|
441
|
+
c.masterConfigPort = (config === null || config === void 0 ? void 0 : config.masterConfigPort) || exports.kEmptyProxy.masterConfigPort;
|
|
442
|
+
c.installationId = (config === null || config === void 0 ? void 0 : config.installationId) || exports.kEmptyProxy.installationId;
|
|
443
|
+
c.nodeNumber = (typeof (config === null || config === void 0 ? void 0 : config.nodeNumber) === "number") ? config.nodeNumber : exports.kEmptyProxy.nodeNumber;
|
|
430
444
|
c.httpEnabled = !!(config === null || config === void 0 ? void 0 : config.httpEnabled);
|
|
431
|
-
c.gatewayPhysicalId = (config === null || config === void 0 ? void 0 : config.gatewayPhysicalId) ||
|
|
445
|
+
c.gatewayPhysicalId = (config === null || config === void 0 ? void 0 : config.gatewayPhysicalId) || exports.kEmptyProxy.gatewayPhysicalId;
|
|
432
446
|
c.kind = "gw";
|
|
447
|
+
c.dserverEmail = (config === null || config === void 0 ? void 0 : config.dserverEmail) || exports.kEmptyProxy.dserverEmail;
|
|
448
|
+
c.dserverPassword = (config === null || config === void 0 ? void 0 : config.dserverPassword) || exports.kEmptyProxy.dserverPassword;
|
|
433
449
|
return c;
|
|
434
450
|
},
|
|
435
451
|
//////////////
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-smartsystem",
|
|
3
3
|
"displayname": "Duotecno Bridge",
|
|
4
|
-
"version": "7.1.
|
|
4
|
+
"version": "7.1.24",
|
|
5
5
|
"description": "SmartServer (Proxy TCP sockets to the cloud, Smappee MQTT, Duotecno IP Nodes, Homekit interface)",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"author": "Johan Coppieters",
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
//
|
|
3
|
+
// Minimal client for dserver's Installation Backup API (see
|
|
4
|
+
// installation-backup-api.md in the dserver repo), used by the Proxy Config
|
|
5
|
+
// page's Backup/Restore buttons to push/pull this gateway's own config.json.
|
|
6
|
+
//
|
|
7
|
+
// dserver has no machine/device auth: a gateway authenticates exactly like
|
|
8
|
+
// a human user would (email+password -> rotating ticket), mirroring the
|
|
9
|
+
// Duotecno Designer app's DserverService.ts. The numeric installation id
|
|
10
|
+
// dserver needs is resolved from the same free-form string already used as
|
|
11
|
+
// ProxyConfig.installationId, via GET /identifier/{name} (HTTP Basic Auth
|
|
12
|
+
// only -- IdentifierController.isAllowed() never accepts a ticket there).
|
|
13
|
+
//
|
|
14
|
+
// Uses Node's built-in https module (not fetch) since this project's
|
|
15
|
+
// @types/node predates fetch/FormData/Blob typings.
|
|
16
|
+
//
|
|
17
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
18
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
19
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
20
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
21
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
22
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
23
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.downloadBackupFile = exports.createBackup = exports.getBackupDetail = exports.listBackups = exports.resolveInstallation = exports.clearDserverTicketCache = exports.kGatewayTag = void 0;
|
|
28
|
+
const https = require("https");
|
|
29
|
+
const kDserverHost = "duotecno.eu";
|
|
30
|
+
const kLang = "en";
|
|
31
|
+
exports.kGatewayTag = "gateway";
|
|
32
|
+
// rotating session ticket, cached in memory for the life of this process
|
|
33
|
+
let cachedTicket = null;
|
|
34
|
+
function clearDserverTicketCache() {
|
|
35
|
+
cachedTicket = null;
|
|
36
|
+
}
|
|
37
|
+
exports.clearDserverTicketCache = clearDserverTicketCache;
|
|
38
|
+
function basicAuthHeader(creds) {
|
|
39
|
+
return "Basic " + Buffer.from(`${creds.email}:${creds.password}`).toString("base64");
|
|
40
|
+
}
|
|
41
|
+
function rawRequest(options, body) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const req = https.request(Object.assign({ host: kDserverHost }, options), res => {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
res.on('data', (c) => chunks.push(c));
|
|
46
|
+
res.on('end', () => resolve({ status: res.statusCode || 0, body: Buffer.concat(chunks) }));
|
|
47
|
+
});
|
|
48
|
+
req.on('error', reject);
|
|
49
|
+
req.setTimeout(15000, () => req.destroy(new Error("dserver request timed out")));
|
|
50
|
+
if (body)
|
|
51
|
+
req.write(body);
|
|
52
|
+
req.end();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function parseJson(body) {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(body.toString('utf-8'));
|
|
58
|
+
}
|
|
59
|
+
catch (_a) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function login(creds) {
|
|
64
|
+
var _a;
|
|
65
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
const body = Buffer.from(JSON.stringify({ request: "login", email: creds.email, password: creds.password }));
|
|
67
|
+
const { status, body: respBody } = yield rawRequest({
|
|
68
|
+
path: `/${kLang}/api`,
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/json", "Content-Length": body.length }
|
|
71
|
+
}, body);
|
|
72
|
+
const json = parseJson(respBody);
|
|
73
|
+
if (status !== 200 || !json || String(json.status).toLowerCase() !== "ok" || !((_a = json.user) === null || _a === void 0 ? void 0 : _a.ticket)) {
|
|
74
|
+
throw new Error((json === null || json === void 0 ? void 0 : json.message) || `dserver login failed (HTTP ${status})`);
|
|
75
|
+
}
|
|
76
|
+
cachedTicket = json.user.ticket;
|
|
77
|
+
return cachedTicket;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function ensureTicket(creds) {
|
|
81
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
82
|
+
return cachedTicket || login(creds);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// Ticket-authenticated JSON call against /{lang}{path}, retrying once (with a forced fresh
|
|
86
|
+
// login) if the cached ticket turned out to be stale.
|
|
87
|
+
function call(creds, method, path, query = {}, body, extraHeaders = {}, retry = true) {
|
|
88
|
+
var _a;
|
|
89
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
90
|
+
const ticket = yield ensureTicket(creds);
|
|
91
|
+
const params = new URLSearchParams(Object.assign(Object.assign({}, query), { ticket }));
|
|
92
|
+
const headers = Object.assign({}, extraHeaders);
|
|
93
|
+
if (!body)
|
|
94
|
+
headers["Content-Type"] = "application/json"; // forces dserver's JSON (vs HTML) response branch
|
|
95
|
+
if (body)
|
|
96
|
+
headers["Content-Length"] = String(body.length);
|
|
97
|
+
const { status, body: respBody } = yield rawRequest({
|
|
98
|
+
path: `/${kLang}${path}?${params.toString()}`,
|
|
99
|
+
method,
|
|
100
|
+
headers
|
|
101
|
+
}, body);
|
|
102
|
+
const json = parseJson(respBody);
|
|
103
|
+
if (json === null || json === void 0 ? void 0 : json.ticket)
|
|
104
|
+
cachedTicket = json.ticket; // dserver rotates the ticket on every response
|
|
105
|
+
if (status === 403 && retry) {
|
|
106
|
+
cachedTicket = null;
|
|
107
|
+
return call(creds, method, path, query, body, extraHeaders, false);
|
|
108
|
+
}
|
|
109
|
+
if (!json || status < 200 || status >= 300 || ["nok", "err"].includes(String(json.status).toLowerCase())) {
|
|
110
|
+
throw new Error((json === null || json === void 0 ? void 0 : json.message) || `dserver call failed (HTTP ${status})`);
|
|
111
|
+
}
|
|
112
|
+
return (_a = json.data) !== null && _a !== void 0 ? _a : json;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// GET /{lang}/identifier/{name} -- the one endpoint that only accepts HTTP Basic Auth,
|
|
116
|
+
// never a ticket (see IdentifierController.isAllowed() in dserver).
|
|
117
|
+
function resolveInstallation(creds, name) {
|
|
118
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
119
|
+
const { status, body } = yield rawRequest({
|
|
120
|
+
path: `/${kLang}/identifier/${encodeURIComponent(name)}?nocookie=1`,
|
|
121
|
+
method: "GET",
|
|
122
|
+
headers: { "Authorization": basicAuthHeader(creds), "Content-Type": "application/json" }
|
|
123
|
+
});
|
|
124
|
+
const json = parseJson(body);
|
|
125
|
+
if (status !== 200 || !json || String(json.status).toLowerCase() !== "ok") {
|
|
126
|
+
throw new Error((json === null || json === void 0 ? void 0 : json.message) || `dserver installation lookup failed (HTTP ${status})`);
|
|
127
|
+
}
|
|
128
|
+
return { exists: !!json.exists, id: json.id, name };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
exports.resolveInstallation = resolveInstallation;
|
|
132
|
+
function listBackups(creds, installationId, tag = exports.kGatewayTag) {
|
|
133
|
+
return call(creds, "GET", `/installation/${installationId}/backup`, { tag });
|
|
134
|
+
}
|
|
135
|
+
exports.listBackups = listBackups;
|
|
136
|
+
function getBackupDetail(creds, installationId, backupId) {
|
|
137
|
+
return call(creds, "GET", `/installation/${installationId}/backup/${backupId}`);
|
|
138
|
+
}
|
|
139
|
+
exports.getBackupDetail = getBackupDetail;
|
|
140
|
+
function createBackup(creds, installationId, description, filename, fileContent, tag = exports.kGatewayTag) {
|
|
141
|
+
const boundary = "----smartsocketBackup" + Date.now().toString(16);
|
|
142
|
+
const crlf = "\r\n";
|
|
143
|
+
const parts = [
|
|
144
|
+
Buffer.from(`--${boundary}${crlf}Content-Disposition: form-data; name="tag"${crlf}${crlf}${tag}${crlf}`),
|
|
145
|
+
];
|
|
146
|
+
if (description) {
|
|
147
|
+
parts.push(Buffer.from(`--${boundary}${crlf}Content-Disposition: form-data; name="description"${crlf}${crlf}${description}${crlf}`));
|
|
148
|
+
}
|
|
149
|
+
parts.push(Buffer.from(`--${boundary}${crlf}Content-Disposition: form-data; name="file"; filename="${filename}"${crlf}Content-Type: application/json${crlf}${crlf}`));
|
|
150
|
+
parts.push(fileContent);
|
|
151
|
+
parts.push(Buffer.from(`${crlf}--${boundary}--${crlf}`));
|
|
152
|
+
const body = Buffer.concat(parts);
|
|
153
|
+
return call(creds, "POST", `/installation/${installationId}/backup`, {}, body, { "Content-Type": `multipart/form-data; boundary=${boundary}` });
|
|
154
|
+
}
|
|
155
|
+
exports.createBackup = createBackup;
|
|
156
|
+
function downloadBackupFile(creds, installationId, backupId, filename) {
|
|
157
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
158
|
+
const ticket = yield ensureTicket(creds);
|
|
159
|
+
// filename may contain "/" (a preserved subfolder path) -- encode each segment separately,
|
|
160
|
+
// matching dserver's own getFilename() which splits the URL path into segments itself.
|
|
161
|
+
const encodedPath = filename.split("/").map(encodeURIComponent).join("/");
|
|
162
|
+
const params = new URLSearchParams({ ticket });
|
|
163
|
+
const { status, body } = yield rawRequest({
|
|
164
|
+
path: `/${kLang}/installation/${installationId}/backup/${backupId}/files/${encodedPath}?${params.toString()}`,
|
|
165
|
+
method: "GET",
|
|
166
|
+
headers: { "Content-Type": "application/json" }
|
|
167
|
+
});
|
|
168
|
+
if (status !== 200) {
|
|
169
|
+
throw new Error(`dserver file download failed: ${filename} (HTTP ${status})`);
|
|
170
|
+
}
|
|
171
|
+
return body;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
exports.downloadBackupFile = downloadBackupFile;
|
|
175
|
+
//# sourceMappingURL=dserver.js.map
|
package/server/proxy.js
CHANGED
|
@@ -9,12 +9,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.Context = exports.cleanStart = exports.makeNewCloudConnection = exports.gCloudConnections = exports.setProxyConfig = exports.setWebApp = exports.
|
|
12
|
+
exports.Context = exports.cleanStart = exports.makeNewCloudConnection = exports.gCloudConnections = exports.setProxyConfig = exports.setWebApp = exports.gatewayHttpPort = exports.makeDeviceId = exports.kMasterNodeNumber = exports.kEmptyProxy = void 0;
|
|
13
13
|
const net = require("net");
|
|
14
14
|
const os = require("os");
|
|
15
15
|
const child_process_1 = require("child_process");
|
|
16
16
|
const process_1 = require("process");
|
|
17
17
|
const config_1 = require("../duotecno/config");
|
|
18
|
+
const types_1 = require("../duotecno/types");
|
|
19
|
+
Object.defineProperty(exports, "kEmptyProxy", { enumerable: true, get: function () { return types_1.kEmptyProxy; } });
|
|
18
20
|
// Fixed, non-configurable logical node number every Duotecno master identifies itself with
|
|
19
21
|
// on the bus -- used for both of the master's own registrations (raw protocol + Config API).
|
|
20
22
|
exports.kMasterNodeNumber = 0xfc;
|
|
@@ -74,20 +76,7 @@ function gatewayPhysicalAddress() {
|
|
|
74
76
|
}
|
|
75
77
|
return config.gatewayPhysicalId;
|
|
76
78
|
}
|
|
77
|
-
|
|
78
|
-
cloudServer: "masters.duotecno.eu",
|
|
79
|
-
cloudPort: 5097,
|
|
80
|
-
masterAddress: "",
|
|
81
|
-
masterPort: 5001,
|
|
82
|
-
masterConfigPort: 8080,
|
|
83
|
-
installationId: "",
|
|
84
|
-
nodeNumber: 0xff,
|
|
85
|
-
httpEnabled: false,
|
|
86
|
-
gatewayPhysicalId: "",
|
|
87
|
-
debug: false,
|
|
88
|
-
kind: "gw" // default running under the gateway or other process manager -> don't restart yourself
|
|
89
|
-
};
|
|
90
|
-
const config = Object.assign({}, exports.kEmptyProxy);
|
|
79
|
+
const config = Object.assign({}, types_1.kEmptyProxy);
|
|
91
80
|
// Store reference to the webapp so we can close servers before restarting
|
|
92
81
|
let gWebApp = null;
|
|
93
82
|
function setWebApp(webapp) {
|
|
@@ -97,17 +86,19 @@ exports.setWebApp = setWebApp;
|
|
|
97
86
|
function setProxyConfig(c) {
|
|
98
87
|
// set the proxy config
|
|
99
88
|
if (c) {
|
|
100
|
-
config.cloudServer = c.cloudServer ||
|
|
101
|
-
config.cloudPort = c.cloudPort ||
|
|
102
|
-
config.masterAddress = c.masterAddress ||
|
|
103
|
-
config.masterPort = c.masterPort ||
|
|
104
|
-
config.masterConfigPort = c.masterConfigPort ||
|
|
105
|
-
config.installationId = c.installationId ||
|
|
106
|
-
config.nodeNumber = (typeof c.nodeNumber === "number") ? c.nodeNumber :
|
|
89
|
+
config.cloudServer = c.cloudServer || types_1.kEmptyProxy.cloudServer;
|
|
90
|
+
config.cloudPort = c.cloudPort || types_1.kEmptyProxy.cloudPort;
|
|
91
|
+
config.masterAddress = c.masterAddress || types_1.kEmptyProxy.masterAddress;
|
|
92
|
+
config.masterPort = c.masterPort || types_1.kEmptyProxy.masterPort;
|
|
93
|
+
config.masterConfigPort = c.masterConfigPort || types_1.kEmptyProxy.masterConfigPort;
|
|
94
|
+
config.installationId = c.installationId || types_1.kEmptyProxy.installationId;
|
|
95
|
+
config.nodeNumber = (typeof c.nodeNumber === "number") ? c.nodeNumber : types_1.kEmptyProxy.nodeNumber;
|
|
107
96
|
config.httpEnabled = !!c.httpEnabled;
|
|
108
|
-
config.gatewayPhysicalId = c.gatewayPhysicalId ||
|
|
97
|
+
config.gatewayPhysicalId = c.gatewayPhysicalId || types_1.kEmptyProxy.gatewayPhysicalId;
|
|
109
98
|
config.debug = !!c.debug;
|
|
110
|
-
config.kind = c.kind ||
|
|
99
|
+
config.kind = c.kind || types_1.kEmptyProxy.kind;
|
|
100
|
+
config.dserverEmail = c.dserverEmail || types_1.kEmptyProxy.dserverEmail;
|
|
101
|
+
config.dserverPassword = c.dserverPassword || types_1.kEmptyProxy.dserverPassword;
|
|
111
102
|
}
|
|
112
103
|
return config;
|
|
113
104
|
}
|
package/server/smartapp.js
CHANGED
|
@@ -29,6 +29,8 @@ const config_1 = require("../duotecno/config");
|
|
|
29
29
|
const HB_1 = require("./HB");
|
|
30
30
|
const HA_API_1 = require("./HA-API");
|
|
31
31
|
const proxy_1 = require("./proxy");
|
|
32
|
+
const config_2 = require("../duotecno/config");
|
|
33
|
+
const dserver_1 = require("./dserver");
|
|
32
34
|
const WebSocket = require("ws");
|
|
33
35
|
const kMaster = { name: "master", type: "string", default: "0.0.0.0:5001" };
|
|
34
36
|
const kAddress = { name: "address", type: "string", default: "0.0.0.0" };
|
|
@@ -377,6 +379,11 @@ function SmartSocketInit(message){
|
|
|
377
379
|
doProxy(context) {
|
|
378
380
|
return __awaiter(this, void 0, void 0, function* () {
|
|
379
381
|
let config = this.read("proxy") || proxy_1.kEmptyProxy;
|
|
382
|
+
// "grey" (nothing configured yet), "blue" (installation id set, not checked against the
|
|
383
|
+
// cloud yet) or "warning" (a backup/restore attempt this request found a problem) -- see
|
|
384
|
+
// doBackupToCloud()/doRestoreFromCloud() below.
|
|
385
|
+
let backupState = config.installationId ? "blue" : "grey";
|
|
386
|
+
let backupMessage = "";
|
|
380
387
|
if (context.action === "save") {
|
|
381
388
|
// save the proxy config
|
|
382
389
|
config.cloudServer = context.getParam({ name: "cloudServer", type: "string" });
|
|
@@ -388,14 +395,33 @@ function SmartSocketInit(message){
|
|
|
388
395
|
config.nodeNumber = context.getParam({ name: "nodeNumber", type: "string" })
|
|
389
396
|
? parseInt(context.getParam({ name: "nodeNumber", type: "string" }), 16) : proxy_1.kEmptyProxy.nodeNumber;
|
|
390
397
|
config.httpEnabled = !!context.getParam({ name: "httpEnabled", type: "string" });
|
|
398
|
+
config.dserverEmail = context.getParam({ name: "dserverEmail", type: "string" });
|
|
399
|
+
config.dserverPassword = context.getParam({ name: "dserverPassword", type: "string" });
|
|
391
400
|
this.write("proxy", config);
|
|
392
401
|
(0, proxy_1.setProxyConfig)(config);
|
|
393
402
|
// Automatically restart proxy with new config
|
|
394
403
|
(0, proxy_1.cleanStart)(true);
|
|
404
|
+
backupState = config.installationId ? "blue" : "grey";
|
|
395
405
|
}
|
|
396
406
|
else if (context.action === "restart") {
|
|
397
407
|
(0, proxy_1.cleanStart)(true);
|
|
398
408
|
}
|
|
409
|
+
else if (context.action === "backup") {
|
|
410
|
+
const result = yield this.doBackupToCloud(config);
|
|
411
|
+
backupState = result.state;
|
|
412
|
+
backupMessage = result.message;
|
|
413
|
+
}
|
|
414
|
+
else if (context.action === "restore") {
|
|
415
|
+
const backupId = parseInt(context.getParam({ name: "backupId", type: "string" }) || "", 10);
|
|
416
|
+
if (backupId) {
|
|
417
|
+
const result = yield this.doRestoreFromCloud(config, backupId);
|
|
418
|
+
if (result.ok) {
|
|
419
|
+
return this.doRestart(false); // process exits, gateway comes back up with the restored config
|
|
420
|
+
}
|
|
421
|
+
backupState = "warning";
|
|
422
|
+
backupMessage = result.message;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
399
425
|
else {
|
|
400
426
|
// just show the proxy config
|
|
401
427
|
}
|
|
@@ -412,7 +438,78 @@ function SmartSocketInit(message){
|
|
|
412
438
|
configHttp: (0, proxy_1.makeDeviceId)(config, proxy_1.kMasterNodeNumber, "http", config.masterConfigPort || proxy_1.kEmptyProxy.masterConfigPort || 8080),
|
|
413
439
|
http: (0, proxy_1.makeDeviceId)(config, config.nodeNumber, "http", (0, proxy_1.gatewayHttpPort)()),
|
|
414
440
|
};
|
|
415
|
-
|
|
441
|
+
// Best-effort: list existing "gateway"-tagged backups for display, so the page can show
|
|
442
|
+
// "N backup versions, last: ..." and offer a version to restore. Any failure here (no
|
|
443
|
+
// credentials yet, installation not registered, cloud unreachable) just shows an empty list
|
|
444
|
+
// rather than failing the whole page -- the Backup/Restore buttons already surface the same
|
|
445
|
+
// error more directly when actually clicked.
|
|
446
|
+
let backups = [];
|
|
447
|
+
let backupListError = "";
|
|
448
|
+
if (config.installationId && config.dserverEmail && config.dserverPassword) {
|
|
449
|
+
try {
|
|
450
|
+
const creds = { email: config.dserverEmail, password: config.dserverPassword };
|
|
451
|
+
const inst = yield (0, dserver_1.resolveInstallation)(creds, config.installationId);
|
|
452
|
+
if (inst.exists && inst.id) {
|
|
453
|
+
backups = yield (0, dserver_1.listBackups)(creds, inst.id);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch (e) {
|
|
457
|
+
backupListError = e.message || String(e);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return this.ejs("proxy", context, { config, connections: proxy_1.gCloudConnections, mdnsStatus, deviceIds,
|
|
461
|
+
backupState, backupMessage, backups, backupListError });
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
// Pushes this gateway's own persisted config.json to dserver as a new "gateway"-tagged backup.
|
|
465
|
+
doBackupToCloud(config) {
|
|
466
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
467
|
+
if (!config.installationId) {
|
|
468
|
+
return { state: "grey", message: "" };
|
|
469
|
+
}
|
|
470
|
+
if (!config.dserverEmail || !config.dserverPassword) {
|
|
471
|
+
return { state: "warning", message: "No Duotecno account configured -- fill in the account email/password below and save first." };
|
|
472
|
+
}
|
|
473
|
+
const creds = { email: config.dserverEmail, password: config.dserverPassword };
|
|
474
|
+
try {
|
|
475
|
+
const inst = yield (0, dserver_1.resolveInstallation)(creds, config.installationId);
|
|
476
|
+
if (!inst.exists || !inst.id) {
|
|
477
|
+
return { state: "warning", message: `Installation "${config.installationId}" is not registered on the cloud server.` };
|
|
478
|
+
}
|
|
479
|
+
const fileContent = (0, fs_1.readFileSync)(config_2.kConfigFiles + "/config.json");
|
|
480
|
+
yield (0, dserver_1.createBackup)(creds, inst.id, `Gateway backup ${new Date().toISOString()}`, "config.json", fileContent);
|
|
481
|
+
return { state: "blue", message: "Backup created." };
|
|
482
|
+
}
|
|
483
|
+
catch (e) {
|
|
484
|
+
return { state: "warning", message: e.message || String(e) };
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
// Downloads the chosen "gateway"-tagged backup's config.json from dserver and overwrites this
|
|
489
|
+
// gateway's own persisted config.json with it -- caller is responsible for restarting after.
|
|
490
|
+
doRestoreFromCloud(config, backupId) {
|
|
491
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
492
|
+
if (!config.installationId || !config.dserverEmail || !config.dserverPassword) {
|
|
493
|
+
return { ok: false, message: "Missing installation ID or Duotecno account credentials." };
|
|
494
|
+
}
|
|
495
|
+
const creds = { email: config.dserverEmail, password: config.dserverPassword };
|
|
496
|
+
try {
|
|
497
|
+
const inst = yield (0, dserver_1.resolveInstallation)(creds, config.installationId);
|
|
498
|
+
if (!inst.exists || !inst.id) {
|
|
499
|
+
return { ok: false, message: `Installation "${config.installationId}" is not registered on the cloud server.` };
|
|
500
|
+
}
|
|
501
|
+
const detail = yield (0, dserver_1.getBackupDetail)(creds, inst.id, backupId);
|
|
502
|
+
const file = (detail.files || []).find(f => f.filename === "config.json" || f.path === "config.json");
|
|
503
|
+
if (!file) {
|
|
504
|
+
return { ok: false, message: "Selected backup does not contain a config.json file." };
|
|
505
|
+
}
|
|
506
|
+
const content = yield (0, dserver_1.downloadBackupFile)(creds, inst.id, backupId, file.path);
|
|
507
|
+
(0, fs_1.writeFileSync)(config_2.kConfigFiles + "/config.json", content);
|
|
508
|
+
return { ok: true, message: "Config restored." };
|
|
509
|
+
}
|
|
510
|
+
catch (e) {
|
|
511
|
+
return { ok: false, message: e.message || String(e) };
|
|
512
|
+
}
|
|
416
513
|
});
|
|
417
514
|
}
|
|
418
515
|
//////////////
|
package/server/views/proxy.ejs
CHANGED
|
@@ -14,8 +14,56 @@
|
|
|
14
14
|
<button class="right btn waves-effect waves-light" type="submit" name="action" value="cancel">Back</button>
|
|
15
15
|
<button class="right btn waves-effect waves-light" type="submit" name="action" value="save">Save</button>
|
|
16
16
|
<button class="right btn waves-effect waves-light red" type="submit" name="action" value="restart">Restart</button>
|
|
17
|
+
<button class="right btn waves-effect waves-light
|
|
18
|
+
<%= backupState === 'warning' ? 'orange darken-1' : (backupState === 'blue' ? 'blue' : 'grey') %>"
|
|
19
|
+
type="submit" name="action" value="backup" <%= backupState === 'grey' ? 'disabled' : '' %>>Backup</button>
|
|
17
20
|
</h1>
|
|
18
21
|
|
|
22
|
+
<div class="row" style="clear: both; margin: 0 0 1rem 0;">
|
|
23
|
+
<div class="col s12">
|
|
24
|
+
<% // dd/mm/yyyy hh:mm:ss, per the backup versions list below
|
|
25
|
+
function fmtBackupDate(iso) {
|
|
26
|
+
const d = new Date(iso);
|
|
27
|
+
const two = (n) => (n < 10 ? "0" : "") + n;
|
|
28
|
+
return two(d.getDate()) + "/" + two(d.getMonth() + 1) + "/" + d.getFullYear() + " "
|
|
29
|
+
+ two(d.getHours()) + ":" + two(d.getMinutes()) + ":" + two(d.getSeconds());
|
|
30
|
+
}
|
|
31
|
+
%>
|
|
32
|
+
<% if (backupMessage) { %>
|
|
33
|
+
<p class="<%= backupState === 'warning' ? 'orange-text text-darken-3' : 'green-text text-darken-3' %>" style="margin: 0 0 0.5rem 0;">
|
|
34
|
+
<%= backupMessage %>
|
|
35
|
+
</p>
|
|
36
|
+
<% } %>
|
|
37
|
+
<% if (backups && backups.length > 0) { %>
|
|
38
|
+
<p style="margin: 0 0 0.5rem 0;">
|
|
39
|
+
<%= backups.length %> backup version<%= backups.length === 1 ? "" : "s" %>, last:
|
|
40
|
+
<%= fmtBackupDate(backups[0].created) %>
|
|
41
|
+
</p>
|
|
42
|
+
<% backups.forEach(function(b, i) { %>
|
|
43
|
+
<p style="margin: 0;">
|
|
44
|
+
<label>
|
|
45
|
+
<input name="backupId" type="radio" class="with-gap" value="<%= b.id %>" <%= i === 0 ? "checked" : "" %> />
|
|
46
|
+
<span>
|
|
47
|
+
<%= fmtBackupDate(b.created) %>
|
|
48
|
+
<% if (b.description) { %>— <%= b.description %><% } %>
|
|
49
|
+
</span>
|
|
50
|
+
</label>
|
|
51
|
+
</p>
|
|
52
|
+
<% }); %>
|
|
53
|
+
<button class="btn waves-effect waves-light blue" type="submit" name="action" value="restore"
|
|
54
|
+
style="margin-top: 0.5rem;"
|
|
55
|
+
onclick="return confirm('This will overwrite the current gateway configuration with the selected backup and restart the gateway. Continue?');">
|
|
56
|
+
Restore
|
|
57
|
+
</button>
|
|
58
|
+
<% } else { %>
|
|
59
|
+
<p class="grey-text" style="margin: 0;">0 backup versions, last: -not yet-</p>
|
|
60
|
+
<% } %>
|
|
61
|
+
<% if (backupListError) { %>
|
|
62
|
+
<p class="red-text" style="margin: 0.5rem 0 0 0;"><small><%= backupListError %></small></p>
|
|
63
|
+
<% } %>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
19
67
|
<h2 style="margin-top: 2rem; font-size: 1.5rem; font-weight: 500;">
|
|
20
68
|
<i class="material-icons left" style="vertical-align: middle;">cloud</i>
|
|
21
69
|
Cloud Server Settings
|
|
@@ -60,6 +108,22 @@
|
|
|
60
108
|
</div>
|
|
61
109
|
</div>
|
|
62
110
|
|
|
111
|
+
<p class="grey-text" style="margin-bottom: 1rem;">
|
|
112
|
+
Duotecno account used by the <strong>Backup</strong>/<strong>Restore</strong> buttons below to push/pull
|
|
113
|
+
this gateway's config to/from the cloud server, tagged "gateway" -- the same account you'd use to log
|
|
114
|
+
into Duotecno Designer.
|
|
115
|
+
</p>
|
|
116
|
+
<div class="row" style="margin-top: 0;">
|
|
117
|
+
<div class="input-field col s6">
|
|
118
|
+
<label for="dserverEmail">Duotecno Account Email</label>
|
|
119
|
+
<input id="dserverEmail" name="dserverEmail" type="email" value="<%= config.dserverEmail || '' %>">
|
|
120
|
+
</div>
|
|
121
|
+
<div class="input-field col s6">
|
|
122
|
+
<label for="dserverPassword">Duotecno Account Password</label>
|
|
123
|
+
<input id="dserverPassword" name="dserverPassword" type="password" value="<%= config.dserverPassword || '' %>">
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
63
127
|
<h2 style="margin-top: 3rem; font-size: 1.5rem; font-weight: 500;">
|
|
64
128
|
<i class="material-icons left" style="vertical-align: middle;">home</i>
|
|
65
129
|
Master Device
|