@camstack/addon-provider-homeassistant 1.1.25 → 1.1.26
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 +352 -10
- package/dist/addon.mjs +352 -10
- package/package.json +4 -1
package/dist/addon.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let node_crypto = require("node:crypto");
|
|
3
|
+
let node_zlib = require("node:zlib");
|
|
3
4
|
//#region ../../node_modules/zod/v4/core/core.js
|
|
4
5
|
var _a$1;
|
|
5
6
|
function $constructor(name, initializer, params) {
|
|
@@ -4637,7 +4638,7 @@ function _instanceof(cls, params = {}) {
|
|
|
4637
4638
|
return inst;
|
|
4638
4639
|
}
|
|
4639
4640
|
//#endregion
|
|
4640
|
-
//#region ../types/dist/sleep-
|
|
4641
|
+
//#region ../types/dist/sleep-_sv7WKkq.mjs
|
|
4641
4642
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4642
4643
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4643
4644
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -5068,6 +5069,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
5068
5069
|
EventCategory["PipelineAnalyticsDetectionEvent"] = "pipeline-analytics.detection-event";
|
|
5069
5070
|
EventCategory["PipelineAnalyticsFrameTracked"] = "pipeline-analytics.frame-tracked";
|
|
5070
5071
|
/**
|
|
5072
|
+
* Fired by `addon-post-analysis` when a parked (stationary) object appears
|
|
5073
|
+
* (a track was promoted to a stationary-registry entry) or departs (the
|
|
5074
|
+
* object moved / was removed). Telemetry (D8): lossy, drives a UI refresh of
|
|
5075
|
+
* the dedicated "Stationary" section — never the live event feed. Payload:
|
|
5076
|
+
* `{ deviceId, entryId, className, phase:'appeared'|'departed', timestamp }`.
|
|
5077
|
+
*/
|
|
5078
|
+
EventCategory["PipelineAnalyticsStationaryChanged"] = "pipeline-analytics.stationary-changed";
|
|
5079
|
+
/**
|
|
5071
5080
|
* Fired by `addon-post-analysis` whenever a gallery face row changes:
|
|
5072
5081
|
* `kind:'buffered'` a new detected face was persisted, `'assigned'` /
|
|
5073
5082
|
* `'unassigned'` its identity link changed, `'deleted'` the row was
|
|
@@ -7015,6 +7024,15 @@ var ModelCatalogEntrySchema = object({
|
|
|
7015
7024
|
width: number(),
|
|
7016
7025
|
height: number()
|
|
7017
7026
|
}),
|
|
7027
|
+
/**
|
|
7028
|
+
* Channel count of the model input tensor. Omit ⇒ 3 (RGB), the default for
|
|
7029
|
+
* every detector / classifier / embedder. Set to 1 for a grayscale CTC text
|
|
7030
|
+
* recognizer (EasyOCR VGG plate-OCR: input `[N,1,H,W]`) so the preprocess
|
|
7031
|
+
* feeds a single-channel, EasyOCR-normalized tensor instead of the default
|
|
7032
|
+
* 3-channel RGB one. Threaded through `PoolModelConfig.inputChannels` to the
|
|
7033
|
+
* Python inference pool.
|
|
7034
|
+
*/
|
|
7035
|
+
inputChannels: number().int().positive().optional(),
|
|
7018
7036
|
labels: array(LabelDefinitionSchema).readonly(),
|
|
7019
7037
|
inputLayout: _enum(["nchw", "nhwc"]).optional(),
|
|
7020
7038
|
inputNormalization: _enum([
|
|
@@ -7024,6 +7042,16 @@ var ModelCatalogEntrySchema = object({
|
|
|
7024
7042
|
]).optional(),
|
|
7025
7043
|
preprocessMode: _enum(["letterbox", "resize"]).optional(),
|
|
7026
7044
|
/**
|
|
7045
|
+
* Per-MODEL postprocessor override. Absent ⇒ the step's own
|
|
7046
|
+
* `StepDefinition.postprocessor` applies (the normal case — every model in a
|
|
7047
|
+
* step shares its decode). Set it when a step hosts models with DIFFERENT raw
|
|
7048
|
+
* output layouts under one slot: e.g. object-detection is `'yolo'` by default,
|
|
7049
|
+
* but a Coral SSD MobileNet build emits the `TFLite_Detection_PostProcess`
|
|
7050
|
+
* 4-tensor layout and needs `'ssd'`. Threaded into `PoolModelConfig.postprocessor`
|
|
7051
|
+
* by the engine factory (`modelEntry.postprocessor ?? def.postprocessor`).
|
|
7052
|
+
*/
|
|
7053
|
+
postprocessor: custom().optional(),
|
|
7054
|
+
/**
|
|
7027
7055
|
* When true, the executor produces a landmark-aligned crop (similarity warp
|
|
7028
7056
|
* onto the canonical template) before this step runs, instead of a plain
|
|
7029
7057
|
* axis-aligned bbox crop. Required for face-recognition embedders (ArcFace):
|
|
@@ -7113,6 +7141,123 @@ var ConvertResultSchema = object({
|
|
|
7113
7141
|
})).readonly()
|
|
7114
7142
|
});
|
|
7115
7143
|
/**
|
|
7144
|
+
* Build an `IAddonRouteProvider` from a list of routes. Implements
|
|
7145
|
+
* both the operator-facing `getRoutes` (returning route descriptors
|
|
7146
|
+
* minus the handlers, which can't cross JSON) and the framework-
|
|
7147
|
+
* private `invoke` method that the hub calls when this provider lives
|
|
7148
|
+
* in a forked worker.
|
|
7149
|
+
*
|
|
7150
|
+
* Co-located addons use the returned `getRoutes` directly because
|
|
7151
|
+
* their handlers don't need to cross any wire. The `invoke` method
|
|
7152
|
+
* is present anyway so the bridge code on the hub is uniform — it
|
|
7153
|
+
* doesn't need to switch on "local vs remote provider" at the call
|
|
7154
|
+
* site.
|
|
7155
|
+
*
|
|
7156
|
+
* Example:
|
|
7157
|
+
* const routes: IAddonHttpRoute[] = [
|
|
7158
|
+
* { method: 'GET', path: '/start', access: 'public', handler: this.handleStart },
|
|
7159
|
+
* ]
|
|
7160
|
+
* return [
|
|
7161
|
+
* {
|
|
7162
|
+
* capability: addonRoutesCapability,
|
|
7163
|
+
* provider: buildAddonRouteProvider('auth-oidc', routes),
|
|
7164
|
+
* },
|
|
7165
|
+
* ]
|
|
7166
|
+
*/
|
|
7167
|
+
function buildAddonRouteProvider(id, routes) {
|
|
7168
|
+
return {
|
|
7169
|
+
id,
|
|
7170
|
+
getRoutes: () => routes,
|
|
7171
|
+
invoke: async (input) => {
|
|
7172
|
+
const match = matchRoute(routes, input.method, input.path);
|
|
7173
|
+
if (!match) return {
|
|
7174
|
+
status: 404,
|
|
7175
|
+
headers: {},
|
|
7176
|
+
redirectUrl: null,
|
|
7177
|
+
body: { error: `No route matches ${input.method} ${input.path}` }
|
|
7178
|
+
};
|
|
7179
|
+
const envelope = {
|
|
7180
|
+
status: 200,
|
|
7181
|
+
headers: {},
|
|
7182
|
+
redirectUrl: null
|
|
7183
|
+
};
|
|
7184
|
+
const reply = buildCapturingReply(envelope);
|
|
7185
|
+
const request = {
|
|
7186
|
+
params: {
|
|
7187
|
+
...input.params,
|
|
7188
|
+
...match.params
|
|
7189
|
+
},
|
|
7190
|
+
query: input.query,
|
|
7191
|
+
body: input.body,
|
|
7192
|
+
headers: input.headers,
|
|
7193
|
+
...input.user ? { user: input.user } : {},
|
|
7194
|
+
...input.scopedToken !== void 0 ? { scopedToken: input.scopedToken } : {}
|
|
7195
|
+
};
|
|
7196
|
+
await match.route.handler(request, reply);
|
|
7197
|
+
return envelope;
|
|
7198
|
+
}
|
|
7199
|
+
};
|
|
7200
|
+
}
|
|
7201
|
+
/**
|
|
7202
|
+
* Pattern matcher: same semantics as `AddonRouteRegistry.matchRoute`
|
|
7203
|
+
* but operating on a flat list and bypassing the `/addon/<id>/` prefix
|
|
7204
|
+
* — the bridge sends the post-prefix path directly so we don't need
|
|
7205
|
+
* to round-trip it through normalization.
|
|
7206
|
+
*/
|
|
7207
|
+
function matchRoute(routes, method, path) {
|
|
7208
|
+
const normalizedMethod = method.toUpperCase();
|
|
7209
|
+
for (const route of routes) {
|
|
7210
|
+
if (route.method !== normalizedMethod) continue;
|
|
7211
|
+
const params = matchPath(route.path, path);
|
|
7212
|
+
if (params !== null) return {
|
|
7213
|
+
route,
|
|
7214
|
+
params
|
|
7215
|
+
};
|
|
7216
|
+
}
|
|
7217
|
+
return null;
|
|
7218
|
+
}
|
|
7219
|
+
function matchPath(pattern, p) {
|
|
7220
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
7221
|
+
const pathParts = p.split("/").filter(Boolean);
|
|
7222
|
+
if (patternParts.length !== pathParts.length) return null;
|
|
7223
|
+
const params = {};
|
|
7224
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
7225
|
+
const a = patternParts[i];
|
|
7226
|
+
const b = pathParts[i];
|
|
7227
|
+
if (a.startsWith(":")) params[a.slice(1)] = b;
|
|
7228
|
+
else if (a !== b) return null;
|
|
7229
|
+
}
|
|
7230
|
+
return params;
|
|
7231
|
+
}
|
|
7232
|
+
function buildCapturingReply(envelope) {
|
|
7233
|
+
const wrapper = {
|
|
7234
|
+
status(code) {
|
|
7235
|
+
envelope.status = code;
|
|
7236
|
+
return wrapper;
|
|
7237
|
+
},
|
|
7238
|
+
code(code) {
|
|
7239
|
+
envelope.status = code;
|
|
7240
|
+
return wrapper;
|
|
7241
|
+
},
|
|
7242
|
+
send(data) {
|
|
7243
|
+
envelope.body = data;
|
|
7244
|
+
},
|
|
7245
|
+
redirect(url) {
|
|
7246
|
+
envelope.redirectUrl = url;
|
|
7247
|
+
if (envelope.status === 200) envelope.status = 302;
|
|
7248
|
+
},
|
|
7249
|
+
header(name, value) {
|
|
7250
|
+
envelope.headers[name.toLowerCase()] = value;
|
|
7251
|
+
return wrapper;
|
|
7252
|
+
},
|
|
7253
|
+
type(mime) {
|
|
7254
|
+
envelope.contentType = mime;
|
|
7255
|
+
return wrapper;
|
|
7256
|
+
}
|
|
7257
|
+
};
|
|
7258
|
+
return wrapper;
|
|
7259
|
+
}
|
|
7260
|
+
/**
|
|
7116
7261
|
* Numeric day-of-week: 0 = Sunday … 6 = Saturday (matches `Date.getDay`).
|
|
7117
7262
|
* Named `RecordingWeekday` to avoid collision with the string-union
|
|
7118
7263
|
* `Weekday` exported from `interfaces/timezones.ts`.
|
|
@@ -12063,7 +12208,8 @@ var EngineProvisioningSchema = object({
|
|
|
12063
12208
|
runtimeId: _enum([
|
|
12064
12209
|
"onnx",
|
|
12065
12210
|
"openvino",
|
|
12066
|
-
"coreml"
|
|
12211
|
+
"coreml",
|
|
12212
|
+
"edgetpu"
|
|
12067
12213
|
]).nullable(),
|
|
12068
12214
|
device: string().nullable(),
|
|
12069
12215
|
state: _enum([
|
|
@@ -14596,6 +14742,36 @@ var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
|
|
|
14596
14742
|
* with the per-track detail panel and live overlay. */
|
|
14597
14743
|
trackIds: array(string()).readonly()
|
|
14598
14744
|
});
|
|
14745
|
+
/**
|
|
14746
|
+
* A parked ("stationary") object surfaced alongside occupancy — an object that
|
|
14747
|
+
* settled and stopped moving. It is NO LONGER a tracked object (the tracker was
|
|
14748
|
+
* told to forget it so it stops re-spawning tracks/events), but it IS still
|
|
14749
|
+
* physically present, so it keeps counting toward `frame` occupancy and is
|
|
14750
|
+
* listed here so the UI can show it in a dedicated "Stationary" section instead
|
|
14751
|
+
* of flooding the live event feed.
|
|
14752
|
+
*/
|
|
14753
|
+
var StationaryObjectSchema = object({
|
|
14754
|
+
id: string(),
|
|
14755
|
+
className: string(),
|
|
14756
|
+
bbox: object({
|
|
14757
|
+
x: number(),
|
|
14758
|
+
y: number(),
|
|
14759
|
+
w: number(),
|
|
14760
|
+
h: number()
|
|
14761
|
+
}),
|
|
14762
|
+
frameWidth: number().int().nonnegative(),
|
|
14763
|
+
frameHeight: number().int().nonnegative(),
|
|
14764
|
+
/** When the source track was first seen. */
|
|
14765
|
+
firstSeenAt: number().int(),
|
|
14766
|
+
/** When the object was recognised as parked (promotion time). */
|
|
14767
|
+
becameStationaryAt: number().int(),
|
|
14768
|
+
/** Last frame a detection confirmed the object is still there. */
|
|
14769
|
+
lastConfirmedAt: number().int(),
|
|
14770
|
+
/** Enrichment label carried from the source track (identity / plate). */
|
|
14771
|
+
label: string().optional(),
|
|
14772
|
+
/** Native-resolution key-frame media key for the parked object's best image. */
|
|
14773
|
+
keyFrameMediaKey: string().optional()
|
|
14774
|
+
});
|
|
14599
14775
|
var CameraOccupancySnapshotSchema = object({
|
|
14600
14776
|
/** Frame timestamp of the inference result that produced this snapshot. */
|
|
14601
14777
|
ts: number().int(),
|
|
@@ -14604,10 +14780,15 @@ var CameraOccupancySnapshotSchema = object({
|
|
|
14604
14780
|
frameHeight: number().int().nonnegative(),
|
|
14605
14781
|
/** Per-zone breakdown — one entry per defined zone (user + onboard). */
|
|
14606
14782
|
zones: array(ZoneScopeBreakdownSchema).readonly(),
|
|
14607
|
-
/** Frame-wide aggregate (everywhere, regardless of zone membership).
|
|
14783
|
+
/** Frame-wide aggregate (everywhere, regardless of zone membership).
|
|
14784
|
+
* INCLUDES currently-confirmed stationary objects (they are still present). */
|
|
14608
14785
|
frame: PerScopeBreakdownSchema,
|
|
14609
14786
|
/** Detections that landed outside every zone. Empty when no zones defined. */
|
|
14610
|
-
unzoned: PerScopeBreakdownSchema
|
|
14787
|
+
unzoned: PerScopeBreakdownSchema,
|
|
14788
|
+
/** Parked objects on this camera (additive — absent on legacy snapshots).
|
|
14789
|
+
* Surfaced separately so the UI shows them in a dedicated section rather
|
|
14790
|
+
* than as repeated tracks/events. */
|
|
14791
|
+
stationaryObjects: array(StationaryObjectSchema).readonly().optional()
|
|
14611
14792
|
});
|
|
14612
14793
|
/**
|
|
14613
14794
|
* Time-series resolution. The history methods return one bucket per
|
|
@@ -15895,7 +16076,26 @@ var InvokeReplyEnvelopeSchema = object({
|
|
|
15895
16076
|
/** Set when the handler called `reply.type(mime)`. */
|
|
15896
16077
|
contentType: string().optional()
|
|
15897
16078
|
});
|
|
15898
|
-
|
|
16079
|
+
var addonRoutesCapability = {
|
|
16080
|
+
name: "addon-routes",
|
|
16081
|
+
scope: "system",
|
|
16082
|
+
mode: "collection",
|
|
16083
|
+
internal: true,
|
|
16084
|
+
methods: {
|
|
16085
|
+
getRoutes: method(_void(), array(AddonHttpRouteSchema)),
|
|
16086
|
+
/**
|
|
16087
|
+
* Cross-process dispatch entry point. Forked addons implement this
|
|
16088
|
+
* (via `buildAddonRouteProvider`) so the hub's Fastify catch-all
|
|
16089
|
+
* can route through Moleculer when the handler lives in a worker.
|
|
16090
|
+
*
|
|
16091
|
+
* Local addons can implement it for free with the same helper;
|
|
16092
|
+
* the hub bypasses the wire on co-located addons.
|
|
16093
|
+
*/
|
|
16094
|
+
invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
|
|
16095
|
+
},
|
|
16096
|
+
/** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
|
|
16097
|
+
mount: { kind: "skip" }
|
|
16098
|
+
};
|
|
15899
16099
|
var ConfigTabDeclarationSchema = object({
|
|
15900
16100
|
id: string(),
|
|
15901
16101
|
label: string(),
|
|
@@ -18916,6 +19116,22 @@ var TrackSnapshotSchema = object({
|
|
|
18916
19116
|
/** MediaStore key; resolve via `getTrackMedia({ trackId })`. */
|
|
18917
19117
|
mediaKey: string()
|
|
18918
19118
|
});
|
|
19119
|
+
/**
|
|
19120
|
+
* One audio-classification label heard on the track's camera while the
|
|
19121
|
+
* track was alive, aggregated per label. An "episode" is one persisted
|
|
19122
|
+
* audio event (the confident-classification path: score ≥ the device's
|
|
19123
|
+
* `classificationMinScore`, class-change-or-heartbeat coalesced) — NOT
|
|
19124
|
+
* one 32 ms inference chunk, so counts stay human-scaled.
|
|
19125
|
+
*/
|
|
19126
|
+
var TrackAudioLabelSchema = object({
|
|
19127
|
+
label: string(),
|
|
19128
|
+
/** Highest classification score observed across the label's episodes. */
|
|
19129
|
+
peakScore: number(),
|
|
19130
|
+
/** Number of coalesced audio-event episodes carrying this label. */
|
|
19131
|
+
count: number(),
|
|
19132
|
+
firstAt: number(),
|
|
19133
|
+
lastAt: number()
|
|
19134
|
+
});
|
|
18919
19135
|
var TrackSchema = object({
|
|
18920
19136
|
trackId: string(),
|
|
18921
19137
|
deviceId: number(),
|
|
@@ -18947,7 +19163,11 @@ var TrackSchema = object({
|
|
|
18947
19163
|
bestEventId: string().optional(),
|
|
18948
19164
|
/** Tag of the importance sub-signal that dominated the score
|
|
18949
19165
|
* (identity|dwell|proximity|class|confidence|travel|zone). */
|
|
18950
|
-
importanceReason: string().optional()
|
|
19166
|
+
importanceReason: string().optional(),
|
|
19167
|
+
/** Audio-classification labels heard on the camera during the track's
|
|
19168
|
+
* life (score ≥ device `classificationMinScore`), aggregated per label.
|
|
19169
|
+
* Absent on legacy rows / tracks with no confident audio. */
|
|
19170
|
+
audioLabels: array(TrackAudioLabelSchema).readonly().optional()
|
|
18951
19171
|
});
|
|
18952
19172
|
var BaseEventFields = {
|
|
18953
19173
|
id: string(),
|
|
@@ -22257,6 +22477,10 @@ var GpuInfoSchema = object({
|
|
|
22257
22477
|
memoryMB: number().optional()
|
|
22258
22478
|
});
|
|
22259
22479
|
var NpuInfoSchema = object({ type: _enum(["apple-ane", "intel-npu"]) });
|
|
22480
|
+
var CoralInfoSchema = object({
|
|
22481
|
+
type: literal("coral-edgetpu"),
|
|
22482
|
+
bus: string().optional()
|
|
22483
|
+
});
|
|
22260
22484
|
var HardwareInfoSchema = object({
|
|
22261
22485
|
platform: HardwarePlatformSchema,
|
|
22262
22486
|
arch: HardwareArchSchema,
|
|
@@ -22265,7 +22489,8 @@ var HardwareInfoSchema = object({
|
|
|
22265
22489
|
totalRAM_MB: number(),
|
|
22266
22490
|
availableRAM_MB: number(),
|
|
22267
22491
|
gpu: GpuInfoSchema.nullable(),
|
|
22268
|
-
npu: NpuInfoSchema.nullable()
|
|
22492
|
+
npu: NpuInfoSchema.nullable(),
|
|
22493
|
+
coral: CoralInfoSchema.nullable().optional()
|
|
22269
22494
|
});
|
|
22270
22495
|
var PlatformScoreSchema = object({
|
|
22271
22496
|
runtime: _enum(["node", "python"]),
|
|
@@ -33859,6 +34084,90 @@ function deriveIntegrationDomain(platforms) {
|
|
|
33859
34084
|
return best ?? present[0] ?? null;
|
|
33860
34085
|
}
|
|
33861
34086
|
//#endregion
|
|
34087
|
+
//#region src/ha-notification-icon.ts
|
|
34088
|
+
/**
|
|
34089
|
+
* Bundled brand icon for the `homeassistant` notification-output kind, served
|
|
34090
|
+
* by THIS addon over its own `addon-routes` HTTP surface. Mirrors the pattern
|
|
34091
|
+
* `addon-notifiers` uses for the other notifier kinds (each provider serves the
|
|
34092
|
+
* icon for the kinds it contributes), so `notificationOutput.listTargetKinds`
|
|
34093
|
+
* can stamp a self-hosted `iconUrl` onto the `homeassistant` descriptor and the
|
|
34094
|
+
* admin UI renders `<img src=iconUrl>` instead of a text label.
|
|
34095
|
+
*
|
|
34096
|
+
* GET /addon/provider-homeassistant/icons/homeassistant → image/svg+xml
|
|
34097
|
+
*
|
|
34098
|
+
* The bytes ship INSIDE the addon (no external CDN / hotlink) so the icon is
|
|
34099
|
+
* self-contained and CSP-safe. The SVG is a standalone document (declares the
|
|
34100
|
+
* default `xmlns`, a `viewBox`, and an intrinsic `fill`) with NO editor-tool
|
|
34101
|
+
* namespaces, so a browser `<img src>` renders it in its strict SVG mode.
|
|
34102
|
+
*
|
|
34103
|
+
* We register ONE CONCRETE static route (`/icons/homeassistant`), never a
|
|
34104
|
+
* `/:kind` param route: the forked-addon route bridge re-matches the pattern
|
|
34105
|
+
* against itself and would clobber a `:kind` path param with the literal token
|
|
34106
|
+
* `':kind'`. A static path carries no param, so it routes correctly across the
|
|
34107
|
+
* process boundary.
|
|
34108
|
+
*
|
|
34109
|
+
* The route negotiates `Accept-Encoding` and stamps `content-encoding` itself
|
|
34110
|
+
* (serving a pre-computed br/gzip/deflate variant, or identity). This makes the
|
|
34111
|
+
* hub's global `@fastify/compress` skip the response — on the forked-addon route
|
|
34112
|
+
* bridge that compressor emits an EMPTY brotli stream for any compressible body
|
|
34113
|
+
* over its 1 KiB threshold, which shows as a broken `<img>`. Owning the coding
|
|
34114
|
+
* keeps this icon robust even if its bytes ever grow past the threshold, and
|
|
34115
|
+
* matches how `addon-notifiers` serves its notifier-kind icons.
|
|
34116
|
+
*/
|
|
34117
|
+
/** The single notifier kind this addon contributes an icon for. */
|
|
34118
|
+
var HA_ICON_KIND = "homeassistant";
|
|
34119
|
+
/** The addon id — also the `/addon/<id>/…` route prefix. Mirrors the manifest id. */
|
|
34120
|
+
var HA_ROUTE_ID = "provider-homeassistant";
|
|
34121
|
+
/** One day; the icon is immutable per addon version. */
|
|
34122
|
+
var CACHE_CONTROL = "public, max-age=86400, immutable";
|
|
34123
|
+
/**
|
|
34124
|
+
* Home Assistant's official brand mark (simple-icons `homeassistant`, CC0) —
|
|
34125
|
+
* the blue "house + circuit" glyph — tinted with the official HA brand hex
|
|
34126
|
+
* (#18BCF2) so it reads on the dark admin UI. A standalone SVG document, no
|
|
34127
|
+
* editor-tool namespaces.
|
|
34128
|
+
*/
|
|
34129
|
+
var HOME_ASSISTANT_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"#18BCF2\"><path d=\"M22.939 10.627 13.061.749a1.505 1.505 0 0 0-2.121 0l-9.879 9.878C.478 11.21 0 12.363 0 13.187v9c0 .826.675 1.5 1.5 1.5h9.227l-4.063-4.062a2.034 2.034 0 0 1-.664.113c-1.13 0-2.05-.92-2.05-2.05s.92-2.05 2.05-2.05 2.05.92 2.05 2.05c0 .233-.041.456-.113.665l3.163 3.163V9.928a2.05 2.05 0 0 1-1.15-1.84c0-1.13.92-2.05 2.05-2.05s2.05.92 2.05 2.05a2.05 2.05 0 0 1-1.15 1.84v8.127l3.146-3.146A2.051 2.051 0 0 1 18 12.239c1.13 0 2.05.92 2.05 2.05s-.92 2.05-2.05 2.05c-.25 0-.488-.047-.709-.13L12.9 20.602v3.088h9.6c.825 0 1.5-.675 1.5-1.5v-9c0-.825-.477-1.977-1.061-2.561z\"/></svg>";
|
|
34130
|
+
/**
|
|
34131
|
+
* The stable icon URL for the `homeassistant` kind. Stamped onto the descriptor
|
|
34132
|
+
* by `listTargetKinds`. Root-relative so it resolves against whatever origin
|
|
34133
|
+
* the admin UI is served from.
|
|
34134
|
+
*/
|
|
34135
|
+
var HA_NOTIFICATION_ICON_URL = `/addon/${HA_ROUTE_ID}/icons/${HA_ICON_KIND}`;
|
|
34136
|
+
/** The icon's identity bytes plus every content coding we may serve, computed
|
|
34137
|
+
* once at module load (the icon is immutable). */
|
|
34138
|
+
var HA_ICON_IDENTITY = Buffer.from(HOME_ASSISTANT_SVG, "utf8");
|
|
34139
|
+
var HA_ICON_ENCODED = {
|
|
34140
|
+
br: (0, node_zlib.brotliCompressSync)(HA_ICON_IDENTITY),
|
|
34141
|
+
gzip: (0, node_zlib.gzipSync)(HA_ICON_IDENTITY),
|
|
34142
|
+
deflate: (0, node_zlib.deflateSync)(HA_ICON_IDENTITY)
|
|
34143
|
+
};
|
|
34144
|
+
/** Pick the best coding the client accepts (br › gzip › deflate), or `null` for
|
|
34145
|
+
* identity when the header names none of them. */
|
|
34146
|
+
function negotiateEncoding(acceptEncoding) {
|
|
34147
|
+
const tokens = acceptEncoding.toLowerCase().split(",").map((part) => part.trim().split(";")[0]?.trim());
|
|
34148
|
+
for (const coding of [
|
|
34149
|
+
"br",
|
|
34150
|
+
"gzip",
|
|
34151
|
+
"deflate"
|
|
34152
|
+
]) if (tokens.includes(coding)) return coding;
|
|
34153
|
+
return null;
|
|
34154
|
+
}
|
|
34155
|
+
/** Build the `addon-routes` provider that serves the bundled Home Assistant icon. */
|
|
34156
|
+
function createHaIconRouteProvider() {
|
|
34157
|
+
return buildAddonRouteProvider(HA_ROUTE_ID, [{
|
|
34158
|
+
method: "GET",
|
|
34159
|
+
path: `/icons/${HA_ICON_KIND}`,
|
|
34160
|
+
access: "public",
|
|
34161
|
+
description: "Official brand icon (SVG) for the 'homeassistant' notifier kind.",
|
|
34162
|
+
handler: async (request, reply) => {
|
|
34163
|
+
const coding = negotiateEncoding(String(request.headers["accept-encoding"] ?? ""));
|
|
34164
|
+
reply.code(200).type("image/svg+xml").header("cache-control", CACHE_CONTROL).header("vary", "accept-encoding");
|
|
34165
|
+
if (coding !== null) reply.header("content-encoding", coding);
|
|
34166
|
+
reply.send(coding !== null ? HA_ICON_ENCODED[coding] : HA_ICON_IDENTITY);
|
|
34167
|
+
}
|
|
34168
|
+
}]);
|
|
34169
|
+
}
|
|
34170
|
+
//#endregion
|
|
33862
34171
|
//#region src/ha-notification-output.ts
|
|
33863
34172
|
/**
|
|
33864
34173
|
* `homeassistant` notification-output kind — hosted BY the HA provider addon.
|
|
@@ -34048,6 +34357,13 @@ function parseNotifyServices(result, brokerId) {
|
|
|
34048
34357
|
}
|
|
34049
34358
|
}));
|
|
34050
34359
|
}
|
|
34360
|
+
/** KV JSON-blob shape — a single `data` column routes the row through the
|
|
34361
|
+
* settings backend's canonical key/value path (id TEXT PK, data TEXT). */
|
|
34362
|
+
var KV_BLOB_COLUMNS = [{
|
|
34363
|
+
name: "data",
|
|
34364
|
+
type: "TEXT",
|
|
34365
|
+
notNull: true
|
|
34366
|
+
}];
|
|
34051
34367
|
function rowToTarget(row) {
|
|
34052
34368
|
const parsed = TargetSchema.safeParse({
|
|
34053
34369
|
id: row.id,
|
|
@@ -34072,6 +34388,12 @@ var HaTargetStore = class {
|
|
|
34072
34388
|
this.store = store;
|
|
34073
34389
|
this.collection = collection;
|
|
34074
34390
|
}
|
|
34391
|
+
/** Declare the backing collection before any read/write. The SQLite settings
|
|
34392
|
+
* backend rejects undeclared collections (fail-fast), so this MUST run before
|
|
34393
|
+
* `list`/`getById`/`upsert`/`delete` — mirrors addon-ai's ProfileStore.init. */
|
|
34394
|
+
async init() {
|
|
34395
|
+
await this.store.declareCollection(this.collection, KV_BLOB_COLUMNS);
|
|
34396
|
+
}
|
|
34075
34397
|
async list() {
|
|
34076
34398
|
const rows = await this.store.query(this.collection);
|
|
34077
34399
|
const out = [];
|
|
@@ -34138,6 +34460,12 @@ function createApiHaSettingsStorePort(api) {
|
|
|
34138
34460
|
collection,
|
|
34139
34461
|
key: id
|
|
34140
34462
|
});
|
|
34463
|
+
},
|
|
34464
|
+
declareCollection: async (collection, columns) => {
|
|
34465
|
+
await api.settingsStore.declareCollection.mutate({
|
|
34466
|
+
collection,
|
|
34467
|
+
columns: columns.map((c) => ({ ...c }))
|
|
34468
|
+
});
|
|
34141
34469
|
}
|
|
34142
34470
|
};
|
|
34143
34471
|
}
|
|
@@ -34168,6 +34496,9 @@ function createMemoryHaSettingsStorePort() {
|
|
|
34168
34496
|
},
|
|
34169
34497
|
remove: async (collection, id) => {
|
|
34170
34498
|
bucket(collection).delete(id);
|
|
34499
|
+
},
|
|
34500
|
+
declareCollection: async (collection) => {
|
|
34501
|
+
bucket(collection);
|
|
34171
34502
|
}
|
|
34172
34503
|
};
|
|
34173
34504
|
}
|
|
@@ -34237,7 +34568,12 @@ function createHaNotificationOutputProvider(deps) {
|
|
|
34237
34568
|
}
|
|
34238
34569
|
}
|
|
34239
34570
|
return {
|
|
34240
|
-
listTargetKinds: async () =>
|
|
34571
|
+
listTargetKinds: async () => {
|
|
34572
|
+
return [deps.iconUrl !== void 0 ? {
|
|
34573
|
+
...descriptor,
|
|
34574
|
+
iconUrl: deps.iconUrl
|
|
34575
|
+
} : descriptor];
|
|
34576
|
+
},
|
|
34241
34577
|
listTargets: async () => {
|
|
34242
34578
|
return (await store.list()).map((target) => ({
|
|
34243
34579
|
...target,
|
|
@@ -34803,17 +35139,23 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
34803
35139
|
},
|
|
34804
35140
|
{
|
|
34805
35141
|
capability: notificationOutputCapability,
|
|
34806
|
-
provider: this.buildNotificationOutputProvider()
|
|
35142
|
+
provider: await this.buildNotificationOutputProvider()
|
|
35143
|
+
},
|
|
35144
|
+
{
|
|
35145
|
+
capability: addonRoutesCapability,
|
|
35146
|
+
provider: createHaIconRouteProvider()
|
|
34807
35147
|
}
|
|
34808
35148
|
];
|
|
34809
35149
|
}
|
|
34810
|
-
buildNotificationOutputProvider() {
|
|
35150
|
+
async buildNotificationOutputProvider() {
|
|
34811
35151
|
const api = this.ctx.api;
|
|
34812
35152
|
const port = api ? createApiHaSettingsStorePort(api) : createMemoryHaSettingsStorePort();
|
|
34813
35153
|
if (!api) this.ctx.logger.warn("ha notification-output: no ctx.api — targets persist in-memory only");
|
|
34814
35154
|
const store = new HaTargetStore(port);
|
|
35155
|
+
await store.init();
|
|
34815
35156
|
return createHaNotificationOutputProvider({
|
|
34816
35157
|
addonId: this.ctx.id,
|
|
35158
|
+
iconUrl: HA_NOTIFICATION_ICON_URL,
|
|
34817
35159
|
store,
|
|
34818
35160
|
publish: async (brokerId, service, serviceData) => {
|
|
34819
35161
|
await this.requireRegistry().publish(brokerId, {
|
package/dist/addon.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
|
|
2
3
|
//#region ../../node_modules/zod/v4/core/core.js
|
|
3
4
|
var _a$1;
|
|
4
5
|
function $constructor(name, initializer, params) {
|
|
@@ -4636,7 +4637,7 @@ function _instanceof(cls, params = {}) {
|
|
|
4636
4637
|
return inst;
|
|
4637
4638
|
}
|
|
4638
4639
|
//#endregion
|
|
4639
|
-
//#region ../types/dist/sleep-
|
|
4640
|
+
//#region ../types/dist/sleep-_sv7WKkq.mjs
|
|
4640
4641
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4641
4642
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4642
4643
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -5067,6 +5068,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
5067
5068
|
EventCategory["PipelineAnalyticsDetectionEvent"] = "pipeline-analytics.detection-event";
|
|
5068
5069
|
EventCategory["PipelineAnalyticsFrameTracked"] = "pipeline-analytics.frame-tracked";
|
|
5069
5070
|
/**
|
|
5071
|
+
* Fired by `addon-post-analysis` when a parked (stationary) object appears
|
|
5072
|
+
* (a track was promoted to a stationary-registry entry) or departs (the
|
|
5073
|
+
* object moved / was removed). Telemetry (D8): lossy, drives a UI refresh of
|
|
5074
|
+
* the dedicated "Stationary" section — never the live event feed. Payload:
|
|
5075
|
+
* `{ deviceId, entryId, className, phase:'appeared'|'departed', timestamp }`.
|
|
5076
|
+
*/
|
|
5077
|
+
EventCategory["PipelineAnalyticsStationaryChanged"] = "pipeline-analytics.stationary-changed";
|
|
5078
|
+
/**
|
|
5070
5079
|
* Fired by `addon-post-analysis` whenever a gallery face row changes:
|
|
5071
5080
|
* `kind:'buffered'` a new detected face was persisted, `'assigned'` /
|
|
5072
5081
|
* `'unassigned'` its identity link changed, `'deleted'` the row was
|
|
@@ -7014,6 +7023,15 @@ var ModelCatalogEntrySchema = object({
|
|
|
7014
7023
|
width: number(),
|
|
7015
7024
|
height: number()
|
|
7016
7025
|
}),
|
|
7026
|
+
/**
|
|
7027
|
+
* Channel count of the model input tensor. Omit ⇒ 3 (RGB), the default for
|
|
7028
|
+
* every detector / classifier / embedder. Set to 1 for a grayscale CTC text
|
|
7029
|
+
* recognizer (EasyOCR VGG plate-OCR: input `[N,1,H,W]`) so the preprocess
|
|
7030
|
+
* feeds a single-channel, EasyOCR-normalized tensor instead of the default
|
|
7031
|
+
* 3-channel RGB one. Threaded through `PoolModelConfig.inputChannels` to the
|
|
7032
|
+
* Python inference pool.
|
|
7033
|
+
*/
|
|
7034
|
+
inputChannels: number().int().positive().optional(),
|
|
7017
7035
|
labels: array(LabelDefinitionSchema).readonly(),
|
|
7018
7036
|
inputLayout: _enum(["nchw", "nhwc"]).optional(),
|
|
7019
7037
|
inputNormalization: _enum([
|
|
@@ -7023,6 +7041,16 @@ var ModelCatalogEntrySchema = object({
|
|
|
7023
7041
|
]).optional(),
|
|
7024
7042
|
preprocessMode: _enum(["letterbox", "resize"]).optional(),
|
|
7025
7043
|
/**
|
|
7044
|
+
* Per-MODEL postprocessor override. Absent ⇒ the step's own
|
|
7045
|
+
* `StepDefinition.postprocessor` applies (the normal case — every model in a
|
|
7046
|
+
* step shares its decode). Set it when a step hosts models with DIFFERENT raw
|
|
7047
|
+
* output layouts under one slot: e.g. object-detection is `'yolo'` by default,
|
|
7048
|
+
* but a Coral SSD MobileNet build emits the `TFLite_Detection_PostProcess`
|
|
7049
|
+
* 4-tensor layout and needs `'ssd'`. Threaded into `PoolModelConfig.postprocessor`
|
|
7050
|
+
* by the engine factory (`modelEntry.postprocessor ?? def.postprocessor`).
|
|
7051
|
+
*/
|
|
7052
|
+
postprocessor: custom().optional(),
|
|
7053
|
+
/**
|
|
7026
7054
|
* When true, the executor produces a landmark-aligned crop (similarity warp
|
|
7027
7055
|
* onto the canonical template) before this step runs, instead of a plain
|
|
7028
7056
|
* axis-aligned bbox crop. Required for face-recognition embedders (ArcFace):
|
|
@@ -7112,6 +7140,123 @@ var ConvertResultSchema = object({
|
|
|
7112
7140
|
})).readonly()
|
|
7113
7141
|
});
|
|
7114
7142
|
/**
|
|
7143
|
+
* Build an `IAddonRouteProvider` from a list of routes. Implements
|
|
7144
|
+
* both the operator-facing `getRoutes` (returning route descriptors
|
|
7145
|
+
* minus the handlers, which can't cross JSON) and the framework-
|
|
7146
|
+
* private `invoke` method that the hub calls when this provider lives
|
|
7147
|
+
* in a forked worker.
|
|
7148
|
+
*
|
|
7149
|
+
* Co-located addons use the returned `getRoutes` directly because
|
|
7150
|
+
* their handlers don't need to cross any wire. The `invoke` method
|
|
7151
|
+
* is present anyway so the bridge code on the hub is uniform — it
|
|
7152
|
+
* doesn't need to switch on "local vs remote provider" at the call
|
|
7153
|
+
* site.
|
|
7154
|
+
*
|
|
7155
|
+
* Example:
|
|
7156
|
+
* const routes: IAddonHttpRoute[] = [
|
|
7157
|
+
* { method: 'GET', path: '/start', access: 'public', handler: this.handleStart },
|
|
7158
|
+
* ]
|
|
7159
|
+
* return [
|
|
7160
|
+
* {
|
|
7161
|
+
* capability: addonRoutesCapability,
|
|
7162
|
+
* provider: buildAddonRouteProvider('auth-oidc', routes),
|
|
7163
|
+
* },
|
|
7164
|
+
* ]
|
|
7165
|
+
*/
|
|
7166
|
+
function buildAddonRouteProvider(id, routes) {
|
|
7167
|
+
return {
|
|
7168
|
+
id,
|
|
7169
|
+
getRoutes: () => routes,
|
|
7170
|
+
invoke: async (input) => {
|
|
7171
|
+
const match = matchRoute(routes, input.method, input.path);
|
|
7172
|
+
if (!match) return {
|
|
7173
|
+
status: 404,
|
|
7174
|
+
headers: {},
|
|
7175
|
+
redirectUrl: null,
|
|
7176
|
+
body: { error: `No route matches ${input.method} ${input.path}` }
|
|
7177
|
+
};
|
|
7178
|
+
const envelope = {
|
|
7179
|
+
status: 200,
|
|
7180
|
+
headers: {},
|
|
7181
|
+
redirectUrl: null
|
|
7182
|
+
};
|
|
7183
|
+
const reply = buildCapturingReply(envelope);
|
|
7184
|
+
const request = {
|
|
7185
|
+
params: {
|
|
7186
|
+
...input.params,
|
|
7187
|
+
...match.params
|
|
7188
|
+
},
|
|
7189
|
+
query: input.query,
|
|
7190
|
+
body: input.body,
|
|
7191
|
+
headers: input.headers,
|
|
7192
|
+
...input.user ? { user: input.user } : {},
|
|
7193
|
+
...input.scopedToken !== void 0 ? { scopedToken: input.scopedToken } : {}
|
|
7194
|
+
};
|
|
7195
|
+
await match.route.handler(request, reply);
|
|
7196
|
+
return envelope;
|
|
7197
|
+
}
|
|
7198
|
+
};
|
|
7199
|
+
}
|
|
7200
|
+
/**
|
|
7201
|
+
* Pattern matcher: same semantics as `AddonRouteRegistry.matchRoute`
|
|
7202
|
+
* but operating on a flat list and bypassing the `/addon/<id>/` prefix
|
|
7203
|
+
* — the bridge sends the post-prefix path directly so we don't need
|
|
7204
|
+
* to round-trip it through normalization.
|
|
7205
|
+
*/
|
|
7206
|
+
function matchRoute(routes, method, path) {
|
|
7207
|
+
const normalizedMethod = method.toUpperCase();
|
|
7208
|
+
for (const route of routes) {
|
|
7209
|
+
if (route.method !== normalizedMethod) continue;
|
|
7210
|
+
const params = matchPath(route.path, path);
|
|
7211
|
+
if (params !== null) return {
|
|
7212
|
+
route,
|
|
7213
|
+
params
|
|
7214
|
+
};
|
|
7215
|
+
}
|
|
7216
|
+
return null;
|
|
7217
|
+
}
|
|
7218
|
+
function matchPath(pattern, p) {
|
|
7219
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
7220
|
+
const pathParts = p.split("/").filter(Boolean);
|
|
7221
|
+
if (patternParts.length !== pathParts.length) return null;
|
|
7222
|
+
const params = {};
|
|
7223
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
7224
|
+
const a = patternParts[i];
|
|
7225
|
+
const b = pathParts[i];
|
|
7226
|
+
if (a.startsWith(":")) params[a.slice(1)] = b;
|
|
7227
|
+
else if (a !== b) return null;
|
|
7228
|
+
}
|
|
7229
|
+
return params;
|
|
7230
|
+
}
|
|
7231
|
+
function buildCapturingReply(envelope) {
|
|
7232
|
+
const wrapper = {
|
|
7233
|
+
status(code) {
|
|
7234
|
+
envelope.status = code;
|
|
7235
|
+
return wrapper;
|
|
7236
|
+
},
|
|
7237
|
+
code(code) {
|
|
7238
|
+
envelope.status = code;
|
|
7239
|
+
return wrapper;
|
|
7240
|
+
},
|
|
7241
|
+
send(data) {
|
|
7242
|
+
envelope.body = data;
|
|
7243
|
+
},
|
|
7244
|
+
redirect(url) {
|
|
7245
|
+
envelope.redirectUrl = url;
|
|
7246
|
+
if (envelope.status === 200) envelope.status = 302;
|
|
7247
|
+
},
|
|
7248
|
+
header(name, value) {
|
|
7249
|
+
envelope.headers[name.toLowerCase()] = value;
|
|
7250
|
+
return wrapper;
|
|
7251
|
+
},
|
|
7252
|
+
type(mime) {
|
|
7253
|
+
envelope.contentType = mime;
|
|
7254
|
+
return wrapper;
|
|
7255
|
+
}
|
|
7256
|
+
};
|
|
7257
|
+
return wrapper;
|
|
7258
|
+
}
|
|
7259
|
+
/**
|
|
7115
7260
|
* Numeric day-of-week: 0 = Sunday … 6 = Saturday (matches `Date.getDay`).
|
|
7116
7261
|
* Named `RecordingWeekday` to avoid collision with the string-union
|
|
7117
7262
|
* `Weekday` exported from `interfaces/timezones.ts`.
|
|
@@ -12062,7 +12207,8 @@ var EngineProvisioningSchema = object({
|
|
|
12062
12207
|
runtimeId: _enum([
|
|
12063
12208
|
"onnx",
|
|
12064
12209
|
"openvino",
|
|
12065
|
-
"coreml"
|
|
12210
|
+
"coreml",
|
|
12211
|
+
"edgetpu"
|
|
12066
12212
|
]).nullable(),
|
|
12067
12213
|
device: string().nullable(),
|
|
12068
12214
|
state: _enum([
|
|
@@ -14595,6 +14741,36 @@ var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
|
|
|
14595
14741
|
* with the per-track detail panel and live overlay. */
|
|
14596
14742
|
trackIds: array(string()).readonly()
|
|
14597
14743
|
});
|
|
14744
|
+
/**
|
|
14745
|
+
* A parked ("stationary") object surfaced alongside occupancy — an object that
|
|
14746
|
+
* settled and stopped moving. It is NO LONGER a tracked object (the tracker was
|
|
14747
|
+
* told to forget it so it stops re-spawning tracks/events), but it IS still
|
|
14748
|
+
* physically present, so it keeps counting toward `frame` occupancy and is
|
|
14749
|
+
* listed here so the UI can show it in a dedicated "Stationary" section instead
|
|
14750
|
+
* of flooding the live event feed.
|
|
14751
|
+
*/
|
|
14752
|
+
var StationaryObjectSchema = object({
|
|
14753
|
+
id: string(),
|
|
14754
|
+
className: string(),
|
|
14755
|
+
bbox: object({
|
|
14756
|
+
x: number(),
|
|
14757
|
+
y: number(),
|
|
14758
|
+
w: number(),
|
|
14759
|
+
h: number()
|
|
14760
|
+
}),
|
|
14761
|
+
frameWidth: number().int().nonnegative(),
|
|
14762
|
+
frameHeight: number().int().nonnegative(),
|
|
14763
|
+
/** When the source track was first seen. */
|
|
14764
|
+
firstSeenAt: number().int(),
|
|
14765
|
+
/** When the object was recognised as parked (promotion time). */
|
|
14766
|
+
becameStationaryAt: number().int(),
|
|
14767
|
+
/** Last frame a detection confirmed the object is still there. */
|
|
14768
|
+
lastConfirmedAt: number().int(),
|
|
14769
|
+
/** Enrichment label carried from the source track (identity / plate). */
|
|
14770
|
+
label: string().optional(),
|
|
14771
|
+
/** Native-resolution key-frame media key for the parked object's best image. */
|
|
14772
|
+
keyFrameMediaKey: string().optional()
|
|
14773
|
+
});
|
|
14598
14774
|
var CameraOccupancySnapshotSchema = object({
|
|
14599
14775
|
/** Frame timestamp of the inference result that produced this snapshot. */
|
|
14600
14776
|
ts: number().int(),
|
|
@@ -14603,10 +14779,15 @@ var CameraOccupancySnapshotSchema = object({
|
|
|
14603
14779
|
frameHeight: number().int().nonnegative(),
|
|
14604
14780
|
/** Per-zone breakdown — one entry per defined zone (user + onboard). */
|
|
14605
14781
|
zones: array(ZoneScopeBreakdownSchema).readonly(),
|
|
14606
|
-
/** Frame-wide aggregate (everywhere, regardless of zone membership).
|
|
14782
|
+
/** Frame-wide aggregate (everywhere, regardless of zone membership).
|
|
14783
|
+
* INCLUDES currently-confirmed stationary objects (they are still present). */
|
|
14607
14784
|
frame: PerScopeBreakdownSchema,
|
|
14608
14785
|
/** Detections that landed outside every zone. Empty when no zones defined. */
|
|
14609
|
-
unzoned: PerScopeBreakdownSchema
|
|
14786
|
+
unzoned: PerScopeBreakdownSchema,
|
|
14787
|
+
/** Parked objects on this camera (additive — absent on legacy snapshots).
|
|
14788
|
+
* Surfaced separately so the UI shows them in a dedicated section rather
|
|
14789
|
+
* than as repeated tracks/events. */
|
|
14790
|
+
stationaryObjects: array(StationaryObjectSchema).readonly().optional()
|
|
14610
14791
|
});
|
|
14611
14792
|
/**
|
|
14612
14793
|
* Time-series resolution. The history methods return one bucket per
|
|
@@ -15894,7 +16075,26 @@ var InvokeReplyEnvelopeSchema = object({
|
|
|
15894
16075
|
/** Set when the handler called `reply.type(mime)`. */
|
|
15895
16076
|
contentType: string().optional()
|
|
15896
16077
|
});
|
|
15897
|
-
|
|
16078
|
+
var addonRoutesCapability = {
|
|
16079
|
+
name: "addon-routes",
|
|
16080
|
+
scope: "system",
|
|
16081
|
+
mode: "collection",
|
|
16082
|
+
internal: true,
|
|
16083
|
+
methods: {
|
|
16084
|
+
getRoutes: method(_void(), array(AddonHttpRouteSchema)),
|
|
16085
|
+
/**
|
|
16086
|
+
* Cross-process dispatch entry point. Forked addons implement this
|
|
16087
|
+
* (via `buildAddonRouteProvider`) so the hub's Fastify catch-all
|
|
16088
|
+
* can route through Moleculer when the handler lives in a worker.
|
|
16089
|
+
*
|
|
16090
|
+
* Local addons can implement it for free with the same helper;
|
|
16091
|
+
* the hub bypasses the wire on co-located addons.
|
|
16092
|
+
*/
|
|
16093
|
+
invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
|
|
16094
|
+
},
|
|
16095
|
+
/** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
|
|
16096
|
+
mount: { kind: "skip" }
|
|
16097
|
+
};
|
|
15898
16098
|
var ConfigTabDeclarationSchema = object({
|
|
15899
16099
|
id: string(),
|
|
15900
16100
|
label: string(),
|
|
@@ -18915,6 +19115,22 @@ var TrackSnapshotSchema = object({
|
|
|
18915
19115
|
/** MediaStore key; resolve via `getTrackMedia({ trackId })`. */
|
|
18916
19116
|
mediaKey: string()
|
|
18917
19117
|
});
|
|
19118
|
+
/**
|
|
19119
|
+
* One audio-classification label heard on the track's camera while the
|
|
19120
|
+
* track was alive, aggregated per label. An "episode" is one persisted
|
|
19121
|
+
* audio event (the confident-classification path: score ≥ the device's
|
|
19122
|
+
* `classificationMinScore`, class-change-or-heartbeat coalesced) — NOT
|
|
19123
|
+
* one 32 ms inference chunk, so counts stay human-scaled.
|
|
19124
|
+
*/
|
|
19125
|
+
var TrackAudioLabelSchema = object({
|
|
19126
|
+
label: string(),
|
|
19127
|
+
/** Highest classification score observed across the label's episodes. */
|
|
19128
|
+
peakScore: number(),
|
|
19129
|
+
/** Number of coalesced audio-event episodes carrying this label. */
|
|
19130
|
+
count: number(),
|
|
19131
|
+
firstAt: number(),
|
|
19132
|
+
lastAt: number()
|
|
19133
|
+
});
|
|
18918
19134
|
var TrackSchema = object({
|
|
18919
19135
|
trackId: string(),
|
|
18920
19136
|
deviceId: number(),
|
|
@@ -18946,7 +19162,11 @@ var TrackSchema = object({
|
|
|
18946
19162
|
bestEventId: string().optional(),
|
|
18947
19163
|
/** Tag of the importance sub-signal that dominated the score
|
|
18948
19164
|
* (identity|dwell|proximity|class|confidence|travel|zone). */
|
|
18949
|
-
importanceReason: string().optional()
|
|
19165
|
+
importanceReason: string().optional(),
|
|
19166
|
+
/** Audio-classification labels heard on the camera during the track's
|
|
19167
|
+
* life (score ≥ device `classificationMinScore`), aggregated per label.
|
|
19168
|
+
* Absent on legacy rows / tracks with no confident audio. */
|
|
19169
|
+
audioLabels: array(TrackAudioLabelSchema).readonly().optional()
|
|
18950
19170
|
});
|
|
18951
19171
|
var BaseEventFields = {
|
|
18952
19172
|
id: string(),
|
|
@@ -22256,6 +22476,10 @@ var GpuInfoSchema = object({
|
|
|
22256
22476
|
memoryMB: number().optional()
|
|
22257
22477
|
});
|
|
22258
22478
|
var NpuInfoSchema = object({ type: _enum(["apple-ane", "intel-npu"]) });
|
|
22479
|
+
var CoralInfoSchema = object({
|
|
22480
|
+
type: literal("coral-edgetpu"),
|
|
22481
|
+
bus: string().optional()
|
|
22482
|
+
});
|
|
22259
22483
|
var HardwareInfoSchema = object({
|
|
22260
22484
|
platform: HardwarePlatformSchema,
|
|
22261
22485
|
arch: HardwareArchSchema,
|
|
@@ -22264,7 +22488,8 @@ var HardwareInfoSchema = object({
|
|
|
22264
22488
|
totalRAM_MB: number(),
|
|
22265
22489
|
availableRAM_MB: number(),
|
|
22266
22490
|
gpu: GpuInfoSchema.nullable(),
|
|
22267
|
-
npu: NpuInfoSchema.nullable()
|
|
22491
|
+
npu: NpuInfoSchema.nullable(),
|
|
22492
|
+
coral: CoralInfoSchema.nullable().optional()
|
|
22268
22493
|
});
|
|
22269
22494
|
var PlatformScoreSchema = object({
|
|
22270
22495
|
runtime: _enum(["node", "python"]),
|
|
@@ -33858,6 +34083,90 @@ function deriveIntegrationDomain(platforms) {
|
|
|
33858
34083
|
return best ?? present[0] ?? null;
|
|
33859
34084
|
}
|
|
33860
34085
|
//#endregion
|
|
34086
|
+
//#region src/ha-notification-icon.ts
|
|
34087
|
+
/**
|
|
34088
|
+
* Bundled brand icon for the `homeassistant` notification-output kind, served
|
|
34089
|
+
* by THIS addon over its own `addon-routes` HTTP surface. Mirrors the pattern
|
|
34090
|
+
* `addon-notifiers` uses for the other notifier kinds (each provider serves the
|
|
34091
|
+
* icon for the kinds it contributes), so `notificationOutput.listTargetKinds`
|
|
34092
|
+
* can stamp a self-hosted `iconUrl` onto the `homeassistant` descriptor and the
|
|
34093
|
+
* admin UI renders `<img src=iconUrl>` instead of a text label.
|
|
34094
|
+
*
|
|
34095
|
+
* GET /addon/provider-homeassistant/icons/homeassistant → image/svg+xml
|
|
34096
|
+
*
|
|
34097
|
+
* The bytes ship INSIDE the addon (no external CDN / hotlink) so the icon is
|
|
34098
|
+
* self-contained and CSP-safe. The SVG is a standalone document (declares the
|
|
34099
|
+
* default `xmlns`, a `viewBox`, and an intrinsic `fill`) with NO editor-tool
|
|
34100
|
+
* namespaces, so a browser `<img src>` renders it in its strict SVG mode.
|
|
34101
|
+
*
|
|
34102
|
+
* We register ONE CONCRETE static route (`/icons/homeassistant`), never a
|
|
34103
|
+
* `/:kind` param route: the forked-addon route bridge re-matches the pattern
|
|
34104
|
+
* against itself and would clobber a `:kind` path param with the literal token
|
|
34105
|
+
* `':kind'`. A static path carries no param, so it routes correctly across the
|
|
34106
|
+
* process boundary.
|
|
34107
|
+
*
|
|
34108
|
+
* The route negotiates `Accept-Encoding` and stamps `content-encoding` itself
|
|
34109
|
+
* (serving a pre-computed br/gzip/deflate variant, or identity). This makes the
|
|
34110
|
+
* hub's global `@fastify/compress` skip the response — on the forked-addon route
|
|
34111
|
+
* bridge that compressor emits an EMPTY brotli stream for any compressible body
|
|
34112
|
+
* over its 1 KiB threshold, which shows as a broken `<img>`. Owning the coding
|
|
34113
|
+
* keeps this icon robust even if its bytes ever grow past the threshold, and
|
|
34114
|
+
* matches how `addon-notifiers` serves its notifier-kind icons.
|
|
34115
|
+
*/
|
|
34116
|
+
/** The single notifier kind this addon contributes an icon for. */
|
|
34117
|
+
var HA_ICON_KIND = "homeassistant";
|
|
34118
|
+
/** The addon id — also the `/addon/<id>/…` route prefix. Mirrors the manifest id. */
|
|
34119
|
+
var HA_ROUTE_ID = "provider-homeassistant";
|
|
34120
|
+
/** One day; the icon is immutable per addon version. */
|
|
34121
|
+
var CACHE_CONTROL = "public, max-age=86400, immutable";
|
|
34122
|
+
/**
|
|
34123
|
+
* Home Assistant's official brand mark (simple-icons `homeassistant`, CC0) —
|
|
34124
|
+
* the blue "house + circuit" glyph — tinted with the official HA brand hex
|
|
34125
|
+
* (#18BCF2) so it reads on the dark admin UI. A standalone SVG document, no
|
|
34126
|
+
* editor-tool namespaces.
|
|
34127
|
+
*/
|
|
34128
|
+
var HOME_ASSISTANT_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"#18BCF2\"><path d=\"M22.939 10.627 13.061.749a1.505 1.505 0 0 0-2.121 0l-9.879 9.878C.478 11.21 0 12.363 0 13.187v9c0 .826.675 1.5 1.5 1.5h9.227l-4.063-4.062a2.034 2.034 0 0 1-.664.113c-1.13 0-2.05-.92-2.05-2.05s.92-2.05 2.05-2.05 2.05.92 2.05 2.05c0 .233-.041.456-.113.665l3.163 3.163V9.928a2.05 2.05 0 0 1-1.15-1.84c0-1.13.92-2.05 2.05-2.05s2.05.92 2.05 2.05a2.05 2.05 0 0 1-1.15 1.84v8.127l3.146-3.146A2.051 2.051 0 0 1 18 12.239c1.13 0 2.05.92 2.05 2.05s-.92 2.05-2.05 2.05c-.25 0-.488-.047-.709-.13L12.9 20.602v3.088h9.6c.825 0 1.5-.675 1.5-1.5v-9c0-.825-.477-1.977-1.061-2.561z\"/></svg>";
|
|
34129
|
+
/**
|
|
34130
|
+
* The stable icon URL for the `homeassistant` kind. Stamped onto the descriptor
|
|
34131
|
+
* by `listTargetKinds`. Root-relative so it resolves against whatever origin
|
|
34132
|
+
* the admin UI is served from.
|
|
34133
|
+
*/
|
|
34134
|
+
var HA_NOTIFICATION_ICON_URL = `/addon/${HA_ROUTE_ID}/icons/${HA_ICON_KIND}`;
|
|
34135
|
+
/** The icon's identity bytes plus every content coding we may serve, computed
|
|
34136
|
+
* once at module load (the icon is immutable). */
|
|
34137
|
+
var HA_ICON_IDENTITY = Buffer.from(HOME_ASSISTANT_SVG, "utf8");
|
|
34138
|
+
var HA_ICON_ENCODED = {
|
|
34139
|
+
br: brotliCompressSync(HA_ICON_IDENTITY),
|
|
34140
|
+
gzip: gzipSync(HA_ICON_IDENTITY),
|
|
34141
|
+
deflate: deflateSync(HA_ICON_IDENTITY)
|
|
34142
|
+
};
|
|
34143
|
+
/** Pick the best coding the client accepts (br › gzip › deflate), or `null` for
|
|
34144
|
+
* identity when the header names none of them. */
|
|
34145
|
+
function negotiateEncoding(acceptEncoding) {
|
|
34146
|
+
const tokens = acceptEncoding.toLowerCase().split(",").map((part) => part.trim().split(";")[0]?.trim());
|
|
34147
|
+
for (const coding of [
|
|
34148
|
+
"br",
|
|
34149
|
+
"gzip",
|
|
34150
|
+
"deflate"
|
|
34151
|
+
]) if (tokens.includes(coding)) return coding;
|
|
34152
|
+
return null;
|
|
34153
|
+
}
|
|
34154
|
+
/** Build the `addon-routes` provider that serves the bundled Home Assistant icon. */
|
|
34155
|
+
function createHaIconRouteProvider() {
|
|
34156
|
+
return buildAddonRouteProvider(HA_ROUTE_ID, [{
|
|
34157
|
+
method: "GET",
|
|
34158
|
+
path: `/icons/${HA_ICON_KIND}`,
|
|
34159
|
+
access: "public",
|
|
34160
|
+
description: "Official brand icon (SVG) for the 'homeassistant' notifier kind.",
|
|
34161
|
+
handler: async (request, reply) => {
|
|
34162
|
+
const coding = negotiateEncoding(String(request.headers["accept-encoding"] ?? ""));
|
|
34163
|
+
reply.code(200).type("image/svg+xml").header("cache-control", CACHE_CONTROL).header("vary", "accept-encoding");
|
|
34164
|
+
if (coding !== null) reply.header("content-encoding", coding);
|
|
34165
|
+
reply.send(coding !== null ? HA_ICON_ENCODED[coding] : HA_ICON_IDENTITY);
|
|
34166
|
+
}
|
|
34167
|
+
}]);
|
|
34168
|
+
}
|
|
34169
|
+
//#endregion
|
|
33861
34170
|
//#region src/ha-notification-output.ts
|
|
33862
34171
|
/**
|
|
33863
34172
|
* `homeassistant` notification-output kind — hosted BY the HA provider addon.
|
|
@@ -34047,6 +34356,13 @@ function parseNotifyServices(result, brokerId) {
|
|
|
34047
34356
|
}
|
|
34048
34357
|
}));
|
|
34049
34358
|
}
|
|
34359
|
+
/** KV JSON-blob shape — a single `data` column routes the row through the
|
|
34360
|
+
* settings backend's canonical key/value path (id TEXT PK, data TEXT). */
|
|
34361
|
+
var KV_BLOB_COLUMNS = [{
|
|
34362
|
+
name: "data",
|
|
34363
|
+
type: "TEXT",
|
|
34364
|
+
notNull: true
|
|
34365
|
+
}];
|
|
34050
34366
|
function rowToTarget(row) {
|
|
34051
34367
|
const parsed = TargetSchema.safeParse({
|
|
34052
34368
|
id: row.id,
|
|
@@ -34071,6 +34387,12 @@ var HaTargetStore = class {
|
|
|
34071
34387
|
this.store = store;
|
|
34072
34388
|
this.collection = collection;
|
|
34073
34389
|
}
|
|
34390
|
+
/** Declare the backing collection before any read/write. The SQLite settings
|
|
34391
|
+
* backend rejects undeclared collections (fail-fast), so this MUST run before
|
|
34392
|
+
* `list`/`getById`/`upsert`/`delete` — mirrors addon-ai's ProfileStore.init. */
|
|
34393
|
+
async init() {
|
|
34394
|
+
await this.store.declareCollection(this.collection, KV_BLOB_COLUMNS);
|
|
34395
|
+
}
|
|
34074
34396
|
async list() {
|
|
34075
34397
|
const rows = await this.store.query(this.collection);
|
|
34076
34398
|
const out = [];
|
|
@@ -34137,6 +34459,12 @@ function createApiHaSettingsStorePort(api) {
|
|
|
34137
34459
|
collection,
|
|
34138
34460
|
key: id
|
|
34139
34461
|
});
|
|
34462
|
+
},
|
|
34463
|
+
declareCollection: async (collection, columns) => {
|
|
34464
|
+
await api.settingsStore.declareCollection.mutate({
|
|
34465
|
+
collection,
|
|
34466
|
+
columns: columns.map((c) => ({ ...c }))
|
|
34467
|
+
});
|
|
34140
34468
|
}
|
|
34141
34469
|
};
|
|
34142
34470
|
}
|
|
@@ -34167,6 +34495,9 @@ function createMemoryHaSettingsStorePort() {
|
|
|
34167
34495
|
},
|
|
34168
34496
|
remove: async (collection, id) => {
|
|
34169
34497
|
bucket(collection).delete(id);
|
|
34498
|
+
},
|
|
34499
|
+
declareCollection: async (collection) => {
|
|
34500
|
+
bucket(collection);
|
|
34170
34501
|
}
|
|
34171
34502
|
};
|
|
34172
34503
|
}
|
|
@@ -34236,7 +34567,12 @@ function createHaNotificationOutputProvider(deps) {
|
|
|
34236
34567
|
}
|
|
34237
34568
|
}
|
|
34238
34569
|
return {
|
|
34239
|
-
listTargetKinds: async () =>
|
|
34570
|
+
listTargetKinds: async () => {
|
|
34571
|
+
return [deps.iconUrl !== void 0 ? {
|
|
34572
|
+
...descriptor,
|
|
34573
|
+
iconUrl: deps.iconUrl
|
|
34574
|
+
} : descriptor];
|
|
34575
|
+
},
|
|
34240
34576
|
listTargets: async () => {
|
|
34241
34577
|
return (await store.list()).map((target) => ({
|
|
34242
34578
|
...target,
|
|
@@ -34802,17 +35138,23 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
34802
35138
|
},
|
|
34803
35139
|
{
|
|
34804
35140
|
capability: notificationOutputCapability,
|
|
34805
|
-
provider: this.buildNotificationOutputProvider()
|
|
35141
|
+
provider: await this.buildNotificationOutputProvider()
|
|
35142
|
+
},
|
|
35143
|
+
{
|
|
35144
|
+
capability: addonRoutesCapability,
|
|
35145
|
+
provider: createHaIconRouteProvider()
|
|
34806
35146
|
}
|
|
34807
35147
|
];
|
|
34808
35148
|
}
|
|
34809
|
-
buildNotificationOutputProvider() {
|
|
35149
|
+
async buildNotificationOutputProvider() {
|
|
34810
35150
|
const api = this.ctx.api;
|
|
34811
35151
|
const port = api ? createApiHaSettingsStorePort(api) : createMemoryHaSettingsStorePort();
|
|
34812
35152
|
if (!api) this.ctx.logger.warn("ha notification-output: no ctx.api — targets persist in-memory only");
|
|
34813
35153
|
const store = new HaTargetStore(port);
|
|
35154
|
+
await store.init();
|
|
34814
35155
|
return createHaNotificationOutputProvider({
|
|
34815
35156
|
addonId: this.ctx.id,
|
|
35157
|
+
iconUrl: HA_NOTIFICATION_ICON_URL,
|
|
34816
35158
|
store,
|
|
34817
35159
|
publish: async (brokerId, service, serviceData) => {
|
|
34818
35160
|
await this.requireRegistry().publish(brokerId, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-homeassistant",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.26",
|
|
4
4
|
"description": "Home Assistant device provider addon for CamStack",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -78,6 +78,9 @@
|
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
"name": "notification-output"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "addon-routes"
|
|
81
84
|
}
|
|
82
85
|
]
|
|
83
86
|
}
|