@camstack/addon-provider-homeassistant 1.2.78 → 1.2.80

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-HZncHwuU.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-BpePMREY.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
@@ -13383,7 +13401,12 @@ var ConfigEntrySchema = object({
13383
13401
  value: unknown(),
13384
13402
  description: string().optional()
13385
13403
  });
13386
- var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
13404
+ /**
13405
+ * Only `manual` since D356: the operator picks, nothing links itself. The
13406
+ * field survives on the wire because a viewer already OTA'd parses it —
13407
+ * drop it once no shipped client reads `mode`.
13408
+ */
13409
+ var LinkedDevicesModeSchema = _enum(["manual"]);
13387
13410
  /** One resolved linked device — the compact projection consumers need. */
13388
13411
  var LinkedDeviceSchema = object({
13389
13412
  deviceId: number(),
@@ -19041,6 +19064,9 @@ var RetrainStatusSchema = _enum([
19041
19064
  *
19042
19065
  * `debug` does NOT pin; it is attention, not durability.
19043
19066
  *
19067
+ * `debugNote` is the operator's own words about WHAT should be checked on this
19068
+ * track, and it lives and dies with `debug` — see {@link MAX_TRACK_DEBUG_NOTE_LEN}.
19069
+ *
19044
19070
  * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
19045
19071
  * A favourited track is skipped by retention the same way `staging` is, but
19046
19072
  * it does not enter `none|staging|trained` and has no staging budget.
@@ -19051,6 +19077,21 @@ var TrackFlagFields = {
19051
19077
  markForTrain: boolean().optional(),
19052
19078
  /** Operator marked this track for diagnostic attention. */
19053
19079
  debug: boolean().optional(),
19080
+ /**
19081
+ * What the operator wants CHECKED on this track, in their own words.
19082
+ *
19083
+ * The flag alone says "look at this" and nothing about what for; a debug set
19084
+ * reviewed hours later is a list of tracks with no question attached. Bound
19085
+ * to `debug` on the WRITE side — the body clears it when `debug` goes off,
19086
+ * and refuses it on a track whose debug is off — so a note can never outlive
19087
+ * the attention it belongs to.
19088
+ *
19089
+ * `''` is a real value ("marked, nothing to add"); the ABSENT key means
19090
+ * "leave whatever is stored alone", which is what lets a surface turn debug
19091
+ * on without a note (a cancelled prompt) and what lets it edit the note
19092
+ * without touching the flag.
19093
+ */
19094
+ debugNote: string().max(500).optional(),
19054
19095
  /** Operator favourited this track. Pins it against pruning. */
19055
19096
  favourited: boolean().optional()
19056
19097
  };
@@ -19077,6 +19118,12 @@ var TrackFlagsSchema = object({
19077
19118
  trackId: string(),
19078
19119
  markForTrain: boolean(),
19079
19120
  debug: boolean(),
19121
+ /** The note as it STANDS after the write — never absent here (a track with no
19122
+ * note reports `''`), for the same reason the booleans are required: a
19123
+ * surface that has just written must be able to render the note it now owns
19124
+ * without a re-fetch, and an absent key would read as "unchanged" on a
19125
+ * screen that has no previous value to keep. */
19126
+ debugNote: string(),
19080
19127
  favourited: boolean(),
19081
19128
  /** The lifecycle state the boolean was derived from. Required here (unlike on
19082
19129
  * 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
@@ -13383,7 +13401,12 @@ var ConfigEntrySchema = object({
13383
13401
  value: unknown(),
13384
13402
  description: string().optional()
13385
13403
  });
13386
- var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
13404
+ /**
13405
+ * Only `manual` since D356: the operator picks, nothing links itself. The
13406
+ * field survives on the wire because a viewer already OTA'd parses it —
13407
+ * drop it once no shipped client reads `mode`.
13408
+ */
13409
+ var LinkedDevicesModeSchema = _enum(["manual"]);
13387
13410
  /** One resolved linked device — the compact projection consumers need. */
13388
13411
  var LinkedDeviceSchema = object({
13389
13412
  deviceId: number(),
@@ -19041,6 +19064,9 @@ var RetrainStatusSchema = _enum([
19041
19064
  *
19042
19065
  * `debug` does NOT pin; it is attention, not durability.
19043
19066
  *
19067
+ * `debugNote` is the operator's own words about WHAT should be checked on this
19068
+ * track, and it lives and dies with `debug` — see {@link MAX_TRACK_DEBUG_NOTE_LEN}.
19069
+ *
19044
19070
  * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
19045
19071
  * A favourited track is skipped by retention the same way `staging` is, but
19046
19072
  * it does not enter `none|staging|trained` and has no staging budget.
@@ -19051,6 +19077,21 @@ var TrackFlagFields = {
19051
19077
  markForTrain: boolean().optional(),
19052
19078
  /** Operator marked this track for diagnostic attention. */
19053
19079
  debug: boolean().optional(),
19080
+ /**
19081
+ * What the operator wants CHECKED on this track, in their own words.
19082
+ *
19083
+ * The flag alone says "look at this" and nothing about what for; a debug set
19084
+ * reviewed hours later is a list of tracks with no question attached. Bound
19085
+ * to `debug` on the WRITE side — the body clears it when `debug` goes off,
19086
+ * and refuses it on a track whose debug is off — so a note can never outlive
19087
+ * the attention it belongs to.
19088
+ *
19089
+ * `''` is a real value ("marked, nothing to add"); the ABSENT key means
19090
+ * "leave whatever is stored alone", which is what lets a surface turn debug
19091
+ * on without a note (a cancelled prompt) and what lets it edit the note
19092
+ * without touching the flag.
19093
+ */
19094
+ debugNote: string().max(500).optional(),
19054
19095
  /** Operator favourited this track. Pins it against pruning. */
19055
19096
  favourited: boolean().optional()
19056
19097
  };
@@ -19077,6 +19118,12 @@ var TrackFlagsSchema = object({
19077
19118
  trackId: string(),
19078
19119
  markForTrain: boolean(),
19079
19120
  debug: boolean(),
19121
+ /** The note as it STANDS after the write — never absent here (a track with no
19122
+ * note reports `''`), for the same reason the booleans are required: a
19123
+ * surface that has just written must be able to render the note it now owns
19124
+ * without a re-fetch, and an absent key would read as "unchanged" on a
19125
+ * screen that has no previous value to keep. */
19126
+ debugNote: string(),
19080
19127
  favourited: boolean(),
19081
19128
  /** The lifecycle state the boolean was derived from. Required here (unlike on
19082
19129
  * 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-HZncHwuU.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  //#region src/ha-export/topics.ts
9
9
  /**
@@ -4807,14 +4807,19 @@ function projectCapSlice(deviceKey, capName, slice) {
4807
4807
  * the second in Home Assistant's registry with nothing arriving on it —
4808
4808
  * which is precisely the defect the descriptor table replaced.
4809
4809
  */
4810
+ const missingFields = [];
4810
4811
  for (const ref of capEntities(capName)) {
4811
4812
  if (ref.descriptor.platform === "button") continue;
4812
- if (!Object.prototype.hasOwnProperty.call(slice, ref.descriptor.field)) return {
4813
- kind: "missing-field",
4814
- capName,
4815
- field: ref.descriptor.field,
4816
- sliceFields: Object.keys(slice)
4817
- };
4813
+ if (!Object.prototype.hasOwnProperty.call(slice, ref.descriptor.field)) {
4814
+ if (ref.entity === entity) return {
4815
+ kind: "missing-field",
4816
+ capName,
4817
+ field: ref.descriptor.field,
4818
+ sliceFields: Object.keys(slice)
4819
+ };
4820
+ missingFields.push(ref.descriptor.field);
4821
+ continue;
4822
+ }
4818
4823
  const raw = slice[ref.descriptor.field];
4819
4824
  if (raw === null || raw === void 0) continue;
4820
4825
  const rendered = renderCapValue(ref.descriptor, deriveCapValue(ref.descriptor.derive, raw));
@@ -4833,7 +4838,12 @@ function projectCapSlice(deviceKey, capName, slice) {
4833
4838
  kind: "no-entity",
4834
4839
  capName
4835
4840
  };
4836
- return {
4841
+ return missingFields.length > 0 ? {
4842
+ kind: "values",
4843
+ entity,
4844
+ values,
4845
+ missingFields
4846
+ } : {
4837
4847
  kind: "values",
4838
4848
  entity,
4839
4849
  values
@@ -5772,6 +5782,23 @@ function projectSynthetic(input, nowMs) {
5772
5782
  return values;
5773
5783
  }
5774
5784
  //#endregion
5785
+ //#region src/ha-export/node-reads.ts
5786
+ var UNREAD = {
5787
+ footageUsedBytes: null,
5788
+ footageFreePercent: null
5789
+ };
5790
+ function footageForNode(nodeId, usage) {
5791
+ if (usage === null || usage.nodeId !== nodeId) return UNREAD;
5792
+ const freePercents = (usage.locations ?? []).filter((loc) => loc.availableBytes !== null && loc.totalBytes !== null && loc.totalBytes > 0).map((loc) => loc.availableBytes / loc.totalBytes * 100);
5793
+ return {
5794
+ footageUsedBytes: usage.totalUsedBytes,
5795
+ footageFreePercent: freePercents.length > 0 ? Math.min(...freePercents) : null
5796
+ };
5797
+ }
5798
+ function shouldReadPackageStatus(node) {
5799
+ return !node.isHub && node.isOnline;
5800
+ }
5801
+ //#endregion
5775
5802
  //#region src/ha-export/rule-triggers.ts
5776
5803
  function reduceOne(row) {
5777
5804
  return {
@@ -6193,6 +6220,14 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6193
6220
  switch (projection.kind) {
6194
6221
  case "values":
6195
6222
  if (projection.values.length > 0) this.push(deviceId, projection.values);
6223
+ for (const field of projection.missingFields ?? []) this.warnOnce(deviceId, capName, field, {
6224
+ message: "ha-export: a derived extra entity reads a field its slice does not carry",
6225
+ meta: {
6226
+ capName,
6227
+ field,
6228
+ sliceFields: Object.keys(slice)
6229
+ }
6230
+ });
6196
6231
  return;
6197
6232
  case "no-entity": return;
6198
6233
  case "missing-field":
@@ -6803,35 +6838,33 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6803
6838
  }
6804
6839
  }
6805
6840
  /**
6806
- * Per-node disk usage (Task D2) and per-AGENT package status (Task D3),
6807
- * fanned out from the topology this pass already read. Bounded per node —
6808
- * one unreachable node degrades its own two rows, never the others'.
6841
+ * Per-node disk usage (Task D2) and per-AGENT package status (Task D3).
6842
+ * `recording` is a singleton cap, so its usage is read ONCE, unpinned, and
6843
+ * attributed to the node the answer names (`footageForNode`); package
6844
+ * status is asked only of online agents (`shouldReadPackageStatus`). A node
6845
+ * that is not read stays `unknown` — unread, never failed.
6809
6846
  */
6810
6847
  async buildSyntheticNodes(topology) {
6848
+ const usage = await this.ctx.api.recording.getStorageUsage.query({}).catch((err) => {
6849
+ this.ctx.logger.warn("ha-export: could not read storage usage, the footage entities go unknown", { meta: { error: errMsg(err) } });
6850
+ return null;
6851
+ });
6811
6852
  let oldestFootageMs = null;
6853
+ for (const device of usage?.devices ?? []) {
6854
+ const oldest = device.oldestMs;
6855
+ if (oldest === null || oldest === void 0) continue;
6856
+ if (oldestFootageMs === null || oldest < oldestFootageMs) oldestFootageMs = oldest;
6857
+ }
6812
6858
  return {
6813
6859
  nodes: await Promise.all(topology.map(async (node) => {
6814
- const disk = await this.ctx.api.recording.getStorageUsage.query({}, require_dist.nodePin(node.id)).catch((err) => {
6815
- this.ctx.logger.warn("ha-export: could not read storage usage for a node, its disk entities go unknown", { meta: {
6816
- nodeId: node.id,
6817
- error: errMsg(err)
6818
- } });
6819
- return null;
6820
- });
6821
- const freePercents = (disk?.locations ?? []).filter((loc) => loc.availableBytes !== null && loc.totalBytes !== null && loc.totalBytes > 0).map((loc) => loc.availableBytes / loc.totalBytes * 100);
6822
- const footageFreePercent = freePercents.length > 0 ? Math.min(...freePercents) : null;
6823
- for (const device of disk?.devices ?? []) {
6824
- const oldest = device.oldestMs;
6825
- if (oldest === null || oldest === void 0) continue;
6826
- if (oldestFootageMs === null || oldest < oldestFootageMs) oldestFootageMs = oldest;
6827
- }
6828
- const packageStatus = node.isHub ? null : await this.ctx.api.serverManagement.getServerPackageStatus.query({ nodeId: node.id }).catch((err) => {
6860
+ const footage = footageForNode(node.id, usage);
6861
+ const packageStatus = shouldReadPackageStatus(node) ? await this.ctx.api.serverManagement.getServerPackageStatus.query({ nodeId: node.id }).catch((err) => {
6829
6862
  this.ctx.logger.warn("ha-export: could not read package status for an agent node, its update entities go unknown", { meta: {
6830
6863
  nodeId: node.id,
6831
6864
  error: errMsg(err)
6832
6865
  } });
6833
6866
  return null;
6834
- });
6867
+ }) : null;
6835
6868
  return {
6836
6869
  id: node.id,
6837
6870
  name: node.name,
@@ -6840,8 +6873,8 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6840
6873
  memoryPercent: node.memoryPercent,
6841
6874
  uptime: node.uptime,
6842
6875
  isHub: node.isHub,
6843
- footageUsedBytes: disk?.totalUsedBytes ?? null,
6844
- footageFreePercent,
6876
+ footageUsedBytes: footage.footageUsedBytes,
6877
+ footageFreePercent: footage.footageFreePercent,
6845
6878
  rootPackageVersion: node.rootPackage?.version ?? null,
6846
6879
  updateAvailable: packageStatus?.updateAvailable ?? null
6847
6880
  };
@@ -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-BpePMREY.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  //#region src/ha-export/topics.ts
4
4
  /**
@@ -4802,14 +4802,19 @@ function projectCapSlice(deviceKey, capName, slice) {
4802
4802
  * the second in Home Assistant's registry with nothing arriving on it —
4803
4803
  * which is precisely the defect the descriptor table replaced.
4804
4804
  */
4805
+ const missingFields = [];
4805
4806
  for (const ref of capEntities(capName)) {
4806
4807
  if (ref.descriptor.platform === "button") continue;
4807
- if (!Object.prototype.hasOwnProperty.call(slice, ref.descriptor.field)) return {
4808
- kind: "missing-field",
4809
- capName,
4810
- field: ref.descriptor.field,
4811
- sliceFields: Object.keys(slice)
4812
- };
4808
+ if (!Object.prototype.hasOwnProperty.call(slice, ref.descriptor.field)) {
4809
+ if (ref.entity === entity) return {
4810
+ kind: "missing-field",
4811
+ capName,
4812
+ field: ref.descriptor.field,
4813
+ sliceFields: Object.keys(slice)
4814
+ };
4815
+ missingFields.push(ref.descriptor.field);
4816
+ continue;
4817
+ }
4813
4818
  const raw = slice[ref.descriptor.field];
4814
4819
  if (raw === null || raw === void 0) continue;
4815
4820
  const rendered = renderCapValue(ref.descriptor, deriveCapValue(ref.descriptor.derive, raw));
@@ -4828,7 +4833,12 @@ function projectCapSlice(deviceKey, capName, slice) {
4828
4833
  kind: "no-entity",
4829
4834
  capName
4830
4835
  };
4831
- return {
4836
+ return missingFields.length > 0 ? {
4837
+ kind: "values",
4838
+ entity,
4839
+ values,
4840
+ missingFields
4841
+ } : {
4832
4842
  kind: "values",
4833
4843
  entity,
4834
4844
  values
@@ -5767,6 +5777,23 @@ function projectSynthetic(input, nowMs) {
5767
5777
  return values;
5768
5778
  }
5769
5779
  //#endregion
5780
+ //#region src/ha-export/node-reads.ts
5781
+ var UNREAD = {
5782
+ footageUsedBytes: null,
5783
+ footageFreePercent: null
5784
+ };
5785
+ function footageForNode(nodeId, usage) {
5786
+ if (usage === null || usage.nodeId !== nodeId) return UNREAD;
5787
+ const freePercents = (usage.locations ?? []).filter((loc) => loc.availableBytes !== null && loc.totalBytes !== null && loc.totalBytes > 0).map((loc) => loc.availableBytes / loc.totalBytes * 100);
5788
+ return {
5789
+ footageUsedBytes: usage.totalUsedBytes,
5790
+ footageFreePercent: freePercents.length > 0 ? Math.min(...freePercents) : null
5791
+ };
5792
+ }
5793
+ function shouldReadPackageStatus(node) {
5794
+ return !node.isHub && node.isOnline;
5795
+ }
5796
+ //#endregion
5770
5797
  //#region src/ha-export/rule-triggers.ts
5771
5798
  function reduceOne(row) {
5772
5799
  return {
@@ -6188,6 +6215,14 @@ var HaExportAddon = class extends BaseAddon {
6188
6215
  switch (projection.kind) {
6189
6216
  case "values":
6190
6217
  if (projection.values.length > 0) this.push(deviceId, projection.values);
6218
+ for (const field of projection.missingFields ?? []) this.warnOnce(deviceId, capName, field, {
6219
+ message: "ha-export: a derived extra entity reads a field its slice does not carry",
6220
+ meta: {
6221
+ capName,
6222
+ field,
6223
+ sliceFields: Object.keys(slice)
6224
+ }
6225
+ });
6191
6226
  return;
6192
6227
  case "no-entity": return;
6193
6228
  case "missing-field":
@@ -6798,35 +6833,33 @@ var HaExportAddon = class extends BaseAddon {
6798
6833
  }
6799
6834
  }
6800
6835
  /**
6801
- * Per-node disk usage (Task D2) and per-AGENT package status (Task D3),
6802
- * fanned out from the topology this pass already read. Bounded per node —
6803
- * one unreachable node degrades its own two rows, never the others'.
6836
+ * Per-node disk usage (Task D2) and per-AGENT package status (Task D3).
6837
+ * `recording` is a singleton cap, so its usage is read ONCE, unpinned, and
6838
+ * attributed to the node the answer names (`footageForNode`); package
6839
+ * status is asked only of online agents (`shouldReadPackageStatus`). A node
6840
+ * that is not read stays `unknown` — unread, never failed.
6804
6841
  */
6805
6842
  async buildSyntheticNodes(topology) {
6843
+ const usage = await this.ctx.api.recording.getStorageUsage.query({}).catch((err) => {
6844
+ this.ctx.logger.warn("ha-export: could not read storage usage, the footage entities go unknown", { meta: { error: errMsg(err) } });
6845
+ return null;
6846
+ });
6806
6847
  let oldestFootageMs = null;
6848
+ for (const device of usage?.devices ?? []) {
6849
+ const oldest = device.oldestMs;
6850
+ if (oldest === null || oldest === void 0) continue;
6851
+ if (oldestFootageMs === null || oldest < oldestFootageMs) oldestFootageMs = oldest;
6852
+ }
6807
6853
  return {
6808
6854
  nodes: await Promise.all(topology.map(async (node) => {
6809
- const disk = await this.ctx.api.recording.getStorageUsage.query({}, nodePin(node.id)).catch((err) => {
6810
- this.ctx.logger.warn("ha-export: could not read storage usage for a node, its disk entities go unknown", { meta: {
6811
- nodeId: node.id,
6812
- error: errMsg(err)
6813
- } });
6814
- return null;
6815
- });
6816
- const freePercents = (disk?.locations ?? []).filter((loc) => loc.availableBytes !== null && loc.totalBytes !== null && loc.totalBytes > 0).map((loc) => loc.availableBytes / loc.totalBytes * 100);
6817
- const footageFreePercent = freePercents.length > 0 ? Math.min(...freePercents) : null;
6818
- for (const device of disk?.devices ?? []) {
6819
- const oldest = device.oldestMs;
6820
- if (oldest === null || oldest === void 0) continue;
6821
- if (oldestFootageMs === null || oldest < oldestFootageMs) oldestFootageMs = oldest;
6822
- }
6823
- const packageStatus = node.isHub ? null : await this.ctx.api.serverManagement.getServerPackageStatus.query({ nodeId: node.id }).catch((err) => {
6855
+ const footage = footageForNode(node.id, usage);
6856
+ const packageStatus = shouldReadPackageStatus(node) ? await this.ctx.api.serverManagement.getServerPackageStatus.query({ nodeId: node.id }).catch((err) => {
6824
6857
  this.ctx.logger.warn("ha-export: could not read package status for an agent node, its update entities go unknown", { meta: {
6825
6858
  nodeId: node.id,
6826
6859
  error: errMsg(err)
6827
6860
  } });
6828
6861
  return null;
6829
- });
6862
+ }) : null;
6830
6863
  return {
6831
6864
  id: node.id,
6832
6865
  name: node.name,
@@ -6835,8 +6868,8 @@ var HaExportAddon = class extends BaseAddon {
6835
6868
  memoryPercent: node.memoryPercent,
6836
6869
  uptime: node.uptime,
6837
6870
  isHub: node.isHub,
6838
- footageUsedBytes: disk?.totalUsedBytes ?? null,
6839
- footageFreePercent,
6871
+ footageUsedBytes: footage.footageUsedBytes,
6872
+ footageFreePercent: footage.footageFreePercent,
6840
6873
  rootPackageVersion: node.rootPackage?.version ?? null,
6841
6874
  updateAvailable: packageStatus?.updateAvailable ?? null
6842
6875
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.78",
3
+ "version": "1.2.80",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",