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/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
- this.log.error('Failed to discover TryFi devices:', error);
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
- this.log.error('Failed to poll TryFi API:', error);
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
  /**