@camstack/types 1.2.143 → 1.2.144

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.
@@ -1365,6 +1365,9 @@ export declare const deviceManagerCapability: {
1365
1365
  uniqueId: z.ZodOptional<z.ZodString>;
1366
1366
  raw: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1367
1367
  }, z.core.$strip>>;
1368
+ alreadyOnboarded: z.ZodOptional<z.ZodBoolean>;
1369
+ onboardedDeviceId: z.ZodOptional<z.ZodNumber>;
1370
+ onboardedName: z.ZodOptional<z.ZodString>;
1368
1371
  }, z.core.$strip>>, "mutation">;
1369
1372
  /** Adopt a discovered device via the device-provider capability. */
1370
1373
  readonly adoptDevice: import("./capability-definition.js").CapabilityMethodSchema<z.ZodObject<{
@@ -1380,6 +1383,9 @@ export declare const deviceManagerCapability: {
1380
1383
  uniqueId: z.ZodOptional<z.ZodString>;
1381
1384
  raw: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1382
1385
  }, z.core.$strip>>;
1386
+ alreadyOnboarded: z.ZodOptional<z.ZodBoolean>;
1387
+ onboardedDeviceId: z.ZodOptional<z.ZodNumber>;
1388
+ onboardedName: z.ZodOptional<z.ZodString>;
1383
1389
  }, z.core.$strip>;
1384
1390
  integrationId: z.ZodOptional<z.ZodString>;
1385
1391
  }, z.core.$strip>, z.ZodObject<{
@@ -1622,6 +1628,9 @@ export declare const deviceManagerCapability: {
1622
1628
  uniqueId: z.ZodOptional<z.ZodString>;
1623
1629
  raw: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1624
1630
  }, z.core.$strip>>;
1631
+ alreadyOnboarded: z.ZodOptional<z.ZodBoolean>;
1632
+ onboardedDeviceId: z.ZodOptional<z.ZodNumber>;
1633
+ onboardedName: z.ZodOptional<z.ZodString>;
1625
1634
  }, z.core.$strip>>>;
1626
1635
  error: z.ZodNullable<z.ZodString>;
1627
1636
  }, z.core.$strip>>>;
@@ -1643,6 +1652,9 @@ export declare const deviceManagerCapability: {
1643
1652
  uniqueId: z.ZodOptional<z.ZodString>;
1644
1653
  raw: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1645
1654
  }, z.core.$strip>>;
1655
+ alreadyOnboarded: z.ZodOptional<z.ZodBoolean>;
1656
+ onboardedDeviceId: z.ZodOptional<z.ZodNumber>;
1657
+ onboardedName: z.ZodOptional<z.ZodString>;
1646
1658
  }, z.core.$strip>>>;
1647
1659
  }, z.core.$strip>, "mutation">;
1648
1660
  /** The device type a provider creates via manual add (Camera/Container/Hub),
@@ -28,6 +28,9 @@ declare const DiscoveryCandidateSchema: z.ZodObject<{
28
28
  uniqueId: z.ZodOptional<z.ZodString>;
29
29
  raw: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
30
30
  }, z.core.$strip>>;
31
+ alreadyOnboarded: z.ZodOptional<z.ZodBoolean>;
32
+ onboardedDeviceId: z.ZodOptional<z.ZodNumber>;
33
+ onboardedName: z.ZodOptional<z.ZodString>;
31
34
  }, z.core.$strip>;
32
35
  /**
33
36
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -132,6 +135,9 @@ export declare const deviceProviderCapability: {
132
135
  uniqueId: z.ZodOptional<z.ZodString>;
133
136
  raw: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
134
137
  }, z.core.$strip>>;
138
+ alreadyOnboarded: z.ZodOptional<z.ZodBoolean>;
139
+ onboardedDeviceId: z.ZodOptional<z.ZodNumber>;
140
+ onboardedName: z.ZodOptional<z.ZodString>;
135
141
  }, z.core.$strip>>, "mutation">;
136
142
  /**
137
143
  * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
@@ -161,6 +167,9 @@ export declare const deviceProviderCapability: {
161
167
  uniqueId: z.ZodOptional<z.ZodString>;
162
168
  raw: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
163
169
  }, z.core.$strip>>;
170
+ alreadyOnboarded: z.ZodOptional<z.ZodBoolean>;
171
+ onboardedDeviceId: z.ZodOptional<z.ZodNumber>;
172
+ onboardedName: z.ZodOptional<z.ZodString>;
164
173
  }, z.core.$strip>;
165
174
  }, z.core.$strip>, z.ZodObject<{
166
175
  id: z.ZodNumber;
@@ -17,6 +17,16 @@ export interface DiscoveryCandidate {
17
17
  * knows the upstream identity ahead of adoption (HA: entity_id +
18
18
  * unit + device_class from `GET /api/states`). */
19
19
  readonly sourceInfo?: SourceInfo;
20
+ /** Set when the provider recognises this candidate as a device it ALREADY
21
+ * owns. A scan usually cannot reproduce the identity a device was onboarded
22
+ * under, so a stableId comparison never matches and an owned device looks
23
+ * addable — re-adopting it overwrites its config with scan-derived values.
24
+ * Absent means "not recognised", NOT "known to be new". */
25
+ readonly alreadyOnboarded?: boolean;
26
+ /** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
27
+ readonly onboardedDeviceId?: number;
28
+ /** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
29
+ readonly onboardedName?: string;
20
30
  }
21
31
  export interface DeviceSummary {
22
32
  readonly id: number;
@@ -263,6 +273,22 @@ export declare abstract class BaseDeviceProvider<TConfig extends object = Record
263
273
  * failed — logged at ERROR with `tags.deviceId` and surfaced via
264
274
  * `getStatus().error`.
265
275
  */
276
+ /**
277
+ * Repair a row's PERSISTED config blob immediately before it is restored.
278
+ * Default: no-op — most providers have nothing to heal.
279
+ *
280
+ * This exists because a restored device self-hydrates from the DB: `create()`
281
+ * passes `{}` and `BaseDevice` parses the stored blob against the device
282
+ * schema. A blob that lost a REQUIRED field therefore fails restore forever,
283
+ * and no later pass revisits it — a hub-adopted Reolink camera whose blob had
284
+ * been emptied failed all four bounded attempts against fields
285
+ * (`host`, `password`) it inherits from its parent and never dials itself.
286
+ *
287
+ * Implementations get every saved row, so a child can read its parent's blob.
288
+ * A heal that throws is treated like any other restore failure: retried under
289
+ * the bound, then reported — never swallowed.
290
+ */
291
+ protected healSavedConfig(_saved: SavedDevice, _allSaved: readonly SavedDevice[]): Promise<void>;
266
292
  protected onRestoreDevices(savedDevices: readonly SavedDevice[]): Promise<DeviceRestoreReport | void>;
267
293
  /** Convert an IDevice to the flat DeviceSummary for the cap router. */
268
294
  protected toSummary(device: IDevice): DeviceSummary;
package/dist/index.js CHANGED
@@ -10061,7 +10061,26 @@ var DiscoveryCandidateSchema = zod.z.object({
10061
10061
  * identity ahead of adoption. Rendering metadata (unit, precision)
10062
10062
  * flows live through the cap STATUS SLICE after adoption.
10063
10063
  */
10064
- sourceInfo: SourceInfoSchema.optional()
10064
+ sourceInfo: SourceInfoSchema.optional(),
10065
+ /**
10066
+ * Set when this candidate is a device the provider ALREADY owns.
10067
+ *
10068
+ * A scan cannot generally produce the identity a device was onboarded under
10069
+ * (Reolink keys on `mac-<mac>`, learned at adopt time), so a stableId
10070
+ * comparison never matches and an owned device looks addable. Re-adopting one
10071
+ * overwrites its config with scan-derived values — that is how a Home Hub's
10072
+ * Baichuan port was overwritten with its ONVIF port, taking the hub and its
10073
+ * three child cameras offline for four hours.
10074
+ *
10075
+ * A provider that can recognise its own devices says so here. Absent means
10076
+ * "not recognised", which is not the same as "known to be new" — a provider
10077
+ * that cannot tell simply never sets it.
10078
+ */
10079
+ alreadyOnboarded: zod.z.boolean().optional(),
10080
+ /** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
10081
+ onboardedDeviceId: zod.z.number().optional(),
10082
+ /** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
10083
+ onboardedName: zod.z.string().optional()
10065
10084
  });
10066
10085
  /**
10067
10086
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -36904,6 +36923,22 @@ var BaseDeviceProvider = class extends require_sleep.BaseAddon {
36904
36923
  * failed — logged at ERROR with `tags.deviceId` and surfaced via
36905
36924
  * `getStatus().error`.
36906
36925
  */
36926
+ /**
36927
+ * Repair a row's PERSISTED config blob immediately before it is restored.
36928
+ * Default: no-op — most providers have nothing to heal.
36929
+ *
36930
+ * This exists because a restored device self-hydrates from the DB: `create()`
36931
+ * passes `{}` and `BaseDevice` parses the stored blob against the device
36932
+ * schema. A blob that lost a REQUIRED field therefore fails restore forever,
36933
+ * and no later pass revisits it — a hub-adopted Reolink camera whose blob had
36934
+ * been emptied failed all four bounded attempts against fields
36935
+ * (`host`, `password`) it inherits from its parent and never dials itself.
36936
+ *
36937
+ * Implementations get every saved row, so a child can read its parent's blob.
36938
+ * A heal that throws is treated like any other restore failure: retried under
36939
+ * the bound, then reported — never swallowed.
36940
+ */
36941
+ async healSavedConfig(_saved, _allSaved) {}
36907
36942
  async onRestoreDevices(savedDevices) {
36908
36943
  const restored = /* @__PURE__ */ new Set();
36909
36944
  const failures = [];
@@ -36912,6 +36947,7 @@ var BaseDeviceProvider = class extends require_sleep.BaseAddon {
36912
36947
  const Class = this.deviceClasses[saved.type];
36913
36948
  if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
36914
36949
  if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
36950
+ await this.healSavedConfig(saved, savedDevices);
36915
36951
  await this.ctx.kernel.devices.create(saved.stableId, Class, {}, saved.parentDeviceId);
36916
36952
  restored.add(saved.id);
36917
36953
  };
package/dist/index.mjs CHANGED
@@ -10060,7 +10060,26 @@ var DiscoveryCandidateSchema = z.object({
10060
10060
  * identity ahead of adoption. Rendering metadata (unit, precision)
10061
10061
  * flows live through the cap STATUS SLICE after adoption.
10062
10062
  */
10063
- sourceInfo: SourceInfoSchema.optional()
10063
+ sourceInfo: SourceInfoSchema.optional(),
10064
+ /**
10065
+ * Set when this candidate is a device the provider ALREADY owns.
10066
+ *
10067
+ * A scan cannot generally produce the identity a device was onboarded under
10068
+ * (Reolink keys on `mac-<mac>`, learned at adopt time), so a stableId
10069
+ * comparison never matches and an owned device looks addable. Re-adopting one
10070
+ * overwrites its config with scan-derived values — that is how a Home Hub's
10071
+ * Baichuan port was overwritten with its ONVIF port, taking the hub and its
10072
+ * three child cameras offline for four hours.
10073
+ *
10074
+ * A provider that can recognise its own devices says so here. Absent means
10075
+ * "not recognised", which is not the same as "known to be new" — a provider
10076
+ * that cannot tell simply never sets it.
10077
+ */
10078
+ alreadyOnboarded: z.boolean().optional(),
10079
+ /** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
10080
+ onboardedDeviceId: z.number().optional(),
10081
+ /** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
10082
+ onboardedName: z.string().optional()
10064
10083
  });
10065
10084
  /**
10066
10085
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -36896,6 +36915,22 @@ var BaseDeviceProvider = class extends BaseAddon {
36896
36915
  * failed — logged at ERROR with `tags.deviceId` and surfaced via
36897
36916
  * `getStatus().error`.
36898
36917
  */
36918
+ /**
36919
+ * Repair a row's PERSISTED config blob immediately before it is restored.
36920
+ * Default: no-op — most providers have nothing to heal.
36921
+ *
36922
+ * This exists because a restored device self-hydrates from the DB: `create()`
36923
+ * passes `{}` and `BaseDevice` parses the stored blob against the device
36924
+ * schema. A blob that lost a REQUIRED field therefore fails restore forever,
36925
+ * and no later pass revisits it — a hub-adopted Reolink camera whose blob had
36926
+ * been emptied failed all four bounded attempts against fields
36927
+ * (`host`, `password`) it inherits from its parent and never dials itself.
36928
+ *
36929
+ * Implementations get every saved row, so a child can read its parent's blob.
36930
+ * A heal that throws is treated like any other restore failure: retried under
36931
+ * the bound, then reported — never swallowed.
36932
+ */
36933
+ async healSavedConfig(_saved, _allSaved) {}
36899
36934
  async onRestoreDevices(savedDevices) {
36900
36935
  const restored = /* @__PURE__ */ new Set();
36901
36936
  const failures = [];
@@ -36904,6 +36939,7 @@ var BaseDeviceProvider = class extends BaseAddon {
36904
36939
  const Class = this.deviceClasses[saved.type];
36905
36940
  if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
36906
36941
  if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
36942
+ await this.healSavedConfig(saved, savedDevices);
36907
36943
  await this.ctx.kernel.devices.create(saved.stableId, Class, {}, saved.parentDeviceId);
36908
36944
  restored.add(saved.id);
36909
36945
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/types",
3
- "version": "1.2.143",
3
+ "version": "1.2.144",
4
4
  "description": "Shared types, interfaces, and model catalogs for the CamStack detection ecosystem",
5
5
  "keywords": [
6
6
  "camstack",