homebridge-tesy-heater-mqtt 1.0.2 → 1.0.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
@@ -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
@@ -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
  {
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) {
@@ -61,19 +61,16 @@ class TesyHeaterPlatform {
61
61
  'lang': 'en'
62
62
  });
63
63
 
64
- const options = {
65
- 'method': 'GET',
66
- 'url': 'https://ad.mytesy.com/rest/get-my-devices?' + queryParams
67
- };
64
+ const url = 'https://ad.mytesy.com/rest/get-my-devices?' + queryParams;
68
65
 
69
- request(options, (error, response) => {
66
+ this._httpsGet(url, (error, body) => {
70
67
  if (error) {
71
68
  this.log.error("API Error:", error);
72
69
  return;
73
70
  }
74
71
 
75
72
  try {
76
- const data = JSON.parse(response.body);
73
+ const data = JSON.parse(body);
77
74
 
78
75
  if (!data || Object.keys(data).length === 0) {
79
76
  this.log.warn("No devices found in your Tesy Cloud account");
@@ -115,6 +112,7 @@ class TesyHeaterPlatform {
115
112
  mac: mac,
116
113
  token: deviceData.token,
117
114
  model: deviceData.model || 'cn05uv',
115
+ firmware_version: deviceData.firmware_version,
118
116
  name: deviceName,
119
117
  state: state
120
118
  });
@@ -225,7 +223,8 @@ class TesyHeaterPlatform {
225
223
  informationService
226
224
  .setCharacteristic(Characteristic.Manufacturer, 'Tesy')
227
225
  .setCharacteristic(Characteristic.Model, deviceInfo.model || 'Convector')
228
- .setCharacteristic(Characteristic.SerialNumber, deviceInfo.id);
226
+ .setCharacteristic(Characteristic.SerialNumber, `${deviceInfo.id} (${deviceInfo.mac})`)
227
+ .setCharacteristic(Characteristic.FirmwareRevision, deviceInfo.firmware_version || '0.0.0');
229
228
 
230
229
  // HeaterCooler Service
231
230
  let service = accessory.getService(Service.HeaterCooler);
@@ -315,6 +314,83 @@ class TesyHeaterPlatform {
315
314
  this.mqttClient.on('offline', () => {
316
315
  this.log.warn("MQTT client offline");
317
316
  });
317
+
318
+ // Handle incoming MQTT messages for real-time status updates
319
+ this.mqttClient.on('message', (topic, message) => {
320
+ try {
321
+ // Parse topic: v1/{MAC}/response/{MODEL}/{TOKEN}/{COMMAND}
322
+ const topicParts = topic.split('/');
323
+ if (topicParts.length < 6) return;
324
+
325
+ const mac = topicParts[1];
326
+ const command = topicParts[5];
327
+
328
+ // Only process setTempStatistic messages (periodic status updates from device)
329
+ if (command !== 'setTempStatistic') return;
330
+
331
+ const data = JSON.parse(message.toString());
332
+ if (!data.payload) return;
333
+
334
+ const payload = data.payload;
335
+
336
+ // Find device by MAC address
337
+ const device = Object.values(this.devices).find(d => d.info.mac === mac);
338
+ if (!device) return;
339
+
340
+ // Update temperatures immediately from MQTT
341
+ // Note: setTempStatistic messages don't include 'status' (on/off) field,
342
+ // so we'll fetch full status separately when heating state changes
343
+ const service = device.accessory.getService(Service.HeaterCooler);
344
+ if (!service) return;
345
+
346
+ // Update current temperature
347
+ if (payload.currentTemp !== undefined) {
348
+ const currentTemp = parseFloat(payload.currentTemp);
349
+ if (!isNaN(currentTemp)) {
350
+ const oldTemp = service.getCharacteristic(Characteristic.CurrentTemperature).value;
351
+ if (currentTemp !== oldTemp) {
352
+ service.getCharacteristic(Characteristic.CurrentTemperature).updateValue(currentTemp);
353
+ this.log.debug("%s: [MQTT] CurrentTemperature %s -> %s",
354
+ device.info.name, oldTemp, currentTemp);
355
+ }
356
+ }
357
+ }
358
+
359
+ // Update target temperature
360
+ if (payload.target !== undefined && payload.target > 0) {
361
+ const targetTemp = parseFloat(payload.target);
362
+ if (!isNaN(targetTemp) && targetTemp >= this.minTemp && targetTemp <= this.maxTemp) {
363
+ const oldTarget = service.getCharacteristic(Characteristic.HeatingThresholdTemperature).value;
364
+ if (targetTemp !== oldTarget) {
365
+ service.getCharacteristic(Characteristic.HeatingThresholdTemperature).updateValue(targetTemp);
366
+ this.log.debug("%s: [MQTT] HeatingThresholdTemperature %s -> %s",
367
+ device.info.name, oldTarget, targetTemp);
368
+ }
369
+ }
370
+ }
371
+
372
+ // For heating state, we need full status including 'status' (on/off)
373
+ // MQTT setTempStatistic doesn't include 'status' field
374
+ // So we'll trigger a fetch if heating field changed
375
+ if (payload.heating !== undefined) {
376
+ // Fetch full status to update heating state correctly
377
+ this.fetchDeviceStatus(device.info, (error, fullStatus) => {
378
+ if (error) return;
379
+
380
+ const heatingState = this._calculateHeatingState(fullStatus);
381
+ const oldState = service.getCharacteristic(Characteristic.CurrentHeaterCoolerState).value;
382
+
383
+ if (heatingState !== oldState) {
384
+ service.getCharacteristic(Characteristic.CurrentHeaterCoolerState).updateValue(heatingState);
385
+ this.log.debug("%s: [MQTT] Heating state %s -> %s",
386
+ device.info.name, oldState, heatingState);
387
+ }
388
+ });
389
+ }
390
+ } catch (error) {
391
+ this.log.debug("Error processing MQTT message:", error.message);
392
+ }
393
+ });
318
394
  }
319
395
 
320
396
  sendMQTTCommand(deviceInfo, command, payload, callback) {
@@ -349,18 +425,15 @@ class TesyHeaterPlatform {
349
425
  'lang': 'en'
350
426
  });
351
427
 
352
- const options = {
353
- 'method': 'GET',
354
- 'url': 'https://ad.mytesy.com/rest/get-my-devices?' + queryParams
355
- };
428
+ const url = 'https://ad.mytesy.com/rest/get-my-devices?' + queryParams;
356
429
 
357
- request(options, (error, response) => {
430
+ this._httpsGet(url, (error, body) => {
358
431
  if (error) {
359
432
  return callback(error, null);
360
433
  }
361
434
 
362
435
  try {
363
- const data = JSON.parse(response.body);
436
+ const data = JSON.parse(body);
364
437
  const deviceData = data[deviceInfo.mac];
365
438
 
366
439
  if (!deviceData || !deviceData.state) {
@@ -573,6 +646,23 @@ class TesyHeaterPlatform {
573
646
  });
574
647
  });
575
648
  }
649
+
650
+ // Helper method for HTTPS GET requests
651
+ _httpsGet(url, callback) {
652
+ https.get(url, (response) => {
653
+ let data = '';
654
+
655
+ response.on('data', (chunk) => {
656
+ data += chunk;
657
+ });
658
+
659
+ response.on('end', () => {
660
+ callback(null, data);
661
+ });
662
+ }).on('error', (error) => {
663
+ callback(error, null);
664
+ });
665
+ }
576
666
  }
577
667
 
578
668
  // 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.3",
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"