homebridge-smartsystem 6.4.7 → 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 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": 34,
253
+ "logicalAddress": 31,
254
254
  "masterAddress": "192.168.0.97",
255
255
  "masterPort": 5001,
256
- "name": "Demo",
257
- "displayName": "Demo",
256
+ "name": "Somfy Windowcovering",
257
+ "displayName": "Somfy Windowcovering",
258
258
  "extendedType": 0,
259
259
  "nomacro": "N",
260
260
  "nostop": "N",
261
- "unitName": "unit 3:34",
261
+ "unitName": "Somfy Dimmer",
262
262
  "value": 0,
263
- "data": "b=2",
264
- "header": "{\"a\":101, \"b\": \"hallo\"}",
263
+ "data": "{CurrentPosition: $}",
264
+ "header": "Pi:GMTecn",
265
265
  "method": "PUT",
266
- "type": "http",
267
- "plug": "http://localhost:3001/api/demo/haha?set=#B"
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": 1,
390
+ "extendedType": 0,
379
391
  "active": "N",
380
392
  "used": "Y",
381
393
  "group": 0
package/duotecno/api.ts CHANGED
@@ -2,6 +2,8 @@ import * as http from "http";
2
2
  import { err } from "./logger";
3
3
 
4
4
 
5
+
6
+
5
7
  ///////////////////
6
8
  // http function //
7
9
  ///////////////////
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", header: "", nomacro: "N", nostop: "N"} as const;
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/log.txt CHANGED
@@ -58,4 +58,17 @@ accessory: setBrightness 100
58
58
  at processTimers (node:internal/timers:523:7)
59
59
  master: 192.168.0.97: receiveRegister(1) = 3359
60
60
  [20/05/2025, 10:20:41] [HB Supervisor] Homebridge process ended. Code: 143, signal: null.
61
- [20/05/2025, 10:20:46] [HB Supervisor] Restarting Homebridge...
61
+ [20/05/2025, 10:20:46] [HB Supervisor] Restarting Homebridge...
62
+
63
+
64
+ -- Grote Moerstraat --
65
+
66
+
67
+ smartapp: HTTP-Switch(true) -> http://192.168.0.200/fcgi/do?action=OpenDoor&UserName=admin&Password=Grote46b&DoorNum=1
68
+ smartapp> Sending to http://192.168.0.200/fcgi/do?action=OpenDoor&UserName=admin&Password=Grote46b&DoorNum=1 with options: {"method":"GET"}
69
+ smartapp> http GET -> 200: http://192.168.0.200/fcgi/do?action=OpenDoor&UserName=admin&Password=Grote46b&DoorNum=1 = {"retcode":0,"message":"OK"}
70
+ smartapp: HTTP-Switch(false) -> http://192.168.0.200/fcgi/do?action=OpenDoor&UserName=admin&Password=Grote46b&DoorNum=xxx
71
+ smartapp> Sending to http://192.168.0.200/fcgi/do?action=OpenDoor&UserName=admin&Password=Grote46b&DoorNum=xxx with options: {"method":"GET"}
72
+ smartapp> http GET -> 200: http://192.168.0.200/fcgi/do?action=OpenDoor&UserName=admin&Password=Grote46b&DoorNum=xxx = {"retcode":-2,"message":"Error Param"}
73
+ power: smappee > RealTime totals: Power = 4431, Export = 72509864400, Import = 532865754000
74
+ master: 192.168.0.8: Received heartbeat at Tue May 20 2025 10:44:27 GMT+0200 (Central European Summer
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-smartsystem",
3
3
  "displayname": "Duotecno Bridge",
4
- "version": "6.4.7",
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",
@@ -13,8 +13,8 @@
13
13
  "publish": "npx tsc --build && npm i && npm publish"
14
14
  },
15
15
  "engines": {
16
- "node": ">=14.18.1",
17
- "homebridge": ">=1.3.5"
16
+ "node": "^18.20.4 || ^20.15.1 || ^22",
17
+ "homebridge": "^1.6.0 || ^2.0.0-beta.0"
18
18
  },
19
19
  "keywords": [
20
20
  "websockets",
@@ -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",
@@ -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
- this.accessoryList.push( new Mood(this.homebridgeAPI, unit) );
245
+ this.accessoryList.push( new Mood(this.homebridgeAPI, unit) );
246
246
  break;
247
247
 
248
248
  case UnitExtendedType.kTemperature:
@@ -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 body = { username: user, password: pw };
121
- const header = { "Content-Type": "application/json" };
122
- const resp = yield (0, api_1.post)(this.config.apiHost, this.config.apiPort, "/api/auth/login", body, header);
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
- wrequest(url, method = "GET", formdata = "", contentType = ContentType.form, header = "") {
644
- try {
645
- // const data = querystring.stringify(formdata);
646
- const options = { method };
647
- if (formdata) {
648
- if (contentType === ContentType.form) {
649
- options.headers = {
650
- 'Content-Type': 'application/x-www-form-urlencoded',
651
- 'Content-Length': Buffer.byteLength(formdata)
652
- };
653
- }
654
- else if (contentType === ContentType.plain) {
655
- options.headers = {
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
- if (header) {
663
- if (!options.headers)
664
- options.headers = {};
665
- if (header.substring(0, 1) === "{") {
666
- try {
667
- options.headers = Object.assign(Object.assign({}, options.headers), JSON.parse(header));
668
- }
669
- catch (e) {
670
- (0, logger_1.err)("smartapp", "error parsing :" + header + " -> " + JSON.stringify(e));
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
- const s = header.split(":");
675
- options.headers[s[0]] = s[1];
683
+ (0, logger_1.err)("hab", "habCheckToken: failed -> no username or password");
684
+ return false;
676
685
  }
677
686
  }
678
- (0, logger_1.debug)("smartapp", "Sending to " + url + " with options: " + JSON.stringify(options));
679
- const req = http.request(url, options, res => {
680
- let resp = "";
681
- // res.setEncoding('utf8');
682
- res.on('data', chunk => {
683
- resp += chunk;
684
- });
685
- res.on('end', () => {
686
- // try to make something out of it...
687
- try {
688
- const x = JSON.parse(resp);
689
- if ((x.type === "Buffer") && x.data)
690
- x.data = x.data.toString();
691
- (0, logger_1.debug)("smartapp", "http " + method + " -> " + res.statusCode + ": " + url + " = " + JSON.stringify(x));
692
- }
693
- catch (e) {
694
- (0, logger_1.debug)("smartapp", "http " + method + " -> " + res.statusCode + ": " + url + " = " + resp);
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
- if (formdata) {
700
- req.write(formdata);
701
- }
702
- req.on("error", (e) => {
703
- (0, logger_1.log)("smartapp", "The request to " + url + ", error: " + (e ? e.message : "unknown"));
704
- });
705
- req.on("timeout", (e) => {
706
- (0, logger_1.log)("smartapp", "The request to " + url + ", timeout: " + (e ? e.message : "unknown"));
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
- req.end();
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);
@@ -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 body = { username : user, password: pw };
141
- const header = {"Content-Type": "application/json"};
142
- const resp = await post(this.config.apiHost, this.config.apiPort, "/api/auth/login", body, header);
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
- wrequest(url: string, method = "GET", formdata = "", contentType = ContentType.form, header = "") {
709
- try {
710
- // const data = querystring.stringify(formdata);
711
- const options: {method: string, [key: string]: object | string} = { method };
712
-
713
- if (formdata) {
714
- if (contentType === ContentType.form) {
715
- options.headers = {
716
- 'Content-Type': 'application/x-www-form-urlencoded',
717
- 'Content-Length': Buffer.byteLength(formdata)
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
- } else if (contentType === ContentType.plain) {
720
- options.headers = {
721
- 'Content-Type': 'text/plain',
722
- 'Accept': 'application/json'
723
- };
724
- }
725
- };
726
- if (header) {
727
- if (!options.headers) options.headers = {};
728
- if (header.substring(0,1) === "{") {
729
- try {
730
- options.headers = { ...<object>options.headers, ...JSON.parse(header)};
731
- } catch(e) {
732
- err("smartapp", "error parsing :" + header + " -> " + JSON.stringify(e));
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
- debug("smartapp", "Sending to " + url + " with options: " + JSON.stringify(options));
741
- const req = http.request(url, options, res => {
742
- let resp = "";
743
- // res.setEncoding('utf8');
744
- res.on('data', chunk => {
745
- resp += chunk;
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
- res.on('end', () => {
748
- // try to make something out of it...
749
- try {
750
- const x = JSON.parse(resp);
751
- if ((x.type === "Buffer") && x.data) x.data = x.data.toString();
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
- if (formdata) {
759
- req.write(formdata);
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
- } catch(e) {
770
- log("smartapp", "** http error ** " + (e ? e.message : "unknown") + " **");
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() %>" "<%= (m.same(swtch.master, swtch.port)) ? "selected" : "" %>"><%= m.getURL() %></option>
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">Unit (hex or unit name)</label>
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>