@camstack/addon-provider-homeassistant 1.2.79 → 1.2.81

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-BI0EGQ5s.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-CXuG5e6V.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
@@ -13401,7 +13401,12 @@ var ConfigEntrySchema = object({
13401
13401
  value: unknown(),
13402
13402
  description: string().optional()
13403
13403
  });
13404
- 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"]);
13405
13410
  /** One resolved linked device — the compact projection consumers need. */
13406
13411
  var LinkedDeviceSchema = object({
13407
13412
  deviceId: number(),
@@ -13401,7 +13401,12 @@ var ConfigEntrySchema = object({
13401
13401
  value: unknown(),
13402
13402
  description: string().optional()
13403
13403
  });
13404
- 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"]);
13405
13410
  /** One resolved linked device — the compact projection consumers need. */
13406
13411
  var LinkedDeviceSchema = object({
13407
13412
  deviceId: number(),
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  //#endregion
6
- const require_dist = require("../dist-BI0EGQ5s.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
  /**
@@ -1075,31 +1075,6 @@ var ZONE_MACROS = [
1075
1075
  * already answer for automation), the per-zone timestamps, and the
1076
1076
  * doorbell's since-restart counter.
1077
1077
  */
1078
- /**
1079
- * The snooze surface, as a `select`.
1080
- *
1081
- * `NC_SNOOZE_MAX_MINUTES` is 1440 server-side and there is no enum of
1082
- * preset durations, so the presets are chosen here. `Off` cancels.
1083
- */
1084
- var SNOOZE_OPTIONS = [
1085
- "Off",
1086
- "15 minutes",
1087
- "30 minutes",
1088
- "1 hour",
1089
- "2 hours",
1090
- "4 hours",
1091
- "8 hours",
1092
- "24 hours"
1093
- ];
1094
- var SNOOZE_MINUTES = {
1095
- "15 minutes": 15,
1096
- "30 minutes": 30,
1097
- "1 hour": 60,
1098
- "2 hours": 120,
1099
- "4 hours": 240,
1100
- "8 hours": 480,
1101
- "24 hours": 1440
1102
- };
1103
1078
  var PTZ_BUTTONS = [
1104
1079
  "ptz_up",
1105
1080
  "ptz_down",
@@ -1504,14 +1479,6 @@ function cameraSpecs(device) {
1504
1479
  deviceClass: "running",
1505
1480
  enabledByDefault: true
1506
1481
  });
1507
- specs.push({
1508
- entity: "snooze",
1509
- platform: "select",
1510
- label: "Snooze notifications",
1511
- writable: true,
1512
- options: SNOOZE_OPTIONS,
1513
- enabledByDefault: true
1514
- });
1515
1482
  for (const macro of EXPORTED_MACROS) {
1516
1483
  const label = humanise(macro);
1517
1484
  specs.push({
@@ -3671,21 +3638,6 @@ function resolveCommand(target, entity, value) {
3671
3638
  }
3672
3639
  };
3673
3640
  }
3674
- if (entity === "snooze") {
3675
- if (!SNOOZE_OPTIONS.includes(value)) return {
3676
- ok: false,
3677
- reason: "bad-value"
3678
- };
3679
- const minutes = SNOOZE_MINUTES[value];
3680
- return {
3681
- ok: true,
3682
- command: {
3683
- kind: "snooze",
3684
- deviceId: target.deviceId,
3685
- minutes: minutes ?? null
3686
- }
3687
- };
3688
- }
3689
3641
  return {
3690
3642
  ok: false,
3691
3643
  reason: "unknown-entity"
@@ -3784,8 +3736,7 @@ function isCameraCommand(command) {
3784
3736
  case "camera-function":
3785
3737
  case "reboot":
3786
3738
  case "ptz-move":
3787
- case "ptz-preset":
3788
- case "snooze": return true;
3739
+ case "ptz-preset": return true;
3789
3740
  default: return false;
3790
3741
  }
3791
3742
  }
@@ -4807,14 +4758,19 @@ function projectCapSlice(deviceKey, capName, slice) {
4807
4758
  * the second in Home Assistant's registry with nothing arriving on it —
4808
4759
  * which is precisely the defect the descriptor table replaced.
4809
4760
  */
4761
+ const missingFields = [];
4810
4762
  for (const ref of capEntities(capName)) {
4811
4763
  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
- };
4764
+ if (!Object.prototype.hasOwnProperty.call(slice, ref.descriptor.field)) {
4765
+ if (ref.entity === entity) return {
4766
+ kind: "missing-field",
4767
+ capName,
4768
+ field: ref.descriptor.field,
4769
+ sliceFields: Object.keys(slice)
4770
+ };
4771
+ missingFields.push(ref.descriptor.field);
4772
+ continue;
4773
+ }
4818
4774
  const raw = slice[ref.descriptor.field];
4819
4775
  if (raw === null || raw === void 0) continue;
4820
4776
  const rendered = renderCapValue(ref.descriptor, deriveCapValue(ref.descriptor.derive, raw));
@@ -4833,7 +4789,12 @@ function projectCapSlice(deviceKey, capName, slice) {
4833
4789
  kind: "no-entity",
4834
4790
  capName
4835
4791
  };
4836
- return {
4792
+ return missingFields.length > 0 ? {
4793
+ kind: "values",
4794
+ entity,
4795
+ values,
4796
+ missingFields
4797
+ } : {
4837
4798
  kind: "values",
4838
4799
  entity,
4839
4800
  values
@@ -5117,38 +5078,6 @@ function projectAudioWindow(deviceKey, input) {
5117
5078
  });
5118
5079
  return values;
5119
5080
  }
5120
- /** Inverse of `SNOOZE_MINUTES` — a minute count back to the option label. */
5121
- var SNOOZE_OPTION_BY_MINUTES = new Map(Object.entries(SNOOZE_MINUTES).map(([option, minutes]) => [minutes, option]));
5122
- /**
5123
- * The `snooze` select's STATE. The authority is
5124
- * `notificationRules.listSnoozes`, which carries `startedAt`/`expiresAt` but
5125
- * not the preset label an operator picked — there is nowhere to store that
5126
- * label, and there must not be: it is reconstructed from the two timestamps
5127
- * every time, so it can never drift from the authority.
5128
- *
5129
- * No active snooze publishes `Off`, never silence: Home Assistant reads a
5130
- * missing state update as "the last value still holds", so a select gone
5131
- * quiet after a snooze expires would keep showing the expired duration
5132
- * forever.
5133
- *
5134
- * A duration the select's closed vocabulary cannot express — a snooze the
5135
- * viewer created with an arbitrary minute count — publishes NOTHING.
5136
- * Rounding it to the nearest preset would let Home Assistant re-send a
5137
- * duration the operator never chose; `unknown` is the honest state.
5138
- */
5139
- function projectSnooze(deviceKey, window) {
5140
- if (window === null) return [{
5141
- topic: stateTopic(deviceKey, "snooze"),
5142
- value: "Off"
5143
- }];
5144
- const minutes = Math.round((window.expiresAt - window.startedAt) / 6e4);
5145
- const option = SNOOZE_OPTION_BY_MINUTES.get(minutes);
5146
- if (option === void 0) return [];
5147
- return [{
5148
- topic: stateTopic(deviceKey, "snooze"),
5149
- value: option
5150
- }];
5151
- }
5152
5081
  /**
5153
5082
  * `status` — the entity D62 exists for. A camera an operator switched off and
5154
5083
  * a camera that is genuinely broken must never paint the same picture in Home
@@ -5307,12 +5236,6 @@ function ruleTriggerEntity(ruleId, suffix) {
5307
5236
  }
5308
5237
  function notificationCenterSpecs(input) {
5309
5238
  return [
5310
- {
5311
- entity: "snoozed",
5312
- platform: "binary_sensor",
5313
- label: "Notifications snoozed",
5314
- icon: "mdi:bell-sleep"
5315
- },
5316
5239
  {
5317
5240
  entity: "rules_enabled",
5318
5241
  platform: "sensor",
@@ -5322,21 +5245,6 @@ function notificationCenterSpecs(input) {
5322
5245
  },
5323
5246
  (
5324
5247
  /**
5325
- * The GLOBAL snooze, as a control (Task D4) — the half `snoozed` (above)
5326
- * never had. Its state is NOT projected here: `ha-export.addon.ts` pushes
5327
- * it through the SAME `projectSnooze` a camera's snooze uses, so the rule
5328
- * lives in one place — two expressions of it in two files is how
5329
- * `audio_detected` went mute for a year.
5330
- */
5331
- {
5332
- entity: "snooze",
5333
- platform: "select",
5334
- label: "Snooze all notifications",
5335
- writable: true,
5336
- options: SNOOZE_OPTIONS
5337
- }),
5338
- (
5339
- /**
5340
5248
  * The last trigger, six entities: five facts plus its picture — "conoscere
5341
5249
  * gli ultimi trigger, le ultime immagini... e da quale label" (operator,
5342
5250
  * 2026-08-25). ENABLED by default like every other entity on this device
@@ -5657,10 +5565,6 @@ function projectSynthetic(input, nowMs) {
5657
5565
  const srv = syntheticDeviceKeyFor(SERVER_STABLE_ID);
5658
5566
  const online = input.nodes.filter((node) => node.online);
5659
5567
  const values = [
5660
- {
5661
- topic: stateTopic(nc, "snoozed"),
5662
- value: String(input.snoozed)
5663
- },
5664
5568
  {
5665
5569
  topic: stateTopic(nc, "rules_enabled"),
5666
5570
  value: String(input.rules.filter((rule) => rule.enabled).length)
@@ -6210,6 +6114,14 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6210
6114
  switch (projection.kind) {
6211
6115
  case "values":
6212
6116
  if (projection.values.length > 0) this.push(deviceId, projection.values);
6117
+ for (const field of projection.missingFields ?? []) this.warnOnce(deviceId, capName, field, {
6118
+ message: "ha-export: a derived extra entity reads a field its slice does not carry",
6119
+ meta: {
6120
+ capName,
6121
+ field,
6122
+ sliceFields: Object.keys(slice)
6123
+ }
6124
+ });
6213
6125
  return;
6214
6126
  case "no-entity": return;
6215
6127
  case "missing-field":
@@ -6488,16 +6400,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6488
6400
  });
6489
6401
  /** `deviceManager.listBindableCapsForDeviceType` — once per TYPE, not per device. */
6490
6402
  const bindableCapsByType = /* @__PURE__ */ new Map();
6491
- /**
6492
- * `notificationRules.listSnoozes` — ONE read for the whole pass, shared
6493
- * by every camera's `snooze` select (Task B1) and the synthetic
6494
- * `snooze` select on *Notification Center* (Task D4): two expressions
6495
- * of the same read is how `audio_detected` went mute for a year.
6496
- */
6497
- const snoozes = await this.ctx.api.notificationRules.listSnoozes.query({}).then((r) => r.snoozes).catch((err) => {
6498
- this.ctx.logger.warn("ha-export: could not read snoozes, snooze selects go unknown", { meta: { error: errMsg(err) } });
6499
- return [];
6500
- });
6501
6403
  const exported = /* @__PURE__ */ new Map();
6502
6404
  const keyByDeviceId = /* @__PURE__ */ new Map();
6503
6405
  const allCaps = /* @__PURE__ */ new Set();
@@ -6590,7 +6492,7 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6590
6492
  * `brokerIds` is every enabled broker, because they belong to the server
6591
6493
  * and not to a membership the operator picks per camera.
6592
6494
  */
6593
- const synthetic = await this.buildSynthetic(snoozes, byId);
6495
+ const synthetic = await this.buildSynthetic(byId);
6594
6496
  if (synthetic !== null) for (const plan of syntheticPlans(synthetic)) {
6595
6497
  exported.set(plan.deviceKey, {
6596
6498
  plan,
@@ -6630,16 +6532,8 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6630
6532
  }
6631
6533
  });
6632
6534
  await this.announce(exported);
6633
- await this.pushFullState(exported, snapshots, snoozes);
6634
- if (synthetic !== null) {
6635
- /**
6636
- * The global snooze's STATE (Task D4) rides the SAME `projectSnooze`
6637
- * a camera's `snooze` select uses, over the SAME `snoozes` read this
6638
- * pass already made — one expression of the rule, not two.
6639
- */
6640
- const ncKey = syntheticDeviceKeyFor(NOTIFICATION_CENTER_STABLE_ID);
6641
- await this.pushSynthetic([...projectSynthetic(synthetic, Date.now()), ...projectSnooze(ncKey, latestGlobalSnoozeWindow(snoozes))], [...enabled]);
6642
- }
6535
+ await this.pushFullState(exported, snapshots);
6536
+ if (synthetic !== null) await this.pushSynthetic(projectSynthetic(synthetic, Date.now()), [...enabled]);
6643
6537
  this.ctx.logger.info("ha-export: reconciled", { meta: {
6644
6538
  reason,
6645
6539
  brokers: enabled.length,
@@ -6673,7 +6567,7 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6673
6567
  * Returns `null` only when NOTHING answered, so the devices are not
6674
6568
  * announced empty on a hub that is still booting.
6675
6569
  */
6676
- async buildSynthetic(snoozes, byId) {
6570
+ async buildSynthetic(byId) {
6677
6571
  const rules = await this.ctx.api.notificationRules.listRules.query({}).then((r) => r.rules.map((rule) => ({
6678
6572
  id: rule.id,
6679
6573
  name: rule.name,
@@ -6682,8 +6576,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6682
6576
  this.ctx.logger.warn("ha-export: could not read notification rules", { meta: { error: errMsg(err) } });
6683
6577
  return null;
6684
6578
  });
6685
- /** Reuses the ONE `listSnoozes` read the pass already made (Task B1/D4). */
6686
- const anySnoozed = snoozes.length > 0;
6687
6579
  const topology = await this.ctx.api.nodes.topology.query().catch((err) => {
6688
6580
  this.ctx.logger.warn("ha-export: could not read the cluster topology", { meta: { error: errMsg(err) } });
6689
6581
  return null;
@@ -6707,7 +6599,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6707
6599
  const { nodes, oldestFootageMs } = await this.buildSyntheticNodes(topology ?? []);
6708
6600
  return {
6709
6601
  rules: effectiveRules,
6710
- snoozed: anySnoozed,
6711
6602
  lastTrigger,
6712
6603
  lastTriggerByRule,
6713
6604
  nodes,
@@ -6889,9 +6780,9 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6889
6780
  }
6890
6781
  }
6891
6782
  }
6892
- async pushFullState(exported, snapshots, snoozes) {
6783
+ async pushFullState(exported, snapshots) {
6893
6784
  for (const entry of exported.values()) try {
6894
- const values = await this.currentStateFor(entry, snapshots[String(entry.deviceId)], snoozes);
6785
+ const values = await this.currentStateFor(entry, snapshots[String(entry.deviceId)]);
6895
6786
  this.push(entry.deviceId, values);
6896
6787
  } catch (err) {
6897
6788
  this.ctx.logger.warn("ha-export: could not read current state for a device", {
@@ -6902,7 +6793,7 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6902
6793
  for (const link of this.links.values()) await link.client.flush();
6903
6794
  }
6904
6795
  /** Everything readable without waiting for an event. */
6905
- async currentStateFor(entry, snapshotRaw, snoozes) {
6796
+ async currentStateFor(entry, snapshotRaw) {
6906
6797
  const key = entry.plan.deviceKey;
6907
6798
  const values = [];
6908
6799
  const snapshot = toSnapshot(snapshotRaw);
@@ -6982,16 +6873,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6982
6873
  */
6983
6874
  const cameraStatus = this.cameraStatusByDeviceId.get(entry.deviceId);
6984
6875
  if (cameraStatus !== void 0) values.push(...projectCameraStatus(key, toCameraStatusInput(cameraStatus)));
6985
- /**
6986
- * `snooze` (Task B1) — reconstructed from `notificationRules.listSnoozes`,
6987
- * never stored: the select's state is the SAME authority the write path
6988
- * (`applySnooze`) already uses.
6989
- */
6990
- const snoozeValues = projectSnooze(key, latestDeviceSnoozeWindow(snoozes, entry.deviceId));
6991
- if (snoozeValues.length === 0) {
6992
- if (snoozes.filter((s) => s.scope === "device" && s.deviceId === entry.deviceId).length > 0) this.ctx.logger.debug("ha-export: an active snooze has a duration the select cannot express, publishing nothing", { tags: { deviceId: entry.deviceId } });
6993
- }
6994
- values.push(...snoozeValues);
6995
6876
  const occupancy = await this.ctx.api.zoneAnalytics.getCurrentSnapshot.query({ deviceId: entry.deviceId });
6996
6877
  if (occupancy !== null) values.push(...projectZoneOccupancy(key, {
6997
6878
  zones: occupancy.zones.map((zone) => ({
@@ -7483,38 +7364,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
7483
7364
  * disagree with the admin UI is worse than no knob (D62).
7484
7365
  */
7485
7366
  if (isSyntheticDeviceKey(parsed.deviceKey)) {
7486
- /**
7487
- * The global snooze select (Task D4) — the OTHER control this branch
7488
- * routes, alongside the per-rule switches below. Same rule as the
7489
- * camera's own snooze (`applySnooze`): the export writes
7490
- * `createSnooze`/`cancelSnooze` directly, `scope: 'all'`, never a
7491
- * store of its own.
7492
- */
7493
- if (parsed.deviceKey === syntheticDeviceKeyFor("synthetic-notification-center") && parsed.entity === "snooze") {
7494
- if (!SNOOZE_OPTIONS.includes(value)) {
7495
- this.ctx.logger.warn("ha-export: dropping a global snooze command, unknown option", { meta: {
7496
- topic,
7497
- value
7498
- } });
7499
- reply.status(422);
7500
- reply.send({ error: "unroutable command" });
7501
- return;
7502
- }
7503
- const minutes = SNOOZE_MINUTES[value] ?? null;
7504
- try {
7505
- await this.applyGlobalSnooze(minutes);
7506
- reply.status(200);
7507
- reply.send({});
7508
- } catch (err) {
7509
- this.ctx.logger.warn("ha-export: could not apply the global snooze from Home Assistant", { meta: {
7510
- minutes,
7511
- error: errMsg(err)
7512
- } });
7513
- reply.status(422);
7514
- reply.send({ error: "command not applied" });
7515
- }
7516
- return;
7517
- }
7518
7367
  const ruleId = ruleIdFromEntity(parsed.entity, this.syntheticRules);
7519
7368
  if (ruleId === null) {
7520
7369
  this.ctx.logger.warn("ha-export: dropping a synthetic command with no authority behind it", { meta: {
@@ -7684,7 +7533,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
7684
7533
  await device.ptz.goToPreset({ presetId: preset.id });
7685
7534
  return true;
7686
7535
  }
7687
- case "snooze": return await this.applySnooze(command.deviceId, command.minutes);
7688
7536
  }
7689
7537
  } catch (err) {
7690
7538
  /**
@@ -7710,78 +7558,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
7710
7558
  });
7711
7559
  return false;
7712
7560
  }
7713
- async applySnooze(deviceId, minutes) {
7714
- if (minutes === null) {
7715
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7716
- const mine = snoozes.filter((snooze) => snooze.deviceId === deviceId);
7717
- for (const snooze of mine) await this.ctx.api.notificationRules.cancelSnooze.mutate({ snoozeId: snooze.id });
7718
- this.ctx.logger.info("ha-export: cancelled snoozes from Home Assistant", {
7719
- tags: { deviceId },
7720
- meta: { cancelled: mine.length }
7721
- });
7722
- } else {
7723
- await this.ctx.api.notificationRules.createSnooze.mutate({ snooze: {
7724
- scope: "device",
7725
- deviceId,
7726
- durationMinutes: minutes
7727
- } });
7728
- this.ctx.logger.info("ha-export: snoozed from Home Assistant", {
7729
- tags: { deviceId },
7730
- meta: { minutes }
7731
- });
7732
- }
7733
- /**
7734
- * Republish right away — the select is otherwise stale for up to
7735
- * `reconcileIntervalSec` after a write Home Assistant itself made
7736
- * (Task B1 passo 4).
7737
- */
7738
- await this.republishSnooze(deviceId);
7739
- return true;
7740
- }
7741
- /** Push `snooze`'s current state for one camera, outside the reconcile cadence. */
7742
- async republishSnooze(deviceId) {
7743
- const key = this.keyByDeviceId.get(deviceId);
7744
- if (key === void 0) return;
7745
- try {
7746
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7747
- this.push(deviceId, projectSnooze(key, latestDeviceSnoozeWindow(snoozes, deviceId)));
7748
- } catch (err) {
7749
- this.ctx.logger.warn("ha-export: could not republish snooze after a write from Home Assistant", {
7750
- tags: { deviceId },
7751
- meta: { error: errMsg(err) }
7752
- });
7753
- }
7754
- }
7755
- /**
7756
- * The global `select` on *Notification Center* (Task D4) — `scope: 'all'`,
7757
- * the same authority `applySnooze` already writes for a camera. `Off`
7758
- * cancels every global snooze, exactly as a camera's `Off` cancels its own.
7759
- */
7760
- async applyGlobalSnooze(minutes) {
7761
- if (minutes === null) {
7762
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7763
- const mine = snoozes.filter((snooze) => snooze.scope === "all");
7764
- for (const snooze of mine) await this.ctx.api.notificationRules.cancelSnooze.mutate({ snoozeId: snooze.id });
7765
- this.ctx.logger.info("ha-export: cancelled the global snooze from Home Assistant", { meta: { cancelled: mine.length } });
7766
- } else {
7767
- await this.ctx.api.notificationRules.createSnooze.mutate({ snooze: {
7768
- scope: "all",
7769
- durationMinutes: minutes
7770
- } });
7771
- this.ctx.logger.info("ha-export: snoozed globally from Home Assistant", { meta: { minutes } });
7772
- }
7773
- await this.republishGlobalSnooze();
7774
- }
7775
- /** Push the global `snooze` select's current state, outside the reconcile cadence. */
7776
- async republishGlobalSnooze() {
7777
- try {
7778
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7779
- const ncKey = syntheticDeviceKeyFor(NOTIFICATION_CENTER_STABLE_ID);
7780
- await this.pushSynthetic(projectSnooze(ncKey, latestGlobalSnoozeWindow(snoozes)), this.enabledBrokerIds());
7781
- } catch (err) {
7782
- this.ctx.logger.warn("ha-export: could not republish the global snooze after a write from Home Assistant", { meta: { error: errMsg(err) } });
7783
- }
7784
- }
7785
7561
  /**
7786
7562
  * A public, signed, expiring route Home Assistant can fetch bytes from.
7787
7563
  *
@@ -8079,10 +7855,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
8079
7855
  {
8080
7856
  label: "Capabilities unclassified",
8081
7857
  value: this.unclassified.length === 0 ? "none" : this.unclassified.join(", ")
8082
- },
8083
- {
8084
- label: "Snooze options",
8085
- value: SNOOZE_OPTIONS.join(", ")
8086
7858
  }
8087
7859
  ],
8088
7860
  note: "CamStack pushes to the CamStack custom component in Home Assistant. Home Assistant never polls the hub for a value; when the heartbeat stops, every entity is marked unavailable."
@@ -8175,29 +7947,6 @@ function toCameraStatusInput(status) {
8175
7947
  recording: status.recording === null ? null : { active: status.recording.active }
8176
7948
  };
8177
7949
  }
8178
- /**
8179
- * The ACTIVE window for one scope, from a flat `listSnoozes` answer.
8180
- *
8181
- * "Active" is `expiresAt` in the future — `listSnoozes` never sweeps expired
8182
- * rows (expiry is a comparison, not a job), so a reader that skipped this
8183
- * would keep publishing a duration that ended hours ago. When several windows
8184
- * match, the one expiring LATEST wins: that is the duration still in effect.
8185
- */
8186
- function latestSnoozeWindow(snoozes, matches, now) {
8187
- const latest = snoozes.filter((s) => matches(s) && s.expiresAt > now).reduce((top, s) => top === null || s.expiresAt > top.expiresAt ? s : top, null);
8188
- return latest === null ? null : {
8189
- startedAt: latest.startedAt,
8190
- expiresAt: latest.expiresAt
8191
- };
8192
- }
8193
- /** The camera's own `scope: 'device'` snoozes (Task B1). */
8194
- function latestDeviceSnoozeWindow(snoozes, deviceId) {
8195
- return latestSnoozeWindow(snoozes, (s) => s.scope === "device" && s.deviceId === deviceId, Date.now());
8196
- }
8197
- /** The global `scope: 'all'` snooze (Task D4). */
8198
- function latestGlobalSnoozeWindow(snoozes) {
8199
- return latestSnoozeWindow(snoozes, (s) => s.scope === "all", Date.now());
8200
- }
8201
7950
  function toCountRecord(raw) {
8202
7951
  if (raw === null || typeof raw !== "object") return {};
8203
7952
  const out = {};
@@ -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-CXuG5e6V.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
  /**
@@ -1070,31 +1070,6 @@ var ZONE_MACROS = [
1070
1070
  * already answer for automation), the per-zone timestamps, and the
1071
1071
  * doorbell's since-restart counter.
1072
1072
  */
1073
- /**
1074
- * The snooze surface, as a `select`.
1075
- *
1076
- * `NC_SNOOZE_MAX_MINUTES` is 1440 server-side and there is no enum of
1077
- * preset durations, so the presets are chosen here. `Off` cancels.
1078
- */
1079
- var SNOOZE_OPTIONS = [
1080
- "Off",
1081
- "15 minutes",
1082
- "30 minutes",
1083
- "1 hour",
1084
- "2 hours",
1085
- "4 hours",
1086
- "8 hours",
1087
- "24 hours"
1088
- ];
1089
- var SNOOZE_MINUTES = {
1090
- "15 minutes": 15,
1091
- "30 minutes": 30,
1092
- "1 hour": 60,
1093
- "2 hours": 120,
1094
- "4 hours": 240,
1095
- "8 hours": 480,
1096
- "24 hours": 1440
1097
- };
1098
1073
  var PTZ_BUTTONS = [
1099
1074
  "ptz_up",
1100
1075
  "ptz_down",
@@ -1499,14 +1474,6 @@ function cameraSpecs(device) {
1499
1474
  deviceClass: "running",
1500
1475
  enabledByDefault: true
1501
1476
  });
1502
- specs.push({
1503
- entity: "snooze",
1504
- platform: "select",
1505
- label: "Snooze notifications",
1506
- writable: true,
1507
- options: SNOOZE_OPTIONS,
1508
- enabledByDefault: true
1509
- });
1510
1477
  for (const macro of EXPORTED_MACROS) {
1511
1478
  const label = humanise(macro);
1512
1479
  specs.push({
@@ -3666,21 +3633,6 @@ function resolveCommand(target, entity, value) {
3666
3633
  }
3667
3634
  };
3668
3635
  }
3669
- if (entity === "snooze") {
3670
- if (!SNOOZE_OPTIONS.includes(value)) return {
3671
- ok: false,
3672
- reason: "bad-value"
3673
- };
3674
- const minutes = SNOOZE_MINUTES[value];
3675
- return {
3676
- ok: true,
3677
- command: {
3678
- kind: "snooze",
3679
- deviceId: target.deviceId,
3680
- minutes: minutes ?? null
3681
- }
3682
- };
3683
- }
3684
3636
  return {
3685
3637
  ok: false,
3686
3638
  reason: "unknown-entity"
@@ -3779,8 +3731,7 @@ function isCameraCommand(command) {
3779
3731
  case "camera-function":
3780
3732
  case "reboot":
3781
3733
  case "ptz-move":
3782
- case "ptz-preset":
3783
- case "snooze": return true;
3734
+ case "ptz-preset": return true;
3784
3735
  default: return false;
3785
3736
  }
3786
3737
  }
@@ -4802,14 +4753,19 @@ function projectCapSlice(deviceKey, capName, slice) {
4802
4753
  * the second in Home Assistant's registry with nothing arriving on it —
4803
4754
  * which is precisely the defect the descriptor table replaced.
4804
4755
  */
4756
+ const missingFields = [];
4805
4757
  for (const ref of capEntities(capName)) {
4806
4758
  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
- };
4759
+ if (!Object.prototype.hasOwnProperty.call(slice, ref.descriptor.field)) {
4760
+ if (ref.entity === entity) return {
4761
+ kind: "missing-field",
4762
+ capName,
4763
+ field: ref.descriptor.field,
4764
+ sliceFields: Object.keys(slice)
4765
+ };
4766
+ missingFields.push(ref.descriptor.field);
4767
+ continue;
4768
+ }
4813
4769
  const raw = slice[ref.descriptor.field];
4814
4770
  if (raw === null || raw === void 0) continue;
4815
4771
  const rendered = renderCapValue(ref.descriptor, deriveCapValue(ref.descriptor.derive, raw));
@@ -4828,7 +4784,12 @@ function projectCapSlice(deviceKey, capName, slice) {
4828
4784
  kind: "no-entity",
4829
4785
  capName
4830
4786
  };
4831
- return {
4787
+ return missingFields.length > 0 ? {
4788
+ kind: "values",
4789
+ entity,
4790
+ values,
4791
+ missingFields
4792
+ } : {
4832
4793
  kind: "values",
4833
4794
  entity,
4834
4795
  values
@@ -5112,38 +5073,6 @@ function projectAudioWindow(deviceKey, input) {
5112
5073
  });
5113
5074
  return values;
5114
5075
  }
5115
- /** Inverse of `SNOOZE_MINUTES` — a minute count back to the option label. */
5116
- var SNOOZE_OPTION_BY_MINUTES = new Map(Object.entries(SNOOZE_MINUTES).map(([option, minutes]) => [minutes, option]));
5117
- /**
5118
- * The `snooze` select's STATE. The authority is
5119
- * `notificationRules.listSnoozes`, which carries `startedAt`/`expiresAt` but
5120
- * not the preset label an operator picked — there is nowhere to store that
5121
- * label, and there must not be: it is reconstructed from the two timestamps
5122
- * every time, so it can never drift from the authority.
5123
- *
5124
- * No active snooze publishes `Off`, never silence: Home Assistant reads a
5125
- * missing state update as "the last value still holds", so a select gone
5126
- * quiet after a snooze expires would keep showing the expired duration
5127
- * forever.
5128
- *
5129
- * A duration the select's closed vocabulary cannot express — a snooze the
5130
- * viewer created with an arbitrary minute count — publishes NOTHING.
5131
- * Rounding it to the nearest preset would let Home Assistant re-send a
5132
- * duration the operator never chose; `unknown` is the honest state.
5133
- */
5134
- function projectSnooze(deviceKey, window) {
5135
- if (window === null) return [{
5136
- topic: stateTopic(deviceKey, "snooze"),
5137
- value: "Off"
5138
- }];
5139
- const minutes = Math.round((window.expiresAt - window.startedAt) / 6e4);
5140
- const option = SNOOZE_OPTION_BY_MINUTES.get(minutes);
5141
- if (option === void 0) return [];
5142
- return [{
5143
- topic: stateTopic(deviceKey, "snooze"),
5144
- value: option
5145
- }];
5146
- }
5147
5076
  /**
5148
5077
  * `status` — the entity D62 exists for. A camera an operator switched off and
5149
5078
  * a camera that is genuinely broken must never paint the same picture in Home
@@ -5302,12 +5231,6 @@ function ruleTriggerEntity(ruleId, suffix) {
5302
5231
  }
5303
5232
  function notificationCenterSpecs(input) {
5304
5233
  return [
5305
- {
5306
- entity: "snoozed",
5307
- platform: "binary_sensor",
5308
- label: "Notifications snoozed",
5309
- icon: "mdi:bell-sleep"
5310
- },
5311
5234
  {
5312
5235
  entity: "rules_enabled",
5313
5236
  platform: "sensor",
@@ -5317,21 +5240,6 @@ function notificationCenterSpecs(input) {
5317
5240
  },
5318
5241
  (
5319
5242
  /**
5320
- * The GLOBAL snooze, as a control (Task D4) — the half `snoozed` (above)
5321
- * never had. Its state is NOT projected here: `ha-export.addon.ts` pushes
5322
- * it through the SAME `projectSnooze` a camera's snooze uses, so the rule
5323
- * lives in one place — two expressions of it in two files is how
5324
- * `audio_detected` went mute for a year.
5325
- */
5326
- {
5327
- entity: "snooze",
5328
- platform: "select",
5329
- label: "Snooze all notifications",
5330
- writable: true,
5331
- options: SNOOZE_OPTIONS
5332
- }),
5333
- (
5334
- /**
5335
5243
  * The last trigger, six entities: five facts plus its picture — "conoscere
5336
5244
  * gli ultimi trigger, le ultime immagini... e da quale label" (operator,
5337
5245
  * 2026-08-25). ENABLED by default like every other entity on this device
@@ -5652,10 +5560,6 @@ function projectSynthetic(input, nowMs) {
5652
5560
  const srv = syntheticDeviceKeyFor(SERVER_STABLE_ID);
5653
5561
  const online = input.nodes.filter((node) => node.online);
5654
5562
  const values = [
5655
- {
5656
- topic: stateTopic(nc, "snoozed"),
5657
- value: String(input.snoozed)
5658
- },
5659
5563
  {
5660
5564
  topic: stateTopic(nc, "rules_enabled"),
5661
5565
  value: String(input.rules.filter((rule) => rule.enabled).length)
@@ -6205,6 +6109,14 @@ var HaExportAddon = class extends BaseAddon {
6205
6109
  switch (projection.kind) {
6206
6110
  case "values":
6207
6111
  if (projection.values.length > 0) this.push(deviceId, projection.values);
6112
+ for (const field of projection.missingFields ?? []) this.warnOnce(deviceId, capName, field, {
6113
+ message: "ha-export: a derived extra entity reads a field its slice does not carry",
6114
+ meta: {
6115
+ capName,
6116
+ field,
6117
+ sliceFields: Object.keys(slice)
6118
+ }
6119
+ });
6208
6120
  return;
6209
6121
  case "no-entity": return;
6210
6122
  case "missing-field":
@@ -6483,16 +6395,6 @@ var HaExportAddon = class extends BaseAddon {
6483
6395
  });
6484
6396
  /** `deviceManager.listBindableCapsForDeviceType` — once per TYPE, not per device. */
6485
6397
  const bindableCapsByType = /* @__PURE__ */ new Map();
6486
- /**
6487
- * `notificationRules.listSnoozes` — ONE read for the whole pass, shared
6488
- * by every camera's `snooze` select (Task B1) and the synthetic
6489
- * `snooze` select on *Notification Center* (Task D4): two expressions
6490
- * of the same read is how `audio_detected` went mute for a year.
6491
- */
6492
- const snoozes = await this.ctx.api.notificationRules.listSnoozes.query({}).then((r) => r.snoozes).catch((err) => {
6493
- this.ctx.logger.warn("ha-export: could not read snoozes, snooze selects go unknown", { meta: { error: errMsg(err) } });
6494
- return [];
6495
- });
6496
6398
  const exported = /* @__PURE__ */ new Map();
6497
6399
  const keyByDeviceId = /* @__PURE__ */ new Map();
6498
6400
  const allCaps = /* @__PURE__ */ new Set();
@@ -6585,7 +6487,7 @@ var HaExportAddon = class extends BaseAddon {
6585
6487
  * `brokerIds` is every enabled broker, because they belong to the server
6586
6488
  * and not to a membership the operator picks per camera.
6587
6489
  */
6588
- const synthetic = await this.buildSynthetic(snoozes, byId);
6490
+ const synthetic = await this.buildSynthetic(byId);
6589
6491
  if (synthetic !== null) for (const plan of syntheticPlans(synthetic)) {
6590
6492
  exported.set(plan.deviceKey, {
6591
6493
  plan,
@@ -6625,16 +6527,8 @@ var HaExportAddon = class extends BaseAddon {
6625
6527
  }
6626
6528
  });
6627
6529
  await this.announce(exported);
6628
- await this.pushFullState(exported, snapshots, snoozes);
6629
- if (synthetic !== null) {
6630
- /**
6631
- * The global snooze's STATE (Task D4) rides the SAME `projectSnooze`
6632
- * a camera's `snooze` select uses, over the SAME `snoozes` read this
6633
- * pass already made — one expression of the rule, not two.
6634
- */
6635
- const ncKey = syntheticDeviceKeyFor(NOTIFICATION_CENTER_STABLE_ID);
6636
- await this.pushSynthetic([...projectSynthetic(synthetic, Date.now()), ...projectSnooze(ncKey, latestGlobalSnoozeWindow(snoozes))], [...enabled]);
6637
- }
6530
+ await this.pushFullState(exported, snapshots);
6531
+ if (synthetic !== null) await this.pushSynthetic(projectSynthetic(synthetic, Date.now()), [...enabled]);
6638
6532
  this.ctx.logger.info("ha-export: reconciled", { meta: {
6639
6533
  reason,
6640
6534
  brokers: enabled.length,
@@ -6668,7 +6562,7 @@ var HaExportAddon = class extends BaseAddon {
6668
6562
  * Returns `null` only when NOTHING answered, so the devices are not
6669
6563
  * announced empty on a hub that is still booting.
6670
6564
  */
6671
- async buildSynthetic(snoozes, byId) {
6565
+ async buildSynthetic(byId) {
6672
6566
  const rules = await this.ctx.api.notificationRules.listRules.query({}).then((r) => r.rules.map((rule) => ({
6673
6567
  id: rule.id,
6674
6568
  name: rule.name,
@@ -6677,8 +6571,6 @@ var HaExportAddon = class extends BaseAddon {
6677
6571
  this.ctx.logger.warn("ha-export: could not read notification rules", { meta: { error: errMsg(err) } });
6678
6572
  return null;
6679
6573
  });
6680
- /** Reuses the ONE `listSnoozes` read the pass already made (Task B1/D4). */
6681
- const anySnoozed = snoozes.length > 0;
6682
6574
  const topology = await this.ctx.api.nodes.topology.query().catch((err) => {
6683
6575
  this.ctx.logger.warn("ha-export: could not read the cluster topology", { meta: { error: errMsg(err) } });
6684
6576
  return null;
@@ -6702,7 +6594,6 @@ var HaExportAddon = class extends BaseAddon {
6702
6594
  const { nodes, oldestFootageMs } = await this.buildSyntheticNodes(topology ?? []);
6703
6595
  return {
6704
6596
  rules: effectiveRules,
6705
- snoozed: anySnoozed,
6706
6597
  lastTrigger,
6707
6598
  lastTriggerByRule,
6708
6599
  nodes,
@@ -6884,9 +6775,9 @@ var HaExportAddon = class extends BaseAddon {
6884
6775
  }
6885
6776
  }
6886
6777
  }
6887
- async pushFullState(exported, snapshots, snoozes) {
6778
+ async pushFullState(exported, snapshots) {
6888
6779
  for (const entry of exported.values()) try {
6889
- const values = await this.currentStateFor(entry, snapshots[String(entry.deviceId)], snoozes);
6780
+ const values = await this.currentStateFor(entry, snapshots[String(entry.deviceId)]);
6890
6781
  this.push(entry.deviceId, values);
6891
6782
  } catch (err) {
6892
6783
  this.ctx.logger.warn("ha-export: could not read current state for a device", {
@@ -6897,7 +6788,7 @@ var HaExportAddon = class extends BaseAddon {
6897
6788
  for (const link of this.links.values()) await link.client.flush();
6898
6789
  }
6899
6790
  /** Everything readable without waiting for an event. */
6900
- async currentStateFor(entry, snapshotRaw, snoozes) {
6791
+ async currentStateFor(entry, snapshotRaw) {
6901
6792
  const key = entry.plan.deviceKey;
6902
6793
  const values = [];
6903
6794
  const snapshot = toSnapshot(snapshotRaw);
@@ -6977,16 +6868,6 @@ var HaExportAddon = class extends BaseAddon {
6977
6868
  */
6978
6869
  const cameraStatus = this.cameraStatusByDeviceId.get(entry.deviceId);
6979
6870
  if (cameraStatus !== void 0) values.push(...projectCameraStatus(key, toCameraStatusInput(cameraStatus)));
6980
- /**
6981
- * `snooze` (Task B1) — reconstructed from `notificationRules.listSnoozes`,
6982
- * never stored: the select's state is the SAME authority the write path
6983
- * (`applySnooze`) already uses.
6984
- */
6985
- const snoozeValues = projectSnooze(key, latestDeviceSnoozeWindow(snoozes, entry.deviceId));
6986
- if (snoozeValues.length === 0) {
6987
- if (snoozes.filter((s) => s.scope === "device" && s.deviceId === entry.deviceId).length > 0) this.ctx.logger.debug("ha-export: an active snooze has a duration the select cannot express, publishing nothing", { tags: { deviceId: entry.deviceId } });
6988
- }
6989
- values.push(...snoozeValues);
6990
6871
  const occupancy = await this.ctx.api.zoneAnalytics.getCurrentSnapshot.query({ deviceId: entry.deviceId });
6991
6872
  if (occupancy !== null) values.push(...projectZoneOccupancy(key, {
6992
6873
  zones: occupancy.zones.map((zone) => ({
@@ -7478,38 +7359,6 @@ var HaExportAddon = class extends BaseAddon {
7478
7359
  * disagree with the admin UI is worse than no knob (D62).
7479
7360
  */
7480
7361
  if (isSyntheticDeviceKey(parsed.deviceKey)) {
7481
- /**
7482
- * The global snooze select (Task D4) — the OTHER control this branch
7483
- * routes, alongside the per-rule switches below. Same rule as the
7484
- * camera's own snooze (`applySnooze`): the export writes
7485
- * `createSnooze`/`cancelSnooze` directly, `scope: 'all'`, never a
7486
- * store of its own.
7487
- */
7488
- if (parsed.deviceKey === syntheticDeviceKeyFor("synthetic-notification-center") && parsed.entity === "snooze") {
7489
- if (!SNOOZE_OPTIONS.includes(value)) {
7490
- this.ctx.logger.warn("ha-export: dropping a global snooze command, unknown option", { meta: {
7491
- topic,
7492
- value
7493
- } });
7494
- reply.status(422);
7495
- reply.send({ error: "unroutable command" });
7496
- return;
7497
- }
7498
- const minutes = SNOOZE_MINUTES[value] ?? null;
7499
- try {
7500
- await this.applyGlobalSnooze(minutes);
7501
- reply.status(200);
7502
- reply.send({});
7503
- } catch (err) {
7504
- this.ctx.logger.warn("ha-export: could not apply the global snooze from Home Assistant", { meta: {
7505
- minutes,
7506
- error: errMsg(err)
7507
- } });
7508
- reply.status(422);
7509
- reply.send({ error: "command not applied" });
7510
- }
7511
- return;
7512
- }
7513
7362
  const ruleId = ruleIdFromEntity(parsed.entity, this.syntheticRules);
7514
7363
  if (ruleId === null) {
7515
7364
  this.ctx.logger.warn("ha-export: dropping a synthetic command with no authority behind it", { meta: {
@@ -7679,7 +7528,6 @@ var HaExportAddon = class extends BaseAddon {
7679
7528
  await device.ptz.goToPreset({ presetId: preset.id });
7680
7529
  return true;
7681
7530
  }
7682
- case "snooze": return await this.applySnooze(command.deviceId, command.minutes);
7683
7531
  }
7684
7532
  } catch (err) {
7685
7533
  /**
@@ -7705,78 +7553,6 @@ var HaExportAddon = class extends BaseAddon {
7705
7553
  });
7706
7554
  return false;
7707
7555
  }
7708
- async applySnooze(deviceId, minutes) {
7709
- if (minutes === null) {
7710
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7711
- const mine = snoozes.filter((snooze) => snooze.deviceId === deviceId);
7712
- for (const snooze of mine) await this.ctx.api.notificationRules.cancelSnooze.mutate({ snoozeId: snooze.id });
7713
- this.ctx.logger.info("ha-export: cancelled snoozes from Home Assistant", {
7714
- tags: { deviceId },
7715
- meta: { cancelled: mine.length }
7716
- });
7717
- } else {
7718
- await this.ctx.api.notificationRules.createSnooze.mutate({ snooze: {
7719
- scope: "device",
7720
- deviceId,
7721
- durationMinutes: minutes
7722
- } });
7723
- this.ctx.logger.info("ha-export: snoozed from Home Assistant", {
7724
- tags: { deviceId },
7725
- meta: { minutes }
7726
- });
7727
- }
7728
- /**
7729
- * Republish right away — the select is otherwise stale for up to
7730
- * `reconcileIntervalSec` after a write Home Assistant itself made
7731
- * (Task B1 passo 4).
7732
- */
7733
- await this.republishSnooze(deviceId);
7734
- return true;
7735
- }
7736
- /** Push `snooze`'s current state for one camera, outside the reconcile cadence. */
7737
- async republishSnooze(deviceId) {
7738
- const key = this.keyByDeviceId.get(deviceId);
7739
- if (key === void 0) return;
7740
- try {
7741
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7742
- this.push(deviceId, projectSnooze(key, latestDeviceSnoozeWindow(snoozes, deviceId)));
7743
- } catch (err) {
7744
- this.ctx.logger.warn("ha-export: could not republish snooze after a write from Home Assistant", {
7745
- tags: { deviceId },
7746
- meta: { error: errMsg(err) }
7747
- });
7748
- }
7749
- }
7750
- /**
7751
- * The global `select` on *Notification Center* (Task D4) — `scope: 'all'`,
7752
- * the same authority `applySnooze` already writes for a camera. `Off`
7753
- * cancels every global snooze, exactly as a camera's `Off` cancels its own.
7754
- */
7755
- async applyGlobalSnooze(minutes) {
7756
- if (minutes === null) {
7757
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7758
- const mine = snoozes.filter((snooze) => snooze.scope === "all");
7759
- for (const snooze of mine) await this.ctx.api.notificationRules.cancelSnooze.mutate({ snoozeId: snooze.id });
7760
- this.ctx.logger.info("ha-export: cancelled the global snooze from Home Assistant", { meta: { cancelled: mine.length } });
7761
- } else {
7762
- await this.ctx.api.notificationRules.createSnooze.mutate({ snooze: {
7763
- scope: "all",
7764
- durationMinutes: minutes
7765
- } });
7766
- this.ctx.logger.info("ha-export: snoozed globally from Home Assistant", { meta: { minutes } });
7767
- }
7768
- await this.republishGlobalSnooze();
7769
- }
7770
- /** Push the global `snooze` select's current state, outside the reconcile cadence. */
7771
- async republishGlobalSnooze() {
7772
- try {
7773
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7774
- const ncKey = syntheticDeviceKeyFor(NOTIFICATION_CENTER_STABLE_ID);
7775
- await this.pushSynthetic(projectSnooze(ncKey, latestGlobalSnoozeWindow(snoozes)), this.enabledBrokerIds());
7776
- } catch (err) {
7777
- this.ctx.logger.warn("ha-export: could not republish the global snooze after a write from Home Assistant", { meta: { error: errMsg(err) } });
7778
- }
7779
- }
7780
7556
  /**
7781
7557
  * A public, signed, expiring route Home Assistant can fetch bytes from.
7782
7558
  *
@@ -8074,10 +7850,6 @@ var HaExportAddon = class extends BaseAddon {
8074
7850
  {
8075
7851
  label: "Capabilities unclassified",
8076
7852
  value: this.unclassified.length === 0 ? "none" : this.unclassified.join(", ")
8077
- },
8078
- {
8079
- label: "Snooze options",
8080
- value: SNOOZE_OPTIONS.join(", ")
8081
7853
  }
8082
7854
  ],
8083
7855
  note: "CamStack pushes to the CamStack custom component in Home Assistant. Home Assistant never polls the hub for a value; when the heartbeat stops, every entity is marked unavailable."
@@ -8170,29 +7942,6 @@ function toCameraStatusInput(status) {
8170
7942
  recording: status.recording === null ? null : { active: status.recording.active }
8171
7943
  };
8172
7944
  }
8173
- /**
8174
- * The ACTIVE window for one scope, from a flat `listSnoozes` answer.
8175
- *
8176
- * "Active" is `expiresAt` in the future — `listSnoozes` never sweeps expired
8177
- * rows (expiry is a comparison, not a job), so a reader that skipped this
8178
- * would keep publishing a duration that ended hours ago. When several windows
8179
- * match, the one expiring LATEST wins: that is the duration still in effect.
8180
- */
8181
- function latestSnoozeWindow(snoozes, matches, now) {
8182
- const latest = snoozes.filter((s) => matches(s) && s.expiresAt > now).reduce((top, s) => top === null || s.expiresAt > top.expiresAt ? s : top, null);
8183
- return latest === null ? null : {
8184
- startedAt: latest.startedAt,
8185
- expiresAt: latest.expiresAt
8186
- };
8187
- }
8188
- /** The camera's own `scope: 'device'` snoozes (Task B1). */
8189
- function latestDeviceSnoozeWindow(snoozes, deviceId) {
8190
- return latestSnoozeWindow(snoozes, (s) => s.scope === "device" && s.deviceId === deviceId, Date.now());
8191
- }
8192
- /** The global `scope: 'all'` snooze (Task D4). */
8193
- function latestGlobalSnoozeWindow(snoozes) {
8194
- return latestSnoozeWindow(snoozes, (s) => s.scope === "all", Date.now());
8195
- }
8196
7945
  function toCountRecord(raw) {
8197
7946
  if (raw === null || typeof raw !== "object") return {};
8198
7947
  const out = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.79",
3
+ "version": "1.2.81",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",