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 +41 -1
- package/config.schema.json +3 -6
- package/index.js +104 -14
- package/package.json +2 -3
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.
|
|
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/config.schema.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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"
|