@camstack/addon-provider-wyze 0.2.18 → 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 +253 -38
- package/dist/addon.mjs +253 -38
- 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
|
|
@@ -19611,7 +19627,14 @@ targets: array(object({
|
|
|
19611
19627
|
"sleeping",
|
|
19612
19628
|
"unreachable",
|
|
19613
19629
|
"waking"
|
|
19614
|
-
]).nullable()
|
|
19630
|
+
]).nullable(),
|
|
19631
|
+
/** A battery camera (whatever its current state). An AWAKE battery
|
|
19632
|
+
* camera is deliberately NOT recaptured on the poll cadence — every
|
|
19633
|
+
* capture is a camera hit that would keep it out of sleep — so its
|
|
19634
|
+
* cached frame legitimately ages past the currency ceiling while
|
|
19635
|
+
* nothing is streaming. A surface must keep painting it (a fresh
|
|
19636
|
+
* frame is captured at each wake), not blank to "unavailable". */
|
|
19637
|
+
battery: boolean()
|
|
19615
19638
|
})))
|
|
19616
19639
|
},
|
|
19617
19640
|
status: {
|
|
@@ -20308,8 +20331,28 @@ var ClipSchema = object({
|
|
|
20308
20331
|
startMs: number(),
|
|
20309
20332
|
endMs: number()
|
|
20310
20333
|
}),
|
|
20311
|
-
/**
|
|
20312
|
-
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()
|
|
20313
20356
|
});
|
|
20314
20357
|
var ClipPlaybackSchema = object({
|
|
20315
20358
|
/** HLS master URL through the hub data-plane (Range + token in path). */
|
|
@@ -21884,10 +21927,18 @@ settings: record(string(), unknown()) });
|
|
|
21884
21927
|
* descriptive — it never changes routing.
|
|
21885
21928
|
*/
|
|
21886
21929
|
var ConnectionTestDescriptorSchema = object({ label: string() });
|
|
21887
|
-
|
|
21888
|
-
|
|
21889
|
-
|
|
21890
|
-
|
|
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
|
+
};
|
|
21891
21942
|
/**
|
|
21892
21943
|
* Upstream-system connectivity sensor — distinct from `device-status`,
|
|
21893
21944
|
* which is the kernel-managed online/offline flag for the device's
|
|
@@ -26595,7 +26646,9 @@ var ExportOptionsSchema = object({
|
|
|
26595
26646
|
includeAudio: boolean(),
|
|
26596
26647
|
maxLifeMs: number().int().positive(),
|
|
26597
26648
|
deleteAfterDownload: boolean(),
|
|
26598
|
-
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()
|
|
26599
26652
|
}).superRefine((v, ctx) => {
|
|
26600
26653
|
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
26601
26654
|
code: ZodIssueCode.custom,
|
|
@@ -26654,10 +26707,18 @@ var ExportBytesSchema = object({
|
|
|
26654
26707
|
});
|
|
26655
26708
|
method(object({
|
|
26656
26709
|
deviceId: number(),
|
|
26657
|
-
|
|
26710
|
+
/** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
|
|
26711
|
+
profile: string().optional(),
|
|
26712
|
+
profiles: array(string()).min(1).optional(),
|
|
26658
26713
|
fromMs: number(),
|
|
26659
26714
|
toMs: number(),
|
|
26660
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
|
+
});
|
|
26661
26722
|
}), ExportRecordSchema, {
|
|
26662
26723
|
kind: "mutation",
|
|
26663
26724
|
auth: "protected"
|
|
@@ -29599,15 +29660,6 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
29599
29660
|
labels: ["probe not implemented"]
|
|
29600
29661
|
};
|
|
29601
29662
|
}
|
|
29602
|
-
/**
|
|
29603
|
-
* Top-level devices restored at once in {@link onRestoreDevices}.
|
|
29604
|
-
*
|
|
29605
|
-
* Four covers the fleets this ships to without turning a boot into a burst a
|
|
29606
|
-
* camera NVR answers with a refusal. A provider whose upstream is a single
|
|
29607
|
-
* session with a serial command channel (a Baichuan hub, an NVR that
|
|
29608
|
-
* serialises ISAPI) should lower it; nothing needs to raise it.
|
|
29609
|
-
*/
|
|
29610
|
-
restoreConcurrency = 4;
|
|
29611
29663
|
async restoreDevices(savedDevices) {
|
|
29612
29664
|
await this.onRestoreDevices(savedDevices);
|
|
29613
29665
|
if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
|
|
@@ -29662,14 +29714,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
29662
29714
|
});
|
|
29663
29715
|
}
|
|
29664
29716
|
};
|
|
29665
|
-
|
|
29666
|
-
await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
|
|
29667
|
-
for (;;) {
|
|
29668
|
-
const saved = topLevel[nextTopLevel++];
|
|
29669
|
-
if (saved === void 0) return;
|
|
29670
|
-
await restoreOne(saved);
|
|
29671
|
-
}
|
|
29672
|
-
}));
|
|
29717
|
+
await Promise.all(topLevel.map((saved) => restoreOne(saved)));
|
|
29673
29718
|
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
29674
29719
|
for (const saved of childRows) {
|
|
29675
29720
|
const Class = this.deviceClasses[saved.type];
|
|
@@ -40624,7 +40669,8 @@ var WyzeClientRegistry = class {
|
|
|
40624
40669
|
credentials,
|
|
40625
40670
|
cameraListCache: [],
|
|
40626
40671
|
cameraListFetchedAt: 0,
|
|
40627
|
-
cameraListInFlight: null
|
|
40672
|
+
cameraListInFlight: null,
|
|
40673
|
+
lastFetchError: null
|
|
40628
40674
|
});
|
|
40629
40675
|
}
|
|
40630
40676
|
/** Drop the client for an integration that no longer exists / was disabled. */
|
|
@@ -40652,6 +40698,14 @@ var WyzeClientRegistry = class {
|
|
|
40652
40698
|
this.#entries.clear();
|
|
40653
40699
|
}
|
|
40654
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
|
+
/**
|
|
40655
40709
|
* Cloud camera list for ONE integration, cached + debounced + single-flight
|
|
40656
40710
|
* to avoid auth rate-limits. Returns the last cache (possibly empty) when the
|
|
40657
40711
|
* integration is unknown or the fetch fails.
|
|
@@ -40665,9 +40719,11 @@ var WyzeClientRegistry = class {
|
|
|
40665
40719
|
const run = entry.client.getCameraList().then((cams) => {
|
|
40666
40720
|
entry.cameraListCache = cams;
|
|
40667
40721
|
entry.cameraListFetchedAt = Date.now();
|
|
40722
|
+
entry.lastFetchError = null;
|
|
40668
40723
|
return cams;
|
|
40669
40724
|
}).catch((err) => {
|
|
40670
40725
|
const classified = classifyCloudError(err);
|
|
40726
|
+
entry.lastFetchError = classified;
|
|
40671
40727
|
this.#deps.logger.error("Wyze getCameraList failed", { meta: {
|
|
40672
40728
|
integrationId,
|
|
40673
40729
|
kind: classified.kind,
|
|
@@ -41163,11 +41219,16 @@ function buildWyzeAdoptionProvider(deps) {
|
|
|
41163
41219
|
let candidateCount = 0;
|
|
41164
41220
|
for (const integrationId of listIntegrations()) candidateCount += (await candidatesForIntegration(integrationId)).length;
|
|
41165
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
|
+
}
|
|
41166
41227
|
return {
|
|
41167
41228
|
lastDiscoveryAt: Date.now(),
|
|
41168
41229
|
candidateCount,
|
|
41169
41230
|
adoptedCount,
|
|
41170
|
-
lastError: null
|
|
41231
|
+
lastError: fetchErrors.length > 0 ? fetchErrors.join("; ") : null
|
|
41171
41232
|
};
|
|
41172
41233
|
} catch (err) {
|
|
41173
41234
|
logger.warn("wyze adoption: getStatus failed", { meta: { error: errMsg(err) } });
|
|
@@ -41182,11 +41243,12 @@ function buildWyzeAdoptionProvider(deps) {
|
|
|
41182
41243
|
refresh: async ({ integrationId }) => {
|
|
41183
41244
|
const candidateCount = (await candidatesForIntegration(integrationId, true)).length;
|
|
41184
41245
|
const adoptedCount = adoptedMapForIntegration(integrationId, await listAdopted()).size;
|
|
41246
|
+
const fetchErr = deps.getLastFetchError?.(integrationId);
|
|
41185
41247
|
return {
|
|
41186
41248
|
lastDiscoveryAt: Date.now(),
|
|
41187
41249
|
candidateCount,
|
|
41188
41250
|
adoptedCount,
|
|
41189
|
-
lastError: null
|
|
41251
|
+
lastError: fetchErr ? fetchErr.message : null
|
|
41190
41252
|
};
|
|
41191
41253
|
},
|
|
41192
41254
|
adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
|
|
@@ -41918,6 +41980,139 @@ var WyzeCamera = class extends BaseDevice {
|
|
|
41918
41980
|
static fromCloud = cameraConfigFromCloud;
|
|
41919
41981
|
};
|
|
41920
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
|
|
41921
42116
|
//#region src/addon.ts
|
|
41922
42117
|
function getString(obj, key) {
|
|
41923
42118
|
const v = obj[key];
|
|
@@ -41964,6 +42159,7 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
|
|
|
41964
42159
|
super({});
|
|
41965
42160
|
}
|
|
41966
42161
|
async onInitialize() {
|
|
42162
|
+
applyWyzeCloudTlsTrust();
|
|
41967
42163
|
const regs = await super.onInitialize();
|
|
41968
42164
|
this.clients = new WyzeClientRegistry({
|
|
41969
42165
|
dataDir: this.ctx.dataDir,
|
|
@@ -41985,10 +42181,17 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
|
|
|
41985
42181
|
});
|
|
41986
42182
|
await this.reconcileIntegrations();
|
|
41987
42183
|
this.subscribeIntegrationLifecycle();
|
|
41988
|
-
return [
|
|
41989
|
-
|
|
41990
|
-
|
|
41991
|
-
|
|
42184
|
+
return [
|
|
42185
|
+
...regs,
|
|
42186
|
+
{
|
|
42187
|
+
capability: deviceAdoptionCapability,
|
|
42188
|
+
provider: this.buildAdoptionProvider()
|
|
42189
|
+
},
|
|
42190
|
+
{
|
|
42191
|
+
capability: connectionTestCapability,
|
|
42192
|
+
provider: this.buildConnectionTestProvider()
|
|
42193
|
+
}
|
|
42194
|
+
];
|
|
41992
42195
|
}
|
|
41993
42196
|
async onShutdown() {
|
|
41994
42197
|
this.clients?.clear();
|
|
@@ -42051,10 +42254,22 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
|
|
|
42051
42254
|
this.wireCameraDeps(dev);
|
|
42052
42255
|
await dev.materializeStreamSocket(camStreamId);
|
|
42053
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
|
+
}
|
|
42054
42268
|
buildAdoptionProvider() {
|
|
42055
42269
|
return buildWyzeAdoptionProvider({
|
|
42056
42270
|
logger: this.ctx.logger,
|
|
42057
42271
|
getCameraList: (integrationId, force) => this.requireClients().getCameraList(integrationId, force),
|
|
42272
|
+
getLastFetchError: (integrationId) => this.requireClients().getLastFetchError(integrationId),
|
|
42058
42273
|
hasIntegration: (integrationId) => this.requireClients().has(integrationId),
|
|
42059
42274
|
listIntegrations: () => this.requireClients().list(),
|
|
42060
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
|
|
@@ -19590,7 +19606,14 @@ targets: array(object({
|
|
|
19590
19606
|
"sleeping",
|
|
19591
19607
|
"unreachable",
|
|
19592
19608
|
"waking"
|
|
19593
|
-
]).nullable()
|
|
19609
|
+
]).nullable(),
|
|
19610
|
+
/** A battery camera (whatever its current state). An AWAKE battery
|
|
19611
|
+
* camera is deliberately NOT recaptured on the poll cadence — every
|
|
19612
|
+
* capture is a camera hit that would keep it out of sleep — so its
|
|
19613
|
+
* cached frame legitimately ages past the currency ceiling while
|
|
19614
|
+
* nothing is streaming. A surface must keep painting it (a fresh
|
|
19615
|
+
* frame is captured at each wake), not blank to "unavailable". */
|
|
19616
|
+
battery: boolean()
|
|
19594
19617
|
})))
|
|
19595
19618
|
},
|
|
19596
19619
|
status: {
|
|
@@ -20287,8 +20310,28 @@ var ClipSchema = object({
|
|
|
20287
20310
|
startMs: number(),
|
|
20288
20311
|
endMs: number()
|
|
20289
20312
|
}),
|
|
20290
|
-
/**
|
|
20291
|
-
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()
|
|
20292
20335
|
});
|
|
20293
20336
|
var ClipPlaybackSchema = object({
|
|
20294
20337
|
/** HLS master URL through the hub data-plane (Range + token in path). */
|
|
@@ -21863,10 +21906,18 @@ settings: record(string(), unknown()) });
|
|
|
21863
21906
|
* descriptive — it never changes routing.
|
|
21864
21907
|
*/
|
|
21865
21908
|
var ConnectionTestDescriptorSchema = object({ label: string() });
|
|
21866
|
-
|
|
21867
|
-
|
|
21868
|
-
|
|
21869
|
-
|
|
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
|
+
};
|
|
21870
21921
|
/**
|
|
21871
21922
|
* Upstream-system connectivity sensor — distinct from `device-status`,
|
|
21872
21923
|
* which is the kernel-managed online/offline flag for the device's
|
|
@@ -26574,7 +26625,9 @@ var ExportOptionsSchema = object({
|
|
|
26574
26625
|
includeAudio: boolean(),
|
|
26575
26626
|
maxLifeMs: number().int().positive(),
|
|
26576
26627
|
deleteAfterDownload: boolean(),
|
|
26577
|
-
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()
|
|
26578
26631
|
}).superRefine((v, ctx) => {
|
|
26579
26632
|
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
26580
26633
|
code: ZodIssueCode.custom,
|
|
@@ -26633,10 +26686,18 @@ var ExportBytesSchema = object({
|
|
|
26633
26686
|
});
|
|
26634
26687
|
method(object({
|
|
26635
26688
|
deviceId: number(),
|
|
26636
|
-
|
|
26689
|
+
/** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
|
|
26690
|
+
profile: string().optional(),
|
|
26691
|
+
profiles: array(string()).min(1).optional(),
|
|
26637
26692
|
fromMs: number(),
|
|
26638
26693
|
toMs: number(),
|
|
26639
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
|
+
});
|
|
26640
26701
|
}), ExportRecordSchema, {
|
|
26641
26702
|
kind: "mutation",
|
|
26642
26703
|
auth: "protected"
|
|
@@ -29578,15 +29639,6 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
29578
29639
|
labels: ["probe not implemented"]
|
|
29579
29640
|
};
|
|
29580
29641
|
}
|
|
29581
|
-
/**
|
|
29582
|
-
* Top-level devices restored at once in {@link onRestoreDevices}.
|
|
29583
|
-
*
|
|
29584
|
-
* Four covers the fleets this ships to without turning a boot into a burst a
|
|
29585
|
-
* camera NVR answers with a refusal. A provider whose upstream is a single
|
|
29586
|
-
* session with a serial command channel (a Baichuan hub, an NVR that
|
|
29587
|
-
* serialises ISAPI) should lower it; nothing needs to raise it.
|
|
29588
|
-
*/
|
|
29589
|
-
restoreConcurrency = 4;
|
|
29590
29642
|
async restoreDevices(savedDevices) {
|
|
29591
29643
|
await this.onRestoreDevices(savedDevices);
|
|
29592
29644
|
if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
|
|
@@ -29641,14 +29693,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
29641
29693
|
});
|
|
29642
29694
|
}
|
|
29643
29695
|
};
|
|
29644
|
-
|
|
29645
|
-
await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
|
|
29646
|
-
for (;;) {
|
|
29647
|
-
const saved = topLevel[nextTopLevel++];
|
|
29648
|
-
if (saved === void 0) return;
|
|
29649
|
-
await restoreOne(saved);
|
|
29650
|
-
}
|
|
29651
|
-
}));
|
|
29696
|
+
await Promise.all(topLevel.map((saved) => restoreOne(saved)));
|
|
29652
29697
|
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
29653
29698
|
for (const saved of childRows) {
|
|
29654
29699
|
const Class = this.deviceClasses[saved.type];
|
|
@@ -40603,7 +40648,8 @@ var WyzeClientRegistry = class {
|
|
|
40603
40648
|
credentials,
|
|
40604
40649
|
cameraListCache: [],
|
|
40605
40650
|
cameraListFetchedAt: 0,
|
|
40606
|
-
cameraListInFlight: null
|
|
40651
|
+
cameraListInFlight: null,
|
|
40652
|
+
lastFetchError: null
|
|
40607
40653
|
});
|
|
40608
40654
|
}
|
|
40609
40655
|
/** Drop the client for an integration that no longer exists / was disabled. */
|
|
@@ -40631,6 +40677,14 @@ var WyzeClientRegistry = class {
|
|
|
40631
40677
|
this.#entries.clear();
|
|
40632
40678
|
}
|
|
40633
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
|
+
/**
|
|
40634
40688
|
* Cloud camera list for ONE integration, cached + debounced + single-flight
|
|
40635
40689
|
* to avoid auth rate-limits. Returns the last cache (possibly empty) when the
|
|
40636
40690
|
* integration is unknown or the fetch fails.
|
|
@@ -40644,9 +40698,11 @@ var WyzeClientRegistry = class {
|
|
|
40644
40698
|
const run = entry.client.getCameraList().then((cams) => {
|
|
40645
40699
|
entry.cameraListCache = cams;
|
|
40646
40700
|
entry.cameraListFetchedAt = Date.now();
|
|
40701
|
+
entry.lastFetchError = null;
|
|
40647
40702
|
return cams;
|
|
40648
40703
|
}).catch((err) => {
|
|
40649
40704
|
const classified = classifyCloudError(err);
|
|
40705
|
+
entry.lastFetchError = classified;
|
|
40650
40706
|
this.#deps.logger.error("Wyze getCameraList failed", { meta: {
|
|
40651
40707
|
integrationId,
|
|
40652
40708
|
kind: classified.kind,
|
|
@@ -41142,11 +41198,16 @@ function buildWyzeAdoptionProvider(deps) {
|
|
|
41142
41198
|
let candidateCount = 0;
|
|
41143
41199
|
for (const integrationId of listIntegrations()) candidateCount += (await candidatesForIntegration(integrationId)).length;
|
|
41144
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
|
+
}
|
|
41145
41206
|
return {
|
|
41146
41207
|
lastDiscoveryAt: Date.now(),
|
|
41147
41208
|
candidateCount,
|
|
41148
41209
|
adoptedCount,
|
|
41149
|
-
lastError: null
|
|
41210
|
+
lastError: fetchErrors.length > 0 ? fetchErrors.join("; ") : null
|
|
41150
41211
|
};
|
|
41151
41212
|
} catch (err) {
|
|
41152
41213
|
logger.warn("wyze adoption: getStatus failed", { meta: { error: errMsg(err) } });
|
|
@@ -41161,11 +41222,12 @@ function buildWyzeAdoptionProvider(deps) {
|
|
|
41161
41222
|
refresh: async ({ integrationId }) => {
|
|
41162
41223
|
const candidateCount = (await candidatesForIntegration(integrationId, true)).length;
|
|
41163
41224
|
const adoptedCount = adoptedMapForIntegration(integrationId, await listAdopted()).size;
|
|
41225
|
+
const fetchErr = deps.getLastFetchError?.(integrationId);
|
|
41164
41226
|
return {
|
|
41165
41227
|
lastDiscoveryAt: Date.now(),
|
|
41166
41228
|
candidateCount,
|
|
41167
41229
|
adoptedCount,
|
|
41168
|
-
lastError: null
|
|
41230
|
+
lastError: fetchErr ? fetchErr.message : null
|
|
41169
41231
|
};
|
|
41170
41232
|
},
|
|
41171
41233
|
adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
|
|
@@ -41897,6 +41959,139 @@ var WyzeCamera = class extends BaseDevice {
|
|
|
41897
41959
|
static fromCloud = cameraConfigFromCloud;
|
|
41898
41960
|
};
|
|
41899
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
|
|
41900
42095
|
//#region src/addon.ts
|
|
41901
42096
|
function getString(obj, key) {
|
|
41902
42097
|
const v = obj[key];
|
|
@@ -41943,6 +42138,7 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
|
|
|
41943
42138
|
super({});
|
|
41944
42139
|
}
|
|
41945
42140
|
async onInitialize() {
|
|
42141
|
+
applyWyzeCloudTlsTrust();
|
|
41946
42142
|
const regs = await super.onInitialize();
|
|
41947
42143
|
this.clients = new WyzeClientRegistry({
|
|
41948
42144
|
dataDir: this.ctx.dataDir,
|
|
@@ -41964,10 +42160,17 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
|
|
|
41964
42160
|
});
|
|
41965
42161
|
await this.reconcileIntegrations();
|
|
41966
42162
|
this.subscribeIntegrationLifecycle();
|
|
41967
|
-
return [
|
|
41968
|
-
|
|
41969
|
-
|
|
41970
|
-
|
|
42163
|
+
return [
|
|
42164
|
+
...regs,
|
|
42165
|
+
{
|
|
42166
|
+
capability: deviceAdoptionCapability,
|
|
42167
|
+
provider: this.buildAdoptionProvider()
|
|
42168
|
+
},
|
|
42169
|
+
{
|
|
42170
|
+
capability: connectionTestCapability,
|
|
42171
|
+
provider: this.buildConnectionTestProvider()
|
|
42172
|
+
}
|
|
42173
|
+
];
|
|
41971
42174
|
}
|
|
41972
42175
|
async onShutdown() {
|
|
41973
42176
|
this.clients?.clear();
|
|
@@ -42030,10 +42233,22 @@ var WyzeProviderAddon = class extends BaseDeviceProvider {
|
|
|
42030
42233
|
this.wireCameraDeps(dev);
|
|
42031
42234
|
await dev.materializeStreamSocket(camStreamId);
|
|
42032
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
|
+
}
|
|
42033
42247
|
buildAdoptionProvider() {
|
|
42034
42248
|
return buildWyzeAdoptionProvider({
|
|
42035
42249
|
logger: this.ctx.logger,
|
|
42036
42250
|
getCameraList: (integrationId, force) => this.requireClients().getCameraList(integrationId, force),
|
|
42251
|
+
getLastFetchError: (integrationId) => this.requireClients().getLastFetchError(integrationId),
|
|
42037
42252
|
hasIntegration: (integrationId) => this.requireClients().has(integrationId),
|
|
42038
42253
|
listIntegrations: () => this.requireClients().list(),
|
|
42039
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
|
}
|