homebridge-melcloud-control 4.11.1 → 4.11.3

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 CHANGED
@@ -25,7 +25,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
25
25
  - For plugin < v4.6.0 use Homebridge UI <= v5.5.0
26
26
  - For plugin >= v4.6.0 use Homebridge UI >= v5.13.0
27
27
 
28
- ## [4.11.0] - (23.09.2026)
28
+ ## [4.11.3] - (23.09.2026)
29
+
30
+ ### Changes
31
+
32
+ - readme update, added `mqtt.protocolVersion`
33
+ - sample config update, added `restFul.token` and `mqtt.protocolVersion`
34
+
35
+ ## [4.11.2] - (23.09.2026)
36
+
37
+ ### Changes
38
+
39
+ - added: 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
40
+ - readme update
41
+
42
+ ## [4.11.1] - (23.09.2026)
29
43
 
30
44
  ### Changes
31
45
 
package/README.md CHANGED
@@ -358,12 +358,14 @@
358
358
  | `log.debug` | This enable debug log will be displayed in console. |
359
359
  | `restFul{}` | RSTful object. |
360
360
  | `restFul.enable` | This enable RESTful server will start automatically and respond to any path request. |
361
+ | `restFul.token` | Here optional set the access token. When set, every request must send the header `Authorization: Bearer <token>`, otherwise the server responds `401`. |
361
362
  | `mqtt{}` | MQTT object. |
362
363
  | `mqtt.enable` | This enable MQTT Broker will start automatically and publish all awailable PV data. |
363
364
  | `mqtt.host` | Here set the `IP Address` or `Hostname` for MQTT Broker. |
364
365
  | `mqtt.port` | Here set the `Port` for MQTT Broker, default 1883. |
365
366
  | `mqtt.clientId` | Here optional set the `Client Id` of MQTT Broker. |
366
367
  | `mqtt.prefix` | Here set the `Prefix` for `Topic` or leave empty. |
368
+ | `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). |
367
369
  | `mqtt.auth{}` | MQTT authorization object. |
368
370
  | `mqtt.auth.enable` | Here enable authorization for MQTT Broker. |
369
371
  | `mqtt.auth.user` | Here set the MQTT Broker user. |
@@ -376,6 +378,7 @@
376
378
  * MELCLoud Home, start at `30000`, correct port is displayed in HB log during start.
377
379
  * POST data as a JSON Object `{OperationMode: 8}`.
378
380
  * Header content type must be `application/json`.
381
+ * If `Token` is set, every request (GET and POST) must send the header `Authorization: Bearer <token>`.
379
382
  * Path `status` response all available paths.
380
383
 
381
384
  | Method | URL | Path | Response | Type |
@@ -2362,6 +2362,16 @@
2362
2362
  "type": "boolean",
2363
2363
  "default": false,
2364
2364
  "description": "This enable RESTful server."
2365
+ },
2366
+ "token": {
2367
+ "title": "Token",
2368
+ "type": "string",
2369
+ "placeholder": "optional",
2370
+ "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.",
2371
+ "format": "password",
2372
+ "condition": {
2373
+ "functionBody": "return model.accounts[arrayIndices[0]].restFul.enable === true;"
2374
+ }
2365
2375
  }
2366
2376
  }
2367
2377
  },
@@ -3074,7 +3084,8 @@
3074
3084
  "key": "accounts[].restFul",
3075
3085
  "title": "RESTFul",
3076
3086
  "items": [
3077
- "accounts[].restFul.enable"
3087
+ "accounts[].restFul.enable",
3088
+ "accounts[].restFul.token"
3078
3089
  ]
3079
3090
  },
3080
3091
  {
package/index.js CHANGED
@@ -260,6 +260,7 @@ class MelCloudPlatform {
260
260
  const timer = setTimeout(resolve, 5000);
261
261
  state.restFul1 = new RestFul({
262
262
  port: device.restFul.port,
263
+ token: device.restFul.token,
263
264
  logWarn: logLevel.warn,
264
265
  logDebug: logLevel.debug,
265
266
  })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "MELCloud Control",
3
3
  "name": "homebridge-melcloud-control",
4
- "version": "4.11.1",
4
+ "version": "4.11.3",
5
5
  "description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
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
 
@@ -24,6 +26,19 @@ class RestFul extends EventEmitter {
24
26
  app.set('json spaces', 2);
25
27
  app.use(json());
26
28
 
29
+ // Optional token auth, when a token is configured every route requires "Authorization: Bearer <token>"
30
+ if (this.token) {
31
+ app.use((req, res, next) => {
32
+ const header = req.headers.authorization ?? '';
33
+ const provided = header.startsWith('Bearer ') ? header.slice(7) : '';
34
+ if (!this.tokenValid(provided)) {
35
+ if (this.logWarn) this.emit('warn', `RESTFul Unauthorized request from: ${req.ip}, ${req.method} ${req.path}`);
36
+ return res.status(401).json({ error: 'RESTFul Unauthorized' });
37
+ }
38
+ next();
39
+ });
40
+ }
41
+
27
42
  // Register GET routes for all keys
28
43
  for (const key of Object.keys(this.restFulData)) {
29
44
  app.get(`/${key}`, (req, res) => {
@@ -72,6 +87,13 @@ class RestFul extends EventEmitter {
72
87
  }
73
88
  }
74
89
 
90
+ tokenValid(provided) {
91
+ // Compare fixed-length hashes so the check takes the same time regardless of token length or content
92
+ const a = createHash('sha256').update(provided).digest();
93
+ const b = createHash('sha256').update(this.token).digest();
94
+ return timingSafeEqual(a, b);
95
+ }
96
+
75
97
  update(path, data) {
76
98
  if (this.restFulData.hasOwnProperty(path)) {
77
99
  this.restFulData[path] = data;