homebridge-tesy-heater-api-v4 0.0.4 → 0.0.5
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/README.md +22 -8
- package/config.schema.json +6 -1
- package/index.js +78 -69
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
# homebridge-tesy-heater-api-v4
|
|
2
|
-
this is a clone/upgrade to https://github.com/benov84/homebridge-tesy-heater#readme for the newer tesy api
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
Homebridge plugin for Tesy smart heaters using the `ad.mytesy.com/rest/old-app-*` endpoints.
|
|
4
|
+
Uses axios with a cookie jar to capture `PHPSESSID` if the API does not return `acc_session`/`acc_alt` in JSON.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## Installation
|
|
6
|
+
## Install (local dev)
|
|
9
7
|
|
|
10
8
|
```bash
|
|
11
|
-
#git clone https://github.com/YOUR-USERNAME/homebridge-tesy-heater.git
|
|
12
|
-
git clone https://github.com/mpopof/homebridge-tesy-heater-api-v4
|
|
13
|
-
cd homebridge-tesy-heater-api-v4
|
|
14
9
|
npm install
|
|
15
10
|
sudo npm link
|
|
11
|
+
# then configure in Homebridge UI
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Config fields
|
|
15
|
+
|
|
16
|
+
- **name**: display name in HomeKit
|
|
17
|
+
- **device_id**: device identifier as used by Tesy API
|
|
18
|
+
- **username / password**: Tesy account
|
|
19
|
+
- **userid**: (optional) some accounts require it
|
|
20
|
+
- **pullInterval**: status refresh period in ms (default 10000)
|
|
21
|
+
- **minTemp / maxTemp**: bounds for target temperature
|
|
22
|
+
|
|
23
|
+
## Notes
|
|
24
|
+
|
|
25
|
+
- Requires Node 16+ and Homebridge 1.6+
|
|
26
|
+
- Endpoints used:
|
|
27
|
+
- `https://ad.mytesy.com/rest/old-app-login`
|
|
28
|
+
- `https://ad.mytesy.com/rest/old-app-devices`
|
|
29
|
+
- `https://ad.mytesy.com/rest/old-app-set-device-status`
|
package/config.schema.json
CHANGED
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
"required": true,
|
|
28
28
|
"format": "password"
|
|
29
29
|
},
|
|
30
|
+
"userid": {
|
|
31
|
+
"title": "Tesy User ID (optional)",
|
|
32
|
+
"type": "string",
|
|
33
|
+
"required": false
|
|
34
|
+
},
|
|
30
35
|
"pullInterval": {
|
|
31
36
|
"title": "Refresh Interval (ms)",
|
|
32
37
|
"type": "integer",
|
|
@@ -44,4 +49,4 @@
|
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
|
-
}
|
|
52
|
+
}
|
package/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// index.js
|
|
2
|
+
const { wrapper } = require("axios-cookiejar-support");
|
|
3
|
+
const { CookieJar } = require("tough-cookie");
|
|
2
4
|
const axios = require("axios");
|
|
3
5
|
const _http_base = require("homebridge-http-base");
|
|
4
6
|
const PullTimer = _http_base.PullTimer;
|
|
@@ -12,6 +14,23 @@ module.exports = function (homebridge) {
|
|
|
12
14
|
homebridge.registerAccessory("homebridge-tesy-heater-api-v4", "TesyHeater", TesyHeater);
|
|
13
15
|
};
|
|
14
16
|
|
|
17
|
+
// Shared axios instance with cookie jar and browser-like headers
|
|
18
|
+
const jar = new CookieJar();
|
|
19
|
+
const api = wrapper(axios.create({
|
|
20
|
+
jar,
|
|
21
|
+
withCredentials: true,
|
|
22
|
+
timeout: 15000,
|
|
23
|
+
headers: {
|
|
24
|
+
"accept": "application/json, text/plain, */*",
|
|
25
|
+
"content-type": "application/json",
|
|
26
|
+
"origin": "https://v4.mytesy.com",
|
|
27
|
+
"referer": "https://v4.mytesy.com/",
|
|
28
|
+
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
|
|
29
|
+
"dnt": "1"
|
|
30
|
+
},
|
|
31
|
+
validateStatus: (s) => s >= 200 && s < 400
|
|
32
|
+
}));
|
|
33
|
+
|
|
15
34
|
class TesyHeater {
|
|
16
35
|
constructor(log, config) {
|
|
17
36
|
this.log = log;
|
|
@@ -36,7 +55,7 @@ class TesyHeater {
|
|
|
36
55
|
// ---- HomeKit Service ----
|
|
37
56
|
this.service = new Service.HeaterCooler(this.name);
|
|
38
57
|
|
|
39
|
-
// Default initial states
|
|
58
|
+
// Default initial states
|
|
40
59
|
this.service
|
|
41
60
|
.getCharacteristic(Characteristic.CurrentHeaterCoolerState)
|
|
42
61
|
.updateValue(Characteristic.CurrentHeaterCoolerState.INACTIVE);
|
|
@@ -105,7 +124,7 @@ class TesyHeater {
|
|
|
105
124
|
.setCharacteristic(Characteristic.Model, this.model)
|
|
106
125
|
.setCharacteristic(Characteristic.SerialNumber, this.device_id);
|
|
107
126
|
|
|
108
|
-
// ---- Timer
|
|
127
|
+
// ---- Timer ----
|
|
109
128
|
this.pullTimer = new PullTimer(
|
|
110
129
|
this.log,
|
|
111
130
|
this.pullInterval,
|
|
@@ -118,10 +137,9 @@ class TesyHeater {
|
|
|
118
137
|
this.authenticate()
|
|
119
138
|
.then(() => {
|
|
120
139
|
this.pullTimer.start();
|
|
121
|
-
// Do an immediate first refresh
|
|
122
140
|
this.refreshTesyHeaterStatus();
|
|
123
141
|
})
|
|
124
|
-
.catch((
|
|
142
|
+
.catch(() => {
|
|
125
143
|
this.log.error("Initial authentication failed. Will still start timer and retry on next ticks.");
|
|
126
144
|
this.pullTimer.start();
|
|
127
145
|
});
|
|
@@ -156,38 +174,50 @@ class TesyHeater {
|
|
|
156
174
|
}
|
|
157
175
|
|
|
158
176
|
getTesyHeaterCurrentHeaterCoolerState(state) {
|
|
159
|
-
// READY = IDLE; otherwise assume heating when active
|
|
160
177
|
if (!state) return Characteristic.CurrentHeaterCoolerState.INACTIVE;
|
|
161
178
|
return state.toUpperCase() === "READY"
|
|
162
179
|
? Characteristic.CurrentHeaterCoolerState.IDLE
|
|
163
180
|
: Characteristic.CurrentHeaterCoolerState.HEATING;
|
|
164
181
|
}
|
|
165
182
|
|
|
166
|
-
// ---- API calls (axios) ----
|
|
183
|
+
// ---- API calls (axios + cookies) ----
|
|
167
184
|
async authenticate() {
|
|
168
185
|
try {
|
|
169
|
-
const
|
|
186
|
+
const resp = await api.post(
|
|
170
187
|
"https://ad.mytesy.com/rest/old-app-login",
|
|
171
188
|
{
|
|
172
189
|
email: this.username,
|
|
173
190
|
password: this.password,
|
|
174
|
-
userID: this.userid,
|
|
191
|
+
userID: this.userid || "",
|
|
175
192
|
userEmail: this.username,
|
|
176
193
|
userPass: this.password,
|
|
177
194
|
lang: "en",
|
|
178
|
-
}
|
|
179
|
-
{ headers: { "content-type": "application/json" }, timeout: 10000 }
|
|
195
|
+
}
|
|
180
196
|
);
|
|
181
197
|
|
|
182
|
-
|
|
198
|
+
const data = resp.data || {};
|
|
199
|
+
this.log.debug("Login response keys:", Object.keys(data));
|
|
200
|
+
|
|
201
|
+
// Preferred (old behavior)
|
|
183
202
|
this.session = data.acc_session || data.PHPSESSID || "";
|
|
184
|
-
this.alt
|
|
203
|
+
this.alt = data.acc_alt || data.ALT || "";
|
|
204
|
+
|
|
205
|
+
// Fallback to cookies for PHPSESSID
|
|
206
|
+
if (!this.session) {
|
|
207
|
+
const cookies = await jar.getCookies("https://ad.mytesy.com/");
|
|
208
|
+
const phpsess = cookies.find(c => c.key.toUpperCase() === "PHPSESSID");
|
|
209
|
+
if (phpsess) this.session = phpsess.value;
|
|
210
|
+
}
|
|
185
211
|
|
|
186
|
-
if (!this.session
|
|
187
|
-
throw new Error("Missing session
|
|
212
|
+
if (!this.session) {
|
|
213
|
+
throw new Error("Missing session (neither JSON nor cookie contained PHPSESSID)");
|
|
188
214
|
}
|
|
189
215
|
|
|
190
|
-
|
|
216
|
+
if (!this.alt) {
|
|
217
|
+
this.log.warn("ALT not present in login response; will try to infer later.");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.log.info("Authenticated (cookies + session captured).");
|
|
191
221
|
} catch (error) {
|
|
192
222
|
const msg = error?.response?.data || error.message;
|
|
193
223
|
this.log.error("Authentication failed:", msg);
|
|
@@ -197,49 +227,48 @@ class TesyHeater {
|
|
|
197
227
|
|
|
198
228
|
async refreshTesyHeaterStatus() {
|
|
199
229
|
this.log.debug("Executing refreshTesyHeaterStatus");
|
|
200
|
-
|
|
201
|
-
// Avoid overlapping polls
|
|
202
230
|
this.pullTimer.stop();
|
|
203
231
|
|
|
204
232
|
try {
|
|
205
|
-
if (!this.session
|
|
206
|
-
this.log.warn("No session
|
|
233
|
+
if (!this.session) {
|
|
234
|
+
this.log.warn("No session yet; authenticating...");
|
|
207
235
|
await this.authenticate();
|
|
208
236
|
}
|
|
209
237
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
);
|
|
238
|
+
const payload = {
|
|
239
|
+
ALT: this.alt || undefined,
|
|
240
|
+
CURRENT_SESSION: null,
|
|
241
|
+
PHPSESSID: this.session,
|
|
242
|
+
last_login_username: this.username,
|
|
243
|
+
userID: this.userid || "",
|
|
244
|
+
userEmail: this.username,
|
|
245
|
+
userPass: this.password,
|
|
246
|
+
lang: "en"
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const resp = await api.post("https://ad.mytesy.com/rest/old-app-devices", payload);
|
|
250
|
+
const data = resp.data || {};
|
|
251
|
+
this.log.debug("Devices response top-level keys:", Object.keys(data));
|
|
252
|
+
|
|
253
|
+
if (!this.alt && (data.acc_alt || data.ALT)) {
|
|
254
|
+
this.alt = data.acc_alt || data.ALT;
|
|
255
|
+
this.log.info("Captured ALT from devices response.");
|
|
256
|
+
}
|
|
224
257
|
|
|
225
|
-
if (!data
|
|
258
|
+
if (!data.device || Object.keys(data.device).length === 0) {
|
|
226
259
|
throw new Error("No devices in response");
|
|
227
260
|
}
|
|
228
261
|
|
|
229
262
|
const firstKey = Object.keys(data.device)[0];
|
|
230
263
|
const status = data.device[firstKey]?.DeviceStatus;
|
|
231
|
-
|
|
232
264
|
if (!status) throw new Error("DeviceStatus missing");
|
|
233
265
|
|
|
234
266
|
this.updateDeviceStatus(status);
|
|
235
267
|
} catch (error) {
|
|
236
268
|
const msg = error?.response?.data || error.message;
|
|
237
269
|
this.log.error("Failed to refresh heater status:", msg);
|
|
238
|
-
|
|
239
|
-
// Set to INACTIVE on failure to avoid stale "active" UI
|
|
240
270
|
try {
|
|
241
|
-
this.service
|
|
242
|
-
.getCharacteristic(Characteristic.Active)
|
|
271
|
+
this.service.getCharacteristic(Characteristic.Active)
|
|
243
272
|
.updateValue(Characteristic.Active.INACTIVE);
|
|
244
273
|
} catch (_) {}
|
|
245
274
|
} finally {
|
|
@@ -248,7 +277,6 @@ class TesyHeater {
|
|
|
248
277
|
}
|
|
249
278
|
|
|
250
279
|
updateDeviceStatus(status) {
|
|
251
|
-
// Current temperature: status.gradus
|
|
252
280
|
const newCurrentTemperature = parseFloat(status.gradus);
|
|
253
281
|
const oldCurrentTemperature =
|
|
254
282
|
this.service.getCharacteristic(Characteristic.CurrentTemperature).value;
|
|
@@ -269,7 +297,6 @@ class TesyHeater {
|
|
|
269
297
|
);
|
|
270
298
|
}
|
|
271
299
|
|
|
272
|
-
// Target temp: status.ref_gradus
|
|
273
300
|
const newHeatingThresholdTemperature = parseFloat(status.ref_gradus);
|
|
274
301
|
const oldHeatingThresholdTemperature =
|
|
275
302
|
this.service.getCharacteristic(Characteristic.HeatingThresholdTemperature).value;
|
|
@@ -290,7 +317,6 @@ class TesyHeater {
|
|
|
290
317
|
);
|
|
291
318
|
}
|
|
292
319
|
|
|
293
|
-
// Power state: status.power_sw => "on" / "off"
|
|
294
320
|
const newHeaterActiveStatus = this.getTesyHeaterActiveState(status.power_sw);
|
|
295
321
|
const oldHeaterActiveStatus =
|
|
296
322
|
this.service.getCharacteristic(Characteristic.Active).value;
|
|
@@ -309,7 +335,6 @@ class TesyHeater {
|
|
|
309
335
|
);
|
|
310
336
|
}
|
|
311
337
|
|
|
312
|
-
// Heating state: status.heater_state => READY / HEATING
|
|
313
338
|
const newCurrentHeaterCoolerState = this.getTesyHeaterCurrentHeaterCoolerState(
|
|
314
339
|
status.heater_state
|
|
315
340
|
);
|
|
@@ -330,33 +355,27 @@ class TesyHeater {
|
|
|
330
355
|
|
|
331
356
|
// ---- HomeKit characteristic handlers ----
|
|
332
357
|
getActive(callback) {
|
|
333
|
-
// Return current cached value immediately
|
|
334
358
|
try {
|
|
335
359
|
const v = this.service.getCharacteristic(Characteristic.Active).value;
|
|
336
360
|
callback(null, v);
|
|
337
361
|
} catch (e) {
|
|
338
362
|
callback(e);
|
|
339
363
|
}
|
|
340
|
-
|
|
341
|
-
// Also trigger a background refresh (not blocking callback)
|
|
342
364
|
this.refreshTesyHeaterStatus().catch(() => {});
|
|
343
365
|
}
|
|
344
366
|
|
|
345
367
|
async setActive(value, callback) {
|
|
346
368
|
this.log.info("[+] Changing Active status to value:", value);
|
|
347
|
-
|
|
348
369
|
this.pullTimer.stop();
|
|
349
370
|
const newValue = value === 0 ? "off" : "on";
|
|
350
371
|
|
|
351
372
|
try {
|
|
352
|
-
if (!this.session
|
|
353
|
-
await this.authenticate();
|
|
354
|
-
}
|
|
373
|
+
if (!this.session) await this.authenticate();
|
|
355
374
|
|
|
356
|
-
await
|
|
375
|
+
await api.post(
|
|
357
376
|
"https://ad.mytesy.com/rest/old-app-set-device-status",
|
|
358
377
|
{
|
|
359
|
-
ALT: this.alt,
|
|
378
|
+
ALT: this.alt || undefined,
|
|
360
379
|
CURRENT_SESSION: null,
|
|
361
380
|
PHPSESSID: this.session,
|
|
362
381
|
last_login_username: this.username,
|
|
@@ -364,15 +383,13 @@ class TesyHeater {
|
|
|
364
383
|
apiVersion: "apiv1",
|
|
365
384
|
command: "power_sw",
|
|
366
385
|
value: newValue,
|
|
367
|
-
userID: this.userid,
|
|
386
|
+
userID: this.userid || "",
|
|
368
387
|
userEmail: this.username,
|
|
369
388
|
userPass: this.password,
|
|
370
389
|
lang: "en",
|
|
371
|
-
}
|
|
372
|
-
{ headers: { "content-type": "application/json" }, timeout: 10000 }
|
|
390
|
+
}
|
|
373
391
|
);
|
|
374
392
|
|
|
375
|
-
// Optimistically update
|
|
376
393
|
this.service
|
|
377
394
|
.getCharacteristic(Characteristic.Active)
|
|
378
395
|
.updateValue(value);
|
|
@@ -388,7 +405,6 @@ class TesyHeater {
|
|
|
388
405
|
}
|
|
389
406
|
|
|
390
407
|
getCurrentTemperature(callback) {
|
|
391
|
-
// Return cached value
|
|
392
408
|
try {
|
|
393
409
|
const v =
|
|
394
410
|
this.service.getCharacteristic(Characteristic.CurrentTemperature).value;
|
|
@@ -396,8 +412,6 @@ class TesyHeater {
|
|
|
396
412
|
} catch (e) {
|
|
397
413
|
callback(e);
|
|
398
414
|
}
|
|
399
|
-
|
|
400
|
-
// Kick a refresh to keep it warm
|
|
401
415
|
this.refreshTesyHeaterStatus().catch(() => {});
|
|
402
416
|
}
|
|
403
417
|
|
|
@@ -410,14 +424,12 @@ class TesyHeater {
|
|
|
410
424
|
this.pullTimer.stop();
|
|
411
425
|
|
|
412
426
|
try {
|
|
413
|
-
if (!this.session
|
|
414
|
-
await this.authenticate();
|
|
415
|
-
}
|
|
427
|
+
if (!this.session) await this.authenticate();
|
|
416
428
|
|
|
417
|
-
await
|
|
429
|
+
await api.post(
|
|
418
430
|
"https://ad.mytesy.com/rest/old-app-set-device-status",
|
|
419
431
|
{
|
|
420
|
-
ALT: this.alt,
|
|
432
|
+
ALT: this.alt || undefined,
|
|
421
433
|
CURRENT_SESSION: null,
|
|
422
434
|
PHPSESSID: this.session,
|
|
423
435
|
last_login_username: this.username,
|
|
@@ -425,15 +437,13 @@ class TesyHeater {
|
|
|
425
437
|
apiVersion: "apiv1",
|
|
426
438
|
command: "tmpT",
|
|
427
439
|
value: v,
|
|
428
|
-
userID: this.userid,
|
|
440
|
+
userID: this.userid || "",
|
|
429
441
|
userEmail: this.username,
|
|
430
442
|
userPass: this.password,
|
|
431
443
|
lang: "en",
|
|
432
|
-
}
|
|
433
|
-
{ headers: { "content-type": "application/json" }, timeout: 10000 }
|
|
444
|
+
}
|
|
434
445
|
);
|
|
435
446
|
|
|
436
|
-
// Optimistically update
|
|
437
447
|
this.service
|
|
438
448
|
.getCharacteristic(Characteristic.HeatingThresholdTemperature)
|
|
439
449
|
.updateValue(v);
|
|
@@ -450,7 +460,6 @@ class TesyHeater {
|
|
|
450
460
|
|
|
451
461
|
// ---- Services ----
|
|
452
462
|
getServices() {
|
|
453
|
-
// Trigger an initial refresh (non-blocking)
|
|
454
463
|
this.refreshTesyHeaterStatus().catch(() => {});
|
|
455
464
|
return [this.informationService, this.service];
|
|
456
465
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-tesy-heater-api-v4",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Homebridge plugin for Tesy Heater (
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "Homebridge plugin for Tesy Heater (Tesy API old-app endpoints with cookie-based session)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"homebridge-plugin",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"axios": "^1.7.0",
|
|
18
|
+
"axios-cookiejar-support": "^6.0.2",
|
|
19
|
+
"tough-cookie": "^4.1.3",
|
|
18
20
|
"homebridge-http-base": "^1.0.0"
|
|
19
21
|
}
|
|
20
22
|
}
|