homebridge-smartsystem 6.5.0 → 6.6.0
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/README.md +7 -0
- package/config.json +21 -9
- package/duotecno/api.ts +2 -0
- package/duotecno/types.js +3 -2
- package/duotecno/types.ts +6 -2
- package/package.json +4 -2
- package/server/platform.ts +1 -1
- package/server/smartapp.js +165 -69
- package/server/smartapp.ts +161 -65
- package/server/views/switch-details.ejs +4 -3
package/README.md
CHANGED
|
@@ -287,6 +287,13 @@ Homebridge UI version
|
|
|
287
287
|
- 1: ask/show protocol version -> publish
|
|
288
288
|
- 2 & 3: test on protocol version, not firmware for using registers, due to bug in master firmware pre v101.x
|
|
289
289
|
|
|
290
|
+
### v6.5.0 - 18-05-2025 - Homebridge 2.0
|
|
291
|
+
- 0: published
|
|
292
|
+
|
|
293
|
+
### v6.6.0 - 21-05-2025 - OpenHAB
|
|
294
|
+
- 0: Extra "Switch" type openHAB, uses username/password to ask for a token
|
|
295
|
+
|
|
296
|
+
|
|
290
297
|
## Hardware
|
|
291
298
|
A Raspberry Pi that connects to a Duotecno IP Node
|
|
292
299
|
and (if configured) to a Smappee Infinity (power and plugs)
|
package/config.json
CHANGED
|
@@ -250,21 +250,21 @@
|
|
|
250
250
|
},
|
|
251
251
|
{
|
|
252
252
|
"logicalNodeAddress": 3,
|
|
253
|
-
"logicalAddress":
|
|
253
|
+
"logicalAddress": 31,
|
|
254
254
|
"masterAddress": "192.168.0.97",
|
|
255
255
|
"masterPort": 5001,
|
|
256
|
-
"name": "
|
|
257
|
-
"displayName": "
|
|
256
|
+
"name": "Somfy Windowcovering",
|
|
257
|
+
"displayName": "Somfy Windowcovering",
|
|
258
258
|
"extendedType": 0,
|
|
259
259
|
"nomacro": "N",
|
|
260
260
|
"nostop": "N",
|
|
261
|
-
"unitName": "
|
|
261
|
+
"unitName": "Somfy Dimmer",
|
|
262
262
|
"value": 0,
|
|
263
|
-
"data": "
|
|
264
|
-
"header": "
|
|
263
|
+
"data": "{CurrentPosition: $}",
|
|
264
|
+
"header": "Pi:GMTecn",
|
|
265
265
|
"method": "PUT",
|
|
266
|
-
"type": "
|
|
267
|
-
"plug": "http://
|
|
266
|
+
"type": "ohapi",
|
|
267
|
+
"plug": "http://Pi:GMTecno@192.168.0.99:8581/api/accessory/06e5b66f78fe0136ea075e742a7fb388587329a4717393b21aaa180083305704"
|
|
268
268
|
}
|
|
269
269
|
],
|
|
270
270
|
"debug": false
|
|
@@ -368,6 +368,18 @@
|
|
|
368
368
|
"used": "Y",
|
|
369
369
|
"group": 0
|
|
370
370
|
},
|
|
371
|
+
{
|
|
372
|
+
"logicalNodeAddress": 3,
|
|
373
|
+
"logicalAddress": 31,
|
|
374
|
+
"masterAddress": "192.168.0.97",
|
|
375
|
+
"masterPort": 5001,
|
|
376
|
+
"name": "2|Dimmer",
|
|
377
|
+
"displayName": "Somfy Dimmer",
|
|
378
|
+
"extendedType": 0,
|
|
379
|
+
"active": "Y",
|
|
380
|
+
"used": "Y",
|
|
381
|
+
"group": 0
|
|
382
|
+
},
|
|
371
383
|
{
|
|
372
384
|
"logicalNodeAddress": 3,
|
|
373
385
|
"logicalAddress": 30,
|
|
@@ -375,7 +387,7 @@
|
|
|
375
387
|
"masterPort": 5001,
|
|
376
388
|
"name": "Blue LED",
|
|
377
389
|
"displayName": "Blue LED",
|
|
378
|
-
"extendedType":
|
|
390
|
+
"extendedType": 0,
|
|
379
391
|
"active": "N",
|
|
380
392
|
"used": "Y",
|
|
381
393
|
"group": 0
|
package/duotecno/api.ts
CHANGED
package/duotecno/types.js
CHANGED
|
@@ -137,6 +137,7 @@ var SwitchType;
|
|
|
137
137
|
SwitchType["kOhSwitch"] = "oswitch";
|
|
138
138
|
SwitchType["kOhDimmer"] = "odimmer";
|
|
139
139
|
SwitchType["kOhUpDown"] = "oupdown";
|
|
140
|
+
SwitchType["kohAPI"] = "ohapi";
|
|
140
141
|
SwitchType["kSomfy"] = "somfy";
|
|
141
142
|
})(SwitchType = exports.SwitchType || (exports.SwitchType = {}));
|
|
142
143
|
;
|
|
@@ -150,7 +151,7 @@ exports.kEmptyRule = {
|
|
|
150
151
|
actions: [Object.assign(Object.assign({}, exports.kEmptyUnit), { value: false }), Object.assign(Object.assign({}, exports.kEmptyUnit), { value: 50 }), Object.assign(Object.assign({}, exports.kEmptyUnit), { value: true })]
|
|
151
152
|
};
|
|
152
153
|
;
|
|
153
|
-
exports.kEmptySwitch = Object.assign(Object.assign({}, exports.kEmptyUnit), { plug: 0, type: SwitchType.kNoType, unitName: "", name: "", data: "", method: "GET", header: "", nomacro: "N", nostop: "N" });
|
|
154
|
+
exports.kEmptySwitch = Object.assign(Object.assign({}, exports.kEmptyUnit), { plug: 0, type: SwitchType.kNoType, unitName: "", name: "", data: "", method: "GET", header: "", username: "", password: "", nomacro: "N", nostop: "N" });
|
|
154
155
|
;
|
|
155
156
|
;
|
|
156
157
|
;
|
|
@@ -496,7 +497,7 @@ function char(ascii) {
|
|
|
496
497
|
}
|
|
497
498
|
exports.char = char;
|
|
498
499
|
function two(n) {
|
|
499
|
-
return (n < 10) ? ("0" + n) : n.toString();
|
|
500
|
+
return (+n < 10) ? ("0" + n) : n.toString();
|
|
500
501
|
}
|
|
501
502
|
exports.two = two;
|
|
502
503
|
function hex(n) {
|
package/duotecno/types.ts
CHANGED
|
@@ -203,6 +203,7 @@ export enum SwitchType {
|
|
|
203
203
|
kOhSwitch = "oswitch",
|
|
204
204
|
kOhDimmer = "odimmer",
|
|
205
205
|
kOhUpDown = "oupdown",
|
|
206
|
+
kohAPI = "ohapi",
|
|
206
207
|
kSomfy = "somfy"
|
|
207
208
|
};
|
|
208
209
|
|
|
@@ -247,6 +248,8 @@ export interface Switch extends UnitDef {
|
|
|
247
248
|
data: string,
|
|
248
249
|
header: string,
|
|
249
250
|
method: string,
|
|
251
|
+
username?: string;
|
|
252
|
+
password?: string;
|
|
250
253
|
unit?: Unit;
|
|
251
254
|
status?: number | boolean,
|
|
252
255
|
value?: number | boolean | string,
|
|
@@ -254,7 +257,8 @@ export interface Switch extends UnitDef {
|
|
|
254
257
|
nostop: string;
|
|
255
258
|
};
|
|
256
259
|
export const kEmptySwitch: Switch =
|
|
257
|
-
{ ...kEmptyUnit, plug: 0, type: SwitchType.kNoType, unitName: "", name: "", data: "", method: "GET",
|
|
260
|
+
{ ...kEmptyUnit, plug: 0, type: SwitchType.kNoType, unitName: "", name: "", data: "", method: "GET",
|
|
261
|
+
header: "", username: "", password: "", nomacro: "N", nostop: "N"} as const;
|
|
258
262
|
|
|
259
263
|
|
|
260
264
|
export interface SmartAppConfig extends BaseConfig {
|
|
@@ -700,7 +704,7 @@ export function char(ascii: number): string {
|
|
|
700
704
|
|
|
701
705
|
|
|
702
706
|
export function two(n: number | string) {
|
|
703
|
-
return (n < 10) ? ("0" + n) : n.toString();
|
|
707
|
+
return (+n < 10) ? ("0" + n) : n.toString();
|
|
704
708
|
}
|
|
705
709
|
export function hex(n: number): string {
|
|
706
710
|
n = Math.floor(n);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-smartsystem",
|
|
3
3
|
"displayname": "Duotecno Bridge",
|
|
4
|
-
"version": "6.
|
|
4
|
+
"version": "6.6.0",
|
|
5
5
|
"description": "SmartServer (Proxy Websockets to TCP sockets, Smappee MQTT, Duotecno IP Nodes, Homekit interface)",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"author": "Johan Coppieters",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"proxy",
|
|
24
24
|
"duotecno",
|
|
25
25
|
"homebridge",
|
|
26
|
-
"homebridge-plugin"
|
|
26
|
+
"homebridge-plugin",
|
|
27
|
+
"openHAB",
|
|
28
|
+
"Somfy"
|
|
27
29
|
],
|
|
28
30
|
"repository": {
|
|
29
31
|
"type": "git",
|
package/server/platform.ts
CHANGED
|
@@ -242,7 +242,7 @@ export class Platform extends Base {
|
|
|
242
242
|
case UnitExtendedType.kMood:
|
|
243
243
|
case UnitExtendedType.kCondition:
|
|
244
244
|
case UnitExtendedType.kInput:
|
|
245
|
-
|
|
245
|
+
this.accessoryList.push( new Mood(this.homebridgeAPI, unit) );
|
|
246
246
|
break;
|
|
247
247
|
|
|
248
248
|
case UnitExtendedType.kTemperature:
|
package/server/smartapp.js
CHANGED
|
@@ -26,7 +26,6 @@ const protocol_1 = require("../duotecno/protocol");
|
|
|
26
26
|
const http = require("http");
|
|
27
27
|
const somfy = require("./somfy");
|
|
28
28
|
const fs_1 = require("fs");
|
|
29
|
-
const api_1 = require("../duotecno/api");
|
|
30
29
|
const config_1 = require("../duotecno/config");
|
|
31
30
|
const kMaster = { name: "master", type: "string", default: "0.0.0.0:5001" };
|
|
32
31
|
const kAddress = { name: "address", type: "string", default: "0.0.0.0" };
|
|
@@ -51,6 +50,7 @@ var ContentType;
|
|
|
51
50
|
ContentType[ContentType["form"] = 0] = "form";
|
|
52
51
|
ContentType[ContentType["plain"] = 1] = "plain";
|
|
53
52
|
ContentType[ContentType["none"] = 2] = "none";
|
|
53
|
+
ContentType[ContentType["json"] = 3] = "json";
|
|
54
54
|
})(ContentType || (ContentType = {}));
|
|
55
55
|
;
|
|
56
56
|
class SmartApp extends webapp_1.WebApp {
|
|
@@ -117,12 +117,9 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
117
117
|
this.token = "";
|
|
118
118
|
(0, config_1.setToken)("");
|
|
119
119
|
try {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const respBody = JSON.parse(resp);
|
|
124
|
-
if (respBody.access_token) {
|
|
125
|
-
this.token = respBody.access_token;
|
|
120
|
+
const access_token = yield this.habLogin(this.config.apiHost + ":" + this.config.apiPort, user, pw);
|
|
121
|
+
if (access_token) {
|
|
122
|
+
this.token = access_token;
|
|
126
123
|
(0, config_1.setToken)(this.token);
|
|
127
124
|
return true;
|
|
128
125
|
}
|
|
@@ -541,6 +538,9 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
541
538
|
else if (swtch.type === types_1.SwitchType.kHTTPUpDown) {
|
|
542
539
|
this.httpUpDown(swtch);
|
|
543
540
|
}
|
|
541
|
+
else if (swtch.type === types_1.SwitchType.kohAPI) {
|
|
542
|
+
this.ohAPI(swtch);
|
|
543
|
+
}
|
|
544
544
|
else if (swtch.type === types_1.SwitchType.kOhSwitch) {
|
|
545
545
|
this.ohSwitch(swtch);
|
|
546
546
|
}
|
|
@@ -640,80 +640,176 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
640
640
|
(0, logger_1.debug)("smartapp", "Dimmer(" + !!swtch.unit.status + "," + swtch.unit.value + ") -> " + req + " + " + data);
|
|
641
641
|
this.wrequest(req, swtch.method, data, ContentType.form, swtch.header);
|
|
642
642
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
'Content-Type': 'text/plain',
|
|
657
|
-
'Accept': 'application/json'
|
|
658
|
-
};
|
|
659
|
-
}
|
|
643
|
+
////////////////////////
|
|
644
|
+
// http request stuff //
|
|
645
|
+
////////////////////////
|
|
646
|
+
habLogin(url, username, password) {
|
|
647
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
648
|
+
if (url.indexOf("://") === -1)
|
|
649
|
+
url = "http://" + url;
|
|
650
|
+
const header = { "Content-Type": "application/json", "accept": "*/*" };
|
|
651
|
+
const data = { username, password };
|
|
652
|
+
const respBody = yield this.wrequest(url + "/api/auth/login", 'POST', JSON.stringify(data), ContentType.json, JSON.stringify(header));
|
|
653
|
+
if (!respBody.access_token) {
|
|
654
|
+
(0, logger_1.err)("hab", "habLogin: failed -> " + JSON.stringify(respBody));
|
|
655
|
+
return "";
|
|
660
656
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
657
|
+
else {
|
|
658
|
+
return respBody.access_token;
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
habCheckToken(token, url, username, password) {
|
|
663
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
664
|
+
if (!token || !url) {
|
|
665
|
+
(0, logger_1.err)("hab", "habCheckToken: failed -> no token or no url");
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
const header = {
|
|
669
|
+
"Content-Type": "application/json", "accept": "*/*",
|
|
670
|
+
"access_token": token
|
|
671
|
+
};
|
|
672
|
+
const target = url.split("/api")[0];
|
|
673
|
+
const resp = yield this.wrequest(target + "/api/auth/check", 'GET', "", ContentType.none, header);
|
|
674
|
+
if (resp['status'] === "OK") {
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
if (username && password) {
|
|
679
|
+
this.token = yield this.habLogin(target, username, password);
|
|
680
|
+
return !!this.token;
|
|
672
681
|
}
|
|
673
682
|
else {
|
|
674
|
-
|
|
675
|
-
|
|
683
|
+
(0, logger_1.err)("hab", "habCheckToken: failed -> no username or password");
|
|
684
|
+
return false;
|
|
676
685
|
}
|
|
677
686
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
//
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
wrequest(url, method = "GET", formdata = "", contentType = ContentType.form, header = "") {
|
|
690
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
691
|
+
if (url.indexOf("://") === -1)
|
|
692
|
+
url = "http://" + url;
|
|
693
|
+
return new Promise((resolve, reject) => {
|
|
694
|
+
try {
|
|
695
|
+
// const data = querystring.stringify(formdata);
|
|
696
|
+
const options = { method };
|
|
697
|
+
if (formdata) {
|
|
698
|
+
if (contentType === ContentType.form) {
|
|
699
|
+
options.headers = {
|
|
700
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
701
|
+
'Content-Length': Buffer.byteLength(formdata)
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
else if (contentType === ContentType.plain) {
|
|
705
|
+
options.headers = {
|
|
706
|
+
'Content-Type': 'text/plain',
|
|
707
|
+
'Accept': 'application/json'
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
else if (contentType === ContentType.json) {
|
|
711
|
+
options.headers = {
|
|
712
|
+
'Content-Type': 'application/json',
|
|
713
|
+
'Accept': '*/*'
|
|
714
|
+
};
|
|
715
|
+
}
|
|
695
716
|
}
|
|
696
717
|
;
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
718
|
+
// header provide as string or object
|
|
719
|
+
if (header) {
|
|
720
|
+
if (!options.headers)
|
|
721
|
+
options.headers = {};
|
|
722
|
+
if (typeof header === "string") {
|
|
723
|
+
if (header.substring(0, 1) === "{") {
|
|
724
|
+
try {
|
|
725
|
+
options.headers = Object.assign(Object.assign({}, options.headers), JSON.parse(header));
|
|
726
|
+
}
|
|
727
|
+
catch (e) {
|
|
728
|
+
(0, logger_1.err)("smartapp", "error parsing :" + header + " -> " + JSON.stringify(e));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
const s = header.split(":");
|
|
733
|
+
options.headers[s[0]] = s[1];
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
// header is an object
|
|
738
|
+
options.headers = Object.assign(Object.assign({}, options.headers), header);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
(0, logger_1.debug)("smartapp", "Sending to " + url + " with options: " + JSON.stringify(options));
|
|
742
|
+
const req = http.request(url, options, res => {
|
|
743
|
+
let resp = "";
|
|
744
|
+
// res.setEncoding('utf8');
|
|
745
|
+
res.on('error', error => {
|
|
746
|
+
(0, logger_1.log)("smartapp", "The request to " + url + ", error: " + (error ? error.message : "unknown"));
|
|
747
|
+
reject(error);
|
|
748
|
+
});
|
|
749
|
+
res.on('data', chunk => {
|
|
750
|
+
resp += chunk;
|
|
751
|
+
});
|
|
752
|
+
res.on('end', () => {
|
|
753
|
+
// try to make something out of it...
|
|
754
|
+
try {
|
|
755
|
+
const x = JSON.parse(resp);
|
|
756
|
+
if ((x.type === "Buffer") && x.data)
|
|
757
|
+
x.data = x.data.toString();
|
|
758
|
+
(0, logger_1.log)("smartapp", "http " + method + " -> " + res.statusCode + ": " + url + " = " + JSON.stringify(x));
|
|
759
|
+
resolve(x);
|
|
760
|
+
}
|
|
761
|
+
catch (e) {
|
|
762
|
+
(0, logger_1.err)("smartapp", "http " + method + " -> " + res.statusCode + ": " + url + " = " + resp);
|
|
763
|
+
resolve({ status: "NOK", result: resp });
|
|
764
|
+
}
|
|
765
|
+
;
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
if (formdata) {
|
|
769
|
+
req.write(formdata);
|
|
770
|
+
}
|
|
771
|
+
req.on("error", (e) => {
|
|
772
|
+
(0, logger_1.log)("smartapp", "The request to " + url + ", error: " + (e ? e.message : "unknown"));
|
|
773
|
+
});
|
|
774
|
+
req.on("timeout", (e) => {
|
|
775
|
+
(0, logger_1.log)("smartapp", "The request to " + url + ", timeout: " + (e ? e.message : "unknown"));
|
|
776
|
+
});
|
|
777
|
+
req.end();
|
|
778
|
+
}
|
|
779
|
+
catch (e) {
|
|
780
|
+
(0, logger_1.log)("smartapp", "** http error ** " + (e ? e.message : "unknown") + " **");
|
|
781
|
+
reject(e);
|
|
782
|
+
}
|
|
707
783
|
});
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
catch (e) {
|
|
711
|
-
(0, logger_1.log)("smartapp", "** http error ** " + (e ? e.message : "unknown") + " **");
|
|
712
|
-
}
|
|
784
|
+
});
|
|
713
785
|
}
|
|
714
786
|
//////////////
|
|
715
787
|
// open HAB //
|
|
716
788
|
//////////////
|
|
789
|
+
ohAPI(swtch) {
|
|
790
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
791
|
+
const str = "" + swtch.plug;
|
|
792
|
+
// http://username:password@example.com/api/...
|
|
793
|
+
const protocol = str.indexOf("://");
|
|
794
|
+
const login = (protocol === -1) ? str : str.substring(protocol + 3);
|
|
795
|
+
// username:password@example.com/api/...
|
|
796
|
+
const urlParts = login.split("@");
|
|
797
|
+
const userPw = urlParts[0].split(":");
|
|
798
|
+
let req = this.makeVariableURL(urlParts[1], !!swtch.unit.status, +swtch.unit.value);
|
|
799
|
+
req = ((protocol === -1) ? "http" : str.substring(0, protocol)) + "://" + req;
|
|
800
|
+
(0, logger_1.debug)("smartapp", "OH-API(" + !!swtch.unit.status + ", " + swtch.unit.value + ") -> " + req);
|
|
801
|
+
if (userPw.length !== 2) {
|
|
802
|
+
// no user/pw given, try the API call without a token
|
|
803
|
+
yield this.wrequest(req, swtch.method, this.makeVariableURL(swtch.data, !!swtch.unit.status, +swtch.unit.value), ContentType.json, swtch.header);
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
// user/pw given -> check token
|
|
807
|
+
if (yield this.habCheckToken(this.token, req, userPw[0], userPw[1])) {
|
|
808
|
+
yield this.wrequest(req, swtch.method, this.makeVariableURL(swtch.data, !!swtch.unit.status, +swtch.unit.value), ContentType.json, swtch.header);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
717
813
|
ohSwitch(swtch) {
|
|
718
814
|
const req = this.makeVariableURL(swtch.plug, !!swtch.unit.status, +swtch.unit.value);
|
|
719
815
|
(0, logger_1.debug)("smartapp", "OH-Switch(" + !!swtch.unit.status + ") -> " + req);
|
package/server/smartapp.ts
CHANGED
|
@@ -21,7 +21,6 @@ import * as somfy from "./somfy";
|
|
|
21
21
|
import { existsSync, truncateSync, writeFileSync } from 'fs';
|
|
22
22
|
import { Shelly } from './shelly';
|
|
23
23
|
import { P1 } from './p1';
|
|
24
|
-
import { post } from "../duotecno/api";
|
|
25
24
|
import { setToken } from "../duotecno/config";
|
|
26
25
|
import { PowerBase } from "./powerbase";
|
|
27
26
|
|
|
@@ -47,7 +46,7 @@ const isMac = existsSync("/Volumes");
|
|
|
47
46
|
const kDHCPConfigFile = isMac ? "./config.dhcpcd" : "/etc/dhcpcd.conf";
|
|
48
47
|
|
|
49
48
|
|
|
50
|
-
enum ContentType {form, plain, none};
|
|
49
|
+
enum ContentType {form, plain, none, json};
|
|
51
50
|
|
|
52
51
|
export interface Power {
|
|
53
52
|
smappee?: Smappee;
|
|
@@ -137,12 +136,9 @@ export class SmartApp extends WebApp {
|
|
|
137
136
|
this.token = "";
|
|
138
137
|
setToken("");
|
|
139
138
|
try {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const respBody = JSON.parse(resp);
|
|
144
|
-
if (respBody.access_token) {
|
|
145
|
-
this.token = respBody.access_token;
|
|
139
|
+
const access_token = await this.habLogin(this.config.apiHost +":"+ this.config.apiPort, user, pw);
|
|
140
|
+
if (access_token) {
|
|
141
|
+
this.token = access_token;
|
|
146
142
|
setToken(this.token);
|
|
147
143
|
return true;
|
|
148
144
|
}
|
|
@@ -554,6 +550,8 @@ export class SmartApp extends WebApp {
|
|
|
554
550
|
}
|
|
555
551
|
|
|
556
552
|
|
|
553
|
+
setSwitch(inx: number, newstate?: boolean, newvalue?: number)
|
|
554
|
+
setSwitch(inx: Switch, newstate?: boolean, newvalue?: number)
|
|
557
555
|
setSwitch(inx: number | Switch, newstate?: boolean, newvalue?: number) {
|
|
558
556
|
// find the switch if an index is given
|
|
559
557
|
let swtch = null;
|
|
@@ -594,6 +592,9 @@ export class SmartApp extends WebApp {
|
|
|
594
592
|
} else if (swtch.type === SwitchType.kHTTPUpDown) {
|
|
595
593
|
this.httpUpDown(swtch);
|
|
596
594
|
|
|
595
|
+
} else if (swtch.type === SwitchType.kohAPI) {
|
|
596
|
+
this.ohAPI(swtch);
|
|
597
|
+
|
|
597
598
|
} else if (swtch.type === SwitchType.kOhSwitch) {
|
|
598
599
|
this.ohSwitch(swtch);
|
|
599
600
|
|
|
@@ -704,77 +705,172 @@ export class SmartApp extends WebApp {
|
|
|
704
705
|
this.wrequest(req, swtch.method, data, ContentType.form, swtch.header);
|
|
705
706
|
}
|
|
706
707
|
|
|
708
|
+
////////////////////////
|
|
709
|
+
// http request stuff //
|
|
710
|
+
////////////////////////
|
|
707
711
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
712
|
+
async habLogin(url: string, username: string, password: string): Promise<string> {
|
|
713
|
+
if (url.indexOf("://") === -1) url = "http://" + url;
|
|
714
|
+
|
|
715
|
+
const header = {"Content-Type": "application/json", "accept": "*/*"};
|
|
716
|
+
const data = {username, password};
|
|
717
|
+
const respBody = await this.wrequest(url + "/api/auth/login", 'POST', JSON.stringify(data), ContentType.json, JSON.stringify(header));
|
|
718
|
+
|
|
719
|
+
if (!respBody.access_token) {
|
|
720
|
+
err("hab", "habLogin: failed -> " + JSON.stringify(respBody));
|
|
721
|
+
return "";
|
|
722
|
+
|
|
723
|
+
} else {
|
|
724
|
+
return respBody.access_token
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async habCheckToken(token: string, url: string, username?: string, password?: string): Promise<boolean> {
|
|
729
|
+
if (!token || ! url) {
|
|
730
|
+
err("hab", "habCheckToken: failed -> no token or no url");
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const header = {
|
|
735
|
+
"Content-Type": "application/json", "accept": "*/*",
|
|
736
|
+
"access_token": token
|
|
737
|
+
};
|
|
738
|
+
const target = url.split("/api")[0];
|
|
739
|
+
const resp = await this.wrequest(target + "/api/auth/check", 'GET', "", ContentType.none, header);
|
|
740
|
+
if (resp['status'] === "OK") {
|
|
741
|
+
return true;
|
|
742
|
+
} else {
|
|
743
|
+
if (username && password) {
|
|
744
|
+
this.token = await this.habLogin(target, username, password);
|
|
745
|
+
return !!this.token;
|
|
746
|
+
|
|
747
|
+
} else {
|
|
748
|
+
err("hab", "habCheckToken: failed -> no username or password");
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
async wrequest(url: string, method = "GET", formdata = "", contentType = ContentType.form, header: string | object = ""): Promise<Record<string, any>> {
|
|
756
|
+
if (url.indexOf("://") === -1) url = "http://" + url;
|
|
757
|
+
|
|
758
|
+
return new Promise((resolve, reject) => {
|
|
759
|
+
try {
|
|
760
|
+
// const data = querystring.stringify(formdata);
|
|
761
|
+
const options: {method: string, [key: string]: object | string} = { method };
|
|
762
|
+
|
|
763
|
+
if (formdata) {
|
|
764
|
+
if (contentType === ContentType.form) {
|
|
765
|
+
options.headers = {
|
|
766
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
767
|
+
'Content-Length': Buffer.byteLength(formdata)
|
|
768
|
+
}
|
|
769
|
+
} else if (contentType === ContentType.plain) {
|
|
770
|
+
options.headers = {
|
|
771
|
+
'Content-Type': 'text/plain',
|
|
772
|
+
'Accept': 'application/json'
|
|
773
|
+
};
|
|
774
|
+
} else if (contentType === ContentType.json) {
|
|
775
|
+
options.headers = {
|
|
776
|
+
'Content-Type': 'application/json',
|
|
777
|
+
'Accept': '*/*'
|
|
778
|
+
};
|
|
718
779
|
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
};
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
// header provide as string or object
|
|
783
|
+
if (header) {
|
|
784
|
+
if (!options.headers) options.headers = {};
|
|
785
|
+
if (typeof header === "string") {
|
|
786
|
+
if (header.substring(0,1) === "{") {
|
|
787
|
+
try {
|
|
788
|
+
options.headers = { ...<object>options.headers, ...JSON.parse(header)};
|
|
789
|
+
} catch(e) {
|
|
790
|
+
err("smartapp", "error parsing :" + header + " -> " + JSON.stringify(e));
|
|
791
|
+
}
|
|
792
|
+
} else {
|
|
793
|
+
const s = header.split(":");
|
|
794
|
+
options.headers[s[0]] = s[1];
|
|
795
|
+
}
|
|
796
|
+
} else {
|
|
797
|
+
// header is an object
|
|
798
|
+
options.headers = { ...<object>options.headers, ...header };
|
|
733
799
|
}
|
|
734
|
-
} else {
|
|
735
|
-
const s = header.split(":");
|
|
736
|
-
options.headers[s[0]] = s[1];
|
|
737
800
|
}
|
|
738
|
-
}
|
|
739
801
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
802
|
+
debug("smartapp", "Sending to " + url + " with options: " + JSON.stringify(options));
|
|
803
|
+
const req = http.request(url, options, res => {
|
|
804
|
+
let resp = "";
|
|
805
|
+
// res.setEncoding('utf8');
|
|
806
|
+
res.on('error', error => {
|
|
807
|
+
log("smartapp", "The request to " + url + ", error: " + (error ? error.message : "unknown"));
|
|
808
|
+
reject(error);
|
|
809
|
+
});
|
|
810
|
+
res.on('data', chunk => {
|
|
811
|
+
resp += chunk;
|
|
812
|
+
});
|
|
813
|
+
res.on('end', () => {
|
|
814
|
+
// try to make something out of it...
|
|
815
|
+
try {
|
|
816
|
+
const x = JSON.parse(resp);
|
|
817
|
+
if ((x.type === "Buffer") && x.data) x.data = x.data.toString();
|
|
818
|
+
log("smartapp", "http " + method + " -> " + res.statusCode + ": " + url + " = " + JSON.stringify(x));
|
|
819
|
+
resolve(x)
|
|
820
|
+
} catch(e) {
|
|
821
|
+
err("smartapp", "http " + method + " -> " + res.statusCode + ": " + url + " = " + resp);
|
|
822
|
+
resolve({status: "NOK", result: resp});
|
|
823
|
+
};
|
|
824
|
+
});
|
|
746
825
|
});
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
debug("smartapp", "http " + method + " -> " + res.statusCode + ": " + url + " = " + JSON.stringify(x));
|
|
753
|
-
} catch(e) {
|
|
754
|
-
debug("smartapp", "http " + method + " -> " + res.statusCode + ": " + url + " = " + resp);
|
|
755
|
-
};
|
|
826
|
+
if (formdata) {
|
|
827
|
+
req.write(formdata);
|
|
828
|
+
}
|
|
829
|
+
req.on("error", (e) => {
|
|
830
|
+
log("smartapp", "The request to " + url + ", error: " + (e ? e.message : "unknown"));
|
|
756
831
|
});
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
req.on("error", (e) => {
|
|
762
|
-
log("smartapp", "The request to " + url + ", error: " + (e ? e.message : "unknown"));
|
|
763
|
-
});
|
|
764
|
-
req.on("timeout", (e) => {
|
|
765
|
-
log("smartapp", "The request to " + url + ", timeout: " + (e ? e.message : "unknown"));
|
|
766
|
-
});
|
|
767
|
-
req.end();
|
|
832
|
+
req.on("timeout", (e) => {
|
|
833
|
+
log("smartapp", "The request to " + url + ", timeout: " + (e ? e.message : "unknown"));
|
|
834
|
+
});
|
|
835
|
+
req.end();
|
|
768
836
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
837
|
+
} catch(e) {
|
|
838
|
+
log("smartapp", "** http error ** " + (e ? e.message : "unknown") + " **");
|
|
839
|
+
reject(e);
|
|
840
|
+
}
|
|
841
|
+
});
|
|
772
842
|
}
|
|
773
843
|
|
|
774
844
|
|
|
775
845
|
//////////////
|
|
776
846
|
// open HAB //
|
|
777
847
|
//////////////
|
|
848
|
+
async ohAPI(swtch: Switch) {
|
|
849
|
+
const str = "" + swtch.plug;
|
|
850
|
+
// http://username:password@example.com/api/...
|
|
851
|
+
const protocol = str.indexOf("://");
|
|
852
|
+
const login = (protocol === -1) ? str : str.substring(protocol + 3);
|
|
853
|
+
// username:password@example.com/api/...
|
|
854
|
+
const urlParts = login.split("@");
|
|
855
|
+
const userPw = urlParts[0].split(":");
|
|
856
|
+
|
|
857
|
+
let req = this.makeVariableURL(urlParts[1], !!swtch.unit.status, +swtch.unit.value);
|
|
858
|
+
req = ((protocol === -1) ? "http" : str.substring(0, protocol))+ "://" + req;
|
|
859
|
+
|
|
860
|
+
debug("smartapp", "OH-API(" + !!swtch.unit.status + ", " + swtch.unit.value + ") -> " + req);
|
|
861
|
+
|
|
862
|
+
if (userPw.length !== 2) {
|
|
863
|
+
// no user/pw given, try the API call without a token
|
|
864
|
+
await this.wrequest(req, swtch.method, this.makeVariableURL(swtch.data, !!swtch.unit.status, +swtch.unit.value), ContentType.json, swtch.header);
|
|
865
|
+
|
|
866
|
+
} else {
|
|
867
|
+
// user/pw given -> check token
|
|
868
|
+
if (await this.habCheckToken(this.token, req, userPw[0], userPw[1])) {
|
|
869
|
+
await this.wrequest(req, swtch.method, this.makeVariableURL(swtch.data, !!swtch.unit.status, +swtch.unit.value), ContentType.json, swtch.header);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
778
874
|
ohSwitch(swtch: Switch) {
|
|
779
875
|
const req = this.makeVariableURL(swtch.plug, !!swtch.unit.status, +swtch.unit.value);
|
|
780
876
|
debug("smartapp", "OH-Switch(" + !!swtch.unit.status + ") -> " + req);
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
<div class="input-field col s4">
|
|
37
37
|
<select name="master" id="master">
|
|
38
38
|
<% for (let m of masters) { %>
|
|
39
|
-
<option value="<%= m.getURL() %>"
|
|
39
|
+
<option value="<%= m.getURL() %>" <%= (m.same(swtch.masterAddress, swtch.masterPort)) ? "selected" : "" %>><%= m.getURL() %></option>
|
|
40
40
|
<% } %>
|
|
41
41
|
</select>
|
|
42
|
-
<label for="master">Master</label>
|
|
42
|
+
<label for="master">Master of trigger unit</label>
|
|
43
43
|
</div>
|
|
44
44
|
<div class="switch input-field col s4">
|
|
45
45
|
<label>Respond to Macro's
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
<div class="input-field col s12">
|
|
60
60
|
<input placeholder="0" id="unit" name="unit" type="text"
|
|
61
61
|
value="<%= hex(swtch.logicalNodeAddress) %>;<%= hex(swtch.logicalAddress) %><%= (swtch.unitName) ? (' (' + swtch.unitName + ')') : '' %>">
|
|
62
|
-
<label for="unit">
|
|
62
|
+
<label for="unit">Trigger unit in hex (example: 0xfc;0x12) or unit name</label>
|
|
63
63
|
</div>
|
|
64
64
|
</div>
|
|
65
65
|
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
<option value="http" <%= (swtch.type == 'http') ? 'selected' : '' %>>HTTP Switch</option>
|
|
72
72
|
<option value="httpdim" <%= (swtch.type == 'httpdim') ? 'selected' : '' %>>HTTP Dimmer</option>
|
|
73
73
|
<option value="httpupdown" <%= (swtch.type == 'httpupdown') ? 'selected' : '' %>>HTTP Up/Down</option>
|
|
74
|
+
<option value="ohapi" <%= (swtch.type == 'ohapi') ? 'selected' : '' %>>openHAB API</option>
|
|
74
75
|
<option value="oswitch" <%= (swtch.type == 'oswitch') ? 'selected' : '' %>>openHAB Switch</option>
|
|
75
76
|
<option value="odimmer" <%= (swtch.type == 'odimmer') ? 'selected' : '' %>>openHAB Dimmer</option>
|
|
76
77
|
<option value="oupdown" <%= (swtch.type == 'oupdown') ? 'selected' : '' %>>openHAB Up/Down</option>
|