@camstack/addon-provider-homeassistant 1.2.43 → 1.2.44

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dist = require("./dist-DiprHh6i.js");
2
+ const require_dist = require("./dist-B1grYZFh.js");
3
3
  let node_crypto = require("node:crypto");
4
4
  let node_zlib = require("node:zlib");
5
5
  //#region src/ha-device-source.ts
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as presenceCapability, A as controlCapability, At as EventCategory, B as humiditySensorCapability, C as buildAddonRouteProvider, Ct as number, D as colorCapability, Dt as union, E as climateControlCapability, Et as string, F as eventEmitterCapability, G as motionCapability, H as lawnMowerControlCapability, I as fanControlCapability, J as notifierCapability, K as normalizeUnit, L as floodCapability, M as deviceAdoptionCapability, O as connectivityCapability, Ot as unknown, P as enumSensorCapability, Q as prepareNotification, R as gasCapability, S as brokerCapability, St as literal, T as carbonMonoxideCapability, Tt as record, U as lockControlCapability, V as imageCapability, W as mediaPlayerCapability, Y as numericSensorCapability, Z as powerMeterCapability, _ as automationControlCapability, _t as createEvent, at as temperatureSensorCapability, b as binaryCapability, bt as array, ct as valveCapability, d as TargetSchema, dt as weatherCapability, et as pressureSensorCapability, f as accessoriesCapability, ft as errMsg, g as ambientLightSensorCapability, gt as DeviceType, h as alarmPanelCapability, ht as DeviceRole, it as tamperCapability, j as coverCapability, k as contactCapability, kt as url, lt as vibrationCapability, m as airQualitySensorCapability, mt as DeviceFeature, n as BaseDevice, nt as smokeCapability, ot as updateCapability, p as addonRoutesCapability, q as notificationOutputCapability, r as BaseDeviceProvider, rt as switchCapability, s as EnumSensorDateTimeFormatSchema, st as vacuumControlCapability, tt as scriptRunnerCapability, ut as waterHeaterCapability, v as batteryCapability, w as buttonCapability, wt as object, x as brightnessCapability, xt as discriminatedUnion, y as bestLocationMatch, yt as _enum, z as humidifierCapability } from "./dist-Dbn3F-bB.mjs";
1
+ import { $ as presenceCapability, A as controlCapability, At as EventCategory, B as humiditySensorCapability, C as buildAddonRouteProvider, Ct as number, D as colorCapability, Dt as union, E as climateControlCapability, Et as string, F as eventEmitterCapability, G as motionCapability, H as lawnMowerControlCapability, I as fanControlCapability, J as notifierCapability, K as normalizeUnit, L as floodCapability, M as deviceAdoptionCapability, O as connectivityCapability, Ot as unknown, P as enumSensorCapability, Q as prepareNotification, R as gasCapability, S as brokerCapability, St as literal, T as carbonMonoxideCapability, Tt as record, U as lockControlCapability, V as imageCapability, W as mediaPlayerCapability, Y as numericSensorCapability, Z as powerMeterCapability, _ as automationControlCapability, _t as createEvent, at as temperatureSensorCapability, b as binaryCapability, bt as array, ct as valveCapability, d as TargetSchema, dt as weatherCapability, et as pressureSensorCapability, f as accessoriesCapability, ft as errMsg, g as ambientLightSensorCapability, gt as DeviceType, h as alarmPanelCapability, ht as DeviceRole, it as tamperCapability, j as coverCapability, k as contactCapability, kt as url, lt as vibrationCapability, m as airQualitySensorCapability, mt as DeviceFeature, n as BaseDevice, nt as smokeCapability, ot as updateCapability, p as addonRoutesCapability, q as notificationOutputCapability, r as BaseDeviceProvider, rt as switchCapability, s as EnumSensorDateTimeFormatSchema, st as vacuumControlCapability, tt as scriptRunnerCapability, ut as waterHeaterCapability, v as batteryCapability, w as buttonCapability, wt as object, x as brightnessCapability, xt as discriminatedUnion, y as bestLocationMatch, yt as _enum, z as humidifierCapability } from "./dist-BjB1J9mO.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
4
4
  //#region src/ha-device-source.ts
@@ -16154,7 +16154,23 @@ var NcHistoryEntrySchema = object({
16154
16154
  updatedAt: number(),
16155
16155
  /** Failure detail — present on a `dead` row. */
16156
16156
  error: string().optional(),
16157
- subject: NcHistorySubjectSchema
16157
+ subject: NcHistorySubjectSchema,
16158
+ /**
16159
+ * Ids of the artefacts (still, then gif, then clip) this row's successful
16160
+ * delivery indexed in the artefact library — a REFERENCE, never the bytes
16161
+ * (an artefact is often megabytes; this row is durable JSON rewritten on
16162
+ * every delivery attempt). Absent on a row still pending/dead, a row
16163
+ * delivered before this field shipped, or a wiring with no artefact index.
16164
+ *
16165
+ * Resolve one to a fetchable URL with `resolveArtifactUrl` — an id
16166
+ * outlives any one URL's TTL, so a caller mints a fresh link on demand
16167
+ * rather than trusting one frozen at delivery time. `resolveArtifactUrl`
16168
+ * also answers `null` for an id whose artefact has since expired past the
16169
+ * retained shelf's own age bound — the degrade a caller (the Home
16170
+ * Assistant export) must render as "no image right now", never as a
16171
+ * broken link.
16172
+ */
16173
+ artifactIds: array(string().min(1)).optional()
16158
16174
  });
16159
16175
  /**
16160
16176
  * Query filter for `getHistory` (spec §4.2). Every field is a narrowing
@@ -16381,7 +16397,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
16381
16397
  }), method(object({}), object({
16382
16398
  catalog: array(NcConditionDescriptorSchema),
16383
16399
  taxonomy: NcTaxonomySchema.optional()
16384
- })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
16400
+ })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({ artifactId: string().min(1) }), object({ url: string().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
16385
16401
  kind: "mutation",
16386
16402
  caller: "required"
16387
16403
  }), method(object({ snoozeId: string() }), object({ success: literal(true) }), {
@@ -27506,6 +27522,39 @@ var ReadGopBytesResultSchema = object({
27506
27522
  /** Media ms the returned fragment covers. */
27507
27523
  gopDurMs: number()
27508
27524
  });
27525
+ /**
27526
+ * A time WINDOW of one finalized segment, cut by byte range — the multi-GOP
27527
+ * twin of {@link ReadGopBytesResultSchema}'s single instant. Built for the
27528
+ * replay clip's `recording` source (`docs/design/plans/2026-08-26-replay-clip-su-pipeline.md`):
27529
+ * a replay needs several seconds of native pixels, not one frame.
27530
+ *
27531
+ * `ok.data` is standalone-demuxable, same as a GOP read. `ok.reachesRequestedEnd`
27532
+ * is `false` when the returned bytes were cut short by the read's own safety
27533
+ * byte cap before covering `[fromMs, toMs)` — a truncation, reported, not a
27534
+ * silently shorter answer. `spans-multiple-segments` is a REFUSAL, not a
27535
+ * degradation: a window whose end falls past the covering segment would need
27536
+ * bytes stitched from a second segment file (its own `ftyp`+`moov`), which is
27537
+ * not one standalone-demuxable stream — the caller's answer is to request a
27538
+ * shorter window or one aligned to a single segment, not to receive spliced
27539
+ * bytes nothing has proven decodable.
27540
+ */
27541
+ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
27542
+ kind: literal("ok"),
27543
+ data: _instanceof(Uint8Array),
27544
+ /** Absolute epoch ms of the returned bytes' first sample — at or before
27545
+ * the requested `fromMs` (anchored on the nearest keyframe). */
27546
+ gopStartMs: number(),
27547
+ /** Media ms the returned bytes cover, from `gopStartMs`. */
27548
+ gopDurMs: number(),
27549
+ /** `false` ⇒ the safety byte cap cut the read short before it reached
27550
+ * the requested `toMs`; the caller got fewer frames than asked for. */
27551
+ reachesRequestedEnd: boolean()
27552
+ }), object({
27553
+ kind: literal("spans-multiple-segments"),
27554
+ /** Where the covering segment's own footage runs out — informational,
27555
+ * not a retry hint (retrying the same window would refuse again). */
27556
+ segmentEndMs: number()
27557
+ })]);
27509
27558
  method(object({
27510
27559
  deviceId: number(),
27511
27560
  fromMs: number(),
@@ -27556,6 +27605,15 @@ method(object({
27556
27605
  }), ReadGopBytesResultSchema, {
27557
27606
  kind: "query",
27558
27607
  auth: "admin"
27608
+ }), method(object({
27609
+ deviceId: number(),
27610
+ profile: string(),
27611
+ startMs: number(),
27612
+ fromMs: number(),
27613
+ toMs: number()
27614
+ }), ReadWindowBytesResultSchema, {
27615
+ kind: "query",
27616
+ auth: "admin"
27559
27617
  }), method(object({
27560
27618
  deviceId: number(),
27561
27619
  config: RecordingConfigSchema
@@ -33643,6 +33701,12 @@ Object.freeze({
33643
33701
  addonId: null,
33644
33702
  access: "view"
33645
33703
  },
33704
+ "notificationRules.resolveArtifactUrl": {
33705
+ capName: "notification-rules",
33706
+ capScope: "system",
33707
+ addonId: null,
33708
+ access: "view"
33709
+ },
33646
33710
  "notificationRules.setAlarmConfig": {
33647
33711
  capName: "notification-rules",
33648
33712
  capScope: "system",
@@ -35047,6 +35111,12 @@ Object.freeze({
35047
35111
  addonId: null,
35048
35112
  access: "view"
35049
35113
  },
35114
+ "recording.readWindowBytes": {
35115
+ capName: "recording",
35116
+ capScope: "system",
35117
+ addonId: null,
35118
+ access: "view"
35119
+ },
35050
35120
  "recording.refreshStorageLocationsForMigration": {
35051
35121
  capName: "recording",
35052
35122
  capScope: "system",
@@ -37877,6 +37947,11 @@ Object.freeze({
37877
37947
  form: "single",
37878
37948
  optional: false
37879
37949
  }],
37950
+ "recording.readWindowBytes": [{
37951
+ name: "deviceId",
37952
+ form: "single",
37953
+ optional: false
37954
+ }],
37880
37955
  "recording.relocateFootage": [{
37881
37956
  name: "deviceId",
37882
37957
  form: "single",
@@ -16154,7 +16154,23 @@ var NcHistoryEntrySchema = object({
16154
16154
  updatedAt: number(),
16155
16155
  /** Failure detail — present on a `dead` row. */
16156
16156
  error: string().optional(),
16157
- subject: NcHistorySubjectSchema
16157
+ subject: NcHistorySubjectSchema,
16158
+ /**
16159
+ * Ids of the artefacts (still, then gif, then clip) this row's successful
16160
+ * delivery indexed in the artefact library — a REFERENCE, never the bytes
16161
+ * (an artefact is often megabytes; this row is durable JSON rewritten on
16162
+ * every delivery attempt). Absent on a row still pending/dead, a row
16163
+ * delivered before this field shipped, or a wiring with no artefact index.
16164
+ *
16165
+ * Resolve one to a fetchable URL with `resolveArtifactUrl` — an id
16166
+ * outlives any one URL's TTL, so a caller mints a fresh link on demand
16167
+ * rather than trusting one frozen at delivery time. `resolveArtifactUrl`
16168
+ * also answers `null` for an id whose artefact has since expired past the
16169
+ * retained shelf's own age bound — the degrade a caller (the Home
16170
+ * Assistant export) must render as "no image right now", never as a
16171
+ * broken link.
16172
+ */
16173
+ artifactIds: array(string().min(1)).optional()
16158
16174
  });
16159
16175
  /**
16160
16176
  * Query filter for `getHistory` (spec §4.2). Every field is a narrowing
@@ -16381,7 +16397,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
16381
16397
  }), method(object({}), object({
16382
16398
  catalog: array(NcConditionDescriptorSchema),
16383
16399
  taxonomy: NcTaxonomySchema.optional()
16384
- })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
16400
+ })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({ artifactId: string().min(1) }), object({ url: string().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
16385
16401
  kind: "mutation",
16386
16402
  caller: "required"
16387
16403
  }), method(object({ snoozeId: string() }), object({ success: literal(true) }), {
@@ -27506,6 +27522,39 @@ var ReadGopBytesResultSchema = object({
27506
27522
  /** Media ms the returned fragment covers. */
27507
27523
  gopDurMs: number()
27508
27524
  });
27525
+ /**
27526
+ * A time WINDOW of one finalized segment, cut by byte range — the multi-GOP
27527
+ * twin of {@link ReadGopBytesResultSchema}'s single instant. Built for the
27528
+ * replay clip's `recording` source (`docs/design/plans/2026-08-26-replay-clip-su-pipeline.md`):
27529
+ * a replay needs several seconds of native pixels, not one frame.
27530
+ *
27531
+ * `ok.data` is standalone-demuxable, same as a GOP read. `ok.reachesRequestedEnd`
27532
+ * is `false` when the returned bytes were cut short by the read's own safety
27533
+ * byte cap before covering `[fromMs, toMs)` — a truncation, reported, not a
27534
+ * silently shorter answer. `spans-multiple-segments` is a REFUSAL, not a
27535
+ * degradation: a window whose end falls past the covering segment would need
27536
+ * bytes stitched from a second segment file (its own `ftyp`+`moov`), which is
27537
+ * not one standalone-demuxable stream — the caller's answer is to request a
27538
+ * shorter window or one aligned to a single segment, not to receive spliced
27539
+ * bytes nothing has proven decodable.
27540
+ */
27541
+ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
27542
+ kind: literal("ok"),
27543
+ data: _instanceof(Uint8Array),
27544
+ /** Absolute epoch ms of the returned bytes' first sample — at or before
27545
+ * the requested `fromMs` (anchored on the nearest keyframe). */
27546
+ gopStartMs: number(),
27547
+ /** Media ms the returned bytes cover, from `gopStartMs`. */
27548
+ gopDurMs: number(),
27549
+ /** `false` ⇒ the safety byte cap cut the read short before it reached
27550
+ * the requested `toMs`; the caller got fewer frames than asked for. */
27551
+ reachesRequestedEnd: boolean()
27552
+ }), object({
27553
+ kind: literal("spans-multiple-segments"),
27554
+ /** Where the covering segment's own footage runs out — informational,
27555
+ * not a retry hint (retrying the same window would refuse again). */
27556
+ segmentEndMs: number()
27557
+ })]);
27509
27558
  method(object({
27510
27559
  deviceId: number(),
27511
27560
  fromMs: number(),
@@ -27556,6 +27605,15 @@ method(object({
27556
27605
  }), ReadGopBytesResultSchema, {
27557
27606
  kind: "query",
27558
27607
  auth: "admin"
27608
+ }), method(object({
27609
+ deviceId: number(),
27610
+ profile: string(),
27611
+ startMs: number(),
27612
+ fromMs: number(),
27613
+ toMs: number()
27614
+ }), ReadWindowBytesResultSchema, {
27615
+ kind: "query",
27616
+ auth: "admin"
27559
27617
  }), method(object({
27560
27618
  deviceId: number(),
27561
27619
  config: RecordingConfigSchema
@@ -33643,6 +33701,12 @@ Object.freeze({
33643
33701
  addonId: null,
33644
33702
  access: "view"
33645
33703
  },
33704
+ "notificationRules.resolveArtifactUrl": {
33705
+ capName: "notification-rules",
33706
+ capScope: "system",
33707
+ addonId: null,
33708
+ access: "view"
33709
+ },
33646
33710
  "notificationRules.setAlarmConfig": {
33647
33711
  capName: "notification-rules",
33648
33712
  capScope: "system",
@@ -35047,6 +35111,12 @@ Object.freeze({
35047
35111
  addonId: null,
35048
35112
  access: "view"
35049
35113
  },
35114
+ "recording.readWindowBytes": {
35115
+ capName: "recording",
35116
+ capScope: "system",
35117
+ addonId: null,
35118
+ access: "view"
35119
+ },
35050
35120
  "recording.refreshStorageLocationsForMigration": {
35051
35121
  capName: "recording",
35052
35122
  capScope: "system",
@@ -37877,6 +37947,11 @@ Object.freeze({
37877
37947
  form: "single",
37878
37948
  optional: false
37879
37949
  }],
37950
+ "recording.readWindowBytes": [{
37951
+ name: "deviceId",
37952
+ form: "single",
37953
+ optional: false
37954
+ }],
37880
37955
  "recording.relocateFootage": [{
37881
37956
  name: "deviceId",
37882
37957
  form: "single",
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  //#endregion
6
- const require_dist = require("../dist-DiprHh6i.js");
6
+ const require_dist = require("../dist-B1grYZFh.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  //#region src/ha-export/topics.ts
9
9
  /**
@@ -5195,6 +5195,64 @@ function notificationCenterSpecs(input) {
5195
5195
  writable: true,
5196
5196
  options: SNOOZE_OPTIONS
5197
5197
  }),
5198
+ (
5199
+ /**
5200
+ * The last trigger, six entities: five facts plus its picture — "conoscere
5201
+ * gli ultimi trigger, le ultime immagini... e da quale label" (operator,
5202
+ * 2026-08-25). ENABLED by default like every other entity on this device
5203
+ * (see {@link buildComponents}'s docblock): a rule the operator wrote is a
5204
+ * rule they want to watch fire.
5205
+ *
5206
+ * Fixed count regardless of camera fleet size — the fan-out this design
5207
+ * explicitly avoids (960 entities at 30 cameras × 32 rules) lives ONLY in
5208
+ * the per-rule switches below, which already existed. These six are on
5209
+ * ONE synthetic device and never repeat per camera or per rule.
5210
+ */
5211
+ {
5212
+ entity: "last_trigger_at",
5213
+ platform: "sensor",
5214
+ label: "Last trigger",
5215
+ deviceClass: "timestamp"
5216
+ }),
5217
+ {
5218
+ entity: "last_trigger_rule",
5219
+ platform: "sensor",
5220
+ label: "Last trigger rule",
5221
+ icon: "mdi:bell-ring-outline"
5222
+ },
5223
+ {
5224
+ entity: "last_trigger_camera",
5225
+ platform: "sensor",
5226
+ label: "Last trigger camera",
5227
+ icon: "mdi:cctv"
5228
+ },
5229
+ (
5230
+ /** The identified label, or the detected class when none was resolved —
5231
+ * see {@link SyntheticLastTrigger.label}. Never blank. */
5232
+ {
5233
+ entity: "last_trigger_label",
5234
+ platform: "sensor",
5235
+ label: "Last trigger label",
5236
+ icon: "mdi:tag-outline"
5237
+ }),
5238
+ (
5239
+ /** `pending` / `sent` / `dead` — the outbox row's own status, straight
5240
+ * from `getHistory`, never a second delivery ledger. */
5241
+ {
5242
+ entity: "last_trigger_status",
5243
+ platform: "sensor",
5244
+ label: "Last trigger delivery status",
5245
+ icon: "mdi:send-check-outline",
5246
+ entityCategory: "diagnostic"
5247
+ }),
5248
+ (
5249
+ /** See {@link SyntheticLastTrigger.imageUrl} for the expired/unreachable
5250
+ * degrade: no value is published rather than a link that 404s. */
5251
+ {
5252
+ entity: "last_trigger_image",
5253
+ platform: "image",
5254
+ label: "Last trigger image"
5255
+ }),
5198
5256
  ...input.rules.map((rule) => ({
5199
5257
  entity: ruleEntity(rule.id),
5200
5258
  platform: "switch",
@@ -5440,6 +5498,29 @@ function projectSynthetic(input, nowMs) {
5440
5498
  value: String(input.alertsActiveCount)
5441
5499
  }
5442
5500
  ];
5501
+ if (input.lastTrigger !== null) {
5502
+ const t = input.lastTrigger;
5503
+ values.push({
5504
+ topic: stateTopic(nc, "last_trigger_at"),
5505
+ value: new Date(t.at).toISOString()
5506
+ }, {
5507
+ topic: stateTopic(nc, "last_trigger_rule"),
5508
+ value: t.ruleName
5509
+ }, {
5510
+ topic: stateTopic(nc, "last_trigger_camera"),
5511
+ value: t.deviceName
5512
+ }, {
5513
+ topic: stateTopic(nc, "last_trigger_label"),
5514
+ value: t.label
5515
+ }, {
5516
+ topic: stateTopic(nc, "last_trigger_status"),
5517
+ value: t.status
5518
+ });
5519
+ if (t.imageUrl !== null) values.push({
5520
+ topic: stateTopic(nc, "last_trigger_image"),
5521
+ value: t.imageUrl
5522
+ });
5523
+ }
5443
5524
  if (input.alertsLastTitle !== null) values.push({
5444
5525
  topic: stateTopic(srv, "alerts_last_title"),
5445
5526
  value: input.alertsLastTitle
@@ -6259,7 +6340,7 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6259
6340
  * `brokerIds` is every enabled broker, because they belong to the server
6260
6341
  * and not to a membership the operator picks per camera.
6261
6342
  */
6262
- const synthetic = await this.buildSynthetic(snoozes);
6343
+ const synthetic = await this.buildSynthetic(snoozes, byId);
6263
6344
  if (synthetic !== null) for (const plan of syntheticPlans(synthetic)) {
6264
6345
  exported.set(plan.deviceKey, {
6265
6346
  plan,
@@ -6342,7 +6423,7 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6342
6423
  * Returns `null` only when NOTHING answered, so the devices are not
6343
6424
  * announced empty on a hub that is still booting.
6344
6425
  */
6345
- async buildSynthetic(snoozes) {
6426
+ async buildSynthetic(snoozes, byId) {
6346
6427
  const rules = await this.ctx.api.notificationRules.listRules.query({}).then((r) => r.rules.map((rule) => ({
6347
6428
  id: rule.id,
6348
6429
  name: rule.name,
@@ -6369,12 +6450,14 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6369
6450
  }).catch(() => null);
6370
6451
  const server = await this.ctx.api.serverManagement.getServerPackageStatus.query().catch(() => null);
6371
6452
  const alerts = await this.readActiveAlerts();
6453
+ const lastTrigger = await this.buildLastTrigger(byId);
6372
6454
  if (rules === null && topology === null && addons === null && server === null) return null;
6373
6455
  this.syntheticRules = rules ?? this.syntheticRules;
6374
6456
  const { nodes, oldestFootageMs } = await this.buildSyntheticNodes(topology ?? []);
6375
6457
  return {
6376
6458
  rules: rules ?? this.syntheticRules,
6377
6459
  snoozed: anySnoozed,
6460
+ lastTrigger,
6378
6461
  nodes,
6379
6462
  addonsRunning: addons?.running ?? 0,
6380
6463
  addonsFailed: addons?.failed ?? 0,
@@ -6388,6 +6471,44 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6388
6471
  };
6389
6472
  }
6390
6473
  /**
6474
+ * The most recent notification-rule trigger — "conoscere gli ultimi
6475
+ * trigger, le ultime immagini... e da quale label" (operator, 2026-08-25).
6476
+ *
6477
+ * `getHistory` is already newest-first (§3.2), so `limit: 1` is the whole
6478
+ * question. Best-effort like every other synthetic source here: a failed
6479
+ * read costs only these six entities, never the rest of the device.
6480
+ */
6481
+ async buildLastTrigger(byId) {
6482
+ try {
6483
+ const { entries } = await this.ctx.api.notificationRules.getHistory.query({ filter: { limit: 1 } });
6484
+ const entry = entries[0];
6485
+ if (entry === void 0) return null;
6486
+ const label = entry.subject.label ?? entry.subject.className;
6487
+ const artifactId = entry.artifactIds?.[0];
6488
+ const imageUrl = artifactId === void 0 ? null : await this.ctx.api.notificationRules.resolveArtifactUrl.query({ artifactId }).then((r) => r.url).catch((err) => {
6489
+ this.ctx.logger.debug("ha-export: could not resolve the last trigger image url", {
6490
+ tags: { deviceId: entry.deviceId },
6491
+ meta: {
6492
+ artifactId,
6493
+ error: errMsg(err)
6494
+ }
6495
+ });
6496
+ return null;
6497
+ });
6498
+ return {
6499
+ at: entry.createdAt,
6500
+ ruleName: entry.ruleName,
6501
+ deviceName: byId.get(entry.deviceId)?.name ?? `camera ${entry.deviceId}`,
6502
+ label,
6503
+ status: entry.status,
6504
+ imageUrl
6505
+ };
6506
+ } catch (err) {
6507
+ this.ctx.logger.warn("ha-export: could not read the last notification trigger", { meta: { error: errMsg(err) } });
6508
+ return null;
6509
+ }
6510
+ }
6511
+ /**
6391
6512
  * `alerts.list` — the conditions nobody said (Task D1). Best-effort: a
6392
6513
  * failed read yields "nothing active", not a fabricated absence dressed up
6393
6514
  * as a clean one — this addon logs the drop rather than pretending the
@@ -1,4 +1,4 @@
1
- import { At as EventCategory, C as buildAddonRouteProvider, Et as string, N as deviceExportCapability, X as oauthIntegrationCapability, a as COCO_TO_MACRO, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, pt as BaseAddon, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema, vt as nodePin } from "../dist-Dbn3F-bB.mjs";
1
+ import { At as EventCategory, C as buildAddonRouteProvider, Et as string, N as deviceExportCapability, X as oauthIntegrationCapability, a as COCO_TO_MACRO, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, pt as BaseAddon, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema, vt as nodePin } from "../dist-BjB1J9mO.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  //#region src/ha-export/topics.ts
4
4
  /**
@@ -5190,6 +5190,64 @@ function notificationCenterSpecs(input) {
5190
5190
  writable: true,
5191
5191
  options: SNOOZE_OPTIONS
5192
5192
  }),
5193
+ (
5194
+ /**
5195
+ * The last trigger, six entities: five facts plus its picture — "conoscere
5196
+ * gli ultimi trigger, le ultime immagini... e da quale label" (operator,
5197
+ * 2026-08-25). ENABLED by default like every other entity on this device
5198
+ * (see {@link buildComponents}'s docblock): a rule the operator wrote is a
5199
+ * rule they want to watch fire.
5200
+ *
5201
+ * Fixed count regardless of camera fleet size — the fan-out this design
5202
+ * explicitly avoids (960 entities at 30 cameras × 32 rules) lives ONLY in
5203
+ * the per-rule switches below, which already existed. These six are on
5204
+ * ONE synthetic device and never repeat per camera or per rule.
5205
+ */
5206
+ {
5207
+ entity: "last_trigger_at",
5208
+ platform: "sensor",
5209
+ label: "Last trigger",
5210
+ deviceClass: "timestamp"
5211
+ }),
5212
+ {
5213
+ entity: "last_trigger_rule",
5214
+ platform: "sensor",
5215
+ label: "Last trigger rule",
5216
+ icon: "mdi:bell-ring-outline"
5217
+ },
5218
+ {
5219
+ entity: "last_trigger_camera",
5220
+ platform: "sensor",
5221
+ label: "Last trigger camera",
5222
+ icon: "mdi:cctv"
5223
+ },
5224
+ (
5225
+ /** The identified label, or the detected class when none was resolved —
5226
+ * see {@link SyntheticLastTrigger.label}. Never blank. */
5227
+ {
5228
+ entity: "last_trigger_label",
5229
+ platform: "sensor",
5230
+ label: "Last trigger label",
5231
+ icon: "mdi:tag-outline"
5232
+ }),
5233
+ (
5234
+ /** `pending` / `sent` / `dead` — the outbox row's own status, straight
5235
+ * from `getHistory`, never a second delivery ledger. */
5236
+ {
5237
+ entity: "last_trigger_status",
5238
+ platform: "sensor",
5239
+ label: "Last trigger delivery status",
5240
+ icon: "mdi:send-check-outline",
5241
+ entityCategory: "diagnostic"
5242
+ }),
5243
+ (
5244
+ /** See {@link SyntheticLastTrigger.imageUrl} for the expired/unreachable
5245
+ * degrade: no value is published rather than a link that 404s. */
5246
+ {
5247
+ entity: "last_trigger_image",
5248
+ platform: "image",
5249
+ label: "Last trigger image"
5250
+ }),
5193
5251
  ...input.rules.map((rule) => ({
5194
5252
  entity: ruleEntity(rule.id),
5195
5253
  platform: "switch",
@@ -5435,6 +5493,29 @@ function projectSynthetic(input, nowMs) {
5435
5493
  value: String(input.alertsActiveCount)
5436
5494
  }
5437
5495
  ];
5496
+ if (input.lastTrigger !== null) {
5497
+ const t = input.lastTrigger;
5498
+ values.push({
5499
+ topic: stateTopic(nc, "last_trigger_at"),
5500
+ value: new Date(t.at).toISOString()
5501
+ }, {
5502
+ topic: stateTopic(nc, "last_trigger_rule"),
5503
+ value: t.ruleName
5504
+ }, {
5505
+ topic: stateTopic(nc, "last_trigger_camera"),
5506
+ value: t.deviceName
5507
+ }, {
5508
+ topic: stateTopic(nc, "last_trigger_label"),
5509
+ value: t.label
5510
+ }, {
5511
+ topic: stateTopic(nc, "last_trigger_status"),
5512
+ value: t.status
5513
+ });
5514
+ if (t.imageUrl !== null) values.push({
5515
+ topic: stateTopic(nc, "last_trigger_image"),
5516
+ value: t.imageUrl
5517
+ });
5518
+ }
5438
5519
  if (input.alertsLastTitle !== null) values.push({
5439
5520
  topic: stateTopic(srv, "alerts_last_title"),
5440
5521
  value: input.alertsLastTitle
@@ -6254,7 +6335,7 @@ var HaExportAddon = class extends BaseAddon {
6254
6335
  * `brokerIds` is every enabled broker, because they belong to the server
6255
6336
  * and not to a membership the operator picks per camera.
6256
6337
  */
6257
- const synthetic = await this.buildSynthetic(snoozes);
6338
+ const synthetic = await this.buildSynthetic(snoozes, byId);
6258
6339
  if (synthetic !== null) for (const plan of syntheticPlans(synthetic)) {
6259
6340
  exported.set(plan.deviceKey, {
6260
6341
  plan,
@@ -6337,7 +6418,7 @@ var HaExportAddon = class extends BaseAddon {
6337
6418
  * Returns `null` only when NOTHING answered, so the devices are not
6338
6419
  * announced empty on a hub that is still booting.
6339
6420
  */
6340
- async buildSynthetic(snoozes) {
6421
+ async buildSynthetic(snoozes, byId) {
6341
6422
  const rules = await this.ctx.api.notificationRules.listRules.query({}).then((r) => r.rules.map((rule) => ({
6342
6423
  id: rule.id,
6343
6424
  name: rule.name,
@@ -6364,12 +6445,14 @@ var HaExportAddon = class extends BaseAddon {
6364
6445
  }).catch(() => null);
6365
6446
  const server = await this.ctx.api.serverManagement.getServerPackageStatus.query().catch(() => null);
6366
6447
  const alerts = await this.readActiveAlerts();
6448
+ const lastTrigger = await this.buildLastTrigger(byId);
6367
6449
  if (rules === null && topology === null && addons === null && server === null) return null;
6368
6450
  this.syntheticRules = rules ?? this.syntheticRules;
6369
6451
  const { nodes, oldestFootageMs } = await this.buildSyntheticNodes(topology ?? []);
6370
6452
  return {
6371
6453
  rules: rules ?? this.syntheticRules,
6372
6454
  snoozed: anySnoozed,
6455
+ lastTrigger,
6373
6456
  nodes,
6374
6457
  addonsRunning: addons?.running ?? 0,
6375
6458
  addonsFailed: addons?.failed ?? 0,
@@ -6383,6 +6466,44 @@ var HaExportAddon = class extends BaseAddon {
6383
6466
  };
6384
6467
  }
6385
6468
  /**
6469
+ * The most recent notification-rule trigger — "conoscere gli ultimi
6470
+ * trigger, le ultime immagini... e da quale label" (operator, 2026-08-25).
6471
+ *
6472
+ * `getHistory` is already newest-first (§3.2), so `limit: 1` is the whole
6473
+ * question. Best-effort like every other synthetic source here: a failed
6474
+ * read costs only these six entities, never the rest of the device.
6475
+ */
6476
+ async buildLastTrigger(byId) {
6477
+ try {
6478
+ const { entries } = await this.ctx.api.notificationRules.getHistory.query({ filter: { limit: 1 } });
6479
+ const entry = entries[0];
6480
+ if (entry === void 0) return null;
6481
+ const label = entry.subject.label ?? entry.subject.className;
6482
+ const artifactId = entry.artifactIds?.[0];
6483
+ const imageUrl = artifactId === void 0 ? null : await this.ctx.api.notificationRules.resolveArtifactUrl.query({ artifactId }).then((r) => r.url).catch((err) => {
6484
+ this.ctx.logger.debug("ha-export: could not resolve the last trigger image url", {
6485
+ tags: { deviceId: entry.deviceId },
6486
+ meta: {
6487
+ artifactId,
6488
+ error: errMsg(err)
6489
+ }
6490
+ });
6491
+ return null;
6492
+ });
6493
+ return {
6494
+ at: entry.createdAt,
6495
+ ruleName: entry.ruleName,
6496
+ deviceName: byId.get(entry.deviceId)?.name ?? `camera ${entry.deviceId}`,
6497
+ label,
6498
+ status: entry.status,
6499
+ imageUrl
6500
+ };
6501
+ } catch (err) {
6502
+ this.ctx.logger.warn("ha-export: could not read the last notification trigger", { meta: { error: errMsg(err) } });
6503
+ return null;
6504
+ }
6505
+ }
6506
+ /**
6386
6507
  * `alerts.list` — the conditions nobody said (Task D1). Best-effort: a
6387
6508
  * failed read yields "nothing active", not a fabricated absence dressed up
6388
6509
  * as a clean one — this addon logs the drop rather than pretending the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.43",
3
+ "version": "1.2.44",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",