@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.
Files changed (3) hide show
  1. package/dist/addon.js +352 -10
  2. package/dist/addon.mjs +352 -10
  3. 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-DkhOVOjW.mjs
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
- method(_void(), array(AddonHttpRouteSchema)), method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" });
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 () => [descriptor],
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-DkhOVOjW.mjs
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
- method(_void(), array(AddonHttpRouteSchema)), method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" });
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 () => [descriptor],
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.25",
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
  }