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