@camstack/addon-provider-homeassistant 1.1.28 → 1.1.30
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 +270 -17
- package/dist/addon.mjs +270 -17
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let node_crypto = require("node:crypto");
|
|
3
3
|
let node_zlib = require("node:zlib");
|
|
4
|
-
//#region ../types/dist/event-category-
|
|
4
|
+
//#region ../types/dist/event-category-D4HJq7Mw.mjs
|
|
5
5
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
6
6
|
EventCategory["SystemBoot"] = "system.boot";
|
|
7
7
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -154,6 +154,11 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
154
154
|
/** Runner-sampled scrub thumbnail (~1/5 s/camera). Telemetry (D8): a lost
|
|
155
155
|
* thumb is a scrub gap the recorder's keyframe backfill covers. */
|
|
156
156
|
EventCategory["RecordingThumbSampled"] = "recording.thumb-sampled";
|
|
157
|
+
/** Export render progress (0–100). Telemetry (D8): a lost tick is a stale
|
|
158
|
+
* progress bar the client reconciles via `recordingExport.getExport`. */
|
|
159
|
+
EventCategory["RecordingExportProgress"] = "recording.export.progress";
|
|
160
|
+
EventCategory["RecordingExportCompleted"] = "recording.export.completed";
|
|
161
|
+
EventCategory["RecordingExportFailed"] = "recording.export.failed";
|
|
157
162
|
EventCategory["DetectionEvent"] = "detection.event";
|
|
158
163
|
EventCategory["SessionTrackNew"] = "session.track.new";
|
|
159
164
|
EventCategory["SessionTrackExpired"] = "session.track.expired";
|
|
@@ -443,6 +448,25 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
443
448
|
*/
|
|
444
449
|
EventCategory["PipelineAnalyticsStationaryChanged"] = "pipeline-analytics.stationary-changed";
|
|
445
450
|
/**
|
|
451
|
+
* Fired by `addon-post-analysis` when a package-drop is confirmed inside
|
|
452
|
+
* a package zone — a newly-appeared stationary object of a package class
|
|
453
|
+
* that cleared the class / zone / dwell / size gates. Payload:
|
|
454
|
+
* `PipelineAnalyticsPackageDeliveredPayload` carrying `{ deviceId,
|
|
455
|
+
* entryId, className, zoneIds, keyFrameMediaKey?, bbox, timestamp }`.
|
|
456
|
+
* Telemetry (D8): the durable record is the `package-events` store row;
|
|
457
|
+
* this bus topic drives notifier rules + live UI. See
|
|
458
|
+
* docs/superpowers/specs/2026-07-17-package-zones-design.md §5.1.
|
|
459
|
+
*/
|
|
460
|
+
EventCategory["PipelineAnalyticsPackageDelivered"] = "pipeline-analytics.package-delivered";
|
|
461
|
+
/**
|
|
462
|
+
* Fired by `addon-post-analysis` when a previously-delivered package
|
|
463
|
+
* leaves its zone (the stationary entry departed — moved or swept).
|
|
464
|
+
* Payload: `PipelineAnalyticsPackagePickedUpPayload` carrying
|
|
465
|
+
* `{ deviceId, entryId, deliveredEventId, className, timestamp }`.
|
|
466
|
+
* Telemetry (D8). See package-zones-design §5.2.
|
|
467
|
+
*/
|
|
468
|
+
EventCategory["PipelineAnalyticsPackagePickedUp"] = "pipeline-analytics.package-picked-up";
|
|
469
|
+
/**
|
|
446
470
|
* Fired by `addon-post-analysis` whenever a gallery face row changes:
|
|
447
471
|
* `kind:'buffered'` a new detected face was persisted, `'assigned'` /
|
|
448
472
|
* `'unassigned'` its identity link changed, `'deleted'` the row was
|
|
@@ -5181,6 +5205,25 @@ function _instanceof(cls, params = {}) {
|
|
|
5181
5205
|
};
|
|
5182
5206
|
return inst;
|
|
5183
5207
|
}
|
|
5208
|
+
//#endregion
|
|
5209
|
+
//#region ../../node_modules/zod/v4/classic/compat.js
|
|
5210
|
+
/** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
|
|
5211
|
+
var ZodIssueCode = {
|
|
5212
|
+
invalid_type: "invalid_type",
|
|
5213
|
+
too_big: "too_big",
|
|
5214
|
+
too_small: "too_small",
|
|
5215
|
+
invalid_format: "invalid_format",
|
|
5216
|
+
not_multiple_of: "not_multiple_of",
|
|
5217
|
+
unrecognized_keys: "unrecognized_keys",
|
|
5218
|
+
invalid_union: "invalid_union",
|
|
5219
|
+
invalid_key: "invalid_key",
|
|
5220
|
+
invalid_element: "invalid_element",
|
|
5221
|
+
invalid_value: "invalid_value",
|
|
5222
|
+
custom: "custom"
|
|
5223
|
+
};
|
|
5224
|
+
/** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */
|
|
5225
|
+
var ZodFirstPartyTypeKind;
|
|
5226
|
+
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5184
5227
|
Object.fromEntries([
|
|
5185
5228
|
{
|
|
5186
5229
|
id: "overview",
|
|
@@ -7356,6 +7399,24 @@ var RecordingRetentionSchema = object({
|
|
|
7356
7399
|
maxSizeGb: number().min(0).optional()
|
|
7357
7400
|
});
|
|
7358
7401
|
/**
|
|
7402
|
+
* Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
|
|
7403
|
+
* sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
|
|
7404
|
+
* previews at. Five graduated steps; absent on a config = `standard` (the
|
|
7405
|
+
* shipped default, matching `sheet-geometry`/`sheet-composer`).
|
|
7406
|
+
*
|
|
7407
|
+
* Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
|
|
7408
|
+
* Each window's index sidecar carries its own tile dims, so a camera whose
|
|
7409
|
+
* preset changed over time renders every historical window at the dims it was
|
|
7410
|
+
* written with.
|
|
7411
|
+
*/
|
|
7412
|
+
var ScrubThumbnailPresetSchema = _enum([
|
|
7413
|
+
"minimal",
|
|
7414
|
+
"low",
|
|
7415
|
+
"standard",
|
|
7416
|
+
"high",
|
|
7417
|
+
"max"
|
|
7418
|
+
]);
|
|
7419
|
+
/**
|
|
7359
7420
|
* The full per-camera recording intent — the wire shape of a RecordingTarget.
|
|
7360
7421
|
*
|
|
7361
7422
|
* `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
|
|
@@ -7392,7 +7453,14 @@ var RecordingConfigSchema = object({
|
|
|
7392
7453
|
* derived into bands once via `migrateConfigToBands`.
|
|
7393
7454
|
*/
|
|
7394
7455
|
bands: array(RecordingBandSchema).optional(),
|
|
7395
|
-
retention: RecordingRetentionSchema.optional()
|
|
7456
|
+
retention: RecordingRetentionSchema.optional(),
|
|
7457
|
+
/**
|
|
7458
|
+
* Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
|
|
7459
|
+
* timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
|
|
7460
|
+
* windows only — existing sheets are immutable, and each window's index
|
|
7461
|
+
* carries its own tile dims so mixed-preset history renders correctly.
|
|
7462
|
+
*/
|
|
7463
|
+
scrubThumbnails: ScrubThumbnailPresetSchema.optional()
|
|
7396
7464
|
});
|
|
7397
7465
|
/**
|
|
7398
7466
|
* `StorageLocationType` — an addon-declared id that identifies the *kind* of
|
|
@@ -14988,7 +15056,11 @@ array(ZoneRuleSchema).readonly();
|
|
|
14988
15056
|
* Extend the enum here when a new gating consumer comes online (audio
|
|
14989
15057
|
* gating, alert filtering, …) — no other surface needs to change.
|
|
14990
15058
|
*/
|
|
14991
|
-
var ZoneRuleStageEnum = _enum([
|
|
15059
|
+
var ZoneRuleStageEnum = _enum([
|
|
15060
|
+
"motion",
|
|
15061
|
+
"detection",
|
|
15062
|
+
"package"
|
|
15063
|
+
]);
|
|
14992
15064
|
/**
|
|
14993
15065
|
* Runtime registry: cap-property-name → cap definition. `BaseDevice`'s
|
|
14994
15066
|
* `state` getter looks up the cap definition here to construct a
|
|
@@ -15082,16 +15154,24 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
15082
15154
|
})
|
|
15083
15155
|
},
|
|
15084
15156
|
/**
|
|
15085
|
-
* Runtime-state slice —
|
|
15157
|
+
* Runtime-state slice — every stage mirrored together so consumers
|
|
15086
15158
|
* see one reactive handle (`device.state.zoneRules.value`) instead
|
|
15087
|
-
* of
|
|
15088
|
-
* `{motion, detection}` shape, so subscribers always get the
|
|
15159
|
+
* of one per stage. Bulk-replace mutations on any stage write the full
|
|
15160
|
+
* `{motion, detection, package}` shape, so subscribers always get the
|
|
15089
15161
|
* complete current set. Consumers that only care about one stage
|
|
15090
15162
|
* just read the matching property.
|
|
15163
|
+
*
|
|
15164
|
+
* `package` backs the package-drop detector — a package zone is a
|
|
15165
|
+
* `ZoneRule` on the `'package'` stage referencing drawn polygons
|
|
15166
|
+
* (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
|
|
15167
|
+
* The orchestrator provider that writes this stage lands in a later
|
|
15168
|
+
* slice; until then the mirror carries only `{motion, detection}` and
|
|
15169
|
+
* consumers read `package` as absent (treat as `[]`).
|
|
15091
15170
|
*/
|
|
15092
15171
|
runtimeState: object({
|
|
15093
15172
|
motion: array(ZoneRuleSchema).readonly(),
|
|
15094
|
-
detection: array(ZoneRuleSchema).readonly()
|
|
15173
|
+
detection: array(ZoneRuleSchema).readonly(),
|
|
15174
|
+
package: array(ZoneRuleSchema).readonly()
|
|
15095
15175
|
})
|
|
15096
15176
|
},
|
|
15097
15177
|
zones: zonesCapability
|
|
@@ -19152,6 +19232,7 @@ var EventKindIconSchema = _enum([
|
|
|
19152
19232
|
"smoke",
|
|
19153
19233
|
"water",
|
|
19154
19234
|
"button",
|
|
19235
|
+
"package",
|
|
19155
19236
|
"generic"
|
|
19156
19237
|
]);
|
|
19157
19238
|
var EventKindCategorySchema = _enum([
|
|
@@ -19159,7 +19240,8 @@ var EventKindCategorySchema = _enum([
|
|
|
19159
19240
|
"audio",
|
|
19160
19241
|
"detection",
|
|
19161
19242
|
"sensor",
|
|
19162
|
-
"custom"
|
|
19243
|
+
"custom",
|
|
19244
|
+
"package"
|
|
19163
19245
|
]);
|
|
19164
19246
|
var EventKindDescriptorSchema = object({
|
|
19165
19247
|
/** Stable kind id (e.g. 'motion', 'person', 'contact'). */
|
|
@@ -22894,6 +22976,108 @@ method(object({
|
|
|
22894
22976
|
auth: "admin"
|
|
22895
22977
|
});
|
|
22896
22978
|
/**
|
|
22979
|
+
* `recordingExport` cap — render a footage time range into a single downloadable
|
|
22980
|
+
* MP4 (regular / accelerated / decelerated / timelapse, ± audio), kept for a
|
|
22981
|
+
* bounded lifetime with a durable history, auto-expiry, and optional
|
|
22982
|
+
* delete-after-download.
|
|
22983
|
+
*
|
|
22984
|
+
* Like `recording` this is a `scope:'system', mode:'singleton'` cap: the
|
|
22985
|
+
* recorder self-gates so exactly ONE node (the designated `recordingNodeId`,
|
|
22986
|
+
* default `hub`) registers it. Device-scoped methods carry `deviceId` in their
|
|
22987
|
+
* input and dispatch to that single provider; the render runs on the node that
|
|
22988
|
+
* owns the footage (no cross-node segment transfer). Download rides the
|
|
22989
|
+
* framework addon data-plane. State persists in a DurableState blob (NOT
|
|
22990
|
+
* SQLite); history rows survive file deletion for audit.
|
|
22991
|
+
*/
|
|
22992
|
+
/** Playback-speed multiplier for the render (1 = realtime). */
|
|
22993
|
+
var ExportSpeedSchema = number().min(.25).max(32);
|
|
22994
|
+
/** Timelapse cadence — sample one source frame per `everyMs`, output at `outputFps`. */
|
|
22995
|
+
var ExportTimelapseSchema = object({
|
|
22996
|
+
everyMs: number().int().positive(),
|
|
22997
|
+
outputFps: number().int().min(1).max(60).optional()
|
|
22998
|
+
});
|
|
22999
|
+
/**
|
|
23000
|
+
* Render options. `speed` and `timelapse` are mutually exclusive. `includeAudio`
|
|
23001
|
+
* is honoured only for a realtime-ish speed (0.5–2×); timelapse is always
|
|
23002
|
+
* silent. `maxLifeMs` bounds how long the finished file is kept;
|
|
23003
|
+
* `deleteAfterDownload` removes it shortly after the first complete download.
|
|
23004
|
+
*/
|
|
23005
|
+
var ExportOptionsSchema = object({
|
|
23006
|
+
speed: ExportSpeedSchema.optional(),
|
|
23007
|
+
timelapse: ExportTimelapseSchema.optional(),
|
|
23008
|
+
includeAudio: boolean(),
|
|
23009
|
+
maxLifeMs: number().int().positive(),
|
|
23010
|
+
deleteAfterDownload: boolean(),
|
|
23011
|
+
title: string().max(200).optional()
|
|
23012
|
+
}).superRefine((v, ctx) => {
|
|
23013
|
+
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
23014
|
+
code: ZodIssueCode.custom,
|
|
23015
|
+
message: "speed and timelapse are mutually exclusive",
|
|
23016
|
+
path: ["timelapse"]
|
|
23017
|
+
});
|
|
23018
|
+
});
|
|
23019
|
+
var ExportStateSchema = _enum([
|
|
23020
|
+
"queued",
|
|
23021
|
+
"rendering",
|
|
23022
|
+
"ready",
|
|
23023
|
+
"failed",
|
|
23024
|
+
"expired",
|
|
23025
|
+
"deleted"
|
|
23026
|
+
]);
|
|
23027
|
+
/** One export job / history row. */
|
|
23028
|
+
var ExportRecordSchema = object({
|
|
23029
|
+
id: string(),
|
|
23030
|
+
deviceId: number(),
|
|
23031
|
+
profile: string(),
|
|
23032
|
+
fromMs: number(),
|
|
23033
|
+
toMs: number(),
|
|
23034
|
+
options: ExportOptionsSchema,
|
|
23035
|
+
state: ExportStateSchema,
|
|
23036
|
+
/** 0–100 while rendering; null otherwise. */
|
|
23037
|
+
progressPct: number().nullable(),
|
|
23038
|
+
/** File size once ready; null before. */
|
|
23039
|
+
fileBytes: number().nullable(),
|
|
23040
|
+
expiresAt: number(),
|
|
23041
|
+
deleteAfterDownload: boolean(),
|
|
23042
|
+
/** Epoch of the first complete download; null until then. */
|
|
23043
|
+
downloadedAt: number().nullable(),
|
|
23044
|
+
createdAt: number(),
|
|
23045
|
+
/** User id/name that requested the export. */
|
|
23046
|
+
createdBy: string(),
|
|
23047
|
+
/** Failure reason when state is 'failed'; null otherwise. */
|
|
23048
|
+
error: string().nullable()
|
|
23049
|
+
});
|
|
23050
|
+
/** Candidate download URLs (LAN first, then operator extra hosts). */
|
|
23051
|
+
var ExportDownloadSchema = object({
|
|
23052
|
+
url: string(),
|
|
23053
|
+
endpoints: array(string())
|
|
23054
|
+
});
|
|
23055
|
+
method(object({
|
|
23056
|
+
deviceId: number(),
|
|
23057
|
+
profile: string(),
|
|
23058
|
+
fromMs: number(),
|
|
23059
|
+
toMs: number(),
|
|
23060
|
+
options: ExportOptionsSchema
|
|
23061
|
+
}), ExportRecordSchema, {
|
|
23062
|
+
kind: "mutation",
|
|
23063
|
+
auth: "protected"
|
|
23064
|
+
}), method(object({ deviceId: number().optional() }), array(ExportRecordSchema), {
|
|
23065
|
+
kind: "query",
|
|
23066
|
+
auth: "protected"
|
|
23067
|
+
}), method(object({ exportId: string() }), ExportRecordSchema, {
|
|
23068
|
+
kind: "query",
|
|
23069
|
+
auth: "protected"
|
|
23070
|
+
}), method(object({ exportId: string() }), ExportRecordSchema, {
|
|
23071
|
+
kind: "mutation",
|
|
23072
|
+
auth: "protected"
|
|
23073
|
+
}), method(object({ exportId: string() }), ExportRecordSchema, {
|
|
23074
|
+
kind: "mutation",
|
|
23075
|
+
auth: "protected"
|
|
23076
|
+
}), method(object({ exportId: string() }), ExportDownloadSchema, {
|
|
23077
|
+
kind: "query",
|
|
23078
|
+
auth: "protected"
|
|
23079
|
+
});
|
|
23080
|
+
/**
|
|
22897
23081
|
* One publishable camera stream as its OWNING PROVIDER describes it — the same
|
|
22898
23082
|
* payload the (legacy) push `streamBroker.publishCameraStream` carried, minus
|
|
22899
23083
|
* `deviceId`.
|
|
@@ -26817,6 +27001,42 @@ Object.freeze({
|
|
|
26817
27001
|
addonId: null,
|
|
26818
27002
|
access: "create"
|
|
26819
27003
|
},
|
|
27004
|
+
"recordingExport.cancelExport": {
|
|
27005
|
+
capName: "recordingExport",
|
|
27006
|
+
capScope: "system",
|
|
27007
|
+
addonId: null,
|
|
27008
|
+
access: "create"
|
|
27009
|
+
},
|
|
27010
|
+
"recordingExport.createExport": {
|
|
27011
|
+
capName: "recordingExport",
|
|
27012
|
+
capScope: "system",
|
|
27013
|
+
addonId: null,
|
|
27014
|
+
access: "create"
|
|
27015
|
+
},
|
|
27016
|
+
"recordingExport.deleteExport": {
|
|
27017
|
+
capName: "recordingExport",
|
|
27018
|
+
capScope: "system",
|
|
27019
|
+
addonId: null,
|
|
27020
|
+
access: "delete"
|
|
27021
|
+
},
|
|
27022
|
+
"recordingExport.getDownloadUrl": {
|
|
27023
|
+
capName: "recordingExport",
|
|
27024
|
+
capScope: "system",
|
|
27025
|
+
addonId: null,
|
|
27026
|
+
access: "view"
|
|
27027
|
+
},
|
|
27028
|
+
"recordingExport.getExport": {
|
|
27029
|
+
capName: "recordingExport",
|
|
27030
|
+
capScope: "system",
|
|
27031
|
+
addonId: null,
|
|
27032
|
+
access: "view"
|
|
27033
|
+
},
|
|
27034
|
+
"recordingExport.listExports": {
|
|
27035
|
+
capName: "recordingExport",
|
|
27036
|
+
capScope: "system",
|
|
27037
|
+
addonId: null,
|
|
27038
|
+
access: "view"
|
|
27039
|
+
},
|
|
26820
27040
|
"sceneMonitor.captureReference": {
|
|
26821
27041
|
capName: "scene-monitor",
|
|
26822
27042
|
capScope: "device",
|
|
@@ -32801,7 +33021,7 @@ var BUS_EVENT_TYPES = [
|
|
|
32801
33021
|
"deconz_event",
|
|
32802
33022
|
"xiaomi_aqara.click"
|
|
32803
33023
|
];
|
|
32804
|
-
var HaIntegrationManager = class {
|
|
33024
|
+
var HaIntegrationManager = class HaIntegrationManager {
|
|
32805
33025
|
opts;
|
|
32806
33026
|
wsClient = null;
|
|
32807
33027
|
connection = null;
|
|
@@ -32815,6 +33035,15 @@ var HaIntegrationManager = class {
|
|
|
32815
33035
|
lastCheckedAt = null;
|
|
32816
33036
|
lastError = null;
|
|
32817
33037
|
haInfo = {};
|
|
33038
|
+
/** Deadline timer for the INITIAL connect. `start()` no longer blocks on
|
|
33039
|
+
* the socket (see the comment there), so a broker whose host silently
|
|
33040
|
+
* drops packets would otherwise sit at `connecting` for the full OS TCP
|
|
33041
|
+
* timeout (tens of seconds) with no UI signal. This timer flips it to
|
|
33042
|
+
* `unreachable` promptly; a late successful connect still overrides it. */
|
|
33043
|
+
connectDeadlineTimer = null;
|
|
33044
|
+
/** Ceiling on the `connecting` state before the UI is shown `unreachable`.
|
|
33045
|
+
* Slightly above the throwaway `testSettings` probe timeout (8s). */
|
|
33046
|
+
static CONNECT_DEADLINE_MS = 1e4;
|
|
32818
33047
|
constructor(opts) {
|
|
32819
33048
|
this.opts = opts;
|
|
32820
33049
|
}
|
|
@@ -32828,6 +33057,8 @@ var HaIntegrationManager = class {
|
|
|
32828
33057
|
await this.refreshHaConfigInfo(connection);
|
|
32829
33058
|
await this.ensureBaselineSubscription();
|
|
32830
33059
|
await this.ensureBusEventSubscriptions();
|
|
33060
|
+
this.clearConnectDeadline();
|
|
33061
|
+
this.lastError = null;
|
|
32831
33062
|
this.setStatus("connected");
|
|
32832
33063
|
try {
|
|
32833
33064
|
this.opts.onConnected?.(this.opts.id);
|
|
@@ -32863,14 +33094,32 @@ var HaIntegrationManager = class {
|
|
|
32863
33094
|
}
|
|
32864
33095
|
}
|
|
32865
33096
|
});
|
|
32866
|
-
|
|
32867
|
-
|
|
32868
|
-
} catch (err) {
|
|
33097
|
+
this.startConnectDeadline();
|
|
33098
|
+
this.wsClient.start().catch((err) => {
|
|
32869
33099
|
this.classifyAuthError(err);
|
|
32870
|
-
|
|
33100
|
+
});
|
|
33101
|
+
}
|
|
33102
|
+
/** Arm the initial-connect deadline. Idempotent — clears any prior timer. */
|
|
33103
|
+
startConnectDeadline() {
|
|
33104
|
+
this.clearConnectDeadline();
|
|
33105
|
+
const timer = setTimeout(() => {
|
|
33106
|
+
this.connectDeadlineTimer = null;
|
|
33107
|
+
if (this.status === "connecting") {
|
|
33108
|
+
if (this.lastError === null) this.lastError = "Connection timed out";
|
|
33109
|
+
this.setStatus("unreachable");
|
|
33110
|
+
}
|
|
33111
|
+
}, HaIntegrationManager.CONNECT_DEADLINE_MS);
|
|
33112
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
33113
|
+
this.connectDeadlineTimer = timer;
|
|
33114
|
+
}
|
|
33115
|
+
clearConnectDeadline() {
|
|
33116
|
+
if (this.connectDeadlineTimer) {
|
|
33117
|
+
clearTimeout(this.connectDeadlineTimer);
|
|
33118
|
+
this.connectDeadlineTimer = null;
|
|
32871
33119
|
}
|
|
32872
33120
|
}
|
|
32873
33121
|
async stop() {
|
|
33122
|
+
this.clearConnectDeadline();
|
|
32874
33123
|
if (this.upstreamSubscriptionUnsub) {
|
|
32875
33124
|
try {
|
|
32876
33125
|
await this.upstreamSubscriptionUnsub();
|
|
@@ -33085,15 +33334,19 @@ var HaIntegrationManager = class {
|
|
|
33085
33334
|
}));
|
|
33086
33335
|
}
|
|
33087
33336
|
classifyAuthError(err) {
|
|
33337
|
+
this.clearConnectDeadline();
|
|
33088
33338
|
if (err === 2) {
|
|
33339
|
+
this.lastError = "Authentication failed — check the access token";
|
|
33089
33340
|
this.setStatus("auth-failed");
|
|
33090
|
-
|
|
33091
|
-
|
|
33341
|
+
} else if (err === 1) {
|
|
33342
|
+
this.lastError = "Cannot connect — host unreachable or Home Assistant not responding";
|
|
33343
|
+
this.setStatus("unreachable");
|
|
33344
|
+
} else if (err === 4) {
|
|
33345
|
+
this.lastError = "Home Assistant host / URL is required";
|
|
33092
33346
|
this.setStatus("unreachable");
|
|
33093
|
-
this.lastError = errMsg(err);
|
|
33094
33347
|
} else {
|
|
33095
|
-
this.setStatus("error");
|
|
33096
33348
|
this.lastError = errMsg(err);
|
|
33349
|
+
this.setStatus("error");
|
|
33097
33350
|
}
|
|
33098
33351
|
}
|
|
33099
33352
|
requireConnection() {
|
package/dist/addon.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
|
|
3
|
-
//#region ../types/dist/event-category-
|
|
3
|
+
//#region ../types/dist/event-category-D4HJq7Mw.mjs
|
|
4
4
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
5
5
|
EventCategory["SystemBoot"] = "system.boot";
|
|
6
6
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -153,6 +153,11 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
153
153
|
/** Runner-sampled scrub thumbnail (~1/5 s/camera). Telemetry (D8): a lost
|
|
154
154
|
* thumb is a scrub gap the recorder's keyframe backfill covers. */
|
|
155
155
|
EventCategory["RecordingThumbSampled"] = "recording.thumb-sampled";
|
|
156
|
+
/** Export render progress (0–100). Telemetry (D8): a lost tick is a stale
|
|
157
|
+
* progress bar the client reconciles via `recordingExport.getExport`. */
|
|
158
|
+
EventCategory["RecordingExportProgress"] = "recording.export.progress";
|
|
159
|
+
EventCategory["RecordingExportCompleted"] = "recording.export.completed";
|
|
160
|
+
EventCategory["RecordingExportFailed"] = "recording.export.failed";
|
|
156
161
|
EventCategory["DetectionEvent"] = "detection.event";
|
|
157
162
|
EventCategory["SessionTrackNew"] = "session.track.new";
|
|
158
163
|
EventCategory["SessionTrackExpired"] = "session.track.expired";
|
|
@@ -442,6 +447,25 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
442
447
|
*/
|
|
443
448
|
EventCategory["PipelineAnalyticsStationaryChanged"] = "pipeline-analytics.stationary-changed";
|
|
444
449
|
/**
|
|
450
|
+
* Fired by `addon-post-analysis` when a package-drop is confirmed inside
|
|
451
|
+
* a package zone — a newly-appeared stationary object of a package class
|
|
452
|
+
* that cleared the class / zone / dwell / size gates. Payload:
|
|
453
|
+
* `PipelineAnalyticsPackageDeliveredPayload` carrying `{ deviceId,
|
|
454
|
+
* entryId, className, zoneIds, keyFrameMediaKey?, bbox, timestamp }`.
|
|
455
|
+
* Telemetry (D8): the durable record is the `package-events` store row;
|
|
456
|
+
* this bus topic drives notifier rules + live UI. See
|
|
457
|
+
* docs/superpowers/specs/2026-07-17-package-zones-design.md §5.1.
|
|
458
|
+
*/
|
|
459
|
+
EventCategory["PipelineAnalyticsPackageDelivered"] = "pipeline-analytics.package-delivered";
|
|
460
|
+
/**
|
|
461
|
+
* Fired by `addon-post-analysis` when a previously-delivered package
|
|
462
|
+
* leaves its zone (the stationary entry departed — moved or swept).
|
|
463
|
+
* Payload: `PipelineAnalyticsPackagePickedUpPayload` carrying
|
|
464
|
+
* `{ deviceId, entryId, deliveredEventId, className, timestamp }`.
|
|
465
|
+
* Telemetry (D8). See package-zones-design §5.2.
|
|
466
|
+
*/
|
|
467
|
+
EventCategory["PipelineAnalyticsPackagePickedUp"] = "pipeline-analytics.package-picked-up";
|
|
468
|
+
/**
|
|
445
469
|
* Fired by `addon-post-analysis` whenever a gallery face row changes:
|
|
446
470
|
* `kind:'buffered'` a new detected face was persisted, `'assigned'` /
|
|
447
471
|
* `'unassigned'` its identity link changed, `'deleted'` the row was
|
|
@@ -5180,6 +5204,25 @@ function _instanceof(cls, params = {}) {
|
|
|
5180
5204
|
};
|
|
5181
5205
|
return inst;
|
|
5182
5206
|
}
|
|
5207
|
+
//#endregion
|
|
5208
|
+
//#region ../../node_modules/zod/v4/classic/compat.js
|
|
5209
|
+
/** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
|
|
5210
|
+
var ZodIssueCode = {
|
|
5211
|
+
invalid_type: "invalid_type",
|
|
5212
|
+
too_big: "too_big",
|
|
5213
|
+
too_small: "too_small",
|
|
5214
|
+
invalid_format: "invalid_format",
|
|
5215
|
+
not_multiple_of: "not_multiple_of",
|
|
5216
|
+
unrecognized_keys: "unrecognized_keys",
|
|
5217
|
+
invalid_union: "invalid_union",
|
|
5218
|
+
invalid_key: "invalid_key",
|
|
5219
|
+
invalid_element: "invalid_element",
|
|
5220
|
+
invalid_value: "invalid_value",
|
|
5221
|
+
custom: "custom"
|
|
5222
|
+
};
|
|
5223
|
+
/** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */
|
|
5224
|
+
var ZodFirstPartyTypeKind;
|
|
5225
|
+
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5183
5226
|
Object.fromEntries([
|
|
5184
5227
|
{
|
|
5185
5228
|
id: "overview",
|
|
@@ -7355,6 +7398,24 @@ var RecordingRetentionSchema = object({
|
|
|
7355
7398
|
maxSizeGb: number().min(0).optional()
|
|
7356
7399
|
});
|
|
7357
7400
|
/**
|
|
7401
|
+
* Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
|
|
7402
|
+
* sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
|
|
7403
|
+
* previews at. Five graduated steps; absent on a config = `standard` (the
|
|
7404
|
+
* shipped default, matching `sheet-geometry`/`sheet-composer`).
|
|
7405
|
+
*
|
|
7406
|
+
* Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
|
|
7407
|
+
* Each window's index sidecar carries its own tile dims, so a camera whose
|
|
7408
|
+
* preset changed over time renders every historical window at the dims it was
|
|
7409
|
+
* written with.
|
|
7410
|
+
*/
|
|
7411
|
+
var ScrubThumbnailPresetSchema = _enum([
|
|
7412
|
+
"minimal",
|
|
7413
|
+
"low",
|
|
7414
|
+
"standard",
|
|
7415
|
+
"high",
|
|
7416
|
+
"max"
|
|
7417
|
+
]);
|
|
7418
|
+
/**
|
|
7358
7419
|
* The full per-camera recording intent — the wire shape of a RecordingTarget.
|
|
7359
7420
|
*
|
|
7360
7421
|
* `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
|
|
@@ -7391,7 +7452,14 @@ var RecordingConfigSchema = object({
|
|
|
7391
7452
|
* derived into bands once via `migrateConfigToBands`.
|
|
7392
7453
|
*/
|
|
7393
7454
|
bands: array(RecordingBandSchema).optional(),
|
|
7394
|
-
retention: RecordingRetentionSchema.optional()
|
|
7455
|
+
retention: RecordingRetentionSchema.optional(),
|
|
7456
|
+
/**
|
|
7457
|
+
* Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
|
|
7458
|
+
* timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
|
|
7459
|
+
* windows only — existing sheets are immutable, and each window's index
|
|
7460
|
+
* carries its own tile dims so mixed-preset history renders correctly.
|
|
7461
|
+
*/
|
|
7462
|
+
scrubThumbnails: ScrubThumbnailPresetSchema.optional()
|
|
7395
7463
|
});
|
|
7396
7464
|
/**
|
|
7397
7465
|
* `StorageLocationType` — an addon-declared id that identifies the *kind* of
|
|
@@ -14987,7 +15055,11 @@ array(ZoneRuleSchema).readonly();
|
|
|
14987
15055
|
* Extend the enum here when a new gating consumer comes online (audio
|
|
14988
15056
|
* gating, alert filtering, …) — no other surface needs to change.
|
|
14989
15057
|
*/
|
|
14990
|
-
var ZoneRuleStageEnum = _enum([
|
|
15058
|
+
var ZoneRuleStageEnum = _enum([
|
|
15059
|
+
"motion",
|
|
15060
|
+
"detection",
|
|
15061
|
+
"package"
|
|
15062
|
+
]);
|
|
14991
15063
|
/**
|
|
14992
15064
|
* Runtime registry: cap-property-name → cap definition. `BaseDevice`'s
|
|
14993
15065
|
* `state` getter looks up the cap definition here to construct a
|
|
@@ -15081,16 +15153,24 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
15081
15153
|
})
|
|
15082
15154
|
},
|
|
15083
15155
|
/**
|
|
15084
|
-
* Runtime-state slice —
|
|
15156
|
+
* Runtime-state slice — every stage mirrored together so consumers
|
|
15085
15157
|
* see one reactive handle (`device.state.zoneRules.value`) instead
|
|
15086
|
-
* of
|
|
15087
|
-
* `{motion, detection}` shape, so subscribers always get the
|
|
15158
|
+
* of one per stage. Bulk-replace mutations on any stage write the full
|
|
15159
|
+
* `{motion, detection, package}` shape, so subscribers always get the
|
|
15088
15160
|
* complete current set. Consumers that only care about one stage
|
|
15089
15161
|
* just read the matching property.
|
|
15162
|
+
*
|
|
15163
|
+
* `package` backs the package-drop detector — a package zone is a
|
|
15164
|
+
* `ZoneRule` on the `'package'` stage referencing drawn polygons
|
|
15165
|
+
* (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
|
|
15166
|
+
* The orchestrator provider that writes this stage lands in a later
|
|
15167
|
+
* slice; until then the mirror carries only `{motion, detection}` and
|
|
15168
|
+
* consumers read `package` as absent (treat as `[]`).
|
|
15090
15169
|
*/
|
|
15091
15170
|
runtimeState: object({
|
|
15092
15171
|
motion: array(ZoneRuleSchema).readonly(),
|
|
15093
|
-
detection: array(ZoneRuleSchema).readonly()
|
|
15172
|
+
detection: array(ZoneRuleSchema).readonly(),
|
|
15173
|
+
package: array(ZoneRuleSchema).readonly()
|
|
15094
15174
|
})
|
|
15095
15175
|
},
|
|
15096
15176
|
zones: zonesCapability
|
|
@@ -19151,6 +19231,7 @@ var EventKindIconSchema = _enum([
|
|
|
19151
19231
|
"smoke",
|
|
19152
19232
|
"water",
|
|
19153
19233
|
"button",
|
|
19234
|
+
"package",
|
|
19154
19235
|
"generic"
|
|
19155
19236
|
]);
|
|
19156
19237
|
var EventKindCategorySchema = _enum([
|
|
@@ -19158,7 +19239,8 @@ var EventKindCategorySchema = _enum([
|
|
|
19158
19239
|
"audio",
|
|
19159
19240
|
"detection",
|
|
19160
19241
|
"sensor",
|
|
19161
|
-
"custom"
|
|
19242
|
+
"custom",
|
|
19243
|
+
"package"
|
|
19162
19244
|
]);
|
|
19163
19245
|
var EventKindDescriptorSchema = object({
|
|
19164
19246
|
/** Stable kind id (e.g. 'motion', 'person', 'contact'). */
|
|
@@ -22893,6 +22975,108 @@ method(object({
|
|
|
22893
22975
|
auth: "admin"
|
|
22894
22976
|
});
|
|
22895
22977
|
/**
|
|
22978
|
+
* `recordingExport` cap — render a footage time range into a single downloadable
|
|
22979
|
+
* MP4 (regular / accelerated / decelerated / timelapse, ± audio), kept for a
|
|
22980
|
+
* bounded lifetime with a durable history, auto-expiry, and optional
|
|
22981
|
+
* delete-after-download.
|
|
22982
|
+
*
|
|
22983
|
+
* Like `recording` this is a `scope:'system', mode:'singleton'` cap: the
|
|
22984
|
+
* recorder self-gates so exactly ONE node (the designated `recordingNodeId`,
|
|
22985
|
+
* default `hub`) registers it. Device-scoped methods carry `deviceId` in their
|
|
22986
|
+
* input and dispatch to that single provider; the render runs on the node that
|
|
22987
|
+
* owns the footage (no cross-node segment transfer). Download rides the
|
|
22988
|
+
* framework addon data-plane. State persists in a DurableState blob (NOT
|
|
22989
|
+
* SQLite); history rows survive file deletion for audit.
|
|
22990
|
+
*/
|
|
22991
|
+
/** Playback-speed multiplier for the render (1 = realtime). */
|
|
22992
|
+
var ExportSpeedSchema = number().min(.25).max(32);
|
|
22993
|
+
/** Timelapse cadence — sample one source frame per `everyMs`, output at `outputFps`. */
|
|
22994
|
+
var ExportTimelapseSchema = object({
|
|
22995
|
+
everyMs: number().int().positive(),
|
|
22996
|
+
outputFps: number().int().min(1).max(60).optional()
|
|
22997
|
+
});
|
|
22998
|
+
/**
|
|
22999
|
+
* Render options. `speed` and `timelapse` are mutually exclusive. `includeAudio`
|
|
23000
|
+
* is honoured only for a realtime-ish speed (0.5–2×); timelapse is always
|
|
23001
|
+
* silent. `maxLifeMs` bounds how long the finished file is kept;
|
|
23002
|
+
* `deleteAfterDownload` removes it shortly after the first complete download.
|
|
23003
|
+
*/
|
|
23004
|
+
var ExportOptionsSchema = object({
|
|
23005
|
+
speed: ExportSpeedSchema.optional(),
|
|
23006
|
+
timelapse: ExportTimelapseSchema.optional(),
|
|
23007
|
+
includeAudio: boolean(),
|
|
23008
|
+
maxLifeMs: number().int().positive(),
|
|
23009
|
+
deleteAfterDownload: boolean(),
|
|
23010
|
+
title: string().max(200).optional()
|
|
23011
|
+
}).superRefine((v, ctx) => {
|
|
23012
|
+
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
23013
|
+
code: ZodIssueCode.custom,
|
|
23014
|
+
message: "speed and timelapse are mutually exclusive",
|
|
23015
|
+
path: ["timelapse"]
|
|
23016
|
+
});
|
|
23017
|
+
});
|
|
23018
|
+
var ExportStateSchema = _enum([
|
|
23019
|
+
"queued",
|
|
23020
|
+
"rendering",
|
|
23021
|
+
"ready",
|
|
23022
|
+
"failed",
|
|
23023
|
+
"expired",
|
|
23024
|
+
"deleted"
|
|
23025
|
+
]);
|
|
23026
|
+
/** One export job / history row. */
|
|
23027
|
+
var ExportRecordSchema = object({
|
|
23028
|
+
id: string(),
|
|
23029
|
+
deviceId: number(),
|
|
23030
|
+
profile: string(),
|
|
23031
|
+
fromMs: number(),
|
|
23032
|
+
toMs: number(),
|
|
23033
|
+
options: ExportOptionsSchema,
|
|
23034
|
+
state: ExportStateSchema,
|
|
23035
|
+
/** 0–100 while rendering; null otherwise. */
|
|
23036
|
+
progressPct: number().nullable(),
|
|
23037
|
+
/** File size once ready; null before. */
|
|
23038
|
+
fileBytes: number().nullable(),
|
|
23039
|
+
expiresAt: number(),
|
|
23040
|
+
deleteAfterDownload: boolean(),
|
|
23041
|
+
/** Epoch of the first complete download; null until then. */
|
|
23042
|
+
downloadedAt: number().nullable(),
|
|
23043
|
+
createdAt: number(),
|
|
23044
|
+
/** User id/name that requested the export. */
|
|
23045
|
+
createdBy: string(),
|
|
23046
|
+
/** Failure reason when state is 'failed'; null otherwise. */
|
|
23047
|
+
error: string().nullable()
|
|
23048
|
+
});
|
|
23049
|
+
/** Candidate download URLs (LAN first, then operator extra hosts). */
|
|
23050
|
+
var ExportDownloadSchema = object({
|
|
23051
|
+
url: string(),
|
|
23052
|
+
endpoints: array(string())
|
|
23053
|
+
});
|
|
23054
|
+
method(object({
|
|
23055
|
+
deviceId: number(),
|
|
23056
|
+
profile: string(),
|
|
23057
|
+
fromMs: number(),
|
|
23058
|
+
toMs: number(),
|
|
23059
|
+
options: ExportOptionsSchema
|
|
23060
|
+
}), ExportRecordSchema, {
|
|
23061
|
+
kind: "mutation",
|
|
23062
|
+
auth: "protected"
|
|
23063
|
+
}), method(object({ deviceId: number().optional() }), array(ExportRecordSchema), {
|
|
23064
|
+
kind: "query",
|
|
23065
|
+
auth: "protected"
|
|
23066
|
+
}), method(object({ exportId: string() }), ExportRecordSchema, {
|
|
23067
|
+
kind: "query",
|
|
23068
|
+
auth: "protected"
|
|
23069
|
+
}), method(object({ exportId: string() }), ExportRecordSchema, {
|
|
23070
|
+
kind: "mutation",
|
|
23071
|
+
auth: "protected"
|
|
23072
|
+
}), method(object({ exportId: string() }), ExportRecordSchema, {
|
|
23073
|
+
kind: "mutation",
|
|
23074
|
+
auth: "protected"
|
|
23075
|
+
}), method(object({ exportId: string() }), ExportDownloadSchema, {
|
|
23076
|
+
kind: "query",
|
|
23077
|
+
auth: "protected"
|
|
23078
|
+
});
|
|
23079
|
+
/**
|
|
22896
23080
|
* One publishable camera stream as its OWNING PROVIDER describes it — the same
|
|
22897
23081
|
* payload the (legacy) push `streamBroker.publishCameraStream` carried, minus
|
|
22898
23082
|
* `deviceId`.
|
|
@@ -26816,6 +27000,42 @@ Object.freeze({
|
|
|
26816
27000
|
addonId: null,
|
|
26817
27001
|
access: "create"
|
|
26818
27002
|
},
|
|
27003
|
+
"recordingExport.cancelExport": {
|
|
27004
|
+
capName: "recordingExport",
|
|
27005
|
+
capScope: "system",
|
|
27006
|
+
addonId: null,
|
|
27007
|
+
access: "create"
|
|
27008
|
+
},
|
|
27009
|
+
"recordingExport.createExport": {
|
|
27010
|
+
capName: "recordingExport",
|
|
27011
|
+
capScope: "system",
|
|
27012
|
+
addonId: null,
|
|
27013
|
+
access: "create"
|
|
27014
|
+
},
|
|
27015
|
+
"recordingExport.deleteExport": {
|
|
27016
|
+
capName: "recordingExport",
|
|
27017
|
+
capScope: "system",
|
|
27018
|
+
addonId: null,
|
|
27019
|
+
access: "delete"
|
|
27020
|
+
},
|
|
27021
|
+
"recordingExport.getDownloadUrl": {
|
|
27022
|
+
capName: "recordingExport",
|
|
27023
|
+
capScope: "system",
|
|
27024
|
+
addonId: null,
|
|
27025
|
+
access: "view"
|
|
27026
|
+
},
|
|
27027
|
+
"recordingExport.getExport": {
|
|
27028
|
+
capName: "recordingExport",
|
|
27029
|
+
capScope: "system",
|
|
27030
|
+
addonId: null,
|
|
27031
|
+
access: "view"
|
|
27032
|
+
},
|
|
27033
|
+
"recordingExport.listExports": {
|
|
27034
|
+
capName: "recordingExport",
|
|
27035
|
+
capScope: "system",
|
|
27036
|
+
addonId: null,
|
|
27037
|
+
access: "view"
|
|
27038
|
+
},
|
|
26819
27039
|
"sceneMonitor.captureReference": {
|
|
26820
27040
|
capName: "scene-monitor",
|
|
26821
27041
|
capScope: "device",
|
|
@@ -32800,7 +33020,7 @@ var BUS_EVENT_TYPES = [
|
|
|
32800
33020
|
"deconz_event",
|
|
32801
33021
|
"xiaomi_aqara.click"
|
|
32802
33022
|
];
|
|
32803
|
-
var HaIntegrationManager = class {
|
|
33023
|
+
var HaIntegrationManager = class HaIntegrationManager {
|
|
32804
33024
|
opts;
|
|
32805
33025
|
wsClient = null;
|
|
32806
33026
|
connection = null;
|
|
@@ -32814,6 +33034,15 @@ var HaIntegrationManager = class {
|
|
|
32814
33034
|
lastCheckedAt = null;
|
|
32815
33035
|
lastError = null;
|
|
32816
33036
|
haInfo = {};
|
|
33037
|
+
/** Deadline timer for the INITIAL connect. `start()` no longer blocks on
|
|
33038
|
+
* the socket (see the comment there), so a broker whose host silently
|
|
33039
|
+
* drops packets would otherwise sit at `connecting` for the full OS TCP
|
|
33040
|
+
* timeout (tens of seconds) with no UI signal. This timer flips it to
|
|
33041
|
+
* `unreachable` promptly; a late successful connect still overrides it. */
|
|
33042
|
+
connectDeadlineTimer = null;
|
|
33043
|
+
/** Ceiling on the `connecting` state before the UI is shown `unreachable`.
|
|
33044
|
+
* Slightly above the throwaway `testSettings` probe timeout (8s). */
|
|
33045
|
+
static CONNECT_DEADLINE_MS = 1e4;
|
|
32817
33046
|
constructor(opts) {
|
|
32818
33047
|
this.opts = opts;
|
|
32819
33048
|
}
|
|
@@ -32827,6 +33056,8 @@ var HaIntegrationManager = class {
|
|
|
32827
33056
|
await this.refreshHaConfigInfo(connection);
|
|
32828
33057
|
await this.ensureBaselineSubscription();
|
|
32829
33058
|
await this.ensureBusEventSubscriptions();
|
|
33059
|
+
this.clearConnectDeadline();
|
|
33060
|
+
this.lastError = null;
|
|
32830
33061
|
this.setStatus("connected");
|
|
32831
33062
|
try {
|
|
32832
33063
|
this.opts.onConnected?.(this.opts.id);
|
|
@@ -32862,14 +33093,32 @@ var HaIntegrationManager = class {
|
|
|
32862
33093
|
}
|
|
32863
33094
|
}
|
|
32864
33095
|
});
|
|
32865
|
-
|
|
32866
|
-
|
|
32867
|
-
} catch (err) {
|
|
33096
|
+
this.startConnectDeadline();
|
|
33097
|
+
this.wsClient.start().catch((err) => {
|
|
32868
33098
|
this.classifyAuthError(err);
|
|
32869
|
-
|
|
33099
|
+
});
|
|
33100
|
+
}
|
|
33101
|
+
/** Arm the initial-connect deadline. Idempotent — clears any prior timer. */
|
|
33102
|
+
startConnectDeadline() {
|
|
33103
|
+
this.clearConnectDeadline();
|
|
33104
|
+
const timer = setTimeout(() => {
|
|
33105
|
+
this.connectDeadlineTimer = null;
|
|
33106
|
+
if (this.status === "connecting") {
|
|
33107
|
+
if (this.lastError === null) this.lastError = "Connection timed out";
|
|
33108
|
+
this.setStatus("unreachable");
|
|
33109
|
+
}
|
|
33110
|
+
}, HaIntegrationManager.CONNECT_DEADLINE_MS);
|
|
33111
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
33112
|
+
this.connectDeadlineTimer = timer;
|
|
33113
|
+
}
|
|
33114
|
+
clearConnectDeadline() {
|
|
33115
|
+
if (this.connectDeadlineTimer) {
|
|
33116
|
+
clearTimeout(this.connectDeadlineTimer);
|
|
33117
|
+
this.connectDeadlineTimer = null;
|
|
32870
33118
|
}
|
|
32871
33119
|
}
|
|
32872
33120
|
async stop() {
|
|
33121
|
+
this.clearConnectDeadline();
|
|
32873
33122
|
if (this.upstreamSubscriptionUnsub) {
|
|
32874
33123
|
try {
|
|
32875
33124
|
await this.upstreamSubscriptionUnsub();
|
|
@@ -33084,15 +33333,19 @@ var HaIntegrationManager = class {
|
|
|
33084
33333
|
}));
|
|
33085
33334
|
}
|
|
33086
33335
|
classifyAuthError(err) {
|
|
33336
|
+
this.clearConnectDeadline();
|
|
33087
33337
|
if (err === 2) {
|
|
33338
|
+
this.lastError = "Authentication failed — check the access token";
|
|
33088
33339
|
this.setStatus("auth-failed");
|
|
33089
|
-
|
|
33090
|
-
|
|
33340
|
+
} else if (err === 1) {
|
|
33341
|
+
this.lastError = "Cannot connect — host unreachable or Home Assistant not responding";
|
|
33342
|
+
this.setStatus("unreachable");
|
|
33343
|
+
} else if (err === 4) {
|
|
33344
|
+
this.lastError = "Home Assistant host / URL is required";
|
|
33091
33345
|
this.setStatus("unreachable");
|
|
33092
|
-
this.lastError = errMsg(err);
|
|
33093
33346
|
} else {
|
|
33094
|
-
this.setStatus("error");
|
|
33095
33347
|
this.lastError = errMsg(err);
|
|
33348
|
+
this.setStatus("error");
|
|
33096
33349
|
}
|
|
33097
33350
|
}
|
|
33098
33351
|
requireConnection() {
|