homebridge-smartsystem 7.1.25 → 7.2.1
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/.vscode/launch.json +3 -0
- package/duotecno/types.js +1 -0
- package/duotecno/udpConfig.js +90 -0
- package/package.json +1 -1
- package/server/overkiz.js +94 -20
- package/server/smartapp.js +327 -45
- package/server/views/link-details.ejs +14 -6
- package/server/views/master-detail.ejs +10 -0
- package/server/views/nav.ejs +20 -10
- package/server/views/settings.ejs +69 -3
package/.vscode/launch.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"sourceMaps": true,
|
|
9
9
|
"name": "Test HB Platform",
|
|
10
10
|
"program": "${workspaceFolder}/testHB.js",
|
|
11
|
+
"runtimeExecutable": "/Users/johan/.nvm/versions/node/v20.19.5/bin/node",
|
|
11
12
|
"skipFiles": [
|
|
12
13
|
"<node_internals>/**"
|
|
13
14
|
]
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
"sourceMaps": true,
|
|
20
21
|
"name": "Run SmartSocket Proxy",
|
|
21
22
|
"program": "${workspaceFolder}/testProxy.js",
|
|
23
|
+
"runtimeExecutable": "/Users/johan/.nvm/versions/node/v20.19.5/bin/node",
|
|
22
24
|
"skipFiles": [
|
|
23
25
|
"<node_internals>/**"
|
|
24
26
|
]
|
|
@@ -30,6 +32,7 @@
|
|
|
30
32
|
"sourceMaps": true,
|
|
31
33
|
"name": "Run Test Websockets",
|
|
32
34
|
"program": "${workspaceFolder}/testWS.js",
|
|
35
|
+
"runtimeExecutable": "/Users/johan/.nvm/versions/node/v20.19.5/bin/node",
|
|
33
36
|
"skipFiles": [
|
|
34
37
|
"<node_internals>/**"
|
|
35
38
|
]
|
package/duotecno/types.js
CHANGED
|
@@ -167,6 +167,7 @@ var LinkType;
|
|
|
167
167
|
(function (LinkType) {
|
|
168
168
|
LinkType["kNoType"] = "";
|
|
169
169
|
LinkType["kAccessory"] = "accessory";
|
|
170
|
+
LinkType["kOverkiz"] = "overkiz";
|
|
170
171
|
})(LinkType = exports.LinkType || (exports.LinkType = {}));
|
|
171
172
|
exports.kEmptyLink = Object.assign(Object.assign({}, exports.kEmptyUnit), { type: LinkType.kNoType, max: 100, min: 0, nostop: "N", accName: "acc", accId: "", accService: [] });
|
|
172
173
|
/////////////
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.setHomeServerCfg = exports.UDP_CONFIG_PORT = void 0;
|
|
13
|
+
// Minimal client for Duotecno's "UDP config tool" protocol -- just enough to
|
|
14
|
+
// push a new home-server (master) password/port to a master over UDP.
|
|
15
|
+
//
|
|
16
|
+
// Wire format reverse-engineered from the real `dtudpconfigLIB` source (see
|
|
17
|
+
// the Duotecno Designer project's src/services/udpConfig/protocol.ts for the
|
|
18
|
+
// full writeup and provenance notes). Devices listen on UDP port 8002.
|
|
19
|
+
// ASCII framing: `[b0,b1,...]` + LF, every byte its decimal string,
|
|
20
|
+
// comma-separated. Strings are length-prefixed (one length byte, then that
|
|
21
|
+
// many byte-per-char, no null terminator). Multi-byte ints are MSB-first.
|
|
22
|
+
const dgram = require("dgram");
|
|
23
|
+
exports.UDP_CONFIG_PORT = 8002;
|
|
24
|
+
const CMD_FC_APPCONFIG = 189;
|
|
25
|
+
const CMD_EV_APPCONFIG = 15;
|
|
26
|
+
const ATTR_HOMESRVCFG_SET = 2;
|
|
27
|
+
// Always written as "closed + open protocol enabled", matching the legacy
|
|
28
|
+
// config tool -- it's a read-only display value, not a user-editable input.
|
|
29
|
+
const HOMESRVCFG_PROTOCOL_FLAGS = 0x03;
|
|
30
|
+
function encodePacket(bytes) {
|
|
31
|
+
return Buffer.from(`[${bytes.join(",")}]\n`, "ascii");
|
|
32
|
+
}
|
|
33
|
+
function decodePacket(buf) {
|
|
34
|
+
const str = buf.toString("ascii").trim();
|
|
35
|
+
const match = /^\[([^\]]*)\]/.exec(str);
|
|
36
|
+
if (!match || !match[1])
|
|
37
|
+
return [];
|
|
38
|
+
return match[1].split(",").map((s) => parseInt(s.trim(), 10));
|
|
39
|
+
}
|
|
40
|
+
function stringToBytes(s) {
|
|
41
|
+
return [s.length, ...Array.from(s).map((c) => c.charCodeAt(0))];
|
|
42
|
+
}
|
|
43
|
+
function u16ToBytes(v) {
|
|
44
|
+
return [(v >> 8) & 0xff, v & 0xff];
|
|
45
|
+
}
|
|
46
|
+
/** Sends one unicast packet to ip:8002 and waits for a matching reply, retrying on timeout (UDP can drop packets). */
|
|
47
|
+
function request(ip, packetBytes, expectCmd, expectMethod, timeoutMs = 1500, retries = 2) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
|
|
50
|
+
let attemptsLeft = retries;
|
|
51
|
+
let timer;
|
|
52
|
+
const cleanup = () => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
socket.close();
|
|
55
|
+
};
|
|
56
|
+
socket.on("error", (err) => {
|
|
57
|
+
cleanup();
|
|
58
|
+
reject(err);
|
|
59
|
+
});
|
|
60
|
+
socket.on("message", (msg) => {
|
|
61
|
+
const data = decodePacket(msg);
|
|
62
|
+
if (data[0] === expectCmd && data[1] === expectMethod) {
|
|
63
|
+
cleanup();
|
|
64
|
+
resolve(data);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
function send() {
|
|
68
|
+
socket.send(encodePacket(packetBytes), exports.UDP_CONFIG_PORT, ip);
|
|
69
|
+
timer = setTimeout(() => {
|
|
70
|
+
if (attemptsLeft-- > 0)
|
|
71
|
+
send();
|
|
72
|
+
else {
|
|
73
|
+
cleanup();
|
|
74
|
+
reject(new Error(`No UDP config reply from ${ip} (cmd ${expectCmd}/${expectMethod})`));
|
|
75
|
+
}
|
|
76
|
+
}, timeoutMs);
|
|
77
|
+
}
|
|
78
|
+
socket.bind(0, send);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Pushes a new home-server password/TCP-port to a master over the UDP config protocol (port 8002). */
|
|
82
|
+
function setHomeServerCfg(ip, password, ipPort, timeoutMs) {
|
|
83
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
84
|
+
const packet = [CMD_FC_APPCONFIG, ATTR_HOMESRVCFG_SET, ...stringToBytes(password), ...u16ToBytes(ipPort), ...u16ToBytes(HOMESRVCFG_PROTOCOL_FLAGS)];
|
|
85
|
+
const reply = yield request(ip, packet, CMD_EV_APPCONFIG, ATTR_HOMESRVCFG_SET, timeoutMs);
|
|
86
|
+
return reply[2] === 1;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
exports.setHomeServerCfg = setHomeServerCfg;
|
|
90
|
+
//# sourceMappingURL=udpConfig.js.map
|
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.2.1",
|
|
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",
|
package/server/overkiz.js
CHANGED
|
@@ -8,11 +8,32 @@
|
|
|
8
8
|
// then polling its HomeKit accessories back into Duotecno via server/HB.ts
|
|
9
9
|
// and the Link mechanism in smartapp.ts.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// to a specific
|
|
15
|
-
//
|
|
11
|
+
// Connects, discovers devices, sends open/close/stop/dim commands, and emits
|
|
12
|
+
// translated Duotecno-style states when Overkiz reports a change. Wired into
|
|
13
|
+
// the Link UI in smartapp.ts so a Duotecno up/down or dimmer unit can be
|
|
14
|
+
// coupled to a specific Overkiz device.
|
|
15
|
+
//
|
|
16
|
+
// Verified against real hardware (a TaHoma box with Screen and Awning
|
|
17
|
+
// devices): both widget types expose a genuine 'stop' command, unlike the
|
|
18
|
+
// Homebridge-accessory link path (server/HB.ts) which has to fake a stop by
|
|
19
|
+
// re-setting the last known (and often unreliable) position.
|
|
20
|
+
//
|
|
21
|
+
// Screen devices (uiClass "Screen", e.g. roller shutters/blinds) use
|
|
22
|
+
// open/close/stop/setClosure. Awning devices (uiClass "Awning", e.g. sun
|
|
23
|
+
// awnings) use a different command set: undeploy/deploy/stop/setDeployment
|
|
24
|
+
// (there is no "open"/"close" on an Awning). Light devices (uiClass "Light")
|
|
25
|
+
// only expose setIntensity (0-100), no separate on/off.
|
|
26
|
+
//
|
|
27
|
+
// Known overkiz-client (1.0.23) quirk: Client.execute() stores the plain
|
|
28
|
+
// {label, actions} object we pass it into its internal executionPool, but
|
|
29
|
+
// fetchEvents() later assumes pooled entries are Execution instances and
|
|
30
|
+
// calls execution.onStateUpdate(), which throws for our commands ("Polling
|
|
31
|
+
// error - TypeError: execution.onStateUpdate is not a function"). That's
|
|
32
|
+
// caught inside the library and only affects completion-tracking of our own
|
|
33
|
+
// outgoing commands (externally-triggered state changes, e.g. via the Somfy
|
|
34
|
+
// app or remote, aren't affected and still push through normally) -- but to
|
|
35
|
+
// get a reliable resulting state for OUR commands we explicitly refresh the
|
|
36
|
+
// device afterwards instead of trusting that push.
|
|
16
37
|
//
|
|
17
38
|
// Johan Coppieters, 2026.
|
|
18
39
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
@@ -31,10 +52,15 @@ const overkiz_client_1 = require("overkiz-client");
|
|
|
31
52
|
const types_1 = require("../duotecno/types");
|
|
32
53
|
const logger_1 = require("../duotecno/logger");
|
|
33
54
|
const base_1 = require("./base");
|
|
34
|
-
//
|
|
35
|
-
//
|
|
55
|
+
// Position state: 0 = fully open/undeployed, 100 = fully closed/deployed
|
|
56
|
+
// (opposite of HomeKit's TargetPosition scale). Screens report
|
|
57
|
+
// core:ClosureState, Awnings report core:DeploymentState instead.
|
|
58
|
+
const kPositionStateNames = ["core:ClosureState", "core:DeploymentState"];
|
|
59
|
+
const kIntensityStateNames = ["core:LightIntensityState", "core:IntensityState"];
|
|
36
60
|
const kOpenThreshold = 5;
|
|
37
61
|
const kClosedThreshold = 95;
|
|
62
|
+
const kAwningUiClass = "Awning";
|
|
63
|
+
const kIgnoredUiClasses = ["ProtocolGateway"]; // bridges/gateways: not a controllable end-device
|
|
38
64
|
function closureToUnitState(closure, previousClosure) {
|
|
39
65
|
if (closure <= kOpenThreshold)
|
|
40
66
|
return types_1.UnitState.kOpen;
|
|
@@ -62,11 +88,25 @@ class Overkiz extends base_1.Base {
|
|
|
62
88
|
(0, logger_1.log)("overkiz", "Overkiz not configured (missing user/password) -> not starting.");
|
|
63
89
|
}
|
|
64
90
|
}
|
|
91
|
+
// (Re)connect using the current config -- used at startup and after the
|
|
92
|
+
// settings page saves new credentials, so a full process restart isn't
|
|
93
|
+
// required to pick them up.
|
|
65
94
|
connect() {
|
|
66
95
|
return __awaiter(this, void 0, void 0, function* () {
|
|
67
96
|
const bridge = {
|
|
68
97
|
warn: (...args) => (0, logger_1.log)("overkiz", args.join(" ")),
|
|
69
|
-
error: (...args) =>
|
|
98
|
+
error: (...args) => {
|
|
99
|
+
const msg = args.join(" ");
|
|
100
|
+
if (msg.includes("onStateUpdate is not a function")) {
|
|
101
|
+
// the known overkiz-client executionPool quirk described up top --
|
|
102
|
+
// harmless (the command still executes fine), just log it quietly
|
|
103
|
+
// instead of as an error.
|
|
104
|
+
(0, logger_1.debug)("overkiz", msg);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
(0, logger_1.err)("overkiz", msg);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
70
110
|
debug: (...args) => (0, logger_1.debug)("overkiz", args.join(" ")),
|
|
71
111
|
info: (...args) => (0, logger_1.log)("overkiz", args.join(" ")),
|
|
72
112
|
log: (...args) => (0, logger_1.log)("overkiz", args.join(" ")),
|
|
@@ -79,32 +119,46 @@ class Overkiz extends base_1.Base {
|
|
|
79
119
|
refreshPeriod: this.config.refreshPeriod || 60,
|
|
80
120
|
});
|
|
81
121
|
const setup = yield this.client.getSetup();
|
|
122
|
+
this.devices.clear();
|
|
123
|
+
this.closures.clear();
|
|
82
124
|
setup.devices.forEach(device => this.attachDevice(device));
|
|
83
125
|
(0, logger_1.log)("overkiz", `connected -> ${setup.devices.length} device(s) found`);
|
|
84
126
|
});
|
|
85
127
|
}
|
|
86
128
|
attachDevice(device) {
|
|
87
129
|
this.devices.set(device.deviceURL, device);
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
this.closures.set(device.deviceURL,
|
|
130
|
+
const stateName = kPositionStateNames.find(n => device.hasState(n));
|
|
131
|
+
if (stateName)
|
|
132
|
+
this.closures.set(device.deviceURL, device.getNumber(stateName));
|
|
91
133
|
device.on("states", (states) => this.onStates(device, states));
|
|
92
134
|
}
|
|
93
135
|
onStates(device, states) {
|
|
94
136
|
(0, logger_1.debug)("overkiz", `states for ${device.label} -> ${JSON.stringify(states)}`);
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
const closure = Number(
|
|
137
|
+
const positionState = states.find(s => kPositionStateNames.includes(s.name));
|
|
138
|
+
if (positionState) {
|
|
139
|
+
const closure = Number(positionState.value);
|
|
98
140
|
const previous = this.closures.get(device.deviceURL);
|
|
99
141
|
const unitState = closureToUnitState(closure, previous);
|
|
100
142
|
this.closures.set(device.deviceURL, closure);
|
|
101
|
-
(0, logger_1.log)("overkiz", `${device.label} ->
|
|
143
|
+
(0, logger_1.log)("overkiz", `${device.label} -> position ${closure} -> UnitState ${types_1.UnitState[unitState]}`);
|
|
102
144
|
this.emitter.emit("state", device.deviceURL, unitState, closure);
|
|
103
145
|
}
|
|
146
|
+
const intensityState = states.find(s => kIntensityStateNames.includes(s.name));
|
|
147
|
+
if (intensityState) {
|
|
148
|
+
const intensity = Number(intensityState.value);
|
|
149
|
+
(0, logger_1.log)("overkiz", `${device.label} -> intensity ${intensity}`);
|
|
150
|
+
this.emitter.emit("intensity", device.deviceURL, intensity);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
uiClass(deviceURL) {
|
|
154
|
+
var _a;
|
|
155
|
+
return ((_a = this.devices.get(deviceURL)) === null || _a === void 0 ? void 0 : _a.definition.uiClass) || "";
|
|
104
156
|
}
|
|
105
157
|
listDevices() {
|
|
106
|
-
return Array.from(this.devices.values())
|
|
107
|
-
|
|
158
|
+
return Array.from(this.devices.values())
|
|
159
|
+
.filter(d => !kIgnoredUiClasses.includes(d.definition.uiClass))
|
|
160
|
+
.map(d => ({
|
|
161
|
+
deviceURL: d.deviceURL, label: d.label, widget: d.definition.widgetName, uiClass: d.definition.uiClass,
|
|
108
162
|
}));
|
|
109
163
|
}
|
|
110
164
|
execute(deviceURL, commandName, parameters = []) {
|
|
@@ -116,26 +170,46 @@ class Overkiz extends base_1.Base {
|
|
|
116
170
|
label: "smartsocket",
|
|
117
171
|
actions: [{ deviceURL, commands: [{ name: commandName, parameters }] }],
|
|
118
172
|
});
|
|
173
|
+
// work around the executionPool/onStateUpdate quirk described above: get
|
|
174
|
+
// an authoritative resulting state ourselves rather than trusting the
|
|
175
|
+
// (possibly swallowed) push event for our own command.
|
|
176
|
+
this.client.refreshDeviceStates(deviceURL).catch(e => (0, logger_1.debug)("overkiz", `refreshDeviceStates failed: ${e.message || e}`));
|
|
119
177
|
});
|
|
120
178
|
}
|
|
179
|
+
hasCommand(deviceURL, commandName) {
|
|
180
|
+
var _a;
|
|
181
|
+
return ((_a = this.devices.get(deviceURL)) === null || _a === void 0 ? void 0 : _a.hasCommand(commandName)) || false;
|
|
182
|
+
}
|
|
121
183
|
up(deviceURL) {
|
|
122
184
|
return __awaiter(this, void 0, void 0, function* () {
|
|
123
|
-
yield this.execute(deviceURL, "open");
|
|
185
|
+
yield this.execute(deviceURL, (this.uiClass(deviceURL) === kAwningUiClass) ? "undeploy" : "open");
|
|
124
186
|
});
|
|
125
187
|
}
|
|
126
188
|
down(deviceURL) {
|
|
127
189
|
return __awaiter(this, void 0, void 0, function* () {
|
|
128
|
-
yield this.execute(deviceURL, "close");
|
|
190
|
+
yield this.execute(deviceURL, (this.uiClass(deviceURL) === kAwningUiClass) ? "deploy" : "close");
|
|
129
191
|
});
|
|
130
192
|
}
|
|
193
|
+
// Only send a real 'stop' -- never fake one (that's the whole point of this
|
|
194
|
+
// native integration over the Homebridge-accessory link path). If this
|
|
195
|
+
// particular device doesn't expose a 'stop' command, do nothing.
|
|
131
196
|
stop(deviceURL) {
|
|
132
197
|
return __awaiter(this, void 0, void 0, function* () {
|
|
198
|
+
if (!this.hasCommand(deviceURL, "stop")) {
|
|
199
|
+
(0, logger_1.log)("overkiz", `device ${deviceURL} has no native 'stop' command -> not sending anything`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
133
202
|
yield this.execute(deviceURL, "stop");
|
|
134
203
|
});
|
|
135
204
|
}
|
|
136
205
|
setClosure(deviceURL, percentClosed) {
|
|
137
206
|
return __awaiter(this, void 0, void 0, function* () {
|
|
138
|
-
yield this.execute(deviceURL, "setClosure", [percentClosed]);
|
|
207
|
+
yield this.execute(deviceURL, (this.uiClass(deviceURL) === kAwningUiClass) ? "setDeployment" : "setClosure", [percentClosed]);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
setIntensity(deviceURL, percent) {
|
|
211
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
212
|
+
yield this.execute(deviceURL, "setIntensity", [percent]);
|
|
139
213
|
});
|
|
140
214
|
}
|
|
141
215
|
}
|
package/server/smartapp.js
CHANGED
|
@@ -25,7 +25,9 @@ const logger_1 = require("../duotecno/logger");
|
|
|
25
25
|
const protocol_1 = require("../duotecno/protocol");
|
|
26
26
|
const somfy = require("./somfy");
|
|
27
27
|
const fs_1 = require("fs");
|
|
28
|
+
const overkiz_1 = require("./overkiz");
|
|
28
29
|
const config_1 = require("../duotecno/config");
|
|
30
|
+
const udpConfig_1 = require("../duotecno/udpConfig");
|
|
29
31
|
const HB_1 = require("./HB");
|
|
30
32
|
const HA_API_1 = require("./HA-API");
|
|
31
33
|
const proxy_1 = require("./proxy");
|
|
@@ -73,7 +75,7 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
73
75
|
// get some configurated params
|
|
74
76
|
const configuredPort = this.config.port || 80;
|
|
75
77
|
this.password = platform.config.password || this.config.password || "";
|
|
76
|
-
this.user = platform.config.user || this.config.user || "
|
|
78
|
+
this.user = platform.config.user || this.config.user || "duotecno";
|
|
77
79
|
// Set up multi-port listening if port is not 80
|
|
78
80
|
// Note: Port 80 requires root/sudo on Unix-like systems
|
|
79
81
|
// For development on macOS/Linux without sudo, use only configured port
|
|
@@ -516,6 +518,7 @@ function SmartSocketInit(message){
|
|
|
516
518
|
// Settings //
|
|
517
519
|
//////////////
|
|
518
520
|
doSettings(context) {
|
|
521
|
+
var _a;
|
|
519
522
|
return __awaiter(this, void 0, void 0, function* () {
|
|
520
523
|
let config = this.read("settings");
|
|
521
524
|
let message = "";
|
|
@@ -528,6 +531,9 @@ function SmartSocketInit(message){
|
|
|
528
531
|
// Note: Full restart recommended for mDNS changes to take effect properly
|
|
529
532
|
(0, logger_1.log)("smartapp", "Settings saved. mDNS name updated to: " + config.mdnsName);
|
|
530
533
|
}
|
|
534
|
+
const overkizMessage = yield this.saveOverkizSettings(context);
|
|
535
|
+
if (overkizMessage)
|
|
536
|
+
message += " " + overkizMessage;
|
|
531
537
|
}
|
|
532
538
|
else if (context.action === "restart") {
|
|
533
539
|
context.request = "restart";
|
|
@@ -536,7 +542,9 @@ function SmartSocketInit(message){
|
|
|
536
542
|
else {
|
|
537
543
|
// Just display the settings form
|
|
538
544
|
}
|
|
539
|
-
|
|
545
|
+
const overkizConfig = ((_a = this.power.overkiz) === null || _a === void 0 ? void 0 : _a.config) || types_1.Sanitizers.overkiz(null);
|
|
546
|
+
const overkizDevices = this.power.overkiz ? this.power.overkiz.listDevices() : [];
|
|
547
|
+
return this.ejs("settings", context, { config, message, overkizConfig, overkizDevices });
|
|
540
548
|
});
|
|
541
549
|
}
|
|
542
550
|
scrapeSettings(context) {
|
|
@@ -683,6 +691,7 @@ function SmartSocketInit(message){
|
|
|
683
691
|
(0, logger_1.log)("smartapp", "** error ** missing unit: " + (0, types_1.hex)(link.logicalNodeAddress) + "/" + (0, types_1.hex)(link.logicalAddress) + " **");
|
|
684
692
|
}
|
|
685
693
|
});
|
|
694
|
+
this.attachOverkizListeners();
|
|
686
695
|
(0, HB_1.hbSubscribe)(() => __awaiter(this, void 0, void 0, function* () {
|
|
687
696
|
// Get all values for subscribed accessories (links)
|
|
688
697
|
if (this.links.length && (yield this.checkToken())) {
|
|
@@ -724,42 +733,49 @@ function SmartSocketInit(message){
|
|
|
724
733
|
(0, logger_1.debug)("smartapp", "Received DT value from unit: " + u.getDisplayName() + " -> " + u.value + " / " + u.status + " from unit");
|
|
725
734
|
if (!this.links.length)
|
|
726
735
|
return;
|
|
727
|
-
if
|
|
728
|
-
|
|
736
|
+
// only bother logging into the Homebridge API if we actually have an accessory-type link to serve
|
|
737
|
+
const needsToken = this.links.some(link => link.type === types_1.LinkType.kAccessory);
|
|
738
|
+
const tokenOK = needsToken ? yield this.checkToken() : true;
|
|
729
739
|
for (const link of this.links) {
|
|
730
|
-
if (u.isUnit(link.masterAddress, link.masterPort, link.logicalNodeAddress, link.logicalAddress))
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
else if (hbValue !== link.hbValue) {
|
|
746
|
-
(0, logger_1.log)("smartapp", "-> Setting HB value for accessory: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName() + " with value " + u.value);
|
|
747
|
-
link.hbValue = hbValue;
|
|
748
|
-
yield (0, HB_1.hbSetState)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, hbValue, this.token);
|
|
740
|
+
if (!u.isUnit(link.masterAddress, link.masterPort, link.logicalNodeAddress, link.logicalAddress))
|
|
741
|
+
continue;
|
|
742
|
+
if (link.type === types_1.LinkType.kOverkiz) {
|
|
743
|
+
yield this.informOverkizLink(u, link);
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (!tokenOK)
|
|
747
|
+
continue;
|
|
748
|
+
const hbValue = (0, HB_1.dt2hbvalue)(u, link.hbValue, link.min, link.max);
|
|
749
|
+
if (typeof hbValue != "undefined") {
|
|
750
|
+
try {
|
|
751
|
+
if (hbValue === -1) {
|
|
752
|
+
if (link.nostop === "Y") {
|
|
753
|
+
(0, logger_1.debug)("smartapp", "-> Not sending simulated 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName() + " (disabled)");
|
|
749
754
|
}
|
|
750
755
|
else {
|
|
751
|
-
(0, logger_1.
|
|
756
|
+
(0, logger_1.log)("smartapp", "-> Virtual unit send 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
757
|
+
const val = yield (0, HB_1.hbStop)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, this.token);
|
|
758
|
+
if (typeof val != "undefined")
|
|
759
|
+
link.hbValue = val;
|
|
752
760
|
}
|
|
753
761
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
762
|
+
else if (hbValue !== link.hbValue) {
|
|
763
|
+
(0, logger_1.log)("smartapp", "-> Setting HB value for accessory: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName() + " with value " + u.value);
|
|
764
|
+
link.hbValue = hbValue;
|
|
765
|
+
yield (0, HB_1.hbSetState)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, hbValue, this.token);
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
(0, logger_1.debug)("smartapp", "-> No change for accessory: " + link.accName + " to " + u.value + " / " + u.status);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
catch (e) {
|
|
772
|
+
if (e instanceof HB_1.HbAuthError) {
|
|
773
|
+
(0, logger_1.err)("smartapp", "informLinks: Homebridge API token expired for accessory " + link.accName + " -> forcing re-login on next update");
|
|
774
|
+
this.token = "";
|
|
775
|
+
(0, config_1.setToken)("");
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
(0, logger_1.err)("smartapp", "informLinks: failed to set HB value for accessory " + link.accName + " -> " + e.toString());
|
|
763
779
|
}
|
|
764
780
|
}
|
|
765
781
|
}
|
|
@@ -767,6 +783,243 @@ function SmartSocketInit(message){
|
|
|
767
783
|
;
|
|
768
784
|
});
|
|
769
785
|
}
|
|
786
|
+
// Duotecno -> Overkiz direction of a kOverkiz link. Mirrors the kAccessory
|
|
787
|
+
// branch above, but talks straight to the Overkiz device instead of going
|
|
788
|
+
// through the Homebridge REST API -- and unlike hbStop(), overkiz.stop()
|
|
789
|
+
// is a genuine 'stop' command, not a simulated one.
|
|
790
|
+
informOverkizLink(u, link) {
|
|
791
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
792
|
+
if (!this.power.overkiz)
|
|
793
|
+
return;
|
|
794
|
+
if (u.isUpDown()) {
|
|
795
|
+
yield this.informOverkizUpDown(u, link);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
// A dimmer (or, for a Screen/Awning target, a mood carrying a numeric
|
|
799
|
+
// macro payload -- the same "set to X%" trick previously used to park a
|
|
800
|
+
// screen/awning at a specific height) linked to a Screen/Awning sets its
|
|
801
|
+
// position; linked to a Light it sets intensity instead. Dispatch on the
|
|
802
|
+
// Overkiz device's own type, not just the Duotecno unit's type, since a
|
|
803
|
+
// dimmer unit is now also how a real Overkiz dimmer (Light) gets driven.
|
|
804
|
+
const targetUiClass = this.power.overkiz.uiClass(link.accId);
|
|
805
|
+
const isPositional = (targetUiClass === "Screen") || (targetUiClass === "Awning");
|
|
806
|
+
if (isPositional && (u.isDimmer() || u.isMood())) {
|
|
807
|
+
yield this.informOverkizPosition(u, link);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (!u.isDimmer())
|
|
811
|
+
return;
|
|
812
|
+
// dt2hbvalue reports the raw 0-100 intensity directly, no real/virtual
|
|
813
|
+
// distinction to worry about here (unlike up/down above).
|
|
814
|
+
const hbValue = (0, HB_1.dt2hbvalue)(u, link.hbValue, link.min, link.max);
|
|
815
|
+
if ((typeof hbValue === "undefined") || (hbValue === link.hbValue))
|
|
816
|
+
return;
|
|
817
|
+
try {
|
|
818
|
+
link.hbValue = hbValue;
|
|
819
|
+
(0, logger_1.log)("smartapp", "-> Setting Overkiz intensity for: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName());
|
|
820
|
+
yield this.power.overkiz.setIntensity(link.accId, hbValue);
|
|
821
|
+
}
|
|
822
|
+
catch (e) {
|
|
823
|
+
(0, logger_1.err)("smartapp", "informOverkizLink: failed for " + link.accName + " -> " + e.toString());
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
// Sets a Screen/Awning to a specific position (0-100) from a dimmer's raw
|
|
828
|
+
// value, or from a mood unit's value when Duotecno programming sends it a
|
|
829
|
+
// numeric macro payload (the same mechanism dimmers use -- a mood isn't
|
|
830
|
+
// normally more than an on/off trigger, but if its macro carries a value
|
|
831
|
+
// it ends up in unit.value exactly like a dimmer's does). Our scale is
|
|
832
|
+
// 0=off/closed, 100=on/fully open (matching how dimmer links always
|
|
833
|
+
// worked here); Overkiz's closure/deployment scale is the opposite
|
|
834
|
+
// (0=open, 100=closed), so invert on the way out.
|
|
835
|
+
informOverkizPosition(u, link) {
|
|
836
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
837
|
+
if (!this.power.overkiz)
|
|
838
|
+
return;
|
|
839
|
+
const raw = (typeof u.value === "boolean") ? (u.value ? 100 : 0) : Math.round(u.value);
|
|
840
|
+
const position = Math.max(0, Math.min(100, raw));
|
|
841
|
+
if (position === link.hbValue)
|
|
842
|
+
return;
|
|
843
|
+
try {
|
|
844
|
+
link.hbValue = position;
|
|
845
|
+
(0, logger_1.log)("smartapp", "-> Setting Overkiz position for: " + link.accName + " to " + position + "% of Duotecno unit: " + u.getDisplayName());
|
|
846
|
+
yield this.power.overkiz.setClosure(link.accId, 100 - position);
|
|
847
|
+
}
|
|
848
|
+
catch (e) {
|
|
849
|
+
(0, logger_1.err)("smartapp", "informOverkizPosition: failed for " + link.accName + " -> " + e.toString());
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
// A Duotecno up/down (kSwitchingMotor) unit reports status two different
|
|
854
|
+
// ways depending on how it's wired in the Duotecno programming:
|
|
855
|
+
// - a "virtual button" duoswitch macro event -> unit.value = 10 (stop) /
|
|
856
|
+
// 11 (up) / 12 (down), a one-shot pulse (this is what dt2hbvalue/HB.ts
|
|
857
|
+
// expects, and is what the existing Homebridge-accessory links rely on).
|
|
858
|
+
// - the real motor's own busy/idle status -> unit.value === unit.status,
|
|
859
|
+
// one of 0 (stopped), 1 (stopped/down), 2 (stopped/up), 3 (busy/down),
|
|
860
|
+
// 4 (busy/up) -- e.g. the "Garage" unit, which has no duoswitch macro
|
|
861
|
+
// wired to it and only ever reports these real values.
|
|
862
|
+
// Handle both: a real motor's busy<->idle transition is translated into
|
|
863
|
+
// up/down/stop the same way a virtual pulse would be.
|
|
864
|
+
informOverkizUpDown(u, link) {
|
|
865
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
866
|
+
if (!this.power.overkiz)
|
|
867
|
+
return;
|
|
868
|
+
const rawValue = Math.round(u.value);
|
|
869
|
+
try {
|
|
870
|
+
if ((rawValue === 10) || (rawValue === 11) || (rawValue === 12)) {
|
|
871
|
+
// virtual duoswitch pulse
|
|
872
|
+
if (rawValue === link.hbValue)
|
|
873
|
+
return;
|
|
874
|
+
link.hbValue = rawValue;
|
|
875
|
+
if (rawValue === 10) {
|
|
876
|
+
if (link.nostop === "Y") {
|
|
877
|
+
(0, logger_1.debug)("smartapp", "-> Not sending 'stop' to Overkiz device: " + link.accName + " (disabled)");
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
(0, logger_1.log)("smartapp", "-> Sending 'stop' to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
881
|
+
yield this.power.overkiz.stop(link.accId);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
else if (rawValue === 11) {
|
|
885
|
+
(0, logger_1.log)("smartapp", "-> Sending up/open to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
886
|
+
yield this.power.overkiz.up(link.accId);
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
(0, logger_1.log)("smartapp", "-> Sending down/close to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
890
|
+
yield this.power.overkiz.down(link.accId);
|
|
891
|
+
}
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
// real motor busy/idle status
|
|
895
|
+
const status = u.status;
|
|
896
|
+
const wasBusy = (link.hbValue === 3) || (link.hbValue === 4);
|
|
897
|
+
const isBusy = (status === 3) || (status === 4);
|
|
898
|
+
if (isBusy && (status !== link.hbValue)) {
|
|
899
|
+
link.hbValue = status;
|
|
900
|
+
if (status === 4) {
|
|
901
|
+
(0, logger_1.log)("smartapp", "-> Sending up/open to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
902
|
+
yield this.power.overkiz.up(link.accId);
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
(0, logger_1.log)("smartapp", "-> Sending down/close to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
906
|
+
yield this.power.overkiz.down(link.accId);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
else if (!isBusy && wasBusy) {
|
|
910
|
+
link.hbValue = status;
|
|
911
|
+
if (link.nostop === "Y") {
|
|
912
|
+
(0, logger_1.debug)("smartapp", "-> Not sending 'stop' to Overkiz device: " + link.accName + " (disabled)");
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
(0, logger_1.log)("smartapp", "-> Sending 'stop' to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
916
|
+
yield this.power.overkiz.stop(link.accId);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
link.hbValue = status;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
catch (e) {
|
|
924
|
+
(0, logger_1.err)("smartapp", "informOverkizUpDown: failed for " + link.accName + " -> " + e.toString());
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
attachOverkizListeners() {
|
|
929
|
+
if (!this.power.overkiz || (this.power.overkiz === this.attachedOverkiz))
|
|
930
|
+
return;
|
|
931
|
+
this.attachedOverkiz = this.power.overkiz;
|
|
932
|
+
this.power.overkiz.emitter.on("state", (deviceURL, unitState, closure) => {
|
|
933
|
+
for (const link of this.links) {
|
|
934
|
+
if ((link.type !== types_1.LinkType.kOverkiz) || (link.accId !== deviceURL) || !link.unit)
|
|
935
|
+
continue;
|
|
936
|
+
if (link.unit.isDimmer()) {
|
|
937
|
+
// position-sourced link: reflect Overkiz's position back as a
|
|
938
|
+
// dimmer value. Overkiz's closure/deployment scale is the opposite
|
|
939
|
+
// of ours (0=open/100=closed vs our 0=off/100=on) -- invert back.
|
|
940
|
+
const position = Math.max(0, Math.min(100, Math.round(100 - closure)));
|
|
941
|
+
if (position === link.dtValue)
|
|
942
|
+
continue;
|
|
943
|
+
link.dtValue = position;
|
|
944
|
+
(0, logger_1.log)("smartapp", "-> Setting value of Duotecno unit: " + link.unit.getDisplayName() + " to " + position + " from Overkiz position " + closure + " of: " + link.accName);
|
|
945
|
+
link.unit.setState(position);
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
// a mood can only be triggered, not set to an arbitrary value, so a
|
|
949
|
+
// mood-sourced position link is Duotecno -> Overkiz only; nothing to
|
|
950
|
+
// reflect back here. Only an up/down unit gets the state below.
|
|
951
|
+
if (!link.unit.isUpDown())
|
|
952
|
+
continue;
|
|
953
|
+
if (unitState === link.dtValue)
|
|
954
|
+
continue; // no repeat
|
|
955
|
+
link.dtValue = unitState;
|
|
956
|
+
if ((unitState === types_1.UnitState.kOpen) || (unitState === types_1.UnitState.kOpening)) {
|
|
957
|
+
(0, logger_1.log)("smartapp", "-> Overkiz reports " + types_1.UnitState[unitState] + " for: " + link.accName + " -> setting Duotecno unit " + link.unit.getDisplayName() + " to open");
|
|
958
|
+
link.unit.setState(types_1.UnitMotorCmd.kOpen);
|
|
959
|
+
}
|
|
960
|
+
else if ((unitState === types_1.UnitState.kClosed) || (unitState === types_1.UnitState.kClosing)) {
|
|
961
|
+
(0, logger_1.log)("smartapp", "-> Overkiz reports " + types_1.UnitState[unitState] + " for: " + link.accName + " -> setting Duotecno unit " + link.unit.getDisplayName() + " to close");
|
|
962
|
+
link.unit.setState(types_1.UnitMotorCmd.kClose);
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
// kStopped: Overkiz reports the device sitting still somewhere in
|
|
966
|
+
// the middle (e.g. stopped mid-way via the Somfy app/remote) --
|
|
967
|
+
// forward a genuine stop, same as the Duotecno -> Overkiz direction.
|
|
968
|
+
if (link.nostop === "Y") {
|
|
969
|
+
(0, logger_1.debug)("smartapp", "-> Not forwarding Overkiz 'stopped' for: " + link.accName + " (disabled)");
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
(0, logger_1.log)("smartapp", "-> Overkiz reports stopped for: " + link.accName + " -> setting Duotecno unit " + link.unit.getDisplayName() + " to stop");
|
|
973
|
+
link.unit.setState(types_1.UnitMotorCmd.kStop);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
this.power.overkiz.emitter.on("intensity", (deviceURL, intensity) => {
|
|
979
|
+
for (const link of this.links) {
|
|
980
|
+
if ((link.type !== types_1.LinkType.kOverkiz) || (link.accId !== deviceURL) || !link.unit)
|
|
981
|
+
continue;
|
|
982
|
+
const { dtValue, dtTarget } = (0, HB_1.hb2dtvalue)(link.unit, intensity);
|
|
983
|
+
if (dtValue !== link.dtValue) {
|
|
984
|
+
(0, logger_1.log)("smartapp", "-> Setting value of Duotecno unit: " + link.unit.getDisplayName() + " to " + dtTarget + " from Overkiz intensity " + intensity + " of: " + link.accName);
|
|
985
|
+
link.dtValue = dtValue;
|
|
986
|
+
link.unit.setState(dtTarget);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
//////////////////////////////
|
|
992
|
+
// Overkiz (Somfy/TaHoma) //
|
|
993
|
+
//////////////////////////////
|
|
994
|
+
// Saves the Overkiz fields living on the Settings page and (re)connects.
|
|
995
|
+
// Only attempts a connection when credentials are actually present, so
|
|
996
|
+
// saving the rest of the Settings form doesn't produce a spurious
|
|
997
|
+
// "connection failed" for installs that don't use Overkiz at all.
|
|
998
|
+
saveOverkizSettings(context) {
|
|
999
|
+
var _a, _b;
|
|
1000
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1001
|
+
const service = context.getParam({ name: "service", type: "string", default: "local" });
|
|
1002
|
+
const user = context.getParam({ name: "user", type: "string", default: "" });
|
|
1003
|
+
const submittedPassword = context.getParam({ name: "password", type: "string", default: "" });
|
|
1004
|
+
// blank password field means "keep the current one"
|
|
1005
|
+
const password = submittedPassword || ((_b = (_a = this.power.overkiz) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.password) || "";
|
|
1006
|
+
const pollingPeriod = context.getParam({ name: "pollingPeriod", type: "integer", default: 10 });
|
|
1007
|
+
if (!this.power.overkiz)
|
|
1008
|
+
this.power.overkiz = new overkiz_1.Overkiz(this.system);
|
|
1009
|
+
this.power.overkiz.config = types_1.Sanitizers.overkiz({ service, user, password, pollingPeriod });
|
|
1010
|
+
this.power.overkiz.writeConfig();
|
|
1011
|
+
if (!user || !password)
|
|
1012
|
+
return "";
|
|
1013
|
+
try {
|
|
1014
|
+
yield this.power.overkiz.connect();
|
|
1015
|
+
this.attachOverkizListeners();
|
|
1016
|
+
return "Overkiz connected successfully -> " + this.power.overkiz.listDevices().length + " device(s) found.";
|
|
1017
|
+
}
|
|
1018
|
+
catch (e) {
|
|
1019
|
+
return "Overkiz connection failed: " + (e.message || e.toString());
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
770
1023
|
checkToken() {
|
|
771
1024
|
return __awaiter(this, void 0, void 0, function* () {
|
|
772
1025
|
if (!this.token) {
|
|
@@ -793,6 +1046,7 @@ function SmartSocketInit(message){
|
|
|
793
1046
|
return __awaiter(this, void 0, void 0, function* () {
|
|
794
1047
|
let inx = parseInt(context.id);
|
|
795
1048
|
const accessories = yield this.getAccessories();
|
|
1049
|
+
const overkizDevices = this.power.overkiz ? this.power.overkiz.listDevices() : [];
|
|
796
1050
|
const units = this.system.allUsedUnits();
|
|
797
1051
|
let message;
|
|
798
1052
|
try {
|
|
@@ -800,11 +1054,11 @@ function SmartSocketInit(message){
|
|
|
800
1054
|
this.links.push(Object.assign(Object.assign({}, types_1.kEmptyLink), { type: types_1.LinkType.kAccessory }));
|
|
801
1055
|
inx = this.links.length - 1;
|
|
802
1056
|
return this.ejs("linkDetail", context, { link: this.links[inx], id: inx,
|
|
803
|
-
masters: this.system.masters, accessories, units });
|
|
1057
|
+
masters: this.system.masters, accessories, overkizDevices, units });
|
|
804
1058
|
}
|
|
805
1059
|
else if (context.action === "edit") {
|
|
806
1060
|
return this.ejs("linkDetail", context, { link: this.links[inx], id: inx,
|
|
807
|
-
masters: this.system.masters, accessories, units });
|
|
1061
|
+
masters: this.system.masters, accessories, overkizDevices, units });
|
|
808
1062
|
}
|
|
809
1063
|
else if (context.action === "delete") {
|
|
810
1064
|
this.deleteLink(inx);
|
|
@@ -824,22 +1078,29 @@ function SmartSocketInit(message){
|
|
|
824
1078
|
});
|
|
825
1079
|
}
|
|
826
1080
|
scrapeLink(context) {
|
|
1081
|
+
var _a;
|
|
827
1082
|
// don't take the name of the unit
|
|
828
1083
|
const { masterAddress, masterPort, logicalAddress, logicalNodeAddress } = this.scrapeUnit(context, '');
|
|
829
|
-
// for now only accessory links.
|
|
830
|
-
const stype = context.getParam({ name: "type", type: "string", default: types_1.LinkType.kAccessory });
|
|
831
1084
|
const name = context.getParam({ name: "name", type: "string", default: "--" });
|
|
832
1085
|
// max & min values
|
|
833
1086
|
const min = context.getParam({ name: "min", type: "integer", default: 0 });
|
|
834
1087
|
const max = context.getParam({ name: "max", type: "integer", default: 100 });
|
|
835
1088
|
// don't forward the simulated 'stop' of an up/down link to the accessory
|
|
836
1089
|
const nostop = context.getParam({ name: "nostop", type: "string", default: "N" });
|
|
837
|
-
//
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
1090
|
+
// the accessory dropdown offers both Homebridge accessories and Overkiz
|
|
1091
|
+
// devices in one list; an Overkiz option's value is prefixed so we know
|
|
1092
|
+
// which provider -- and hence Link type -- the picked id belongs to.
|
|
1093
|
+
const rawAccId = context.getParam({ name: "accId", type: "string", default: "" });
|
|
1094
|
+
const kOverkizPrefix = "overkiz:";
|
|
1095
|
+
if (rawAccId.startsWith(kOverkizPrefix)) {
|
|
1096
|
+
const accId = rawAccId.substring(kOverkizPrefix.length);
|
|
1097
|
+
const device = (_a = this.power.overkiz) === null || _a === void 0 ? void 0 : _a.listDevices().find(d => d.deviceURL === accId);
|
|
1098
|
+
return { accId, accName: (device === null || device === void 0 ? void 0 : device.label) || "", accService: device ? [device.uiClass] : [],
|
|
1099
|
+
name, masterAddress, masterPort, logicalAddress, logicalNodeAddress, type: types_1.LinkType.kOverkiz, min, max, nostop };
|
|
1100
|
+
}
|
|
1101
|
+
const accName = (0, HB_1.hbName)(rawAccId);
|
|
1102
|
+
const accService = (0, HB_1.hbService)(rawAccId);
|
|
1103
|
+
return { accId: rawAccId, accName, accService, name, masterAddress, masterPort, logicalAddress, logicalNodeAddress, type: types_1.LinkType.kAccessory, min, max, nostop };
|
|
843
1104
|
}
|
|
844
1105
|
updateLink(inx, link) {
|
|
845
1106
|
if ((inx >= 0) && (inx < this.links.length)) {
|
|
@@ -1471,7 +1732,7 @@ function SmartSocketInit(message){
|
|
|
1471
1732
|
let message;
|
|
1472
1733
|
try {
|
|
1473
1734
|
if (context.action === "new") {
|
|
1474
|
-
return this.ejs("masterDetail", context, { config: types_1.Sanitizers.masterConfig(null), registers: [], nodes: [] });
|
|
1735
|
+
return this.ejs("masterDetail", context, { config: types_1.Sanitizers.masterConfig(null), registers: [], nodes: [], loggedIn: false });
|
|
1475
1736
|
}
|
|
1476
1737
|
else if (context.action === "list") {
|
|
1477
1738
|
return this.ejs("unitList", context, { masters: this.system.masters });
|
|
@@ -1483,7 +1744,7 @@ function SmartSocketInit(message){
|
|
|
1483
1744
|
context.getMaster("id");
|
|
1484
1745
|
const master = this.system.findMaster(context["masterAddress"], context["masterPort"]);
|
|
1485
1746
|
if (master)
|
|
1486
|
-
return this.ejs("masterDetail", context, { nodes: master.nodes, config: master.getConfig(), registers: master.registers });
|
|
1747
|
+
return this.ejs("masterDetail", context, { nodes: master.nodes, config: master.getConfig(), registers: master.registers, loggedIn: master.isLoggedIn });
|
|
1487
1748
|
else
|
|
1488
1749
|
message = "Error: Master not found";
|
|
1489
1750
|
}
|
|
@@ -1510,6 +1771,27 @@ function SmartSocketInit(message){
|
|
|
1510
1771
|
else
|
|
1511
1772
|
message = "Error: Master not found";
|
|
1512
1773
|
}
|
|
1774
|
+
else if (context.action === "changepw") {
|
|
1775
|
+
const master = this.system.findMaster(context.getParam(kAddressOld), context.getParam(kPortOld));
|
|
1776
|
+
if (!master)
|
|
1777
|
+
message = "Error: Master not found";
|
|
1778
|
+
else if (!master.isLoggedIn)
|
|
1779
|
+
message = "Error: not logged in to this master -> refused to change its password";
|
|
1780
|
+
else {
|
|
1781
|
+
const address = context.getParam(kAddress);
|
|
1782
|
+
const port = context.getParam(kPort);
|
|
1783
|
+
const password = context.getParam(kPassword);
|
|
1784
|
+
if (yield (0, udpConfig_1.setHomeServerCfg)(address, password, port)) {
|
|
1785
|
+
const updated = yield this.system.addMaster(context.getParam(kAddressOld), context.getParam(kPortOld), { address, port, password, name: context.getParam(kName),
|
|
1786
|
+
active: context.getParam(kActive) != "N", nodenames: {} });
|
|
1787
|
+
this.updateNodes(updated, context.params.nodes || "", context.params);
|
|
1788
|
+
this.system.writeConfig();
|
|
1789
|
+
message = "Password changed on master and saved";
|
|
1790
|
+
}
|
|
1791
|
+
else
|
|
1792
|
+
message = "Error: master refused the new password";
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1513
1795
|
}
|
|
1514
1796
|
catch (e) {
|
|
1515
1797
|
message = e.toString();
|
|
@@ -61,16 +61,24 @@
|
|
|
61
61
|
<select name="accId" id="accId">
|
|
62
62
|
<option value="" <%= (link.accId == '') ? 'selected' : '' %>>Select an Accessory</option>
|
|
63
63
|
<% for (let a of accessories) { %>
|
|
64
|
-
<%
|
|
65
|
-
|
|
66
|
-
manufacturer
|
|
64
|
+
<% const accManufacturer = (a.manufacturer.indexOf("Coppieters") !== -1) ? "Duotecno" : a.manufacturer; %>
|
|
65
|
+
<% if (accManufacturer != manufacturer) {
|
|
66
|
+
if (manufacturer) { %> </optgroup> <% }
|
|
67
|
+
manufacturer = accManufacturer; %>
|
|
67
68
|
<optgroup label="<%= manufacturer %>">
|
|
68
69
|
<% } %>
|
|
69
|
-
<option value="<%= a.id %>" <%= (link.accId == a.id) ? 'selected' : '' %>><%= a.name %></option>
|
|
70
|
-
<% }
|
|
70
|
+
<option value="<%= a.id %>" <%= ((link.type == 'accessory') && (link.accId == a.id)) ? 'selected' : '' %>><%= a.name %></option>
|
|
71
|
+
<% }
|
|
71
72
|
if (manufacturer) { %> </optgroup> <% } %>
|
|
73
|
+
<% if (overkizDevices && overkizDevices.length) { %>
|
|
74
|
+
<optgroup label="Overkiz">
|
|
75
|
+
<% for (let d of overkizDevices) { %>
|
|
76
|
+
<option value="overkiz:<%= d.deviceURL %>" <%= ((link.type == 'overkiz') && (link.accId == d.deviceURL)) ? 'selected' : '' %>><%= d.label %> (<%= d.uiClass %>)</option>
|
|
77
|
+
<% } %>
|
|
78
|
+
</optgroup>
|
|
79
|
+
<% } %>
|
|
72
80
|
</select>
|
|
73
|
-
<label for="id">Linked
|
|
81
|
+
<label for="id">Linked accessory (Homebridge or Overkiz)</label>
|
|
74
82
|
</div>
|
|
75
83
|
</div>
|
|
76
84
|
|
|
@@ -45,6 +45,16 @@
|
|
|
45
45
|
<label for="password">Password</label>
|
|
46
46
|
</div>
|
|
47
47
|
</div>
|
|
48
|
+
<% if (typeof nodes !== "undefined") { %>
|
|
49
|
+
<div class="row">
|
|
50
|
+
<div class="col s12">
|
|
51
|
+
<button class="btn waves-effect waves-light" type="submit" name="action" value="changepw" <%= loggedIn ? '' : 'disabled' %>>Change Password on Master</button>
|
|
52
|
+
<% if (!loggedIn) { %>
|
|
53
|
+
<span class="grey-text"><small>Only available while logged in to this master.</small></span>
|
|
54
|
+
<% } %>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<% } %>
|
|
48
58
|
<div class="row">
|
|
49
59
|
<div class="col s6">
|
|
50
60
|
Active
|
package/server/views/nav.ejs
CHANGED
|
@@ -16,17 +16,25 @@
|
|
|
16
16
|
<li <% if (request === "masters") { %>class="active"<% } %>><a href="<%= basePath %>/masters">Masters</a></li>
|
|
17
17
|
<li <% if (request === "services") { %>class="active"<% } %>><a href="<%= basePath %>/services">Services</a></li>
|
|
18
18
|
<li <% if (request === "proxy") { %>class="active"<% } %>><a href="<%= basePath %>/proxy">Proxy</a></li>
|
|
19
|
-
<li <% if (request === "links") { %>class="active"<% } %>><a href="
|
|
20
|
-
<li <% if (request === "devices") { %>class="active"<% } %>><a href="
|
|
21
|
-
<% if (hasSmappee) { %><li <% if ((request === "power") && (type === "smappee")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=smappee">Smappee</a></li><% } %>
|
|
22
|
-
<% if (hasP1) { %><li <% if ((request === "power") && (type === "p1")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=p1">P1</a></li><% } %>
|
|
23
|
-
<% if (hasShelly) { %><li <% if ((request === "power") && (type === "shelly")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=shelly">Shelly</a></li><% } %>
|
|
24
|
-
<li <% if (request === "switches") { %>class="active"<% } %>><a href="<%= basePath %>/switches">Switches</a></li>
|
|
19
|
+
<li <% if ((request === "links") || (request === "switches")) { %>class="active"<% } %>><a class="dropdown-trigger" href="#" data-target="linking-dropdown">Linking<i class="material-icons right">arrow_drop_down</i></a></li>
|
|
20
|
+
<li <% if ((request === "devices") || (request === "power")) { %>class="active"<% } %>><a class="dropdown-trigger" href="#" data-target="energy-dropdown">Energy<i class="material-icons right">arrow_drop_down</i></a></li>
|
|
25
21
|
<li <% if (request === "settings") { %>class="active"<% } %>><a href="<%= basePath %>/settings">Settings</a></li>
|
|
26
22
|
</ul>
|
|
27
23
|
</div>
|
|
28
24
|
</nav>
|
|
29
25
|
|
|
26
|
+
<ul id="linking-dropdown" class="dropdown-content">
|
|
27
|
+
<li <% if (request === "switches") { %>class="active"<% } %>><a href="<%= basePath %>/switches">Http Switches</a></li>
|
|
28
|
+
<li <% if (request === "links") { %>class="active"<% } %>><a href="<%= basePath %>/links">Accessory Links</a></li>
|
|
29
|
+
</ul>
|
|
30
|
+
|
|
31
|
+
<ul id="energy-dropdown" class="dropdown-content">
|
|
32
|
+
<li <% if (request === "devices") { %>class="active"<% } %>><a href="<%= basePath %>/devices">Websocket devices</a></li>
|
|
33
|
+
<% if (hasSmappee) { %><li <% if ((request === "power") && (type === "smappee")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=smappee">Smappee</a></li><% } %>
|
|
34
|
+
<% if (hasP1) { %><li <% if ((request === "power") && (type === "p1")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=p1">P1</a></li><% } %>
|
|
35
|
+
<% if (hasShelly) { %><li <% if ((request === "power") && (type === "shelly")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=shelly">Shelly</a></li><% } %>
|
|
36
|
+
</ul>
|
|
37
|
+
|
|
30
38
|
<!-- Mobile Slide-out Menu -->
|
|
31
39
|
<ul class="sidenav" id="mobile-menu">
|
|
32
40
|
<li><div class="user-view">
|
|
@@ -35,16 +43,18 @@
|
|
|
35
43
|
</div></li>
|
|
36
44
|
<li <% if (request === "masters") { %>class="active"<% } %>><a href="<%= basePath %>/masters">Masters</a></li>
|
|
37
45
|
<li <% if (request === "services") { %>class="active"<% } %>><a href="<%= basePath %>/services">Services</a></li>
|
|
46
|
+
<li <% if (request === "proxy") { %>class="active"<% } %>><a href="<%= basePath %>/proxy">Proxy</a></li>
|
|
38
47
|
<li><div class="divider"></div></li>
|
|
39
|
-
<li
|
|
40
|
-
<li <% if (request === "
|
|
41
|
-
<li <% if (request === "
|
|
48
|
+
<li class="subheader">Linking</li>
|
|
49
|
+
<li <% if (request === "switches") { %>class="active"<% } %>><a href="<%= basePath %>/switches">Http</a></li>
|
|
50
|
+
<li <% if (request === "links") { %>class="active"<% } %>><a href="<%= basePath %>/links">Accessory</a></li>
|
|
42
51
|
<li><div class="divider"></div></li>
|
|
52
|
+
<li class="subheader">Energy</li>
|
|
53
|
+
<li <% if (request === "devices") { %>class="active"<% } %>><a href="<%= basePath %>/devices">Julix</a></li>
|
|
43
54
|
<% if (hasSmappee) { %><li <% if ((request === "power") && (type === "smappee")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=smappee">Smappee</a></li><% } %>
|
|
44
55
|
<% if (hasP1) { %><li <% if ((request === "power") && (type === "p1")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=p1">P1 Meter</a></li><% } %>
|
|
45
56
|
<% if (hasShelly) { %><li <% if ((request === "power") && (type === "shelly")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=shelly">Shelly PM</a></li><% } %>
|
|
46
57
|
<li><div class="divider"></div></li>
|
|
47
|
-
<li <% if (request === "proxy") { %>class="active"<% } %>><a href="<%= basePath %>/proxy">Proxy</a></li>
|
|
48
58
|
<li <% if (request === "settings") { %>class="active"<% } %>><a href="<%= basePath %>/settings">Settings</a></li>
|
|
49
59
|
</ul>
|
|
50
60
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
4
|
<%- include('/views/head'); -%>
|
|
5
|
+
<style> .select-wrapper { max-width: 300px !important; } </style>
|
|
5
6
|
</head>
|
|
6
7
|
|
|
7
8
|
<body>
|
|
@@ -14,9 +15,9 @@
|
|
|
14
15
|
<% if (typeof message !== 'undefined' && message) { %>
|
|
15
16
|
<div class="row">
|
|
16
17
|
<div class="col s12">
|
|
17
|
-
<div class="card-panel <%= message.includes('
|
|
18
|
-
<span class="<%= message.includes('
|
|
19
|
-
<i class="material-icons left"><%= message.includes('
|
|
18
|
+
<div class="card-panel <%= message.includes('failed') ? 'orange lighten-4' : 'green lighten-4' %>">
|
|
19
|
+
<span class="<%= message.includes('failed') ? 'orange-text text-darken-4' : 'green-text text-darken-4' %>">
|
|
20
|
+
<i class="material-icons left"><%= message.includes('failed') ? 'info' : 'check_circle' %></i>
|
|
20
21
|
<%= message %>
|
|
21
22
|
</span>
|
|
22
23
|
</div>
|
|
@@ -58,6 +59,71 @@
|
|
|
58
59
|
</div>
|
|
59
60
|
</div>
|
|
60
61
|
|
|
62
|
+
<style>
|
|
63
|
+
/* Materialize floats active labels to translateY(-14px), which sits too
|
|
64
|
+
close to the input value here -- push these up further. Scoped to this
|
|
65
|
+
section only, not a site-wide change. */
|
|
66
|
+
.overkiz-settings .input-field > label.active {
|
|
67
|
+
transform: translateY(-26px) scale(0.8) !important;
|
|
68
|
+
-webkit-transform: translateY(-26px) scale(0.8) !important;
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
71
|
+
<div class="overkiz-settings">
|
|
72
|
+
<h5>Overkiz (Somfy / TaHoma)</h5>
|
|
73
|
+
<div class="row">
|
|
74
|
+
<div class="input-field col s3">
|
|
75
|
+
<select name="service" id="service">
|
|
76
|
+
<option value="local" <%= (overkizConfig.service == 'local') ? 'selected' : '' %>>Local (developer mode)</option>
|
|
77
|
+
<option value="tahoma" <%= (overkizConfig.service == 'tahoma') ? 'selected' : '' %>>Cloud - TaHoma / Somfy Europe</option>
|
|
78
|
+
<option value="somfy_australia" <%= (overkizConfig.service == 'somfy_australia') ? 'selected' : '' %>>Cloud - Somfy Australia</option>
|
|
79
|
+
<option value="somfy_north_america" <%= (overkizConfig.service == 'somfy_north_america') ? 'selected' : '' %>>Cloud - Somfy North America</option>
|
|
80
|
+
<option value="connexoon" <%= (overkizConfig.service == 'connexoon') ? 'selected' : '' %>>Cloud - Connexoon</option>
|
|
81
|
+
<option value="cozytouch" <%= (overkizConfig.service == 'cozytouch') ? 'selected' : '' %>>Cloud - Cozytouch</option>
|
|
82
|
+
<option value="hi_kumo" <%= (overkizConfig.service == 'hi_kumo') ? 'selected' : '' %>>Cloud - Hi Kumo</option>
|
|
83
|
+
</select>
|
|
84
|
+
<label for="service">Service</label>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="input-field col s3">
|
|
88
|
+
<input placeholder="1234-5678-9012 or IP" id="user" name="user" type="text" value="<%= overkizConfig.user %>">
|
|
89
|
+
<label for="user">User (local: gateway PIN or IP -- cloud: account username)</label>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="input-field col s3">
|
|
93
|
+
<input placeholder="leave blank to keep current" id="password" name="password" type="password" value="">
|
|
94
|
+
<label for="password">Password (local: developer-mode token -- cloud: account password)</label>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="input-field col s3">
|
|
98
|
+
<input placeholder="10" id="pollingPeriod" name="pollingPeriod" type="number" min="1" value="<%= overkizConfig.pollingPeriod %>">
|
|
99
|
+
<label for="pollingPeriod">Polling period (sec) -- how often we check for remote/app-triggered changes</label>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="row">
|
|
104
|
+
<div class="col s12">
|
|
105
|
+
<b>Local developer mode</b>: activate it in the Somfy TaHoma app first
|
|
106
|
+
(<a href="https://developer.somfy.com/somfy-tahoma-developer-mode" target="_blank">developer.somfy.com</a>),
|
|
107
|
+
which gives you the gateway PIN and lets you generate a token.<br />
|
|
108
|
+
<b>Cloud mode</b> in Belgium choose: Cloud - TaHoma / Somfy Europe.<br />
|
|
109
|
+
Saving here (re)connects immediately.
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<% if (overkizDevices && overkizDevices.length) { %>
|
|
114
|
+
<table>
|
|
115
|
+
<thead>
|
|
116
|
+
<tr><th>Label</th><th>Type</th><th>deviceURL</th></tr>
|
|
117
|
+
</thead>
|
|
118
|
+
<tbody>
|
|
119
|
+
<% for (let d of overkizDevices) { %>
|
|
120
|
+
<tr><td><%= d.label %></td><td><%= d.uiClass %> (<%= d.widget %>)</td><td><%= d.deviceURL %></td></tr>
|
|
121
|
+
<% } %>
|
|
122
|
+
</tbody>
|
|
123
|
+
</table>
|
|
124
|
+
<% } %>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
61
127
|
<div class="row">
|
|
62
128
|
<div class="col s12">
|
|
63
129
|
<button class="btn waves-effect waves-light" type="submit" name="action" value="save">
|