homebridge-tesy-heater-api-v4 0.0.3 → 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 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`
@@ -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,3 +1,6 @@
1
+ // index.js
2
+ const { wrapper } = require("axios-cookiejar-support");
3
+ const { CookieJar } = require("tough-cookie");
1
4
  const axios = require("axios");
2
5
  const _http_base = require("homebridge-http-base");
3
6
  const PullTimer = _http_base.PullTimer;
@@ -7,12 +10,32 @@ let Service, Characteristic;
7
10
  module.exports = function (homebridge) {
8
11
  Service = homebridge.hap.Service;
9
12
  Characteristic = homebridge.hap.Characteristic;
10
- homebridge.registerAccessory("homebridge-tesy-heater-v2", "TesyHeater", TesyHeater);
13
+ // Keep the package name here in case you want to fully-qualify in config:
14
+ homebridge.registerAccessory("homebridge-tesy-heater-api-v4", "TesyHeater", TesyHeater);
11
15
  };
12
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
+
13
34
  class TesyHeater {
14
35
  constructor(log, config) {
15
36
  this.log = log;
37
+
38
+ // ---- Config ----
16
39
  this.name = config.name;
17
40
  this.manufacturer = config.manufacturer || "Tesy";
18
41
  this.model = config.model || "Convector (Heater)";
@@ -20,118 +43,424 @@ class TesyHeater {
20
43
  this.pullInterval = config.pullInterval || 10000;
21
44
  this.maxTemp = config.maxTemp || 30;
22
45
  this.minTemp = config.minTemp || 10;
46
+
47
+ this.userid = config.userid || null; // optional; some accounts have it
23
48
  this.username = config.username || null;
24
49
  this.password = config.password || null;
25
- this.session = "";
26
- this.alt = "";
27
50
 
28
- if (this.username && this.password) {
29
- this.authenticate();
30
- }
51
+ // ---- Session state returned by login ----
52
+ this.session = ""; // PHPSESSID / acc_session
53
+ this.alt = ""; // ALT / acc_alt
31
54
 
55
+ // ---- HomeKit Service ----
32
56
  this.service = new Service.HeaterCooler(this.name);
57
+
58
+ // Default initial states
59
+ this.service
60
+ .getCharacteristic(Characteristic.CurrentHeaterCoolerState)
61
+ .updateValue(Characteristic.CurrentHeaterCoolerState.INACTIVE);
62
+
63
+ this.service
64
+ .getCharacteristic(Characteristic.Active)
65
+ .updateValue(Characteristic.Active.INACTIVE);
66
+
67
+ this.service
68
+ .getCharacteristic(Characteristic.CurrentTemperature)
69
+ .updateValue(this.minTemp);
70
+
71
+ this.service
72
+ .getCharacteristic(Characteristic.HeatingThresholdTemperature)
73
+ .setProps({
74
+ minValue: this.minTemp,
75
+ maxValue: this.maxTemp,
76
+ minStep: 0.5,
77
+ })
78
+ .updateValue(this.minTemp);
79
+
80
+ // Add CoolingThresholdTemperature so Home shows the marker on the wheel
81
+ this.service
82
+ .getCharacteristic(Characteristic.CoolingThresholdTemperature)
83
+ .setProps({
84
+ minValue: this.minTemp,
85
+ maxValue: this.maxTemp,
86
+ minStep: 0.5,
87
+ })
88
+ .updateValue(this.minTemp);
89
+
90
+ // Only support HEAT for TargetHeaterCoolerState
91
+ this.service
92
+ .getCharacteristic(Characteristic.TargetHeaterCoolerState)
93
+ .setProps({
94
+ validValues: [Characteristic.TargetHeaterCoolerState.HEAT],
95
+ });
96
+
97
+ // ---- Bind getters/setters ----
98
+ this.service
99
+ .getCharacteristic(Characteristic.Active)
100
+ .on("get", this.getActive.bind(this))
101
+ .on("set", this.setActive.bind(this));
102
+
103
+ this.service
104
+ .getCharacteristic(Characteristic.CurrentTemperature)
105
+ .setProps({ minStep: 0.1 })
106
+ .on("get", this.getCurrentTemperature.bind(this));
107
+
108
+ this.service
109
+ .getCharacteristic(Characteristic.HeatingThresholdTemperature)
110
+ .on("set", this.setHeatingThresholdTemperature.bind(this));
111
+
112
+ this.service
113
+ .getCharacteristic(Characteristic.TargetHeaterCoolerState)
114
+ .on("get", this.getTargetHeaterCoolerState.bind(this));
115
+
116
+ this.service
117
+ .getCharacteristic(Characteristic.Name)
118
+ .on("get", this.getName.bind(this));
119
+
120
+ // ---- Accessory Information ----
121
+ this.informationService = new Service.AccessoryInformation();
122
+ this.informationService
123
+ .setCharacteristic(Characteristic.Manufacturer, this.manufacturer)
124
+ .setCharacteristic(Characteristic.Model, this.model)
125
+ .setCharacteristic(Characteristic.SerialNumber, this.device_id);
126
+
127
+ // ---- Timer ----
33
128
  this.pullTimer = new PullTimer(
34
129
  this.log,
35
130
  this.pullInterval,
36
131
  this.refreshTesyHeaterStatus.bind(this),
37
132
  () => {}
38
133
  );
39
- this.pullTimer.start();
134
+
135
+ // ---- Kick off login, then start polling ----
136
+ if (this.username && this.password) {
137
+ this.authenticate()
138
+ .then(() => {
139
+ this.pullTimer.start();
140
+ this.refreshTesyHeaterStatus();
141
+ })
142
+ .catch(() => {
143
+ this.log.error("Initial authentication failed. Will still start timer and retry on next ticks.");
144
+ this.pullTimer.start();
145
+ });
146
+ } else {
147
+ this.log.warn("Username/password not provided; device will remain INACTIVE.");
148
+ this.pullTimer.start();
149
+ }
150
+
151
+ this.log.info(this.name);
152
+ }
153
+
154
+ // ---- HomeKit identity ----
155
+ identify(callback) {
156
+ this.log.info("Hi, I'm", this.name);
157
+ callback();
158
+ }
159
+
160
+ getName(callback) {
161
+ callback(null, this.name);
162
+ }
163
+
164
+ getTargetHeaterCoolerState(callback) {
165
+ callback(null, Characteristic.TargetHeaterCoolerState.HEAT);
166
+ }
167
+
168
+ // ---- Helpers to map Tesy fields ----
169
+ getTesyHeaterActiveState(state) {
170
+ if (!state) return Characteristic.Active.INACTIVE;
171
+ return state.toLowerCase() === "on"
172
+ ? Characteristic.Active.ACTIVE
173
+ : Characteristic.Active.INACTIVE;
40
174
  }
41
175
 
176
+ getTesyHeaterCurrentHeaterCoolerState(state) {
177
+ if (!state) return Characteristic.CurrentHeaterCoolerState.INACTIVE;
178
+ return state.toUpperCase() === "READY"
179
+ ? Characteristic.CurrentHeaterCoolerState.IDLE
180
+ : Characteristic.CurrentHeaterCoolerState.HEATING;
181
+ }
182
+
183
+ // ---- API calls (axios + cookies) ----
42
184
  async authenticate() {
43
185
  try {
44
- const response = await axios.post("https://v4.mytesy.com/auth/login", {
45
- email: this.username,
46
- password: this.password,
47
- });
48
- this.session = response.data.session_token;
49
- this.alt = response.data.alt_key;
50
- this.log.info("Successfully authenticated with Tesy API");
186
+ const resp = await api.post(
187
+ "https://ad.mytesy.com/rest/old-app-login",
188
+ {
189
+ email: this.username,
190
+ password: this.password,
191
+ userID: this.userid || "",
192
+ userEmail: this.username,
193
+ userPass: this.password,
194
+ lang: "en",
195
+ }
196
+ );
197
+
198
+ const data = resp.data || {};
199
+ this.log.debug("Login response keys:", Object.keys(data));
200
+
201
+ // Preferred (old behavior)
202
+ this.session = data.acc_session || data.PHPSESSID || "";
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
+ }
211
+
212
+ if (!this.session) {
213
+ throw new Error("Missing session (neither JSON nor cookie contained PHPSESSID)");
214
+ }
215
+
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).");
51
221
  } catch (error) {
52
- this.log.error("Authentication failed:", error.response?.data || error.message);
222
+ const msg = error?.response?.data || error.message;
223
+ this.log.error("Authentication failed:", msg);
224
+ throw error;
53
225
  }
54
226
  }
55
227
 
56
228
  async refreshTesyHeaterStatus() {
57
- this.log.debug("Refreshing heater status");
229
+ this.log.debug("Executing refreshTesyHeaterStatus");
230
+ this.pullTimer.stop();
231
+
58
232
  try {
59
- const response = await axios.post("https://v4.mytesy.com/devices/status", {
60
- session_token: this.session,
61
- device_id: this.device_id,
62
- });
233
+ if (!this.session) {
234
+ this.log.warn("No session yet; authenticating...");
235
+ await this.authenticate();
236
+ }
237
+
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
+ }
257
+
258
+ if (!data.device || Object.keys(data.device).length === 0) {
259
+ throw new Error("No devices in response");
260
+ }
261
+
262
+ const firstKey = Object.keys(data.device)[0];
263
+ const status = data.device[firstKey]?.DeviceStatus;
264
+ if (!status) throw new Error("DeviceStatus missing");
63
265
 
64
- const status = response.data.device_status;
65
266
  this.updateDeviceStatus(status);
66
267
  } catch (error) {
67
- this.log.error("Failed to refresh heater status:", error.response?.data || error.message);
268
+ const msg = error?.response?.data || error.message;
269
+ this.log.error("Failed to refresh heater status:", msg);
270
+ try {
271
+ this.service.getCharacteristic(Characteristic.Active)
272
+ .updateValue(Characteristic.Active.INACTIVE);
273
+ } catch (_) {}
274
+ } finally {
275
+ this.pullTimer.start();
68
276
  }
69
277
  }
70
278
 
71
279
  updateDeviceStatus(status) {
72
- const newCurrentTemperature = parseFloat(status.temperature);
73
- this.service.getCharacteristic(Characteristic.CurrentTemperature).updateValue(newCurrentTemperature);
280
+ const newCurrentTemperature = parseFloat(status.gradus);
281
+ const oldCurrentTemperature =
282
+ this.service.getCharacteristic(Characteristic.CurrentTemperature).value;
283
+
284
+ if (
285
+ Number.isFinite(newCurrentTemperature) &&
286
+ newCurrentTemperature !== oldCurrentTemperature &&
287
+ newCurrentTemperature >= this.minTemp &&
288
+ newCurrentTemperature <= this.maxTemp
289
+ ) {
290
+ this.service
291
+ .getCharacteristic(Characteristic.CurrentTemperature)
292
+ .updateValue(newCurrentTemperature);
293
+ this.log.info(
294
+ "Changing CurrentTemperature from %s to %s",
295
+ oldCurrentTemperature,
296
+ newCurrentTemperature
297
+ );
298
+ }
299
+
300
+ const newHeatingThresholdTemperature = parseFloat(status.ref_gradus);
301
+ const oldHeatingThresholdTemperature =
302
+ this.service.getCharacteristic(Characteristic.HeatingThresholdTemperature).value;
303
+
304
+ if (
305
+ Number.isFinite(newHeatingThresholdTemperature) &&
306
+ newHeatingThresholdTemperature !== oldHeatingThresholdTemperature &&
307
+ newHeatingThresholdTemperature >= this.minTemp &&
308
+ newHeatingThresholdTemperature <= this.maxTemp
309
+ ) {
310
+ this.service
311
+ .getCharacteristic(Characteristic.HeatingThresholdTemperature)
312
+ .updateValue(newHeatingThresholdTemperature);
313
+ this.log.info(
314
+ "Changing HeatingThresholdTemperature from %s to %s",
315
+ oldHeatingThresholdTemperature,
316
+ newHeatingThresholdTemperature
317
+ );
318
+ }
74
319
 
75
- const newHeatingThresholdTemperature = parseFloat(status.target_temperature);
76
- this.service.getCharacteristic(Characteristic.HeatingThresholdTemperature).updateValue(newHeatingThresholdTemperature);
320
+ const newHeaterActiveStatus = this.getTesyHeaterActiveState(status.power_sw);
321
+ const oldHeaterActiveStatus =
322
+ this.service.getCharacteristic(Characteristic.Active).value;
77
323
 
78
- const isActive = status.power === "on" ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE;
79
- this.service.getCharacteristic(Characteristic.Active).updateValue(isActive);
324
+ if (
325
+ newHeaterActiveStatus !== undefined &&
326
+ newHeaterActiveStatus !== oldHeaterActiveStatus
327
+ ) {
328
+ this.service
329
+ .getCharacteristic(Characteristic.Active)
330
+ .updateValue(newHeaterActiveStatus);
331
+ this.log.info(
332
+ "Changing ActiveStatus from %s to %s",
333
+ oldHeaterActiveStatus,
334
+ newHeaterActiveStatus
335
+ );
336
+ }
337
+
338
+ const newCurrentHeaterCoolerState = this.getTesyHeaterCurrentHeaterCoolerState(
339
+ status.heater_state
340
+ );
341
+ const oldCurrentHeaterCoolerState =
342
+ this.service.getCharacteristic(Characteristic.CurrentHeaterCoolerState).value;
343
+
344
+ if (newCurrentHeaterCoolerState !== oldCurrentHeaterCoolerState) {
345
+ this.service
346
+ .getCharacteristic(Characteristic.CurrentHeaterCoolerState)
347
+ .updateValue(newCurrentHeaterCoolerState);
348
+ this.log.info(
349
+ "Changing CurrentHeaterCoolerState from %s to %s",
350
+ oldCurrentHeaterCoolerState,
351
+ newCurrentHeaterCoolerState
352
+ );
353
+ }
354
+ }
355
+
356
+ // ---- HomeKit characteristic handlers ----
357
+ getActive(callback) {
358
+ try {
359
+ const v = this.service.getCharacteristic(Characteristic.Active).value;
360
+ callback(null, v);
361
+ } catch (e) {
362
+ callback(e);
363
+ }
364
+ this.refreshTesyHeaterStatus().catch(() => {});
80
365
  }
81
366
 
82
367
  async setActive(value, callback) {
83
- this.log.info("Setting heater state:", value);
368
+ this.log.info("[+] Changing Active status to value:", value);
369
+ this.pullTimer.stop();
370
+ const newValue = value === 0 ? "off" : "on";
371
+
84
372
  try {
85
- await axios.post("https://v4.mytesy.com/devices/set", {
86
- session_token: this.session,
87
- device_id: this.device_id,
88
- command: "power",
89
- value: value === 0 ? "off" : "on",
90
- });
373
+ if (!this.session) await this.authenticate();
374
+
375
+ await api.post(
376
+ "https://ad.mytesy.com/rest/old-app-set-device-status",
377
+ {
378
+ ALT: this.alt || undefined,
379
+ CURRENT_SESSION: null,
380
+ PHPSESSID: this.session,
381
+ last_login_username: this.username,
382
+ id: this.device_id,
383
+ apiVersion: "apiv1",
384
+ command: "power_sw",
385
+ value: newValue,
386
+ userID: this.userid || "",
387
+ userEmail: this.username,
388
+ userPass: this.password,
389
+ lang: "en",
390
+ }
391
+ );
392
+
393
+ this.service
394
+ .getCharacteristic(Characteristic.Active)
395
+ .updateValue(value);
396
+
91
397
  callback(null, value);
92
398
  } catch (error) {
93
- this.log.error("Failed to set heater state:", error.response?.data || error.message);
399
+ const msg = error?.response?.data || error.message;
400
+ this.log.error("Failed to set Active:", msg);
94
401
  callback(error);
402
+ } finally {
403
+ this.pullTimer.start();
95
404
  }
96
405
  }
97
406
 
98
- async setHeatingThresholdTemperature(value, callback) {
99
- this.log.info("Setting target temperature to:", value);
407
+ getCurrentTemperature(callback) {
100
408
  try {
101
- await axios.post("https://v4.mytesy.com/devices/set", {
102
- session_token: this.session,
103
- device_id: this.device_id,
104
- command: "target_temperature",
105
- value: value,
106
- });
107
- callback(null, value);
108
- } catch (error) {
109
- this.log.error("Failed to set target temperature:", error.response?.data || error.message);
110
- callback(error);
409
+ const v =
410
+ this.service.getCharacteristic(Characteristic.CurrentTemperature).value;
411
+ callback(null, v);
412
+ } catch (e) {
413
+ callback(e);
111
414
  }
415
+ this.refreshTesyHeaterStatus().catch(() => {});
112
416
  }
113
417
 
114
- getServices() {
115
- this.informationService = new Service.AccessoryInformation();
116
- this.informationService
117
- .setCharacteristic(Characteristic.Manufacturer, this.manufacturer)
118
- .setCharacteristic(Characteristic.Model, this.model)
119
- .setCharacteristic(Characteristic.SerialNumber, this.device_id);
418
+ async setHeatingThresholdTemperature(value, callback) {
419
+ let v = value;
420
+ if (v < this.minTemp) v = this.minTemp;
421
+ if (v > this.maxTemp) v = this.maxTemp;
422
+ this.log.info("[+] Changing HeatingThresholdTemperature to:", v);
120
423
 
121
- this.service.getCharacteristic(Characteristic.Active)
122
- .on("set", this.setActive.bind(this));
424
+ this.pullTimer.stop();
123
425
 
124
- this.service.getCharacteristic(Characteristic.CurrentTemperature)
125
- .setProps({ minStep: 0.1 });
426
+ try {
427
+ if (!this.session) await this.authenticate();
126
428
 
127
- this.service.getCharacteristic(Characteristic.HeatingThresholdTemperature)
128
- .setProps({
129
- minValue: this.minTemp,
130
- maxValue: this.maxTemp,
131
- minStep: 0.5,
132
- })
133
- .on("set", this.setHeatingThresholdTemperature.bind(this));
429
+ await api.post(
430
+ "https://ad.mytesy.com/rest/old-app-set-device-status",
431
+ {
432
+ ALT: this.alt || undefined,
433
+ CURRENT_SESSION: null,
434
+ PHPSESSID: this.session,
435
+ last_login_username: this.username,
436
+ id: this.device_id,
437
+ apiVersion: "apiv1",
438
+ command: "tmpT",
439
+ value: v,
440
+ userID: this.userid || "",
441
+ userEmail: this.username,
442
+ userPass: this.password,
443
+ lang: "en",
444
+ }
445
+ );
446
+
447
+ this.service
448
+ .getCharacteristic(Characteristic.HeatingThresholdTemperature)
449
+ .updateValue(v);
134
450
 
451
+ callback(null, v);
452
+ } catch (error) {
453
+ const msg = error?.response?.data || error.message;
454
+ this.log.error("Failed to set target temperature:", msg);
455
+ callback(error);
456
+ } finally {
457
+ this.pullTimer.start();
458
+ }
459
+ }
460
+
461
+ // ---- Services ----
462
+ getServices() {
463
+ this.refreshTesyHeaterStatus().catch(() => {});
135
464
  return [this.informationService, this.service];
136
465
  }
137
466
  }
@@ -0,0 +1,137 @@
1
+ const axios = require("axios");
2
+ const _http_base = require("homebridge-http-base");
3
+ const PullTimer = _http_base.PullTimer;
4
+
5
+ let Service, Characteristic;
6
+
7
+ module.exports = function (homebridge) {
8
+ Service = homebridge.hap.Service;
9
+ Characteristic = homebridge.hap.Characteristic;
10
+ homebridge.registerAccessory("homebridge-tesy-heater-v2", "TesyHeater", TesyHeater);
11
+ };
12
+
13
+ class TesyHeater {
14
+ constructor(log, config) {
15
+ this.log = log;
16
+ this.name = config.name;
17
+ this.manufacturer = config.manufacturer || "Tesy";
18
+ this.model = config.model || "Convector (Heater)";
19
+ this.device_id = config.device_id;
20
+ this.pullInterval = config.pullInterval || 10000;
21
+ this.maxTemp = config.maxTemp || 30;
22
+ this.minTemp = config.minTemp || 10;
23
+ this.username = config.username || null;
24
+ this.password = config.password || null;
25
+ this.session = "";
26
+ this.alt = "";
27
+
28
+ if (this.username && this.password) {
29
+ this.authenticate();
30
+ }
31
+
32
+ this.service = new Service.HeaterCooler(this.name);
33
+ this.pullTimer = new PullTimer(
34
+ this.log,
35
+ this.pullInterval,
36
+ this.refreshTesyHeaterStatus.bind(this),
37
+ () => {}
38
+ );
39
+ this.pullTimer.start();
40
+ }
41
+
42
+ async authenticate() {
43
+ try {
44
+ const response = await axios.post("https://v4.mytesy.com/auth/login", {
45
+ email: this.username,
46
+ password: this.password,
47
+ });
48
+ this.session = response.data.session_token;
49
+ this.alt = response.data.alt_key;
50
+ this.log.info("Successfully authenticated with Tesy API");
51
+ } catch (error) {
52
+ this.log.error("Authentication failed:", error.response?.data || error.message);
53
+ }
54
+ }
55
+
56
+ async refreshTesyHeaterStatus() {
57
+ this.log.debug("Refreshing heater status");
58
+ try {
59
+ const response = await axios.post("https://v4.mytesy.com/devices/status", {
60
+ session_token: this.session,
61
+ device_id: this.device_id,
62
+ });
63
+
64
+ const status = response.data.device_status;
65
+ this.updateDeviceStatus(status);
66
+ } catch (error) {
67
+ this.log.error("Failed to refresh heater status:", error.response?.data || error.message);
68
+ }
69
+ }
70
+
71
+ updateDeviceStatus(status) {
72
+ const newCurrentTemperature = parseFloat(status.temperature);
73
+ this.service.getCharacteristic(Characteristic.CurrentTemperature).updateValue(newCurrentTemperature);
74
+
75
+ const newHeatingThresholdTemperature = parseFloat(status.target_temperature);
76
+ this.service.getCharacteristic(Characteristic.HeatingThresholdTemperature).updateValue(newHeatingThresholdTemperature);
77
+
78
+ const isActive = status.power === "on" ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE;
79
+ this.service.getCharacteristic(Characteristic.Active).updateValue(isActive);
80
+ }
81
+
82
+ async setActive(value, callback) {
83
+ this.log.info("Setting heater state:", value);
84
+ try {
85
+ await axios.post("https://v4.mytesy.com/devices/set", {
86
+ session_token: this.session,
87
+ device_id: this.device_id,
88
+ command: "power",
89
+ value: value === 0 ? "off" : "on",
90
+ });
91
+ callback(null, value);
92
+ } catch (error) {
93
+ this.log.error("Failed to set heater state:", error.response?.data || error.message);
94
+ callback(error);
95
+ }
96
+ }
97
+
98
+ async setHeatingThresholdTemperature(value, callback) {
99
+ this.log.info("Setting target temperature to:", value);
100
+ try {
101
+ await axios.post("https://v4.mytesy.com/devices/set", {
102
+ session_token: this.session,
103
+ device_id: this.device_id,
104
+ command: "target_temperature",
105
+ value: value,
106
+ });
107
+ callback(null, value);
108
+ } catch (error) {
109
+ this.log.error("Failed to set target temperature:", error.response?.data || error.message);
110
+ callback(error);
111
+ }
112
+ }
113
+
114
+ getServices() {
115
+ this.informationService = new Service.AccessoryInformation();
116
+ this.informationService
117
+ .setCharacteristic(Characteristic.Manufacturer, this.manufacturer)
118
+ .setCharacteristic(Characteristic.Model, this.model)
119
+ .setCharacteristic(Characteristic.SerialNumber, this.device_id);
120
+
121
+ this.service.getCharacteristic(Characteristic.Active)
122
+ .on("set", this.setActive.bind(this));
123
+
124
+ this.service.getCharacteristic(Characteristic.CurrentTemperature)
125
+ .setProps({ minStep: 0.1 });
126
+
127
+ this.service.getCharacteristic(Characteristic.HeatingThresholdTemperature)
128
+ .setProps({
129
+ minValue: this.minTemp,
130
+ maxValue: this.maxTemp,
131
+ minStep: 0.5,
132
+ })
133
+ .on("set", this.setHeatingThresholdTemperature.bind(this));
134
+
135
+ return [this.informationService, this.service];
136
+ }
137
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-tesy-heater-api-v4",
3
- "version": "0.0.3",
4
- "description": "Homebridge plugin for Tesy Heater (updated APIv4)",
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
  }