@camstack/addon-provider-wyze 0.2.19 → 0.2.23
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 +245 -37
- package/dist/addon.mjs +245 -37
- package/package.json +4 -1
package/dist/addon.js
CHANGED
|
@@ -30,7 +30,8 @@ let events = require("events");
|
|
|
30
30
|
let net = require("net");
|
|
31
31
|
net = __toESM(net, 1);
|
|
32
32
|
let node_child_process = require("node:child_process");
|
|
33
|
-
|
|
33
|
+
let node_tls = require("node:tls");
|
|
34
|
+
//#region ../types/dist/event-category-C0lyLd5U.mjs
|
|
34
35
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
35
36
|
EventCategory["SystemBoot"] = "system.boot";
|
|
36
37
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -47,9 +48,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
47
48
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
48
49
|
/**
|
|
49
50
|
* A newer addon or server-root package version was found by the
|
|
50
|
-
* authoritative registry check. Emitted once
|
|
51
|
-
* `(
|
|
52
|
-
*
|
|
51
|
+
* authoritative registry check. Emitted once when any observed
|
|
52
|
+
* `latestVersion` changes (or a package/node first appears behind);
|
|
53
|
+
* the payload carries the full currently-available list. Repeated
|
|
54
|
+
* polling of the same latests is silent.
|
|
53
55
|
*/
|
|
54
56
|
EventCategory["UpdateAvailable"] = "update.available";
|
|
55
57
|
/**
|
|
@@ -7579,6 +7581,10 @@ var RecordingBandSchema = object({
|
|
|
7579
7581
|
preBufferSec: number().min(0).optional(),
|
|
7580
7582
|
postBufferSec: number().min(0).optional()
|
|
7581
7583
|
});
|
|
7584
|
+
({
|
|
7585
|
+
preBufferSec: 10,
|
|
7586
|
+
postBufferSec: 30
|
|
7587
|
+
}).postBufferSec * 1e3;
|
|
7582
7588
|
/**
|
|
7583
7589
|
* Per-device retention overrides. Every field is optional; an unset or `0`
|
|
7584
7590
|
* value inherits the node-wide recorder default. Only footage-lifetime limits
|
|
@@ -14595,6 +14601,9 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14595
14601
|
"alarm-disarmed",
|
|
14596
14602
|
"alarm-arming",
|
|
14597
14603
|
"alarm-arm-refused",
|
|
14604
|
+
"addon-updated",
|
|
14605
|
+
"server-updated",
|
|
14606
|
+
"export-completed",
|
|
14598
14607
|
"camera-online",
|
|
14599
14608
|
"camera-offline",
|
|
14600
14609
|
"camera-disabled",
|
|
@@ -16488,13 +16497,19 @@ var RetrainStatusSchema = _enum([
|
|
|
16488
16497
|
* "never marked" from "already trained" must read `retrainStatus`.
|
|
16489
16498
|
*
|
|
16490
16499
|
* `debug` does NOT pin; it is attention, not durability.
|
|
16500
|
+
*
|
|
16501
|
+
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
16502
|
+
* A favourited track is skipped by retention the same way `staging` is, but
|
|
16503
|
+
* it does not enter `none|staging|trained` and has no staging budget.
|
|
16491
16504
|
*/
|
|
16492
16505
|
var TrackFlagFields = {
|
|
16493
16506
|
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
16494
16507
|
* `'staging'`. */
|
|
16495
16508
|
markForTrain: boolean().optional(),
|
|
16496
16509
|
/** Operator marked this track for diagnostic attention. */
|
|
16497
|
-
debug: boolean().optional()
|
|
16510
|
+
debug: boolean().optional(),
|
|
16511
|
+
/** Operator favourited this track. Pins it against pruning. */
|
|
16512
|
+
favourited: boolean().optional()
|
|
16498
16513
|
};
|
|
16499
16514
|
/**
|
|
16500
16515
|
* The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
|
|
@@ -16519,6 +16534,7 @@ var TrackFlagsSchema = object({
|
|
|
16519
16534
|
trackId: string(),
|
|
16520
16535
|
markForTrain: boolean(),
|
|
16521
16536
|
debug: boolean(),
|
|
16537
|
+
favourited: boolean(),
|
|
16522
16538
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
16523
16539
|
* a track row) because this shape is only ever produced by the write body,
|
|
16524
16540
|
* which always knows it — and a surface that has just written needs to render
|
|
@@ -20315,8 +20331,28 @@ var ClipSchema = object({
|
|
|
20315
20331
|
startMs: number(),
|
|
20316
20332
|
endMs: number()
|
|
20317
20333
|
}),
|
|
20318
|
-
/**
|
|
20319
|
-
thumbnail
|
|
20334
|
+
/**
|
|
20335
|
+
* Lazy thumbnail URL, never inlined.
|
|
20336
|
+
*
|
|
20337
|
+
* Recording-derived clips (events-mode keep-window, and the prepared
|
|
20338
|
+
* continuous event+fragment visit) MUST use the snapshot of the **main
|
|
20339
|
+
* event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
|
|
20340
|
+
* of the event that owns `kind` (object > motion > audio). Do not extract
|
|
20341
|
+
* a keyframe from the recorded segments. Other providers (onboard, HKSV)
|
|
20342
|
+
* mint their own stills.
|
|
20343
|
+
*/
|
|
20344
|
+
thumbnail: string().optional(),
|
|
20345
|
+
/** Analytics event ids that overlap this visit. Empty on footage-only clips.
|
|
20346
|
+
* The default provider's visit grain puts many motion heartbeats on one clip
|
|
20347
|
+
* instead of minting one clip per marker. */
|
|
20348
|
+
eventIds: array(string()).optional(),
|
|
20349
|
+
/** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
|
|
20350
|
+
* than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
|
|
20351
|
+
* bar keeps showing them via `recording.getAvailability`. */
|
|
20352
|
+
holes: array(object({
|
|
20353
|
+
startMs: number(),
|
|
20354
|
+
endMs: number()
|
|
20355
|
+
})).optional()
|
|
20320
20356
|
});
|
|
20321
20357
|
var ClipPlaybackSchema = object({
|
|
20322
20358
|
/** HLS master URL through the hub data-plane (Range + token in path). */
|
|
@@ -21891,10 +21927,18 @@ settings: record(string(), unknown()) });
|
|
|
21891
21927
|
* descriptive — it never changes routing.
|
|
21892
21928
|
*/
|
|
21893
21929
|
var ConnectionTestDescriptorSchema = object({ label: string() });
|
|
21894
|
-
|
|
21895
|
-
|
|
21896
|
-
|
|
21897
|
-
|
|
21930
|
+
var connectionTestCapability = {
|
|
21931
|
+
name: "connection-test",
|
|
21932
|
+
scope: "system",
|
|
21933
|
+
mode: "collection",
|
|
21934
|
+
methods: {
|
|
21935
|
+
testSettings: method(ConnectionTestInputSchema, ConnectionTestOutcomeSchema, {
|
|
21936
|
+
kind: "mutation",
|
|
21937
|
+
auth: "admin"
|
|
21938
|
+
}),
|
|
21939
|
+
describeTest: method(_void(), ConnectionTestDescriptorSchema, { auth: "admin" })
|
|
21940
|
+
}
|
|
21941
|
+
};
|
|
21898
21942
|
/**
|
|
21899
21943
|
* Upstream-system connectivity sensor — distinct from `device-status`,
|
|
21900
21944
|
* which is the kernel-managed online/offline flag for the device's
|
|
@@ -26602,7 +26646,9 @@ var ExportOptionsSchema = object({
|
|
|
26602
26646
|
includeAudio: boolean(),
|
|
26603
26647
|
maxLifeMs: number().int().positive(),
|
|
26604
26648
|
deleteAfterDownload: boolean(),
|
|
26605
|
-
title: string().max(200).optional()
|
|
26649
|
+
title: string().max(200).optional(),
|
|
26650
|
+
/** Notification-output target ids to ping when this export becomes ready. */
|
|
26651
|
+
notifyTargetIds: array(string().min(1)).max(20).optional()
|
|
26606
26652
|
}).superRefine((v, ctx) => {
|
|
26607
26653
|
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
26608
26654
|
code: ZodIssueCode.custom,
|
|
@@ -26661,10 +26707,18 @@ var ExportBytesSchema = object({
|
|
|
26661
26707
|
});
|
|
26662
26708
|
method(object({
|
|
26663
26709
|
deviceId: number(),
|
|
26664
|
-
|
|
26710
|
+
/** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
|
|
26711
|
+
profile: string().optional(),
|
|
26712
|
+
profiles: array(string()).min(1).optional(),
|
|
26665
26713
|
fromMs: number(),
|
|
26666
26714
|
toMs: number(),
|
|
26667
26715
|
options: ExportOptionsSchema
|
|
26716
|
+
}).superRefine((v, ctx) => {
|
|
26717
|
+
if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
|
|
26718
|
+
code: ZodIssueCode.custom,
|
|
26719
|
+
message: "pass profiles[] (min 1) or legacy profile",
|
|
26720
|
+
path: ["profiles"]
|
|
26721
|
+
});
|
|
26668
26722
|
}), ExportRecordSchema, {
|
|
26669
26723
|
kind: "mutation",
|
|
26670
26724
|
auth: "protected"
|
|
@@ -29606,15 +29660,6 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
29606
29660
|
labels: ["probe not implemented"]
|
|
29607
29661
|
};
|
|
29608
29662
|
}
|
|
29609
|
-
/**
|
|
29610
|
-
* Top-level devices restored at once in {@link onRestoreDevices}.
|
|
29611
|
-
*
|
|
29612
|
-
* Four covers the fleets this ships to without turning a boot into a burst a
|
|
29613
|
-
* camera NVR answers with a refusal. A provider whose upstream is a single
|
|
29614
|
-
* session with a serial command channel (a Baichuan hub, an NVR that
|
|
29615
|
-
* serialises ISAPI) should lower it; nothing needs to raise it.
|
|
29616
|
-
*/
|
|
29617
|
-
restoreConcurrency = 4;
|
|
29618
29663
|
async restoreDevices(savedDevices) {
|
|
29619
29664
|
await this.onRestoreDevices(savedDevices);
|
|
29620
29665
|
if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
|
|
@@ -29669,14 +29714,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
29669
29714
|
});
|
|
29670
29715
|
}
|
|
29671
29716
|
};
|
|
29672
|
-
|
|
29673
|
-
await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
|
|
29674
|
-
for (;;) {
|
|
29675
|
-
const saved = topLevel[nextTopLevel++];
|
|
29676
|
-
if (saved === void 0) return;
|
|
29677
|
-
await restoreOne(saved);
|
|
29678
|
-
}
|
|
29679
|
-
}));
|
|
29717
|
+
await Promise.all(topLevel.map((saved) => restoreOne(saved)));
|
|
29680
29718
|
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
29681
29719
|
for (const saved of childRows) {
|
|
29682
29720
|
const Class = this.deviceClasses[saved.type];
|
|
@@ -40631,7 +40669,8 @@ var WyzeClientRegistry = class {
|
|
|
40631
40669
|
credentials,
|
|
40632
40670
|
cameraListCache: [],
|
|
40633
40671
|
cameraListFetchedAt: 0,
|
|
40634
|
-
cameraListInFlight: null
|
|
40672
|
+
cameraListInFlight: null,
|
|
40673
|
+
lastFetchError: null
|
|
40635
40674
|
});
|
|
40636
40675
|
}
|
|
40637
40676
|
/** Drop the client for an integration that no longer exists / was disabled. */
|
|
@@ -40659,6 +40698,14 @@ var WyzeClientRegistry = class {
|
|
|
40659
40698
|
this.#entries.clear();
|
|
40660
40699
|
}
|
|
40661
40700
|
/**
|
|
40701
|
+
* The last classified error from a `getCameraList` fetch failure for an
|
|
40702
|
+
* integration, or null when the last fetch succeeded (or no fetch has run
|
|
40703
|
+
* yet). Used by `getStatus` to surface the reason for an empty adoption list.
|
|
40704
|
+
*/
|
|
40705
|
+
getLastFetchError(integrationId) {
|
|
40706
|
+
return this.#entries.get(integrationId)?.lastFetchError ?? null;
|
|
40707
|
+
}
|
|
40708
|
+
/**
|
|
40662
40709
|
* Cloud camera list for ONE integration, cached + debounced + single-flight
|
|
40663
40710
|
* to avoid auth rate-limits. Returns the last cache (possibly empty) when the
|
|
40664
40711
|
* integration is unknown or the fetch fails.
|
|
@@ -40672,9 +40719,11 @@ var WyzeClientRegistry = class {
|
|
|
40672
40719
|
const run = entry.client.getCameraList().then((cams) => {
|
|
40673
40720
|
entry.cameraListCache = cams;
|
|
40674
40721
|
entry.cameraListFetchedAt = Date.now();
|
|
40722
|
+
entry.lastFetchError = null;
|
|
40675
40723
|
return cams;
|
|
40676
40724
|
}).catch((err) => {
|
|
40677
40725
|
const classified = classifyCloudError(err);
|
|
40726
|
+
entry.lastFetchError = classified;
|
|
40678
40727
|
this.#deps.logger.error("Wyze getCameraList failed", { meta: {
|
|
40679
40728
|
integrationId,
|
|
40680
40729
|
kind: classified.kind,
|
|
@@ -41170,11 +41219,16 @@ function buildWyzeAdoptionProvider(deps) {
|
|
|
41170
41219
|
let candidateCount = 0;
|
|
41171
41220
|
for (const integrationId of listIntegrations()) candidateCount += (await candidatesForIntegration(integrationId)).length;
|
|
41172
41221
|
const adoptedCount = (await listAdopted()).length;
|
|
41222
|
+
const fetchErrors = [];
|
|
41223
|
+
if (deps.getLastFetchError) for (const integrationId of listIntegrations()) {
|
|
41224
|
+
const err = deps.getLastFetchError(integrationId);
|
|
41225
|
+
if (err) fetchErrors.push(`[${integrationId}] ${err.message}`);
|
|
41226
|
+
}
|
|
41173
41227
|
return {
|
|
41174
41228
|
lastDiscoveryAt: Date.now(),
|
|
41175
41229
|
candidateCount,
|
|
41176
41230
|
adoptedCount,
|
|
41177
|
-
lastError: null
|
|
41231
|
+
lastError: fetchErrors.length > 0 ? fetchErrors.join("; ") : null
|
|
41178
41232
|
};
|
|
41179
41233
|
} catch (err) {
|
|
41180
41234
|
logger.warn("wyze adoption: getStatus failed", { meta: { error: errMsg(err) } });
|
|
@@ -41189,11 +41243,12 @@ function buildWyzeAdoptionProvider(deps) {
|
|
|
41189
41243
|
refresh: async ({ integrationId }) => {
|
|
41190
41244
|
const candidateCount = (await candidatesForIntegration(integrationId, true)).length;
|
|
41191
41245
|
const adoptedCount = adoptedMapForIntegration(integrationId, await listAdopted()).size;
|
|
41246
|
+
const fetchErr = deps.getLastFetchError?.(integrationId);
|
|
41192
41247
|
return {
|
|
41193
41248
|
lastDiscoveryAt: Date.now(),
|
|
41194
41249
|
candidateCount,
|
|
41195
41250
|
adoptedCount,
|
|
41196
|
-
lastError: null
|
|
41251
|
+
lastError: fetchErr ? fetchErr.message : null
|
|
41197
41252
|
};
|
|
41198
41253
|
},
|
|
41199
41254
|
adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
|
|
@@ -41925,6 +41980,139 @@ var WyzeCamera = class extends BaseDevice {
|
|
|
41925
41980
|
static fromCloud = cameraConfigFromCloud;
|
|
41926
41981
|
};
|
|
41927
41982
|
//#endregion
|
|
41983
|
+
//#region src/wyze-connection-test.ts
|
|
41984
|
+
/**
|
|
41985
|
+
* Production default: creates a `WyzeCloud` with no-op session hooks so
|
|
41986
|
+
* `testSettings` never writes or reads `wyze-session.json`.
|
|
41987
|
+
*/
|
|
41988
|
+
function defaultWyzeCloudFacadeFactory(credentials) {
|
|
41989
|
+
const cloud = new WyzeCloud({
|
|
41990
|
+
apiKey: credentials.apiKey,
|
|
41991
|
+
apiId: credentials.apiId,
|
|
41992
|
+
loadSession: () => null,
|
|
41993
|
+
saveSession: () => void 0,
|
|
41994
|
+
clearSession: () => void 0
|
|
41995
|
+
});
|
|
41996
|
+
return {
|
|
41997
|
+
ensureSession: (email, password) => cloud.ensureSession(email, password),
|
|
41998
|
+
getCameraList: () => cloud.getCameraList()
|
|
41999
|
+
};
|
|
42000
|
+
}
|
|
42001
|
+
var wyzeCredentialsSchema = object({
|
|
42002
|
+
email: string().min(1).describe("Wyze account email"),
|
|
42003
|
+
password: string().min(1).describe("Wyze account password"),
|
|
42004
|
+
apiKey: string().min(1).describe("Wyze developer API key"),
|
|
42005
|
+
apiId: string().min(1).describe("Wyze developer Key ID")
|
|
42006
|
+
});
|
|
42007
|
+
var TEST_LABEL = "Signs in to the Wyze cloud with these account credentials";
|
|
42008
|
+
function buildWyzeConnectionTestProvider(deps) {
|
|
42009
|
+
const { makeCloud, logger } = deps;
|
|
42010
|
+
return {
|
|
42011
|
+
describeTest: async () => ({ label: TEST_LABEL }),
|
|
42012
|
+
testSettings: async ({ settings }) => {
|
|
42013
|
+
const parsed = wyzeCredentialsSchema.safeParse(settings);
|
|
42014
|
+
if (!parsed.success) {
|
|
42015
|
+
const missing = parsed.error.issues.map((i) => i.path.join(".") || "(root)").join(", ");
|
|
42016
|
+
logger.warn("Wyze connection test: settings incomplete", { meta: { missing } });
|
|
42017
|
+
return {
|
|
42018
|
+
outcome: "rejected",
|
|
42019
|
+
error: `Wyze account settings are incomplete or invalid: ${missing}`
|
|
42020
|
+
};
|
|
42021
|
+
}
|
|
42022
|
+
const { email, password, apiKey, apiId } = parsed.data;
|
|
42023
|
+
const startedAt = Date.now();
|
|
42024
|
+
const cloud = makeCloud({
|
|
42025
|
+
email,
|
|
42026
|
+
password,
|
|
42027
|
+
apiKey,
|
|
42028
|
+
apiId
|
|
42029
|
+
});
|
|
42030
|
+
try {
|
|
42031
|
+
await cloud.ensureSession(email, password);
|
|
42032
|
+
let cameraCount = null;
|
|
42033
|
+
try {
|
|
42034
|
+
cameraCount = (await cloud.getCameraList()).length;
|
|
42035
|
+
} catch {}
|
|
42036
|
+
return {
|
|
42037
|
+
outcome: "validated",
|
|
42038
|
+
latencyMs: Date.now() - startedAt,
|
|
42039
|
+
...cameraCount !== null ? { detail: `Found ${cameraCount} camera${cameraCount === 1 ? "" : "s"} on this account` } : {}
|
|
42040
|
+
};
|
|
42041
|
+
} catch (err) {
|
|
42042
|
+
const classified = classifyCloudError(err);
|
|
42043
|
+
if (classified.kind === "mfa") {
|
|
42044
|
+
logger.warn("Wyze connection test: MFA required", { meta: { email } });
|
|
42045
|
+
return {
|
|
42046
|
+
outcome: "rejected",
|
|
42047
|
+
error: classified.message
|
|
42048
|
+
};
|
|
42049
|
+
}
|
|
42050
|
+
if (classified.kind === "credentials") {
|
|
42051
|
+
logger.warn("Wyze connection test: credentials refused", { meta: { email } });
|
|
42052
|
+
return {
|
|
42053
|
+
outcome: "rejected",
|
|
42054
|
+
error: "Wyze refused these credentials — check the email, password, API Key and Key ID. " + classified.message
|
|
42055
|
+
};
|
|
42056
|
+
}
|
|
42057
|
+
logger.warn("Wyze connection test: could not complete", { meta: {
|
|
42058
|
+
email,
|
|
42059
|
+
kind: classified.kind,
|
|
42060
|
+
error: errMsg(err)
|
|
42061
|
+
} });
|
|
42062
|
+
return {
|
|
42063
|
+
outcome: "inconclusive",
|
|
42064
|
+
error: classified.kind === "rate-limited" ? `Could not verify credentials — rate-limited by the Wyze cloud. Retry in ~30 s. ${classified.message}` : `Could not reach the Wyze cloud to verify these credentials: ${errMsg(err)}`
|
|
42065
|
+
};
|
|
42066
|
+
}
|
|
42067
|
+
}
|
|
42068
|
+
};
|
|
42069
|
+
}
|
|
42070
|
+
//#endregion
|
|
42071
|
+
//#region src/wyze-tls-trust.ts
|
|
42072
|
+
/**
|
|
42073
|
+
* Ubuntu 24.04's `ca-certificates` (2026-06) dropped DigiCert Global Root CA
|
|
42074
|
+
* (the 2006 SHA-1-era root, still valid until 2031). Wyze's `*.wyzecam.com`
|
|
42075
|
+
* leaf is issued by "DigiCert TLS RSA SHA256 2020 CA1", which chains to that
|
|
42076
|
+
* root. Node fetch then fails with `UNABLE_TO_GET_ISSUER_CERT_LOCALLY` and
|
|
42077
|
+
* the adoption panel shows an empty camera list.
|
|
42078
|
+
*
|
|
42079
|
+
* macOS still trusts this root in the system keychain, which is why the same
|
|
42080
|
+
* URL verifies on the developer Mac and fails inside the hub container.
|
|
42081
|
+
*
|
|
42082
|
+
* The PEM is DigiCert's public root (https://cacerts.digicert.com/DigiCertGlobalRootCA.crt.pem).
|
|
42083
|
+
* Applied once per addon process via `tls.setDefaultCACertificates` — does
|
|
42084
|
+
* NOT disable verification, it only restores the missing root.
|
|
42085
|
+
*/
|
|
42086
|
+
var DIGICERT_GLOBAL_ROOT_CA_PEM = `-----BEGIN CERTIFICATE-----
|
|
42087
|
+
MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
|
|
42088
|
+
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
|
42089
|
+
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
|
|
42090
|
+
QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
|
|
42091
|
+
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
|
|
42092
|
+
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
|
|
42093
|
+
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
|
|
42094
|
+
CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
|
|
42095
|
+
nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
|
|
42096
|
+
43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
|
|
42097
|
+
T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
|
|
42098
|
+
gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
|
|
42099
|
+
BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
|
|
42100
|
+
TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
|
|
42101
|
+
DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
|
|
42102
|
+
hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
|
|
42103
|
+
06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
|
|
42104
|
+
PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
|
|
42105
|
+
YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
|
|
42106
|
+
CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
|
|
42107
|
+
-----END CERTIFICATE-----
|
|
42108
|
+
`;
|
|
42109
|
+
var applied = false;
|
|
42110
|
+
function applyWyzeCloudTlsTrust() {
|
|
42111
|
+
if (applied) return;
|
|
42112
|
+
applied = true;
|
|
42113
|
+
(0, node_tls.setDefaultCACertificates)([...(0, node_tls.getCACertificates)(), DIGICERT_GLOBAL_ROOT_CA_PEM]);
|
|
42114
|
+
}
|
|
42115
|
+
//#endregion
|
|
41928
42116
|
//#region src/addon.ts
|
|
41929
42117
|
function getString(obj, key) {
|
|
41930
42118
|
const v = obj[key];
|
|
@@ -41971,6 +42159,7 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
|
|
|
41971
42159
|
super({});
|
|
41972
42160
|
}
|
|
41973
42161
|
async onInitialize() {
|
|
42162
|
+
applyWyzeCloudTlsTrust();
|
|
41974
42163
|
const regs = await super.onInitialize();
|
|
41975
42164
|
this.clients = new WyzeClientRegistry({
|
|
41976
42165
|
dataDir: this.ctx.dataDir,
|
|
@@ -41992,10 +42181,17 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
|
|
|
41992
42181
|
});
|
|
41993
42182
|
await this.reconcileIntegrations();
|
|
41994
42183
|
this.subscribeIntegrationLifecycle();
|
|
41995
|
-
return [
|
|
41996
|
-
|
|
41997
|
-
|
|
41998
|
-
|
|
42184
|
+
return [
|
|
42185
|
+
...regs,
|
|
42186
|
+
{
|
|
42187
|
+
capability: deviceAdoptionCapability,
|
|
42188
|
+
provider: this.buildAdoptionProvider()
|
|
42189
|
+
},
|
|
42190
|
+
{
|
|
42191
|
+
capability: connectionTestCapability,
|
|
42192
|
+
provider: this.buildConnectionTestProvider()
|
|
42193
|
+
}
|
|
42194
|
+
];
|
|
41999
42195
|
}
|
|
42000
42196
|
async onShutdown() {
|
|
42001
42197
|
this.clients?.clear();
|
|
@@ -42058,10 +42254,22 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
|
|
|
42058
42254
|
this.wireCameraDeps(dev);
|
|
42059
42255
|
await dev.materializeStreamSocket(camStreamId);
|
|
42060
42256
|
}
|
|
42257
|
+
/**
|
|
42258
|
+
* Pre-creation credential check. Registered unconditionally — it must answer
|
|
42259
|
+
* BEFORE any integration for this addon exists, so it depends on nothing in
|
|
42260
|
+
* {@link WyzeClientRegistry} and opens its own throwaway session (no disk I/O).
|
|
42261
|
+
*/
|
|
42262
|
+
buildConnectionTestProvider() {
|
|
42263
|
+
return buildWyzeConnectionTestProvider({
|
|
42264
|
+
makeCloud: defaultWyzeCloudFacadeFactory,
|
|
42265
|
+
logger: this.ctx.logger
|
|
42266
|
+
});
|
|
42267
|
+
}
|
|
42061
42268
|
buildAdoptionProvider() {
|
|
42062
42269
|
return buildWyzeAdoptionProvider({
|
|
42063
42270
|
logger: this.ctx.logger,
|
|
42064
42271
|
getCameraList: (integrationId, force) => this.requireClients().getCameraList(integrationId, force),
|
|
42272
|
+
getLastFetchError: (integrationId) => this.requireClients().getLastFetchError(integrationId),
|
|
42065
42273
|
hasIntegration: (integrationId) => this.requireClients().has(integrationId),
|
|
42066
42274
|
listIntegrations: () => this.requireClients().list(),
|
|
42067
42275
|
listAdopted: async () => {
|
package/dist/addon.mjs
CHANGED
|
@@ -6,10 +6,11 @@ import * as dgram from "dgram";
|
|
|
6
6
|
import { EventEmitter } from "events";
|
|
7
7
|
import * as net from "net";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
+
import { getCACertificates, setDefaultCACertificates } from "node:tls";
|
|
9
10
|
//#region \0rolldown/runtime.js
|
|
10
11
|
var __require$1 = /* @__PURE__ */ createRequire(import.meta.url);
|
|
11
12
|
//#endregion
|
|
12
|
-
//#region ../types/dist/event-category-
|
|
13
|
+
//#region ../types/dist/event-category-C0lyLd5U.mjs
|
|
13
14
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
14
15
|
EventCategory["SystemBoot"] = "system.boot";
|
|
15
16
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -26,9 +27,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
26
27
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
27
28
|
/**
|
|
28
29
|
* A newer addon or server-root package version was found by the
|
|
29
|
-
* authoritative registry check. Emitted once
|
|
30
|
-
* `(
|
|
31
|
-
*
|
|
30
|
+
* authoritative registry check. Emitted once when any observed
|
|
31
|
+
* `latestVersion` changes (or a package/node first appears behind);
|
|
32
|
+
* the payload carries the full currently-available list. Repeated
|
|
33
|
+
* polling of the same latests is silent.
|
|
32
34
|
*/
|
|
33
35
|
EventCategory["UpdateAvailable"] = "update.available";
|
|
34
36
|
/**
|
|
@@ -7558,6 +7560,10 @@ var RecordingBandSchema = object({
|
|
|
7558
7560
|
preBufferSec: number().min(0).optional(),
|
|
7559
7561
|
postBufferSec: number().min(0).optional()
|
|
7560
7562
|
});
|
|
7563
|
+
({
|
|
7564
|
+
preBufferSec: 10,
|
|
7565
|
+
postBufferSec: 30
|
|
7566
|
+
}).postBufferSec * 1e3;
|
|
7561
7567
|
/**
|
|
7562
7568
|
* Per-device retention overrides. Every field is optional; an unset or `0`
|
|
7563
7569
|
* value inherits the node-wide recorder default. Only footage-lifetime limits
|
|
@@ -14574,6 +14580,9 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14574
14580
|
"alarm-disarmed",
|
|
14575
14581
|
"alarm-arming",
|
|
14576
14582
|
"alarm-arm-refused",
|
|
14583
|
+
"addon-updated",
|
|
14584
|
+
"server-updated",
|
|
14585
|
+
"export-completed",
|
|
14577
14586
|
"camera-online",
|
|
14578
14587
|
"camera-offline",
|
|
14579
14588
|
"camera-disabled",
|
|
@@ -16467,13 +16476,19 @@ var RetrainStatusSchema = _enum([
|
|
|
16467
16476
|
* "never marked" from "already trained" must read `retrainStatus`.
|
|
16468
16477
|
*
|
|
16469
16478
|
* `debug` does NOT pin; it is attention, not durability.
|
|
16479
|
+
*
|
|
16480
|
+
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
16481
|
+
* A favourited track is skipped by retention the same way `staging` is, but
|
|
16482
|
+
* it does not enter `none|staging|trained` and has no staging budget.
|
|
16470
16483
|
*/
|
|
16471
16484
|
var TrackFlagFields = {
|
|
16472
16485
|
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
16473
16486
|
* `'staging'`. */
|
|
16474
16487
|
markForTrain: boolean().optional(),
|
|
16475
16488
|
/** Operator marked this track for diagnostic attention. */
|
|
16476
|
-
debug: boolean().optional()
|
|
16489
|
+
debug: boolean().optional(),
|
|
16490
|
+
/** Operator favourited this track. Pins it against pruning. */
|
|
16491
|
+
favourited: boolean().optional()
|
|
16477
16492
|
};
|
|
16478
16493
|
/**
|
|
16479
16494
|
* The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
|
|
@@ -16498,6 +16513,7 @@ var TrackFlagsSchema = object({
|
|
|
16498
16513
|
trackId: string(),
|
|
16499
16514
|
markForTrain: boolean(),
|
|
16500
16515
|
debug: boolean(),
|
|
16516
|
+
favourited: boolean(),
|
|
16501
16517
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
16502
16518
|
* a track row) because this shape is only ever produced by the write body,
|
|
16503
16519
|
* which always knows it — and a surface that has just written needs to render
|
|
@@ -20294,8 +20310,28 @@ var ClipSchema = object({
|
|
|
20294
20310
|
startMs: number(),
|
|
20295
20311
|
endMs: number()
|
|
20296
20312
|
}),
|
|
20297
|
-
/**
|
|
20298
|
-
thumbnail
|
|
20313
|
+
/**
|
|
20314
|
+
* Lazy thumbnail URL, never inlined.
|
|
20315
|
+
*
|
|
20316
|
+
* Recording-derived clips (events-mode keep-window, and the prepared
|
|
20317
|
+
* continuous event+fragment visit) MUST use the snapshot of the **main
|
|
20318
|
+
* event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
|
|
20319
|
+
* of the event that owns `kind` (object > motion > audio). Do not extract
|
|
20320
|
+
* a keyframe from the recorded segments. Other providers (onboard, HKSV)
|
|
20321
|
+
* mint their own stills.
|
|
20322
|
+
*/
|
|
20323
|
+
thumbnail: string().optional(),
|
|
20324
|
+
/** Analytics event ids that overlap this visit. Empty on footage-only clips.
|
|
20325
|
+
* The default provider's visit grain puts many motion heartbeats on one clip
|
|
20326
|
+
* instead of minting one clip per marker. */
|
|
20327
|
+
eventIds: array(string()).optional(),
|
|
20328
|
+
/** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
|
|
20329
|
+
* than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
|
|
20330
|
+
* bar keeps showing them via `recording.getAvailability`. */
|
|
20331
|
+
holes: array(object({
|
|
20332
|
+
startMs: number(),
|
|
20333
|
+
endMs: number()
|
|
20334
|
+
})).optional()
|
|
20299
20335
|
});
|
|
20300
20336
|
var ClipPlaybackSchema = object({
|
|
20301
20337
|
/** HLS master URL through the hub data-plane (Range + token in path). */
|
|
@@ -21870,10 +21906,18 @@ settings: record(string(), unknown()) });
|
|
|
21870
21906
|
* descriptive — it never changes routing.
|
|
21871
21907
|
*/
|
|
21872
21908
|
var ConnectionTestDescriptorSchema = object({ label: string() });
|
|
21873
|
-
|
|
21874
|
-
|
|
21875
|
-
|
|
21876
|
-
|
|
21909
|
+
var connectionTestCapability = {
|
|
21910
|
+
name: "connection-test",
|
|
21911
|
+
scope: "system",
|
|
21912
|
+
mode: "collection",
|
|
21913
|
+
methods: {
|
|
21914
|
+
testSettings: method(ConnectionTestInputSchema, ConnectionTestOutcomeSchema, {
|
|
21915
|
+
kind: "mutation",
|
|
21916
|
+
auth: "admin"
|
|
21917
|
+
}),
|
|
21918
|
+
describeTest: method(_void(), ConnectionTestDescriptorSchema, { auth: "admin" })
|
|
21919
|
+
}
|
|
21920
|
+
};
|
|
21877
21921
|
/**
|
|
21878
21922
|
* Upstream-system connectivity sensor — distinct from `device-status`,
|
|
21879
21923
|
* which is the kernel-managed online/offline flag for the device's
|
|
@@ -26581,7 +26625,9 @@ var ExportOptionsSchema = object({
|
|
|
26581
26625
|
includeAudio: boolean(),
|
|
26582
26626
|
maxLifeMs: number().int().positive(),
|
|
26583
26627
|
deleteAfterDownload: boolean(),
|
|
26584
|
-
title: string().max(200).optional()
|
|
26628
|
+
title: string().max(200).optional(),
|
|
26629
|
+
/** Notification-output target ids to ping when this export becomes ready. */
|
|
26630
|
+
notifyTargetIds: array(string().min(1)).max(20).optional()
|
|
26585
26631
|
}).superRefine((v, ctx) => {
|
|
26586
26632
|
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
26587
26633
|
code: ZodIssueCode.custom,
|
|
@@ -26640,10 +26686,18 @@ var ExportBytesSchema = object({
|
|
|
26640
26686
|
});
|
|
26641
26687
|
method(object({
|
|
26642
26688
|
deviceId: number(),
|
|
26643
|
-
|
|
26689
|
+
/** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
|
|
26690
|
+
profile: string().optional(),
|
|
26691
|
+
profiles: array(string()).min(1).optional(),
|
|
26644
26692
|
fromMs: number(),
|
|
26645
26693
|
toMs: number(),
|
|
26646
26694
|
options: ExportOptionsSchema
|
|
26695
|
+
}).superRefine((v, ctx) => {
|
|
26696
|
+
if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
|
|
26697
|
+
code: ZodIssueCode.custom,
|
|
26698
|
+
message: "pass profiles[] (min 1) or legacy profile",
|
|
26699
|
+
path: ["profiles"]
|
|
26700
|
+
});
|
|
26647
26701
|
}), ExportRecordSchema, {
|
|
26648
26702
|
kind: "mutation",
|
|
26649
26703
|
auth: "protected"
|
|
@@ -29585,15 +29639,6 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
29585
29639
|
labels: ["probe not implemented"]
|
|
29586
29640
|
};
|
|
29587
29641
|
}
|
|
29588
|
-
/**
|
|
29589
|
-
* Top-level devices restored at once in {@link onRestoreDevices}.
|
|
29590
|
-
*
|
|
29591
|
-
* Four covers the fleets this ships to without turning a boot into a burst a
|
|
29592
|
-
* camera NVR answers with a refusal. A provider whose upstream is a single
|
|
29593
|
-
* session with a serial command channel (a Baichuan hub, an NVR that
|
|
29594
|
-
* serialises ISAPI) should lower it; nothing needs to raise it.
|
|
29595
|
-
*/
|
|
29596
|
-
restoreConcurrency = 4;
|
|
29597
29642
|
async restoreDevices(savedDevices) {
|
|
29598
29643
|
await this.onRestoreDevices(savedDevices);
|
|
29599
29644
|
if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
|
|
@@ -29648,14 +29693,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
29648
29693
|
});
|
|
29649
29694
|
}
|
|
29650
29695
|
};
|
|
29651
|
-
|
|
29652
|
-
await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
|
|
29653
|
-
for (;;) {
|
|
29654
|
-
const saved = topLevel[nextTopLevel++];
|
|
29655
|
-
if (saved === void 0) return;
|
|
29656
|
-
await restoreOne(saved);
|
|
29657
|
-
}
|
|
29658
|
-
}));
|
|
29696
|
+
await Promise.all(topLevel.map((saved) => restoreOne(saved)));
|
|
29659
29697
|
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
29660
29698
|
for (const saved of childRows) {
|
|
29661
29699
|
const Class = this.deviceClasses[saved.type];
|
|
@@ -40610,7 +40648,8 @@ var WyzeClientRegistry = class {
|
|
|
40610
40648
|
credentials,
|
|
40611
40649
|
cameraListCache: [],
|
|
40612
40650
|
cameraListFetchedAt: 0,
|
|
40613
|
-
cameraListInFlight: null
|
|
40651
|
+
cameraListInFlight: null,
|
|
40652
|
+
lastFetchError: null
|
|
40614
40653
|
});
|
|
40615
40654
|
}
|
|
40616
40655
|
/** Drop the client for an integration that no longer exists / was disabled. */
|
|
@@ -40638,6 +40677,14 @@ var WyzeClientRegistry = class {
|
|
|
40638
40677
|
this.#entries.clear();
|
|
40639
40678
|
}
|
|
40640
40679
|
/**
|
|
40680
|
+
* The last classified error from a `getCameraList` fetch failure for an
|
|
40681
|
+
* integration, or null when the last fetch succeeded (or no fetch has run
|
|
40682
|
+
* yet). Used by `getStatus` to surface the reason for an empty adoption list.
|
|
40683
|
+
*/
|
|
40684
|
+
getLastFetchError(integrationId) {
|
|
40685
|
+
return this.#entries.get(integrationId)?.lastFetchError ?? null;
|
|
40686
|
+
}
|
|
40687
|
+
/**
|
|
40641
40688
|
* Cloud camera list for ONE integration, cached + debounced + single-flight
|
|
40642
40689
|
* to avoid auth rate-limits. Returns the last cache (possibly empty) when the
|
|
40643
40690
|
* integration is unknown or the fetch fails.
|
|
@@ -40651,9 +40698,11 @@ var WyzeClientRegistry = class {
|
|
|
40651
40698
|
const run = entry.client.getCameraList().then((cams) => {
|
|
40652
40699
|
entry.cameraListCache = cams;
|
|
40653
40700
|
entry.cameraListFetchedAt = Date.now();
|
|
40701
|
+
entry.lastFetchError = null;
|
|
40654
40702
|
return cams;
|
|
40655
40703
|
}).catch((err) => {
|
|
40656
40704
|
const classified = classifyCloudError(err);
|
|
40705
|
+
entry.lastFetchError = classified;
|
|
40657
40706
|
this.#deps.logger.error("Wyze getCameraList failed", { meta: {
|
|
40658
40707
|
integrationId,
|
|
40659
40708
|
kind: classified.kind,
|
|
@@ -41149,11 +41198,16 @@ function buildWyzeAdoptionProvider(deps) {
|
|
|
41149
41198
|
let candidateCount = 0;
|
|
41150
41199
|
for (const integrationId of listIntegrations()) candidateCount += (await candidatesForIntegration(integrationId)).length;
|
|
41151
41200
|
const adoptedCount = (await listAdopted()).length;
|
|
41201
|
+
const fetchErrors = [];
|
|
41202
|
+
if (deps.getLastFetchError) for (const integrationId of listIntegrations()) {
|
|
41203
|
+
const err = deps.getLastFetchError(integrationId);
|
|
41204
|
+
if (err) fetchErrors.push(`[${integrationId}] ${err.message}`);
|
|
41205
|
+
}
|
|
41152
41206
|
return {
|
|
41153
41207
|
lastDiscoveryAt: Date.now(),
|
|
41154
41208
|
candidateCount,
|
|
41155
41209
|
adoptedCount,
|
|
41156
|
-
lastError: null
|
|
41210
|
+
lastError: fetchErrors.length > 0 ? fetchErrors.join("; ") : null
|
|
41157
41211
|
};
|
|
41158
41212
|
} catch (err) {
|
|
41159
41213
|
logger.warn("wyze adoption: getStatus failed", { meta: { error: errMsg(err) } });
|
|
@@ -41168,11 +41222,12 @@ function buildWyzeAdoptionProvider(deps) {
|
|
|
41168
41222
|
refresh: async ({ integrationId }) => {
|
|
41169
41223
|
const candidateCount = (await candidatesForIntegration(integrationId, true)).length;
|
|
41170
41224
|
const adoptedCount = adoptedMapForIntegration(integrationId, await listAdopted()).size;
|
|
41225
|
+
const fetchErr = deps.getLastFetchError?.(integrationId);
|
|
41171
41226
|
return {
|
|
41172
41227
|
lastDiscoveryAt: Date.now(),
|
|
41173
41228
|
candidateCount,
|
|
41174
41229
|
adoptedCount,
|
|
41175
|
-
lastError: null
|
|
41230
|
+
lastError: fetchErr ? fetchErr.message : null
|
|
41176
41231
|
};
|
|
41177
41232
|
},
|
|
41178
41233
|
adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
|
|
@@ -41904,6 +41959,139 @@ var WyzeCamera = class extends BaseDevice {
|
|
|
41904
41959
|
static fromCloud = cameraConfigFromCloud;
|
|
41905
41960
|
};
|
|
41906
41961
|
//#endregion
|
|
41962
|
+
//#region src/wyze-connection-test.ts
|
|
41963
|
+
/**
|
|
41964
|
+
* Production default: creates a `WyzeCloud` with no-op session hooks so
|
|
41965
|
+
* `testSettings` never writes or reads `wyze-session.json`.
|
|
41966
|
+
*/
|
|
41967
|
+
function defaultWyzeCloudFacadeFactory(credentials) {
|
|
41968
|
+
const cloud = new WyzeCloud({
|
|
41969
|
+
apiKey: credentials.apiKey,
|
|
41970
|
+
apiId: credentials.apiId,
|
|
41971
|
+
loadSession: () => null,
|
|
41972
|
+
saveSession: () => void 0,
|
|
41973
|
+
clearSession: () => void 0
|
|
41974
|
+
});
|
|
41975
|
+
return {
|
|
41976
|
+
ensureSession: (email, password) => cloud.ensureSession(email, password),
|
|
41977
|
+
getCameraList: () => cloud.getCameraList()
|
|
41978
|
+
};
|
|
41979
|
+
}
|
|
41980
|
+
var wyzeCredentialsSchema = object({
|
|
41981
|
+
email: string().min(1).describe("Wyze account email"),
|
|
41982
|
+
password: string().min(1).describe("Wyze account password"),
|
|
41983
|
+
apiKey: string().min(1).describe("Wyze developer API key"),
|
|
41984
|
+
apiId: string().min(1).describe("Wyze developer Key ID")
|
|
41985
|
+
});
|
|
41986
|
+
var TEST_LABEL = "Signs in to the Wyze cloud with these account credentials";
|
|
41987
|
+
function buildWyzeConnectionTestProvider(deps) {
|
|
41988
|
+
const { makeCloud, logger } = deps;
|
|
41989
|
+
return {
|
|
41990
|
+
describeTest: async () => ({ label: TEST_LABEL }),
|
|
41991
|
+
testSettings: async ({ settings }) => {
|
|
41992
|
+
const parsed = wyzeCredentialsSchema.safeParse(settings);
|
|
41993
|
+
if (!parsed.success) {
|
|
41994
|
+
const missing = parsed.error.issues.map((i) => i.path.join(".") || "(root)").join(", ");
|
|
41995
|
+
logger.warn("Wyze connection test: settings incomplete", { meta: { missing } });
|
|
41996
|
+
return {
|
|
41997
|
+
outcome: "rejected",
|
|
41998
|
+
error: `Wyze account settings are incomplete or invalid: ${missing}`
|
|
41999
|
+
};
|
|
42000
|
+
}
|
|
42001
|
+
const { email, password, apiKey, apiId } = parsed.data;
|
|
42002
|
+
const startedAt = Date.now();
|
|
42003
|
+
const cloud = makeCloud({
|
|
42004
|
+
email,
|
|
42005
|
+
password,
|
|
42006
|
+
apiKey,
|
|
42007
|
+
apiId
|
|
42008
|
+
});
|
|
42009
|
+
try {
|
|
42010
|
+
await cloud.ensureSession(email, password);
|
|
42011
|
+
let cameraCount = null;
|
|
42012
|
+
try {
|
|
42013
|
+
cameraCount = (await cloud.getCameraList()).length;
|
|
42014
|
+
} catch {}
|
|
42015
|
+
return {
|
|
42016
|
+
outcome: "validated",
|
|
42017
|
+
latencyMs: Date.now() - startedAt,
|
|
42018
|
+
...cameraCount !== null ? { detail: `Found ${cameraCount} camera${cameraCount === 1 ? "" : "s"} on this account` } : {}
|
|
42019
|
+
};
|
|
42020
|
+
} catch (err) {
|
|
42021
|
+
const classified = classifyCloudError(err);
|
|
42022
|
+
if (classified.kind === "mfa") {
|
|
42023
|
+
logger.warn("Wyze connection test: MFA required", { meta: { email } });
|
|
42024
|
+
return {
|
|
42025
|
+
outcome: "rejected",
|
|
42026
|
+
error: classified.message
|
|
42027
|
+
};
|
|
42028
|
+
}
|
|
42029
|
+
if (classified.kind === "credentials") {
|
|
42030
|
+
logger.warn("Wyze connection test: credentials refused", { meta: { email } });
|
|
42031
|
+
return {
|
|
42032
|
+
outcome: "rejected",
|
|
42033
|
+
error: "Wyze refused these credentials — check the email, password, API Key and Key ID. " + classified.message
|
|
42034
|
+
};
|
|
42035
|
+
}
|
|
42036
|
+
logger.warn("Wyze connection test: could not complete", { meta: {
|
|
42037
|
+
email,
|
|
42038
|
+
kind: classified.kind,
|
|
42039
|
+
error: errMsg(err)
|
|
42040
|
+
} });
|
|
42041
|
+
return {
|
|
42042
|
+
outcome: "inconclusive",
|
|
42043
|
+
error: classified.kind === "rate-limited" ? `Could not verify credentials — rate-limited by the Wyze cloud. Retry in ~30 s. ${classified.message}` : `Could not reach the Wyze cloud to verify these credentials: ${errMsg(err)}`
|
|
42044
|
+
};
|
|
42045
|
+
}
|
|
42046
|
+
}
|
|
42047
|
+
};
|
|
42048
|
+
}
|
|
42049
|
+
//#endregion
|
|
42050
|
+
//#region src/wyze-tls-trust.ts
|
|
42051
|
+
/**
|
|
42052
|
+
* Ubuntu 24.04's `ca-certificates` (2026-06) dropped DigiCert Global Root CA
|
|
42053
|
+
* (the 2006 SHA-1-era root, still valid until 2031). Wyze's `*.wyzecam.com`
|
|
42054
|
+
* leaf is issued by "DigiCert TLS RSA SHA256 2020 CA1", which chains to that
|
|
42055
|
+
* root. Node fetch then fails with `UNABLE_TO_GET_ISSUER_CERT_LOCALLY` and
|
|
42056
|
+
* the adoption panel shows an empty camera list.
|
|
42057
|
+
*
|
|
42058
|
+
* macOS still trusts this root in the system keychain, which is why the same
|
|
42059
|
+
* URL verifies on the developer Mac and fails inside the hub container.
|
|
42060
|
+
*
|
|
42061
|
+
* The PEM is DigiCert's public root (https://cacerts.digicert.com/DigiCertGlobalRootCA.crt.pem).
|
|
42062
|
+
* Applied once per addon process via `tls.setDefaultCACertificates` — does
|
|
42063
|
+
* NOT disable verification, it only restores the missing root.
|
|
42064
|
+
*/
|
|
42065
|
+
var DIGICERT_GLOBAL_ROOT_CA_PEM = `-----BEGIN CERTIFICATE-----
|
|
42066
|
+
MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
|
|
42067
|
+
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
|
42068
|
+
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
|
|
42069
|
+
QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
|
|
42070
|
+
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
|
|
42071
|
+
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
|
|
42072
|
+
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
|
|
42073
|
+
CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
|
|
42074
|
+
nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
|
|
42075
|
+
43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
|
|
42076
|
+
T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
|
|
42077
|
+
gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
|
|
42078
|
+
BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
|
|
42079
|
+
TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
|
|
42080
|
+
DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
|
|
42081
|
+
hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
|
|
42082
|
+
06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
|
|
42083
|
+
PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
|
|
42084
|
+
YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
|
|
42085
|
+
CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
|
|
42086
|
+
-----END CERTIFICATE-----
|
|
42087
|
+
`;
|
|
42088
|
+
var applied = false;
|
|
42089
|
+
function applyWyzeCloudTlsTrust() {
|
|
42090
|
+
if (applied) return;
|
|
42091
|
+
applied = true;
|
|
42092
|
+
setDefaultCACertificates([...getCACertificates(), DIGICERT_GLOBAL_ROOT_CA_PEM]);
|
|
42093
|
+
}
|
|
42094
|
+
//#endregion
|
|
41907
42095
|
//#region src/addon.ts
|
|
41908
42096
|
function getString(obj, key) {
|
|
41909
42097
|
const v = obj[key];
|
|
@@ -41950,6 +42138,7 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
|
|
|
41950
42138
|
super({});
|
|
41951
42139
|
}
|
|
41952
42140
|
async onInitialize() {
|
|
42141
|
+
applyWyzeCloudTlsTrust();
|
|
41953
42142
|
const regs = await super.onInitialize();
|
|
41954
42143
|
this.clients = new WyzeClientRegistry({
|
|
41955
42144
|
dataDir: this.ctx.dataDir,
|
|
@@ -41971,10 +42160,17 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
|
|
|
41971
42160
|
});
|
|
41972
42161
|
await this.reconcileIntegrations();
|
|
41973
42162
|
this.subscribeIntegrationLifecycle();
|
|
41974
|
-
return [
|
|
41975
|
-
|
|
41976
|
-
|
|
41977
|
-
|
|
42163
|
+
return [
|
|
42164
|
+
...regs,
|
|
42165
|
+
{
|
|
42166
|
+
capability: deviceAdoptionCapability,
|
|
42167
|
+
provider: this.buildAdoptionProvider()
|
|
42168
|
+
},
|
|
42169
|
+
{
|
|
42170
|
+
capability: connectionTestCapability,
|
|
42171
|
+
provider: this.buildConnectionTestProvider()
|
|
42172
|
+
}
|
|
42173
|
+
];
|
|
41978
42174
|
}
|
|
41979
42175
|
async onShutdown() {
|
|
41980
42176
|
this.clients?.clear();
|
|
@@ -42037,10 +42233,22 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
|
|
|
42037
42233
|
this.wireCameraDeps(dev);
|
|
42038
42234
|
await dev.materializeStreamSocket(camStreamId);
|
|
42039
42235
|
}
|
|
42236
|
+
/**
|
|
42237
|
+
* Pre-creation credential check. Registered unconditionally — it must answer
|
|
42238
|
+
* BEFORE any integration for this addon exists, so it depends on nothing in
|
|
42239
|
+
* {@link WyzeClientRegistry} and opens its own throwaway session (no disk I/O).
|
|
42240
|
+
*/
|
|
42241
|
+
buildConnectionTestProvider() {
|
|
42242
|
+
return buildWyzeConnectionTestProvider({
|
|
42243
|
+
makeCloud: defaultWyzeCloudFacadeFactory,
|
|
42244
|
+
logger: this.ctx.logger
|
|
42245
|
+
});
|
|
42246
|
+
}
|
|
42040
42247
|
buildAdoptionProvider() {
|
|
42041
42248
|
return buildWyzeAdoptionProvider({
|
|
42042
42249
|
logger: this.ctx.logger,
|
|
42043
42250
|
getCameraList: (integrationId, force) => this.requireClients().getCameraList(integrationId, force),
|
|
42251
|
+
getLastFetchError: (integrationId) => this.requireClients().getLastFetchError(integrationId),
|
|
42044
42252
|
hasIntegration: (integrationId) => this.requireClients().has(integrationId),
|
|
42045
42253
|
listIntegrations: () => this.requireClients().list(),
|
|
42046
42254
|
listAdopted: async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-wyze",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
4
4
|
"description": "Wyze camera device-provider addon for CamStack — wraps the @apocaliss92/wyze-bridge-js P2P/DTLS client, feeding the stream-broker via the pull-rfc4571 lazy-publish path (a structural twin of addon-provider-reolink)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -54,6 +54,9 @@
|
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
"name": "stream-params"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "connection-test"
|
|
57
60
|
}
|
|
58
61
|
]
|
|
59
62
|
}
|