@camstack/addon-provider-homeassistant 1.1.28 → 1.1.30

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 +270 -17
  2. package/dist/addon.mjs +270 -17
  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-H4AVePnn.mjs
4
+ //#region ../types/dist/event-category-D4HJq7Mw.mjs
5
5
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
6
6
  EventCategory["SystemBoot"] = "system.boot";
7
7
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -154,6 +154,11 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
154
154
  /** Runner-sampled scrub thumbnail (~1/5 s/camera). Telemetry (D8): a lost
155
155
  * thumb is a scrub gap the recorder's keyframe backfill covers. */
156
156
  EventCategory["RecordingThumbSampled"] = "recording.thumb-sampled";
157
+ /** Export render progress (0–100). Telemetry (D8): a lost tick is a stale
158
+ * progress bar the client reconciles via `recordingExport.getExport`. */
159
+ EventCategory["RecordingExportProgress"] = "recording.export.progress";
160
+ EventCategory["RecordingExportCompleted"] = "recording.export.completed";
161
+ EventCategory["RecordingExportFailed"] = "recording.export.failed";
157
162
  EventCategory["DetectionEvent"] = "detection.event";
158
163
  EventCategory["SessionTrackNew"] = "session.track.new";
159
164
  EventCategory["SessionTrackExpired"] = "session.track.expired";
@@ -443,6 +448,25 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
443
448
  */
444
449
  EventCategory["PipelineAnalyticsStationaryChanged"] = "pipeline-analytics.stationary-changed";
445
450
  /**
451
+ * Fired by `addon-post-analysis` when a package-drop is confirmed inside
452
+ * a package zone — a newly-appeared stationary object of a package class
453
+ * that cleared the class / zone / dwell / size gates. Payload:
454
+ * `PipelineAnalyticsPackageDeliveredPayload` carrying `{ deviceId,
455
+ * entryId, className, zoneIds, keyFrameMediaKey?, bbox, timestamp }`.
456
+ * Telemetry (D8): the durable record is the `package-events` store row;
457
+ * this bus topic drives notifier rules + live UI. See
458
+ * docs/superpowers/specs/2026-07-17-package-zones-design.md §5.1.
459
+ */
460
+ EventCategory["PipelineAnalyticsPackageDelivered"] = "pipeline-analytics.package-delivered";
461
+ /**
462
+ * Fired by `addon-post-analysis` when a previously-delivered package
463
+ * leaves its zone (the stationary entry departed — moved or swept).
464
+ * Payload: `PipelineAnalyticsPackagePickedUpPayload` carrying
465
+ * `{ deviceId, entryId, deliveredEventId, className, timestamp }`.
466
+ * Telemetry (D8). See package-zones-design §5.2.
467
+ */
468
+ EventCategory["PipelineAnalyticsPackagePickedUp"] = "pipeline-analytics.package-picked-up";
469
+ /**
446
470
  * Fired by `addon-post-analysis` whenever a gallery face row changes:
447
471
  * `kind:'buffered'` a new detected face was persisted, `'assigned'` /
448
472
  * `'unassigned'` its identity link changed, `'deleted'` the row was
@@ -5181,6 +5205,25 @@ function _instanceof(cls, params = {}) {
5181
5205
  };
5182
5206
  return inst;
5183
5207
  }
5208
+ //#endregion
5209
+ //#region ../../node_modules/zod/v4/classic/compat.js
5210
+ /** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
5211
+ var ZodIssueCode = {
5212
+ invalid_type: "invalid_type",
5213
+ too_big: "too_big",
5214
+ too_small: "too_small",
5215
+ invalid_format: "invalid_format",
5216
+ not_multiple_of: "not_multiple_of",
5217
+ unrecognized_keys: "unrecognized_keys",
5218
+ invalid_union: "invalid_union",
5219
+ invalid_key: "invalid_key",
5220
+ invalid_element: "invalid_element",
5221
+ invalid_value: "invalid_value",
5222
+ custom: "custom"
5223
+ };
5224
+ /** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */
5225
+ var ZodFirstPartyTypeKind;
5226
+ ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5184
5227
  Object.fromEntries([
5185
5228
  {
5186
5229
  id: "overview",
@@ -7356,6 +7399,24 @@ var RecordingRetentionSchema = object({
7356
7399
  maxSizeGb: number().min(0).optional()
7357
7400
  });
7358
7401
  /**
7402
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
7403
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
7404
+ * previews at. Five graduated steps; absent on a config = `standard` (the
7405
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
7406
+ *
7407
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
7408
+ * Each window's index sidecar carries its own tile dims, so a camera whose
7409
+ * preset changed over time renders every historical window at the dims it was
7410
+ * written with.
7411
+ */
7412
+ var ScrubThumbnailPresetSchema = _enum([
7413
+ "minimal",
7414
+ "low",
7415
+ "standard",
7416
+ "high",
7417
+ "max"
7418
+ ]);
7419
+ /**
7359
7420
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7360
7421
  *
7361
7422
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -7392,7 +7453,14 @@ var RecordingConfigSchema = object({
7392
7453
  * derived into bands once via `migrateConfigToBands`.
7393
7454
  */
7394
7455
  bands: array(RecordingBandSchema).optional(),
7395
- retention: RecordingRetentionSchema.optional()
7456
+ retention: RecordingRetentionSchema.optional(),
7457
+ /**
7458
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
7459
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
7460
+ * windows only — existing sheets are immutable, and each window's index
7461
+ * carries its own tile dims so mixed-preset history renders correctly.
7462
+ */
7463
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7396
7464
  });
7397
7465
  /**
7398
7466
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -14988,7 +15056,11 @@ array(ZoneRuleSchema).readonly();
14988
15056
  * Extend the enum here when a new gating consumer comes online (audio
14989
15057
  * gating, alert filtering, …) — no other surface needs to change.
14990
15058
  */
14991
- var ZoneRuleStageEnum = _enum(["motion", "detection"]);
15059
+ var ZoneRuleStageEnum = _enum([
15060
+ "motion",
15061
+ "detection",
15062
+ "package"
15063
+ ]);
14992
15064
  /**
14993
15065
  * Runtime registry: cap-property-name → cap definition. `BaseDevice`'s
14994
15066
  * `state` getter looks up the cap definition here to construct a
@@ -15082,16 +15154,24 @@ var DEVICE_LOCAL_STATE_CAPS = {
15082
15154
  })
15083
15155
  },
15084
15156
  /**
15085
- * Runtime-state slice — both stages mirrored together so consumers
15157
+ * Runtime-state slice — every stage mirrored together so consumers
15086
15158
  * see one reactive handle (`device.state.zoneRules.value`) instead
15087
- * of two. Bulk-replace mutations on either stage write the full
15088
- * `{motion, detection}` shape, so subscribers always get the
15159
+ * of one per stage. Bulk-replace mutations on any stage write the full
15160
+ * `{motion, detection, package}` shape, so subscribers always get the
15089
15161
  * complete current set. Consumers that only care about one stage
15090
15162
  * just read the matching property.
15163
+ *
15164
+ * `package` backs the package-drop detector — a package zone is a
15165
+ * `ZoneRule` on the `'package'` stage referencing drawn polygons
15166
+ * (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
15167
+ * The orchestrator provider that writes this stage lands in a later
15168
+ * slice; until then the mirror carries only `{motion, detection}` and
15169
+ * consumers read `package` as absent (treat as `[]`).
15091
15170
  */
15092
15171
  runtimeState: object({
15093
15172
  motion: array(ZoneRuleSchema).readonly(),
15094
- detection: array(ZoneRuleSchema).readonly()
15173
+ detection: array(ZoneRuleSchema).readonly(),
15174
+ package: array(ZoneRuleSchema).readonly()
15095
15175
  })
15096
15176
  },
15097
15177
  zones: zonesCapability
@@ -19152,6 +19232,7 @@ var EventKindIconSchema = _enum([
19152
19232
  "smoke",
19153
19233
  "water",
19154
19234
  "button",
19235
+ "package",
19155
19236
  "generic"
19156
19237
  ]);
19157
19238
  var EventKindCategorySchema = _enum([
@@ -19159,7 +19240,8 @@ var EventKindCategorySchema = _enum([
19159
19240
  "audio",
19160
19241
  "detection",
19161
19242
  "sensor",
19162
- "custom"
19243
+ "custom",
19244
+ "package"
19163
19245
  ]);
19164
19246
  var EventKindDescriptorSchema = object({
19165
19247
  /** Stable kind id (e.g. 'motion', 'person', 'contact'). */
@@ -22894,6 +22976,108 @@ method(object({
22894
22976
  auth: "admin"
22895
22977
  });
22896
22978
  /**
22979
+ * `recordingExport` cap — render a footage time range into a single downloadable
22980
+ * MP4 (regular / accelerated / decelerated / timelapse, ± audio), kept for a
22981
+ * bounded lifetime with a durable history, auto-expiry, and optional
22982
+ * delete-after-download.
22983
+ *
22984
+ * Like `recording` this is a `scope:'system', mode:'singleton'` cap: the
22985
+ * recorder self-gates so exactly ONE node (the designated `recordingNodeId`,
22986
+ * default `hub`) registers it. Device-scoped methods carry `deviceId` in their
22987
+ * input and dispatch to that single provider; the render runs on the node that
22988
+ * owns the footage (no cross-node segment transfer). Download rides the
22989
+ * framework addon data-plane. State persists in a DurableState blob (NOT
22990
+ * SQLite); history rows survive file deletion for audit.
22991
+ */
22992
+ /** Playback-speed multiplier for the render (1 = realtime). */
22993
+ var ExportSpeedSchema = number().min(.25).max(32);
22994
+ /** Timelapse cadence — sample one source frame per `everyMs`, output at `outputFps`. */
22995
+ var ExportTimelapseSchema = object({
22996
+ everyMs: number().int().positive(),
22997
+ outputFps: number().int().min(1).max(60).optional()
22998
+ });
22999
+ /**
23000
+ * Render options. `speed` and `timelapse` are mutually exclusive. `includeAudio`
23001
+ * is honoured only for a realtime-ish speed (0.5–2×); timelapse is always
23002
+ * silent. `maxLifeMs` bounds how long the finished file is kept;
23003
+ * `deleteAfterDownload` removes it shortly after the first complete download.
23004
+ */
23005
+ var ExportOptionsSchema = object({
23006
+ speed: ExportSpeedSchema.optional(),
23007
+ timelapse: ExportTimelapseSchema.optional(),
23008
+ includeAudio: boolean(),
23009
+ maxLifeMs: number().int().positive(),
23010
+ deleteAfterDownload: boolean(),
23011
+ title: string().max(200).optional()
23012
+ }).superRefine((v, ctx) => {
23013
+ if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
23014
+ code: ZodIssueCode.custom,
23015
+ message: "speed and timelapse are mutually exclusive",
23016
+ path: ["timelapse"]
23017
+ });
23018
+ });
23019
+ var ExportStateSchema = _enum([
23020
+ "queued",
23021
+ "rendering",
23022
+ "ready",
23023
+ "failed",
23024
+ "expired",
23025
+ "deleted"
23026
+ ]);
23027
+ /** One export job / history row. */
23028
+ var ExportRecordSchema = object({
23029
+ id: string(),
23030
+ deviceId: number(),
23031
+ profile: string(),
23032
+ fromMs: number(),
23033
+ toMs: number(),
23034
+ options: ExportOptionsSchema,
23035
+ state: ExportStateSchema,
23036
+ /** 0–100 while rendering; null otherwise. */
23037
+ progressPct: number().nullable(),
23038
+ /** File size once ready; null before. */
23039
+ fileBytes: number().nullable(),
23040
+ expiresAt: number(),
23041
+ deleteAfterDownload: boolean(),
23042
+ /** Epoch of the first complete download; null until then. */
23043
+ downloadedAt: number().nullable(),
23044
+ createdAt: number(),
23045
+ /** User id/name that requested the export. */
23046
+ createdBy: string(),
23047
+ /** Failure reason when state is 'failed'; null otherwise. */
23048
+ error: string().nullable()
23049
+ });
23050
+ /** Candidate download URLs (LAN first, then operator extra hosts). */
23051
+ var ExportDownloadSchema = object({
23052
+ url: string(),
23053
+ endpoints: array(string())
23054
+ });
23055
+ method(object({
23056
+ deviceId: number(),
23057
+ profile: string(),
23058
+ fromMs: number(),
23059
+ toMs: number(),
23060
+ options: ExportOptionsSchema
23061
+ }), ExportRecordSchema, {
23062
+ kind: "mutation",
23063
+ auth: "protected"
23064
+ }), method(object({ deviceId: number().optional() }), array(ExportRecordSchema), {
23065
+ kind: "query",
23066
+ auth: "protected"
23067
+ }), method(object({ exportId: string() }), ExportRecordSchema, {
23068
+ kind: "query",
23069
+ auth: "protected"
23070
+ }), method(object({ exportId: string() }), ExportRecordSchema, {
23071
+ kind: "mutation",
23072
+ auth: "protected"
23073
+ }), method(object({ exportId: string() }), ExportRecordSchema, {
23074
+ kind: "mutation",
23075
+ auth: "protected"
23076
+ }), method(object({ exportId: string() }), ExportDownloadSchema, {
23077
+ kind: "query",
23078
+ auth: "protected"
23079
+ });
23080
+ /**
22897
23081
  * One publishable camera stream as its OWNING PROVIDER describes it — the same
22898
23082
  * payload the (legacy) push `streamBroker.publishCameraStream` carried, minus
22899
23083
  * `deviceId`.
@@ -26817,6 +27001,42 @@ Object.freeze({
26817
27001
  addonId: null,
26818
27002
  access: "create"
26819
27003
  },
27004
+ "recordingExport.cancelExport": {
27005
+ capName: "recordingExport",
27006
+ capScope: "system",
27007
+ addonId: null,
27008
+ access: "create"
27009
+ },
27010
+ "recordingExport.createExport": {
27011
+ capName: "recordingExport",
27012
+ capScope: "system",
27013
+ addonId: null,
27014
+ access: "create"
27015
+ },
27016
+ "recordingExport.deleteExport": {
27017
+ capName: "recordingExport",
27018
+ capScope: "system",
27019
+ addonId: null,
27020
+ access: "delete"
27021
+ },
27022
+ "recordingExport.getDownloadUrl": {
27023
+ capName: "recordingExport",
27024
+ capScope: "system",
27025
+ addonId: null,
27026
+ access: "view"
27027
+ },
27028
+ "recordingExport.getExport": {
27029
+ capName: "recordingExport",
27030
+ capScope: "system",
27031
+ addonId: null,
27032
+ access: "view"
27033
+ },
27034
+ "recordingExport.listExports": {
27035
+ capName: "recordingExport",
27036
+ capScope: "system",
27037
+ addonId: null,
27038
+ access: "view"
27039
+ },
26820
27040
  "sceneMonitor.captureReference": {
26821
27041
  capName: "scene-monitor",
26822
27042
  capScope: "device",
@@ -32801,7 +33021,7 @@ var BUS_EVENT_TYPES = [
32801
33021
  "deconz_event",
32802
33022
  "xiaomi_aqara.click"
32803
33023
  ];
32804
- var HaIntegrationManager = class {
33024
+ var HaIntegrationManager = class HaIntegrationManager {
32805
33025
  opts;
32806
33026
  wsClient = null;
32807
33027
  connection = null;
@@ -32815,6 +33035,15 @@ var HaIntegrationManager = class {
32815
33035
  lastCheckedAt = null;
32816
33036
  lastError = null;
32817
33037
  haInfo = {};
33038
+ /** Deadline timer for the INITIAL connect. `start()` no longer blocks on
33039
+ * the socket (see the comment there), so a broker whose host silently
33040
+ * drops packets would otherwise sit at `connecting` for the full OS TCP
33041
+ * timeout (tens of seconds) with no UI signal. This timer flips it to
33042
+ * `unreachable` promptly; a late successful connect still overrides it. */
33043
+ connectDeadlineTimer = null;
33044
+ /** Ceiling on the `connecting` state before the UI is shown `unreachable`.
33045
+ * Slightly above the throwaway `testSettings` probe timeout (8s). */
33046
+ static CONNECT_DEADLINE_MS = 1e4;
32818
33047
  constructor(opts) {
32819
33048
  this.opts = opts;
32820
33049
  }
@@ -32828,6 +33057,8 @@ var HaIntegrationManager = class {
32828
33057
  await this.refreshHaConfigInfo(connection);
32829
33058
  await this.ensureBaselineSubscription();
32830
33059
  await this.ensureBusEventSubscriptions();
33060
+ this.clearConnectDeadline();
33061
+ this.lastError = null;
32831
33062
  this.setStatus("connected");
32832
33063
  try {
32833
33064
  this.opts.onConnected?.(this.opts.id);
@@ -32863,14 +33094,32 @@ var HaIntegrationManager = class {
32863
33094
  }
32864
33095
  }
32865
33096
  });
32866
- try {
32867
- await this.wsClient.start();
32868
- } catch (err) {
33097
+ this.startConnectDeadline();
33098
+ this.wsClient.start().catch((err) => {
32869
33099
  this.classifyAuthError(err);
32870
- throw err;
33100
+ });
33101
+ }
33102
+ /** Arm the initial-connect deadline. Idempotent — clears any prior timer. */
33103
+ startConnectDeadline() {
33104
+ this.clearConnectDeadline();
33105
+ const timer = setTimeout(() => {
33106
+ this.connectDeadlineTimer = null;
33107
+ if (this.status === "connecting") {
33108
+ if (this.lastError === null) this.lastError = "Connection timed out";
33109
+ this.setStatus("unreachable");
33110
+ }
33111
+ }, HaIntegrationManager.CONNECT_DEADLINE_MS);
33112
+ if (typeof timer.unref === "function") timer.unref();
33113
+ this.connectDeadlineTimer = timer;
33114
+ }
33115
+ clearConnectDeadline() {
33116
+ if (this.connectDeadlineTimer) {
33117
+ clearTimeout(this.connectDeadlineTimer);
33118
+ this.connectDeadlineTimer = null;
32871
33119
  }
32872
33120
  }
32873
33121
  async stop() {
33122
+ this.clearConnectDeadline();
32874
33123
  if (this.upstreamSubscriptionUnsub) {
32875
33124
  try {
32876
33125
  await this.upstreamSubscriptionUnsub();
@@ -33085,15 +33334,19 @@ var HaIntegrationManager = class {
33085
33334
  }));
33086
33335
  }
33087
33336
  classifyAuthError(err) {
33337
+ this.clearConnectDeadline();
33088
33338
  if (err === 2) {
33339
+ this.lastError = "Authentication failed — check the access token";
33089
33340
  this.setStatus("auth-failed");
33090
- this.lastError = "invalid_auth";
33091
- } else if (err === 1 || err === 4) {
33341
+ } else if (err === 1) {
33342
+ this.lastError = "Cannot connect host unreachable or Home Assistant not responding";
33343
+ this.setStatus("unreachable");
33344
+ } else if (err === 4) {
33345
+ this.lastError = "Home Assistant host / URL is required";
33092
33346
  this.setStatus("unreachable");
33093
- this.lastError = errMsg(err);
33094
33347
  } else {
33095
- this.setStatus("error");
33096
33348
  this.lastError = errMsg(err);
33349
+ this.setStatus("error");
33097
33350
  }
33098
33351
  }
33099
33352
  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-H4AVePnn.mjs
3
+ //#region ../types/dist/event-category-D4HJq7Mw.mjs
4
4
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
5
5
  EventCategory["SystemBoot"] = "system.boot";
6
6
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -153,6 +153,11 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
153
153
  /** Runner-sampled scrub thumbnail (~1/5 s/camera). Telemetry (D8): a lost
154
154
  * thumb is a scrub gap the recorder's keyframe backfill covers. */
155
155
  EventCategory["RecordingThumbSampled"] = "recording.thumb-sampled";
156
+ /** Export render progress (0–100). Telemetry (D8): a lost tick is a stale
157
+ * progress bar the client reconciles via `recordingExport.getExport`. */
158
+ EventCategory["RecordingExportProgress"] = "recording.export.progress";
159
+ EventCategory["RecordingExportCompleted"] = "recording.export.completed";
160
+ EventCategory["RecordingExportFailed"] = "recording.export.failed";
156
161
  EventCategory["DetectionEvent"] = "detection.event";
157
162
  EventCategory["SessionTrackNew"] = "session.track.new";
158
163
  EventCategory["SessionTrackExpired"] = "session.track.expired";
@@ -442,6 +447,25 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
442
447
  */
443
448
  EventCategory["PipelineAnalyticsStationaryChanged"] = "pipeline-analytics.stationary-changed";
444
449
  /**
450
+ * Fired by `addon-post-analysis` when a package-drop is confirmed inside
451
+ * a package zone — a newly-appeared stationary object of a package class
452
+ * that cleared the class / zone / dwell / size gates. Payload:
453
+ * `PipelineAnalyticsPackageDeliveredPayload` carrying `{ deviceId,
454
+ * entryId, className, zoneIds, keyFrameMediaKey?, bbox, timestamp }`.
455
+ * Telemetry (D8): the durable record is the `package-events` store row;
456
+ * this bus topic drives notifier rules + live UI. See
457
+ * docs/superpowers/specs/2026-07-17-package-zones-design.md §5.1.
458
+ */
459
+ EventCategory["PipelineAnalyticsPackageDelivered"] = "pipeline-analytics.package-delivered";
460
+ /**
461
+ * Fired by `addon-post-analysis` when a previously-delivered package
462
+ * leaves its zone (the stationary entry departed — moved or swept).
463
+ * Payload: `PipelineAnalyticsPackagePickedUpPayload` carrying
464
+ * `{ deviceId, entryId, deliveredEventId, className, timestamp }`.
465
+ * Telemetry (D8). See package-zones-design §5.2.
466
+ */
467
+ EventCategory["PipelineAnalyticsPackagePickedUp"] = "pipeline-analytics.package-picked-up";
468
+ /**
445
469
  * Fired by `addon-post-analysis` whenever a gallery face row changes:
446
470
  * `kind:'buffered'` a new detected face was persisted, `'assigned'` /
447
471
  * `'unassigned'` its identity link changed, `'deleted'` the row was
@@ -5180,6 +5204,25 @@ function _instanceof(cls, params = {}) {
5180
5204
  };
5181
5205
  return inst;
5182
5206
  }
5207
+ //#endregion
5208
+ //#region ../../node_modules/zod/v4/classic/compat.js
5209
+ /** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
5210
+ var ZodIssueCode = {
5211
+ invalid_type: "invalid_type",
5212
+ too_big: "too_big",
5213
+ too_small: "too_small",
5214
+ invalid_format: "invalid_format",
5215
+ not_multiple_of: "not_multiple_of",
5216
+ unrecognized_keys: "unrecognized_keys",
5217
+ invalid_union: "invalid_union",
5218
+ invalid_key: "invalid_key",
5219
+ invalid_element: "invalid_element",
5220
+ invalid_value: "invalid_value",
5221
+ custom: "custom"
5222
+ };
5223
+ /** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */
5224
+ var ZodFirstPartyTypeKind;
5225
+ ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5183
5226
  Object.fromEntries([
5184
5227
  {
5185
5228
  id: "overview",
@@ -7355,6 +7398,24 @@ var RecordingRetentionSchema = object({
7355
7398
  maxSizeGb: number().min(0).optional()
7356
7399
  });
7357
7400
  /**
7401
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
7402
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
7403
+ * previews at. Five graduated steps; absent on a config = `standard` (the
7404
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
7405
+ *
7406
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
7407
+ * Each window's index sidecar carries its own tile dims, so a camera whose
7408
+ * preset changed over time renders every historical window at the dims it was
7409
+ * written with.
7410
+ */
7411
+ var ScrubThumbnailPresetSchema = _enum([
7412
+ "minimal",
7413
+ "low",
7414
+ "standard",
7415
+ "high",
7416
+ "max"
7417
+ ]);
7418
+ /**
7358
7419
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7359
7420
  *
7360
7421
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -7391,7 +7452,14 @@ var RecordingConfigSchema = object({
7391
7452
  * derived into bands once via `migrateConfigToBands`.
7392
7453
  */
7393
7454
  bands: array(RecordingBandSchema).optional(),
7394
- retention: RecordingRetentionSchema.optional()
7455
+ retention: RecordingRetentionSchema.optional(),
7456
+ /**
7457
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
7458
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
7459
+ * windows only — existing sheets are immutable, and each window's index
7460
+ * carries its own tile dims so mixed-preset history renders correctly.
7461
+ */
7462
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7395
7463
  });
7396
7464
  /**
7397
7465
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -14987,7 +15055,11 @@ array(ZoneRuleSchema).readonly();
14987
15055
  * Extend the enum here when a new gating consumer comes online (audio
14988
15056
  * gating, alert filtering, …) — no other surface needs to change.
14989
15057
  */
14990
- var ZoneRuleStageEnum = _enum(["motion", "detection"]);
15058
+ var ZoneRuleStageEnum = _enum([
15059
+ "motion",
15060
+ "detection",
15061
+ "package"
15062
+ ]);
14991
15063
  /**
14992
15064
  * Runtime registry: cap-property-name → cap definition. `BaseDevice`'s
14993
15065
  * `state` getter looks up the cap definition here to construct a
@@ -15081,16 +15153,24 @@ var DEVICE_LOCAL_STATE_CAPS = {
15081
15153
  })
15082
15154
  },
15083
15155
  /**
15084
- * Runtime-state slice — both stages mirrored together so consumers
15156
+ * Runtime-state slice — every stage mirrored together so consumers
15085
15157
  * see one reactive handle (`device.state.zoneRules.value`) instead
15086
- * of two. Bulk-replace mutations on either stage write the full
15087
- * `{motion, detection}` shape, so subscribers always get the
15158
+ * of one per stage. Bulk-replace mutations on any stage write the full
15159
+ * `{motion, detection, package}` shape, so subscribers always get the
15088
15160
  * complete current set. Consumers that only care about one stage
15089
15161
  * just read the matching property.
15162
+ *
15163
+ * `package` backs the package-drop detector — a package zone is a
15164
+ * `ZoneRule` on the `'package'` stage referencing drawn polygons
15165
+ * (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
15166
+ * The orchestrator provider that writes this stage lands in a later
15167
+ * slice; until then the mirror carries only `{motion, detection}` and
15168
+ * consumers read `package` as absent (treat as `[]`).
15090
15169
  */
15091
15170
  runtimeState: object({
15092
15171
  motion: array(ZoneRuleSchema).readonly(),
15093
- detection: array(ZoneRuleSchema).readonly()
15172
+ detection: array(ZoneRuleSchema).readonly(),
15173
+ package: array(ZoneRuleSchema).readonly()
15094
15174
  })
15095
15175
  },
15096
15176
  zones: zonesCapability
@@ -19151,6 +19231,7 @@ var EventKindIconSchema = _enum([
19151
19231
  "smoke",
19152
19232
  "water",
19153
19233
  "button",
19234
+ "package",
19154
19235
  "generic"
19155
19236
  ]);
19156
19237
  var EventKindCategorySchema = _enum([
@@ -19158,7 +19239,8 @@ var EventKindCategorySchema = _enum([
19158
19239
  "audio",
19159
19240
  "detection",
19160
19241
  "sensor",
19161
- "custom"
19242
+ "custom",
19243
+ "package"
19162
19244
  ]);
19163
19245
  var EventKindDescriptorSchema = object({
19164
19246
  /** Stable kind id (e.g. 'motion', 'person', 'contact'). */
@@ -22893,6 +22975,108 @@ method(object({
22893
22975
  auth: "admin"
22894
22976
  });
22895
22977
  /**
22978
+ * `recordingExport` cap — render a footage time range into a single downloadable
22979
+ * MP4 (regular / accelerated / decelerated / timelapse, ± audio), kept for a
22980
+ * bounded lifetime with a durable history, auto-expiry, and optional
22981
+ * delete-after-download.
22982
+ *
22983
+ * Like `recording` this is a `scope:'system', mode:'singleton'` cap: the
22984
+ * recorder self-gates so exactly ONE node (the designated `recordingNodeId`,
22985
+ * default `hub`) registers it. Device-scoped methods carry `deviceId` in their
22986
+ * input and dispatch to that single provider; the render runs on the node that
22987
+ * owns the footage (no cross-node segment transfer). Download rides the
22988
+ * framework addon data-plane. State persists in a DurableState blob (NOT
22989
+ * SQLite); history rows survive file deletion for audit.
22990
+ */
22991
+ /** Playback-speed multiplier for the render (1 = realtime). */
22992
+ var ExportSpeedSchema = number().min(.25).max(32);
22993
+ /** Timelapse cadence — sample one source frame per `everyMs`, output at `outputFps`. */
22994
+ var ExportTimelapseSchema = object({
22995
+ everyMs: number().int().positive(),
22996
+ outputFps: number().int().min(1).max(60).optional()
22997
+ });
22998
+ /**
22999
+ * Render options. `speed` and `timelapse` are mutually exclusive. `includeAudio`
23000
+ * is honoured only for a realtime-ish speed (0.5–2×); timelapse is always
23001
+ * silent. `maxLifeMs` bounds how long the finished file is kept;
23002
+ * `deleteAfterDownload` removes it shortly after the first complete download.
23003
+ */
23004
+ var ExportOptionsSchema = object({
23005
+ speed: ExportSpeedSchema.optional(),
23006
+ timelapse: ExportTimelapseSchema.optional(),
23007
+ includeAudio: boolean(),
23008
+ maxLifeMs: number().int().positive(),
23009
+ deleteAfterDownload: boolean(),
23010
+ title: string().max(200).optional()
23011
+ }).superRefine((v, ctx) => {
23012
+ if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
23013
+ code: ZodIssueCode.custom,
23014
+ message: "speed and timelapse are mutually exclusive",
23015
+ path: ["timelapse"]
23016
+ });
23017
+ });
23018
+ var ExportStateSchema = _enum([
23019
+ "queued",
23020
+ "rendering",
23021
+ "ready",
23022
+ "failed",
23023
+ "expired",
23024
+ "deleted"
23025
+ ]);
23026
+ /** One export job / history row. */
23027
+ var ExportRecordSchema = object({
23028
+ id: string(),
23029
+ deviceId: number(),
23030
+ profile: string(),
23031
+ fromMs: number(),
23032
+ toMs: number(),
23033
+ options: ExportOptionsSchema,
23034
+ state: ExportStateSchema,
23035
+ /** 0–100 while rendering; null otherwise. */
23036
+ progressPct: number().nullable(),
23037
+ /** File size once ready; null before. */
23038
+ fileBytes: number().nullable(),
23039
+ expiresAt: number(),
23040
+ deleteAfterDownload: boolean(),
23041
+ /** Epoch of the first complete download; null until then. */
23042
+ downloadedAt: number().nullable(),
23043
+ createdAt: number(),
23044
+ /** User id/name that requested the export. */
23045
+ createdBy: string(),
23046
+ /** Failure reason when state is 'failed'; null otherwise. */
23047
+ error: string().nullable()
23048
+ });
23049
+ /** Candidate download URLs (LAN first, then operator extra hosts). */
23050
+ var ExportDownloadSchema = object({
23051
+ url: string(),
23052
+ endpoints: array(string())
23053
+ });
23054
+ method(object({
23055
+ deviceId: number(),
23056
+ profile: string(),
23057
+ fromMs: number(),
23058
+ toMs: number(),
23059
+ options: ExportOptionsSchema
23060
+ }), ExportRecordSchema, {
23061
+ kind: "mutation",
23062
+ auth: "protected"
23063
+ }), method(object({ deviceId: number().optional() }), array(ExportRecordSchema), {
23064
+ kind: "query",
23065
+ auth: "protected"
23066
+ }), method(object({ exportId: string() }), ExportRecordSchema, {
23067
+ kind: "query",
23068
+ auth: "protected"
23069
+ }), method(object({ exportId: string() }), ExportRecordSchema, {
23070
+ kind: "mutation",
23071
+ auth: "protected"
23072
+ }), method(object({ exportId: string() }), ExportRecordSchema, {
23073
+ kind: "mutation",
23074
+ auth: "protected"
23075
+ }), method(object({ exportId: string() }), ExportDownloadSchema, {
23076
+ kind: "query",
23077
+ auth: "protected"
23078
+ });
23079
+ /**
22896
23080
  * One publishable camera stream as its OWNING PROVIDER describes it — the same
22897
23081
  * payload the (legacy) push `streamBroker.publishCameraStream` carried, minus
22898
23082
  * `deviceId`.
@@ -26816,6 +27000,42 @@ Object.freeze({
26816
27000
  addonId: null,
26817
27001
  access: "create"
26818
27002
  },
27003
+ "recordingExport.cancelExport": {
27004
+ capName: "recordingExport",
27005
+ capScope: "system",
27006
+ addonId: null,
27007
+ access: "create"
27008
+ },
27009
+ "recordingExport.createExport": {
27010
+ capName: "recordingExport",
27011
+ capScope: "system",
27012
+ addonId: null,
27013
+ access: "create"
27014
+ },
27015
+ "recordingExport.deleteExport": {
27016
+ capName: "recordingExport",
27017
+ capScope: "system",
27018
+ addonId: null,
27019
+ access: "delete"
27020
+ },
27021
+ "recordingExport.getDownloadUrl": {
27022
+ capName: "recordingExport",
27023
+ capScope: "system",
27024
+ addonId: null,
27025
+ access: "view"
27026
+ },
27027
+ "recordingExport.getExport": {
27028
+ capName: "recordingExport",
27029
+ capScope: "system",
27030
+ addonId: null,
27031
+ access: "view"
27032
+ },
27033
+ "recordingExport.listExports": {
27034
+ capName: "recordingExport",
27035
+ capScope: "system",
27036
+ addonId: null,
27037
+ access: "view"
27038
+ },
26819
27039
  "sceneMonitor.captureReference": {
26820
27040
  capName: "scene-monitor",
26821
27041
  capScope: "device",
@@ -32800,7 +33020,7 @@ var BUS_EVENT_TYPES = [
32800
33020
  "deconz_event",
32801
33021
  "xiaomi_aqara.click"
32802
33022
  ];
32803
- var HaIntegrationManager = class {
33023
+ var HaIntegrationManager = class HaIntegrationManager {
32804
33024
  opts;
32805
33025
  wsClient = null;
32806
33026
  connection = null;
@@ -32814,6 +33034,15 @@ var HaIntegrationManager = class {
32814
33034
  lastCheckedAt = null;
32815
33035
  lastError = null;
32816
33036
  haInfo = {};
33037
+ /** Deadline timer for the INITIAL connect. `start()` no longer blocks on
33038
+ * the socket (see the comment there), so a broker whose host silently
33039
+ * drops packets would otherwise sit at `connecting` for the full OS TCP
33040
+ * timeout (tens of seconds) with no UI signal. This timer flips it to
33041
+ * `unreachable` promptly; a late successful connect still overrides it. */
33042
+ connectDeadlineTimer = null;
33043
+ /** Ceiling on the `connecting` state before the UI is shown `unreachable`.
33044
+ * Slightly above the throwaway `testSettings` probe timeout (8s). */
33045
+ static CONNECT_DEADLINE_MS = 1e4;
32817
33046
  constructor(opts) {
32818
33047
  this.opts = opts;
32819
33048
  }
@@ -32827,6 +33056,8 @@ var HaIntegrationManager = class {
32827
33056
  await this.refreshHaConfigInfo(connection);
32828
33057
  await this.ensureBaselineSubscription();
32829
33058
  await this.ensureBusEventSubscriptions();
33059
+ this.clearConnectDeadline();
33060
+ this.lastError = null;
32830
33061
  this.setStatus("connected");
32831
33062
  try {
32832
33063
  this.opts.onConnected?.(this.opts.id);
@@ -32862,14 +33093,32 @@ var HaIntegrationManager = class {
32862
33093
  }
32863
33094
  }
32864
33095
  });
32865
- try {
32866
- await this.wsClient.start();
32867
- } catch (err) {
33096
+ this.startConnectDeadline();
33097
+ this.wsClient.start().catch((err) => {
32868
33098
  this.classifyAuthError(err);
32869
- throw err;
33099
+ });
33100
+ }
33101
+ /** Arm the initial-connect deadline. Idempotent — clears any prior timer. */
33102
+ startConnectDeadline() {
33103
+ this.clearConnectDeadline();
33104
+ const timer = setTimeout(() => {
33105
+ this.connectDeadlineTimer = null;
33106
+ if (this.status === "connecting") {
33107
+ if (this.lastError === null) this.lastError = "Connection timed out";
33108
+ this.setStatus("unreachable");
33109
+ }
33110
+ }, HaIntegrationManager.CONNECT_DEADLINE_MS);
33111
+ if (typeof timer.unref === "function") timer.unref();
33112
+ this.connectDeadlineTimer = timer;
33113
+ }
33114
+ clearConnectDeadline() {
33115
+ if (this.connectDeadlineTimer) {
33116
+ clearTimeout(this.connectDeadlineTimer);
33117
+ this.connectDeadlineTimer = null;
32870
33118
  }
32871
33119
  }
32872
33120
  async stop() {
33121
+ this.clearConnectDeadline();
32873
33122
  if (this.upstreamSubscriptionUnsub) {
32874
33123
  try {
32875
33124
  await this.upstreamSubscriptionUnsub();
@@ -33084,15 +33333,19 @@ var HaIntegrationManager = class {
33084
33333
  }));
33085
33334
  }
33086
33335
  classifyAuthError(err) {
33336
+ this.clearConnectDeadline();
33087
33337
  if (err === 2) {
33338
+ this.lastError = "Authentication failed — check the access token";
33088
33339
  this.setStatus("auth-failed");
33089
- this.lastError = "invalid_auth";
33090
- } else if (err === 1 || err === 4) {
33340
+ } else if (err === 1) {
33341
+ this.lastError = "Cannot connect host unreachable or Home Assistant not responding";
33342
+ this.setStatus("unreachable");
33343
+ } else if (err === 4) {
33344
+ this.lastError = "Home Assistant host / URL is required";
33091
33345
  this.setStatus("unreachable");
33092
- this.lastError = errMsg(err);
33093
33346
  } else {
33094
- this.setStatus("error");
33095
33347
  this.lastError = errMsg(err);
33348
+ this.setStatus("error");
33096
33349
  }
33097
33350
  }
33098
33351
  requireConnection() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.1.28",
3
+ "version": "1.1.30",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",