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 +22 -8
- package/config.schema.json +6 -1
- package/index.js +394 -65
- package/index.js-backup +137 -0
- 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,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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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("
|
|
229
|
+
this.log.debug("Executing refreshTesyHeaterStatus");
|
|
230
|
+
this.pullTimer.stop();
|
|
231
|
+
|
|
58
232
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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.
|
|
73
|
-
|
|
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
|
|
76
|
-
|
|
320
|
+
const newHeaterActiveStatus = this.getTesyHeaterActiveState(status.power_sw);
|
|
321
|
+
const oldHeaterActiveStatus =
|
|
322
|
+
this.service.getCharacteristic(Characteristic.Active).value;
|
|
77
323
|
|
|
78
|
-
|
|
79
|
-
|
|
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("
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
this.log.info("Setting target temperature to:", value);
|
|
407
|
+
getCurrentTemperature(callback) {
|
|
100
408
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
this.
|
|
117
|
-
|
|
118
|
-
|
|
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.
|
|
122
|
-
.on("set", this.setActive.bind(this));
|
|
424
|
+
this.pullTimer.stop();
|
|
123
425
|
|
|
124
|
-
|
|
125
|
-
.
|
|
426
|
+
try {
|
|
427
|
+
if (!this.session) await this.authenticate();
|
|
126
428
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
}
|
package/index.js-backup
ADDED
|
@@ -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.
|
|
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
|
}
|