@camstack/addon-provider-wyze 0.1.7 → 0.1.9

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.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
2
  import { join } from "node:path";
3
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { createCipheriv, createDecipheriv, createHash, createHmac, createPublicKey, diffieHellman, generateKeyPairSync, randomBytes } from "crypto";
5
5
  import * as dgram from "dgram";
6
6
  import { EventEmitter } from "events";
@@ -4638,7 +4638,7 @@ function _instanceof(cls, params = {}) {
4638
4638
  return inst;
4639
4639
  }
4640
4640
  //#endregion
4641
- //#region ../types/dist/sleep-B3AOslwX.mjs
4641
+ //#region ../types/dist/sleep-C2M2zF7x.mjs
4642
4642
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4643
4643
  EventCategory["SystemBoot"] = "system.boot";
4644
4644
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -6206,6 +6206,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
6206
6206
  DeviceType["Switch"] = "switch";
6207
6207
  DeviceType["Sensor"] = "sensor";
6208
6208
  DeviceType["Thermostat"] = "thermostat";
6209
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
6210
+ * `climate-control` cap surface with `Thermostat` but renders a
6211
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
6212
+ * independent vertical/horizontal swing). Sources: native Gree, and
6213
+ * reusable by other AC integrations. */
6214
+ DeviceType["Climate"] = "climate";
6209
6215
  DeviceType["Button"] = "button";
6210
6216
  /** Generic stateless event emitter — carries a device's EXACT declared
6211
6217
  * event vocabulary verbatim (no normalization). Installed with the
@@ -6623,6 +6629,18 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
6623
6629
  input: unknown()
6624
6630
  }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
6625
6631
  //#endregion
6632
+ //#region ../types/dist/err-msg-IQTHeDzc.mjs
6633
+ /**
6634
+ import { errMsg } from '@camstack/types'
6635
+ * Extract a human-readable message from an unknown error value.
6636
+ * Replaces the ubiquitous `errMsg(err)` pattern.
6637
+ */
6638
+ function errMsg(err) {
6639
+ if (err instanceof Error) return err.message;
6640
+ if (typeof err === "string") return err;
6641
+ return String(err);
6642
+ }
6643
+ //#endregion
6626
6644
  //#region ../types/dist/index.mjs
6627
6645
  /**
6628
6646
  * Deep wiring healthcheck — snapshot of active reachability probes across
@@ -8989,7 +9007,7 @@ var climateControlCapability = {
8989
9007
  scope: "device",
8990
9008
  deviceNative: true,
8991
9009
  mode: "singleton",
8992
- deviceTypes: [DeviceType.Thermostat],
9010
+ deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
8993
9011
  methods: {
8994
9012
  setMode: method(object({
8995
9013
  deviceId: number().int().nonnegative(),
@@ -13588,10 +13606,30 @@ var deviceProviderCapability = {
13588
13606
  type: string()
13589
13607
  }))),
13590
13608
  supportsDiscovery: method(object({}), boolean()),
13591
- discoverDevices: method(object({}), array(DiscoveryCandidateSchema), {
13609
+ /**
13610
+ * Run a network scan. `params` carries optional provider-specific scan
13611
+ * inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
13612
+ * shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
13613
+ * (provider uses its local-network default).
13614
+ */
13615
+ discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
13592
13616
  kind: "mutation",
13593
13617
  auth: "admin"
13594
13618
  }),
13619
+ /**
13620
+ * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
13621
+ * provider accepts (e.g. Gree's broadcast address for a different subnet).
13622
+ * `null` when the provider takes no extra scan params — the generic
13623
+ * aggregated scan never renders this; the per-integration scan does.
13624
+ */
13625
+ getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
13626
+ /**
13627
+ * The DeviceType this provider creates via manual add (Camera for
13628
+ * Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
13629
+ * provider does not support manual creation. Lets the Add-Device dialog
13630
+ * pick the right type instead of assuming Camera.
13631
+ */
13632
+ getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
13595
13633
  adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
13596
13634
  kind: "mutation",
13597
13635
  auth: "admin"
@@ -13715,9 +13753,23 @@ var BaseDeviceProvider = class extends BaseAddon {
13715
13753
  async supportsDiscovery() {
13716
13754
  return false;
13717
13755
  }
13718
- async discoverDevices() {
13756
+ async discoverDevices(_input) {
13719
13757
  return [];
13720
13758
  }
13759
+ /** Extra per-scan input form (e.g. a broadcast address for another subnet).
13760
+ * Null = no extra params. Override in providers that support scoped scans. */
13761
+ async getDiscoveryParamsSchema() {
13762
+ return null;
13763
+ }
13764
+ /**
13765
+ * The DeviceType this provider creates via manual add — derived from the
13766
+ * `deviceClasses` map (first registered type). `null` when manual creation is
13767
+ * unsupported. Lets the Add-Device dialog pick the right type per provider.
13768
+ */
13769
+ async getManualCreationType() {
13770
+ if (!await this.supportsManualCreation()) return { deviceType: null };
13771
+ return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
13772
+ }
13721
13773
  async adoptDiscoveredDevice(_input) {
13722
13774
  throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
13723
13775
  }
@@ -15027,19 +15079,36 @@ var ResyncResultSchema = object({
15027
15079
  * provider re-derived the device. 0/absent for a normal incremental re-sync. */
15028
15080
  removedChildren: number().int().nonnegative().optional()
15029
15081
  });
15030
- method(object({ integrationId: string() }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema, ListCandidatesOutputSchema, { auth: "admin" }), method(GetCandidateInputSchema, DiscoveredChildDeviceSchema.nullable(), { auth: "admin" }), method(object({ integrationId: string() }), AdoptionStatusSchema, {
15031
- kind: "mutation",
15032
- auth: "admin"
15033
- }), method(AdoptInputSchema, AdoptResultSchema, {
15034
- kind: "mutation",
15035
- auth: "admin"
15036
- }), method(ReleaseInputSchema, _void(), {
15037
- kind: "mutation",
15038
- auth: "admin"
15039
- }), method(ResyncInputSchema, ResyncResultSchema, {
15040
- kind: "mutation",
15041
- auth: "admin"
15042
- });
15082
+ var deviceAdoptionCapability = {
15083
+ name: "device-adoption",
15084
+ scope: "system",
15085
+ mode: "singleton",
15086
+ status: {
15087
+ schema: AdoptionStatusSchema,
15088
+ kind: "poll"
15089
+ },
15090
+ methods: {
15091
+ listCandidateFilters: method(object({ integrationId: string() }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }),
15092
+ listCandidates: method(ListCandidatesInputSchema, ListCandidatesOutputSchema, { auth: "admin" }),
15093
+ getCandidate: method(GetCandidateInputSchema, DiscoveredChildDeviceSchema.nullable(), { auth: "admin" }),
15094
+ refresh: method(object({ integrationId: string() }), AdoptionStatusSchema, {
15095
+ kind: "mutation",
15096
+ auth: "admin"
15097
+ }),
15098
+ adopt: method(AdoptInputSchema, AdoptResultSchema, {
15099
+ kind: "mutation",
15100
+ auth: "admin"
15101
+ }),
15102
+ release: method(ReleaseInputSchema, _void(), {
15103
+ kind: "mutation",
15104
+ auth: "admin"
15105
+ }),
15106
+ resync: method(ResyncInputSchema, ResyncResultSchema, {
15107
+ kind: "mutation",
15108
+ auth: "admin"
15109
+ })
15110
+ }
15111
+ };
15043
15112
  /**
15044
15113
  * `device-export` — collection cap for addons that export camstack
15045
15114
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
@@ -15562,7 +15631,10 @@ method(object({
15562
15631
  }), FieldProbeResultSchema, {
15563
15632
  kind: "mutation",
15564
15633
  auth: "admin"
15565
- }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
15634
+ }), method(object({
15635
+ addonId: string(),
15636
+ integrationId: string()
15637
+ }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
15566
15638
  addonId: string(),
15567
15639
  integrationId: string()
15568
15640
  }), AdoptionStatusSchema, {
@@ -15577,7 +15649,24 @@ method(object({
15577
15649
  }), method(ResyncInputSchema, ResyncResultSchema, {
15578
15650
  kind: "mutation",
15579
15651
  auth: "admin"
15652
+ }), method(object({}), object({ providers: array(object({
15653
+ addonId: string(),
15654
+ label: string()
15655
+ })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
15656
+ addonId: string(),
15657
+ label: string(),
15658
+ candidates: array(DiscoveryCandidateSchema).readonly(),
15659
+ error: string().nullable()
15660
+ })).readonly() }), {
15661
+ kind: "mutation",
15662
+ auth: "admin"
15580
15663
  }), method(object({
15664
+ addonId: string(),
15665
+ params: record(string(), unknown()).optional()
15666
+ }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
15667
+ kind: "mutation",
15668
+ auth: "admin"
15669
+ }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
15581
15670
  deviceId: number(),
15582
15671
  key: string(),
15583
15672
  value: unknown()
@@ -18272,6 +18361,17 @@ var AvailableIntegrationTypeSchema = object({
18272
18361
  iconUrl: string().nullable(),
18273
18362
  color: string(),
18274
18363
  instanceMode: string(),
18364
+ /**
18365
+ * Integration wizard `mode` (LOCKED MODEL): `standalone` (create
18366
+ * immediately then add devices, no config step/button), `account` (config
18367
+ * step), or `broker` (broker step). Derived server-side by
18368
+ * `getAvailableTypes` when the addon manifest omits an explicit `mode`.
18369
+ */
18370
+ mode: _enum([
18371
+ "standalone",
18372
+ "account",
18373
+ "broker"
18374
+ ]),
18275
18375
  discoveryMode: string(),
18276
18376
  /**
18277
18377
  * Which integration-marker cap the addon declared, so the wizard can
@@ -20796,6 +20896,12 @@ Object.freeze({
20796
20896
  addonId: null,
20797
20897
  access: "create"
20798
20898
  },
20899
+ "deviceManager.adoptionListCandidateFilters": {
20900
+ capName: "device-manager",
20901
+ capScope: "system",
20902
+ addonId: null,
20903
+ access: "view"
20904
+ },
20799
20905
  "deviceManager.adoptionListCandidates": {
20800
20906
  capName: "device-manager",
20801
20907
  capScope: "system",
@@ -20844,12 +20950,30 @@ Object.freeze({
20844
20950
  addonId: null,
20845
20951
  access: "create"
20846
20952
  },
20953
+ "deviceManager.discoverAllProviders": {
20954
+ capName: "device-manager",
20955
+ capScope: "system",
20956
+ addonId: null,
20957
+ access: "create"
20958
+ },
20847
20959
  "deviceManager.discoverDevices": {
20848
20960
  capName: "device-manager",
20849
20961
  capScope: "system",
20850
20962
  addonId: null,
20851
20963
  access: "create"
20852
20964
  },
20965
+ "deviceManager.discoverProvider": {
20966
+ capName: "device-manager",
20967
+ capScope: "system",
20968
+ addonId: null,
20969
+ access: "create"
20970
+ },
20971
+ "deviceManager.discoveryProviders": {
20972
+ capName: "device-manager",
20973
+ capScope: "system",
20974
+ addonId: null,
20975
+ access: "view"
20976
+ },
20853
20977
  "deviceManager.enable": {
20854
20978
  capName: "device-manager",
20855
20979
  capScope: "system",
@@ -21000,6 +21124,18 @@ Object.freeze({
21000
21124
  addonId: null,
21001
21125
  access: "create"
21002
21126
  },
21127
+ "deviceManager.providerCreationType": {
21128
+ capName: "device-manager",
21129
+ capScope: "system",
21130
+ addonId: null,
21131
+ access: "view"
21132
+ },
21133
+ "deviceManager.providerDiscoveryParamsSchema": {
21134
+ capName: "device-manager",
21135
+ capScope: "system",
21136
+ addonId: null,
21137
+ access: "view"
21138
+ },
21003
21139
  "deviceManager.registerDevice": {
21004
21140
  capName: "device-manager",
21005
21141
  capScope: "system",
@@ -21216,6 +21352,18 @@ Object.freeze({
21216
21352
  addonId: null,
21217
21353
  access: "view"
21218
21354
  },
21355
+ "deviceProvider.getDiscoveryParamsSchema": {
21356
+ capName: "device-provider",
21357
+ capScope: "system",
21358
+ addonId: null,
21359
+ access: "view"
21360
+ },
21361
+ "deviceProvider.getManualCreationType": {
21362
+ capName: "device-provider",
21363
+ capScope: "system",
21364
+ addonId: null,
21365
+ access: "view"
21366
+ },
21219
21367
  "deviceProvider.getStatus": {
21220
21368
  capName: "device-provider",
21221
21369
  capScope: "system",
@@ -27170,6 +27318,147 @@ var WyzeCloudClient = class {
27170
27318
  }
27171
27319
  };
27172
27320
  //#endregion
27321
+ //#region src/error-classifier.ts
27322
+ function isWyzeAuthError(err) {
27323
+ return typeof err === "object" && err !== null && "needsMfa" in err && err.needsMfa === true;
27324
+ }
27325
+ function classifyCloudError(err) {
27326
+ if (isWyzeAuthError(err)) return {
27327
+ kind: "mfa",
27328
+ message: `Wyze account requires MFA${typeof err.mfaType === "string" ? ` (${err.mfaType})` : ""}. The bridge library has no MFA-completion flow — use a sub-account or disable MFA on this account.`
27329
+ };
27330
+ const message = err instanceof Error ? err.message : String(err);
27331
+ const lower = message.toLowerCase();
27332
+ if (lower.includes("429") || lower.includes("too many") || lower.includes("rate")) return {
27333
+ kind: "rate-limited",
27334
+ message: `Wyze auth is rate-limited (~1 request / 30s per IP). Retry later — the session is persisted and reused to avoid this. ${message}`
27335
+ };
27336
+ if (lower.includes("password") || lower.includes("credential") || lower.includes("login failed") || lower.includes("access token") || lower.includes("unauthorized")) return {
27337
+ kind: "credentials",
27338
+ message: `Wyze login failed — check email / password / API key / Key ID. ${message}`
27339
+ };
27340
+ if (lower.includes("dtls") || lower.includes("keyframe") || lower.includes("p2p") || lower.includes("discover") || lower.includes("handshake") || lower.includes("kauth")) return {
27341
+ kind: "p2p",
27342
+ message: `Wyze P2P session failed (DTLS / discovery / keyframe). ${message}`
27343
+ };
27344
+ return {
27345
+ kind: "unknown",
27346
+ message
27347
+ };
27348
+ }
27349
+ //#endregion
27350
+ //#region src/client-registry.ts
27351
+ /**
27352
+ * Per-integration Wyze cloud client registry (account mode).
27353
+ *
27354
+ * Under the LOCKED integration/adoption model, Wyze is a `mode: account`
27355
+ * addon: each Wyze integration carries its own account credentials in its
27356
+ * `integration.settings`, and this registry holds one
27357
+ * {@link WyzeCloudClient} per `integrationId`
27358
+ * (`Map<integrationId, WyzeCloudClient>`). Multi-account = multiple
27359
+ * integrations; there is NO shared broker.
27360
+ *
27361
+ * Each client's persisted session lives under a per-integration
27362
+ * subdirectory of `dataDir` so two accounts never clobber each other's
27363
+ * `wyze-session.json`.
27364
+ *
27365
+ * The registry also owns a per-integration camera-list cache (debounced to
27366
+ * respect Wyze's auth rate-limit) so both the adoption provider and the
27367
+ * live cameras read a single shared, rate-safe list per account.
27368
+ */
27369
+ var CAMERA_LIST_REFRESH_MS = 5 * 6e4;
27370
+ /** True when two credential blobs are identical (session-preserving reconcile). */
27371
+ function sameCredentials(a, b) {
27372
+ return a.email === b.email && a.password === b.password && a.apiKey === b.apiKey && a.apiId === b.apiId;
27373
+ }
27374
+ /**
27375
+ * Owns the `Map<integrationId, WyzeCloudClient>`. Callers reconcile the map
27376
+ * against the live integration list on boot + on every integration lifecycle
27377
+ * event; device classes + the adoption provider resolve their client by
27378
+ * `integrationId`.
27379
+ */
27380
+ var WyzeClientRegistry = class {
27381
+ #entries = /* @__PURE__ */ new Map();
27382
+ #deps;
27383
+ constructor(deps) {
27384
+ this.#deps = deps;
27385
+ }
27386
+ /**
27387
+ * Ensure a client exists for `integrationId` with the given credentials.
27388
+ * Idempotent: an existing entry with identical credentials is preserved
27389
+ * (keeps its live session + cache); a credentials change rebuilds it.
27390
+ */
27391
+ upsert(integrationId, credentials) {
27392
+ const existing = this.#entries.get(integrationId);
27393
+ if (existing && sameCredentials(existing.credentials, credentials)) return;
27394
+ const client = new WyzeCloudClient({
27395
+ dataDir: join(this.#deps.dataDir, "integrations", integrationId),
27396
+ logger: this.#deps.logger.child(`cloud:${integrationId}`),
27397
+ credentials
27398
+ });
27399
+ this.#entries.set(integrationId, {
27400
+ client,
27401
+ credentials,
27402
+ cameraListCache: [],
27403
+ cameraListFetchedAt: 0,
27404
+ cameraListInFlight: null
27405
+ });
27406
+ }
27407
+ /** Drop the client for an integration that no longer exists / was disabled. */
27408
+ remove(integrationId) {
27409
+ this.#entries.delete(integrationId);
27410
+ }
27411
+ /** Replace the whole set with `keep` — removes any client not in the set. */
27412
+ retain(keep) {
27413
+ for (const id of [...this.#entries.keys()]) if (!keep.has(id)) this.#entries.delete(id);
27414
+ }
27415
+ /** The live client for an integration, or null when unknown. */
27416
+ client(integrationId) {
27417
+ return this.#entries.get(integrationId)?.client ?? null;
27418
+ }
27419
+ /** True when a client is registered for the integration. */
27420
+ has(integrationId) {
27421
+ return this.#entries.has(integrationId);
27422
+ }
27423
+ /** The registered integration ids (one per account). */
27424
+ list() {
27425
+ return [...this.#entries.keys()];
27426
+ }
27427
+ /** Remove all clients (full shutdown). */
27428
+ clear() {
27429
+ this.#entries.clear();
27430
+ }
27431
+ /**
27432
+ * Cloud camera list for ONE integration, cached + debounced + single-flight
27433
+ * to avoid auth rate-limits. Returns the last cache (possibly empty) when the
27434
+ * integration is unknown or the fetch fails.
27435
+ */
27436
+ async getCameraList(integrationId, force = false) {
27437
+ const entry = this.#entries.get(integrationId);
27438
+ if (!entry) return [];
27439
+ const fresh = Date.now() - entry.cameraListFetchedAt < CAMERA_LIST_REFRESH_MS;
27440
+ if (!force && fresh && entry.cameraListCache.length > 0) return entry.cameraListCache;
27441
+ if (entry.cameraListInFlight) return entry.cameraListInFlight;
27442
+ const run = entry.client.getCameraList().then((cams) => {
27443
+ entry.cameraListCache = cams;
27444
+ entry.cameraListFetchedAt = Date.now();
27445
+ return cams;
27446
+ }).catch((err) => {
27447
+ const classified = classifyCloudError(err);
27448
+ this.#deps.logger.error("Wyze getCameraList failed", { meta: {
27449
+ integrationId,
27450
+ kind: classified.kind,
27451
+ error: classified.message
27452
+ } });
27453
+ return entry.cameraListCache;
27454
+ }).finally(() => {
27455
+ entry.cameraListInFlight = null;
27456
+ });
27457
+ entry.cameraListInFlight = run;
27458
+ return run;
27459
+ }
27460
+ };
27461
+ //#endregion
27173
27462
  //#region src/constants.ts
27174
27463
  /**
27175
27464
  * Single source of truth for the Wyze provider addon id. Must match the
@@ -27408,35 +27697,6 @@ function buildSettingsSchema() {
27408
27697
  }] };
27409
27698
  }
27410
27699
  //#endregion
27411
- //#region src/error-classifier.ts
27412
- function isWyzeAuthError(err) {
27413
- return typeof err === "object" && err !== null && "needsMfa" in err && err.needsMfa === true;
27414
- }
27415
- function classifyCloudError(err) {
27416
- if (isWyzeAuthError(err)) return {
27417
- kind: "mfa",
27418
- message: `Wyze account requires MFA${typeof err.mfaType === "string" ? ` (${err.mfaType})` : ""}. The bridge library has no MFA-completion flow — use a sub-account or disable MFA on this account.`
27419
- };
27420
- const message = err instanceof Error ? err.message : String(err);
27421
- const lower = message.toLowerCase();
27422
- if (lower.includes("429") || lower.includes("too many") || lower.includes("rate")) return {
27423
- kind: "rate-limited",
27424
- message: `Wyze auth is rate-limited (~1 request / 30s per IP). Retry later — the session is persisted and reused to avoid this. ${message}`
27425
- };
27426
- if (lower.includes("password") || lower.includes("credential") || lower.includes("login failed") || lower.includes("access token") || lower.includes("unauthorized")) return {
27427
- kind: "credentials",
27428
- message: `Wyze login failed — check email / password / API key / Key ID. ${message}`
27429
- };
27430
- if (lower.includes("dtls") || lower.includes("keyframe") || lower.includes("p2p") || lower.includes("discover") || lower.includes("handshake") || lower.includes("kauth")) return {
27431
- kind: "p2p",
27432
- message: `Wyze P2P session failed (DTLS / discovery / keyframe). ${message}`
27433
- };
27434
- return {
27435
- kind: "unknown",
27436
- message
27437
- };
27438
- }
27439
- //#endregion
27440
27700
  //#region src/mapping.ts
27441
27701
  /** Canonical camstack codec name from the lib's `videoType`. */
27442
27702
  function codecFromVideoType(videoType) {
@@ -27468,9 +27728,14 @@ function stableIdForMac(mac) {
27468
27728
  * Reduce a cloud `WyzeCamera` blob to the persisted camera config. Pure:
27469
27729
  * `resolution` / `bitrate` / `motionSource` carry their schema defaults
27470
27730
  * here (the device config schema re-applies them on parse).
27731
+ *
27732
+ * `integrationId` stamps the owning account so the camera resolves its
27733
+ * per-account cloud client from the registry. Defaults to `''` (legacy
27734
+ * single-global-credentials cameras) when not supplied by the adopt path.
27471
27735
  */
27472
- function cameraConfigFromCloud(cam) {
27736
+ function cameraConfigFromCloud(cam, integrationId = "") {
27473
27737
  return {
27738
+ integrationId,
27474
27739
  mac: cam.mac,
27475
27740
  p2pId: cam.p2pId,
27476
27741
  enr: cam.enr,
@@ -27536,6 +27801,15 @@ var WyzeResolutionSchema = _enum([
27536
27801
  var WyzeBitrateSchema = _enum(["max", "sd"]);
27537
27802
  var WyzeMotionSourceSchema = _enum(["cloud", "boa"]);
27538
27803
  var wyzeCameraSchema = object({
27804
+ /**
27805
+ * Owning Wyze integration id (account). Stamped at adopt/create so the
27806
+ * camera resolves its per-account {@link WyzeCloudClient} from the client
27807
+ * registry (multi-account: one integration = one account = one client).
27808
+ * Optional for back-compat with cameras persisted under the legacy
27809
+ * single-global-credentials model — those resolve via the migration
27810
+ * fallback until re-adopted.
27811
+ */
27812
+ integrationId: string().default(""),
27539
27813
  /** Hardware MAC — globally unique, the stableId discriminator. */
27540
27814
  mac: string().describe("Wyze camera MAC (hardware id)"),
27541
27815
  /** P2P UID (TUTK) — the cloud `device_params.p2p_id`. */
@@ -27563,6 +27837,192 @@ var wyzeCameraSchema = object({
27563
27837
  motionSource: WyzeMotionSourceSchema.default("cloud")
27564
27838
  });
27565
27839
  //#endregion
27840
+ //#region src/candidates.ts
27841
+ /**
27842
+ * Pure mapping: cloud camera list → `device-adoption` candidates.
27843
+ *
27844
+ * Mirrors `buildDreoCandidates` — no network I/O, no side effects, so it is
27845
+ * exhaustively unit-testable without the lib or a live account. A camera is a
27846
+ * candidate only when it carries the local P2P parameters the DTLS client
27847
+ * needs (`isAdoptableCamera`).
27848
+ */
27849
+ /**
27850
+ * Map live cloud cameras to adoption candidates. The `childNativeId` is the
27851
+ * camera MAC — globally unique + durable, the same key used for the device
27852
+ * stableId — so re-discovery of an already-adopted camera is de-duped.
27853
+ */
27854
+ function buildWyzeCandidates(input) {
27855
+ const out = [];
27856
+ for (const cam of input.cameras) {
27857
+ if (!isAdoptableCamera(cam)) continue;
27858
+ const adoptedId = input.adopted.get(cam.mac.toUpperCase()) ?? null;
27859
+ out.push({
27860
+ childNativeId: cam.mac,
27861
+ name: cam.nickname || cam.productModel || cam.mac,
27862
+ type: DeviceType.Camera,
27863
+ status: cam.isOnline === false ? "offline" : "online",
27864
+ metadata: {
27865
+ serialNumber: cam.mac,
27866
+ model: cam.productModel,
27867
+ manufacturer: "Wyze"
27868
+ },
27869
+ alreadyAdopted: adoptedId !== null,
27870
+ adoptedDeviceId: adoptedId
27871
+ });
27872
+ }
27873
+ return out;
27874
+ }
27875
+ //#endregion
27876
+ //#region src/wyze-adoption-provider.ts
27877
+ /**
27878
+ * `device-adoption` cap provider for the Wyze addon (account mode).
27879
+ *
27880
+ * Broker-less: candidates + adoption are resolved directly by
27881
+ * `integrationId` → the per-account {@link WyzeClientRegistry} client. Mirrors
27882
+ * `buildDreoAdoptionProvider`'s shape (listCandidateFilters / listCandidates /
27883
+ * getCandidate / getStatus / refresh / adopt / release / resync) with a single
27884
+ * `devices` granularity (one Camera per cloud device). Pure builder: every
27885
+ * side-effecting dependency is injected.
27886
+ */
27887
+ /** The single granularity this provider advertises: whole-device adoption. */
27888
+ var DEVICES_FILTER = {
27889
+ id: "devices",
27890
+ label: "Cameras",
27891
+ isDefault: true
27892
+ };
27893
+ /** Build a `MAC(uppercased) → CamStack deviceId` map for one integration. */
27894
+ function adoptedMapForIntegration(integrationId, adopted) {
27895
+ const map = /* @__PURE__ */ new Map();
27896
+ for (const device of adopted) {
27897
+ if (device.config["integrationId"] !== integrationId) continue;
27898
+ const mac = device.config["mac"];
27899
+ if (typeof mac === "string" && mac.length > 0) map.set(mac.toUpperCase(), device.id);
27900
+ }
27901
+ return map;
27902
+ }
27903
+ function buildWyzeAdoptionProvider(deps) {
27904
+ const { getCameraList, hasIntegration, listIntegrations, listAdopted, adoptCamera, removeDevice, findDeviceConfig, logger } = deps;
27905
+ async function candidatesForIntegration(integrationId, force = false) {
27906
+ return buildWyzeCandidates({
27907
+ cameras: await getCameraList(integrationId, force),
27908
+ adopted: adoptedMapForIntegration(integrationId, await listAdopted())
27909
+ });
27910
+ }
27911
+ function applyCandidateTextFilter(cands, filterText) {
27912
+ let filtered = [...cands];
27913
+ if (filterText === void 0) return filtered;
27914
+ const { search, adoptedOnly, unadoptedOnly } = filterText;
27915
+ if (search !== void 0 && search.length > 0) {
27916
+ const lower = search.toLowerCase();
27917
+ filtered = filtered.filter((c) => c.name.toLowerCase().includes(lower) || (c.metadata.model?.toLowerCase().includes(lower) ?? false) || c.childNativeId.toLowerCase().includes(lower));
27918
+ }
27919
+ if (adoptedOnly === true) filtered = filtered.filter((c) => c.alreadyAdopted);
27920
+ if (unadoptedOnly === true) filtered = filtered.filter((c) => !c.alreadyAdopted);
27921
+ return filtered;
27922
+ }
27923
+ return {
27924
+ listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
27925
+ listCandidates: async ({ integrationId, page, pageSize, filterText }) => {
27926
+ const filtered = applyCandidateTextFilter(await candidatesForIntegration(integrationId), filterText);
27927
+ const start = (page - 1) * pageSize;
27928
+ return {
27929
+ candidates: filtered.slice(start, start + pageSize),
27930
+ totalCount: filtered.length,
27931
+ page,
27932
+ pageSize
27933
+ };
27934
+ },
27935
+ getCandidate: async ({ integrationId, childNativeId }) => {
27936
+ return (await candidatesForIntegration(integrationId)).find((c) => c.childNativeId === childNativeId) ?? null;
27937
+ },
27938
+ getStatus: async () => {
27939
+ try {
27940
+ let candidateCount = 0;
27941
+ for (const integrationId of listIntegrations()) candidateCount += (await candidatesForIntegration(integrationId)).length;
27942
+ const adoptedCount = (await listAdopted()).length;
27943
+ return {
27944
+ lastDiscoveryAt: Date.now(),
27945
+ candidateCount,
27946
+ adoptedCount,
27947
+ lastError: null
27948
+ };
27949
+ } catch (err) {
27950
+ logger.warn("wyze adoption: getStatus failed", { meta: { error: errMsg(err) } });
27951
+ return {
27952
+ lastDiscoveryAt: null,
27953
+ candidateCount: 0,
27954
+ adoptedCount: 0,
27955
+ lastError: errMsg(err)
27956
+ };
27957
+ }
27958
+ },
27959
+ refresh: async ({ integrationId }) => {
27960
+ const candidateCount = (await candidatesForIntegration(integrationId, true)).length;
27961
+ const adoptedCount = adoptedMapForIntegration(integrationId, await listAdopted()).size;
27962
+ return {
27963
+ lastDiscoveryAt: Date.now(),
27964
+ candidateCount,
27965
+ adoptedCount,
27966
+ lastError: null
27967
+ };
27968
+ },
27969
+ adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
27970
+ if (!hasIntegration(integrationId)) throw new Error(`wyze adopt: integration ${integrationId} not connected`);
27971
+ const cameras = await getCameraList(integrationId, true);
27972
+ const adopted = [];
27973
+ let failures = 0;
27974
+ for (const mac of childNativeIds) try {
27975
+ const cam = cameras.find((c) => c.mac.toUpperCase() === mac.toUpperCase());
27976
+ if (cam === void 0) {
27977
+ logger.warn("wyze adopt: camera not found on account — skipping", { meta: {
27978
+ mac,
27979
+ integrationId
27980
+ } });
27981
+ failures++;
27982
+ continue;
27983
+ }
27984
+ const name = perCandidate?.[mac]?.name ?? cam.nickname ?? cam.productModel ?? mac;
27985
+ const { deviceId } = await adoptCamera({
27986
+ integrationId,
27987
+ mac: cam.mac,
27988
+ name
27989
+ });
27990
+ adopted.push({
27991
+ childNativeId: mac,
27992
+ parentDeviceId: deviceId,
27993
+ accessoryDeviceIds: []
27994
+ });
27995
+ } catch (err) {
27996
+ logger.warn("wyze adopt: failed to adopt camera", { meta: {
27997
+ mac,
27998
+ integrationId,
27999
+ error: errMsg(err)
28000
+ } });
28001
+ failures++;
28002
+ }
28003
+ if (adopted.length === 0 && failures > 0) throw new Error(`wyze adopt: all ${failures} adopt(s) failed`);
28004
+ return { adopted };
28005
+ },
28006
+ release: async ({ camDeviceId }) => {
28007
+ await removeDevice(camDeviceId);
28008
+ },
28009
+ resync: async ({ camDeviceId }) => {
28010
+ const cfg = await findDeviceConfig(camDeviceId);
28011
+ if (cfg === null) throw new Error(`wyze resync: device ${camDeviceId} not found`);
28012
+ const integrationId = typeof cfg["integrationId"] === "string" ? cfg["integrationId"] : "";
28013
+ const mac = typeof cfg["mac"] === "string" ? cfg["mac"] : "";
28014
+ if (mac.length === 0) throw new Error(`wyze resync: device ${camDeviceId} has no MAC`);
28015
+ if (integrationId.length > 0 && hasIntegration(integrationId)) {
28016
+ if (!(await getCameraList(integrationId)).some((c) => c.mac.toUpperCase() === mac.toUpperCase())) throw new Error(`wyze resync: camera ${mac} no longer present on integration ${integrationId}`);
28017
+ }
28018
+ return {
28019
+ changed: false,
28020
+ rebuiltChildren: 0
28021
+ };
28022
+ }
28023
+ };
28024
+ }
28025
+ //#endregion
27566
28026
  //#region src/snapshot-ffmpeg.ts
27567
28027
  /**
27568
28028
  * Decode a raw Annex-B keyframe (H264 or H265) into a single JPEG via
@@ -28236,34 +28696,56 @@ var WyzeCamera = class extends BaseDevice {
28236
28696
  };
28237
28697
  //#endregion
28238
28698
  //#region src/addon.ts
28239
- var CAMERA_LIST_REFRESH_MS = 5 * 6e4;
28240
28699
  function getString(obj, key) {
28241
28700
  const v = obj[key];
28242
28701
  return typeof v === "string" ? v : "";
28243
28702
  }
28703
+ /** Extract account credentials from an integration's settings, or null. */
28704
+ function credentialsFromSettings(settings) {
28705
+ const email = getString(settings, "email").trim();
28706
+ const password = getString(settings, "password");
28707
+ const apiKey = getString(settings, "apiKey").trim();
28708
+ const apiId = getString(settings, "apiId").trim();
28709
+ if (!email || !password || !apiKey || !apiId) return null;
28710
+ return {
28711
+ email,
28712
+ password,
28713
+ apiKey,
28714
+ apiId
28715
+ };
28716
+ }
28244
28717
  /**
28245
- * Wyze device-provider addon. Discovers cameras via the Wyze cloud
28246
- * (`getCameraList`), persists each camera's P2P parameters, and feeds the
28247
- * stream-broker through the `pull-rfc4571` lazy-publish + dial-time
28248
- * `materializeStreamSocket` path structurally identical to Reolink.
28718
+ * Wyze device-provider addon `mode: account` (multi-account).
28719
+ *
28720
+ * Each Wyze integration carries its own account credentials in its
28721
+ * `integration.settings`; the addon holds one `WyzeCloudClient` per
28722
+ * `integrationId` in {@link WyzeClientRegistry} (`Map<integrationId,
28723
+ * WyzeCloudClient>`). There is NO shared broker. A `device-adoption` cap
28724
+ * provider enumerates each account's cameras and adopts them; each adopted
28725
+ * camera is stamped with its `integrationId` and resolves its account client
28726
+ * by that id.
28727
+ *
28728
+ * Cameras still feed the stream-broker through the `pull-rfc4571`
28729
+ * lazy-publish + dial-time `materializeStreamSocket` path — structurally
28730
+ * identical to Reolink.
28249
28731
  *
28250
28732
  * `hub-only`: the loopback RFC 4571 server binds 127.0.0.1, so the broker
28251
- * that dials it must be co-resident (same as every other camstack
28252
- * provider).
28733
+ * that dials it must be co-resident.
28253
28734
  */
28254
28735
  var WyzeProviderAddon = class extends BaseDeviceProvider {
28255
28736
  addonId = WYZE_ADDON_ID;
28256
28737
  providerName = "Wyze";
28257
28738
  deviceClasses = { [DeviceType.Camera]: WyzeCamera };
28258
- cloud = null;
28259
- cameraListCache = [];
28260
- cameraListFetchedAt = 0;
28261
- cameraListInFlight = null;
28739
+ clients = null;
28262
28740
  constructor() {
28263
28741
  super({});
28264
28742
  }
28265
28743
  async onInitialize() {
28266
28744
  const regs = await super.onInitialize();
28745
+ this.clients = new WyzeClientRegistry({
28746
+ dataDir: this.ctx.dataDir,
28747
+ logger: this.ctx.logger
28748
+ });
28267
28749
  this.subscribe({ category: EventCategory.StreamBrokerOnRequestStreamSourceRefresh }, (event) => {
28268
28750
  const data = event.data;
28269
28751
  const deviceId = typeof data.deviceId === "number" ? data.deviceId : null;
@@ -28273,15 +28755,68 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
28273
28755
  tags: { deviceId },
28274
28756
  meta: {
28275
28757
  camStreamId: data.camStreamId ?? null,
28276
- error: err instanceof Error ? err.message : String(err)
28758
+ error: errMsg(err)
28277
28759
  }
28278
28760
  });
28279
28761
  });
28280
28762
  });
28281
- return regs;
28763
+ await this.reconcileIntegrations();
28764
+ this.subscribeIntegrationLifecycle();
28765
+ return [...regs, {
28766
+ capability: deviceAdoptionCapability,
28767
+ provider: this.buildAdoptionProvider()
28768
+ }];
28769
+ }
28770
+ async onShutdown() {
28771
+ this.clients?.clear();
28772
+ this.clients = null;
28773
+ await super.onShutdown();
28774
+ }
28775
+ requireClients() {
28776
+ if (!this.clients) throw new Error("Wyze provider not initialised");
28777
+ return this.clients;
28778
+ }
28779
+ /**
28780
+ * Rebuild the `Map<integrationId, WyzeCloudClient>` from the live
28781
+ * integration list: for each surviving Wyze integration read its settings
28782
+ * and upsert a client (session-preserving if credentials are unchanged);
28783
+ * drop clients whose integration was deleted/disabled. Idempotent; runs on
28784
+ * boot + on every integration lifecycle event. Guarded so a failure never
28785
+ * fails init.
28786
+ */
28787
+ async reconcileIntegrations() {
28788
+ const reg = this.requireClients();
28789
+ try {
28790
+ const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
28791
+ const surviving = /* @__PURE__ */ new Set();
28792
+ for (const integration of mine) try {
28793
+ const credentials = credentialsFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
28794
+ if (!credentials) {
28795
+ this.ctx.logger.warn("Wyze integration has no complete credentials — skipping", { meta: { integrationId: integration.id } });
28796
+ continue;
28797
+ }
28798
+ reg.upsert(integration.id, credentials);
28799
+ surviving.add(integration.id);
28800
+ } catch (err) {
28801
+ this.ctx.logger.warn("Wyze reconcile: failed to read integration settings", { meta: {
28802
+ integrationId: integration.id,
28803
+ error: errMsg(err)
28804
+ } });
28805
+ }
28806
+ reg.retain(surviving);
28807
+ } catch (err) {
28808
+ this.ctx.logger.warn("Wyze integration reconcile failed", { meta: { error: errMsg(err) } });
28809
+ }
28282
28810
  }
28283
- async onConfigChanged() {
28284
- this.cloud = null;
28811
+ subscribeIntegrationLifecycle() {
28812
+ const handler = (event) => {
28813
+ const addonId = event.data["addonId"];
28814
+ if (typeof addonId === "string" && addonId !== this.ctx.id) return;
28815
+ this.reconcileIntegrations();
28816
+ };
28817
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
28818
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
28819
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
28285
28820
  }
28286
28821
  async refreshDeviceStream(deviceId, camStreamId) {
28287
28822
  const dev = this.ctx.kernel.deviceRegistry?.getById(deviceId);
@@ -28293,94 +28828,57 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
28293
28828
  this.wireCameraDeps(dev);
28294
28829
  await dev.materializeStreamSocket(camStreamId);
28295
28830
  }
28296
- async getGlobalSettings() {
28297
- const raw = await this.ctx.settings?.readAddonStore() ?? {};
28298
- return hydrateSchema(buildSettingsSchema(), raw);
28299
- }
28300
- async updateGlobalSettings(patch) {
28301
- await this.ctx.settings?.writeAddonStore(patch);
28302
- await this.onConfigChanged();
28303
- }
28304
- async readCredentials() {
28305
- const raw = await this.ctx.settings?.readAddonStore() ?? {};
28306
- const email = getString(raw, "email").trim();
28307
- const password = getString(raw, "password");
28308
- const apiKey = getString(raw, "apiKey").trim();
28309
- const apiId = getString(raw, "apiId").trim();
28310
- if (!email || !password || !apiKey || !apiId) return null;
28311
- return {
28312
- email,
28313
- password,
28314
- apiKey,
28315
- apiId
28316
- };
28317
- }
28318
- /** Lazily build the shared cloud client from persisted credentials. */
28319
- async ensureCloud() {
28320
- if (this.cloud) return this.cloud;
28321
- const credentials = await this.readCredentials();
28322
- if (!credentials) {
28323
- this.ctx.logger.warn("Wyze credentials not configured — set email / password / API key / Key ID in the addon settings");
28324
- return null;
28325
- }
28326
- this.cloud = new WyzeCloudClient({
28327
- dataDir: this.ctx.dataDir,
28328
- logger: this.ctx.logger.child("cloud"),
28329
- credentials
28330
- });
28331
- return this.cloud;
28332
- }
28333
- /** Cloud camera list, cached + debounced to avoid auth rate-limits. */
28334
- async getCameraList(force = false) {
28335
- const fresh = Date.now() - this.cameraListFetchedAt < CAMERA_LIST_REFRESH_MS;
28336
- if (!force && fresh && this.cameraListCache.length > 0) return this.cameraListCache;
28337
- if (this.cameraListInFlight) return this.cameraListInFlight;
28338
- const cloud = await this.ensureCloud();
28339
- if (!cloud) return this.cameraListCache;
28340
- const run = cloud.getCameraList().then((cams) => {
28341
- this.cameraListCache = cams;
28342
- this.cameraListFetchedAt = Date.now();
28343
- return cams;
28344
- }).catch((err) => {
28345
- const classified = classifyCloudError(err);
28346
- this.ctx.logger.error("Wyze getCameraList failed", { meta: {
28347
- kind: classified.kind,
28348
- error: classified.message
28349
- } });
28350
- return this.cameraListCache;
28351
- }).finally(() => {
28352
- this.cameraListInFlight = null;
28353
- });
28354
- this.cameraListInFlight = run;
28355
- return run;
28356
- }
28357
- async supportsDiscovery() {
28358
- return true;
28359
- }
28360
- async discoverDevices() {
28361
- const cams = await this.getCameraList(true);
28362
- const candidates = [];
28363
- for (const cam of cams) {
28364
- if (!isAdoptableCamera(cam)) continue;
28365
- candidates.push({
28366
- stableId: stableIdForMac(cam.mac),
28367
- type: DeviceType.Camera,
28368
- suggestedName: cam.nickname || cam.productModel || cam.mac,
28369
- prefilledConfig: cameraConfigFromCloud(cam)
28370
- });
28371
- }
28372
- return candidates;
28373
- }
28374
- async adoptDiscoveredDevice(input) {
28375
- const config = wyzeCameraSchema.parse(input.candidate.prefilledConfig);
28376
- return this.createDevice({
28377
- type: DeviceType.Camera,
28378
- config: {
28379
- ...config,
28380
- name: input.candidate.suggestedName
28831
+ buildAdoptionProvider() {
28832
+ return buildWyzeAdoptionProvider({
28833
+ logger: this.ctx.logger,
28834
+ getCameraList: (integrationId, force) => this.requireClients().getCameraList(integrationId, force),
28835
+ hasIntegration: (integrationId) => this.requireClients().has(integrationId),
28836
+ listIntegrations: () => this.requireClients().list(),
28837
+ listAdopted: async () => {
28838
+ const reg = this.ctx.kernel.deviceRegistry;
28839
+ const devices = this.ctx.kernel.devices;
28840
+ if (!reg || !devices) return [];
28841
+ const out = [];
28842
+ for (const d of reg.getAllForAddon(this.addonId)) {
28843
+ if (d.parentDeviceId !== null) continue;
28844
+ const config = await devices.loadConfig(d.id).catch(() => ({}));
28845
+ out.push({
28846
+ id: d.id,
28847
+ config
28848
+ });
28849
+ }
28850
+ return out;
28851
+ },
28852
+ adoptCamera: async ({ integrationId, mac, name }) => {
28853
+ const cam = (await this.requireClients().getCameraList(integrationId)).find((c) => c.mac.toUpperCase() === mac.toUpperCase());
28854
+ if (!cam) throw new Error(`wyze adopt: camera ${mac} not found on integration ${integrationId}`);
28855
+ const config = {
28856
+ ...cameraConfigFromCloud(cam, integrationId),
28857
+ name
28858
+ };
28859
+ const parsed = wyzeCameraSchema.parse(config);
28860
+ return { deviceId: (await this.createDevice({
28861
+ type: DeviceType.Camera,
28862
+ config: {
28863
+ ...parsed,
28864
+ name,
28865
+ integrationId
28866
+ }
28867
+ })).id };
28868
+ },
28869
+ removeDevice: async (id) => {
28870
+ await this.ctx.kernel.devices?.remove(id);
28871
+ },
28872
+ findDeviceConfig: async (id) => {
28873
+ const devices = this.ctx.kernel.devices;
28874
+ if (!devices) return null;
28875
+ return devices.loadConfig(id).catch(() => null);
28381
28876
  }
28382
28877
  });
28383
28878
  }
28879
+ async getGlobalSettings() {
28880
+ return hydrateSchema(buildSettingsSchema(), {});
28881
+ }
28384
28882
  async onGetCreationSchema(type) {
28385
28883
  if (type !== DeviceType.Camera) return null;
28386
28884
  return buildCreationFormSchema();
@@ -28389,20 +28887,25 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
28389
28887
  if (type !== DeviceType.Camera) throw new Error(`Wyze provider does not support device type: ${type}`);
28390
28888
  const name = getString(config, "name").trim();
28391
28889
  if (!name) throw new Error("Camera name is required");
28890
+ const integrationId = getString(config, "integrationId").trim();
28392
28891
  const mac = getString(config, "mac").trim();
28393
28892
  let merged = config;
28394
- if (mac) {
28395
- const fromCloud = (await this.getCameraList()).find((c) => c.mac.toUpperCase() === mac.toUpperCase());
28893
+ if (mac && integrationId) {
28894
+ const fromCloud = (await this.requireClients().getCameraList(integrationId)).find((c) => c.mac.toUpperCase() === mac.toUpperCase());
28396
28895
  if (fromCloud) merged = {
28397
28896
  ...config,
28398
- ...cameraConfigFromCloud(fromCloud)
28897
+ ...cameraConfigFromCloud(fromCloud, integrationId)
28399
28898
  };
28400
28899
  }
28401
- const parsed = wyzeCameraSchema.parse(merged);
28900
+ const parsed = wyzeCameraSchema.parse({
28901
+ ...merged,
28902
+ integrationId
28903
+ });
28402
28904
  return {
28403
28905
  meta: {
28404
28906
  type: DeviceType.Camera,
28405
- name
28907
+ name,
28908
+ ...integrationId ? { integrationId } : {}
28406
28909
  },
28407
28910
  config: parsed,
28408
28911
  onAfterCreate: async (device) => {
@@ -28416,16 +28919,19 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
28416
28919
  throw new Error("Wyze: cannot persist a camera without a MAC address");
28417
28920
  }
28418
28921
  /**
28419
- * Inject the cloud resolvers into a camera so it can (a) refresh its
28420
- * local IP before each dial and (b) poll cloud events for motion +
28421
- * thumbnails — without holding a direct provider reference.
28922
+ * Inject the per-account cloud resolvers into a camera so it can (a) refresh
28923
+ * its local IP before each dial and (b) poll cloud events for motion +
28924
+ * thumbnails — resolving its account {@link WyzeCloudClient} by the camera's
28925
+ * own `integrationId` config, without holding a direct provider reference.
28422
28926
  */
28423
28927
  wireCameraDeps(camera) {
28424
28928
  const mac = camera.config.get("mac");
28929
+ const integrationId = camera.config.get("integrationId");
28425
28930
  camera.setDeps({
28426
- cloud: () => this.cloud,
28931
+ cloud: () => this.clients?.client(integrationId) ?? null,
28427
28932
  resolveCloudCamera: async () => {
28428
- return (await this.getCameraList()).find((c) => c.mac.toUpperCase() === mac.toUpperCase()) ?? null;
28933
+ if (!integrationId) return null;
28934
+ return (await this.requireClients().getCameraList(integrationId)).find((c) => c.mac.toUpperCase() === mac.toUpperCase()) ?? null;
28429
28935
  }
28430
28936
  });
28431
28937
  }
@@ -28436,4 +28942,4 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
28436
28942
  }
28437
28943
  };
28438
28944
  //#endregion
28439
- export { WyzeProviderAddon, wyzeCameraSchema as n, WyzeCamera as t };
28945
+ export { WyzeProviderAddon, WyzeClientRegistry as a, wyzeCameraSchema as i, buildWyzeAdoptionProvider as n, buildWyzeCandidates as r, WyzeCamera as t };