@camstack/addon-provider-homeassistant 1.1.27 → 1.1.29

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.
Files changed (3) hide show
  1. package/dist/addon.js +262 -24
  2. package/dist/addon.mjs +262 -24
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let node_crypto = require("node:crypto");
3
3
  let node_zlib = require("node:zlib");
4
- //#region ../types/dist/event-category-CFZs3jI4.mjs
4
+ //#region ../types/dist/event-category-H4AVePnn.mjs
5
5
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
6
6
  EventCategory["SystemBoot"] = "system.boot";
7
7
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -151,6 +151,9 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
151
151
  EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
152
152
  EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
153
153
  EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
154
+ /** Runner-sampled scrub thumbnail (~1/5 s/camera). Telemetry (D8): a lost
155
+ * thumb is a scrub gap the recorder's keyframe backfill covers. */
156
+ EventCategory["RecordingThumbSampled"] = "recording.thumb-sampled";
154
157
  EventCategory["DetectionEvent"] = "detection.event";
155
158
  EventCategory["SessionTrackNew"] = "session.track.new";
156
159
  EventCategory["SessionTrackExpired"] = "session.track.expired";
@@ -7353,6 +7356,24 @@ var RecordingRetentionSchema = object({
7353
7356
  maxSizeGb: number().min(0).optional()
7354
7357
  });
7355
7358
  /**
7359
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
7360
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
7361
+ * previews at. Five graduated steps; absent on a config = `standard` (the
7362
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
7363
+ *
7364
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
7365
+ * Each window's index sidecar carries its own tile dims, so a camera whose
7366
+ * preset changed over time renders every historical window at the dims it was
7367
+ * written with.
7368
+ */
7369
+ var ScrubThumbnailPresetSchema = _enum([
7370
+ "minimal",
7371
+ "low",
7372
+ "standard",
7373
+ "high",
7374
+ "max"
7375
+ ]);
7376
+ /**
7356
7377
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7357
7378
  *
7358
7379
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -7389,7 +7410,14 @@ var RecordingConfigSchema = object({
7389
7410
  * derived into bands once via `migrateConfigToBands`.
7390
7411
  */
7391
7412
  bands: array(RecordingBandSchema).optional(),
7392
- retention: RecordingRetentionSchema.optional()
7413
+ retention: RecordingRetentionSchema.optional(),
7414
+ /**
7415
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
7416
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
7417
+ * windows only — existing sheets are immutable, and each window's index
7418
+ * carries its own tile dims so mixed-preset history renders correctly.
7419
+ */
7420
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7393
7421
  });
7394
7422
  /**
7395
7423
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -11139,10 +11167,23 @@ var deviceDiscoveryCapability = {
11139
11167
  }
11140
11168
  };
11141
11169
  /**
11142
- * Doorbell button cap. Installed on a `DeviceType.Button` accessory
11143
- * with `role: DeviceRole.Doorbell`. Emits an `onPressed` event every
11144
- * time the firmware pushes a ring; status tracks the last press and
11145
- * a pressCount since start (diagnostic).
11170
+ * Doorbell button cap. Two kinds of providers coexist behind this cap
11171
+ * name (same pattern as `snapshot`):
11172
+ *
11173
+ * - **Native** providers: registered per-device by device-driver
11174
+ * addons via `ctx.registerNativeCap` — either on a
11175
+ * `DeviceType.Button` accessory with `role: DeviceRole.Doorbell`,
11176
+ * or directly on the camera (Reolink registers at camera level).
11177
+ * Emits an `onPressed` event every time the firmware pushes a
11178
+ * ring; status tracks the last press and a pressCount since start
11179
+ * (diagnostic).
11180
+ *
11181
+ * - **Wrapper** provider: the `virtual-doorbell` system builtin
11182
+ * (`@camstack/system/builtins/doorbell`). Turns ANY binary-ish
11183
+ * device (contact / switch / event-emitter …) into a doorbell for
11184
+ * a camera. `defaultActive: false` — the operator explicitly binds
11185
+ * it per camera in the device-bindings UI, then picks the source
11186
+ * device + trigger in the per-device settings.
11146
11187
  *
11147
11188
  * The DeviceEventPropagator re-emits `onPressed` on the camera parent
11148
11189
  * — subscribers listening at the camera level receive ring events
@@ -11163,7 +11204,10 @@ var doorbellCapability = {
11163
11204
  scope: "device",
11164
11205
  deviceNative: true,
11165
11206
  mode: "singleton",
11166
- deviceTypes: [DeviceType.Button],
11207
+ kind: "wrapper",
11208
+ defaultActive: false,
11209
+ deviceTypes: [DeviceType.Button, DeviceType.Camera],
11210
+ exposesDeviceSettings: true,
11167
11211
  methods: {},
11168
11212
  events: {
11169
11213
  /**
@@ -17600,6 +17644,14 @@ var ConfigEntrySchema = object({
17600
17644
  value: unknown(),
17601
17645
  description: string().optional()
17602
17646
  });
17647
+ var DeviceLinkModeSchema = _enum(["auto", "manual"]);
17648
+ /** One resolved linked device — the compact projection consumers need. */
17649
+ var LinkedDeviceSchema = object({
17650
+ deviceId: number(),
17651
+ name: string(),
17652
+ location: string().nullable(),
17653
+ features: array(string())
17654
+ });
17603
17655
  var SavedDeviceRowSchema = object({
17604
17656
  /** Numeric id reserved at allocateDeviceId time. */
17605
17657
  id: number(),
@@ -17821,7 +17873,10 @@ method(object({
17821
17873
  }), _void(), {
17822
17874
  kind: "mutation",
17823
17875
  auth: "admin"
17824
- }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({ addonId: string().optional() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
17876
+ }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({ addonId: string().optional() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
17877
+ mode: DeviceLinkModeSchema,
17878
+ devices: array(LinkedDeviceSchema)
17879
+ })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
17825
17880
  deviceId: number(),
17826
17881
  values: record(string(), unknown())
17827
17882
  }), object({ success: literal(true) }), {
@@ -19104,6 +19159,61 @@ var EventKindSchema = _enum([
19104
19159
  "object",
19105
19160
  "audio"
19106
19161
  ]);
19162
+ /**
19163
+ * Spatial filter for `listTracks` — the rect + polygon variants of the shared
19164
+ * MaskShape vocabulary (see `mask-shape.ts`). Coordinates are NORMALIZED 0..1
19165
+ * of the camera frame (top-left origin), matching the drawing-plane editor.
19166
+ */
19167
+ var TrackZoneFilterSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
19168
+ /** Closed icon vocabulary so clients render a known glyph per kind. */
19169
+ var EventKindIconSchema = _enum([
19170
+ "motion",
19171
+ "audio",
19172
+ "person",
19173
+ "vehicle",
19174
+ "animal",
19175
+ "door",
19176
+ "pir",
19177
+ "smoke",
19178
+ "water",
19179
+ "button",
19180
+ "generic"
19181
+ ]);
19182
+ var EventKindCategorySchema = _enum([
19183
+ "motion",
19184
+ "audio",
19185
+ "detection",
19186
+ "sensor",
19187
+ "custom"
19188
+ ]);
19189
+ var EventKindDescriptorSchema = object({
19190
+ /** Stable kind id (e.g. 'motion', 'person', 'contact'). */
19191
+ kind: string(),
19192
+ label: string(),
19193
+ /** Hex color for timeline/legend rendering. */
19194
+ color: string(),
19195
+ icon: EventKindIconSchema,
19196
+ category: EventKindCategorySchema,
19197
+ /** Which cap + device contributes this kind. For built-ins the camera
19198
+ * itself; for sensor kinds the LINKED source device. */
19199
+ source: object({
19200
+ capName: string(),
19201
+ deviceId: number()
19202
+ })
19203
+ });
19204
+ var SensorEventSchema = object({
19205
+ id: string(),
19206
+ /** The CAMERA the event is attributed to (a sensor linked to N cameras
19207
+ * yields N rows, one per camera). */
19208
+ deviceId: number(),
19209
+ /** The linked sensor device whose state changed. */
19210
+ sourceDeviceId: number(),
19211
+ /** Event kind id — matches an `EventKindDescriptor.kind`. */
19212
+ kind: string(),
19213
+ /** Snapshot of the sensor cap's runtime-state slice at the change. */
19214
+ value: record(string(), unknown()).nullable(),
19215
+ timestamp: number()
19216
+ });
19107
19217
  var TrackPositionSchema = object({
19108
19218
  x: number(),
19109
19219
  y: number(),
@@ -19117,6 +19227,30 @@ var TrackSnapshotSchema = object({
19117
19227
  mediaKey: string()
19118
19228
  });
19119
19229
  /**
19230
+ * Normalized 0..1 trajectory envelope (min/max over every position bbox,
19231
+ * divided by the track's detection-frame dims), computed at persist time.
19232
+ * Absent when the frame dims were unknown when the track was persisted
19233
+ * (legacy rows / dims-less sources) and on active (in-RAM) tracks.
19234
+ */
19235
+ var TrackEnvelopeSchema = object({
19236
+ minX: number(),
19237
+ minY: number(),
19238
+ maxX: number(),
19239
+ maxY: number()
19240
+ });
19241
+ /**
19242
+ * Row projection for track list queries. `full` (default) returns the
19243
+ * complete Track including the frame-rate `positions[]` history and the
19244
+ * `snapshots[]` references — megabytes across a page of tracks. `slim`
19245
+ * keeps every scalar the list surfaces actually render (ids, class(es),
19246
+ * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
19247
+ * zonesVisited, bestEventId, envelope) and returns `positions` /
19248
+ * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
19249
+ * `getTrack`. Mirrors the event-store `projection` convention
19250
+ * (`getObjectEvents` et al.).
19251
+ */
19252
+ var TrackProjectionSchema = _enum(["full", "slim"]);
19253
+ /**
19120
19254
  * One audio-classification label heard on the track's camera while the
19121
19255
  * track was alive, aggregated per label. An "episode" is one persisted
19122
19256
  * audio event (the confident-classification path: score ≥ the device's
@@ -19167,7 +19301,11 @@ var TrackSchema = object({
19167
19301
  /** Audio-classification labels heard on the camera during the track's
19168
19302
  * life (score ≥ device `classificationMinScore`), aggregated per label.
19169
19303
  * Absent on legacy rows / tracks with no confident audio. */
19170
- audioLabels: array(TrackAudioLabelSchema).readonly().optional()
19304
+ audioLabels: array(TrackAudioLabelSchema).readonly().optional(),
19305
+ /** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
19306
+ * Populated from the persisted envelope columns on historical reads;
19307
+ * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
19308
+ envelope: TrackEnvelopeSchema.optional()
19171
19309
  });
19172
19310
  var BaseEventFields = {
19173
19311
  id: string(),
@@ -19277,11 +19415,13 @@ var MediaFileSchema = object({
19277
19415
  sizeBytes: number(),
19278
19416
  timestamp: number()
19279
19417
  });
19418
+ var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
19419
+ var MAX_EVENT_QUERY_LIMIT = 5e3;
19280
19420
  var DeviceEventQueryInput = object({
19281
19421
  deviceId: number(),
19282
19422
  since: number().optional(),
19283
19423
  until: number().optional(),
19284
- limit: number().int().min(1).max(5e3).default(1e3),
19424
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT),
19285
19425
  /** `slim` drops heavy JSON fields (regions/bbox/zones) and carries an
19286
19426
  * optional `mediaUrl` (populated by B5). `full` (default) keeps today's
19287
19427
  * exact behaviour. Callers may omit this field — the store defaults to
@@ -19289,6 +19429,27 @@ var DeviceEventQueryInput = object({
19289
19429
  projection: _enum(["full", "slim"]).optional()
19290
19430
  });
19291
19431
  var ObjectEventQueryInput = DeviceEventQueryInput.extend({ classFilter: string().optional() });
19432
+ var RecentTracksQueryInput = object({
19433
+ /** Devices to merge. An empty array yields `{ tracks: [], nextCursor: null }`. */
19434
+ deviceIds: array(number()),
19435
+ /** Window lower bound on `lastSeen` (inclusive). */
19436
+ since: number().optional(),
19437
+ /** Window upper bound on `lastSeen` (inclusive). */
19438
+ until: number().optional(),
19439
+ /** Page size. Default 200, max 1000. */
19440
+ limit: number().int().min(1).max(1e3).default(200),
19441
+ /** Opaque continuation cursor from a previous page's `nextCursor`.
19442
+ * Encodes the (lastSeen, trackId) sort position — treat as opaque. */
19443
+ cursor: string().optional(),
19444
+ /** See {@link TrackProjectionSchema}. Default `full`. */
19445
+ projection: TrackProjectionSchema.optional()
19446
+ });
19447
+ var RecentTracksPageSchema = object({
19448
+ /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
19449
+ tracks: array(TrackSchema).readonly(),
19450
+ /** Cursor for the next page, or null when this page is the last. */
19451
+ nextCursor: string().nullable()
19452
+ });
19292
19453
  var KeyEventQueryInput = object({
19293
19454
  deviceId: number(),
19294
19455
  /** Window lower bound (track firstSeen ≥ since). */
@@ -19371,11 +19532,25 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19371
19532
  deviceId: number(),
19372
19533
  since: number().optional(),
19373
19534
  until: number().optional(),
19374
- limit: number().optional()
19375
- }), array(TrackSchema).readonly()), method(object({ deviceId: number() }), _void(), {
19535
+ limit: number().optional(),
19536
+ /** Spatial filter only tracks whose trajectory intersects the zone
19537
+ * (normalized 0..1 rect or polygon). SQL-prefiltered on the persisted
19538
+ * envelope columns, then precisely tested per position. Tracks with
19539
+ * an unknown envelope (no frame dims at persist time) always match. */
19540
+ zone: TrackZoneFilterSchema.optional(),
19541
+ /** See {@link TrackProjectionSchema}. Default `full` (backward
19542
+ * compatible — omitting the field keeps today's exact behaviour). */
19543
+ projection: TrackProjectionSchema.optional()
19544
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
19376
19545
  kind: "mutation",
19377
19546
  auth: "admin"
19378
- }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19547
+ }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({
19548
+ deviceId: number(),
19549
+ since: number().optional(),
19550
+ until: number().optional(),
19551
+ kinds: array(string()).optional(),
19552
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19553
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19379
19554
  deviceId: number(),
19380
19555
  since: number(),
19381
19556
  until: number(),
@@ -22665,7 +22840,12 @@ var RecordingStorageUsageSchema = object({
22665
22840
  /**
22666
22841
  * Result of locating footage at a wall-clock instant for one device/profile.
22667
22842
  * `segment` carries the covering segment's window; `gap` reports the forward
22668
- * nearest covered edge (`null` past the end of footage / when none exists).
22843
+ * nearest covered edge (`null` past the end of footage / when none exists)
22844
+ * and the backward covered edge `prevEndMs` (exclusive end of the nearest
22845
+ * footage behind the epoch; `null` when none — optional so older providers
22846
+ * that omit it stay valid). `prevEndMs` lets a backward frame-step hop the
22847
+ * small inter-segment cracks (durMs under-covers the span to the next
22848
+ * startMs by ~11-17 ms) instead of no-opping at a segment head.
22669
22849
  */
22670
22850
  var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
22671
22851
  kind: literal("segment"),
@@ -22674,7 +22854,8 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
22674
22854
  bytes: number()
22675
22855
  }), object({
22676
22856
  kind: literal("gap"),
22677
- nearestEdgeMs: number().nullable()
22857
+ nearestEdgeMs: number().nullable(),
22858
+ prevEndMs: number().nullable().optional()
22678
22859
  })]);
22679
22860
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
22680
22861
  var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
@@ -24243,6 +24424,12 @@ Object.freeze({
24243
24424
  addonId: null,
24244
24425
  access: "view"
24245
24426
  },
24427
+ "deviceManager.getLinkedDevices": {
24428
+ capName: "device-manager",
24429
+ capScope: "system",
24430
+ addonId: null,
24431
+ access: "view"
24432
+ },
24246
24433
  "deviceManager.getRoleDisplayDefaults": {
24247
24434
  capName: "device-manager",
24248
24435
  capScope: "system",
@@ -25797,6 +25984,12 @@ Object.freeze({
25797
25984
  addonId: null,
25798
25985
  access: "view"
25799
25986
  },
25987
+ "pipelineAnalytics.getSensorEvents": {
25988
+ capName: "pipeline-analytics",
25989
+ capScope: "device",
25990
+ addonId: null,
25991
+ access: "view"
25992
+ },
25800
25993
  "pipelineAnalytics.getTrack": {
25801
25994
  capName: "pipeline-analytics",
25802
25995
  capScope: "device",
@@ -25809,6 +26002,18 @@ Object.freeze({
25809
26002
  addonId: null,
25810
26003
  access: "view"
25811
26004
  },
26005
+ "pipelineAnalytics.listEventKinds": {
26006
+ capName: "pipeline-analytics",
26007
+ capScope: "device",
26008
+ addonId: null,
26009
+ access: "view"
26010
+ },
26011
+ "pipelineAnalytics.listRecentTracks": {
26012
+ capName: "pipeline-analytics",
26013
+ capScope: "device",
26014
+ addonId: null,
26015
+ access: "view"
26016
+ },
25812
26017
  "pipelineAnalytics.listTracks": {
25813
26018
  capName: "pipeline-analytics",
25814
26019
  capScope: "device",
@@ -32621,7 +32826,7 @@ var BUS_EVENT_TYPES = [
32621
32826
  "deconz_event",
32622
32827
  "xiaomi_aqara.click"
32623
32828
  ];
32624
- var HaIntegrationManager = class {
32829
+ var HaIntegrationManager = class HaIntegrationManager {
32625
32830
  opts;
32626
32831
  wsClient = null;
32627
32832
  connection = null;
@@ -32635,6 +32840,15 @@ var HaIntegrationManager = class {
32635
32840
  lastCheckedAt = null;
32636
32841
  lastError = null;
32637
32842
  haInfo = {};
32843
+ /** Deadline timer for the INITIAL connect. `start()` no longer blocks on
32844
+ * the socket (see the comment there), so a broker whose host silently
32845
+ * drops packets would otherwise sit at `connecting` for the full OS TCP
32846
+ * timeout (tens of seconds) with no UI signal. This timer flips it to
32847
+ * `unreachable` promptly; a late successful connect still overrides it. */
32848
+ connectDeadlineTimer = null;
32849
+ /** Ceiling on the `connecting` state before the UI is shown `unreachable`.
32850
+ * Slightly above the throwaway `testSettings` probe timeout (8s). */
32851
+ static CONNECT_DEADLINE_MS = 1e4;
32638
32852
  constructor(opts) {
32639
32853
  this.opts = opts;
32640
32854
  }
@@ -32648,6 +32862,8 @@ var HaIntegrationManager = class {
32648
32862
  await this.refreshHaConfigInfo(connection);
32649
32863
  await this.ensureBaselineSubscription();
32650
32864
  await this.ensureBusEventSubscriptions();
32865
+ this.clearConnectDeadline();
32866
+ this.lastError = null;
32651
32867
  this.setStatus("connected");
32652
32868
  try {
32653
32869
  this.opts.onConnected?.(this.opts.id);
@@ -32683,14 +32899,32 @@ var HaIntegrationManager = class {
32683
32899
  }
32684
32900
  }
32685
32901
  });
32686
- try {
32687
- await this.wsClient.start();
32688
- } catch (err) {
32902
+ this.startConnectDeadline();
32903
+ this.wsClient.start().catch((err) => {
32689
32904
  this.classifyAuthError(err);
32690
- throw err;
32905
+ });
32906
+ }
32907
+ /** Arm the initial-connect deadline. Idempotent — clears any prior timer. */
32908
+ startConnectDeadline() {
32909
+ this.clearConnectDeadline();
32910
+ const timer = setTimeout(() => {
32911
+ this.connectDeadlineTimer = null;
32912
+ if (this.status === "connecting") {
32913
+ if (this.lastError === null) this.lastError = "Connection timed out";
32914
+ this.setStatus("unreachable");
32915
+ }
32916
+ }, HaIntegrationManager.CONNECT_DEADLINE_MS);
32917
+ if (typeof timer.unref === "function") timer.unref();
32918
+ this.connectDeadlineTimer = timer;
32919
+ }
32920
+ clearConnectDeadline() {
32921
+ if (this.connectDeadlineTimer) {
32922
+ clearTimeout(this.connectDeadlineTimer);
32923
+ this.connectDeadlineTimer = null;
32691
32924
  }
32692
32925
  }
32693
32926
  async stop() {
32927
+ this.clearConnectDeadline();
32694
32928
  if (this.upstreamSubscriptionUnsub) {
32695
32929
  try {
32696
32930
  await this.upstreamSubscriptionUnsub();
@@ -32905,15 +33139,19 @@ var HaIntegrationManager = class {
32905
33139
  }));
32906
33140
  }
32907
33141
  classifyAuthError(err) {
33142
+ this.clearConnectDeadline();
32908
33143
  if (err === 2) {
33144
+ this.lastError = "Authentication failed — check the access token";
32909
33145
  this.setStatus("auth-failed");
32910
- this.lastError = "invalid_auth";
32911
- } else if (err === 1 || err === 4) {
33146
+ } else if (err === 1) {
33147
+ this.lastError = "Cannot connect host unreachable or Home Assistant not responding";
33148
+ this.setStatus("unreachable");
33149
+ } else if (err === 4) {
33150
+ this.lastError = "Home Assistant host / URL is required";
32912
33151
  this.setStatus("unreachable");
32913
- this.lastError = errMsg(err);
32914
33152
  } else {
32915
- this.setStatus("error");
32916
33153
  this.lastError = errMsg(err);
33154
+ this.setStatus("error");
32917
33155
  }
32918
33156
  }
32919
33157
  requireConnection() {
package/dist/addon.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
3
- //#region ../types/dist/event-category-CFZs3jI4.mjs
3
+ //#region ../types/dist/event-category-H4AVePnn.mjs
4
4
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
5
5
  EventCategory["SystemBoot"] = "system.boot";
6
6
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -150,6 +150,9 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
150
150
  EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
151
151
  EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
152
152
  EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
153
+ /** Runner-sampled scrub thumbnail (~1/5 s/camera). Telemetry (D8): a lost
154
+ * thumb is a scrub gap the recorder's keyframe backfill covers. */
155
+ EventCategory["RecordingThumbSampled"] = "recording.thumb-sampled";
153
156
  EventCategory["DetectionEvent"] = "detection.event";
154
157
  EventCategory["SessionTrackNew"] = "session.track.new";
155
158
  EventCategory["SessionTrackExpired"] = "session.track.expired";
@@ -7352,6 +7355,24 @@ var RecordingRetentionSchema = object({
7352
7355
  maxSizeGb: number().min(0).optional()
7353
7356
  });
7354
7357
  /**
7358
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
7359
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
7360
+ * previews at. Five graduated steps; absent on a config = `standard` (the
7361
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
7362
+ *
7363
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
7364
+ * Each window's index sidecar carries its own tile dims, so a camera whose
7365
+ * preset changed over time renders every historical window at the dims it was
7366
+ * written with.
7367
+ */
7368
+ var ScrubThumbnailPresetSchema = _enum([
7369
+ "minimal",
7370
+ "low",
7371
+ "standard",
7372
+ "high",
7373
+ "max"
7374
+ ]);
7375
+ /**
7355
7376
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7356
7377
  *
7357
7378
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -7388,7 +7409,14 @@ var RecordingConfigSchema = object({
7388
7409
  * derived into bands once via `migrateConfigToBands`.
7389
7410
  */
7390
7411
  bands: array(RecordingBandSchema).optional(),
7391
- retention: RecordingRetentionSchema.optional()
7412
+ retention: RecordingRetentionSchema.optional(),
7413
+ /**
7414
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
7415
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
7416
+ * windows only — existing sheets are immutable, and each window's index
7417
+ * carries its own tile dims so mixed-preset history renders correctly.
7418
+ */
7419
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7392
7420
  });
7393
7421
  /**
7394
7422
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -11138,10 +11166,23 @@ var deviceDiscoveryCapability = {
11138
11166
  }
11139
11167
  };
11140
11168
  /**
11141
- * Doorbell button cap. Installed on a `DeviceType.Button` accessory
11142
- * with `role: DeviceRole.Doorbell`. Emits an `onPressed` event every
11143
- * time the firmware pushes a ring; status tracks the last press and
11144
- * a pressCount since start (diagnostic).
11169
+ * Doorbell button cap. Two kinds of providers coexist behind this cap
11170
+ * name (same pattern as `snapshot`):
11171
+ *
11172
+ * - **Native** providers: registered per-device by device-driver
11173
+ * addons via `ctx.registerNativeCap` — either on a
11174
+ * `DeviceType.Button` accessory with `role: DeviceRole.Doorbell`,
11175
+ * or directly on the camera (Reolink registers at camera level).
11176
+ * Emits an `onPressed` event every time the firmware pushes a
11177
+ * ring; status tracks the last press and a pressCount since start
11178
+ * (diagnostic).
11179
+ *
11180
+ * - **Wrapper** provider: the `virtual-doorbell` system builtin
11181
+ * (`@camstack/system/builtins/doorbell`). Turns ANY binary-ish
11182
+ * device (contact / switch / event-emitter …) into a doorbell for
11183
+ * a camera. `defaultActive: false` — the operator explicitly binds
11184
+ * it per camera in the device-bindings UI, then picks the source
11185
+ * device + trigger in the per-device settings.
11145
11186
  *
11146
11187
  * The DeviceEventPropagator re-emits `onPressed` on the camera parent
11147
11188
  * — subscribers listening at the camera level receive ring events
@@ -11162,7 +11203,10 @@ var doorbellCapability = {
11162
11203
  scope: "device",
11163
11204
  deviceNative: true,
11164
11205
  mode: "singleton",
11165
- deviceTypes: [DeviceType.Button],
11206
+ kind: "wrapper",
11207
+ defaultActive: false,
11208
+ deviceTypes: [DeviceType.Button, DeviceType.Camera],
11209
+ exposesDeviceSettings: true,
11166
11210
  methods: {},
11167
11211
  events: {
11168
11212
  /**
@@ -17599,6 +17643,14 @@ var ConfigEntrySchema = object({
17599
17643
  value: unknown(),
17600
17644
  description: string().optional()
17601
17645
  });
17646
+ var DeviceLinkModeSchema = _enum(["auto", "manual"]);
17647
+ /** One resolved linked device — the compact projection consumers need. */
17648
+ var LinkedDeviceSchema = object({
17649
+ deviceId: number(),
17650
+ name: string(),
17651
+ location: string().nullable(),
17652
+ features: array(string())
17653
+ });
17602
17654
  var SavedDeviceRowSchema = object({
17603
17655
  /** Numeric id reserved at allocateDeviceId time. */
17604
17656
  id: number(),
@@ -17820,7 +17872,10 @@ method(object({
17820
17872
  }), _void(), {
17821
17873
  kind: "mutation",
17822
17874
  auth: "admin"
17823
- }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({ addonId: string().optional() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
17875
+ }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({ addonId: string().optional() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
17876
+ mode: DeviceLinkModeSchema,
17877
+ devices: array(LinkedDeviceSchema)
17878
+ })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
17824
17879
  deviceId: number(),
17825
17880
  values: record(string(), unknown())
17826
17881
  }), object({ success: literal(true) }), {
@@ -19103,6 +19158,61 @@ var EventKindSchema = _enum([
19103
19158
  "object",
19104
19159
  "audio"
19105
19160
  ]);
19161
+ /**
19162
+ * Spatial filter for `listTracks` — the rect + polygon variants of the shared
19163
+ * MaskShape vocabulary (see `mask-shape.ts`). Coordinates are NORMALIZED 0..1
19164
+ * of the camera frame (top-left origin), matching the drawing-plane editor.
19165
+ */
19166
+ var TrackZoneFilterSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
19167
+ /** Closed icon vocabulary so clients render a known glyph per kind. */
19168
+ var EventKindIconSchema = _enum([
19169
+ "motion",
19170
+ "audio",
19171
+ "person",
19172
+ "vehicle",
19173
+ "animal",
19174
+ "door",
19175
+ "pir",
19176
+ "smoke",
19177
+ "water",
19178
+ "button",
19179
+ "generic"
19180
+ ]);
19181
+ var EventKindCategorySchema = _enum([
19182
+ "motion",
19183
+ "audio",
19184
+ "detection",
19185
+ "sensor",
19186
+ "custom"
19187
+ ]);
19188
+ var EventKindDescriptorSchema = object({
19189
+ /** Stable kind id (e.g. 'motion', 'person', 'contact'). */
19190
+ kind: string(),
19191
+ label: string(),
19192
+ /** Hex color for timeline/legend rendering. */
19193
+ color: string(),
19194
+ icon: EventKindIconSchema,
19195
+ category: EventKindCategorySchema,
19196
+ /** Which cap + device contributes this kind. For built-ins the camera
19197
+ * itself; for sensor kinds the LINKED source device. */
19198
+ source: object({
19199
+ capName: string(),
19200
+ deviceId: number()
19201
+ })
19202
+ });
19203
+ var SensorEventSchema = object({
19204
+ id: string(),
19205
+ /** The CAMERA the event is attributed to (a sensor linked to N cameras
19206
+ * yields N rows, one per camera). */
19207
+ deviceId: number(),
19208
+ /** The linked sensor device whose state changed. */
19209
+ sourceDeviceId: number(),
19210
+ /** Event kind id — matches an `EventKindDescriptor.kind`. */
19211
+ kind: string(),
19212
+ /** Snapshot of the sensor cap's runtime-state slice at the change. */
19213
+ value: record(string(), unknown()).nullable(),
19214
+ timestamp: number()
19215
+ });
19106
19216
  var TrackPositionSchema = object({
19107
19217
  x: number(),
19108
19218
  y: number(),
@@ -19116,6 +19226,30 @@ var TrackSnapshotSchema = object({
19116
19226
  mediaKey: string()
19117
19227
  });
19118
19228
  /**
19229
+ * Normalized 0..1 trajectory envelope (min/max over every position bbox,
19230
+ * divided by the track's detection-frame dims), computed at persist time.
19231
+ * Absent when the frame dims were unknown when the track was persisted
19232
+ * (legacy rows / dims-less sources) and on active (in-RAM) tracks.
19233
+ */
19234
+ var TrackEnvelopeSchema = object({
19235
+ minX: number(),
19236
+ minY: number(),
19237
+ maxX: number(),
19238
+ maxY: number()
19239
+ });
19240
+ /**
19241
+ * Row projection for track list queries. `full` (default) returns the
19242
+ * complete Track including the frame-rate `positions[]` history and the
19243
+ * `snapshots[]` references — megabytes across a page of tracks. `slim`
19244
+ * keeps every scalar the list surfaces actually render (ids, class(es),
19245
+ * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
19246
+ * zonesVisited, bestEventId, envelope) and returns `positions` /
19247
+ * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
19248
+ * `getTrack`. Mirrors the event-store `projection` convention
19249
+ * (`getObjectEvents` et al.).
19250
+ */
19251
+ var TrackProjectionSchema = _enum(["full", "slim"]);
19252
+ /**
19119
19253
  * One audio-classification label heard on the track's camera while the
19120
19254
  * track was alive, aggregated per label. An "episode" is one persisted
19121
19255
  * audio event (the confident-classification path: score ≥ the device's
@@ -19166,7 +19300,11 @@ var TrackSchema = object({
19166
19300
  /** Audio-classification labels heard on the camera during the track's
19167
19301
  * life (score ≥ device `classificationMinScore`), aggregated per label.
19168
19302
  * Absent on legacy rows / tracks with no confident audio. */
19169
- audioLabels: array(TrackAudioLabelSchema).readonly().optional()
19303
+ audioLabels: array(TrackAudioLabelSchema).readonly().optional(),
19304
+ /** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
19305
+ * Populated from the persisted envelope columns on historical reads;
19306
+ * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
19307
+ envelope: TrackEnvelopeSchema.optional()
19170
19308
  });
19171
19309
  var BaseEventFields = {
19172
19310
  id: string(),
@@ -19276,11 +19414,13 @@ var MediaFileSchema = object({
19276
19414
  sizeBytes: number(),
19277
19415
  timestamp: number()
19278
19416
  });
19417
+ var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
19418
+ var MAX_EVENT_QUERY_LIMIT = 5e3;
19279
19419
  var DeviceEventQueryInput = object({
19280
19420
  deviceId: number(),
19281
19421
  since: number().optional(),
19282
19422
  until: number().optional(),
19283
- limit: number().int().min(1).max(5e3).default(1e3),
19423
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT),
19284
19424
  /** `slim` drops heavy JSON fields (regions/bbox/zones) and carries an
19285
19425
  * optional `mediaUrl` (populated by B5). `full` (default) keeps today's
19286
19426
  * exact behaviour. Callers may omit this field — the store defaults to
@@ -19288,6 +19428,27 @@ var DeviceEventQueryInput = object({
19288
19428
  projection: _enum(["full", "slim"]).optional()
19289
19429
  });
19290
19430
  var ObjectEventQueryInput = DeviceEventQueryInput.extend({ classFilter: string().optional() });
19431
+ var RecentTracksQueryInput = object({
19432
+ /** Devices to merge. An empty array yields `{ tracks: [], nextCursor: null }`. */
19433
+ deviceIds: array(number()),
19434
+ /** Window lower bound on `lastSeen` (inclusive). */
19435
+ since: number().optional(),
19436
+ /** Window upper bound on `lastSeen` (inclusive). */
19437
+ until: number().optional(),
19438
+ /** Page size. Default 200, max 1000. */
19439
+ limit: number().int().min(1).max(1e3).default(200),
19440
+ /** Opaque continuation cursor from a previous page's `nextCursor`.
19441
+ * Encodes the (lastSeen, trackId) sort position — treat as opaque. */
19442
+ cursor: string().optional(),
19443
+ /** See {@link TrackProjectionSchema}. Default `full`. */
19444
+ projection: TrackProjectionSchema.optional()
19445
+ });
19446
+ var RecentTracksPageSchema = object({
19447
+ /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
19448
+ tracks: array(TrackSchema).readonly(),
19449
+ /** Cursor for the next page, or null when this page is the last. */
19450
+ nextCursor: string().nullable()
19451
+ });
19291
19452
  var KeyEventQueryInput = object({
19292
19453
  deviceId: number(),
19293
19454
  /** Window lower bound (track firstSeen ≥ since). */
@@ -19370,11 +19531,25 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19370
19531
  deviceId: number(),
19371
19532
  since: number().optional(),
19372
19533
  until: number().optional(),
19373
- limit: number().optional()
19374
- }), array(TrackSchema).readonly()), method(object({ deviceId: number() }), _void(), {
19534
+ limit: number().optional(),
19535
+ /** Spatial filter only tracks whose trajectory intersects the zone
19536
+ * (normalized 0..1 rect or polygon). SQL-prefiltered on the persisted
19537
+ * envelope columns, then precisely tested per position. Tracks with
19538
+ * an unknown envelope (no frame dims at persist time) always match. */
19539
+ zone: TrackZoneFilterSchema.optional(),
19540
+ /** See {@link TrackProjectionSchema}. Default `full` (backward
19541
+ * compatible — omitting the field keeps today's exact behaviour). */
19542
+ projection: TrackProjectionSchema.optional()
19543
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
19375
19544
  kind: "mutation",
19376
19545
  auth: "admin"
19377
- }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19546
+ }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({
19547
+ deviceId: number(),
19548
+ since: number().optional(),
19549
+ until: number().optional(),
19550
+ kinds: array(string()).optional(),
19551
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19552
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19378
19553
  deviceId: number(),
19379
19554
  since: number(),
19380
19555
  until: number(),
@@ -22664,7 +22839,12 @@ var RecordingStorageUsageSchema = object({
22664
22839
  /**
22665
22840
  * Result of locating footage at a wall-clock instant for one device/profile.
22666
22841
  * `segment` carries the covering segment's window; `gap` reports the forward
22667
- * nearest covered edge (`null` past the end of footage / when none exists).
22842
+ * nearest covered edge (`null` past the end of footage / when none exists)
22843
+ * and the backward covered edge `prevEndMs` (exclusive end of the nearest
22844
+ * footage behind the epoch; `null` when none — optional so older providers
22845
+ * that omit it stay valid). `prevEndMs` lets a backward frame-step hop the
22846
+ * small inter-segment cracks (durMs under-covers the span to the next
22847
+ * startMs by ~11-17 ms) instead of no-opping at a segment head.
22668
22848
  */
22669
22849
  var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
22670
22850
  kind: literal("segment"),
@@ -22673,7 +22853,8 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
22673
22853
  bytes: number()
22674
22854
  }), object({
22675
22855
  kind: literal("gap"),
22676
- nearestEdgeMs: number().nullable()
22856
+ nearestEdgeMs: number().nullable(),
22857
+ prevEndMs: number().nullable().optional()
22677
22858
  })]);
22678
22859
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
22679
22860
  var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
@@ -24242,6 +24423,12 @@ Object.freeze({
24242
24423
  addonId: null,
24243
24424
  access: "view"
24244
24425
  },
24426
+ "deviceManager.getLinkedDevices": {
24427
+ capName: "device-manager",
24428
+ capScope: "system",
24429
+ addonId: null,
24430
+ access: "view"
24431
+ },
24245
24432
  "deviceManager.getRoleDisplayDefaults": {
24246
24433
  capName: "device-manager",
24247
24434
  capScope: "system",
@@ -25796,6 +25983,12 @@ Object.freeze({
25796
25983
  addonId: null,
25797
25984
  access: "view"
25798
25985
  },
25986
+ "pipelineAnalytics.getSensorEvents": {
25987
+ capName: "pipeline-analytics",
25988
+ capScope: "device",
25989
+ addonId: null,
25990
+ access: "view"
25991
+ },
25799
25992
  "pipelineAnalytics.getTrack": {
25800
25993
  capName: "pipeline-analytics",
25801
25994
  capScope: "device",
@@ -25808,6 +26001,18 @@ Object.freeze({
25808
26001
  addonId: null,
25809
26002
  access: "view"
25810
26003
  },
26004
+ "pipelineAnalytics.listEventKinds": {
26005
+ capName: "pipeline-analytics",
26006
+ capScope: "device",
26007
+ addonId: null,
26008
+ access: "view"
26009
+ },
26010
+ "pipelineAnalytics.listRecentTracks": {
26011
+ capName: "pipeline-analytics",
26012
+ capScope: "device",
26013
+ addonId: null,
26014
+ access: "view"
26015
+ },
25811
26016
  "pipelineAnalytics.listTracks": {
25812
26017
  capName: "pipeline-analytics",
25813
26018
  capScope: "device",
@@ -32620,7 +32825,7 @@ var BUS_EVENT_TYPES = [
32620
32825
  "deconz_event",
32621
32826
  "xiaomi_aqara.click"
32622
32827
  ];
32623
- var HaIntegrationManager = class {
32828
+ var HaIntegrationManager = class HaIntegrationManager {
32624
32829
  opts;
32625
32830
  wsClient = null;
32626
32831
  connection = null;
@@ -32634,6 +32839,15 @@ var HaIntegrationManager = class {
32634
32839
  lastCheckedAt = null;
32635
32840
  lastError = null;
32636
32841
  haInfo = {};
32842
+ /** Deadline timer for the INITIAL connect. `start()` no longer blocks on
32843
+ * the socket (see the comment there), so a broker whose host silently
32844
+ * drops packets would otherwise sit at `connecting` for the full OS TCP
32845
+ * timeout (tens of seconds) with no UI signal. This timer flips it to
32846
+ * `unreachable` promptly; a late successful connect still overrides it. */
32847
+ connectDeadlineTimer = null;
32848
+ /** Ceiling on the `connecting` state before the UI is shown `unreachable`.
32849
+ * Slightly above the throwaway `testSettings` probe timeout (8s). */
32850
+ static CONNECT_DEADLINE_MS = 1e4;
32637
32851
  constructor(opts) {
32638
32852
  this.opts = opts;
32639
32853
  }
@@ -32647,6 +32861,8 @@ var HaIntegrationManager = class {
32647
32861
  await this.refreshHaConfigInfo(connection);
32648
32862
  await this.ensureBaselineSubscription();
32649
32863
  await this.ensureBusEventSubscriptions();
32864
+ this.clearConnectDeadline();
32865
+ this.lastError = null;
32650
32866
  this.setStatus("connected");
32651
32867
  try {
32652
32868
  this.opts.onConnected?.(this.opts.id);
@@ -32682,14 +32898,32 @@ var HaIntegrationManager = class {
32682
32898
  }
32683
32899
  }
32684
32900
  });
32685
- try {
32686
- await this.wsClient.start();
32687
- } catch (err) {
32901
+ this.startConnectDeadline();
32902
+ this.wsClient.start().catch((err) => {
32688
32903
  this.classifyAuthError(err);
32689
- throw err;
32904
+ });
32905
+ }
32906
+ /** Arm the initial-connect deadline. Idempotent — clears any prior timer. */
32907
+ startConnectDeadline() {
32908
+ this.clearConnectDeadline();
32909
+ const timer = setTimeout(() => {
32910
+ this.connectDeadlineTimer = null;
32911
+ if (this.status === "connecting") {
32912
+ if (this.lastError === null) this.lastError = "Connection timed out";
32913
+ this.setStatus("unreachable");
32914
+ }
32915
+ }, HaIntegrationManager.CONNECT_DEADLINE_MS);
32916
+ if (typeof timer.unref === "function") timer.unref();
32917
+ this.connectDeadlineTimer = timer;
32918
+ }
32919
+ clearConnectDeadline() {
32920
+ if (this.connectDeadlineTimer) {
32921
+ clearTimeout(this.connectDeadlineTimer);
32922
+ this.connectDeadlineTimer = null;
32690
32923
  }
32691
32924
  }
32692
32925
  async stop() {
32926
+ this.clearConnectDeadline();
32693
32927
  if (this.upstreamSubscriptionUnsub) {
32694
32928
  try {
32695
32929
  await this.upstreamSubscriptionUnsub();
@@ -32904,15 +33138,19 @@ var HaIntegrationManager = class {
32904
33138
  }));
32905
33139
  }
32906
33140
  classifyAuthError(err) {
33141
+ this.clearConnectDeadline();
32907
33142
  if (err === 2) {
33143
+ this.lastError = "Authentication failed — check the access token";
32908
33144
  this.setStatus("auth-failed");
32909
- this.lastError = "invalid_auth";
32910
- } else if (err === 1 || err === 4) {
33145
+ } else if (err === 1) {
33146
+ this.lastError = "Cannot connect host unreachable or Home Assistant not responding";
33147
+ this.setStatus("unreachable");
33148
+ } else if (err === 4) {
33149
+ this.lastError = "Home Assistant host / URL is required";
32911
33150
  this.setStatus("unreachable");
32912
- this.lastError = errMsg(err);
32913
33151
  } else {
32914
- this.setStatus("error");
32915
33152
  this.lastError = errMsg(err);
33153
+ this.setStatus("error");
32916
33154
  }
32917
33155
  }
32918
33156
  requireConnection() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.1.27",
3
+ "version": "1.1.29",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",