@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.js CHANGED
@@ -21,8 +21,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- let node_fs = require("node:fs");
25
24
  let node_path = require("node:path");
25
+ let node_fs = require("node:fs");
26
26
  let crypto$1 = require("crypto");
27
27
  let dgram = require("dgram");
28
28
  dgram = __toESM(dgram, 1);
@@ -4659,7 +4659,7 @@ function _instanceof(cls, params = {}) {
4659
4659
  return inst;
4660
4660
  }
4661
4661
  //#endregion
4662
- //#region ../types/dist/sleep-B3AOslwX.mjs
4662
+ //#region ../types/dist/sleep-C2M2zF7x.mjs
4663
4663
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4664
4664
  EventCategory["SystemBoot"] = "system.boot";
4665
4665
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -6227,6 +6227,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
6227
6227
  DeviceType["Switch"] = "switch";
6228
6228
  DeviceType["Sensor"] = "sensor";
6229
6229
  DeviceType["Thermostat"] = "thermostat";
6230
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
6231
+ * `climate-control` cap surface with `Thermostat` but renders a
6232
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
6233
+ * independent vertical/horizontal swing). Sources: native Gree, and
6234
+ * reusable by other AC integrations. */
6235
+ DeviceType["Climate"] = "climate";
6230
6236
  DeviceType["Button"] = "button";
6231
6237
  /** Generic stateless event emitter — carries a device's EXACT declared
6232
6238
  * event vocabulary verbatim (no normalization). Installed with the
@@ -6644,6 +6650,18 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
6644
6650
  input: unknown()
6645
6651
  }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
6646
6652
  //#endregion
6653
+ //#region ../types/dist/err-msg-IQTHeDzc.mjs
6654
+ /**
6655
+ import { errMsg } from '@camstack/types'
6656
+ * Extract a human-readable message from an unknown error value.
6657
+ * Replaces the ubiquitous `errMsg(err)` pattern.
6658
+ */
6659
+ function errMsg(err) {
6660
+ if (err instanceof Error) return err.message;
6661
+ if (typeof err === "string") return err;
6662
+ return String(err);
6663
+ }
6664
+ //#endregion
6647
6665
  //#region ../types/dist/index.mjs
6648
6666
  /**
6649
6667
  * Deep wiring healthcheck — snapshot of active reachability probes across
@@ -9010,7 +9028,7 @@ var climateControlCapability = {
9010
9028
  scope: "device",
9011
9029
  deviceNative: true,
9012
9030
  mode: "singleton",
9013
- deviceTypes: [DeviceType.Thermostat],
9031
+ deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
9014
9032
  methods: {
9015
9033
  setMode: method(object({
9016
9034
  deviceId: number().int().nonnegative(),
@@ -13609,10 +13627,30 @@ var deviceProviderCapability = {
13609
13627
  type: string()
13610
13628
  }))),
13611
13629
  supportsDiscovery: method(object({}), boolean()),
13612
- discoverDevices: method(object({}), array(DiscoveryCandidateSchema), {
13630
+ /**
13631
+ * Run a network scan. `params` carries optional provider-specific scan
13632
+ * inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
13633
+ * shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
13634
+ * (provider uses its local-network default).
13635
+ */
13636
+ discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
13613
13637
  kind: "mutation",
13614
13638
  auth: "admin"
13615
13639
  }),
13640
+ /**
13641
+ * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
13642
+ * provider accepts (e.g. Gree's broadcast address for a different subnet).
13643
+ * `null` when the provider takes no extra scan params — the generic
13644
+ * aggregated scan never renders this; the per-integration scan does.
13645
+ */
13646
+ getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
13647
+ /**
13648
+ * The DeviceType this provider creates via manual add (Camera for
13649
+ * Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
13650
+ * provider does not support manual creation. Lets the Add-Device dialog
13651
+ * pick the right type instead of assuming Camera.
13652
+ */
13653
+ getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
13616
13654
  adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
13617
13655
  kind: "mutation",
13618
13656
  auth: "admin"
@@ -13736,9 +13774,23 @@ var BaseDeviceProvider = class extends BaseAddon {
13736
13774
  async supportsDiscovery() {
13737
13775
  return false;
13738
13776
  }
13739
- async discoverDevices() {
13777
+ async discoverDevices(_input) {
13740
13778
  return [];
13741
13779
  }
13780
+ /** Extra per-scan input form (e.g. a broadcast address for another subnet).
13781
+ * Null = no extra params. Override in providers that support scoped scans. */
13782
+ async getDiscoveryParamsSchema() {
13783
+ return null;
13784
+ }
13785
+ /**
13786
+ * The DeviceType this provider creates via manual add — derived from the
13787
+ * `deviceClasses` map (first registered type). `null` when manual creation is
13788
+ * unsupported. Lets the Add-Device dialog pick the right type per provider.
13789
+ */
13790
+ async getManualCreationType() {
13791
+ if (!await this.supportsManualCreation()) return { deviceType: null };
13792
+ return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
13793
+ }
13742
13794
  async adoptDiscoveredDevice(_input) {
13743
13795
  throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
13744
13796
  }
@@ -15048,19 +15100,36 @@ var ResyncResultSchema = object({
15048
15100
  * provider re-derived the device. 0/absent for a normal incremental re-sync. */
15049
15101
  removedChildren: number().int().nonnegative().optional()
15050
15102
  });
15051
- 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, {
15052
- kind: "mutation",
15053
- auth: "admin"
15054
- }), method(AdoptInputSchema, AdoptResultSchema, {
15055
- kind: "mutation",
15056
- auth: "admin"
15057
- }), method(ReleaseInputSchema, _void(), {
15058
- kind: "mutation",
15059
- auth: "admin"
15060
- }), method(ResyncInputSchema, ResyncResultSchema, {
15061
- kind: "mutation",
15062
- auth: "admin"
15063
- });
15103
+ var deviceAdoptionCapability = {
15104
+ name: "device-adoption",
15105
+ scope: "system",
15106
+ mode: "singleton",
15107
+ status: {
15108
+ schema: AdoptionStatusSchema,
15109
+ kind: "poll"
15110
+ },
15111
+ methods: {
15112
+ listCandidateFilters: method(object({ integrationId: string() }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }),
15113
+ listCandidates: method(ListCandidatesInputSchema, ListCandidatesOutputSchema, { auth: "admin" }),
15114
+ getCandidate: method(GetCandidateInputSchema, DiscoveredChildDeviceSchema.nullable(), { auth: "admin" }),
15115
+ refresh: method(object({ integrationId: string() }), AdoptionStatusSchema, {
15116
+ kind: "mutation",
15117
+ auth: "admin"
15118
+ }),
15119
+ adopt: method(AdoptInputSchema, AdoptResultSchema, {
15120
+ kind: "mutation",
15121
+ auth: "admin"
15122
+ }),
15123
+ release: method(ReleaseInputSchema, _void(), {
15124
+ kind: "mutation",
15125
+ auth: "admin"
15126
+ }),
15127
+ resync: method(ResyncInputSchema, ResyncResultSchema, {
15128
+ kind: "mutation",
15129
+ auth: "admin"
15130
+ })
15131
+ }
15132
+ };
15064
15133
  /**
15065
15134
  * `device-export` — collection cap for addons that export camstack
15066
15135
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
@@ -15583,7 +15652,10 @@ method(object({
15583
15652
  }), FieldProbeResultSchema, {
15584
15653
  kind: "mutation",
15585
15654
  auth: "admin"
15586
- }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
15655
+ }), method(object({
15656
+ addonId: string(),
15657
+ integrationId: string()
15658
+ }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
15587
15659
  addonId: string(),
15588
15660
  integrationId: string()
15589
15661
  }), AdoptionStatusSchema, {
@@ -15598,7 +15670,24 @@ method(object({
15598
15670
  }), method(ResyncInputSchema, ResyncResultSchema, {
15599
15671
  kind: "mutation",
15600
15672
  auth: "admin"
15673
+ }), method(object({}), object({ providers: array(object({
15674
+ addonId: string(),
15675
+ label: string()
15676
+ })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
15677
+ addonId: string(),
15678
+ label: string(),
15679
+ candidates: array(DiscoveryCandidateSchema).readonly(),
15680
+ error: string().nullable()
15681
+ })).readonly() }), {
15682
+ kind: "mutation",
15683
+ auth: "admin"
15601
15684
  }), method(object({
15685
+ addonId: string(),
15686
+ params: record(string(), unknown()).optional()
15687
+ }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
15688
+ kind: "mutation",
15689
+ auth: "admin"
15690
+ }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
15602
15691
  deviceId: number(),
15603
15692
  key: string(),
15604
15693
  value: unknown()
@@ -18293,6 +18382,17 @@ var AvailableIntegrationTypeSchema = object({
18293
18382
  iconUrl: string().nullable(),
18294
18383
  color: string(),
18295
18384
  instanceMode: string(),
18385
+ /**
18386
+ * Integration wizard `mode` (LOCKED MODEL): `standalone` (create
18387
+ * immediately then add devices, no config step/button), `account` (config
18388
+ * step), or `broker` (broker step). Derived server-side by
18389
+ * `getAvailableTypes` when the addon manifest omits an explicit `mode`.
18390
+ */
18391
+ mode: _enum([
18392
+ "standalone",
18393
+ "account",
18394
+ "broker"
18395
+ ]),
18296
18396
  discoveryMode: string(),
18297
18397
  /**
18298
18398
  * Which integration-marker cap the addon declared, so the wizard can
@@ -20817,6 +20917,12 @@ Object.freeze({
20817
20917
  addonId: null,
20818
20918
  access: "create"
20819
20919
  },
20920
+ "deviceManager.adoptionListCandidateFilters": {
20921
+ capName: "device-manager",
20922
+ capScope: "system",
20923
+ addonId: null,
20924
+ access: "view"
20925
+ },
20820
20926
  "deviceManager.adoptionListCandidates": {
20821
20927
  capName: "device-manager",
20822
20928
  capScope: "system",
@@ -20865,12 +20971,30 @@ Object.freeze({
20865
20971
  addonId: null,
20866
20972
  access: "create"
20867
20973
  },
20974
+ "deviceManager.discoverAllProviders": {
20975
+ capName: "device-manager",
20976
+ capScope: "system",
20977
+ addonId: null,
20978
+ access: "create"
20979
+ },
20868
20980
  "deviceManager.discoverDevices": {
20869
20981
  capName: "device-manager",
20870
20982
  capScope: "system",
20871
20983
  addonId: null,
20872
20984
  access: "create"
20873
20985
  },
20986
+ "deviceManager.discoverProvider": {
20987
+ capName: "device-manager",
20988
+ capScope: "system",
20989
+ addonId: null,
20990
+ access: "create"
20991
+ },
20992
+ "deviceManager.discoveryProviders": {
20993
+ capName: "device-manager",
20994
+ capScope: "system",
20995
+ addonId: null,
20996
+ access: "view"
20997
+ },
20874
20998
  "deviceManager.enable": {
20875
20999
  capName: "device-manager",
20876
21000
  capScope: "system",
@@ -21021,6 +21145,18 @@ Object.freeze({
21021
21145
  addonId: null,
21022
21146
  access: "create"
21023
21147
  },
21148
+ "deviceManager.providerCreationType": {
21149
+ capName: "device-manager",
21150
+ capScope: "system",
21151
+ addonId: null,
21152
+ access: "view"
21153
+ },
21154
+ "deviceManager.providerDiscoveryParamsSchema": {
21155
+ capName: "device-manager",
21156
+ capScope: "system",
21157
+ addonId: null,
21158
+ access: "view"
21159
+ },
21024
21160
  "deviceManager.registerDevice": {
21025
21161
  capName: "device-manager",
21026
21162
  capScope: "system",
@@ -21237,6 +21373,18 @@ Object.freeze({
21237
21373
  addonId: null,
21238
21374
  access: "view"
21239
21375
  },
21376
+ "deviceProvider.getDiscoveryParamsSchema": {
21377
+ capName: "device-provider",
21378
+ capScope: "system",
21379
+ addonId: null,
21380
+ access: "view"
21381
+ },
21382
+ "deviceProvider.getManualCreationType": {
21383
+ capName: "device-provider",
21384
+ capScope: "system",
21385
+ addonId: null,
21386
+ access: "view"
21387
+ },
21240
21388
  "deviceProvider.getStatus": {
21241
21389
  capName: "device-provider",
21242
21390
  capScope: "system",
@@ -27191,6 +27339,147 @@ var WyzeCloudClient = class {
27191
27339
  }
27192
27340
  };
27193
27341
  //#endregion
27342
+ //#region src/error-classifier.ts
27343
+ function isWyzeAuthError(err) {
27344
+ return typeof err === "object" && err !== null && "needsMfa" in err && err.needsMfa === true;
27345
+ }
27346
+ function classifyCloudError(err) {
27347
+ if (isWyzeAuthError(err)) return {
27348
+ kind: "mfa",
27349
+ 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.`
27350
+ };
27351
+ const message = err instanceof Error ? err.message : String(err);
27352
+ const lower = message.toLowerCase();
27353
+ if (lower.includes("429") || lower.includes("too many") || lower.includes("rate")) return {
27354
+ kind: "rate-limited",
27355
+ message: `Wyze auth is rate-limited (~1 request / 30s per IP). Retry later — the session is persisted and reused to avoid this. ${message}`
27356
+ };
27357
+ if (lower.includes("password") || lower.includes("credential") || lower.includes("login failed") || lower.includes("access token") || lower.includes("unauthorized")) return {
27358
+ kind: "credentials",
27359
+ message: `Wyze login failed — check email / password / API key / Key ID. ${message}`
27360
+ };
27361
+ if (lower.includes("dtls") || lower.includes("keyframe") || lower.includes("p2p") || lower.includes("discover") || lower.includes("handshake") || lower.includes("kauth")) return {
27362
+ kind: "p2p",
27363
+ message: `Wyze P2P session failed (DTLS / discovery / keyframe). ${message}`
27364
+ };
27365
+ return {
27366
+ kind: "unknown",
27367
+ message
27368
+ };
27369
+ }
27370
+ //#endregion
27371
+ //#region src/client-registry.ts
27372
+ /**
27373
+ * Per-integration Wyze cloud client registry (account mode).
27374
+ *
27375
+ * Under the LOCKED integration/adoption model, Wyze is a `mode: account`
27376
+ * addon: each Wyze integration carries its own account credentials in its
27377
+ * `integration.settings`, and this registry holds one
27378
+ * {@link WyzeCloudClient} per `integrationId`
27379
+ * (`Map<integrationId, WyzeCloudClient>`). Multi-account = multiple
27380
+ * integrations; there is NO shared broker.
27381
+ *
27382
+ * Each client's persisted session lives under a per-integration
27383
+ * subdirectory of `dataDir` so two accounts never clobber each other's
27384
+ * `wyze-session.json`.
27385
+ *
27386
+ * The registry also owns a per-integration camera-list cache (debounced to
27387
+ * respect Wyze's auth rate-limit) so both the adoption provider and the
27388
+ * live cameras read a single shared, rate-safe list per account.
27389
+ */
27390
+ var CAMERA_LIST_REFRESH_MS = 5 * 6e4;
27391
+ /** True when two credential blobs are identical (session-preserving reconcile). */
27392
+ function sameCredentials(a, b) {
27393
+ return a.email === b.email && a.password === b.password && a.apiKey === b.apiKey && a.apiId === b.apiId;
27394
+ }
27395
+ /**
27396
+ * Owns the `Map<integrationId, WyzeCloudClient>`. Callers reconcile the map
27397
+ * against the live integration list on boot + on every integration lifecycle
27398
+ * event; device classes + the adoption provider resolve their client by
27399
+ * `integrationId`.
27400
+ */
27401
+ var WyzeClientRegistry = class {
27402
+ #entries = /* @__PURE__ */ new Map();
27403
+ #deps;
27404
+ constructor(deps) {
27405
+ this.#deps = deps;
27406
+ }
27407
+ /**
27408
+ * Ensure a client exists for `integrationId` with the given credentials.
27409
+ * Idempotent: an existing entry with identical credentials is preserved
27410
+ * (keeps its live session + cache); a credentials change rebuilds it.
27411
+ */
27412
+ upsert(integrationId, credentials) {
27413
+ const existing = this.#entries.get(integrationId);
27414
+ if (existing && sameCredentials(existing.credentials, credentials)) return;
27415
+ const client = new WyzeCloudClient({
27416
+ dataDir: (0, node_path.join)(this.#deps.dataDir, "integrations", integrationId),
27417
+ logger: this.#deps.logger.child(`cloud:${integrationId}`),
27418
+ credentials
27419
+ });
27420
+ this.#entries.set(integrationId, {
27421
+ client,
27422
+ credentials,
27423
+ cameraListCache: [],
27424
+ cameraListFetchedAt: 0,
27425
+ cameraListInFlight: null
27426
+ });
27427
+ }
27428
+ /** Drop the client for an integration that no longer exists / was disabled. */
27429
+ remove(integrationId) {
27430
+ this.#entries.delete(integrationId);
27431
+ }
27432
+ /** Replace the whole set with `keep` — removes any client not in the set. */
27433
+ retain(keep) {
27434
+ for (const id of [...this.#entries.keys()]) if (!keep.has(id)) this.#entries.delete(id);
27435
+ }
27436
+ /** The live client for an integration, or null when unknown. */
27437
+ client(integrationId) {
27438
+ return this.#entries.get(integrationId)?.client ?? null;
27439
+ }
27440
+ /** True when a client is registered for the integration. */
27441
+ has(integrationId) {
27442
+ return this.#entries.has(integrationId);
27443
+ }
27444
+ /** The registered integration ids (one per account). */
27445
+ list() {
27446
+ return [...this.#entries.keys()];
27447
+ }
27448
+ /** Remove all clients (full shutdown). */
27449
+ clear() {
27450
+ this.#entries.clear();
27451
+ }
27452
+ /**
27453
+ * Cloud camera list for ONE integration, cached + debounced + single-flight
27454
+ * to avoid auth rate-limits. Returns the last cache (possibly empty) when the
27455
+ * integration is unknown or the fetch fails.
27456
+ */
27457
+ async getCameraList(integrationId, force = false) {
27458
+ const entry = this.#entries.get(integrationId);
27459
+ if (!entry) return [];
27460
+ const fresh = Date.now() - entry.cameraListFetchedAt < CAMERA_LIST_REFRESH_MS;
27461
+ if (!force && fresh && entry.cameraListCache.length > 0) return entry.cameraListCache;
27462
+ if (entry.cameraListInFlight) return entry.cameraListInFlight;
27463
+ const run = entry.client.getCameraList().then((cams) => {
27464
+ entry.cameraListCache = cams;
27465
+ entry.cameraListFetchedAt = Date.now();
27466
+ return cams;
27467
+ }).catch((err) => {
27468
+ const classified = classifyCloudError(err);
27469
+ this.#deps.logger.error("Wyze getCameraList failed", { meta: {
27470
+ integrationId,
27471
+ kind: classified.kind,
27472
+ error: classified.message
27473
+ } });
27474
+ return entry.cameraListCache;
27475
+ }).finally(() => {
27476
+ entry.cameraListInFlight = null;
27477
+ });
27478
+ entry.cameraListInFlight = run;
27479
+ return run;
27480
+ }
27481
+ };
27482
+ //#endregion
27194
27483
  //#region src/constants.ts
27195
27484
  /**
27196
27485
  * Single source of truth for the Wyze provider addon id. Must match the
@@ -27429,35 +27718,6 @@ function buildSettingsSchema() {
27429
27718
  }] };
27430
27719
  }
27431
27720
  //#endregion
27432
- //#region src/error-classifier.ts
27433
- function isWyzeAuthError(err) {
27434
- return typeof err === "object" && err !== null && "needsMfa" in err && err.needsMfa === true;
27435
- }
27436
- function classifyCloudError(err) {
27437
- if (isWyzeAuthError(err)) return {
27438
- kind: "mfa",
27439
- 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.`
27440
- };
27441
- const message = err instanceof Error ? err.message : String(err);
27442
- const lower = message.toLowerCase();
27443
- if (lower.includes("429") || lower.includes("too many") || lower.includes("rate")) return {
27444
- kind: "rate-limited",
27445
- message: `Wyze auth is rate-limited (~1 request / 30s per IP). Retry later — the session is persisted and reused to avoid this. ${message}`
27446
- };
27447
- if (lower.includes("password") || lower.includes("credential") || lower.includes("login failed") || lower.includes("access token") || lower.includes("unauthorized")) return {
27448
- kind: "credentials",
27449
- message: `Wyze login failed — check email / password / API key / Key ID. ${message}`
27450
- };
27451
- if (lower.includes("dtls") || lower.includes("keyframe") || lower.includes("p2p") || lower.includes("discover") || lower.includes("handshake") || lower.includes("kauth")) return {
27452
- kind: "p2p",
27453
- message: `Wyze P2P session failed (DTLS / discovery / keyframe). ${message}`
27454
- };
27455
- return {
27456
- kind: "unknown",
27457
- message
27458
- };
27459
- }
27460
- //#endregion
27461
27721
  //#region src/mapping.ts
27462
27722
  /** Canonical camstack codec name from the lib's `videoType`. */
27463
27723
  function codecFromVideoType(videoType) {
@@ -27489,9 +27749,14 @@ function stableIdForMac(mac) {
27489
27749
  * Reduce a cloud `WyzeCamera` blob to the persisted camera config. Pure:
27490
27750
  * `resolution` / `bitrate` / `motionSource` carry their schema defaults
27491
27751
  * here (the device config schema re-applies them on parse).
27752
+ *
27753
+ * `integrationId` stamps the owning account so the camera resolves its
27754
+ * per-account cloud client from the registry. Defaults to `''` (legacy
27755
+ * single-global-credentials cameras) when not supplied by the adopt path.
27492
27756
  */
27493
- function cameraConfigFromCloud(cam) {
27757
+ function cameraConfigFromCloud(cam, integrationId = "") {
27494
27758
  return {
27759
+ integrationId,
27495
27760
  mac: cam.mac,
27496
27761
  p2pId: cam.p2pId,
27497
27762
  enr: cam.enr,
@@ -27557,6 +27822,15 @@ var WyzeResolutionSchema = _enum([
27557
27822
  var WyzeBitrateSchema = _enum(["max", "sd"]);
27558
27823
  var WyzeMotionSourceSchema = _enum(["cloud", "boa"]);
27559
27824
  var wyzeCameraSchema = object({
27825
+ /**
27826
+ * Owning Wyze integration id (account). Stamped at adopt/create so the
27827
+ * camera resolves its per-account {@link WyzeCloudClient} from the client
27828
+ * registry (multi-account: one integration = one account = one client).
27829
+ * Optional for back-compat with cameras persisted under the legacy
27830
+ * single-global-credentials model — those resolve via the migration
27831
+ * fallback until re-adopted.
27832
+ */
27833
+ integrationId: string().default(""),
27560
27834
  /** Hardware MAC — globally unique, the stableId discriminator. */
27561
27835
  mac: string().describe("Wyze camera MAC (hardware id)"),
27562
27836
  /** P2P UID (TUTK) — the cloud `device_params.p2p_id`. */
@@ -27584,6 +27858,192 @@ var wyzeCameraSchema = object({
27584
27858
  motionSource: WyzeMotionSourceSchema.default("cloud")
27585
27859
  });
27586
27860
  //#endregion
27861
+ //#region src/candidates.ts
27862
+ /**
27863
+ * Pure mapping: cloud camera list → `device-adoption` candidates.
27864
+ *
27865
+ * Mirrors `buildDreoCandidates` — no network I/O, no side effects, so it is
27866
+ * exhaustively unit-testable without the lib or a live account. A camera is a
27867
+ * candidate only when it carries the local P2P parameters the DTLS client
27868
+ * needs (`isAdoptableCamera`).
27869
+ */
27870
+ /**
27871
+ * Map live cloud cameras to adoption candidates. The `childNativeId` is the
27872
+ * camera MAC — globally unique + durable, the same key used for the device
27873
+ * stableId — so re-discovery of an already-adopted camera is de-duped.
27874
+ */
27875
+ function buildWyzeCandidates(input) {
27876
+ const out = [];
27877
+ for (const cam of input.cameras) {
27878
+ if (!isAdoptableCamera(cam)) continue;
27879
+ const adoptedId = input.adopted.get(cam.mac.toUpperCase()) ?? null;
27880
+ out.push({
27881
+ childNativeId: cam.mac,
27882
+ name: cam.nickname || cam.productModel || cam.mac,
27883
+ type: DeviceType.Camera,
27884
+ status: cam.isOnline === false ? "offline" : "online",
27885
+ metadata: {
27886
+ serialNumber: cam.mac,
27887
+ model: cam.productModel,
27888
+ manufacturer: "Wyze"
27889
+ },
27890
+ alreadyAdopted: adoptedId !== null,
27891
+ adoptedDeviceId: adoptedId
27892
+ });
27893
+ }
27894
+ return out;
27895
+ }
27896
+ //#endregion
27897
+ //#region src/wyze-adoption-provider.ts
27898
+ /**
27899
+ * `device-adoption` cap provider for the Wyze addon (account mode).
27900
+ *
27901
+ * Broker-less: candidates + adoption are resolved directly by
27902
+ * `integrationId` → the per-account {@link WyzeClientRegistry} client. Mirrors
27903
+ * `buildDreoAdoptionProvider`'s shape (listCandidateFilters / listCandidates /
27904
+ * getCandidate / getStatus / refresh / adopt / release / resync) with a single
27905
+ * `devices` granularity (one Camera per cloud device). Pure builder: every
27906
+ * side-effecting dependency is injected.
27907
+ */
27908
+ /** The single granularity this provider advertises: whole-device adoption. */
27909
+ var DEVICES_FILTER = {
27910
+ id: "devices",
27911
+ label: "Cameras",
27912
+ isDefault: true
27913
+ };
27914
+ /** Build a `MAC(uppercased) → CamStack deviceId` map for one integration. */
27915
+ function adoptedMapForIntegration(integrationId, adopted) {
27916
+ const map = /* @__PURE__ */ new Map();
27917
+ for (const device of adopted) {
27918
+ if (device.config["integrationId"] !== integrationId) continue;
27919
+ const mac = device.config["mac"];
27920
+ if (typeof mac === "string" && mac.length > 0) map.set(mac.toUpperCase(), device.id);
27921
+ }
27922
+ return map;
27923
+ }
27924
+ function buildWyzeAdoptionProvider(deps) {
27925
+ const { getCameraList, hasIntegration, listIntegrations, listAdopted, adoptCamera, removeDevice, findDeviceConfig, logger } = deps;
27926
+ async function candidatesForIntegration(integrationId, force = false) {
27927
+ return buildWyzeCandidates({
27928
+ cameras: await getCameraList(integrationId, force),
27929
+ adopted: adoptedMapForIntegration(integrationId, await listAdopted())
27930
+ });
27931
+ }
27932
+ function applyCandidateTextFilter(cands, filterText) {
27933
+ let filtered = [...cands];
27934
+ if (filterText === void 0) return filtered;
27935
+ const { search, adoptedOnly, unadoptedOnly } = filterText;
27936
+ if (search !== void 0 && search.length > 0) {
27937
+ const lower = search.toLowerCase();
27938
+ filtered = filtered.filter((c) => c.name.toLowerCase().includes(lower) || (c.metadata.model?.toLowerCase().includes(lower) ?? false) || c.childNativeId.toLowerCase().includes(lower));
27939
+ }
27940
+ if (adoptedOnly === true) filtered = filtered.filter((c) => c.alreadyAdopted);
27941
+ if (unadoptedOnly === true) filtered = filtered.filter((c) => !c.alreadyAdopted);
27942
+ return filtered;
27943
+ }
27944
+ return {
27945
+ listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
27946
+ listCandidates: async ({ integrationId, page, pageSize, filterText }) => {
27947
+ const filtered = applyCandidateTextFilter(await candidatesForIntegration(integrationId), filterText);
27948
+ const start = (page - 1) * pageSize;
27949
+ return {
27950
+ candidates: filtered.slice(start, start + pageSize),
27951
+ totalCount: filtered.length,
27952
+ page,
27953
+ pageSize
27954
+ };
27955
+ },
27956
+ getCandidate: async ({ integrationId, childNativeId }) => {
27957
+ return (await candidatesForIntegration(integrationId)).find((c) => c.childNativeId === childNativeId) ?? null;
27958
+ },
27959
+ getStatus: async () => {
27960
+ try {
27961
+ let candidateCount = 0;
27962
+ for (const integrationId of listIntegrations()) candidateCount += (await candidatesForIntegration(integrationId)).length;
27963
+ const adoptedCount = (await listAdopted()).length;
27964
+ return {
27965
+ lastDiscoveryAt: Date.now(),
27966
+ candidateCount,
27967
+ adoptedCount,
27968
+ lastError: null
27969
+ };
27970
+ } catch (err) {
27971
+ logger.warn("wyze adoption: getStatus failed", { meta: { error: errMsg(err) } });
27972
+ return {
27973
+ lastDiscoveryAt: null,
27974
+ candidateCount: 0,
27975
+ adoptedCount: 0,
27976
+ lastError: errMsg(err)
27977
+ };
27978
+ }
27979
+ },
27980
+ refresh: async ({ integrationId }) => {
27981
+ const candidateCount = (await candidatesForIntegration(integrationId, true)).length;
27982
+ const adoptedCount = adoptedMapForIntegration(integrationId, await listAdopted()).size;
27983
+ return {
27984
+ lastDiscoveryAt: Date.now(),
27985
+ candidateCount,
27986
+ adoptedCount,
27987
+ lastError: null
27988
+ };
27989
+ },
27990
+ adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
27991
+ if (!hasIntegration(integrationId)) throw new Error(`wyze adopt: integration ${integrationId} not connected`);
27992
+ const cameras = await getCameraList(integrationId, true);
27993
+ const adopted = [];
27994
+ let failures = 0;
27995
+ for (const mac of childNativeIds) try {
27996
+ const cam = cameras.find((c) => c.mac.toUpperCase() === mac.toUpperCase());
27997
+ if (cam === void 0) {
27998
+ logger.warn("wyze adopt: camera not found on account — skipping", { meta: {
27999
+ mac,
28000
+ integrationId
28001
+ } });
28002
+ failures++;
28003
+ continue;
28004
+ }
28005
+ const name = perCandidate?.[mac]?.name ?? cam.nickname ?? cam.productModel ?? mac;
28006
+ const { deviceId } = await adoptCamera({
28007
+ integrationId,
28008
+ mac: cam.mac,
28009
+ name
28010
+ });
28011
+ adopted.push({
28012
+ childNativeId: mac,
28013
+ parentDeviceId: deviceId,
28014
+ accessoryDeviceIds: []
28015
+ });
28016
+ } catch (err) {
28017
+ logger.warn("wyze adopt: failed to adopt camera", { meta: {
28018
+ mac,
28019
+ integrationId,
28020
+ error: errMsg(err)
28021
+ } });
28022
+ failures++;
28023
+ }
28024
+ if (adopted.length === 0 && failures > 0) throw new Error(`wyze adopt: all ${failures} adopt(s) failed`);
28025
+ return { adopted };
28026
+ },
28027
+ release: async ({ camDeviceId }) => {
28028
+ await removeDevice(camDeviceId);
28029
+ },
28030
+ resync: async ({ camDeviceId }) => {
28031
+ const cfg = await findDeviceConfig(camDeviceId);
28032
+ if (cfg === null) throw new Error(`wyze resync: device ${camDeviceId} not found`);
28033
+ const integrationId = typeof cfg["integrationId"] === "string" ? cfg["integrationId"] : "";
28034
+ const mac = typeof cfg["mac"] === "string" ? cfg["mac"] : "";
28035
+ if (mac.length === 0) throw new Error(`wyze resync: device ${camDeviceId} has no MAC`);
28036
+ if (integrationId.length > 0 && hasIntegration(integrationId)) {
28037
+ if (!(await getCameraList(integrationId)).some((c) => c.mac.toUpperCase() === mac.toUpperCase())) throw new Error(`wyze resync: camera ${mac} no longer present on integration ${integrationId}`);
28038
+ }
28039
+ return {
28040
+ changed: false,
28041
+ rebuiltChildren: 0
28042
+ };
28043
+ }
28044
+ };
28045
+ }
28046
+ //#endregion
27587
28047
  //#region src/snapshot-ffmpeg.ts
27588
28048
  /**
27589
28049
  * Decode a raw Annex-B keyframe (H264 or H265) into a single JPEG via
@@ -28257,34 +28717,56 @@ var WyzeCamera = class extends BaseDevice {
28257
28717
  };
28258
28718
  //#endregion
28259
28719
  //#region src/addon.ts
28260
- var CAMERA_LIST_REFRESH_MS = 5 * 6e4;
28261
28720
  function getString(obj, key) {
28262
28721
  const v = obj[key];
28263
28722
  return typeof v === "string" ? v : "";
28264
28723
  }
28724
+ /** Extract account credentials from an integration's settings, or null. */
28725
+ function credentialsFromSettings(settings) {
28726
+ const email = getString(settings, "email").trim();
28727
+ const password = getString(settings, "password");
28728
+ const apiKey = getString(settings, "apiKey").trim();
28729
+ const apiId = getString(settings, "apiId").trim();
28730
+ if (!email || !password || !apiKey || !apiId) return null;
28731
+ return {
28732
+ email,
28733
+ password,
28734
+ apiKey,
28735
+ apiId
28736
+ };
28737
+ }
28265
28738
  /**
28266
- * Wyze device-provider addon. Discovers cameras via the Wyze cloud
28267
- * (`getCameraList`), persists each camera's P2P parameters, and feeds the
28268
- * stream-broker through the `pull-rfc4571` lazy-publish + dial-time
28269
- * `materializeStreamSocket` path structurally identical to Reolink.
28739
+ * Wyze device-provider addon `mode: account` (multi-account).
28740
+ *
28741
+ * Each Wyze integration carries its own account credentials in its
28742
+ * `integration.settings`; the addon holds one `WyzeCloudClient` per
28743
+ * `integrationId` in {@link WyzeClientRegistry} (`Map<integrationId,
28744
+ * WyzeCloudClient>`). There is NO shared broker. A `device-adoption` cap
28745
+ * provider enumerates each account's cameras and adopts them; each adopted
28746
+ * camera is stamped with its `integrationId` and resolves its account client
28747
+ * by that id.
28748
+ *
28749
+ * Cameras still feed the stream-broker through the `pull-rfc4571`
28750
+ * lazy-publish + dial-time `materializeStreamSocket` path — structurally
28751
+ * identical to Reolink.
28270
28752
  *
28271
28753
  * `hub-only`: the loopback RFC 4571 server binds 127.0.0.1, so the broker
28272
- * that dials it must be co-resident (same as every other camstack
28273
- * provider).
28754
+ * that dials it must be co-resident.
28274
28755
  */
28275
28756
  var WyzeProviderAddon = class extends BaseDeviceProvider {
28276
28757
  addonId = WYZE_ADDON_ID;
28277
28758
  providerName = "Wyze";
28278
28759
  deviceClasses = { [DeviceType.Camera]: WyzeCamera };
28279
- cloud = null;
28280
- cameraListCache = [];
28281
- cameraListFetchedAt = 0;
28282
- cameraListInFlight = null;
28760
+ clients = null;
28283
28761
  constructor() {
28284
28762
  super({});
28285
28763
  }
28286
28764
  async onInitialize() {
28287
28765
  const regs = await super.onInitialize();
28766
+ this.clients = new WyzeClientRegistry({
28767
+ dataDir: this.ctx.dataDir,
28768
+ logger: this.ctx.logger
28769
+ });
28288
28770
  this.subscribe({ category: EventCategory.StreamBrokerOnRequestStreamSourceRefresh }, (event) => {
28289
28771
  const data = event.data;
28290
28772
  const deviceId = typeof data.deviceId === "number" ? data.deviceId : null;
@@ -28294,15 +28776,68 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
28294
28776
  tags: { deviceId },
28295
28777
  meta: {
28296
28778
  camStreamId: data.camStreamId ?? null,
28297
- error: err instanceof Error ? err.message : String(err)
28779
+ error: errMsg(err)
28298
28780
  }
28299
28781
  });
28300
28782
  });
28301
28783
  });
28302
- return regs;
28784
+ await this.reconcileIntegrations();
28785
+ this.subscribeIntegrationLifecycle();
28786
+ return [...regs, {
28787
+ capability: deviceAdoptionCapability,
28788
+ provider: this.buildAdoptionProvider()
28789
+ }];
28790
+ }
28791
+ async onShutdown() {
28792
+ this.clients?.clear();
28793
+ this.clients = null;
28794
+ await super.onShutdown();
28795
+ }
28796
+ requireClients() {
28797
+ if (!this.clients) throw new Error("Wyze provider not initialised");
28798
+ return this.clients;
28799
+ }
28800
+ /**
28801
+ * Rebuild the `Map<integrationId, WyzeCloudClient>` from the live
28802
+ * integration list: for each surviving Wyze integration read its settings
28803
+ * and upsert a client (session-preserving if credentials are unchanged);
28804
+ * drop clients whose integration was deleted/disabled. Idempotent; runs on
28805
+ * boot + on every integration lifecycle event. Guarded so a failure never
28806
+ * fails init.
28807
+ */
28808
+ async reconcileIntegrations() {
28809
+ const reg = this.requireClients();
28810
+ try {
28811
+ const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
28812
+ const surviving = /* @__PURE__ */ new Set();
28813
+ for (const integration of mine) try {
28814
+ const credentials = credentialsFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
28815
+ if (!credentials) {
28816
+ this.ctx.logger.warn("Wyze integration has no complete credentials — skipping", { meta: { integrationId: integration.id } });
28817
+ continue;
28818
+ }
28819
+ reg.upsert(integration.id, credentials);
28820
+ surviving.add(integration.id);
28821
+ } catch (err) {
28822
+ this.ctx.logger.warn("Wyze reconcile: failed to read integration settings", { meta: {
28823
+ integrationId: integration.id,
28824
+ error: errMsg(err)
28825
+ } });
28826
+ }
28827
+ reg.retain(surviving);
28828
+ } catch (err) {
28829
+ this.ctx.logger.warn("Wyze integration reconcile failed", { meta: { error: errMsg(err) } });
28830
+ }
28303
28831
  }
28304
- async onConfigChanged() {
28305
- this.cloud = null;
28832
+ subscribeIntegrationLifecycle() {
28833
+ const handler = (event) => {
28834
+ const addonId = event.data["addonId"];
28835
+ if (typeof addonId === "string" && addonId !== this.ctx.id) return;
28836
+ this.reconcileIntegrations();
28837
+ };
28838
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
28839
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
28840
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
28306
28841
  }
28307
28842
  async refreshDeviceStream(deviceId, camStreamId) {
28308
28843
  const dev = this.ctx.kernel.deviceRegistry?.getById(deviceId);
@@ -28314,94 +28849,57 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
28314
28849
  this.wireCameraDeps(dev);
28315
28850
  await dev.materializeStreamSocket(camStreamId);
28316
28851
  }
28317
- async getGlobalSettings() {
28318
- const raw = await this.ctx.settings?.readAddonStore() ?? {};
28319
- return hydrateSchema(buildSettingsSchema(), raw);
28320
- }
28321
- async updateGlobalSettings(patch) {
28322
- await this.ctx.settings?.writeAddonStore(patch);
28323
- await this.onConfigChanged();
28324
- }
28325
- async readCredentials() {
28326
- const raw = await this.ctx.settings?.readAddonStore() ?? {};
28327
- const email = getString(raw, "email").trim();
28328
- const password = getString(raw, "password");
28329
- const apiKey = getString(raw, "apiKey").trim();
28330
- const apiId = getString(raw, "apiId").trim();
28331
- if (!email || !password || !apiKey || !apiId) return null;
28332
- return {
28333
- email,
28334
- password,
28335
- apiKey,
28336
- apiId
28337
- };
28338
- }
28339
- /** Lazily build the shared cloud client from persisted credentials. */
28340
- async ensureCloud() {
28341
- if (this.cloud) return this.cloud;
28342
- const credentials = await this.readCredentials();
28343
- if (!credentials) {
28344
- this.ctx.logger.warn("Wyze credentials not configured — set email / password / API key / Key ID in the addon settings");
28345
- return null;
28346
- }
28347
- this.cloud = new WyzeCloudClient({
28348
- dataDir: this.ctx.dataDir,
28349
- logger: this.ctx.logger.child("cloud"),
28350
- credentials
28351
- });
28352
- return this.cloud;
28353
- }
28354
- /** Cloud camera list, cached + debounced to avoid auth rate-limits. */
28355
- async getCameraList(force = false) {
28356
- const fresh = Date.now() - this.cameraListFetchedAt < CAMERA_LIST_REFRESH_MS;
28357
- if (!force && fresh && this.cameraListCache.length > 0) return this.cameraListCache;
28358
- if (this.cameraListInFlight) return this.cameraListInFlight;
28359
- const cloud = await this.ensureCloud();
28360
- if (!cloud) return this.cameraListCache;
28361
- const run = cloud.getCameraList().then((cams) => {
28362
- this.cameraListCache = cams;
28363
- this.cameraListFetchedAt = Date.now();
28364
- return cams;
28365
- }).catch((err) => {
28366
- const classified = classifyCloudError(err);
28367
- this.ctx.logger.error("Wyze getCameraList failed", { meta: {
28368
- kind: classified.kind,
28369
- error: classified.message
28370
- } });
28371
- return this.cameraListCache;
28372
- }).finally(() => {
28373
- this.cameraListInFlight = null;
28374
- });
28375
- this.cameraListInFlight = run;
28376
- return run;
28377
- }
28378
- async supportsDiscovery() {
28379
- return true;
28380
- }
28381
- async discoverDevices() {
28382
- const cams = await this.getCameraList(true);
28383
- const candidates = [];
28384
- for (const cam of cams) {
28385
- if (!isAdoptableCamera(cam)) continue;
28386
- candidates.push({
28387
- stableId: stableIdForMac(cam.mac),
28388
- type: DeviceType.Camera,
28389
- suggestedName: cam.nickname || cam.productModel || cam.mac,
28390
- prefilledConfig: cameraConfigFromCloud(cam)
28391
- });
28392
- }
28393
- return candidates;
28394
- }
28395
- async adoptDiscoveredDevice(input) {
28396
- const config = wyzeCameraSchema.parse(input.candidate.prefilledConfig);
28397
- return this.createDevice({
28398
- type: DeviceType.Camera,
28399
- config: {
28400
- ...config,
28401
- name: input.candidate.suggestedName
28852
+ buildAdoptionProvider() {
28853
+ return buildWyzeAdoptionProvider({
28854
+ logger: this.ctx.logger,
28855
+ getCameraList: (integrationId, force) => this.requireClients().getCameraList(integrationId, force),
28856
+ hasIntegration: (integrationId) => this.requireClients().has(integrationId),
28857
+ listIntegrations: () => this.requireClients().list(),
28858
+ listAdopted: async () => {
28859
+ const reg = this.ctx.kernel.deviceRegistry;
28860
+ const devices = this.ctx.kernel.devices;
28861
+ if (!reg || !devices) return [];
28862
+ const out = [];
28863
+ for (const d of reg.getAllForAddon(this.addonId)) {
28864
+ if (d.parentDeviceId !== null) continue;
28865
+ const config = await devices.loadConfig(d.id).catch(() => ({}));
28866
+ out.push({
28867
+ id: d.id,
28868
+ config
28869
+ });
28870
+ }
28871
+ return out;
28872
+ },
28873
+ adoptCamera: async ({ integrationId, mac, name }) => {
28874
+ const cam = (await this.requireClients().getCameraList(integrationId)).find((c) => c.mac.toUpperCase() === mac.toUpperCase());
28875
+ if (!cam) throw new Error(`wyze adopt: camera ${mac} not found on integration ${integrationId}`);
28876
+ const config = {
28877
+ ...cameraConfigFromCloud(cam, integrationId),
28878
+ name
28879
+ };
28880
+ const parsed = wyzeCameraSchema.parse(config);
28881
+ return { deviceId: (await this.createDevice({
28882
+ type: DeviceType.Camera,
28883
+ config: {
28884
+ ...parsed,
28885
+ name,
28886
+ integrationId
28887
+ }
28888
+ })).id };
28889
+ },
28890
+ removeDevice: async (id) => {
28891
+ await this.ctx.kernel.devices?.remove(id);
28892
+ },
28893
+ findDeviceConfig: async (id) => {
28894
+ const devices = this.ctx.kernel.devices;
28895
+ if (!devices) return null;
28896
+ return devices.loadConfig(id).catch(() => null);
28402
28897
  }
28403
28898
  });
28404
28899
  }
28900
+ async getGlobalSettings() {
28901
+ return hydrateSchema(buildSettingsSchema(), {});
28902
+ }
28405
28903
  async onGetCreationSchema(type) {
28406
28904
  if (type !== DeviceType.Camera) return null;
28407
28905
  return buildCreationFormSchema();
@@ -28410,20 +28908,25 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
28410
28908
  if (type !== DeviceType.Camera) throw new Error(`Wyze provider does not support device type: ${type}`);
28411
28909
  const name = getString(config, "name").trim();
28412
28910
  if (!name) throw new Error("Camera name is required");
28911
+ const integrationId = getString(config, "integrationId").trim();
28413
28912
  const mac = getString(config, "mac").trim();
28414
28913
  let merged = config;
28415
- if (mac) {
28416
- const fromCloud = (await this.getCameraList()).find((c) => c.mac.toUpperCase() === mac.toUpperCase());
28914
+ if (mac && integrationId) {
28915
+ const fromCloud = (await this.requireClients().getCameraList(integrationId)).find((c) => c.mac.toUpperCase() === mac.toUpperCase());
28417
28916
  if (fromCloud) merged = {
28418
28917
  ...config,
28419
- ...cameraConfigFromCloud(fromCloud)
28918
+ ...cameraConfigFromCloud(fromCloud, integrationId)
28420
28919
  };
28421
28920
  }
28422
- const parsed = wyzeCameraSchema.parse(merged);
28921
+ const parsed = wyzeCameraSchema.parse({
28922
+ ...merged,
28923
+ integrationId
28924
+ });
28423
28925
  return {
28424
28926
  meta: {
28425
28927
  type: DeviceType.Camera,
28426
- name
28928
+ name,
28929
+ ...integrationId ? { integrationId } : {}
28427
28930
  },
28428
28931
  config: parsed,
28429
28932
  onAfterCreate: async (device) => {
@@ -28437,16 +28940,19 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
28437
28940
  throw new Error("Wyze: cannot persist a camera without a MAC address");
28438
28941
  }
28439
28942
  /**
28440
- * Inject the cloud resolvers into a camera so it can (a) refresh its
28441
- * local IP before each dial and (b) poll cloud events for motion +
28442
- * thumbnails — without holding a direct provider reference.
28943
+ * Inject the per-account cloud resolvers into a camera so it can (a) refresh
28944
+ * its local IP before each dial and (b) poll cloud events for motion +
28945
+ * thumbnails — resolving its account {@link WyzeCloudClient} by the camera's
28946
+ * own `integrationId` config, without holding a direct provider reference.
28443
28947
  */
28444
28948
  wireCameraDeps(camera) {
28445
28949
  const mac = camera.config.get("mac");
28950
+ const integrationId = camera.config.get("integrationId");
28446
28951
  camera.setDeps({
28447
- cloud: () => this.cloud,
28952
+ cloud: () => this.clients?.client(integrationId) ?? null,
28448
28953
  resolveCloudCamera: async () => {
28449
- return (await this.getCameraList()).find((c) => c.mac.toUpperCase() === mac.toUpperCase()) ?? null;
28954
+ if (!integrationId) return null;
28955
+ return (await this.requireClients().getCameraList(integrationId)).find((c) => c.mac.toUpperCase() === mac.toUpperCase()) ?? null;
28450
28956
  }
28451
28957
  });
28452
28958
  }
@@ -28458,5 +28964,8 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
28458
28964
  };
28459
28965
  //#endregion
28460
28966
  exports.WyzeCamera = WyzeCamera;
28967
+ exports.WyzeClientRegistry = WyzeClientRegistry;
28461
28968
  exports.WyzeProviderAddon = WyzeProviderAddon;
28969
+ exports.buildWyzeAdoptionProvider = buildWyzeAdoptionProvider;
28970
+ exports.buildWyzeCandidates = buildWyzeCandidates;
28462
28971
  exports.wyzeCameraSchema = wyzeCameraSchema;