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 +41 -1
- package/README.md +17 -19
- package/config.schema.json +4 -7
- package/index.js +128 -17
- 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/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
|
|
111
|
-
|
|
112
|
-
|
|
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
|
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
|
{
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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"
|