@camstack/addon-provider-homeassistant 1.2.81 → 1.2.83

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-D5pBNTWh.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-DPdEnWGf.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,13 +33565,30 @@ 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`);
33564
33582
  await this.healSavedConfig(saved, savedDevices);
33565
- await this.ctx.kernel.devices.create(saved.stableId, Class, {}, saved.parentDeviceId);
33583
+ try {
33584
+ await this.ctx.kernel.devices.create(saved.stableId, Class, {}, saved.parentDeviceId);
33585
+ } catch (err) {
33586
+ if (!(await this.ctx.kernel.devices.getAll()).some((device) => device.stableId === saved.stableId)) throw err;
33587
+ this.ctx.logger.debug("Restore row came to life during its own dial — kept", { tags: {
33588
+ deviceId: saved.id,
33589
+ stableId: saved.stableId
33590
+ } });
33591
+ }
33566
33592
  restored.add(saved.id);
33567
33593
  };
33568
33594
  const topLevel = savedDevices.filter((saved) => saved.parentDeviceId === null);
@@ -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,13 +33565,30 @@ 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`);
33564
33582
  await this.healSavedConfig(saved, savedDevices);
33565
- await this.ctx.kernel.devices.create(saved.stableId, Class, {}, saved.parentDeviceId);
33583
+ try {
33584
+ await this.ctx.kernel.devices.create(saved.stableId, Class, {}, saved.parentDeviceId);
33585
+ } catch (err) {
33586
+ if (!(await this.ctx.kernel.devices.getAll()).some((device) => device.stableId === saved.stableId)) throw err;
33587
+ this.ctx.logger.debug("Restore row came to life during its own dial — kept", { tags: {
33588
+ deviceId: saved.id,
33589
+ stableId: saved.stableId
33590
+ } });
33591
+ }
33566
33592
  restored.add(saved.id);
33567
33593
  };
33568
33594
  const topLevel = savedDevices.filter((saved) => saved.parentDeviceId === null);
@@ -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-D5pBNTWh.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  //#region src/ha-export/topics.ts
9
9
  /**
@@ -4959,12 +4959,13 @@ function projectZoneOccupancy(deviceKey, input) {
4959
4959
  }
4960
4960
  function projectBatterySlice(deviceKey, slice) {
4961
4961
  const values = [{
4962
- topic: stateTopic(deviceKey, "charger"),
4963
- value: bool(slice.charging !== "none")
4964
- }, {
4965
4962
  topic: stateTopic(deviceKey, "sleeping"),
4966
4963
  value: bool(slice.sleeping)
4967
4964
  }];
4965
+ if (slice.charging !== "unknown") values.push({
4966
+ topic: stateTopic(deviceKey, "charger"),
4967
+ value: bool(slice.charging !== "none")
4968
+ });
4968
4969
  /**
4969
4970
  * `binary: true` means the device reports "low" or "not low" and the
4970
4971
  * percentage is a stand-in. Publishing it would put a fabricated
@@ -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-DPdEnWGf.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  //#region src/ha-export/topics.ts
4
4
  /**
@@ -4954,12 +4954,13 @@ function projectZoneOccupancy(deviceKey, input) {
4954
4954
  }
4955
4955
  function projectBatterySlice(deviceKey, slice) {
4956
4956
  const values = [{
4957
- topic: stateTopic(deviceKey, "charger"),
4958
- value: bool(slice.charging !== "none")
4959
- }, {
4960
4957
  topic: stateTopic(deviceKey, "sleeping"),
4961
4958
  value: bool(slice.sleeping)
4962
4959
  }];
4960
+ if (slice.charging !== "unknown") values.push({
4961
+ topic: stateTopic(deviceKey, "charger"),
4962
+ value: bool(slice.charging !== "none")
4963
+ });
4963
4964
  /**
4964
4965
  * `binary: true` means the device reports "low" or "not low" and the
4965
4966
  * percentage is a stand-in. Publishing it would put a fabricated
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.81",
3
+ "version": "1.2.83",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",