homebridge-openwrt-control 0.3.7 → 0.3.9
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/CHANGELOG.md +21 -0
- package/README.md +3 -0
- package/config.schema.json +32 -1
- package/package.json +1 -1
- package/src/mqtt.js +36 -18
- package/src/restful.js +22 -0
- package/src/router.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
- After update to v0.1.0 the plugin need to be reconfigured!
|
|
11
11
|
|
|
12
|
+
## [0.3.9] - (23.09.2026)
|
|
13
|
+
|
|
14
|
+
### Changes
|
|
15
|
+
|
|
16
|
+
- fix: MQTT client had `protocolVersion: 5` hardcoded, so brokers that only support MQTT 3.1.1 (e.g. the ioBroker MQTT adapter) accepted the TCP connection, failed the handshake, and got disconnected every second forever, with nothing published and no error logged. Added a `Protocol Version` option (5.0 / 3.1.1) in the MQTT section, and a one-time warning if no successful connection is established within 30 seconds
|
|
17
|
+
- readme update
|
|
18
|
+
- sample config update
|
|
19
|
+
|
|
20
|
+
## [0.3.8] - (23.09.2026)
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- optional RESTful access `Token`. When set, every request (GET and POST) must send the header `Authorization: Bearer <token>`, otherwise the server responds `401`. Leaving it empty keeps the previous behaviour, so existing configs keep working
|
|
25
|
+
- readme update
|
|
26
|
+
|
|
27
|
+
## [0.3.7] - (13.09.2026)
|
|
28
|
+
|
|
29
|
+
### Changes
|
|
30
|
+
|
|
31
|
+
- bump dependencies
|
|
32
|
+
|
|
12
33
|
## [0.3.6] - (11.05.2026)
|
|
13
34
|
|
|
14
35
|
## Changes
|
package/README.md
CHANGED
|
@@ -99,12 +99,14 @@
|
|
|
99
99
|
| `restFul{}` | RESTFul object. |
|
|
100
100
|
| `restFul.enable` | If enabled, RESTful server will start automatically and respond to any path request. |
|
|
101
101
|
| `restFul.port` | Here set the listening `Port` for RESTful server. |
|
|
102
|
+
| `restFul.token` | Here optional set the access token. When set, every request must send the header `Authorization: Bearer <token>`, otherwise the server responds `401`. |
|
|
102
103
|
| `mqtt{}` | MQTT object. |
|
|
103
104
|
| `mqtt.enable` | If enabled, MQTT Broker will start automatically and publish all available data. |
|
|
104
105
|
| `mqtt.host` | Here set the `IP Address` or `Hostname` for MQTT Broker. |
|
|
105
106
|
| `mqtt.port` | Here set the `Port` for MQTT Broker, default 1883. |
|
|
106
107
|
| `mqtt.clientId` | Here optional set the `Client Id` of MQTT Broker. |
|
|
107
108
|
| `mqtt.prefix` | Here set the `Prefix` for `Topic` or leave empty. |
|
|
109
|
+
| `mqtt.protocolVersion` | Here select the MQTT protocol version, `5.0` (default) or `3.1.1` for brokers that only support 3.1.1 (e.g. ioBroker MQTT adapter). |
|
|
108
110
|
| `mqtt.auth{}` | MQTT authorization object. |
|
|
109
111
|
| `mqtt.auth.enable` | Here enable authorization for MQTT Broker. |
|
|
110
112
|
| `mqtt.auth.user` | Here set the MQTT Broker `Username`. |
|
|
@@ -113,6 +115,7 @@
|
|
|
113
115
|
### REST Integration
|
|
114
116
|
|
|
115
117
|
REST POST calls must include a content-type header of `application/json`.
|
|
118
|
+
If `Token` is set, every request (GET and POST) must send the header `Authorization: Bearer <token>`.
|
|
116
119
|
Path `status` response all available paths.
|
|
117
120
|
|
|
118
121
|
| Method | URL | Path | Response | Type |
|
package/config.schema.json
CHANGED
|
@@ -478,6 +478,16 @@
|
|
|
478
478
|
"condition": {
|
|
479
479
|
"functionBody": "return model.devices[arrayIndices].restFul.enable == true;"
|
|
480
480
|
}
|
|
481
|
+
},
|
|
482
|
+
"token": {
|
|
483
|
+
"title": "Token",
|
|
484
|
+
"type": "string",
|
|
485
|
+
"placeholder": "optional",
|
|
486
|
+
"description": "Optional access token. When set, every request must send the header 'Authorization: Bearer <token>'. Leave empty to keep the server open on the local network.",
|
|
487
|
+
"format": "password",
|
|
488
|
+
"condition": {
|
|
489
|
+
"functionBody": "return model.devices[arrayIndices].restFul.enable == true;"
|
|
490
|
+
}
|
|
481
491
|
}
|
|
482
492
|
}
|
|
483
493
|
},
|
|
@@ -528,6 +538,25 @@
|
|
|
528
538
|
"functionBody": "return model.devices[arrayIndices].mqtt.enable == true;"
|
|
529
539
|
}
|
|
530
540
|
},
|
|
541
|
+
"protocolVersion": {
|
|
542
|
+
"title": "Protocol Version",
|
|
543
|
+
"type": "integer",
|
|
544
|
+
"default": 5,
|
|
545
|
+
"oneOf": [
|
|
546
|
+
{
|
|
547
|
+
"title": "5.0",
|
|
548
|
+
"enum": [5]
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
"title": "3.1.1",
|
|
552
|
+
"enum": [4]
|
|
553
|
+
}
|
|
554
|
+
],
|
|
555
|
+
"description": "MQTT protocol version used to connect to the broker. Some brokers (e.g. the ioBroker MQTT adapter) only support 3.1.1 and will silently reject 5.0 connections.",
|
|
556
|
+
"condition": {
|
|
557
|
+
"functionBody": "return model.devices[arrayIndices].mqtt.enable == true;"
|
|
558
|
+
}
|
|
559
|
+
},
|
|
531
560
|
"auth": {
|
|
532
561
|
"title": "Authorization",
|
|
533
562
|
"type": "object",
|
|
@@ -782,7 +811,8 @@
|
|
|
782
811
|
"title": "RESTFul",
|
|
783
812
|
"items": [
|
|
784
813
|
"devices[].restFul.enable",
|
|
785
|
-
"devices[].restFul.port"
|
|
814
|
+
"devices[].restFul.port",
|
|
815
|
+
"devices[].restFul.token"
|
|
786
816
|
]
|
|
787
817
|
},
|
|
788
818
|
{
|
|
@@ -794,6 +824,7 @@
|
|
|
794
824
|
"devices[].mqtt.port",
|
|
795
825
|
"devices[].mqtt.clientId",
|
|
796
826
|
"devices[].mqtt.prefix",
|
|
827
|
+
"devices[].mqtt.protocolVersion",
|
|
797
828
|
{
|
|
798
829
|
"key": "devices[].mqtt.auth",
|
|
799
830
|
"title": "Authorization",
|
package/package.json
CHANGED
package/src/mqtt.js
CHANGED
|
@@ -8,35 +8,47 @@ class Mqtt extends EventEmitter {
|
|
|
8
8
|
|
|
9
9
|
const url = `mqtt://${config.host}:${config.port}`;
|
|
10
10
|
const subscribeTopic = `${config.prefix}/Set`;
|
|
11
|
+
const protocolVersion = config.protocolVersion === 4 ? 4 : 5;
|
|
12
|
+
const isV5 = protocolVersion === 5;
|
|
13
|
+
this.isV5 = isV5;
|
|
11
14
|
|
|
12
15
|
const options = {
|
|
13
16
|
clientId: config.clientId,
|
|
14
17
|
username: config.user,
|
|
15
18
|
password: config.passwd,
|
|
16
|
-
protocolVersion
|
|
17
|
-
clean:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
protocolVersion,
|
|
20
|
+
clean: !isV5,
|
|
21
|
+
...(isV5 ? {
|
|
22
|
+
properties: {
|
|
23
|
+
sessionExpiryInterval: 60 * 60,
|
|
24
|
+
userProperties: {
|
|
25
|
+
source: 'node-client'
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
|
-
}
|
|
28
|
+
} : {})
|
|
24
29
|
};
|
|
25
30
|
|
|
31
|
+
const startTime = Date.now();
|
|
32
|
+
let hasConnected = false;
|
|
33
|
+
let warnedStalled = false;
|
|
34
|
+
|
|
26
35
|
this.mqttClient = connect(url, options)
|
|
27
36
|
.on('connect', async () => {
|
|
28
|
-
|
|
37
|
+
hasConnected = true;
|
|
38
|
+
this.emit('connected', `MQTT v${protocolVersion} connected.`);
|
|
29
39
|
|
|
30
40
|
try {
|
|
31
41
|
await new Promise((resolve, reject) => {
|
|
32
42
|
this.mqttClient.subscribe(subscribeTopic,
|
|
33
43
|
{
|
|
34
44
|
qos: 1,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
...(isV5 ? {
|
|
46
|
+
properties: {
|
|
47
|
+
userProperties: {
|
|
48
|
+
type: 'subscription'
|
|
49
|
+
}
|
|
38
50
|
}
|
|
39
|
-
}
|
|
51
|
+
} : {})
|
|
40
52
|
},
|
|
41
53
|
(error) => {
|
|
42
54
|
if (error) return reject(error);
|
|
@@ -69,6 +81,10 @@ class Mqtt extends EventEmitter {
|
|
|
69
81
|
if (config.logDebug) this.emit('debug', 'MQTT Reconnecting...');
|
|
70
82
|
})
|
|
71
83
|
.on('close', () => {
|
|
84
|
+
if (!hasConnected && !warnedStalled && Date.now() - startTime > 30000) {
|
|
85
|
+
warnedStalled = true;
|
|
86
|
+
this.emit('warn', `MQTT has not connected after 30s of retries using protocol v${protocolVersion}. Check broker address, credentials, and whether the broker supports this MQTT protocol version.`);
|
|
87
|
+
}
|
|
72
88
|
if (config.logDebug) this.emit('debug', 'MQTT Connection closed.');
|
|
73
89
|
});
|
|
74
90
|
}
|
|
@@ -81,13 +97,15 @@ class Mqtt extends EventEmitter {
|
|
|
81
97
|
this.mqttClient.publish(fullTopic, publishMessage,
|
|
82
98
|
{
|
|
83
99
|
qos: 1,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
...(this.isV5 ? {
|
|
101
|
+
properties: {
|
|
102
|
+
contentType: 'application/json',
|
|
103
|
+
userProperties: {
|
|
104
|
+
source: 'node',
|
|
105
|
+
action: 'set'
|
|
106
|
+
}
|
|
89
107
|
}
|
|
90
|
-
}
|
|
108
|
+
} : {})
|
|
91
109
|
},
|
|
92
110
|
(error) => {
|
|
93
111
|
if (error) {
|
package/src/restful.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import express, { json } from 'express';
|
|
2
2
|
import EventEmitter from 'events';
|
|
3
|
+
import { createHash, timingSafeEqual } from 'crypto';
|
|
3
4
|
|
|
4
5
|
const DEFAULT_MESSAGE = 'This data is not available at this time.';
|
|
5
6
|
|
|
@@ -7,6 +8,7 @@ class RestFul extends EventEmitter {
|
|
|
7
8
|
constructor(config) {
|
|
8
9
|
super();
|
|
9
10
|
this.port = config.port;
|
|
11
|
+
this.token = typeof config.token === 'string' ? config.token.trim() : '';
|
|
10
12
|
this.logWarn = config.logWarn;
|
|
11
13
|
this.logDebug = config.logDebug;
|
|
12
14
|
|
|
@@ -23,6 +25,19 @@ class RestFul extends EventEmitter {
|
|
|
23
25
|
app.set('json spaces', 2);
|
|
24
26
|
app.use(json());
|
|
25
27
|
|
|
28
|
+
// Optional token auth, when a token is configured every route requires "Authorization: Bearer <token>"
|
|
29
|
+
if (this.token) {
|
|
30
|
+
app.use((req, res, next) => {
|
|
31
|
+
const header = req.headers.authorization ?? '';
|
|
32
|
+
const provided = header.startsWith('Bearer ') ? header.slice(7) : '';
|
|
33
|
+
if (!this.tokenValid(provided)) {
|
|
34
|
+
if (this.logWarn) this.emit('warn', `RESTFul Unauthorized request from: ${req.ip}, ${req.method} ${req.path}`);
|
|
35
|
+
return res.status(401).json({ error: 'RESTFul Unauthorized' });
|
|
36
|
+
}
|
|
37
|
+
next();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
// Register GET routes for all keys
|
|
27
42
|
for (const key of Object.keys(this.restFulData)) {
|
|
28
43
|
app.get(`/${key}`, (req, res) => {
|
|
@@ -71,6 +86,13 @@ class RestFul extends EventEmitter {
|
|
|
71
86
|
}
|
|
72
87
|
}
|
|
73
88
|
|
|
89
|
+
tokenValid(provided) {
|
|
90
|
+
// Compare fixed-length hashes so the check takes the same time regardless of token length or content
|
|
91
|
+
const a = createHash('sha256').update(provided).digest();
|
|
92
|
+
const b = createHash('sha256').update(this.token).digest();
|
|
93
|
+
return timingSafeEqual(a, b);
|
|
94
|
+
}
|
|
95
|
+
|
|
74
96
|
update(path, data) {
|
|
75
97
|
if (Object.hasOwn(this.restFulData, path)) {
|
|
76
98
|
this.restFulData[path] = data;
|
package/src/router.js
CHANGED
|
@@ -185,6 +185,7 @@ class Router extends EventEmitter {
|
|
|
185
185
|
if (this.restFul.enable) {
|
|
186
186
|
this.restFul1 = new RestFul({
|
|
187
187
|
port: this.restFul.port || 3000,
|
|
188
|
+
token: this.restFul.token,
|
|
188
189
|
logWarn: this.logWarn,
|
|
189
190
|
logDebug: this.logDebug
|
|
190
191
|
})
|
|
@@ -206,6 +207,7 @@ class Router extends EventEmitter {
|
|
|
206
207
|
port: this.mqtt.port || 1883,
|
|
207
208
|
clientId: `${this.name}_${Math.random().toString(16).slice(3)}`,
|
|
208
209
|
prefix: this.mqtt.prefix ? `${this.openWrtInfo.systemInfo.model}/${this.mqtt.prefix}/${this.name}` : `${this.openWrtInfo.systemInfo.model}/${this.name}`,
|
|
210
|
+
protocolVersion: this.mqtt.protocolVersion,
|
|
209
211
|
user: this.mqtt.auth?.user,
|
|
210
212
|
passwd: this.mqtt.auth?.passwd,
|
|
211
213
|
logWarn: this.logWarn,
|