@camstack/addon-provider-homeassistant 1.2.80 → 1.2.82

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-HZncHwuU.js");
2
+ const require_dist = require("./dist-GeeaZrMS.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
@@ -1410,31 +1410,14 @@ var HaClimateDevice = class extends HaBaseChildDevice {
1410
1410
  this.registerClimateCap();
1411
1411
  this.runtimeState.setCapState("climate-control", COLD_START$15);
1412
1412
  }
1413
+ /** The louver vocabularies HA last advertised, for the swing setters. */
1414
+ swingModes = [];
1415
+ swingHorizontalModes = [];
1413
1416
  handleStatePush(state) {
1414
1417
  if (state.state === "unavailable" || state.state === "unknown") return;
1415
- const lastFetchedAt = Date.parse(state.last_updated) || Date.now();
1416
- const attrs = state.attributes;
1417
- const mode = sanitizeHvacMode(state.state);
1418
- const availableModes = readStringArray$4(attrs, "hvac_modes").map(sanitizeHvacMode).filter((m) => m !== null);
1419
- const fanMode = readStringAttr$6(attrs, "fan_mode") ?? "";
1420
- const availableFanModes = readStringArray$4(attrs, "fan_modes");
1421
- const preset = readStringAttr$6(attrs, "preset_mode") ?? "";
1422
- const availablePresets = readStringArray$4(attrs, "preset_modes");
1423
- this.runtimeState.setCapState("climate-control", {
1424
- mode: mode ?? "off",
1425
- availableModes,
1426
- fanMode,
1427
- availableFanModes,
1428
- preset,
1429
- availablePresets,
1430
- target: readNumberAttr$5(attrs, "temperature"),
1431
- targetHigh: readNumberAttr$5(attrs, "target_temp_high"),
1432
- targetLow: readNumberAttr$5(attrs, "target_temp_low"),
1433
- targetHumidity: readNumberAttr$5(attrs, "target_humidity"),
1434
- currentTemp: readNumberAttr$5(attrs, "current_temperature"),
1435
- currentHumidity: readNumberAttr$5(attrs, "current_humidity"),
1436
- lastFetchedAt
1437
- });
1418
+ this.swingModes = readStringArray$4(state.attributes, "swing_modes");
1419
+ this.swingHorizontalModes = readStringArray$4(state.attributes, "swing_horizontal_modes");
1420
+ this.runtimeState.setCapState("climate-control", climateSliceFromState(state));
1438
1421
  }
1439
1422
  registerClimateCap() {
1440
1423
  this.ctx.registerNativeCap(require_dist.climateControlCapability, {
@@ -1462,15 +1445,96 @@ var HaClimateDevice = class extends HaBaseChildDevice {
1462
1445
  setTargetHumidity: async ({ targetHumidity }) => {
1463
1446
  await this.callHaService("climate", "set_humidity", { humidity: targetHumidity });
1464
1447
  },
1465
- setSwingVertical: async () => {
1466
- throw new Error("HaClimateDevice: vertical swing not supported");
1448
+ setSwingVertical: async ({ on }) => {
1449
+ const swingMode = pickSwingMode(this.swingModes, on, VERTICAL_ON_MODES);
1450
+ if (swingMode === null) throw new Error("HaClimateDevice: vertical swing not supported");
1451
+ await this.callHaService("climate", "set_swing_mode", { swing_mode: swingMode });
1467
1452
  },
1468
- setSwingHorizontal: async () => {
1469
- throw new Error("HaClimateDevice: horizontal swing not supported");
1453
+ setSwingHorizontal: async ({ on }) => {
1454
+ if (this.swingHorizontalModes.length > 0) {
1455
+ const mode = pickSwingMode(this.swingHorizontalModes, on, HORIZONTAL_ON_MODES);
1456
+ if (mode === null) throw new Error("HaClimateDevice: horizontal swing not supported");
1457
+ await this.callHaService("climate", "set_swing_horizontal_mode", { swing_horizontal_mode: mode });
1458
+ return;
1459
+ }
1460
+ const swingMode = pickSwingMode(this.swingModes, on, HORIZONTAL_ON_MODES);
1461
+ if (swingMode === null) throw new Error("HaClimateDevice: horizontal swing not supported");
1462
+ await this.callHaService("climate", "set_swing_mode", { swing_mode: swingMode });
1470
1463
  }
1471
1464
  });
1472
1465
  }
1473
1466
  };
1467
+ /**
1468
+ * The `climate-control` slice for one HA `climate.*` state. Pure, and the ONLY
1469
+ * place the slice is assembled: every key the cap schema declares is present —
1470
+ * `swingVertical` / `swingHorizontal` are `nullable`, not optional, and a slice
1471
+ * that omitted them was refused by `setCapState` on every push (828 dropped
1472
+ * pushes in a day on the live hub, every thermostat stuck at its cold start).
1473
+ */
1474
+ function climateSliceFromState(state) {
1475
+ const lastFetchedAt = Date.parse(state.last_updated) || Date.now();
1476
+ const attrs = state.attributes;
1477
+ const mode = sanitizeHvacMode(state.state);
1478
+ const availableModes = readStringArray$4(attrs, "hvac_modes").map(sanitizeHvacMode).filter((m) => m !== null);
1479
+ const fanMode = readStringAttr$6(attrs, "fan_mode") ?? "";
1480
+ const availableFanModes = readStringArray$4(attrs, "fan_modes");
1481
+ const preset = readStringAttr$6(attrs, "preset_mode") ?? "";
1482
+ const availablePresets = readStringArray$4(attrs, "preset_modes");
1483
+ return {
1484
+ mode: mode ?? "off",
1485
+ availableModes,
1486
+ fanMode,
1487
+ availableFanModes,
1488
+ preset,
1489
+ availablePresets,
1490
+ target: readNumberAttr$5(attrs, "temperature"),
1491
+ targetHigh: readNumberAttr$5(attrs, "target_temp_high"),
1492
+ targetLow: readNumberAttr$5(attrs, "target_temp_low"),
1493
+ targetHumidity: readNumberAttr$5(attrs, "target_humidity"),
1494
+ currentTemp: readNumberAttr$5(attrs, "current_temperature"),
1495
+ currentHumidity: readNumberAttr$5(attrs, "current_humidity"),
1496
+ ...readSwing(attrs),
1497
+ lastFetchedAt
1498
+ };
1499
+ }
1500
+ /** HA `swing_mode` values that mean the VERTICAL louver is moving. */
1501
+ var VERTICAL_ON_MODES = [
1502
+ "vertical",
1503
+ "both",
1504
+ "on"
1505
+ ];
1506
+ /** HA `swing_mode` / `swing_horizontal_mode` values that mean the HORIZONTAL louver is moving. */
1507
+ var HORIZONTAL_ON_MODES = [
1508
+ "horizontal",
1509
+ "both",
1510
+ "on"
1511
+ ];
1512
+ /**
1513
+ * HA models swing as one `swing_mode` picked from `swing_modes` (`off`,
1514
+ * `vertical`, `horizontal`, `both`, vendor strings), plus — on newer cores —
1515
+ * a separate `swing_horizontal_mode`. An axis the device never advertised is
1516
+ * `null`: no vocabulary, no control (D62).
1517
+ */
1518
+ function readSwing(attrs) {
1519
+ const swingModes = readStringArray$4(attrs, "swing_modes");
1520
+ const horizontalModes = readStringArray$4(attrs, "swing_horizontal_modes");
1521
+ const swingMode = (readStringAttr$6(attrs, "swing_mode") ?? "").toLowerCase();
1522
+ const horizontalMode = (readStringAttr$6(attrs, "swing_horizontal_mode") ?? "").toLowerCase();
1523
+ return {
1524
+ swingVertical: swingModes.length === 0 ? null : VERTICAL_ON_MODES.includes(swingMode),
1525
+ swingHorizontal: horizontalModes.length > 0 ? HORIZONTAL_ON_MODES.includes(horizontalMode) : swingModes.some((m) => HORIZONTAL_ON_MODES.includes(m.toLowerCase())) ? HORIZONTAL_ON_MODES.includes(swingMode) : null
1526
+ };
1527
+ }
1528
+ /**
1529
+ * The HA mode that turns one axis on or off, chosen from what the device
1530
+ * advertised; `null` when the vocabulary has no such value.
1531
+ */
1532
+ function pickSwingMode(advertised, enabled, onModes) {
1533
+ const lower = advertised.map((m) => m.toLowerCase());
1534
+ const wanted = enabled ? onModes : ["off"];
1535
+ const index = lower.findIndex((m) => wanted.includes(m));
1536
+ return index === -1 ? null : advertised[index] ?? null;
1537
+ }
1474
1538
  function sanitizeHvacMode(s) {
1475
1539
  switch (s) {
1476
1540
  case "off":
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-BpePMREY.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-ru9DzHlP.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
@@ -1409,31 +1409,14 @@ var HaClimateDevice = class extends HaBaseChildDevice {
1409
1409
  this.registerClimateCap();
1410
1410
  this.runtimeState.setCapState("climate-control", COLD_START$15);
1411
1411
  }
1412
+ /** The louver vocabularies HA last advertised, for the swing setters. */
1413
+ swingModes = [];
1414
+ swingHorizontalModes = [];
1412
1415
  handleStatePush(state) {
1413
1416
  if (state.state === "unavailable" || state.state === "unknown") return;
1414
- const lastFetchedAt = Date.parse(state.last_updated) || Date.now();
1415
- const attrs = state.attributes;
1416
- const mode = sanitizeHvacMode(state.state);
1417
- const availableModes = readStringArray$4(attrs, "hvac_modes").map(sanitizeHvacMode).filter((m) => m !== null);
1418
- const fanMode = readStringAttr$6(attrs, "fan_mode") ?? "";
1419
- const availableFanModes = readStringArray$4(attrs, "fan_modes");
1420
- const preset = readStringAttr$6(attrs, "preset_mode") ?? "";
1421
- const availablePresets = readStringArray$4(attrs, "preset_modes");
1422
- this.runtimeState.setCapState("climate-control", {
1423
- mode: mode ?? "off",
1424
- availableModes,
1425
- fanMode,
1426
- availableFanModes,
1427
- preset,
1428
- availablePresets,
1429
- target: readNumberAttr$5(attrs, "temperature"),
1430
- targetHigh: readNumberAttr$5(attrs, "target_temp_high"),
1431
- targetLow: readNumberAttr$5(attrs, "target_temp_low"),
1432
- targetHumidity: readNumberAttr$5(attrs, "target_humidity"),
1433
- currentTemp: readNumberAttr$5(attrs, "current_temperature"),
1434
- currentHumidity: readNumberAttr$5(attrs, "current_humidity"),
1435
- lastFetchedAt
1436
- });
1417
+ this.swingModes = readStringArray$4(state.attributes, "swing_modes");
1418
+ this.swingHorizontalModes = readStringArray$4(state.attributes, "swing_horizontal_modes");
1419
+ this.runtimeState.setCapState("climate-control", climateSliceFromState(state));
1437
1420
  }
1438
1421
  registerClimateCap() {
1439
1422
  this.ctx.registerNativeCap(climateControlCapability, {
@@ -1461,15 +1444,96 @@ var HaClimateDevice = class extends HaBaseChildDevice {
1461
1444
  setTargetHumidity: async ({ targetHumidity }) => {
1462
1445
  await this.callHaService("climate", "set_humidity", { humidity: targetHumidity });
1463
1446
  },
1464
- setSwingVertical: async () => {
1465
- throw new Error("HaClimateDevice: vertical swing not supported");
1447
+ setSwingVertical: async ({ on }) => {
1448
+ const swingMode = pickSwingMode(this.swingModes, on, VERTICAL_ON_MODES);
1449
+ if (swingMode === null) throw new Error("HaClimateDevice: vertical swing not supported");
1450
+ await this.callHaService("climate", "set_swing_mode", { swing_mode: swingMode });
1466
1451
  },
1467
- setSwingHorizontal: async () => {
1468
- throw new Error("HaClimateDevice: horizontal swing not supported");
1452
+ setSwingHorizontal: async ({ on }) => {
1453
+ if (this.swingHorizontalModes.length > 0) {
1454
+ const mode = pickSwingMode(this.swingHorizontalModes, on, HORIZONTAL_ON_MODES);
1455
+ if (mode === null) throw new Error("HaClimateDevice: horizontal swing not supported");
1456
+ await this.callHaService("climate", "set_swing_horizontal_mode", { swing_horizontal_mode: mode });
1457
+ return;
1458
+ }
1459
+ const swingMode = pickSwingMode(this.swingModes, on, HORIZONTAL_ON_MODES);
1460
+ if (swingMode === null) throw new Error("HaClimateDevice: horizontal swing not supported");
1461
+ await this.callHaService("climate", "set_swing_mode", { swing_mode: swingMode });
1469
1462
  }
1470
1463
  });
1471
1464
  }
1472
1465
  };
1466
+ /**
1467
+ * The `climate-control` slice for one HA `climate.*` state. Pure, and the ONLY
1468
+ * place the slice is assembled: every key the cap schema declares is present —
1469
+ * `swingVertical` / `swingHorizontal` are `nullable`, not optional, and a slice
1470
+ * that omitted them was refused by `setCapState` on every push (828 dropped
1471
+ * pushes in a day on the live hub, every thermostat stuck at its cold start).
1472
+ */
1473
+ function climateSliceFromState(state) {
1474
+ const lastFetchedAt = Date.parse(state.last_updated) || Date.now();
1475
+ const attrs = state.attributes;
1476
+ const mode = sanitizeHvacMode(state.state);
1477
+ const availableModes = readStringArray$4(attrs, "hvac_modes").map(sanitizeHvacMode).filter((m) => m !== null);
1478
+ const fanMode = readStringAttr$6(attrs, "fan_mode") ?? "";
1479
+ const availableFanModes = readStringArray$4(attrs, "fan_modes");
1480
+ const preset = readStringAttr$6(attrs, "preset_mode") ?? "";
1481
+ const availablePresets = readStringArray$4(attrs, "preset_modes");
1482
+ return {
1483
+ mode: mode ?? "off",
1484
+ availableModes,
1485
+ fanMode,
1486
+ availableFanModes,
1487
+ preset,
1488
+ availablePresets,
1489
+ target: readNumberAttr$5(attrs, "temperature"),
1490
+ targetHigh: readNumberAttr$5(attrs, "target_temp_high"),
1491
+ targetLow: readNumberAttr$5(attrs, "target_temp_low"),
1492
+ targetHumidity: readNumberAttr$5(attrs, "target_humidity"),
1493
+ currentTemp: readNumberAttr$5(attrs, "current_temperature"),
1494
+ currentHumidity: readNumberAttr$5(attrs, "current_humidity"),
1495
+ ...readSwing(attrs),
1496
+ lastFetchedAt
1497
+ };
1498
+ }
1499
+ /** HA `swing_mode` values that mean the VERTICAL louver is moving. */
1500
+ var VERTICAL_ON_MODES = [
1501
+ "vertical",
1502
+ "both",
1503
+ "on"
1504
+ ];
1505
+ /** HA `swing_mode` / `swing_horizontal_mode` values that mean the HORIZONTAL louver is moving. */
1506
+ var HORIZONTAL_ON_MODES = [
1507
+ "horizontal",
1508
+ "both",
1509
+ "on"
1510
+ ];
1511
+ /**
1512
+ * HA models swing as one `swing_mode` picked from `swing_modes` (`off`,
1513
+ * `vertical`, `horizontal`, `both`, vendor strings), plus — on newer cores —
1514
+ * a separate `swing_horizontal_mode`. An axis the device never advertised is
1515
+ * `null`: no vocabulary, no control (D62).
1516
+ */
1517
+ function readSwing(attrs) {
1518
+ const swingModes = readStringArray$4(attrs, "swing_modes");
1519
+ const horizontalModes = readStringArray$4(attrs, "swing_horizontal_modes");
1520
+ const swingMode = (readStringAttr$6(attrs, "swing_mode") ?? "").toLowerCase();
1521
+ const horizontalMode = (readStringAttr$6(attrs, "swing_horizontal_mode") ?? "").toLowerCase();
1522
+ return {
1523
+ swingVertical: swingModes.length === 0 ? null : VERTICAL_ON_MODES.includes(swingMode),
1524
+ swingHorizontal: horizontalModes.length > 0 ? HORIZONTAL_ON_MODES.includes(horizontalMode) : swingModes.some((m) => HORIZONTAL_ON_MODES.includes(m.toLowerCase())) ? HORIZONTAL_ON_MODES.includes(swingMode) : null
1525
+ };
1526
+ }
1527
+ /**
1528
+ * The HA mode that turns one axis on or off, chosen from what the device
1529
+ * advertised; `null` when the vocabulary has no such value.
1530
+ */
1531
+ function pickSwingMode(advertised, enabled, onModes) {
1532
+ const lower = advertised.map((m) => m.toLowerCase());
1533
+ const wanted = enabled ? onModes : ["off"];
1534
+ const index = lower.findIndex((m) => wanted.includes(m));
1535
+ return index === -1 ? null : advertised[index] ?? null;
1536
+ }
1473
1537
  function sanitizeHvacMode(s) {
1474
1538
  switch (s) {
1475
1539
  case "off":
@@ -7756,7 +7756,8 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7756
7756
  var CameraSwitchUnavailableReasonSchema = _enum([
7757
7757
  "no-provider",
7758
7758
  "source-unreachable",
7759
- "not-configured"
7759
+ "not-configured",
7760
+ "device-disabled"
7760
7761
  ]);
7761
7762
  /**
7762
7763
  * One switch, resolved for one camera.
@@ -24036,12 +24037,20 @@ var BatteryStatusSchema = object({
24036
24037
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
24037
24038
  * Reolink-specific for the Solar Panel 2 accessory (will become
24038
24039
  * common on other battery cams). `'none'` means running on battery
24039
- * alone.
24040
+ * alone — a NEGATIVE the firmware actually reported.
24041
+ *
24042
+ * `'unknown'` is the absence of an answer: the provider has not read the
24043
+ * power fields yet, or the firmware reported neither. Before it existed,
24044
+ * two unreadable fields produced `'none'`, and a camera nobody had reached
24045
+ * was drawn "on battery alone" — the same D315 defect `percentage` closed
24046
+ * by going nullable. Consumers SKIP it (no charger state published, no
24047
+ * glyph, no HomeKit ChargingState) rather than coerce it either way.
24040
24048
  */
24041
24049
  charging: _enum([
24042
24050
  "dc",
24043
24051
  "solar",
24044
- "none"
24052
+ "none",
24053
+ "unknown"
24045
24054
  ]),
24046
24055
  /**
24047
24056
  * True when the camera firmware has gone into low-power mode. Battery
@@ -24131,7 +24140,7 @@ onStatusChanged: { data: object({
24131
24140
  kind: "push",
24132
24141
  empty: {
24133
24142
  percentage: 0,
24134
- charging: "none",
24143
+ charging: "unknown",
24135
24144
  sleeping: false,
24136
24145
  lastUpdated: 0
24137
24146
  }
@@ -33556,8 +33565,17 @@ var BaseDeviceProvider = class extends BaseAddon {
33556
33565
  async onRestoreDevices(savedDevices) {
33557
33566
  const restored = /* @__PURE__ */ new Set();
33558
33567
  const failures = [];
33568
+ const liveStableIds = new Set((await this.ctx.kernel.devices.getAll()).map((device) => device.stableId));
33559
33569
  const attemptRestore = async (saved) => {
33560
33570
  if (restored.has(saved.id)) return;
33571
+ if (liveStableIds.has(saved.stableId)) {
33572
+ this.ctx.logger.debug("Restore row already live — not dialled again", { tags: {
33573
+ deviceId: saved.id,
33574
+ stableId: saved.stableId
33575
+ } });
33576
+ restored.add(saved.id);
33577
+ return;
33578
+ }
33561
33579
  const Class = this.deviceClasses[saved.type];
33562
33580
  if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
33563
33581
  if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
@@ -7756,7 +7756,8 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7756
7756
  var CameraSwitchUnavailableReasonSchema = _enum([
7757
7757
  "no-provider",
7758
7758
  "source-unreachable",
7759
- "not-configured"
7759
+ "not-configured",
7760
+ "device-disabled"
7760
7761
  ]);
7761
7762
  /**
7762
7763
  * One switch, resolved for one camera.
@@ -24036,12 +24037,20 @@ var BatteryStatusSchema = object({
24036
24037
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
24037
24038
  * Reolink-specific for the Solar Panel 2 accessory (will become
24038
24039
  * common on other battery cams). `'none'` means running on battery
24039
- * alone.
24040
+ * alone — a NEGATIVE the firmware actually reported.
24041
+ *
24042
+ * `'unknown'` is the absence of an answer: the provider has not read the
24043
+ * power fields yet, or the firmware reported neither. Before it existed,
24044
+ * two unreadable fields produced `'none'`, and a camera nobody had reached
24045
+ * was drawn "on battery alone" — the same D315 defect `percentage` closed
24046
+ * by going nullable. Consumers SKIP it (no charger state published, no
24047
+ * glyph, no HomeKit ChargingState) rather than coerce it either way.
24040
24048
  */
24041
24049
  charging: _enum([
24042
24050
  "dc",
24043
24051
  "solar",
24044
- "none"
24052
+ "none",
24053
+ "unknown"
24045
24054
  ]),
24046
24055
  /**
24047
24056
  * True when the camera firmware has gone into low-power mode. Battery
@@ -24131,7 +24140,7 @@ onStatusChanged: { data: object({
24131
24140
  kind: "push",
24132
24141
  empty: {
24133
24142
  percentage: 0,
24134
- charging: "none",
24143
+ charging: "unknown",
24135
24144
  sleeping: false,
24136
24145
  lastUpdated: 0
24137
24146
  }
@@ -33556,8 +33565,17 @@ var BaseDeviceProvider = class extends BaseAddon {
33556
33565
  async onRestoreDevices(savedDevices) {
33557
33566
  const restored = /* @__PURE__ */ new Set();
33558
33567
  const failures = [];
33568
+ const liveStableIds = new Set((await this.ctx.kernel.devices.getAll()).map((device) => device.stableId));
33559
33569
  const attemptRestore = async (saved) => {
33560
33570
  if (restored.has(saved.id)) return;
33571
+ if (liveStableIds.has(saved.stableId)) {
33572
+ this.ctx.logger.debug("Restore row already live — not dialled again", { tags: {
33573
+ deviceId: saved.id,
33574
+ stableId: saved.stableId
33575
+ } });
33576
+ restored.add(saved.id);
33577
+ return;
33578
+ }
33561
33579
  const Class = this.deviceClasses[saved.type];
33562
33580
  if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
33563
33581
  if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  //#endregion
6
- const require_dist = require("../dist-HZncHwuU.js");
6
+ const require_dist = require("../dist-GeeaZrMS.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
  }
@@ -5008,12 +4959,13 @@ function projectZoneOccupancy(deviceKey, input) {
5008
4959
  }
5009
4960
  function projectBatterySlice(deviceKey, slice) {
5010
4961
  const values = [{
5011
- topic: stateTopic(deviceKey, "charger"),
5012
- value: bool(slice.charging !== "none")
5013
- }, {
5014
4962
  topic: stateTopic(deviceKey, "sleeping"),
5015
4963
  value: bool(slice.sleeping)
5016
4964
  }];
4965
+ if (slice.charging !== "unknown") values.push({
4966
+ topic: stateTopic(deviceKey, "charger"),
4967
+ value: bool(slice.charging !== "none")
4968
+ });
5017
4969
  /**
5018
4970
  * `binary: true` means the device reports "low" or "not low" and the
5019
4971
  * percentage is a stand-in. Publishing it would put a fabricated
@@ -5127,38 +5079,6 @@ function projectAudioWindow(deviceKey, input) {
5127
5079
  });
5128
5080
  return values;
5129
5081
  }
5130
- /** Inverse of `SNOOZE_MINUTES` — a minute count back to the option label. */
5131
- var SNOOZE_OPTION_BY_MINUTES = new Map(Object.entries(SNOOZE_MINUTES).map(([option, minutes]) => [minutes, option]));
5132
- /**
5133
- * The `snooze` select's STATE. The authority is
5134
- * `notificationRules.listSnoozes`, which carries `startedAt`/`expiresAt` but
5135
- * not the preset label an operator picked — there is nowhere to store that
5136
- * label, and there must not be: it is reconstructed from the two timestamps
5137
- * every time, so it can never drift from the authority.
5138
- *
5139
- * No active snooze publishes `Off`, never silence: Home Assistant reads a
5140
- * missing state update as "the last value still holds", so a select gone
5141
- * quiet after a snooze expires would keep showing the expired duration
5142
- * forever.
5143
- *
5144
- * A duration the select's closed vocabulary cannot express — a snooze the
5145
- * viewer created with an arbitrary minute count — publishes NOTHING.
5146
- * Rounding it to the nearest preset would let Home Assistant re-send a
5147
- * duration the operator never chose; `unknown` is the honest state.
5148
- */
5149
- function projectSnooze(deviceKey, window) {
5150
- if (window === null) return [{
5151
- topic: stateTopic(deviceKey, "snooze"),
5152
- value: "Off"
5153
- }];
5154
- const minutes = Math.round((window.expiresAt - window.startedAt) / 6e4);
5155
- const option = SNOOZE_OPTION_BY_MINUTES.get(minutes);
5156
- if (option === void 0) return [];
5157
- return [{
5158
- topic: stateTopic(deviceKey, "snooze"),
5159
- value: option
5160
- }];
5161
- }
5162
5082
  /**
5163
5083
  * `status` — the entity D62 exists for. A camera an operator switched off and
5164
5084
  * a camera that is genuinely broken must never paint the same picture in Home
@@ -5317,12 +5237,6 @@ function ruleTriggerEntity(ruleId, suffix) {
5317
5237
  }
5318
5238
  function notificationCenterSpecs(input) {
5319
5239
  return [
5320
- {
5321
- entity: "snoozed",
5322
- platform: "binary_sensor",
5323
- label: "Notifications snoozed",
5324
- icon: "mdi:bell-sleep"
5325
- },
5326
5240
  {
5327
5241
  entity: "rules_enabled",
5328
5242
  platform: "sensor",
@@ -5332,21 +5246,6 @@ function notificationCenterSpecs(input) {
5332
5246
  },
5333
5247
  (
5334
5248
  /**
5335
- * The GLOBAL snooze, as a control (Task D4) — the half `snoozed` (above)
5336
- * never had. Its state is NOT projected here: `ha-export.addon.ts` pushes
5337
- * it through the SAME `projectSnooze` a camera's snooze uses, so the rule
5338
- * lives in one place — two expressions of it in two files is how
5339
- * `audio_detected` went mute for a year.
5340
- */
5341
- {
5342
- entity: "snooze",
5343
- platform: "select",
5344
- label: "Snooze all notifications",
5345
- writable: true,
5346
- options: SNOOZE_OPTIONS
5347
- }),
5348
- (
5349
- /**
5350
5249
  * The last trigger, six entities: five facts plus its picture — "conoscere
5351
5250
  * gli ultimi trigger, le ultime immagini... e da quale label" (operator,
5352
5251
  * 2026-08-25). ENABLED by default like every other entity on this device
@@ -5667,10 +5566,6 @@ function projectSynthetic(input, nowMs) {
5667
5566
  const srv = syntheticDeviceKeyFor(SERVER_STABLE_ID);
5668
5567
  const online = input.nodes.filter((node) => node.online);
5669
5568
  const values = [
5670
- {
5671
- topic: stateTopic(nc, "snoozed"),
5672
- value: String(input.snoozed)
5673
- },
5674
5569
  {
5675
5570
  topic: stateTopic(nc, "rules_enabled"),
5676
5571
  value: String(input.rules.filter((rule) => rule.enabled).length)
@@ -6506,16 +6401,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6506
6401
  });
6507
6402
  /** `deviceManager.listBindableCapsForDeviceType` — once per TYPE, not per device. */
6508
6403
  const bindableCapsByType = /* @__PURE__ */ new Map();
6509
- /**
6510
- * `notificationRules.listSnoozes` — ONE read for the whole pass, shared
6511
- * by every camera's `snooze` select (Task B1) and the synthetic
6512
- * `snooze` select on *Notification Center* (Task D4): two expressions
6513
- * of the same read is how `audio_detected` went mute for a year.
6514
- */
6515
- const snoozes = await this.ctx.api.notificationRules.listSnoozes.query({}).then((r) => r.snoozes).catch((err) => {
6516
- this.ctx.logger.warn("ha-export: could not read snoozes, snooze selects go unknown", { meta: { error: errMsg(err) } });
6517
- return [];
6518
- });
6519
6404
  const exported = /* @__PURE__ */ new Map();
6520
6405
  const keyByDeviceId = /* @__PURE__ */ new Map();
6521
6406
  const allCaps = /* @__PURE__ */ new Set();
@@ -6608,7 +6493,7 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6608
6493
  * `brokerIds` is every enabled broker, because they belong to the server
6609
6494
  * and not to a membership the operator picks per camera.
6610
6495
  */
6611
- const synthetic = await this.buildSynthetic(snoozes, byId);
6496
+ const synthetic = await this.buildSynthetic(byId);
6612
6497
  if (synthetic !== null) for (const plan of syntheticPlans(synthetic)) {
6613
6498
  exported.set(plan.deviceKey, {
6614
6499
  plan,
@@ -6648,16 +6533,8 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6648
6533
  }
6649
6534
  });
6650
6535
  await this.announce(exported);
6651
- await this.pushFullState(exported, snapshots, snoozes);
6652
- if (synthetic !== null) {
6653
- /**
6654
- * The global snooze's STATE (Task D4) rides the SAME `projectSnooze`
6655
- * a camera's `snooze` select uses, over the SAME `snoozes` read this
6656
- * pass already made — one expression of the rule, not two.
6657
- */
6658
- const ncKey = syntheticDeviceKeyFor(NOTIFICATION_CENTER_STABLE_ID);
6659
- await this.pushSynthetic([...projectSynthetic(synthetic, Date.now()), ...projectSnooze(ncKey, latestGlobalSnoozeWindow(snoozes))], [...enabled]);
6660
- }
6536
+ await this.pushFullState(exported, snapshots);
6537
+ if (synthetic !== null) await this.pushSynthetic(projectSynthetic(synthetic, Date.now()), [...enabled]);
6661
6538
  this.ctx.logger.info("ha-export: reconciled", { meta: {
6662
6539
  reason,
6663
6540
  brokers: enabled.length,
@@ -6691,7 +6568,7 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6691
6568
  * Returns `null` only when NOTHING answered, so the devices are not
6692
6569
  * announced empty on a hub that is still booting.
6693
6570
  */
6694
- async buildSynthetic(snoozes, byId) {
6571
+ async buildSynthetic(byId) {
6695
6572
  const rules = await this.ctx.api.notificationRules.listRules.query({}).then((r) => r.rules.map((rule) => ({
6696
6573
  id: rule.id,
6697
6574
  name: rule.name,
@@ -6700,8 +6577,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6700
6577
  this.ctx.logger.warn("ha-export: could not read notification rules", { meta: { error: errMsg(err) } });
6701
6578
  return null;
6702
6579
  });
6703
- /** Reuses the ONE `listSnoozes` read the pass already made (Task B1/D4). */
6704
- const anySnoozed = snoozes.length > 0;
6705
6580
  const topology = await this.ctx.api.nodes.topology.query().catch((err) => {
6706
6581
  this.ctx.logger.warn("ha-export: could not read the cluster topology", { meta: { error: errMsg(err) } });
6707
6582
  return null;
@@ -6725,7 +6600,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6725
6600
  const { nodes, oldestFootageMs } = await this.buildSyntheticNodes(topology ?? []);
6726
6601
  return {
6727
6602
  rules: effectiveRules,
6728
- snoozed: anySnoozed,
6729
6603
  lastTrigger,
6730
6604
  lastTriggerByRule,
6731
6605
  nodes,
@@ -6907,9 +6781,9 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6907
6781
  }
6908
6782
  }
6909
6783
  }
6910
- async pushFullState(exported, snapshots, snoozes) {
6784
+ async pushFullState(exported, snapshots) {
6911
6785
  for (const entry of exported.values()) try {
6912
- const values = await this.currentStateFor(entry, snapshots[String(entry.deviceId)], snoozes);
6786
+ const values = await this.currentStateFor(entry, snapshots[String(entry.deviceId)]);
6913
6787
  this.push(entry.deviceId, values);
6914
6788
  } catch (err) {
6915
6789
  this.ctx.logger.warn("ha-export: could not read current state for a device", {
@@ -6920,7 +6794,7 @@ var HaExportAddon = class extends require_dist.BaseAddon {
6920
6794
  for (const link of this.links.values()) await link.client.flush();
6921
6795
  }
6922
6796
  /** Everything readable without waiting for an event. */
6923
- async currentStateFor(entry, snapshotRaw, snoozes) {
6797
+ async currentStateFor(entry, snapshotRaw) {
6924
6798
  const key = entry.plan.deviceKey;
6925
6799
  const values = [];
6926
6800
  const snapshot = toSnapshot(snapshotRaw);
@@ -7000,16 +6874,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
7000
6874
  */
7001
6875
  const cameraStatus = this.cameraStatusByDeviceId.get(entry.deviceId);
7002
6876
  if (cameraStatus !== void 0) values.push(...projectCameraStatus(key, toCameraStatusInput(cameraStatus)));
7003
- /**
7004
- * `snooze` (Task B1) — reconstructed from `notificationRules.listSnoozes`,
7005
- * never stored: the select's state is the SAME authority the write path
7006
- * (`applySnooze`) already uses.
7007
- */
7008
- const snoozeValues = projectSnooze(key, latestDeviceSnoozeWindow(snoozes, entry.deviceId));
7009
- if (snoozeValues.length === 0) {
7010
- 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 } });
7011
- }
7012
- values.push(...snoozeValues);
7013
6877
  const occupancy = await this.ctx.api.zoneAnalytics.getCurrentSnapshot.query({ deviceId: entry.deviceId });
7014
6878
  if (occupancy !== null) values.push(...projectZoneOccupancy(key, {
7015
6879
  zones: occupancy.zones.map((zone) => ({
@@ -7501,38 +7365,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
7501
7365
  * disagree with the admin UI is worse than no knob (D62).
7502
7366
  */
7503
7367
  if (isSyntheticDeviceKey(parsed.deviceKey)) {
7504
- /**
7505
- * The global snooze select (Task D4) — the OTHER control this branch
7506
- * routes, alongside the per-rule switches below. Same rule as the
7507
- * camera's own snooze (`applySnooze`): the export writes
7508
- * `createSnooze`/`cancelSnooze` directly, `scope: 'all'`, never a
7509
- * store of its own.
7510
- */
7511
- if (parsed.deviceKey === syntheticDeviceKeyFor("synthetic-notification-center") && parsed.entity === "snooze") {
7512
- if (!SNOOZE_OPTIONS.includes(value)) {
7513
- this.ctx.logger.warn("ha-export: dropping a global snooze command, unknown option", { meta: {
7514
- topic,
7515
- value
7516
- } });
7517
- reply.status(422);
7518
- reply.send({ error: "unroutable command" });
7519
- return;
7520
- }
7521
- const minutes = SNOOZE_MINUTES[value] ?? null;
7522
- try {
7523
- await this.applyGlobalSnooze(minutes);
7524
- reply.status(200);
7525
- reply.send({});
7526
- } catch (err) {
7527
- this.ctx.logger.warn("ha-export: could not apply the global snooze from Home Assistant", { meta: {
7528
- minutes,
7529
- error: errMsg(err)
7530
- } });
7531
- reply.status(422);
7532
- reply.send({ error: "command not applied" });
7533
- }
7534
- return;
7535
- }
7536
7368
  const ruleId = ruleIdFromEntity(parsed.entity, this.syntheticRules);
7537
7369
  if (ruleId === null) {
7538
7370
  this.ctx.logger.warn("ha-export: dropping a synthetic command with no authority behind it", { meta: {
@@ -7702,7 +7534,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
7702
7534
  await device.ptz.goToPreset({ presetId: preset.id });
7703
7535
  return true;
7704
7536
  }
7705
- case "snooze": return await this.applySnooze(command.deviceId, command.minutes);
7706
7537
  }
7707
7538
  } catch (err) {
7708
7539
  /**
@@ -7728,78 +7559,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
7728
7559
  });
7729
7560
  return false;
7730
7561
  }
7731
- async applySnooze(deviceId, minutes) {
7732
- if (minutes === null) {
7733
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7734
- const mine = snoozes.filter((snooze) => snooze.deviceId === deviceId);
7735
- for (const snooze of mine) await this.ctx.api.notificationRules.cancelSnooze.mutate({ snoozeId: snooze.id });
7736
- this.ctx.logger.info("ha-export: cancelled snoozes from Home Assistant", {
7737
- tags: { deviceId },
7738
- meta: { cancelled: mine.length }
7739
- });
7740
- } else {
7741
- await this.ctx.api.notificationRules.createSnooze.mutate({ snooze: {
7742
- scope: "device",
7743
- deviceId,
7744
- durationMinutes: minutes
7745
- } });
7746
- this.ctx.logger.info("ha-export: snoozed from Home Assistant", {
7747
- tags: { deviceId },
7748
- meta: { minutes }
7749
- });
7750
- }
7751
- /**
7752
- * Republish right away — the select is otherwise stale for up to
7753
- * `reconcileIntervalSec` after a write Home Assistant itself made
7754
- * (Task B1 passo 4).
7755
- */
7756
- await this.republishSnooze(deviceId);
7757
- return true;
7758
- }
7759
- /** Push `snooze`'s current state for one camera, outside the reconcile cadence. */
7760
- async republishSnooze(deviceId) {
7761
- const key = this.keyByDeviceId.get(deviceId);
7762
- if (key === void 0) return;
7763
- try {
7764
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7765
- this.push(deviceId, projectSnooze(key, latestDeviceSnoozeWindow(snoozes, deviceId)));
7766
- } catch (err) {
7767
- this.ctx.logger.warn("ha-export: could not republish snooze after a write from Home Assistant", {
7768
- tags: { deviceId },
7769
- meta: { error: errMsg(err) }
7770
- });
7771
- }
7772
- }
7773
- /**
7774
- * The global `select` on *Notification Center* (Task D4) — `scope: 'all'`,
7775
- * the same authority `applySnooze` already writes for a camera. `Off`
7776
- * cancels every global snooze, exactly as a camera's `Off` cancels its own.
7777
- */
7778
- async applyGlobalSnooze(minutes) {
7779
- if (minutes === null) {
7780
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7781
- const mine = snoozes.filter((snooze) => snooze.scope === "all");
7782
- for (const snooze of mine) await this.ctx.api.notificationRules.cancelSnooze.mutate({ snoozeId: snooze.id });
7783
- this.ctx.logger.info("ha-export: cancelled the global snooze from Home Assistant", { meta: { cancelled: mine.length } });
7784
- } else {
7785
- await this.ctx.api.notificationRules.createSnooze.mutate({ snooze: {
7786
- scope: "all",
7787
- durationMinutes: minutes
7788
- } });
7789
- this.ctx.logger.info("ha-export: snoozed globally from Home Assistant", { meta: { minutes } });
7790
- }
7791
- await this.republishGlobalSnooze();
7792
- }
7793
- /** Push the global `snooze` select's current state, outside the reconcile cadence. */
7794
- async republishGlobalSnooze() {
7795
- try {
7796
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7797
- const ncKey = syntheticDeviceKeyFor(NOTIFICATION_CENTER_STABLE_ID);
7798
- await this.pushSynthetic(projectSnooze(ncKey, latestGlobalSnoozeWindow(snoozes)), this.enabledBrokerIds());
7799
- } catch (err) {
7800
- this.ctx.logger.warn("ha-export: could not republish the global snooze after a write from Home Assistant", { meta: { error: errMsg(err) } });
7801
- }
7802
- }
7803
7562
  /**
7804
7563
  * A public, signed, expiring route Home Assistant can fetch bytes from.
7805
7564
  *
@@ -8097,10 +7856,6 @@ var HaExportAddon = class extends require_dist.BaseAddon {
8097
7856
  {
8098
7857
  label: "Capabilities unclassified",
8099
7858
  value: this.unclassified.length === 0 ? "none" : this.unclassified.join(", ")
8100
- },
8101
- {
8102
- label: "Snooze options",
8103
- value: SNOOZE_OPTIONS.join(", ")
8104
7859
  }
8105
7860
  ],
8106
7861
  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."
@@ -8193,29 +7948,6 @@ function toCameraStatusInput(status) {
8193
7948
  recording: status.recording === null ? null : { active: status.recording.active }
8194
7949
  };
8195
7950
  }
8196
- /**
8197
- * The ACTIVE window for one scope, from a flat `listSnoozes` answer.
8198
- *
8199
- * "Active" is `expiresAt` in the future — `listSnoozes` never sweeps expired
8200
- * rows (expiry is a comparison, not a job), so a reader that skipped this
8201
- * would keep publishing a duration that ended hours ago. When several windows
8202
- * match, the one expiring LATEST wins: that is the duration still in effect.
8203
- */
8204
- function latestSnoozeWindow(snoozes, matches, now) {
8205
- const latest = snoozes.filter((s) => matches(s) && s.expiresAt > now).reduce((top, s) => top === null || s.expiresAt > top.expiresAt ? s : top, null);
8206
- return latest === null ? null : {
8207
- startedAt: latest.startedAt,
8208
- expiresAt: latest.expiresAt
8209
- };
8210
- }
8211
- /** The camera's own `scope: 'device'` snoozes (Task B1). */
8212
- function latestDeviceSnoozeWindow(snoozes, deviceId) {
8213
- return latestSnoozeWindow(snoozes, (s) => s.scope === "device" && s.deviceId === deviceId, Date.now());
8214
- }
8215
- /** The global `scope: 'all'` snooze (Task D4). */
8216
- function latestGlobalSnoozeWindow(snoozes) {
8217
- return latestSnoozeWindow(snoozes, (s) => s.scope === "all", Date.now());
8218
- }
8219
7951
  function toCountRecord(raw) {
8220
7952
  if (raw === null || typeof raw !== "object") return {};
8221
7953
  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-BpePMREY.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-ru9DzHlP.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
  }
@@ -5003,12 +4954,13 @@ function projectZoneOccupancy(deviceKey, input) {
5003
4954
  }
5004
4955
  function projectBatterySlice(deviceKey, slice) {
5005
4956
  const values = [{
5006
- topic: stateTopic(deviceKey, "charger"),
5007
- value: bool(slice.charging !== "none")
5008
- }, {
5009
4957
  topic: stateTopic(deviceKey, "sleeping"),
5010
4958
  value: bool(slice.sleeping)
5011
4959
  }];
4960
+ if (slice.charging !== "unknown") values.push({
4961
+ topic: stateTopic(deviceKey, "charger"),
4962
+ value: bool(slice.charging !== "none")
4963
+ });
5012
4964
  /**
5013
4965
  * `binary: true` means the device reports "low" or "not low" and the
5014
4966
  * percentage is a stand-in. Publishing it would put a fabricated
@@ -5122,38 +5074,6 @@ function projectAudioWindow(deviceKey, input) {
5122
5074
  });
5123
5075
  return values;
5124
5076
  }
5125
- /** Inverse of `SNOOZE_MINUTES` — a minute count back to the option label. */
5126
- var SNOOZE_OPTION_BY_MINUTES = new Map(Object.entries(SNOOZE_MINUTES).map(([option, minutes]) => [minutes, option]));
5127
- /**
5128
- * The `snooze` select's STATE. The authority is
5129
- * `notificationRules.listSnoozes`, which carries `startedAt`/`expiresAt` but
5130
- * not the preset label an operator picked — there is nowhere to store that
5131
- * label, and there must not be: it is reconstructed from the two timestamps
5132
- * every time, so it can never drift from the authority.
5133
- *
5134
- * No active snooze publishes `Off`, never silence: Home Assistant reads a
5135
- * missing state update as "the last value still holds", so a select gone
5136
- * quiet after a snooze expires would keep showing the expired duration
5137
- * forever.
5138
- *
5139
- * A duration the select's closed vocabulary cannot express — a snooze the
5140
- * viewer created with an arbitrary minute count — publishes NOTHING.
5141
- * Rounding it to the nearest preset would let Home Assistant re-send a
5142
- * duration the operator never chose; `unknown` is the honest state.
5143
- */
5144
- function projectSnooze(deviceKey, window) {
5145
- if (window === null) return [{
5146
- topic: stateTopic(deviceKey, "snooze"),
5147
- value: "Off"
5148
- }];
5149
- const minutes = Math.round((window.expiresAt - window.startedAt) / 6e4);
5150
- const option = SNOOZE_OPTION_BY_MINUTES.get(minutes);
5151
- if (option === void 0) return [];
5152
- return [{
5153
- topic: stateTopic(deviceKey, "snooze"),
5154
- value: option
5155
- }];
5156
- }
5157
5077
  /**
5158
5078
  * `status` — the entity D62 exists for. A camera an operator switched off and
5159
5079
  * a camera that is genuinely broken must never paint the same picture in Home
@@ -5312,12 +5232,6 @@ function ruleTriggerEntity(ruleId, suffix) {
5312
5232
  }
5313
5233
  function notificationCenterSpecs(input) {
5314
5234
  return [
5315
- {
5316
- entity: "snoozed",
5317
- platform: "binary_sensor",
5318
- label: "Notifications snoozed",
5319
- icon: "mdi:bell-sleep"
5320
- },
5321
5235
  {
5322
5236
  entity: "rules_enabled",
5323
5237
  platform: "sensor",
@@ -5327,21 +5241,6 @@ function notificationCenterSpecs(input) {
5327
5241
  },
5328
5242
  (
5329
5243
  /**
5330
- * The GLOBAL snooze, as a control (Task D4) — the half `snoozed` (above)
5331
- * never had. Its state is NOT projected here: `ha-export.addon.ts` pushes
5332
- * it through the SAME `projectSnooze` a camera's snooze uses, so the rule
5333
- * lives in one place — two expressions of it in two files is how
5334
- * `audio_detected` went mute for a year.
5335
- */
5336
- {
5337
- entity: "snooze",
5338
- platform: "select",
5339
- label: "Snooze all notifications",
5340
- writable: true,
5341
- options: SNOOZE_OPTIONS
5342
- }),
5343
- (
5344
- /**
5345
5244
  * The last trigger, six entities: five facts plus its picture — "conoscere
5346
5245
  * gli ultimi trigger, le ultime immagini... e da quale label" (operator,
5347
5246
  * 2026-08-25). ENABLED by default like every other entity on this device
@@ -5662,10 +5561,6 @@ function projectSynthetic(input, nowMs) {
5662
5561
  const srv = syntheticDeviceKeyFor(SERVER_STABLE_ID);
5663
5562
  const online = input.nodes.filter((node) => node.online);
5664
5563
  const values = [
5665
- {
5666
- topic: stateTopic(nc, "snoozed"),
5667
- value: String(input.snoozed)
5668
- },
5669
5564
  {
5670
5565
  topic: stateTopic(nc, "rules_enabled"),
5671
5566
  value: String(input.rules.filter((rule) => rule.enabled).length)
@@ -6501,16 +6396,6 @@ var HaExportAddon = class extends BaseAddon {
6501
6396
  });
6502
6397
  /** `deviceManager.listBindableCapsForDeviceType` — once per TYPE, not per device. */
6503
6398
  const bindableCapsByType = /* @__PURE__ */ new Map();
6504
- /**
6505
- * `notificationRules.listSnoozes` — ONE read for the whole pass, shared
6506
- * by every camera's `snooze` select (Task B1) and the synthetic
6507
- * `snooze` select on *Notification Center* (Task D4): two expressions
6508
- * of the same read is how `audio_detected` went mute for a year.
6509
- */
6510
- const snoozes = await this.ctx.api.notificationRules.listSnoozes.query({}).then((r) => r.snoozes).catch((err) => {
6511
- this.ctx.logger.warn("ha-export: could not read snoozes, snooze selects go unknown", { meta: { error: errMsg(err) } });
6512
- return [];
6513
- });
6514
6399
  const exported = /* @__PURE__ */ new Map();
6515
6400
  const keyByDeviceId = /* @__PURE__ */ new Map();
6516
6401
  const allCaps = /* @__PURE__ */ new Set();
@@ -6603,7 +6488,7 @@ var HaExportAddon = class extends BaseAddon {
6603
6488
  * `brokerIds` is every enabled broker, because they belong to the server
6604
6489
  * and not to a membership the operator picks per camera.
6605
6490
  */
6606
- const synthetic = await this.buildSynthetic(snoozes, byId);
6491
+ const synthetic = await this.buildSynthetic(byId);
6607
6492
  if (synthetic !== null) for (const plan of syntheticPlans(synthetic)) {
6608
6493
  exported.set(plan.deviceKey, {
6609
6494
  plan,
@@ -6643,16 +6528,8 @@ var HaExportAddon = class extends BaseAddon {
6643
6528
  }
6644
6529
  });
6645
6530
  await this.announce(exported);
6646
- await this.pushFullState(exported, snapshots, snoozes);
6647
- if (synthetic !== null) {
6648
- /**
6649
- * The global snooze's STATE (Task D4) rides the SAME `projectSnooze`
6650
- * a camera's `snooze` select uses, over the SAME `snoozes` read this
6651
- * pass already made — one expression of the rule, not two.
6652
- */
6653
- const ncKey = syntheticDeviceKeyFor(NOTIFICATION_CENTER_STABLE_ID);
6654
- await this.pushSynthetic([...projectSynthetic(synthetic, Date.now()), ...projectSnooze(ncKey, latestGlobalSnoozeWindow(snoozes))], [...enabled]);
6655
- }
6531
+ await this.pushFullState(exported, snapshots);
6532
+ if (synthetic !== null) await this.pushSynthetic(projectSynthetic(synthetic, Date.now()), [...enabled]);
6656
6533
  this.ctx.logger.info("ha-export: reconciled", { meta: {
6657
6534
  reason,
6658
6535
  brokers: enabled.length,
@@ -6686,7 +6563,7 @@ var HaExportAddon = class extends BaseAddon {
6686
6563
  * Returns `null` only when NOTHING answered, so the devices are not
6687
6564
  * announced empty on a hub that is still booting.
6688
6565
  */
6689
- async buildSynthetic(snoozes, byId) {
6566
+ async buildSynthetic(byId) {
6690
6567
  const rules = await this.ctx.api.notificationRules.listRules.query({}).then((r) => r.rules.map((rule) => ({
6691
6568
  id: rule.id,
6692
6569
  name: rule.name,
@@ -6695,8 +6572,6 @@ var HaExportAddon = class extends BaseAddon {
6695
6572
  this.ctx.logger.warn("ha-export: could not read notification rules", { meta: { error: errMsg(err) } });
6696
6573
  return null;
6697
6574
  });
6698
- /** Reuses the ONE `listSnoozes` read the pass already made (Task B1/D4). */
6699
- const anySnoozed = snoozes.length > 0;
6700
6575
  const topology = await this.ctx.api.nodes.topology.query().catch((err) => {
6701
6576
  this.ctx.logger.warn("ha-export: could not read the cluster topology", { meta: { error: errMsg(err) } });
6702
6577
  return null;
@@ -6720,7 +6595,6 @@ var HaExportAddon = class extends BaseAddon {
6720
6595
  const { nodes, oldestFootageMs } = await this.buildSyntheticNodes(topology ?? []);
6721
6596
  return {
6722
6597
  rules: effectiveRules,
6723
- snoozed: anySnoozed,
6724
6598
  lastTrigger,
6725
6599
  lastTriggerByRule,
6726
6600
  nodes,
@@ -6902,9 +6776,9 @@ var HaExportAddon = class extends BaseAddon {
6902
6776
  }
6903
6777
  }
6904
6778
  }
6905
- async pushFullState(exported, snapshots, snoozes) {
6779
+ async pushFullState(exported, snapshots) {
6906
6780
  for (const entry of exported.values()) try {
6907
- const values = await this.currentStateFor(entry, snapshots[String(entry.deviceId)], snoozes);
6781
+ const values = await this.currentStateFor(entry, snapshots[String(entry.deviceId)]);
6908
6782
  this.push(entry.deviceId, values);
6909
6783
  } catch (err) {
6910
6784
  this.ctx.logger.warn("ha-export: could not read current state for a device", {
@@ -6915,7 +6789,7 @@ var HaExportAddon = class extends BaseAddon {
6915
6789
  for (const link of this.links.values()) await link.client.flush();
6916
6790
  }
6917
6791
  /** Everything readable without waiting for an event. */
6918
- async currentStateFor(entry, snapshotRaw, snoozes) {
6792
+ async currentStateFor(entry, snapshotRaw) {
6919
6793
  const key = entry.plan.deviceKey;
6920
6794
  const values = [];
6921
6795
  const snapshot = toSnapshot(snapshotRaw);
@@ -6995,16 +6869,6 @@ var HaExportAddon = class extends BaseAddon {
6995
6869
  */
6996
6870
  const cameraStatus = this.cameraStatusByDeviceId.get(entry.deviceId);
6997
6871
  if (cameraStatus !== void 0) values.push(...projectCameraStatus(key, toCameraStatusInput(cameraStatus)));
6998
- /**
6999
- * `snooze` (Task B1) — reconstructed from `notificationRules.listSnoozes`,
7000
- * never stored: the select's state is the SAME authority the write path
7001
- * (`applySnooze`) already uses.
7002
- */
7003
- const snoozeValues = projectSnooze(key, latestDeviceSnoozeWindow(snoozes, entry.deviceId));
7004
- if (snoozeValues.length === 0) {
7005
- 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 } });
7006
- }
7007
- values.push(...snoozeValues);
7008
6872
  const occupancy = await this.ctx.api.zoneAnalytics.getCurrentSnapshot.query({ deviceId: entry.deviceId });
7009
6873
  if (occupancy !== null) values.push(...projectZoneOccupancy(key, {
7010
6874
  zones: occupancy.zones.map((zone) => ({
@@ -7496,38 +7360,6 @@ var HaExportAddon = class extends BaseAddon {
7496
7360
  * disagree with the admin UI is worse than no knob (D62).
7497
7361
  */
7498
7362
  if (isSyntheticDeviceKey(parsed.deviceKey)) {
7499
- /**
7500
- * The global snooze select (Task D4) — the OTHER control this branch
7501
- * routes, alongside the per-rule switches below. Same rule as the
7502
- * camera's own snooze (`applySnooze`): the export writes
7503
- * `createSnooze`/`cancelSnooze` directly, `scope: 'all'`, never a
7504
- * store of its own.
7505
- */
7506
- if (parsed.deviceKey === syntheticDeviceKeyFor("synthetic-notification-center") && parsed.entity === "snooze") {
7507
- if (!SNOOZE_OPTIONS.includes(value)) {
7508
- this.ctx.logger.warn("ha-export: dropping a global snooze command, unknown option", { meta: {
7509
- topic,
7510
- value
7511
- } });
7512
- reply.status(422);
7513
- reply.send({ error: "unroutable command" });
7514
- return;
7515
- }
7516
- const minutes = SNOOZE_MINUTES[value] ?? null;
7517
- try {
7518
- await this.applyGlobalSnooze(minutes);
7519
- reply.status(200);
7520
- reply.send({});
7521
- } catch (err) {
7522
- this.ctx.logger.warn("ha-export: could not apply the global snooze from Home Assistant", { meta: {
7523
- minutes,
7524
- error: errMsg(err)
7525
- } });
7526
- reply.status(422);
7527
- reply.send({ error: "command not applied" });
7528
- }
7529
- return;
7530
- }
7531
7363
  const ruleId = ruleIdFromEntity(parsed.entity, this.syntheticRules);
7532
7364
  if (ruleId === null) {
7533
7365
  this.ctx.logger.warn("ha-export: dropping a synthetic command with no authority behind it", { meta: {
@@ -7697,7 +7529,6 @@ var HaExportAddon = class extends BaseAddon {
7697
7529
  await device.ptz.goToPreset({ presetId: preset.id });
7698
7530
  return true;
7699
7531
  }
7700
- case "snooze": return await this.applySnooze(command.deviceId, command.minutes);
7701
7532
  }
7702
7533
  } catch (err) {
7703
7534
  /**
@@ -7723,78 +7554,6 @@ var HaExportAddon = class extends BaseAddon {
7723
7554
  });
7724
7555
  return false;
7725
7556
  }
7726
- async applySnooze(deviceId, minutes) {
7727
- if (minutes === null) {
7728
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7729
- const mine = snoozes.filter((snooze) => snooze.deviceId === deviceId);
7730
- for (const snooze of mine) await this.ctx.api.notificationRules.cancelSnooze.mutate({ snoozeId: snooze.id });
7731
- this.ctx.logger.info("ha-export: cancelled snoozes from Home Assistant", {
7732
- tags: { deviceId },
7733
- meta: { cancelled: mine.length }
7734
- });
7735
- } else {
7736
- await this.ctx.api.notificationRules.createSnooze.mutate({ snooze: {
7737
- scope: "device",
7738
- deviceId,
7739
- durationMinutes: minutes
7740
- } });
7741
- this.ctx.logger.info("ha-export: snoozed from Home Assistant", {
7742
- tags: { deviceId },
7743
- meta: { minutes }
7744
- });
7745
- }
7746
- /**
7747
- * Republish right away — the select is otherwise stale for up to
7748
- * `reconcileIntervalSec` after a write Home Assistant itself made
7749
- * (Task B1 passo 4).
7750
- */
7751
- await this.republishSnooze(deviceId);
7752
- return true;
7753
- }
7754
- /** Push `snooze`'s current state for one camera, outside the reconcile cadence. */
7755
- async republishSnooze(deviceId) {
7756
- const key = this.keyByDeviceId.get(deviceId);
7757
- if (key === void 0) return;
7758
- try {
7759
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7760
- this.push(deviceId, projectSnooze(key, latestDeviceSnoozeWindow(snoozes, deviceId)));
7761
- } catch (err) {
7762
- this.ctx.logger.warn("ha-export: could not republish snooze after a write from Home Assistant", {
7763
- tags: { deviceId },
7764
- meta: { error: errMsg(err) }
7765
- });
7766
- }
7767
- }
7768
- /**
7769
- * The global `select` on *Notification Center* (Task D4) — `scope: 'all'`,
7770
- * the same authority `applySnooze` already writes for a camera. `Off`
7771
- * cancels every global snooze, exactly as a camera's `Off` cancels its own.
7772
- */
7773
- async applyGlobalSnooze(minutes) {
7774
- if (minutes === null) {
7775
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7776
- const mine = snoozes.filter((snooze) => snooze.scope === "all");
7777
- for (const snooze of mine) await this.ctx.api.notificationRules.cancelSnooze.mutate({ snoozeId: snooze.id });
7778
- this.ctx.logger.info("ha-export: cancelled the global snooze from Home Assistant", { meta: { cancelled: mine.length } });
7779
- } else {
7780
- await this.ctx.api.notificationRules.createSnooze.mutate({ snooze: {
7781
- scope: "all",
7782
- durationMinutes: minutes
7783
- } });
7784
- this.ctx.logger.info("ha-export: snoozed globally from Home Assistant", { meta: { minutes } });
7785
- }
7786
- await this.republishGlobalSnooze();
7787
- }
7788
- /** Push the global `snooze` select's current state, outside the reconcile cadence. */
7789
- async republishGlobalSnooze() {
7790
- try {
7791
- const { snoozes } = await this.ctx.api.notificationRules.listSnoozes.query({});
7792
- const ncKey = syntheticDeviceKeyFor(NOTIFICATION_CENTER_STABLE_ID);
7793
- await this.pushSynthetic(projectSnooze(ncKey, latestGlobalSnoozeWindow(snoozes)), this.enabledBrokerIds());
7794
- } catch (err) {
7795
- this.ctx.logger.warn("ha-export: could not republish the global snooze after a write from Home Assistant", { meta: { error: errMsg(err) } });
7796
- }
7797
- }
7798
7557
  /**
7799
7558
  * A public, signed, expiring route Home Assistant can fetch bytes from.
7800
7559
  *
@@ -8092,10 +7851,6 @@ var HaExportAddon = class extends BaseAddon {
8092
7851
  {
8093
7852
  label: "Capabilities unclassified",
8094
7853
  value: this.unclassified.length === 0 ? "none" : this.unclassified.join(", ")
8095
- },
8096
- {
8097
- label: "Snooze options",
8098
- value: SNOOZE_OPTIONS.join(", ")
8099
7854
  }
8100
7855
  ],
8101
7856
  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."
@@ -8188,29 +7943,6 @@ function toCameraStatusInput(status) {
8188
7943
  recording: status.recording === null ? null : { active: status.recording.active }
8189
7944
  };
8190
7945
  }
8191
- /**
8192
- * The ACTIVE window for one scope, from a flat `listSnoozes` answer.
8193
- *
8194
- * "Active" is `expiresAt` in the future — `listSnoozes` never sweeps expired
8195
- * rows (expiry is a comparison, not a job), so a reader that skipped this
8196
- * would keep publishing a duration that ended hours ago. When several windows
8197
- * match, the one expiring LATEST wins: that is the duration still in effect.
8198
- */
8199
- function latestSnoozeWindow(snoozes, matches, now) {
8200
- const latest = snoozes.filter((s) => matches(s) && s.expiresAt > now).reduce((top, s) => top === null || s.expiresAt > top.expiresAt ? s : top, null);
8201
- return latest === null ? null : {
8202
- startedAt: latest.startedAt,
8203
- expiresAt: latest.expiresAt
8204
- };
8205
- }
8206
- /** The camera's own `scope: 'device'` snoozes (Task B1). */
8207
- function latestDeviceSnoozeWindow(snoozes, deviceId) {
8208
- return latestSnoozeWindow(snoozes, (s) => s.scope === "device" && s.deviceId === deviceId, Date.now());
8209
- }
8210
- /** The global `scope: 'all'` snooze (Task D4). */
8211
- function latestGlobalSnoozeWindow(snoozes) {
8212
- return latestSnoozeWindow(snoozes, (s) => s.scope === "all", Date.now());
8213
- }
8214
7946
  function toCountRecord(raw) {
8215
7947
  if (raw === null || typeof raw !== "object") return {};
8216
7948
  const out = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.80",
3
+ "version": "1.2.82",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",