homebridge-tryfi 1.1.0 → 1.2.0
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/CHANGELOG.md +73 -0
- package/README.md +308 -21
- package/config.schema.json +24 -0
- package/dist/accessory.d.ts +1 -0
- package/dist/accessory.d.ts.map +1 -1
- package/dist/accessory.js +21 -7
- package/dist/accessory.js.map +1 -1
- package/dist/api.d.ts +2 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +42 -6
- package/dist/api.js.map +1 -1
- package/dist/platform.d.ts +16 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +148 -2
- package/dist/platform.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/accessory.ts +28 -12
- package/src/api.ts +52 -7
- package/src/platform.ts +172 -5
- package/src/types.ts +2 -0
package/src/platform.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from 'homebridge';
|
|
10
10
|
import { TryFiAPI } from './api';
|
|
11
11
|
import { TryFiCollarAccessory } from './accessory';
|
|
12
|
-
import { TryFiPlatformConfig } from './types';
|
|
12
|
+
import { TryFiPlatformConfig, TryFiPet } from './types';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* TryFi Platform Plugin
|
|
@@ -25,6 +25,10 @@ export class TryFiPlatform implements DynamicPlatformPlugin {
|
|
|
25
25
|
public readonly tryfiApi: TryFiAPI;
|
|
26
26
|
public readonly api: TryFiAPI; // Alias for accessory use
|
|
27
27
|
private pollingInterval?: NodeJS.Timeout;
|
|
28
|
+
|
|
29
|
+
// Escape alert hysteresis tracking (in-memory only, resets on restart)
|
|
30
|
+
private escapeCounters: Map<string, number> = new Map();
|
|
31
|
+
private quickCheckTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
|
28
32
|
|
|
29
33
|
constructor(
|
|
30
34
|
public readonly log: Logger,
|
|
@@ -161,8 +165,29 @@ export class TryFiPlatform implements DynamicPlatformPlugin {
|
|
|
161
165
|
|
|
162
166
|
// Start polling for updates
|
|
163
167
|
this.startPolling();
|
|
164
|
-
} catch (error) {
|
|
165
|
-
|
|
168
|
+
} catch (error: any) {
|
|
169
|
+
// Handle different error types during discovery
|
|
170
|
+
if (error.response?.status) {
|
|
171
|
+
const status = error.response.status;
|
|
172
|
+
|
|
173
|
+
// Transient server errors - warn and start polling anyway (will retry)
|
|
174
|
+
if (status === 502 || status === 503 || status === 504) {
|
|
175
|
+
this.log.warn(`TryFi API temporarily unavailable (${status}) during startup`);
|
|
176
|
+
this.log.warn('Will continue to retry during polling');
|
|
177
|
+
this.startPolling(); // Start polling anyway, will retry
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Authentication errors
|
|
182
|
+
if (status === 401 || status === 403) {
|
|
183
|
+
this.log.error('Authentication failed - please check your username and password');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.log.error(`Failed to discover TryFi devices (HTTP ${status}):`, error.message);
|
|
188
|
+
} else {
|
|
189
|
+
this.log.error('Failed to discover TryFi devices:', error.message || error);
|
|
190
|
+
}
|
|
166
191
|
}
|
|
167
192
|
}
|
|
168
193
|
|
|
@@ -200,12 +225,148 @@ export class TryFiPlatform implements DynamicPlatformPlugin {
|
|
|
200
225
|
const accessory = this.collarAccessories.get(pet.petId);
|
|
201
226
|
if (accessory) {
|
|
202
227
|
accessory.updatePetData(pet);
|
|
228
|
+
|
|
229
|
+
// Handle escape alert hysteresis
|
|
230
|
+
this.handleEscapeDetection(pet);
|
|
203
231
|
}
|
|
204
232
|
}
|
|
205
233
|
|
|
206
234
|
this.log.debug(`Updated ${pets.length} collar(s)`);
|
|
207
|
-
} catch (error) {
|
|
208
|
-
|
|
235
|
+
} catch (error: any) {
|
|
236
|
+
// Handle different error types appropriately
|
|
237
|
+
if (error.response?.status) {
|
|
238
|
+
const status = error.response.status;
|
|
239
|
+
|
|
240
|
+
// Transient server errors (502, 503, 504) - just warn and retry next interval
|
|
241
|
+
if (status === 502 || status === 503 || status === 504) {
|
|
242
|
+
this.log.warn(`TryFi API temporarily unavailable (${status}), will retry on next poll`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Authentication errors (401, 403) - try to re-authenticate
|
|
247
|
+
if (status === 401 || status === 403) {
|
|
248
|
+
this.log.warn('Authentication expired, attempting to re-login...');
|
|
249
|
+
try {
|
|
250
|
+
await this.tryfiApi.login();
|
|
251
|
+
this.log.info('Successfully re-authenticated with TryFi');
|
|
252
|
+
// Try polling again immediately after re-auth
|
|
253
|
+
const allPets = await this.tryfiApi.getPets();
|
|
254
|
+
const ignoredPets = (this.config.ignoredPets || []).map(name => name.toLowerCase());
|
|
255
|
+
const pets = allPets.filter(pet => !ignoredPets.includes(pet.name.toLowerCase()));
|
|
256
|
+
for (const pet of pets) {
|
|
257
|
+
const accessory = this.collarAccessories.get(pet.petId);
|
|
258
|
+
if (accessory) {
|
|
259
|
+
accessory.updatePetData(pet);
|
|
260
|
+
this.handleEscapeDetection(pet);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
this.log.debug(`Updated ${pets.length} collar(s) after re-auth`);
|
|
264
|
+
} catch (reAuthError) {
|
|
265
|
+
this.log.error('Failed to re-authenticate with TryFi:', reAuthError);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Other HTTP errors
|
|
271
|
+
this.log.error(`TryFi API error (${status}):`, error.message);
|
|
272
|
+
} else {
|
|
273
|
+
// Network errors, timeouts, etc.
|
|
274
|
+
this.log.error('Failed to poll TryFi API:', error.message || error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Handle escape detection with hysteresis (debouncing)
|
|
281
|
+
* Prevents false alarms from GPS drift by requiring multiple consecutive detections
|
|
282
|
+
*/
|
|
283
|
+
private handleEscapeDetection(pet: TryFiPet) {
|
|
284
|
+
// Check if pet is escaped (out of zone AND not with anyone)
|
|
285
|
+
const isEscaped = (pet.placeName === null && pet.connectedToUser === null);
|
|
286
|
+
|
|
287
|
+
const currentCount = this.escapeCounters.get(pet.petId) || 0;
|
|
288
|
+
const confirmationsRequired = this.config.escapeConfirmations || 2;
|
|
289
|
+
|
|
290
|
+
if (isEscaped) {
|
|
291
|
+
// Potential escape detected - increment counter
|
|
292
|
+
const newCount = currentCount + 1;
|
|
293
|
+
this.escapeCounters.set(pet.petId, newCount);
|
|
294
|
+
|
|
295
|
+
this.log.debug(
|
|
296
|
+
`${pet.name} out of zone (${newCount}/${confirmationsRequired} confirmations)`,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (newCount >= confirmationsRequired) {
|
|
300
|
+
// Threshold reached - escape confirmed
|
|
301
|
+
// Accessory will handle the actual alert state update
|
|
302
|
+
this.log.info(`${pet.name} escape confirmed after ${newCount} check(s)`);
|
|
303
|
+
} else {
|
|
304
|
+
// Not confirmed yet - schedule quick re-check
|
|
305
|
+
this.scheduleQuickCheck(pet.petId, pet.name);
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
// Pet is safe (in zone OR with someone) - reset counter
|
|
309
|
+
const hadCount = currentCount > 0;
|
|
310
|
+
this.escapeCounters.set(pet.petId, 0);
|
|
311
|
+
this.cancelQuickCheck(pet.petId);
|
|
312
|
+
|
|
313
|
+
if (hadCount) {
|
|
314
|
+
this.log.debug(`${pet.name} back in safe state, reset escape counter`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Schedule a quick check for a specific pet
|
|
321
|
+
* Used during escape detection to re-verify faster than normal polling
|
|
322
|
+
*/
|
|
323
|
+
private scheduleQuickCheck(petId: string, petName: string) {
|
|
324
|
+
// Don't schedule if already scheduled
|
|
325
|
+
if (this.quickCheckTimeouts.has(petId)) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const interval = (this.config.escapeCheckInterval || 30) * 1000;
|
|
330
|
+
|
|
331
|
+
this.log.debug(`Scheduling quick check for ${petName} in ${interval / 1000}s`);
|
|
332
|
+
|
|
333
|
+
const timeout = setTimeout(async () => {
|
|
334
|
+
this.log.debug(`Running quick check for ${petName}`);
|
|
335
|
+
|
|
336
|
+
// Remove from timeouts map
|
|
337
|
+
this.quickCheckTimeouts.delete(petId);
|
|
338
|
+
|
|
339
|
+
// Poll just this one pet
|
|
340
|
+
try {
|
|
341
|
+
const allPets = await this.tryfiApi.getPets();
|
|
342
|
+
const pet = allPets.find(p => p.petId === petId);
|
|
343
|
+
|
|
344
|
+
if (pet) {
|
|
345
|
+
const accessory = this.collarAccessories.get(petId);
|
|
346
|
+
if (accessory) {
|
|
347
|
+
accessory.updatePetData(pet);
|
|
348
|
+
|
|
349
|
+
// Handle escape detection (may schedule another quick check)
|
|
350
|
+
this.handleEscapeDetection(pet);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch (error: any) {
|
|
354
|
+
this.log.warn(`Quick check failed for ${petName}:`, error.message || error);
|
|
355
|
+
// Counter will remain, next normal poll will try again
|
|
356
|
+
}
|
|
357
|
+
}, interval);
|
|
358
|
+
|
|
359
|
+
this.quickCheckTimeouts.set(petId, timeout);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Cancel any pending quick check for a pet
|
|
364
|
+
*/
|
|
365
|
+
private cancelQuickCheck(petId: string) {
|
|
366
|
+
const timeout = this.quickCheckTimeouts.get(petId);
|
|
367
|
+
if (timeout) {
|
|
368
|
+
clearTimeout(timeout);
|
|
369
|
+
this.quickCheckTimeouts.delete(petId);
|
|
209
370
|
}
|
|
210
371
|
}
|
|
211
372
|
|
|
@@ -216,5 +377,11 @@ export class TryFiPlatform implements DynamicPlatformPlugin {
|
|
|
216
377
|
if (this.pollingInterval) {
|
|
217
378
|
clearInterval(this.pollingInterval);
|
|
218
379
|
}
|
|
380
|
+
|
|
381
|
+
// Cancel all pending quick checks
|
|
382
|
+
for (const timeout of this.quickCheckTimeouts.values()) {
|
|
383
|
+
clearTimeout(timeout);
|
|
384
|
+
}
|
|
385
|
+
this.quickCheckTimeouts.clear();
|
|
219
386
|
}
|
|
220
387
|
}
|
package/src/types.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface TryFiPlatformConfig extends PlatformConfig {
|
|
|
9
9
|
pollingInterval?: number; // seconds, default 60
|
|
10
10
|
escapeAlertType?: 'leak' | 'motion'; // default 'leak'
|
|
11
11
|
ignoredPets?: string[]; // pet names to ignore (case-insensitive)
|
|
12
|
+
escapeConfirmations?: number; // consecutive out-of-zone readings required, default 2
|
|
13
|
+
escapeCheckInterval?: number; // seconds between quick checks, default 30
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/**
|