homebridge-tesy-heater-mqtt 1.0.2 → 1.0.4

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
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.3] - 2025-12-15
11
+
12
+ ### Fixed
13
+ - **Fixed Config Schema validation** (Homebridge verification requirement)
14
+ - Removed invalid `required: true` from individual properties
15
+ - Added proper `required` array at schema object level: `["name", "userid", "username", "password"]`
16
+ - Config schema now passes Homebridge plugin verification checks
17
+
18
+ ### Added
19
+ - **MQTT real-time status updates** (Performance improvement)
20
+ - Added message handler for incoming MQTT `setTempStatistic` messages
21
+ - Devices send status updates every ~10 seconds automatically
22
+ - Temperatures (current and target) now update within 1 second of device changes
23
+ - Heating state updates triggered on MQTT messages (with API fetch for full status)
24
+ - Significantly faster than polling-only approach (10s vs 60s)
25
+ - Debug logging with [MQTT] prefix to distinguish from polling updates
26
+ - Polling still active as fallback for reliability
27
+
28
+ ### Security
29
+ - **Removed deprecated `request` dependency** (Critical security fix)
30
+ - Eliminated 2 critical vulnerabilities (form-data unsafe random, tough-cookie prototype pollution)
31
+ - Replaced with Node.js built-in `https` module
32
+ - Reduces package size by removing 3 vulnerable transitive dependencies
33
+ - No functionality changes - API calls work identically
34
+ - All 38 unit tests pass with new implementation
35
+
36
+ ### Changed
37
+ - Implemented `_httpsGet()` helper method for API requests
38
+ - Updated `discoverDevices()` and `fetchDeviceStatus()` to use native HTTPS
39
+ - **Enhanced Accessory Information display**
40
+ - Serial Number now shows Device ID and MAC address (e.g., "216884 (1C:9D:C2:36:9D:84)")
41
+ - Firmware Revision now displays actual device firmware version (e.g., "4.79")
42
+ - Improves device identification in Homebridge UI and HomeKit
43
+ - **Improved test coverage**
44
+ - Added 11 unit tests for MQTT message handler
45
+ - Test coverage increased from 49% to 64% (+15%)
46
+ - 49 tests total, all passing
47
+
10
48
  ## [1.0.2] - 2025-12-15
11
49
 
12
50
  ### Fixed
@@ -119,7 +157,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
119
157
  ### Tested Devices
120
158
  - Tesy CN 06 100 EА CLOUD AS W
121
159
 
122
- [Unreleased]: https://github.com/svjakrm/homebridge-tesy-heater-mqtt/compare/v1.0.1...HEAD
160
+ [Unreleased]: https://github.com/svjakrm/homebridge-tesy-heater-mqtt/compare/v1.0.3...HEAD
161
+ [1.0.3]: https://github.com/svjakrm/homebridge-tesy-heater-mqtt/compare/v1.0.2...v1.0.3
162
+ [1.0.2]: https://github.com/svjakrm/homebridge-tesy-heater-mqtt/compare/v1.0.1...v1.0.2
123
163
  [1.0.1]: https://github.com/svjakrm/homebridge-tesy-heater-mqtt/compare/v1.0.0...v1.0.1
124
164
  [1.0.0]: https://github.com/svjakrm/homebridge-tesy-heater-mqtt/compare/v0.0.1...v1.0.0
125
165
  [0.0.1]: https://github.com/svjakrm/homebridge-tesy-heater-mqtt/releases/tag/v0.0.1
package/README.md CHANGED
@@ -107,28 +107,26 @@ Add this platform to your Homebridge `config.json`:
107
107
  - `username` is your email used to log in to Tesy Cloud
108
108
  - `password` is your Tesy Cloud password
109
109
 
110
- **To find your User ID**, run this command:
111
- ```bash
112
- curl -s 'https://ad.mytesy.com/rest/get-my-devices?userEmail=YOUR_EMAIL&userPass=YOUR_PASSWORD&lang=en' | python3 -m json.tool
110
+ **To find your User ID**:
111
+
112
+ 1. Log in to [Tesy Cloud](https://v4.mytesy.com) in your browser
113
+ 2. Open Developer Tools:
114
+ - **Chrome/Edge**: Press `F12` or right-click → **Inspect**
115
+ - **Firefox**: Press `F12` or right-click → **Inspect Element**
116
+ - **Safari**: First enable developer menu in **Safari → Settings → Advanced** → check "Show features for web developers", then press `Cmd+Option+I`
117
+ 3. Go to the **Network** tab
118
+ 4. Refresh the page (`Cmd+R` or `F5`)
119
+ 5. Find a request named `get-my-devices` or `get-my-messages` in the list
120
+ 6. Click on it and look for the **Payload**, **Headers**, or **Request** tab
121
+ 7. Find the `userID` parameter - this is your User ID
122
+
123
+ Example:
113
124
  ```
114
-
115
- The response will show your `userid` and all your devices. Example:
116
- ```json
117
- {
118
- "AA:BB:CC:DD:EE:FF": {
119
- "deviceName": "Living Room Heater",
120
- "token": "abc1234",
121
- "state": {
122
- "id": 123456,
123
- "mac": "AA:BB:CC:DD:EE:FF",
124
- "status": "on",
125
- "temp": 20,
126
- ...
127
- }
128
- }
129
- }
125
+ GET https://ad.mytesy.com/rest/get-my-devices?userID=11111&userEmail=...
130
126
  ```
131
127
 
128
+ Your `userID` in this example is `11111`.
129
+
132
130
  **That's it!** The plugin will automatically discover all devices in your account.
133
131
 
134
132
  ## Technical Details
@@ -8,26 +8,22 @@
8
8
  "name": {
9
9
  "title": "Platform Name",
10
10
  "type": "string",
11
- "default": "TesyHeater",
12
- "required": true
11
+ "default": "TesyHeater"
13
12
  },
14
13
  "userid": {
15
14
  "title": "Tesy Cloud User ID",
16
15
  "type": "string",
17
- "required": true,
18
16
  "description": "Your Tesy Cloud account user ID (numeric)"
19
17
  },
20
18
  "username": {
21
19
  "title": "Tesy Cloud Email",
22
20
  "type": "string",
23
- "required": true,
24
21
  "format": "email",
25
22
  "description": "Your Tesy Cloud account email address"
26
23
  },
27
24
  "password": {
28
25
  "title": "Tesy Cloud Password",
29
26
  "type": "string",
30
- "required": true,
31
27
  "description": "Your Tesy Cloud account password"
32
28
  },
33
29
  "maxTemp": {
@@ -54,7 +50,8 @@
54
50
  "maximum": 300000,
55
51
  "description": "How often to poll device status (in milliseconds). Default: 60000 (1 minute)"
56
52
  }
57
- }
53
+ },
54
+ "required": ["name", "userid", "username", "password"]
58
55
  },
59
56
  "layout": [
60
57
  {
@@ -107,7 +104,7 @@
107
104
  },
108
105
  {
109
106
  "type": "help",
110
- "helpvalue": "<p><strong>How to find your User ID:</strong></p><ol><li>Log in to <a href='https://v4.mytesy.com' target='_blank'>Tesy Cloud</a></li><li>Check the browser URL or use the API to retrieve your user ID</li><li>Or run: <code>curl 'https://ad.mytesy.com/rest/get-my-devices?userEmail=YOUR_EMAIL&userPass=YOUR_PASSWORD&lang=en'</code></li></ol>"
107
+ "helpvalue": "<p><strong>How to find your User ID:</strong></p><ol><li>Log in to <a href='https://v4.mytesy.com' target='_blank'>Tesy Cloud</a> in your browser</li><li>Open Developer Tools (<kbd>F12</kbd> or right-click Inspect)</li><li>Go to the <strong>Network</strong> tab and refresh the page</li><li>Find request named <code>get-my-devices</code> or <code>get-my-messages</code></li><li>Click on it and look for <code>userID</code> parameter in the request URL or Payload tab</li><li>Example: <code>userID=27356</code> - your User ID is <strong>27356</strong></li></ol>"
111
108
  }
112
109
  ]
113
110
  }
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var Service, Characteristic, API;
2
2
  const mqtt = require('mqtt');
3
- const request = require('request');
3
+ const https = require('https');
4
4
  const querystring = require('querystring');
5
5
 
6
6
  module.exports = function(homebridge) {
@@ -20,6 +20,7 @@ class TesyHeaterPlatform {
20
20
  this.accessories = [];
21
21
  this.devices = {};
22
22
  this.mqttClient = null;
23
+ this.mqttReconnecting = false;
23
24
 
24
25
  // Configuration
25
26
  this.userid = this.config.userid;
@@ -61,19 +62,16 @@ class TesyHeaterPlatform {
61
62
  'lang': 'en'
62
63
  });
63
64
 
64
- const options = {
65
- 'method': 'GET',
66
- 'url': 'https://ad.mytesy.com/rest/get-my-devices?' + queryParams
67
- };
65
+ const url = 'https://ad.mytesy.com/rest/get-my-devices?' + queryParams;
68
66
 
69
- request(options, (error, response) => {
67
+ this._httpsGet(url, (error, body) => {
70
68
  if (error) {
71
69
  this.log.error("API Error:", error);
72
70
  return;
73
71
  }
74
72
 
75
73
  try {
76
- const data = JSON.parse(response.body);
74
+ const data = JSON.parse(body);
77
75
 
78
76
  if (!data || Object.keys(data).length === 0) {
79
77
  this.log.warn("No devices found in your Tesy Cloud account");
@@ -115,6 +113,7 @@ class TesyHeaterPlatform {
115
113
  mac: mac,
116
114
  token: deviceData.token,
117
115
  model: deviceData.model || 'cn05uv',
116
+ firmware_version: deviceData.firmware_version,
118
117
  name: deviceName,
119
118
  state: state
120
119
  });
@@ -225,7 +224,8 @@ class TesyHeaterPlatform {
225
224
  informationService
226
225
  .setCharacteristic(Characteristic.Manufacturer, 'Tesy')
227
226
  .setCharacteristic(Characteristic.Model, deviceInfo.model || 'Convector')
228
- .setCharacteristic(Characteristic.SerialNumber, deviceInfo.id);
227
+ .setCharacteristic(Characteristic.SerialNumber, `${deviceInfo.id} (${deviceInfo.mac})`)
228
+ .setCharacteristic(Characteristic.FirmwareRevision, deviceInfo.firmware_version || '0.0.0');
229
229
 
230
230
  // HeaterCooler Service
231
231
  let service = accessory.getService(Service.HeaterCooler);
@@ -285,10 +285,14 @@ class TesyHeaterPlatform {
285
285
  clientId: 'mqttjs_' + Math.random().toString(16).substr(2, 8),
286
286
  protocol: 'wss',
287
287
  reconnectPeriod: 5000,
288
+ keepalive: 60,
289
+ clean: true,
290
+ connectTimeout: 30 * 1000,
288
291
  });
289
292
 
290
293
  this.mqttClient.on('connect', () => {
291
294
  this.log.info("✓ Connected to MQTT broker");
295
+ this.mqttReconnecting = false;
292
296
 
293
297
  // Subscribe to all device response topics
294
298
  Object.values(this.devices).forEach(device => {
@@ -304,16 +308,109 @@ class TesyHeaterPlatform {
304
308
  });
305
309
  });
306
310
 
311
+ this.mqttClient.on('reconnect', () => {
312
+ if (!this.mqttReconnecting) {
313
+ this.log.info("Reconnecting to MQTT broker...");
314
+ this.mqttReconnecting = true;
315
+ }
316
+ });
317
+
307
318
  this.mqttClient.on('error', (error) => {
308
- this.log.error("MQTT Error:", error);
319
+ // Keepalive timeout is expected and will trigger reconnect automatically
320
+ if (error.message && error.message.includes('Keepalive timeout')) {
321
+ this.log.warn("MQTT keepalive timeout - reconnecting...");
322
+ } else {
323
+ this.log.error("MQTT Error:", error.message || error);
324
+ }
309
325
  });
310
326
 
311
327
  this.mqttClient.on('close', () => {
312
- this.log.warn("MQTT connection closed");
328
+ this.log.debug("MQTT connection closed");
313
329
  });
314
330
 
315
331
  this.mqttClient.on('offline', () => {
316
- this.log.warn("MQTT client offline");
332
+ this.log.warn("MQTT client offline - will retry connection");
333
+ });
334
+
335
+ this.mqttClient.on('end', () => {
336
+ this.log.debug("MQTT client ended");
337
+ });
338
+
339
+ // Handle incoming MQTT messages for real-time status updates
340
+ this.mqttClient.on('message', (topic, message) => {
341
+ try {
342
+ // Parse topic: v1/{MAC}/response/{MODEL}/{TOKEN}/{COMMAND}
343
+ const topicParts = topic.split('/');
344
+ if (topicParts.length < 6) return;
345
+
346
+ const mac = topicParts[1];
347
+ const command = topicParts[5];
348
+
349
+ // Only process setTempStatistic messages (periodic status updates from device)
350
+ if (command !== 'setTempStatistic') return;
351
+
352
+ const data = JSON.parse(message.toString());
353
+ if (!data.payload) return;
354
+
355
+ const payload = data.payload;
356
+
357
+ // Find device by MAC address
358
+ const device = Object.values(this.devices).find(d => d.info.mac === mac);
359
+ if (!device) return;
360
+
361
+ // Update temperatures immediately from MQTT
362
+ // Note: setTempStatistic messages don't include 'status' (on/off) field,
363
+ // so we'll fetch full status separately when heating state changes
364
+ const service = device.accessory.getService(Service.HeaterCooler);
365
+ if (!service) return;
366
+
367
+ // Update current temperature
368
+ if (payload.currentTemp !== undefined) {
369
+ const currentTemp = parseFloat(payload.currentTemp);
370
+ if (!isNaN(currentTemp)) {
371
+ const oldTemp = service.getCharacteristic(Characteristic.CurrentTemperature).value;
372
+ if (currentTemp !== oldTemp) {
373
+ service.getCharacteristic(Characteristic.CurrentTemperature).updateValue(currentTemp);
374
+ this.log.debug("%s: [MQTT] CurrentTemperature %s -> %s",
375
+ device.info.name, oldTemp, currentTemp);
376
+ }
377
+ }
378
+ }
379
+
380
+ // Update target temperature
381
+ if (payload.target !== undefined && payload.target > 0) {
382
+ const targetTemp = parseFloat(payload.target);
383
+ if (!isNaN(targetTemp) && targetTemp >= this.minTemp && targetTemp <= this.maxTemp) {
384
+ const oldTarget = service.getCharacteristic(Characteristic.HeatingThresholdTemperature).value;
385
+ if (targetTemp !== oldTarget) {
386
+ service.getCharacteristic(Characteristic.HeatingThresholdTemperature).updateValue(targetTemp);
387
+ this.log.debug("%s: [MQTT] HeatingThresholdTemperature %s -> %s",
388
+ device.info.name, oldTarget, targetTemp);
389
+ }
390
+ }
391
+ }
392
+
393
+ // For heating state, we need full status including 'status' (on/off)
394
+ // MQTT setTempStatistic doesn't include 'status' field
395
+ // So we'll trigger a fetch if heating field changed
396
+ if (payload.heating !== undefined) {
397
+ // Fetch full status to update heating state correctly
398
+ this.fetchDeviceStatus(device.info, (error, fullStatus) => {
399
+ if (error) return;
400
+
401
+ const heatingState = this._calculateHeatingState(fullStatus);
402
+ const oldState = service.getCharacteristic(Characteristic.CurrentHeaterCoolerState).value;
403
+
404
+ if (heatingState !== oldState) {
405
+ service.getCharacteristic(Characteristic.CurrentHeaterCoolerState).updateValue(heatingState);
406
+ this.log.debug("%s: [MQTT] Heating state %s -> %s",
407
+ device.info.name, oldState, heatingState);
408
+ }
409
+ });
410
+ }
411
+ } catch (error) {
412
+ this.log.debug("Error processing MQTT message:", error.message);
413
+ }
317
414
  });
318
415
  }
319
416
 
@@ -349,18 +446,15 @@ class TesyHeaterPlatform {
349
446
  'lang': 'en'
350
447
  });
351
448
 
352
- const options = {
353
- 'method': 'GET',
354
- 'url': 'https://ad.mytesy.com/rest/get-my-devices?' + queryParams
355
- };
449
+ const url = 'https://ad.mytesy.com/rest/get-my-devices?' + queryParams;
356
450
 
357
- request(options, (error, response) => {
451
+ this._httpsGet(url, (error, body) => {
358
452
  if (error) {
359
453
  return callback(error, null);
360
454
  }
361
455
 
362
456
  try {
363
- const data = JSON.parse(response.body);
457
+ const data = JSON.parse(body);
364
458
  const deviceData = data[deviceInfo.mac];
365
459
 
366
460
  if (!deviceData || !deviceData.state) {
@@ -573,6 +667,23 @@ class TesyHeaterPlatform {
573
667
  });
574
668
  });
575
669
  }
670
+
671
+ // Helper method for HTTPS GET requests
672
+ _httpsGet(url, callback) {
673
+ https.get(url, (response) => {
674
+ let data = '';
675
+
676
+ response.on('data', (chunk) => {
677
+ data += chunk;
678
+ });
679
+
680
+ response.on('end', () => {
681
+ callback(null, data);
682
+ });
683
+ }).on('error', (error) => {
684
+ callback(error, null);
685
+ });
686
+ }
576
687
  }
577
688
 
578
689
  // Export for testing
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-tesy-heater-mqtt",
3
3
  "displayName": "Homebridge Tesy Heater MQTT",
4
- "version": "1.0.2",
4
+ "version": "1.0.4",
5
5
  "description": "Tesy heater plugin for Homebridge using MQTT control (API v4). Supports on/off control and temperature adjustment.",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -32,8 +32,7 @@
32
32
  "url": "git+https://github.com/svjakrm/homebridge-tesy-heater-mqtt.git"
33
33
  },
34
34
  "dependencies": {
35
- "mqtt": "^5.14.1",
36
- "request": "^2.65.0"
35
+ "mqtt": "^5.14.1"
37
36
  },
38
37
  "bugs": {
39
38
  "url": "https://github.com/svjakrm/homebridge-tesy-heater-mqtt/issues"