homebridge-tesy-heater-api-v4 0.0.4 → 0.0.6

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 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
- # Homebridge Tesy Heater (v2)
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
- Homebridge plugin for Tesy smart heaters using the new v4 API.
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.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "bridge": {
3
+ "name": "Homebridge",
4
+ "username": "0E:00:00:00:00:01",
5
+ "port": 51826,
6
+ "pin": "031-45-154"
7
+ },
8
+ "accessories": [
9
+ {
10
+ "accessory": "TesyHeater",
11
+ "name": "Living Room Heater",
12
+ "manufacturer": "Tesy",
13
+ "model": "Convector (Heater)",
14
+ "device_id": "YOUR_DEVICE_ID",
15
+ "userid": "28046",
16
+ "username": "mladen@popov.mp",
17
+ "password": "fyndex-9bIbno-cakcyd",
18
+ "pullInterval": 10000,
19
+ "maxTemp": 30,
20
+ "minTemp": 10
21
+ }
22
+ ],
23
+ "platforms": []
24
+ }
@@ -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 so Home app doesn't show "No Response" before first refresh
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 (created now, started after login) ----
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((e) => {
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 { data } = await axios.post(
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
- // Expected fields from Tesy "old-app" login:
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 = data.acc_alt || data.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 || !this.alt) {
187
- throw new Error("Missing session/alt in login response");
212
+ if (!this.session) {
213
+ throw new Error("Missing session (neither JSON nor cookie contained PHPSESSID)");
188
214
  }
189
215
 
190
- this.log.info("Authenticated to Tesy (old-app).");
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 || !this.alt) {
206
- this.log.warn("No session/alt yet; re-authenticating...");
233
+ if (!this.session) {
234
+ this.log.warn("No session yet; authenticating...");
207
235
  await this.authenticate();
208
236
  }
209
237
 
210
- const { data } = await axios.post(
211
- "https://ad.mytesy.com/rest/old-app-devices",
212
- {
213
- ALT: this.alt,
214
- CURRENT_SESSION: null,
215
- PHPSESSID: this.session,
216
- last_login_username: this.username,
217
- userID: this.userid,
218
- userEmail: this.username,
219
- userPass: this.password,
220
- lang: "en",
221
- },
222
- { headers: { "content-type": "application/json" }, timeout: 10000 }
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?.device || Object.keys(data.device).length === 0) {
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 || !this.alt) {
353
- await this.authenticate();
354
- }
373
+ if (!this.session) await this.authenticate();
355
374
 
356
- await axios.post(
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 || !this.alt) {
414
- await this.authenticate();
415
- }
427
+ if (!this.session) await this.authenticate();
416
428
 
417
- await axios.post(
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",
4
- "description": "Homebridge plugin for Tesy Heater (updated APIv4)",
3
+ "version": "0.0.6",
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
  }