@camstack/addon-provider-homeassistant 1.2.146 → 1.2.147

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/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dist = require("./dist-BTJhdlL4.js");
2
+ const require_dist = require("./dist-CslhAXSs.js");
3
3
  let node_crypto = require("node:crypto");
4
4
  let node_zlib = require("node:zlib");
5
5
  //#region src/ha-admin-link.ts
@@ -8401,17 +8401,9 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8401
8401
  */
8402
8402
  async onRestoreDevices(savedDevices) {
8403
8403
  const { standalone, containerChildren, rest } = partitionRestoreDevices(savedDevices);
8404
- for (const saved of standalone) try {
8404
+ await this.restoreWithBoundedRetry(standalone, async (saved) => {
8405
8405
  await this.ctx.kernel.devices.create(saved.stableId, standaloneEntityDeviceClass, {});
8406
- } catch (err) {
8407
- this.ctx.logger.warn("ha restore: standalone entity device failed", {
8408
- tags: { stableId: saved.stableId },
8409
- meta: {
8410
- type: saved.type,
8411
- error: require_dist.errMsg(err)
8412
- }
8413
- });
8414
- }
8406
+ });
8415
8407
  await super.onRestoreDevices(rest);
8416
8408
  }
8417
8409
  /** Re-sync every currently-connected broker's adopted devices (Containers +
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as parseAdminBase, A as contactCapability, At as union, B as gasCapability, C as buildAddonRouteProvider, Ct as array, D as climateControlCapability, Dt as object, E as carbonMonoxideCapability, Et as number, G as lockControlCapability, H as humiditySensorCapability, I as enumSensorCapability, J as normalizeUnit, K as mediaPlayerCapability, L as eventEmitterCapability, M as coverCapability, Mt as url, N as deviceAdminLinkCapability, Nt as EventCategory, O as colorCapability, Ot as record, P as deviceAdoptionCapability, R as fanControlCapability, S as brokerCapability, St as _enum, T as buttonCapability, Tt as literal, U as imageCapability, V as humidifierCapability, W as lawnMowerControlCapability, X as notifierCapability, Y as notificationOutputCapability, Z as numericSensorCapability, _ as automationControlCapability, _t as DeviceType, at as smokeCapability, b as binaryCapability, bt as createEvent, ct as temperatureSensorCapability, d as TargetSchema, dt as valveCapability, et as powerMeterCapability, f as accessoriesCapability, ft as vibrationCapability, g as ambientLightSensorCapability, gt as DeviceRole, h as alarmPanelCapability, ht as DeviceFeature, it as scriptRunnerCapability, j as controlCapability, jt as unknown, k as connectivityCapability, kt as string, lt as updateCapability, m as airQualitySensorCapability, mt as weatherCapability, n as BaseDevice, nt as presenceCapability, ot as switchCapability, p as addonRoutesCapability, pt as waterHeaterCapability, q as motionCapability, r as BaseDeviceProvider, rt as pressureSensorCapability, s as EnumSensorDateTimeFormatSchema, st as tamperCapability, tt as prepareNotification, ut as vacuumControlCapability, v as batteryCapability, vt as errMsg, w as buildDeviceAdminUrl, wt as discriminatedUnion, x as brightnessCapability, y as bestLocationMatch, z as floodCapability } from "./dist-D2jDnKLv.mjs";
1
+ import { $ as parseAdminBase, A as contactCapability, At as union, B as gasCapability, C as buildAddonRouteProvider, Ct as array, D as climateControlCapability, Dt as object, E as carbonMonoxideCapability, Et as number, G as lockControlCapability, H as humiditySensorCapability, I as enumSensorCapability, J as normalizeUnit, K as mediaPlayerCapability, L as eventEmitterCapability, M as coverCapability, Mt as url, N as deviceAdminLinkCapability, Nt as EventCategory, O as colorCapability, Ot as record, P as deviceAdoptionCapability, R as fanControlCapability, S as brokerCapability, St as _enum, T as buttonCapability, Tt as literal, U as imageCapability, V as humidifierCapability, W as lawnMowerControlCapability, X as notifierCapability, Y as notificationOutputCapability, Z as numericSensorCapability, _ as automationControlCapability, _t as DeviceType, at as smokeCapability, b as binaryCapability, bt as createEvent, ct as temperatureSensorCapability, d as TargetSchema, dt as valveCapability, et as powerMeterCapability, f as accessoriesCapability, ft as vibrationCapability, g as ambientLightSensorCapability, gt as DeviceRole, h as alarmPanelCapability, ht as DeviceFeature, it as scriptRunnerCapability, j as controlCapability, jt as unknown, k as connectivityCapability, kt as string, lt as updateCapability, m as airQualitySensorCapability, mt as weatherCapability, n as BaseDevice, nt as presenceCapability, ot as switchCapability, p as addonRoutesCapability, pt as waterHeaterCapability, q as motionCapability, r as BaseDeviceProvider, rt as pressureSensorCapability, s as EnumSensorDateTimeFormatSchema, st as tamperCapability, tt as prepareNotification, ut as vacuumControlCapability, v as batteryCapability, vt as errMsg, w as buildDeviceAdminUrl, wt as discriminatedUnion, x as brightnessCapability, y as bestLocationMatch, z as floodCapability } from "./dist-DVwhiKWh.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
4
4
  //#region src/ha-admin-link.ts
@@ -8400,17 +8400,9 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8400
8400
  */
8401
8401
  async onRestoreDevices(savedDevices) {
8402
8402
  const { standalone, containerChildren, rest } = partitionRestoreDevices(savedDevices);
8403
- for (const saved of standalone) try {
8403
+ await this.restoreWithBoundedRetry(standalone, async (saved) => {
8404
8404
  await this.ctx.kernel.devices.create(saved.stableId, standaloneEntityDeviceClass, {});
8405
- } catch (err) {
8406
- this.ctx.logger.warn("ha restore: standalone entity device failed", {
8407
- tags: { stableId: saved.stableId },
8408
- meta: {
8409
- type: saved.type,
8410
- error: errMsg(err)
8411
- }
8412
- });
8413
- }
8405
+ });
8414
8406
  await super.onRestoreDevices(rest);
8415
8407
  }
8416
8408
  /** Re-sync every currently-connected broker's adopted devices (Containers +
@@ -5370,7 +5370,7 @@ var ZodIssueCode = {
5370
5370
  var ZodFirstPartyTypeKind;
5371
5371
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5372
5372
  //#endregion
5373
- //#region ../types/dist/sleep-jkRQgtSc.mjs
5373
+ //#region ../types/dist/sleep-DlrgY4Qb.mjs
5374
5374
  /**
5375
5375
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5376
5376
  * window to float samples (D455).
@@ -6985,6 +6985,10 @@ function toNodeId(idOrRunnerId) {
6985
6985
  const head = idOrRunnerId.split("/")[0];
6986
6986
  return head === void 0 || head.length === 0 ? idOrRunnerId : head;
6987
6987
  }
6988
+ object({
6989
+ field: string().min(1),
6990
+ on: literal("advance")
6991
+ });
6988
6992
  /**
6989
6993
  * Output schema shared by the contribution + live methods.
6990
6994
  *
@@ -7468,7 +7472,45 @@ function errMsg(err) {
7468
7472
  return String(err);
7469
7473
  }
7470
7474
  //#endregion
7471
- //#region ../types/dist/composition-CPFIlFfw.mjs
7475
+ //#region ../types/dist/composition-BK-GTFYb.mjs
7476
+ /**
7477
+ * LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
7478
+ * valutatore puro del suo FRONTE.
7479
+ *
7480
+ * Viveva dentro il builtin virtual-doorbell
7481
+ * (`@camstack/system` — `builtins/doorbell/trigger-engine.ts`) e i suoi
7482
+ * predicati erano privati al modulo. Il recorder ne ha bisogno per il trigger
7483
+ * `RecordingTriggers.sensorDeviceIds`: copiarla avrebbe creato la SECONDA
7484
+ * tabella, che diverge alla prima cap aggiunta e il cui sintomo — "il sensore
7485
+ * fa suonare il campanello ma non registra" — è esattamente D62. Quindi si
7486
+ * SPOSTA qui e il doorbell la ri-esporta.
7487
+ *
7488
+ * ⚠ NON è `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), e le
7489
+ * due non vanno unificate: quella risponde a "qual è la PAROLA di stato per una
7490
+ * regola" (e include `presence`, `cover`, `alarm-panel`), questa a "qual è il
7491
+ * booleano il cui FRONTE conta". Vocabolari deliberatamente diversi.
7492
+ */
7493
+ /**
7494
+ * Known binary / switch source caps → the boolean slice field whose
7495
+ * false→true rise counts as ACTIVE. Every entry is "fire on active".
7496
+ * Sensors whose "active" reading is not a plain boolean (presence's string
7497
+ * state, connectivity's connected flag) are deliberately excluded — a
7498
+ * reconnect is not a doorbell press, and it is not a recording either.
7499
+ */
7500
+ var SOURCE_CAP_ACTIVE_FIELD = {
7501
+ contact: "entryOpen",
7502
+ binary: "on",
7503
+ switch: "on",
7504
+ motion: "detected",
7505
+ flood: "flooded",
7506
+ gas: "detected",
7507
+ smoke: "detected",
7508
+ "carbon-monoxide": "detected",
7509
+ vibration: "detected",
7510
+ tamper: "tampered"
7511
+ };
7512
+ /** Cap names whose presence in a device's bindings qualify it as a source. */
7513
+ var SOURCE_CAPS = Object.keys(SOURCE_CAP_ACTIVE_FIELD);
7472
7514
  /**
7473
7515
  * Error types for the safe expression engine. Two distinct classes so callers
7474
7516
  * can tell a compile-time (grammar) failure from a runtime (evaluation)
@@ -7666,6 +7708,20 @@ var table = {
7666
7708
  const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
7667
7709
  return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
7668
7710
  }
7711
+ },
7712
+ rose: {
7713
+ minArgs: 2,
7714
+ maxArgs: 3,
7715
+ apply: () => {
7716
+ throw new ExpressionEvalError("rose: stateful evaluation context is required");
7717
+ }
7718
+ },
7719
+ count: {
7720
+ minArgs: 1,
7721
+ maxArgs: 1,
7722
+ apply: () => {
7723
+ throw new ExpressionEvalError("count: stateful evaluation context is required");
7724
+ }
7669
7725
  }
7670
7726
  };
7671
7727
  Object.freeze(Object.assign(Object.create(null), table));
@@ -8180,6 +8236,7 @@ var CompositionItemEntrySchema = object({
8180
8236
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
8181
8237
  kind: literal("fields"),
8182
8238
  cap: string().min(1),
8239
+ enabled: boolean().optional(),
8183
8240
  fields: record(string().min(1), CompositionFieldSourceSchema).refine((fields) => Object.keys(fields).length <= 32, { message: `at most 32 fields per capability` }),
8184
8241
  /**
8185
8242
  * Items of the cap's item array, keyed by item key (D663). Naming `items`
@@ -8191,6 +8248,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
8191
8248
  }), object({
8192
8249
  kind: literal("passthrough"),
8193
8250
  cap: string().min(1),
8251
+ enabled: boolean().optional(),
8194
8252
  source: CompositionSourceRefSchema
8195
8253
  })]);
8196
8254
  var CompositionSchema = object({
@@ -13138,6 +13196,15 @@ var cameraStreamsCapability = {
13138
13196
  */
13139
13197
  durability: "session"
13140
13198
  };
13199
+ EventCategory.DoorbellOnPressed;
13200
+ var CompositionEventPlanSchema = object({
13201
+ featureIndex: number().int().nonnegative(),
13202
+ cap: string(),
13203
+ eventName: string(),
13204
+ category: _enum(EventCategory),
13205
+ stateField: string(),
13206
+ fieldKey: string()
13207
+ });
13141
13208
  /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
13142
13209
  var CompositionProblemSchema = object({
13143
13210
  code: _enum([
@@ -13187,12 +13254,14 @@ var CompositionUnavailableModeSchema = _enum([
13187
13254
  ]);
13188
13255
  /**
13189
13256
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
13190
- * field of its cap changes value · `constant` reads nothing, computed once.
13257
+ * field of its cap changes value · `constant` reads nothing, computed once ·
13258
+ * `accumulator` advances from the target's seed/current output.
13191
13259
  */
13192
13260
  var CompositionFieldRoleSchema = _enum([
13193
13261
  "derived",
13194
13262
  "clock",
13195
- "constant"
13263
+ "constant",
13264
+ "accumulator"
13196
13265
  ]);
13197
13266
  /** The item a planned field belongs to; its `path` is then the dotted path INSIDE the item. */
13198
13267
  var CompositionFieldItemSchema = object({
@@ -13228,6 +13297,7 @@ var CompositionFeatureModeSchema = _enum([
13228
13297
  "replace"
13229
13298
  ]);
13230
13299
  var CompositionFeaturePlanSchema = object({
13300
+ /** Index in the stored feature array; disabled features are omitted rather than re-indexed. */
13231
13301
  featureIndex: number().int().nonnegative(),
13232
13302
  cap: string(),
13233
13303
  mode: CompositionFeatureModeSchema
@@ -13236,7 +13306,8 @@ var CompositionValidationSchema = object({
13236
13306
  ok: boolean(),
13237
13307
  problems: array(CompositionProblemSchema),
13238
13308
  fields: array(CompositionFieldPlanSchema),
13239
- features: array(CompositionFeaturePlanSchema)
13309
+ features: array(CompositionFeaturePlanSchema),
13310
+ events: array(CompositionEventPlanSchema)
13240
13311
  });
13241
13312
  /**
13242
13313
  * Mirrors `CoreBlockStatus`, pinned by `core-blocks-source.spec.ts`. `degraded`:
@@ -15253,6 +15324,34 @@ var DeviceBindingsForDeviceSchema = object({
15253
15324
  nativeAddonId: string()
15254
15325
  }))
15255
15326
  });
15327
+ /**
15328
+ * The STORED wrapper activation for one `(device, capability)` pair — the row
15329
+ * `setWrapperActive` writes, read back three-way (D675):
15330
+ *
15331
+ * - `value` — the store answered and holds an activation: the bound wrapper's
15332
+ * addon id, or `null`, the operator's explicit "off" marker (D62);
15333
+ * - `missing` — the store answered, its activation map exists, and it holds
15334
+ * nothing for this pair: the operator never activated or deactivated a
15335
+ * wrapper here;
15336
+ * - `failed` — the store could not be read, holds no activation map at all
15337
+ * (an empty or unready snapshot, D44), or holds something that is not an
15338
+ * activation. It carries NO information: a consumer must never read it as
15339
+ * `missing` (D315, D393).
15340
+ *
15341
+ * This is the persisted INPUT, not the derived `getBindings` view — no registry
15342
+ * resolution, no `defaultActive` auto-binding, no cluster native-cap folding.
15343
+ */
15344
+ var WrapperActivationReadSchema = discriminatedUnion("kind", [
15345
+ object({
15346
+ kind: literal("value"),
15347
+ wrapperAddonId: string().nullable()
15348
+ }),
15349
+ object({ kind: literal("missing") }),
15350
+ object({
15351
+ kind: literal("failed"),
15352
+ error: string()
15353
+ })
15354
+ ]);
15256
15355
  var SavedDeviceRowSchema = object({
15257
15356
  /** Numeric id reserved at allocateDeviceId time. */
15258
15357
  id: number(),
@@ -15603,7 +15702,10 @@ method(object({
15603
15702
  }), _void(), {
15604
15703
  kind: "mutation",
15605
15704
  auth: "admin"
15606
- }), method(object({ capName: string() }), array(string())), method(object({ deviceType: string() }), array(object({
15705
+ }), method(object({
15706
+ deviceId: number(),
15707
+ capName: string().min(1)
15708
+ }), WrapperActivationReadSchema), method(object({ capName: string() }), array(string())), method(object({ deviceType: string() }), array(object({
15607
15709
  capName: string(),
15608
15710
  wrappers: array(string())
15609
15711
  }))), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), object({
@@ -27251,44 +27353,6 @@ DeviceType.Camera, method(object({
27251
27353
  auth: "protected"
27252
27354
  });
27253
27355
  /**
27254
- * LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
27255
- * valutatore puro del suo FRONTE.
27256
- *
27257
- * Viveva dentro il builtin virtual-doorbell
27258
- * (`@camstack/system` — `builtins/doorbell/trigger-engine.ts`) e i suoi
27259
- * predicati erano privati al modulo. Il recorder ne ha bisogno per il trigger
27260
- * `RecordingTriggers.sensorDeviceIds`: copiarla avrebbe creato la SECONDA
27261
- * tabella, che diverge alla prima cap aggiunta e il cui sintomo — "il sensore
27262
- * fa suonare il campanello ma non registra" — è esattamente D62. Quindi si
27263
- * SPOSTA qui e il doorbell la ri-esporta.
27264
- *
27265
- * ⚠ NON è `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), e le
27266
- * due non vanno unificate: quella risponde a "qual è la PAROLA di stato per una
27267
- * regola" (e include `presence`, `cover`, `alarm-panel`), questa a "qual è il
27268
- * booleano il cui FRONTE conta". Vocabolari deliberatamente diversi.
27269
- */
27270
- /**
27271
- * Known binary / switch source caps → the boolean slice field whose
27272
- * false→true rise counts as ACTIVE. Every entry is "fire on active".
27273
- * Sensors whose "active" reading is not a plain boolean (presence's string
27274
- * state, connectivity's connected flag) are deliberately excluded — a
27275
- * reconnect is not a doorbell press, and it is not a recording either.
27276
- */
27277
- var SOURCE_CAP_ACTIVE_FIELD = {
27278
- contact: "entryOpen",
27279
- binary: "on",
27280
- switch: "on",
27281
- motion: "detected",
27282
- flood: "flooded",
27283
- gas: "detected",
27284
- smoke: "detected",
27285
- "carbon-monoxide": "detected",
27286
- vibration: "detected",
27287
- tamper: "tampered"
27288
- };
27289
- /** Cap names whose presence in a device's bindings qualify it as a source. */
27290
- var SOURCE_CAPS = Object.keys(SOURCE_CAP_ACTIVE_FIELD);
27291
- /**
27292
27356
  * Doorbell button cap. Two kinds of providers coexist behind this cap
27293
27357
  * name (same pattern as `snapshot`):
27294
27358
  *
@@ -27361,7 +27425,13 @@ var doorbellCapability = {
27361
27425
  * push (`ReolinkSimpleEvent.type === 'doorbell'`). There is no
27362
27426
  * release/duration — it's a pulse.
27363
27427
  */
27364
- onPressed: { data: DoorbellPressEventSchema } },
27428
+ onPressed: {
27429
+ data: DoorbellPressEventSchema,
27430
+ fromState: {
27431
+ field: "lastPressedAt",
27432
+ on: "advance"
27433
+ }
27434
+ } },
27365
27435
  status: {
27366
27436
  schema: DoorbellStatusSchema,
27367
27437
  kind: "push"
@@ -35248,12 +35318,21 @@ var RecordingStorageUsageSchema = object({
35248
35318
  */
35249
35319
  devicesRead: _enum(["archive", "unavailable"]).optional(),
35250
35320
  /**
35321
+ * Is the `archive` answer WHOLE (D674): the hour ledger loaded without
35322
+ * hitting its row cap, and at least one location resolved? Only then may a
35323
+ * camera ABSENT from `devices[]` be read as "no footage" — and only then is a
35324
+ * row's `oldestMs` the archive's oldest rather than the oldest of a fraction.
35325
+ * OPTIONAL: a recorder predating D674 omits it, and a consumer must read the
35326
+ * absence as `false`. A retention floor is a delete; it never guesses.
35327
+ */
35328
+ complete: boolean().optional(),
35329
+ /**
35251
35330
  * Per-physical-volume capacity. OPTIONAL, and consumers must render the rest
35252
- * of the payload without it: the hub serves this method through a BAKED
35253
- * router, so a hub whose framework train predates a change to this array
35254
- * strips it out of an answer a newer recorder happily produced. A UI that
35255
- * dereferences it unconditionally crashes the whole Storage page over a
35256
- * missing disk bar.
35331
+ * of the payload without it: a recorder that could not build it omits it. A
35332
+ * UI that dereferences it unconditionally crashes the whole Storage page over
35333
+ * a missing disk bar. (The hub's cap routers pass output through unvalidated
35334
+ * since 2026-07-11 — `cap-router-builder.ts` — so a newer recorder's fields
35335
+ * are not stripped by an older hub.)
35257
35336
  */
35258
35337
  locations: array(RecordingLocationUsageSchema).optional()
35259
35338
  });
@@ -38772,7 +38851,12 @@ Object.freeze({
38772
38851
  number: {
38773
38852
  kind: "fixed",
38774
38853
  result: ["number", "null"]
38775
- }
38854
+ },
38855
+ rose: {
38856
+ kind: "fixed",
38857
+ result: ["number", "null"]
38858
+ },
38859
+ count: NUMBER
38776
38860
  });
38777
38861
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
38778
38862
  /**
@@ -39813,6 +39897,7 @@ var BaseDeviceProvider = class extends BaseAddon {
39813
39897
  }];
39814
39898
  }
39815
39899
  async onShutdown() {
39900
+ this._restoreShuttingDown = true;
39816
39901
  this.cancelRestoreRetries();
39817
39902
  const devices = await this.ctx.kernel.devices?.getAll() ?? [];
39818
39903
  for (const device of devices) try {
@@ -39930,6 +40015,7 @@ var BaseDeviceProvider = class extends BaseAddon {
39930
40015
  };
39931
40016
  }
39932
40017
  async restoreDevices(savedDevices) {
40018
+ this._restoreShuttingDown = false;
39933
40019
  const report = await this.onRestoreDevices(savedDevices);
39934
40020
  if (savedDevices.length === 0) return;
39935
40021
  if (report && report.failedCount > 0) {
@@ -39944,8 +40030,13 @@ var BaseDeviceProvider = class extends BaseAddon {
39944
40030
  /** Retry lane width. See `device-restore-retry.ts` for why retries
39945
40031
  * never re-stampede full-width while the initial pass does (D167). */
39946
40032
  restoreRetryConcurrency = 4;
39947
- _restoreRetryScheduler = null;
40033
+ /** Every bounded retry this process started — the default pass's, and each
40034
+ * `restoreWithBoundedRetry` of a provider's own rows. Cancelled together. */
40035
+ _restoreRetrySchedulers = [];
39948
40036
  _restoreRetryCompletion = null;
40037
+ /** Set by `onShutdown` BEFORE the retries are cancelled: a restore pass still
40038
+ * in flight must not start a scheduler after the cancel ran. */
40039
+ _restoreShuttingDown = false;
39949
40040
  _permanentRestoreFailures = /* @__PURE__ */ new Map();
39950
40041
  /** Settles when the background retry rounds finish (or `null` when
39951
40042
  * nothing failed). Exposed for tests and subclass diagnostics —
@@ -40014,8 +40105,8 @@ var BaseDeviceProvider = class extends BaseAddon {
40014
40105
  });
40015
40106
  }
40016
40107
  cancelRestoreRetries() {
40017
- this._restoreRetryScheduler?.cancel();
40018
- this._restoreRetryScheduler = null;
40108
+ for (const scheduler of this._restoreRetrySchedulers) scheduler.cancel();
40109
+ this._restoreRetrySchedulers = [];
40019
40110
  }
40020
40111
  recordPermanentRestoreFailure(failure) {
40021
40112
  this._permanentRestoreFailures.set(failure.deviceId, failure);
@@ -40032,6 +40123,13 @@ var BaseDeviceProvider = class extends BaseAddon {
40032
40123
  });
40033
40124
  }
40034
40125
  scheduleRestoreRetries(failures, attempt) {
40126
+ if (this._restoreShuttingDown) {
40127
+ this.ctx.logger.info("Restore retries not scheduled — the provider is shutting down", { meta: {
40128
+ rows: failures.length,
40129
+ deviceIds: failures.map((f) => f.saved.id)
40130
+ } });
40131
+ return;
40132
+ }
40035
40133
  const scheduler = new DeviceRestoreRetryScheduler({
40036
40134
  logger: this.ctx.logger,
40037
40135
  delaysMs: this.restoreRetryDelaysMs,
@@ -40039,10 +40137,54 @@ var BaseDeviceProvider = class extends BaseAddon {
40039
40137
  attempt,
40040
40138
  onPermanentFailure: (failure) => this.recordPermanentRestoreFailure(failure)
40041
40139
  });
40042
- this._restoreRetryScheduler = scheduler;
40043
- this._restoreRetryCompletion = scheduler.run(failures).then(() => void 0).catch((err) => {
40140
+ this._restoreRetrySchedulers = [...this._restoreRetrySchedulers, scheduler];
40141
+ const run = scheduler.run(failures).then(() => void 0).catch((err) => {
40044
40142
  this.ctx.logger.error("Restore retry scheduler crashed", { meta: { error: err instanceof Error ? err.message : String(err) } });
40045
40143
  });
40144
+ const prior = this._restoreRetryCompletion;
40145
+ this._restoreRetryCompletion = prior === null ? run : Promise.all([prior, run]).then(() => void 0);
40146
+ }
40147
+ /**
40148
+ * Restore rows the default pass does not handle (a provider's own restore
40149
+ * loop) under the SAME bounded retry (D347): one attempt each, in order; a
40150
+ * row that throws is logged with `tags.deviceId` and retried in the
40151
+ * background rounds, then marked permanently failed like any other.
40152
+ *
40153
+ * A restore is a device build, and a build whose runtime-state load did not
40154
+ * answer is REFUSED rather than started on cold defaults (D677) — so a
40155
+ * transient hub read must cost a retry, never the device until the next
40156
+ * runner restart. An ANSWERED `{}` is the device's true state and never
40157
+ * refuses.
40158
+ */
40159
+ async restoreWithBoundedRetry(rows, attempt) {
40160
+ const failures = [];
40161
+ for (const saved of rows) {
40162
+ if (this.isUnrestorableShell(saved)) {
40163
+ this.quarantineShell(saved);
40164
+ continue;
40165
+ }
40166
+ try {
40167
+ await attempt(saved);
40168
+ } catch (err) {
40169
+ const error = err instanceof Error ? err.message : String(err);
40170
+ this.ctx.logger.warn("Failed to restore device — bounded retry scheduled", {
40171
+ tags: {
40172
+ deviceId: saved.id,
40173
+ stableId: saved.stableId
40174
+ },
40175
+ meta: {
40176
+ type: saved.type,
40177
+ attempt: 1,
40178
+ error
40179
+ }
40180
+ });
40181
+ failures.push({
40182
+ saved,
40183
+ error
40184
+ });
40185
+ }
40186
+ }
40187
+ if (failures.length > 0) this.scheduleRestoreRetries(failures, attempt);
40046
40188
  }
40047
40189
  /**
40048
40190
  * Tear down and reconstruct ONE device from its persisted rows — the
@@ -43352,6 +43494,12 @@ Object.freeze({
43352
43494
  addonId: null,
43353
43495
  access: "view"
43354
43496
  },
43497
+ "deviceManager.getWrapperActivation": {
43498
+ capName: "device-manager",
43499
+ capScope: "system",
43500
+ addonId: null,
43501
+ access: "view"
43502
+ },
43355
43503
  "deviceManager.listAll": {
43356
43504
  capName: "device-manager",
43357
43505
  capScope: "system",
@@ -49161,6 +49309,11 @@ Object.freeze({
49161
49309
  form: "single",
49162
49310
  optional: false
49163
49311
  }],
49312
+ "deviceManager.getWrapperActivation": [{
49313
+ name: "deviceId",
49314
+ form: "single",
49315
+ optional: false
49316
+ }],
49164
49317
  "deviceManager.listAll": [{
49165
49318
  name: "deviceIds",
49166
49319
  form: "array",
@@ -5369,7 +5369,7 @@ var ZodIssueCode = {
5369
5369
  var ZodFirstPartyTypeKind;
5370
5370
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5371
5371
  //#endregion
5372
- //#region ../types/dist/sleep-jkRQgtSc.mjs
5372
+ //#region ../types/dist/sleep-DlrgY4Qb.mjs
5373
5373
  /**
5374
5374
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5375
5375
  * window to float samples (D455).
@@ -6984,6 +6984,10 @@ function toNodeId(idOrRunnerId) {
6984
6984
  const head = idOrRunnerId.split("/")[0];
6985
6985
  return head === void 0 || head.length === 0 ? idOrRunnerId : head;
6986
6986
  }
6987
+ object({
6988
+ field: string().min(1),
6989
+ on: literal("advance")
6990
+ });
6987
6991
  /**
6988
6992
  * Output schema shared by the contribution + live methods.
6989
6993
  *
@@ -7467,7 +7471,45 @@ function errMsg(err) {
7467
7471
  return String(err);
7468
7472
  }
7469
7473
  //#endregion
7470
- //#region ../types/dist/composition-CPFIlFfw.mjs
7474
+ //#region ../types/dist/composition-BK-GTFYb.mjs
7475
+ /**
7476
+ * LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
7477
+ * valutatore puro del suo FRONTE.
7478
+ *
7479
+ * Viveva dentro il builtin virtual-doorbell
7480
+ * (`@camstack/system` — `builtins/doorbell/trigger-engine.ts`) e i suoi
7481
+ * predicati erano privati al modulo. Il recorder ne ha bisogno per il trigger
7482
+ * `RecordingTriggers.sensorDeviceIds`: copiarla avrebbe creato la SECONDA
7483
+ * tabella, che diverge alla prima cap aggiunta e il cui sintomo — "il sensore
7484
+ * fa suonare il campanello ma non registra" — è esattamente D62. Quindi si
7485
+ * SPOSTA qui e il doorbell la ri-esporta.
7486
+ *
7487
+ * ⚠ NON è `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), e le
7488
+ * due non vanno unificate: quella risponde a "qual è la PAROLA di stato per una
7489
+ * regola" (e include `presence`, `cover`, `alarm-panel`), questa a "qual è il
7490
+ * booleano il cui FRONTE conta". Vocabolari deliberatamente diversi.
7491
+ */
7492
+ /**
7493
+ * Known binary / switch source caps → the boolean slice field whose
7494
+ * false→true rise counts as ACTIVE. Every entry is "fire on active".
7495
+ * Sensors whose "active" reading is not a plain boolean (presence's string
7496
+ * state, connectivity's connected flag) are deliberately excluded — a
7497
+ * reconnect is not a doorbell press, and it is not a recording either.
7498
+ */
7499
+ var SOURCE_CAP_ACTIVE_FIELD = {
7500
+ contact: "entryOpen",
7501
+ binary: "on",
7502
+ switch: "on",
7503
+ motion: "detected",
7504
+ flood: "flooded",
7505
+ gas: "detected",
7506
+ smoke: "detected",
7507
+ "carbon-monoxide": "detected",
7508
+ vibration: "detected",
7509
+ tamper: "tampered"
7510
+ };
7511
+ /** Cap names whose presence in a device's bindings qualify it as a source. */
7512
+ var SOURCE_CAPS = Object.keys(SOURCE_CAP_ACTIVE_FIELD);
7471
7513
  /**
7472
7514
  * Error types for the safe expression engine. Two distinct classes so callers
7473
7515
  * can tell a compile-time (grammar) failure from a runtime (evaluation)
@@ -7665,6 +7707,20 @@ var table = {
7665
7707
  const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
7666
7708
  return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
7667
7709
  }
7710
+ },
7711
+ rose: {
7712
+ minArgs: 2,
7713
+ maxArgs: 3,
7714
+ apply: () => {
7715
+ throw new ExpressionEvalError("rose: stateful evaluation context is required");
7716
+ }
7717
+ },
7718
+ count: {
7719
+ minArgs: 1,
7720
+ maxArgs: 1,
7721
+ apply: () => {
7722
+ throw new ExpressionEvalError("count: stateful evaluation context is required");
7723
+ }
7668
7724
  }
7669
7725
  };
7670
7726
  Object.freeze(Object.assign(Object.create(null), table));
@@ -8179,6 +8235,7 @@ var CompositionItemEntrySchema = object({
8179
8235
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
8180
8236
  kind: literal("fields"),
8181
8237
  cap: string().min(1),
8238
+ enabled: boolean().optional(),
8182
8239
  fields: record(string().min(1), CompositionFieldSourceSchema).refine((fields) => Object.keys(fields).length <= 32, { message: `at most 32 fields per capability` }),
8183
8240
  /**
8184
8241
  * Items of the cap's item array, keyed by item key (D663). Naming `items`
@@ -8190,6 +8247,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
8190
8247
  }), object({
8191
8248
  kind: literal("passthrough"),
8192
8249
  cap: string().min(1),
8250
+ enabled: boolean().optional(),
8193
8251
  source: CompositionSourceRefSchema
8194
8252
  })]);
8195
8253
  var CompositionSchema = object({
@@ -13137,6 +13195,15 @@ var cameraStreamsCapability = {
13137
13195
  */
13138
13196
  durability: "session"
13139
13197
  };
13198
+ EventCategory.DoorbellOnPressed;
13199
+ var CompositionEventPlanSchema = object({
13200
+ featureIndex: number().int().nonnegative(),
13201
+ cap: string(),
13202
+ eventName: string(),
13203
+ category: _enum(EventCategory),
13204
+ stateField: string(),
13205
+ fieldKey: string()
13206
+ });
13140
13207
  /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
13141
13208
  var CompositionProblemSchema = object({
13142
13209
  code: _enum([
@@ -13186,12 +13253,14 @@ var CompositionUnavailableModeSchema = _enum([
13186
13253
  ]);
13187
13254
  /**
13188
13255
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
13189
- * field of its cap changes value · `constant` reads nothing, computed once.
13256
+ * field of its cap changes value · `constant` reads nothing, computed once ·
13257
+ * `accumulator` advances from the target's seed/current output.
13190
13258
  */
13191
13259
  var CompositionFieldRoleSchema = _enum([
13192
13260
  "derived",
13193
13261
  "clock",
13194
- "constant"
13262
+ "constant",
13263
+ "accumulator"
13195
13264
  ]);
13196
13265
  /** The item a planned field belongs to; its `path` is then the dotted path INSIDE the item. */
13197
13266
  var CompositionFieldItemSchema = object({
@@ -13227,6 +13296,7 @@ var CompositionFeatureModeSchema = _enum([
13227
13296
  "replace"
13228
13297
  ]);
13229
13298
  var CompositionFeaturePlanSchema = object({
13299
+ /** Index in the stored feature array; disabled features are omitted rather than re-indexed. */
13230
13300
  featureIndex: number().int().nonnegative(),
13231
13301
  cap: string(),
13232
13302
  mode: CompositionFeatureModeSchema
@@ -13235,7 +13305,8 @@ var CompositionValidationSchema = object({
13235
13305
  ok: boolean(),
13236
13306
  problems: array(CompositionProblemSchema),
13237
13307
  fields: array(CompositionFieldPlanSchema),
13238
- features: array(CompositionFeaturePlanSchema)
13308
+ features: array(CompositionFeaturePlanSchema),
13309
+ events: array(CompositionEventPlanSchema)
13239
13310
  });
13240
13311
  /**
13241
13312
  * Mirrors `CoreBlockStatus`, pinned by `core-blocks-source.spec.ts`. `degraded`:
@@ -15252,6 +15323,34 @@ var DeviceBindingsForDeviceSchema = object({
15252
15323
  nativeAddonId: string()
15253
15324
  }))
15254
15325
  });
15326
+ /**
15327
+ * The STORED wrapper activation for one `(device, capability)` pair — the row
15328
+ * `setWrapperActive` writes, read back three-way (D675):
15329
+ *
15330
+ * - `value` — the store answered and holds an activation: the bound wrapper's
15331
+ * addon id, or `null`, the operator's explicit "off" marker (D62);
15332
+ * - `missing` — the store answered, its activation map exists, and it holds
15333
+ * nothing for this pair: the operator never activated or deactivated a
15334
+ * wrapper here;
15335
+ * - `failed` — the store could not be read, holds no activation map at all
15336
+ * (an empty or unready snapshot, D44), or holds something that is not an
15337
+ * activation. It carries NO information: a consumer must never read it as
15338
+ * `missing` (D315, D393).
15339
+ *
15340
+ * This is the persisted INPUT, not the derived `getBindings` view — no registry
15341
+ * resolution, no `defaultActive` auto-binding, no cluster native-cap folding.
15342
+ */
15343
+ var WrapperActivationReadSchema = discriminatedUnion("kind", [
15344
+ object({
15345
+ kind: literal("value"),
15346
+ wrapperAddonId: string().nullable()
15347
+ }),
15348
+ object({ kind: literal("missing") }),
15349
+ object({
15350
+ kind: literal("failed"),
15351
+ error: string()
15352
+ })
15353
+ ]);
15255
15354
  var SavedDeviceRowSchema = object({
15256
15355
  /** Numeric id reserved at allocateDeviceId time. */
15257
15356
  id: number(),
@@ -15602,7 +15701,10 @@ method(object({
15602
15701
  }), _void(), {
15603
15702
  kind: "mutation",
15604
15703
  auth: "admin"
15605
- }), method(object({ capName: string() }), array(string())), method(object({ deviceType: string() }), array(object({
15704
+ }), method(object({
15705
+ deviceId: number(),
15706
+ capName: string().min(1)
15707
+ }), WrapperActivationReadSchema), method(object({ capName: string() }), array(string())), method(object({ deviceType: string() }), array(object({
15606
15708
  capName: string(),
15607
15709
  wrappers: array(string())
15608
15710
  }))), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), object({
@@ -27250,44 +27352,6 @@ DeviceType.Camera, method(object({
27250
27352
  auth: "protected"
27251
27353
  });
27252
27354
  /**
27253
- * LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
27254
- * valutatore puro del suo FRONTE.
27255
- *
27256
- * Viveva dentro il builtin virtual-doorbell
27257
- * (`@camstack/system` — `builtins/doorbell/trigger-engine.ts`) e i suoi
27258
- * predicati erano privati al modulo. Il recorder ne ha bisogno per il trigger
27259
- * `RecordingTriggers.sensorDeviceIds`: copiarla avrebbe creato la SECONDA
27260
- * tabella, che diverge alla prima cap aggiunta e il cui sintomo — "il sensore
27261
- * fa suonare il campanello ma non registra" — è esattamente D62. Quindi si
27262
- * SPOSTA qui e il doorbell la ri-esporta.
27263
- *
27264
- * ⚠ NON è `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), e le
27265
- * due non vanno unificate: quella risponde a "qual è la PAROLA di stato per una
27266
- * regola" (e include `presence`, `cover`, `alarm-panel`), questa a "qual è il
27267
- * booleano il cui FRONTE conta". Vocabolari deliberatamente diversi.
27268
- */
27269
- /**
27270
- * Known binary / switch source caps → the boolean slice field whose
27271
- * false→true rise counts as ACTIVE. Every entry is "fire on active".
27272
- * Sensors whose "active" reading is not a plain boolean (presence's string
27273
- * state, connectivity's connected flag) are deliberately excluded — a
27274
- * reconnect is not a doorbell press, and it is not a recording either.
27275
- */
27276
- var SOURCE_CAP_ACTIVE_FIELD = {
27277
- contact: "entryOpen",
27278
- binary: "on",
27279
- switch: "on",
27280
- motion: "detected",
27281
- flood: "flooded",
27282
- gas: "detected",
27283
- smoke: "detected",
27284
- "carbon-monoxide": "detected",
27285
- vibration: "detected",
27286
- tamper: "tampered"
27287
- };
27288
- /** Cap names whose presence in a device's bindings qualify it as a source. */
27289
- var SOURCE_CAPS = Object.keys(SOURCE_CAP_ACTIVE_FIELD);
27290
- /**
27291
27355
  * Doorbell button cap. Two kinds of providers coexist behind this cap
27292
27356
  * name (same pattern as `snapshot`):
27293
27357
  *
@@ -27360,7 +27424,13 @@ var doorbellCapability = {
27360
27424
  * push (`ReolinkSimpleEvent.type === 'doorbell'`). There is no
27361
27425
  * release/duration — it's a pulse.
27362
27426
  */
27363
- onPressed: { data: DoorbellPressEventSchema } },
27427
+ onPressed: {
27428
+ data: DoorbellPressEventSchema,
27429
+ fromState: {
27430
+ field: "lastPressedAt",
27431
+ on: "advance"
27432
+ }
27433
+ } },
27364
27434
  status: {
27365
27435
  schema: DoorbellStatusSchema,
27366
27436
  kind: "push"
@@ -35247,12 +35317,21 @@ var RecordingStorageUsageSchema = object({
35247
35317
  */
35248
35318
  devicesRead: _enum(["archive", "unavailable"]).optional(),
35249
35319
  /**
35320
+ * Is the `archive` answer WHOLE (D674): the hour ledger loaded without
35321
+ * hitting its row cap, and at least one location resolved? Only then may a
35322
+ * camera ABSENT from `devices[]` be read as "no footage" — and only then is a
35323
+ * row's `oldestMs` the archive's oldest rather than the oldest of a fraction.
35324
+ * OPTIONAL: a recorder predating D674 omits it, and a consumer must read the
35325
+ * absence as `false`. A retention floor is a delete; it never guesses.
35326
+ */
35327
+ complete: boolean().optional(),
35328
+ /**
35250
35329
  * Per-physical-volume capacity. OPTIONAL, and consumers must render the rest
35251
- * of the payload without it: the hub serves this method through a BAKED
35252
- * router, so a hub whose framework train predates a change to this array
35253
- * strips it out of an answer a newer recorder happily produced. A UI that
35254
- * dereferences it unconditionally crashes the whole Storage page over a
35255
- * missing disk bar.
35330
+ * of the payload without it: a recorder that could not build it omits it. A
35331
+ * UI that dereferences it unconditionally crashes the whole Storage page over
35332
+ * a missing disk bar. (The hub's cap routers pass output through unvalidated
35333
+ * since 2026-07-11 — `cap-router-builder.ts` — so a newer recorder's fields
35334
+ * are not stripped by an older hub.)
35256
35335
  */
35257
35336
  locations: array(RecordingLocationUsageSchema).optional()
35258
35337
  });
@@ -38771,7 +38850,12 @@ Object.freeze({
38771
38850
  number: {
38772
38851
  kind: "fixed",
38773
38852
  result: ["number", "null"]
38774
- }
38853
+ },
38854
+ rose: {
38855
+ kind: "fixed",
38856
+ result: ["number", "null"]
38857
+ },
38858
+ count: NUMBER
38775
38859
  });
38776
38860
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
38777
38861
  /**
@@ -39812,6 +39896,7 @@ var BaseDeviceProvider = class extends BaseAddon {
39812
39896
  }];
39813
39897
  }
39814
39898
  async onShutdown() {
39899
+ this._restoreShuttingDown = true;
39815
39900
  this.cancelRestoreRetries();
39816
39901
  const devices = await this.ctx.kernel.devices?.getAll() ?? [];
39817
39902
  for (const device of devices) try {
@@ -39929,6 +40014,7 @@ var BaseDeviceProvider = class extends BaseAddon {
39929
40014
  };
39930
40015
  }
39931
40016
  async restoreDevices(savedDevices) {
40017
+ this._restoreShuttingDown = false;
39932
40018
  const report = await this.onRestoreDevices(savedDevices);
39933
40019
  if (savedDevices.length === 0) return;
39934
40020
  if (report && report.failedCount > 0) {
@@ -39943,8 +40029,13 @@ var BaseDeviceProvider = class extends BaseAddon {
39943
40029
  /** Retry lane width. See `device-restore-retry.ts` for why retries
39944
40030
  * never re-stampede full-width while the initial pass does (D167). */
39945
40031
  restoreRetryConcurrency = 4;
39946
- _restoreRetryScheduler = null;
40032
+ /** Every bounded retry this process started — the default pass's, and each
40033
+ * `restoreWithBoundedRetry` of a provider's own rows. Cancelled together. */
40034
+ _restoreRetrySchedulers = [];
39947
40035
  _restoreRetryCompletion = null;
40036
+ /** Set by `onShutdown` BEFORE the retries are cancelled: a restore pass still
40037
+ * in flight must not start a scheduler after the cancel ran. */
40038
+ _restoreShuttingDown = false;
39948
40039
  _permanentRestoreFailures = /* @__PURE__ */ new Map();
39949
40040
  /** Settles when the background retry rounds finish (or `null` when
39950
40041
  * nothing failed). Exposed for tests and subclass diagnostics —
@@ -40013,8 +40104,8 @@ var BaseDeviceProvider = class extends BaseAddon {
40013
40104
  });
40014
40105
  }
40015
40106
  cancelRestoreRetries() {
40016
- this._restoreRetryScheduler?.cancel();
40017
- this._restoreRetryScheduler = null;
40107
+ for (const scheduler of this._restoreRetrySchedulers) scheduler.cancel();
40108
+ this._restoreRetrySchedulers = [];
40018
40109
  }
40019
40110
  recordPermanentRestoreFailure(failure) {
40020
40111
  this._permanentRestoreFailures.set(failure.deviceId, failure);
@@ -40031,6 +40122,13 @@ var BaseDeviceProvider = class extends BaseAddon {
40031
40122
  });
40032
40123
  }
40033
40124
  scheduleRestoreRetries(failures, attempt) {
40125
+ if (this._restoreShuttingDown) {
40126
+ this.ctx.logger.info("Restore retries not scheduled — the provider is shutting down", { meta: {
40127
+ rows: failures.length,
40128
+ deviceIds: failures.map((f) => f.saved.id)
40129
+ } });
40130
+ return;
40131
+ }
40034
40132
  const scheduler = new DeviceRestoreRetryScheduler({
40035
40133
  logger: this.ctx.logger,
40036
40134
  delaysMs: this.restoreRetryDelaysMs,
@@ -40038,10 +40136,54 @@ var BaseDeviceProvider = class extends BaseAddon {
40038
40136
  attempt,
40039
40137
  onPermanentFailure: (failure) => this.recordPermanentRestoreFailure(failure)
40040
40138
  });
40041
- this._restoreRetryScheduler = scheduler;
40042
- this._restoreRetryCompletion = scheduler.run(failures).then(() => void 0).catch((err) => {
40139
+ this._restoreRetrySchedulers = [...this._restoreRetrySchedulers, scheduler];
40140
+ const run = scheduler.run(failures).then(() => void 0).catch((err) => {
40043
40141
  this.ctx.logger.error("Restore retry scheduler crashed", { meta: { error: err instanceof Error ? err.message : String(err) } });
40044
40142
  });
40143
+ const prior = this._restoreRetryCompletion;
40144
+ this._restoreRetryCompletion = prior === null ? run : Promise.all([prior, run]).then(() => void 0);
40145
+ }
40146
+ /**
40147
+ * Restore rows the default pass does not handle (a provider's own restore
40148
+ * loop) under the SAME bounded retry (D347): one attempt each, in order; a
40149
+ * row that throws is logged with `tags.deviceId` and retried in the
40150
+ * background rounds, then marked permanently failed like any other.
40151
+ *
40152
+ * A restore is a device build, and a build whose runtime-state load did not
40153
+ * answer is REFUSED rather than started on cold defaults (D677) — so a
40154
+ * transient hub read must cost a retry, never the device until the next
40155
+ * runner restart. An ANSWERED `{}` is the device's true state and never
40156
+ * refuses.
40157
+ */
40158
+ async restoreWithBoundedRetry(rows, attempt) {
40159
+ const failures = [];
40160
+ for (const saved of rows) {
40161
+ if (this.isUnrestorableShell(saved)) {
40162
+ this.quarantineShell(saved);
40163
+ continue;
40164
+ }
40165
+ try {
40166
+ await attempt(saved);
40167
+ } catch (err) {
40168
+ const error = err instanceof Error ? err.message : String(err);
40169
+ this.ctx.logger.warn("Failed to restore device — bounded retry scheduled", {
40170
+ tags: {
40171
+ deviceId: saved.id,
40172
+ stableId: saved.stableId
40173
+ },
40174
+ meta: {
40175
+ type: saved.type,
40176
+ attempt: 1,
40177
+ error
40178
+ }
40179
+ });
40180
+ failures.push({
40181
+ saved,
40182
+ error
40183
+ });
40184
+ }
40185
+ }
40186
+ if (failures.length > 0) this.scheduleRestoreRetries(failures, attempt);
40045
40187
  }
40046
40188
  /**
40047
40189
  * Tear down and reconstruct ONE device from its persisted rows — the
@@ -43351,6 +43493,12 @@ Object.freeze({
43351
43493
  addonId: null,
43352
43494
  access: "view"
43353
43495
  },
43496
+ "deviceManager.getWrapperActivation": {
43497
+ capName: "device-manager",
43498
+ capScope: "system",
43499
+ addonId: null,
43500
+ access: "view"
43501
+ },
43354
43502
  "deviceManager.listAll": {
43355
43503
  capName: "device-manager",
43356
43504
  capScope: "system",
@@ -49160,6 +49308,11 @@ Object.freeze({
49160
49308
  form: "single",
49161
49309
  optional: false
49162
49310
  }],
49311
+ "deviceManager.getWrapperActivation": [{
49312
+ name: "deviceId",
49313
+ form: "single",
49314
+ optional: false
49315
+ }],
49163
49316
  "deviceManager.listAll": [{
49164
49317
  name: "deviceIds",
49165
49318
  form: "array",
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  //#endregion
6
- const require_dist = require("../dist-BTJhdlL4.js");
6
+ const require_dist = require("../dist-CslhAXSs.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  //#region src/ha-export/topics.ts
9
9
  /**
@@ -1,4 +1,4 @@
1
- import { C as buildAddonRouteProvider, F as deviceExportCapability, Nt as EventCategory, Q as oauthIntegrationCapability, a as COCO_TO_MACRO, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, kt as string, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema, xt as nodePin, yt as BaseAddon } from "../dist-D2jDnKLv.mjs";
1
+ import { C as buildAddonRouteProvider, F as deviceExportCapability, Nt as EventCategory, Q as oauthIntegrationCapability, a as COCO_TO_MACRO, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, kt as string, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema, xt as nodePin, yt as BaseAddon } from "../dist-DVwhiKWh.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  //#region src/ha-export/topics.ts
4
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.146",
3
+ "version": "1.2.147",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",