@camstack/addon-provider-homeassistant 1.2.77 → 1.2.79

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-gv0Gmrm1.js");
2
+ const require_dist = require("./dist-BI0EGQ5s.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-CQzCvoCx.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-CXuG5e6V.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
@@ -13285,6 +13285,24 @@ var deviceProviderCapability = {
13285
13285
  })
13286
13286
  }
13287
13287
  };
13288
+ /**
13289
+ * Device Manager capability — hub-side singleton that unifies device persistence,
13290
+ * live registry access, and all management operations into a single tRPC surface.
13291
+ *
13292
+ * Replaces:
13293
+ * - `device-persistence` capability (persistence methods absorbed here)
13294
+ * - `device-management.router.ts` (deleted in Phase 2)
13295
+ * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
13296
+ *
13297
+ * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
13298
+ * fork into separate processes but never run on remote cluster agents. Therefore:
13299
+ * - No nodeId routing needed — this is a pure hub singleton.
13300
+ * - The hub's DeviceRegistry is the single source of truth for all live devices.
13301
+ * - No shadow registry or cross-node aggregation required.
13302
+ *
13303
+ * Forked workers register devices back to the hub via `ctx.devices`
13304
+ * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
13305
+ */
13288
13306
  /** One child-placement directive on a container's `childLayout`. Structurally
13289
13307
  * identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
13290
13308
  * shape for the same field. The child is identified by its re-sync-stable
@@ -19041,6 +19059,9 @@ var RetrainStatusSchema = _enum([
19041
19059
  *
19042
19060
  * `debug` does NOT pin; it is attention, not durability.
19043
19061
  *
19062
+ * `debugNote` is the operator's own words about WHAT should be checked on this
19063
+ * track, and it lives and dies with `debug` — see {@link MAX_TRACK_DEBUG_NOTE_LEN}.
19064
+ *
19044
19065
  * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
19045
19066
  * A favourited track is skipped by retention the same way `staging` is, but
19046
19067
  * it does not enter `none|staging|trained` and has no staging budget.
@@ -19051,6 +19072,21 @@ var TrackFlagFields = {
19051
19072
  markForTrain: boolean().optional(),
19052
19073
  /** Operator marked this track for diagnostic attention. */
19053
19074
  debug: boolean().optional(),
19075
+ /**
19076
+ * What the operator wants CHECKED on this track, in their own words.
19077
+ *
19078
+ * The flag alone says "look at this" and nothing about what for; a debug set
19079
+ * reviewed hours later is a list of tracks with no question attached. Bound
19080
+ * to `debug` on the WRITE side — the body clears it when `debug` goes off,
19081
+ * and refuses it on a track whose debug is off — so a note can never outlive
19082
+ * the attention it belongs to.
19083
+ *
19084
+ * `''` is a real value ("marked, nothing to add"); the ABSENT key means
19085
+ * "leave whatever is stored alone", which is what lets a surface turn debug
19086
+ * on without a note (a cancelled prompt) and what lets it edit the note
19087
+ * without touching the flag.
19088
+ */
19089
+ debugNote: string().max(500).optional(),
19054
19090
  /** Operator favourited this track. Pins it against pruning. */
19055
19091
  favourited: boolean().optional()
19056
19092
  };
@@ -19077,6 +19113,12 @@ var TrackFlagsSchema = object({
19077
19113
  trackId: string(),
19078
19114
  markForTrain: boolean(),
19079
19115
  debug: boolean(),
19116
+ /** The note as it STANDS after the write — never absent here (a track with no
19117
+ * note reports `''`), for the same reason the booleans are required: a
19118
+ * surface that has just written must be able to render the note it now owns
19119
+ * without a re-fetch, and an absent key would read as "unchanged" on a
19120
+ * screen that has no previous value to keep. */
19121
+ debugNote: string(),
19080
19122
  favourited: boolean(),
19081
19123
  /** The lifecycle state the boolean was derived from. Required here (unlike on
19082
19124
  * a track row) because this shape is only ever produced by the write body,
@@ -13285,6 +13285,24 @@ var deviceProviderCapability = {
13285
13285
  })
13286
13286
  }
13287
13287
  };
13288
+ /**
13289
+ * Device Manager capability — hub-side singleton that unifies device persistence,
13290
+ * live registry access, and all management operations into a single tRPC surface.
13291
+ *
13292
+ * Replaces:
13293
+ * - `device-persistence` capability (persistence methods absorbed here)
13294
+ * - `device-management.router.ts` (deleted in Phase 2)
13295
+ * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
13296
+ *
13297
+ * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
13298
+ * fork into separate processes but never run on remote cluster agents. Therefore:
13299
+ * - No nodeId routing needed — this is a pure hub singleton.
13300
+ * - The hub's DeviceRegistry is the single source of truth for all live devices.
13301
+ * - No shadow registry or cross-node aggregation required.
13302
+ *
13303
+ * Forked workers register devices back to the hub via `ctx.devices`
13304
+ * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
13305
+ */
13288
13306
  /** One child-placement directive on a container's `childLayout`. Structurally
13289
13307
  * identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
13290
13308
  * shape for the same field. The child is identified by its re-sync-stable
@@ -19041,6 +19059,9 @@ var RetrainStatusSchema = _enum([
19041
19059
  *
19042
19060
  * `debug` does NOT pin; it is attention, not durability.
19043
19061
  *
19062
+ * `debugNote` is the operator's own words about WHAT should be checked on this
19063
+ * track, and it lives and dies with `debug` — see {@link MAX_TRACK_DEBUG_NOTE_LEN}.
19064
+ *
19044
19065
  * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
19045
19066
  * A favourited track is skipped by retention the same way `staging` is, but
19046
19067
  * it does not enter `none|staging|trained` and has no staging budget.
@@ -19051,6 +19072,21 @@ var TrackFlagFields = {
19051
19072
  markForTrain: boolean().optional(),
19052
19073
  /** Operator marked this track for diagnostic attention. */
19053
19074
  debug: boolean().optional(),
19075
+ /**
19076
+ * What the operator wants CHECKED on this track, in their own words.
19077
+ *
19078
+ * The flag alone says "look at this" and nothing about what for; a debug set
19079
+ * reviewed hours later is a list of tracks with no question attached. Bound
19080
+ * to `debug` on the WRITE side — the body clears it when `debug` goes off,
19081
+ * and refuses it on a track whose debug is off — so a note can never outlive
19082
+ * the attention it belongs to.
19083
+ *
19084
+ * `''` is a real value ("marked, nothing to add"); the ABSENT key means
19085
+ * "leave whatever is stored alone", which is what lets a surface turn debug
19086
+ * on without a note (a cancelled prompt) and what lets it edit the note
19087
+ * without touching the flag.
19088
+ */
19089
+ debugNote: string().max(500).optional(),
19054
19090
  /** Operator favourited this track. Pins it against pruning. */
19055
19091
  favourited: boolean().optional()
19056
19092
  };
@@ -19077,6 +19113,12 @@ var TrackFlagsSchema = object({
19077
19113
  trackId: string(),
19078
19114
  markForTrain: boolean(),
19079
19115
  debug: boolean(),
19116
+ /** The note as it STANDS after the write — never absent here (a track with no
19117
+ * note reports `''`), for the same reason the booleans are required: a
19118
+ * surface that has just written must be able to render the note it now owns
19119
+ * without a re-fetch, and an absent key would read as "unchanged" on a
19120
+ * screen that has no previous value to keep. */
19121
+ debugNote: string(),
19080
19122
  favourited: boolean(),
19081
19123
  /** The lifecycle state the boolean was derived from. Required here (unlike on
19082
19124
  * a track row) because this shape is only ever produced by the write body,
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  //#endregion
6
- const require_dist = require("../dist-gv0Gmrm1.js");
6
+ const require_dist = require("../dist-BI0EGQ5s.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  //#region src/ha-export/topics.ts
9
9
  /**
@@ -1538,13 +1538,31 @@ function cameraSpecs(device) {
1538
1538
  icon: "mdi:counter",
1539
1539
  enabledByDefault: false
1540
1540
  });
1541
- if (LABELLED_MACROS.includes(macro)) specs.push({
1542
- entity: `${macro}_last_label`,
1543
- platform: "sensor",
1544
- label: `${label} last label`,
1545
- icon: "mdi:tag",
1546
- enabledByDefault: true
1547
- });
1541
+ if (LABELLED_MACROS.includes(macro)) specs.push(
1542
+ {
1543
+ entity: `${macro}_last_label`,
1544
+ platform: "sensor",
1545
+ label: `${label} last label`,
1546
+ icon: "mdi:tag",
1547
+ enabledByDefault: true
1548
+ },
1549
+ /**
1550
+ * The identification OCCURRENCE — moves on every recognition,
1551
+ * including a repeat of the SAME name (an unchanged `last_label`
1552
+ * is deduped by the push client, and an identical value is not a
1553
+ * state change in HA). This is the entity an automation's trigger
1554
+ * binds to; the label value arrives in the same batch, ordered
1555
+ * first. Enabled for the same reason `<macro>_last_detection` is:
1556
+ * it IS the automation hook, not a diagnostic.
1557
+ */
1558
+ {
1559
+ entity: `${macro}_last_label_at`,
1560
+ platform: "sensor",
1561
+ label: `${label} last label at`,
1562
+ deviceClass: "timestamp",
1563
+ enabledByDefault: true
1564
+ }
1565
+ );
1548
1566
  }
1549
1567
  /**
1550
1568
  * The live picture — every stream the hub lists EXCEPT the adaptive one,
@@ -4886,10 +4904,29 @@ function projectTrackLifecycle(deviceKey, track, mediaUrl) {
4886
4904
  * topic nothing is subscribed to.
4887
4905
  */
4888
4906
  const label = track.plateText ?? track.label;
4889
- if (label !== void 0 && label.length > 0 && LABELLED_MACROS.includes(macro)) values.push({
4890
- topic: stateTopic(deviceKey, `${macro}_last_label`),
4891
- value: label
4892
- });
4907
+ if (label !== void 0 && label.length > 0 && LABELLED_MACROS.includes(macro)) {
4908
+ values.push({
4909
+ topic: stateTopic(deviceKey, `${macro}_last_label`),
4910
+ value: label
4911
+ });
4912
+ /**
4913
+ * The occurrence STAMP. A second recognition of the same person is a
4914
+ * real occurrence, but it is invisible on `_last_label`: the push
4915
+ * client dedups per topic, and an identical value is not a state
4916
+ * change in Home Assistant anyway — a state-trigger automation would
4917
+ * never re-fire. The stamp is the value that always moves, so an
4918
+ * automation binds its trigger HERE and reads `<macro>_last_label`
4919
+ * for the name — which is ordered BEFORE the stamp in this array, so
4920
+ * it is already applied when the trigger fires. HA's `event` platform
4921
+ * would be the native home for this, but no released component builds
4922
+ * it (`component-support.ts`): announcing one yields a warning in
4923
+ * HA's log and NO entity — the exact D152 zero-entities failure.
4924
+ */
4925
+ values.push({
4926
+ topic: stateTopic(deviceKey, `${macro}_last_label_at`),
4927
+ value: iso(track.lastSeen)
4928
+ });
4929
+ }
4893
4930
  if (mediaUrl !== null) values.push({
4894
4931
  topic: stateTopic(deviceKey, "last_image"),
4895
4932
  value: mediaUrl
@@ -5735,6 +5772,23 @@ function projectSynthetic(input, nowMs) {
5735
5772
  return values;
5736
5773
  }
5737
5774
  //#endregion
5775
+ //#region src/ha-export/node-reads.ts
5776
+ var UNREAD = {
5777
+ footageUsedBytes: null,
5778
+ footageFreePercent: null
5779
+ };
5780
+ function footageForNode(nodeId, usage) {
5781
+ if (usage === null || usage.nodeId !== nodeId) return UNREAD;
5782
+ const freePercents = (usage.locations ?? []).filter((loc) => loc.availableBytes !== null && loc.totalBytes !== null && loc.totalBytes > 0).map((loc) => loc.availableBytes / loc.totalBytes * 100);
5783
+ return {
5784
+ footageUsedBytes: usage.totalUsedBytes,
5785
+ footageFreePercent: freePercents.length > 0 ? Math.min(...freePercents) : null
5786
+ };
5787
+ }
5788
+ function shouldReadPackageStatus(node) {
5789
+ return !node.isHub && node.isOnline;
5790
+ }
5791
+ //#endregion
5738
5792
  //#region src/ha-export/rule-triggers.ts
5739
5793
  function reduceOne(row) {
5740
5794
  return {
@@ -6766,35 +6820,33 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6766
6820
  }
6767
6821
  }
6768
6822
  /**
6769
- * Per-node disk usage (Task D2) and per-AGENT package status (Task D3),
6770
- * fanned out from the topology this pass already read. Bounded per node —
6771
- * one unreachable node degrades its own two rows, never the others'.
6823
+ * Per-node disk usage (Task D2) and per-AGENT package status (Task D3).
6824
+ * `recording` is a singleton cap, so its usage is read ONCE, unpinned, and
6825
+ * attributed to the node the answer names (`footageForNode`); package
6826
+ * status is asked only of online agents (`shouldReadPackageStatus`). A node
6827
+ * that is not read stays `unknown` — unread, never failed.
6772
6828
  */
6773
6829
  async buildSyntheticNodes(topology) {
6830
+ const usage = await this.ctx.api.recording.getStorageUsage.query({}).catch((err) => {
6831
+ this.ctx.logger.warn("ha-export: could not read storage usage, the footage entities go unknown", { meta: { error: errMsg(err) } });
6832
+ return null;
6833
+ });
6774
6834
  let oldestFootageMs = null;
6835
+ for (const device of usage?.devices ?? []) {
6836
+ const oldest = device.oldestMs;
6837
+ if (oldest === null || oldest === void 0) continue;
6838
+ if (oldestFootageMs === null || oldest < oldestFootageMs) oldestFootageMs = oldest;
6839
+ }
6775
6840
  return {
6776
6841
  nodes: await Promise.all(topology.map(async (node) => {
6777
- const disk = await this.ctx.api.recording.getStorageUsage.query({}, require_dist.nodePin(node.id)).catch((err) => {
6778
- this.ctx.logger.warn("ha-export: could not read storage usage for a node, its disk entities go unknown", { meta: {
6779
- nodeId: node.id,
6780
- error: errMsg(err)
6781
- } });
6782
- return null;
6783
- });
6784
- const freePercents = (disk?.locations ?? []).filter((loc) => loc.availableBytes !== null && loc.totalBytes !== null && loc.totalBytes > 0).map((loc) => loc.availableBytes / loc.totalBytes * 100);
6785
- const footageFreePercent = freePercents.length > 0 ? Math.min(...freePercents) : null;
6786
- for (const device of disk?.devices ?? []) {
6787
- const oldest = device.oldestMs;
6788
- if (oldest === null || oldest === void 0) continue;
6789
- if (oldestFootageMs === null || oldest < oldestFootageMs) oldestFootageMs = oldest;
6790
- }
6791
- const packageStatus = node.isHub ? null : await this.ctx.api.serverManagement.getServerPackageStatus.query({ nodeId: node.id }).catch((err) => {
6842
+ const footage = footageForNode(node.id, usage);
6843
+ const packageStatus = shouldReadPackageStatus(node) ? await this.ctx.api.serverManagement.getServerPackageStatus.query({ nodeId: node.id }).catch((err) => {
6792
6844
  this.ctx.logger.warn("ha-export: could not read package status for an agent node, its update entities go unknown", { meta: {
6793
6845
  nodeId: node.id,
6794
6846
  error: errMsg(err)
6795
6847
  } });
6796
6848
  return null;
6797
- });
6849
+ }) : null;
6798
6850
  return {
6799
6851
  id: node.id,
6800
6852
  name: node.name,
@@ -6803,8 +6855,8 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6803
6855
  memoryPercent: node.memoryPercent,
6804
6856
  uptime: node.uptime,
6805
6857
  isHub: node.isHub,
6806
- footageUsedBytes: disk?.totalUsedBytes ?? null,
6807
- footageFreePercent,
6858
+ footageUsedBytes: footage.footageUsedBytes,
6859
+ footageFreePercent: footage.footageFreePercent,
6808
6860
  rootPackageVersion: node.rootPackage?.version ?? null,
6809
6861
  updateAvailable: packageStatus?.updateAvailable ?? null
6810
6862
  };
@@ -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-CQzCvoCx.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-CXuG5e6V.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  //#region src/ha-export/topics.ts
4
4
  /**
@@ -1533,13 +1533,31 @@ function cameraSpecs(device) {
1533
1533
  icon: "mdi:counter",
1534
1534
  enabledByDefault: false
1535
1535
  });
1536
- if (LABELLED_MACROS.includes(macro)) specs.push({
1537
- entity: `${macro}_last_label`,
1538
- platform: "sensor",
1539
- label: `${label} last label`,
1540
- icon: "mdi:tag",
1541
- enabledByDefault: true
1542
- });
1536
+ if (LABELLED_MACROS.includes(macro)) specs.push(
1537
+ {
1538
+ entity: `${macro}_last_label`,
1539
+ platform: "sensor",
1540
+ label: `${label} last label`,
1541
+ icon: "mdi:tag",
1542
+ enabledByDefault: true
1543
+ },
1544
+ /**
1545
+ * The identification OCCURRENCE — moves on every recognition,
1546
+ * including a repeat of the SAME name (an unchanged `last_label`
1547
+ * is deduped by the push client, and an identical value is not a
1548
+ * state change in HA). This is the entity an automation's trigger
1549
+ * binds to; the label value arrives in the same batch, ordered
1550
+ * first. Enabled for the same reason `<macro>_last_detection` is:
1551
+ * it IS the automation hook, not a diagnostic.
1552
+ */
1553
+ {
1554
+ entity: `${macro}_last_label_at`,
1555
+ platform: "sensor",
1556
+ label: `${label} last label at`,
1557
+ deviceClass: "timestamp",
1558
+ enabledByDefault: true
1559
+ }
1560
+ );
1543
1561
  }
1544
1562
  /**
1545
1563
  * The live picture — every stream the hub lists EXCEPT the adaptive one,
@@ -4881,10 +4899,29 @@ function projectTrackLifecycle(deviceKey, track, mediaUrl) {
4881
4899
  * topic nothing is subscribed to.
4882
4900
  */
4883
4901
  const label = track.plateText ?? track.label;
4884
- if (label !== void 0 && label.length > 0 && LABELLED_MACROS.includes(macro)) values.push({
4885
- topic: stateTopic(deviceKey, `${macro}_last_label`),
4886
- value: label
4887
- });
4902
+ if (label !== void 0 && label.length > 0 && LABELLED_MACROS.includes(macro)) {
4903
+ values.push({
4904
+ topic: stateTopic(deviceKey, `${macro}_last_label`),
4905
+ value: label
4906
+ });
4907
+ /**
4908
+ * The occurrence STAMP. A second recognition of the same person is a
4909
+ * real occurrence, but it is invisible on `_last_label`: the push
4910
+ * client dedups per topic, and an identical value is not a state
4911
+ * change in Home Assistant anyway — a state-trigger automation would
4912
+ * never re-fire. The stamp is the value that always moves, so an
4913
+ * automation binds its trigger HERE and reads `<macro>_last_label`
4914
+ * for the name — which is ordered BEFORE the stamp in this array, so
4915
+ * it is already applied when the trigger fires. HA's `event` platform
4916
+ * would be the native home for this, but no released component builds
4917
+ * it (`component-support.ts`): announcing one yields a warning in
4918
+ * HA's log and NO entity — the exact D152 zero-entities failure.
4919
+ */
4920
+ values.push({
4921
+ topic: stateTopic(deviceKey, `${macro}_last_label_at`),
4922
+ value: iso(track.lastSeen)
4923
+ });
4924
+ }
4888
4925
  if (mediaUrl !== null) values.push({
4889
4926
  topic: stateTopic(deviceKey, "last_image"),
4890
4927
  value: mediaUrl
@@ -5730,6 +5767,23 @@ function projectSynthetic(input, nowMs) {
5730
5767
  return values;
5731
5768
  }
5732
5769
  //#endregion
5770
+ //#region src/ha-export/node-reads.ts
5771
+ var UNREAD = {
5772
+ footageUsedBytes: null,
5773
+ footageFreePercent: null
5774
+ };
5775
+ function footageForNode(nodeId, usage) {
5776
+ if (usage === null || usage.nodeId !== nodeId) return UNREAD;
5777
+ const freePercents = (usage.locations ?? []).filter((loc) => loc.availableBytes !== null && loc.totalBytes !== null && loc.totalBytes > 0).map((loc) => loc.availableBytes / loc.totalBytes * 100);
5778
+ return {
5779
+ footageUsedBytes: usage.totalUsedBytes,
5780
+ footageFreePercent: freePercents.length > 0 ? Math.min(...freePercents) : null
5781
+ };
5782
+ }
5783
+ function shouldReadPackageStatus(node) {
5784
+ return !node.isHub && node.isOnline;
5785
+ }
5786
+ //#endregion
5733
5787
  //#region src/ha-export/rule-triggers.ts
5734
5788
  function reduceOne(row) {
5735
5789
  return {
@@ -6761,35 +6815,33 @@ var HaExportAddon = class extends BaseAddon {
6761
6815
  }
6762
6816
  }
6763
6817
  /**
6764
- * Per-node disk usage (Task D2) and per-AGENT package status (Task D3),
6765
- * fanned out from the topology this pass already read. Bounded per node —
6766
- * one unreachable node degrades its own two rows, never the others'.
6818
+ * Per-node disk usage (Task D2) and per-AGENT package status (Task D3).
6819
+ * `recording` is a singleton cap, so its usage is read ONCE, unpinned, and
6820
+ * attributed to the node the answer names (`footageForNode`); package
6821
+ * status is asked only of online agents (`shouldReadPackageStatus`). A node
6822
+ * that is not read stays `unknown` — unread, never failed.
6767
6823
  */
6768
6824
  async buildSyntheticNodes(topology) {
6825
+ const usage = await this.ctx.api.recording.getStorageUsage.query({}).catch((err) => {
6826
+ this.ctx.logger.warn("ha-export: could not read storage usage, the footage entities go unknown", { meta: { error: errMsg(err) } });
6827
+ return null;
6828
+ });
6769
6829
  let oldestFootageMs = null;
6830
+ for (const device of usage?.devices ?? []) {
6831
+ const oldest = device.oldestMs;
6832
+ if (oldest === null || oldest === void 0) continue;
6833
+ if (oldestFootageMs === null || oldest < oldestFootageMs) oldestFootageMs = oldest;
6834
+ }
6770
6835
  return {
6771
6836
  nodes: await Promise.all(topology.map(async (node) => {
6772
- const disk = await this.ctx.api.recording.getStorageUsage.query({}, nodePin(node.id)).catch((err) => {
6773
- this.ctx.logger.warn("ha-export: could not read storage usage for a node, its disk entities go unknown", { meta: {
6774
- nodeId: node.id,
6775
- error: errMsg(err)
6776
- } });
6777
- return null;
6778
- });
6779
- const freePercents = (disk?.locations ?? []).filter((loc) => loc.availableBytes !== null && loc.totalBytes !== null && loc.totalBytes > 0).map((loc) => loc.availableBytes / loc.totalBytes * 100);
6780
- const footageFreePercent = freePercents.length > 0 ? Math.min(...freePercents) : null;
6781
- for (const device of disk?.devices ?? []) {
6782
- const oldest = device.oldestMs;
6783
- if (oldest === null || oldest === void 0) continue;
6784
- if (oldestFootageMs === null || oldest < oldestFootageMs) oldestFootageMs = oldest;
6785
- }
6786
- const packageStatus = node.isHub ? null : await this.ctx.api.serverManagement.getServerPackageStatus.query({ nodeId: node.id }).catch((err) => {
6837
+ const footage = footageForNode(node.id, usage);
6838
+ const packageStatus = shouldReadPackageStatus(node) ? await this.ctx.api.serverManagement.getServerPackageStatus.query({ nodeId: node.id }).catch((err) => {
6787
6839
  this.ctx.logger.warn("ha-export: could not read package status for an agent node, its update entities go unknown", { meta: {
6788
6840
  nodeId: node.id,
6789
6841
  error: errMsg(err)
6790
6842
  } });
6791
6843
  return null;
6792
- });
6844
+ }) : null;
6793
6845
  return {
6794
6846
  id: node.id,
6795
6847
  name: node.name,
@@ -6798,8 +6850,8 @@ var HaExportAddon = class extends BaseAddon {
6798
6850
  memoryPercent: node.memoryPercent,
6799
6851
  uptime: node.uptime,
6800
6852
  isHub: node.isHub,
6801
- footageUsedBytes: disk?.totalUsedBytes ?? null,
6802
- footageFreePercent,
6853
+ footageUsedBytes: footage.footageUsedBytes,
6854
+ footageFreePercent: footage.footageFreePercent,
6803
6855
  rootPackageVersion: node.rootPackage?.version ?? null,
6804
6856
  updateAvailable: packageStatus?.updateAvailable ?? null
6805
6857
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.77",
3
+ "version": "1.2.79",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",