homebridge-smartsystem 7.1.25 → 7.1.26
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/package.json +1 -1
- package/server/overkiz.js +94 -20
- package/server/smartapp.js +245 -42
- package/server/views/link-details.ejs +14 -6
- 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
|
/////////////
|
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.26",
|
|
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,6 +25,7 @@ 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");
|
|
29
30
|
const HB_1 = require("./HB");
|
|
30
31
|
const HA_API_1 = require("./HA-API");
|
|
@@ -516,6 +517,7 @@ function SmartSocketInit(message){
|
|
|
516
517
|
// Settings //
|
|
517
518
|
//////////////
|
|
518
519
|
doSettings(context) {
|
|
520
|
+
var _a;
|
|
519
521
|
return __awaiter(this, void 0, void 0, function* () {
|
|
520
522
|
let config = this.read("settings");
|
|
521
523
|
let message = "";
|
|
@@ -528,6 +530,9 @@ function SmartSocketInit(message){
|
|
|
528
530
|
// Note: Full restart recommended for mDNS changes to take effect properly
|
|
529
531
|
(0, logger_1.log)("smartapp", "Settings saved. mDNS name updated to: " + config.mdnsName);
|
|
530
532
|
}
|
|
533
|
+
const overkizMessage = yield this.saveOverkizSettings(context);
|
|
534
|
+
if (overkizMessage)
|
|
535
|
+
message += " " + overkizMessage;
|
|
531
536
|
}
|
|
532
537
|
else if (context.action === "restart") {
|
|
533
538
|
context.request = "restart";
|
|
@@ -536,7 +541,9 @@ function SmartSocketInit(message){
|
|
|
536
541
|
else {
|
|
537
542
|
// Just display the settings form
|
|
538
543
|
}
|
|
539
|
-
|
|
544
|
+
const overkizConfig = ((_a = this.power.overkiz) === null || _a === void 0 ? void 0 : _a.config) || types_1.Sanitizers.overkiz(null);
|
|
545
|
+
const overkizDevices = this.power.overkiz ? this.power.overkiz.listDevices() : [];
|
|
546
|
+
return this.ejs("settings", context, { config, message, overkizConfig, overkizDevices });
|
|
540
547
|
});
|
|
541
548
|
}
|
|
542
549
|
scrapeSettings(context) {
|
|
@@ -683,6 +690,7 @@ function SmartSocketInit(message){
|
|
|
683
690
|
(0, logger_1.log)("smartapp", "** error ** missing unit: " + (0, types_1.hex)(link.logicalNodeAddress) + "/" + (0, types_1.hex)(link.logicalAddress) + " **");
|
|
684
691
|
}
|
|
685
692
|
});
|
|
693
|
+
this.attachOverkizListeners();
|
|
686
694
|
(0, HB_1.hbSubscribe)(() => __awaiter(this, void 0, void 0, function* () {
|
|
687
695
|
// Get all values for subscribed accessories (links)
|
|
688
696
|
if (this.links.length && (yield this.checkToken())) {
|
|
@@ -724,42 +732,49 @@ function SmartSocketInit(message){
|
|
|
724
732
|
(0, logger_1.debug)("smartapp", "Received DT value from unit: " + u.getDisplayName() + " -> " + u.value + " / " + u.status + " from unit");
|
|
725
733
|
if (!this.links.length)
|
|
726
734
|
return;
|
|
727
|
-
if
|
|
728
|
-
|
|
735
|
+
// only bother logging into the Homebridge API if we actually have an accessory-type link to serve
|
|
736
|
+
const needsToken = this.links.some(link => link.type === types_1.LinkType.kAccessory);
|
|
737
|
+
const tokenOK = needsToken ? yield this.checkToken() : true;
|
|
729
738
|
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);
|
|
739
|
+
if (!u.isUnit(link.masterAddress, link.masterPort, link.logicalNodeAddress, link.logicalAddress))
|
|
740
|
+
continue;
|
|
741
|
+
if (link.type === types_1.LinkType.kOverkiz) {
|
|
742
|
+
yield this.informOverkizLink(u, link);
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (!tokenOK)
|
|
746
|
+
continue;
|
|
747
|
+
const hbValue = (0, HB_1.dt2hbvalue)(u, link.hbValue, link.min, link.max);
|
|
748
|
+
if (typeof hbValue != "undefined") {
|
|
749
|
+
try {
|
|
750
|
+
if (hbValue === -1) {
|
|
751
|
+
if (link.nostop === "Y") {
|
|
752
|
+
(0, logger_1.debug)("smartapp", "-> Not sending simulated 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName() + " (disabled)");
|
|
749
753
|
}
|
|
750
754
|
else {
|
|
751
|
-
(0, logger_1.
|
|
755
|
+
(0, logger_1.log)("smartapp", "-> Virtual unit send 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
756
|
+
const val = yield (0, HB_1.hbStop)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, this.token);
|
|
757
|
+
if (typeof val != "undefined")
|
|
758
|
+
link.hbValue = val;
|
|
752
759
|
}
|
|
753
760
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
761
|
+
else if (hbValue !== link.hbValue) {
|
|
762
|
+
(0, logger_1.log)("smartapp", "-> Setting HB value for accessory: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName() + " with value " + u.value);
|
|
763
|
+
link.hbValue = hbValue;
|
|
764
|
+
yield (0, HB_1.hbSetState)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, hbValue, this.token);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
(0, logger_1.debug)("smartapp", "-> No change for accessory: " + link.accName + " to " + u.value + " / " + u.status);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
catch (e) {
|
|
771
|
+
if (e instanceof HB_1.HbAuthError) {
|
|
772
|
+
(0, logger_1.err)("smartapp", "informLinks: Homebridge API token expired for accessory " + link.accName + " -> forcing re-login on next update");
|
|
773
|
+
this.token = "";
|
|
774
|
+
(0, config_1.setToken)("");
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
(0, logger_1.err)("smartapp", "informLinks: failed to set HB value for accessory " + link.accName + " -> " + e.toString());
|
|
763
778
|
}
|
|
764
779
|
}
|
|
765
780
|
}
|
|
@@ -767,6 +782,186 @@ function SmartSocketInit(message){
|
|
|
767
782
|
;
|
|
768
783
|
});
|
|
769
784
|
}
|
|
785
|
+
// Duotecno -> Overkiz direction of a kOverkiz link. Mirrors the kAccessory
|
|
786
|
+
// branch above, but talks straight to the Overkiz device instead of going
|
|
787
|
+
// through the Homebridge REST API -- and unlike hbStop(), overkiz.stop()
|
|
788
|
+
// is a genuine 'stop' command, not a simulated one.
|
|
789
|
+
informOverkizLink(u, link) {
|
|
790
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
791
|
+
if (!this.power.overkiz)
|
|
792
|
+
return;
|
|
793
|
+
if (u.isUpDown()) {
|
|
794
|
+
yield this.informOverkizUpDown(u, link);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
// dimmer: dt2hbvalue reports the raw 0-100 intensity directly, no
|
|
798
|
+
// real/virtual distinction to worry about here (unlike up/down below).
|
|
799
|
+
const hbValue = (0, HB_1.dt2hbvalue)(u, link.hbValue, link.min, link.max);
|
|
800
|
+
if ((typeof hbValue === "undefined") || (hbValue === link.hbValue))
|
|
801
|
+
return;
|
|
802
|
+
try {
|
|
803
|
+
link.hbValue = hbValue;
|
|
804
|
+
(0, logger_1.log)("smartapp", "-> Setting Overkiz intensity for: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName());
|
|
805
|
+
yield this.power.overkiz.setIntensity(link.accId, hbValue);
|
|
806
|
+
}
|
|
807
|
+
catch (e) {
|
|
808
|
+
(0, logger_1.err)("smartapp", "informOverkizLink: failed for " + link.accName + " -> " + e.toString());
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
// A Duotecno up/down (kSwitchingMotor) unit reports status two different
|
|
813
|
+
// ways depending on how it's wired in the Duotecno programming:
|
|
814
|
+
// - a "virtual button" duoswitch macro event -> unit.value = 10 (stop) /
|
|
815
|
+
// 11 (up) / 12 (down), a one-shot pulse (this is what dt2hbvalue/HB.ts
|
|
816
|
+
// expects, and is what the existing Homebridge-accessory links rely on).
|
|
817
|
+
// - the real motor's own busy/idle status -> unit.value === unit.status,
|
|
818
|
+
// one of 0 (stopped), 1 (stopped/down), 2 (stopped/up), 3 (busy/down),
|
|
819
|
+
// 4 (busy/up) -- e.g. the "Garage" unit, which has no duoswitch macro
|
|
820
|
+
// wired to it and only ever reports these real values.
|
|
821
|
+
// Handle both: a real motor's busy<->idle transition is translated into
|
|
822
|
+
// up/down/stop the same way a virtual pulse would be.
|
|
823
|
+
informOverkizUpDown(u, link) {
|
|
824
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
825
|
+
if (!this.power.overkiz)
|
|
826
|
+
return;
|
|
827
|
+
const rawValue = Math.round(u.value);
|
|
828
|
+
try {
|
|
829
|
+
if ((rawValue === 10) || (rawValue === 11) || (rawValue === 12)) {
|
|
830
|
+
// virtual duoswitch pulse
|
|
831
|
+
if (rawValue === link.hbValue)
|
|
832
|
+
return;
|
|
833
|
+
link.hbValue = rawValue;
|
|
834
|
+
if (rawValue === 10) {
|
|
835
|
+
if (link.nostop === "Y") {
|
|
836
|
+
(0, logger_1.debug)("smartapp", "-> Not sending 'stop' to Overkiz device: " + link.accName + " (disabled)");
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
(0, logger_1.log)("smartapp", "-> Sending 'stop' to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
840
|
+
yield this.power.overkiz.stop(link.accId);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
else if (rawValue === 11) {
|
|
844
|
+
(0, logger_1.log)("smartapp", "-> Sending up/open to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
845
|
+
yield this.power.overkiz.up(link.accId);
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
(0, logger_1.log)("smartapp", "-> Sending down/close to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
849
|
+
yield this.power.overkiz.down(link.accId);
|
|
850
|
+
}
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
// real motor busy/idle status
|
|
854
|
+
const status = u.status;
|
|
855
|
+
const wasBusy = (link.hbValue === 3) || (link.hbValue === 4);
|
|
856
|
+
const isBusy = (status === 3) || (status === 4);
|
|
857
|
+
if (isBusy && (status !== link.hbValue)) {
|
|
858
|
+
link.hbValue = status;
|
|
859
|
+
if (status === 4) {
|
|
860
|
+
(0, logger_1.log)("smartapp", "-> Sending up/open to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
861
|
+
yield this.power.overkiz.up(link.accId);
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
(0, logger_1.log)("smartapp", "-> Sending down/close to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
865
|
+
yield this.power.overkiz.down(link.accId);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
else if (!isBusy && wasBusy) {
|
|
869
|
+
link.hbValue = status;
|
|
870
|
+
if (link.nostop === "Y") {
|
|
871
|
+
(0, logger_1.debug)("smartapp", "-> Not sending 'stop' to Overkiz device: " + link.accName + " (disabled)");
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
(0, logger_1.log)("smartapp", "-> Sending 'stop' to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
875
|
+
yield this.power.overkiz.stop(link.accId);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
link.hbValue = status;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
catch (e) {
|
|
883
|
+
(0, logger_1.err)("smartapp", "informOverkizUpDown: failed for " + link.accName + " -> " + e.toString());
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
attachOverkizListeners() {
|
|
888
|
+
if (!this.power.overkiz || (this.power.overkiz === this.attachedOverkiz))
|
|
889
|
+
return;
|
|
890
|
+
this.attachedOverkiz = this.power.overkiz;
|
|
891
|
+
this.power.overkiz.emitter.on("state", (deviceURL, unitState) => {
|
|
892
|
+
for (const link of this.links) {
|
|
893
|
+
if ((link.type !== types_1.LinkType.kOverkiz) || (link.accId !== deviceURL) || !link.unit)
|
|
894
|
+
continue;
|
|
895
|
+
if (unitState === link.dtValue)
|
|
896
|
+
continue; // no repeat
|
|
897
|
+
link.dtValue = unitState;
|
|
898
|
+
if ((unitState === types_1.UnitState.kOpen) || (unitState === types_1.UnitState.kOpening)) {
|
|
899
|
+
(0, logger_1.log)("smartapp", "-> Overkiz reports " + types_1.UnitState[unitState] + " for: " + link.accName + " -> setting Duotecno unit " + link.unit.getDisplayName() + " to open");
|
|
900
|
+
link.unit.setState(types_1.UnitMotorCmd.kOpen);
|
|
901
|
+
}
|
|
902
|
+
else if ((unitState === types_1.UnitState.kClosed) || (unitState === types_1.UnitState.kClosing)) {
|
|
903
|
+
(0, logger_1.log)("smartapp", "-> Overkiz reports " + types_1.UnitState[unitState] + " for: " + link.accName + " -> setting Duotecno unit " + link.unit.getDisplayName() + " to close");
|
|
904
|
+
link.unit.setState(types_1.UnitMotorCmd.kClose);
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
// kStopped: Overkiz reports the device sitting still somewhere in
|
|
908
|
+
// the middle (e.g. stopped mid-way via the Somfy app/remote) --
|
|
909
|
+
// forward a genuine stop, same as the Duotecno -> Overkiz direction.
|
|
910
|
+
if (link.nostop === "Y") {
|
|
911
|
+
(0, logger_1.debug)("smartapp", "-> Not forwarding Overkiz 'stopped' for: " + link.accName + " (disabled)");
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
(0, logger_1.log)("smartapp", "-> Overkiz reports stopped for: " + link.accName + " -> setting Duotecno unit " + link.unit.getDisplayName() + " to stop");
|
|
915
|
+
link.unit.setState(types_1.UnitMotorCmd.kStop);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
this.power.overkiz.emitter.on("intensity", (deviceURL, intensity) => {
|
|
921
|
+
for (const link of this.links) {
|
|
922
|
+
if ((link.type !== types_1.LinkType.kOverkiz) || (link.accId !== deviceURL) || !link.unit)
|
|
923
|
+
continue;
|
|
924
|
+
const { dtValue, dtTarget } = (0, HB_1.hb2dtvalue)(link.unit, intensity);
|
|
925
|
+
if (dtValue !== link.dtValue) {
|
|
926
|
+
(0, logger_1.log)("smartapp", "-> Setting value of Duotecno unit: " + link.unit.getDisplayName() + " to " + dtTarget + " from Overkiz intensity " + intensity + " of: " + link.accName);
|
|
927
|
+
link.dtValue = dtValue;
|
|
928
|
+
link.unit.setState(dtTarget);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
//////////////////////////////
|
|
934
|
+
// Overkiz (Somfy/TaHoma) //
|
|
935
|
+
//////////////////////////////
|
|
936
|
+
// Saves the Overkiz fields living on the Settings page and (re)connects.
|
|
937
|
+
// Only attempts a connection when credentials are actually present, so
|
|
938
|
+
// saving the rest of the Settings form doesn't produce a spurious
|
|
939
|
+
// "connection failed" for installs that don't use Overkiz at all.
|
|
940
|
+
saveOverkizSettings(context) {
|
|
941
|
+
var _a, _b;
|
|
942
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
943
|
+
const service = context.getParam({ name: "service", type: "string", default: "local" });
|
|
944
|
+
const user = context.getParam({ name: "user", type: "string", default: "" });
|
|
945
|
+
const submittedPassword = context.getParam({ name: "password", type: "string", default: "" });
|
|
946
|
+
// blank password field means "keep the current one"
|
|
947
|
+
const password = submittedPassword || ((_b = (_a = this.power.overkiz) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.password) || "";
|
|
948
|
+
const pollingPeriod = context.getParam({ name: "pollingPeriod", type: "integer", default: 10 });
|
|
949
|
+
if (!this.power.overkiz)
|
|
950
|
+
this.power.overkiz = new overkiz_1.Overkiz(this.system);
|
|
951
|
+
this.power.overkiz.config = types_1.Sanitizers.overkiz({ service, user, password, pollingPeriod });
|
|
952
|
+
this.power.overkiz.writeConfig();
|
|
953
|
+
if (!user || !password)
|
|
954
|
+
return "";
|
|
955
|
+
try {
|
|
956
|
+
yield this.power.overkiz.connect();
|
|
957
|
+
this.attachOverkizListeners();
|
|
958
|
+
return "Overkiz connected successfully -> " + this.power.overkiz.listDevices().length + " device(s) found.";
|
|
959
|
+
}
|
|
960
|
+
catch (e) {
|
|
961
|
+
return "Overkiz connection failed: " + (e.message || e.toString());
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
}
|
|
770
965
|
checkToken() {
|
|
771
966
|
return __awaiter(this, void 0, void 0, function* () {
|
|
772
967
|
if (!this.token) {
|
|
@@ -793,6 +988,7 @@ function SmartSocketInit(message){
|
|
|
793
988
|
return __awaiter(this, void 0, void 0, function* () {
|
|
794
989
|
let inx = parseInt(context.id);
|
|
795
990
|
const accessories = yield this.getAccessories();
|
|
991
|
+
const overkizDevices = this.power.overkiz ? this.power.overkiz.listDevices() : [];
|
|
796
992
|
const units = this.system.allUsedUnits();
|
|
797
993
|
let message;
|
|
798
994
|
try {
|
|
@@ -800,11 +996,11 @@ function SmartSocketInit(message){
|
|
|
800
996
|
this.links.push(Object.assign(Object.assign({}, types_1.kEmptyLink), { type: types_1.LinkType.kAccessory }));
|
|
801
997
|
inx = this.links.length - 1;
|
|
802
998
|
return this.ejs("linkDetail", context, { link: this.links[inx], id: inx,
|
|
803
|
-
masters: this.system.masters, accessories, units });
|
|
999
|
+
masters: this.system.masters, accessories, overkizDevices, units });
|
|
804
1000
|
}
|
|
805
1001
|
else if (context.action === "edit") {
|
|
806
1002
|
return this.ejs("linkDetail", context, { link: this.links[inx], id: inx,
|
|
807
|
-
masters: this.system.masters, accessories, units });
|
|
1003
|
+
masters: this.system.masters, accessories, overkizDevices, units });
|
|
808
1004
|
}
|
|
809
1005
|
else if (context.action === "delete") {
|
|
810
1006
|
this.deleteLink(inx);
|
|
@@ -824,22 +1020,29 @@ function SmartSocketInit(message){
|
|
|
824
1020
|
});
|
|
825
1021
|
}
|
|
826
1022
|
scrapeLink(context) {
|
|
1023
|
+
var _a;
|
|
827
1024
|
// don't take the name of the unit
|
|
828
1025
|
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
1026
|
const name = context.getParam({ name: "name", type: "string", default: "--" });
|
|
832
1027
|
// max & min values
|
|
833
1028
|
const min = context.getParam({ name: "min", type: "integer", default: 0 });
|
|
834
1029
|
const max = context.getParam({ name: "max", type: "integer", default: 100 });
|
|
835
1030
|
// don't forward the simulated 'stop' of an up/down link to the accessory
|
|
836
1031
|
const nostop = context.getParam({ name: "nostop", type: "string", default: "N" });
|
|
837
|
-
//
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
1032
|
+
// the accessory dropdown offers both Homebridge accessories and Overkiz
|
|
1033
|
+
// devices in one list; an Overkiz option's value is prefixed so we know
|
|
1034
|
+
// which provider -- and hence Link type -- the picked id belongs to.
|
|
1035
|
+
const rawAccId = context.getParam({ name: "accId", type: "string", default: "" });
|
|
1036
|
+
const kOverkizPrefix = "overkiz:";
|
|
1037
|
+
if (rawAccId.startsWith(kOverkizPrefix)) {
|
|
1038
|
+
const accId = rawAccId.substring(kOverkizPrefix.length);
|
|
1039
|
+
const device = (_a = this.power.overkiz) === null || _a === void 0 ? void 0 : _a.listDevices().find(d => d.deviceURL === accId);
|
|
1040
|
+
return { accId, accName: (device === null || device === void 0 ? void 0 : device.label) || "", accService: device ? [device.uiClass] : [],
|
|
1041
|
+
name, masterAddress, masterPort, logicalAddress, logicalNodeAddress, type: types_1.LinkType.kOverkiz, min, max, nostop };
|
|
1042
|
+
}
|
|
1043
|
+
const accName = (0, HB_1.hbName)(rawAccId);
|
|
1044
|
+
const accService = (0, HB_1.hbService)(rawAccId);
|
|
1045
|
+
return { accId: rawAccId, accName, accService, name, masterAddress, masterPort, logicalAddress, logicalNodeAddress, type: types_1.LinkType.kAccessory, min, max, nostop };
|
|
843
1046
|
}
|
|
844
1047
|
updateLink(inx, link) {
|
|
845
1048
|
if ((inx >= 0) && (inx < this.links.length)) {
|
|
@@ -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
|
|
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 Switches</a></li>
|
|
50
|
+
<li <% if (request === "links") { %>class="active"<% } %>><a href="<%= basePath %>/links">Accessory Links</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">Websocket devices</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">
|