@camstack/addon-provider-homeassistant 1.2.602 → 1.2.603

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.
@@ -1,4 +1,4 @@
1
- import { $ as oauthIntegrationCapability, Bt as EventCategory, C as buildAddonRouteProvider, Et as nodePin, I as deviceExportCapability, It as string, a as COCO_TO_MACRO, bt as BaseAddon, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema } from "../dist-Dcum8auE.mjs";
1
+ import { $ as oauthIntegrationCapability, Bt as EventCategory, C as buildAddonRouteProvider, Et as nodePin, I as deviceExportCapability, It as string, a as COCO_TO_MACRO, bt as BaseAddon, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema } from "../dist-CM5Z-Rdj.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  import "node:zlib";
4
4
  import "node:fs";
@@ -4739,14 +4739,15 @@ var PushClient = class {
4739
4739
  * is flushed first — a value for an entity HA has not built yet is a
4740
4740
  * value it discards.
4741
4741
  */
4742
- async publishEntityChange(deviceKey, cmps, dev) {
4743
- if (this.disposed) return;
4742
+ async publishEntityChange(deviceKey, cmps, dev, full = false) {
4743
+ if (this.disposed) return false;
4744
4744
  await this.flush();
4745
- await this.send({
4745
+ return this.send({
4746
4746
  type: "entity_change",
4747
4747
  device_id: deviceKey,
4748
4748
  cmps,
4749
- dev
4749
+ dev,
4750
+ ...full ? { full: true } : {}
4750
4751
  });
4751
4752
  }
4752
4753
  /** Send everything buffered now. Called before any structure change. */
@@ -4763,12 +4764,15 @@ var PushClient = class {
4763
4764
  items
4764
4765
  });
4765
4766
  }
4767
+ /** `true` when Home Assistant took the message. */
4766
4768
  async send(message) {
4767
4769
  try {
4768
4770
  await this.transport(message);
4769
4771
  this.onReachable();
4772
+ return true;
4770
4773
  } catch (err) {
4771
4774
  this.onUnreachable(message, err);
4775
+ return false;
4772
4776
  }
4773
4777
  }
4774
4778
  onReachable() {
@@ -4882,6 +4886,130 @@ function shouldSkipAnnounce(reason, fingerprint, lastFingerprint) {
4882
4886
  return reason === "structure-changed" && fingerprint === lastFingerprint;
4883
4887
  }
4884
4888
  //#endregion
4889
+ //#region src/ha-export/held-reads.ts
4890
+ var ReadMirror = class {
4891
+ keyOf;
4892
+ entries = /* @__PURE__ */ new Map();
4893
+ constructor(keyOf) {
4894
+ this.keyOf = keyOf;
4895
+ }
4896
+ resolve(deviceId, outcome) {
4897
+ const entry = this.entries.get(deviceId);
4898
+ if (outcome.kind === "failed") {
4899
+ if (entry === void 0) return {
4900
+ status: "unknown",
4901
+ reason: outcome.reason
4902
+ };
4903
+ return {
4904
+ status: "held-failed",
4905
+ items: entry.confirmed,
4906
+ reason: outcome.reason
4907
+ };
4908
+ }
4909
+ if (entry === void 0) {
4910
+ this.entries.set(deviceId, {
4911
+ confirmed: outcome.items,
4912
+ pendingDrop: null
4913
+ });
4914
+ return {
4915
+ status: "fresh",
4916
+ items: outcome.items
4917
+ };
4918
+ }
4919
+ const incoming = new Set(outcome.items.map(this.keyOf));
4920
+ const dropped = entry.confirmed.map(this.keyOf).filter((key) => !incoming.has(key)).toSorted();
4921
+ if (dropped.length === 0) {
4922
+ entry.confirmed = outcome.items;
4923
+ entry.pendingDrop = null;
4924
+ return {
4925
+ status: "fresh",
4926
+ items: outcome.items
4927
+ };
4928
+ }
4929
+ const vote = dropped.join("\0");
4930
+ if (entry.pendingDrop === vote) {
4931
+ entry.confirmed = outcome.items;
4932
+ entry.pendingDrop = null;
4933
+ return {
4934
+ status: "fresh",
4935
+ items: outcome.items
4936
+ };
4937
+ }
4938
+ entry.pendingDrop = vote;
4939
+ return {
4940
+ status: "held-removal",
4941
+ items: entry.confirmed,
4942
+ reason: `an answered read dropped ${dropped.join(", ")}; waiting for a second answer to agree`
4943
+ };
4944
+ }
4945
+ };
4946
+ /**
4947
+ * The async sources `readCameraFunctions` could not obtain, or `null` when
4948
+ * every one answered. Their absence turns a function switch into "not
4949
+ * available", which removes its entity — a failure must not look like that.
4950
+ * `brokerAudioMuted` is only a read at all when the camera has a source node,
4951
+ * and `privacy` is a snapshot slice (absent = the cap is not there).
4952
+ */
4953
+ function cameraFunctionReadsFailed(reads, sourceNodeId) {
4954
+ const failed = [];
4955
+ if (reads.bindableCapNames === null) failed.push("bindableCapNames");
4956
+ if (reads.wrappedCapNames === null) failed.push("wrappedCapNames");
4957
+ if (reads.recordingEnabled === null) failed.push("recordingEnabled");
4958
+ if (reads.notificationsMuted === null) failed.push("notificationsMuted");
4959
+ if (sourceNodeId !== null && reads.brokerAudioMuted === null) failed.push("brokerAudioMuted");
4960
+ return failed.length === 0 ? null : `did not answer: ${failed.join(", ")}`;
4961
+ }
4962
+ //#endregion
4963
+ //#region src/ha-export/resync-gate.ts
4964
+ /**
4965
+ * When the exporter must send a FULL authoritative announce rather than let an
4966
+ * unchanged structure short-circuit.
4967
+ *
4968
+ * A gap is any moment after which the hub can no longer vouch for what Home
4969
+ * Assistant holds: the first pass after a start (the receiver may have been
4970
+ * running while this addon was down), a broker link that was just opened, an
4971
+ * announce or state push that was not delivered. The next pass after a gap is
4972
+ * a full resync, once, whatever its reason — an event-debounced pass included,
4973
+ * which would otherwise skip on an identical fingerprint and leave the
4974
+ * receiver unrepaired until the periodic pass.
4975
+ *
4976
+ * Passes for 'boot', 'link-restored' and 'periodic' are full by their nature.
4977
+ */
4978
+ /** Reasons whose pass is a full resync whether or not a gap was noted. */
4979
+ var ALWAYS_FULL = new Set([
4980
+ "boot",
4981
+ "link-restored",
4982
+ "periodic"
4983
+ ]);
4984
+ var ResyncGate = class {
4985
+ gap = true;
4986
+ lastReason = "start";
4987
+ /** Note that the receiver may be out of step; the next pass will be full. */
4988
+ markGap(reason) {
4989
+ this.gap = true;
4990
+ this.lastReason = reason;
4991
+ }
4992
+ /** `true` while a gap is waiting for a pass. */
4993
+ pending() {
4994
+ return this.gap;
4995
+ }
4996
+ /** Why the gap was opened (for the log line). */
4997
+ gapReason() {
4998
+ return this.gap ? this.lastReason : null;
4999
+ }
5000
+ /**
5001
+ * Called once at the start of a pass. Returns whether the pass is a full
5002
+ * resync and consumes the gap noted so far; a gap noted DURING the pass is
5003
+ * kept for the next one.
5004
+ */
5005
+ take(reason) {
5006
+ const full = this.gap || ALWAYS_FULL.has(reason);
5007
+ this.gap = false;
5008
+ this.lastReason = null;
5009
+ return full;
5010
+ }
5011
+ };
5012
+ //#endregion
4885
5013
  //#region src/ha-export/rule-triggers.ts
4886
5014
  function reduceOne(row) {
4887
5015
  return {
@@ -5936,9 +6064,9 @@ function projectSynthetic(input, nowMs) {
5936
6064
  topic: stateTopic(srv, `${slug}_memory`),
5937
6065
  value: String(Math.round(node.memoryPercent))
5938
6066
  });
5939
- if (node.online && node.uptime > 0) values.push({
6067
+ if (node.online && node.uptimeMs > 0) values.push({
5940
6068
  topic: stateTopic(srv, `${slug}_uptime`),
5941
- value: (/* @__PURE__ */ new Date(nowMs - node.uptime * 1e3)).toISOString()
6069
+ value: new Date(nowMs - node.uptimeMs).toISOString()
5942
6070
  });
5943
6071
  if (node.footageUsedBytes !== null) values.push({
5944
6072
  topic: stateTopic(srv, `${slug}_footage_used_bytes`),
@@ -6004,6 +6132,8 @@ var PUSH_PATH = "/api/camstack/push";
6004
6132
  var MEDIA_ROUTE_PREFIX = `/addon/${ADDON_ID}/ha-media`;
6005
6133
  /** How long `doorbell_pressed` stays on before its OFF edge is published. */
6006
6134
  var DOORBELL_PULSE_MS = 5e3;
6135
+ /** How soon a pass with a held-back read is retried. */
6136
+ var READ_RETRY_MS = 3e4;
6007
6137
  /**
6008
6138
  * The earliest timestamp a real change could plausibly carry. Below this,
6009
6139
  * a slice field is a placeholder or a seeded "known value, unknown change
@@ -6050,6 +6180,24 @@ var HaExportAddon = class extends BaseAddon {
6050
6180
  * still holds through the periodic pass.
6051
6181
  */
6052
6182
  lastStructureFingerprint;
6183
+ /**
6184
+ * When the next pass must be a FULL authoritative announce (`full: true` on
6185
+ * the wire) whatever the fingerprint says: a start, a new broker link, an
6186
+ * undelivered push, a failed pass. See `resync-gate.ts`.
6187
+ */
6188
+ resync = new ResyncGate();
6189
+ /**
6190
+ * The last EXPORTED set of each part of a device's component set. A read
6191
+ * that failed (or a provider not registered yet) holds it; a shrink needs
6192
+ * two agreeing answers. See `held-reads.ts`.
6193
+ */
6194
+ boundCapsMirror = new ReadMirror((cap) => cap);
6195
+ zonesMirror = new ReadMirror((zone) => zone.id);
6196
+ streamsMirror = new ReadMirror((stream) => stream.id);
6197
+ presetsMirror = new ReadMirror((preset) => preset);
6198
+ switchesMirror = new ReadMirror((fn) => fn.id);
6199
+ /** A held-back read is retried soon, not at the next 300 s pass. */
6200
+ readTimer = null;
6053
6201
  entityCount = 0;
6054
6202
  /** deviceId → the `lastPressedAt` already rung, so a re-read does not ring again. */
6055
6203
  lastDoorbellPressAt = /* @__PURE__ */ new Map();
@@ -6126,6 +6274,8 @@ var HaExportAddon = class extends BaseAddon {
6126
6274
  for (const timer of this.doorbellReleaseTimers.values()) clearTimeout(timer);
6127
6275
  this.doorbellReleaseTimers.clear();
6128
6276
  this.reachability.dispose();
6277
+ if (this.readTimer !== null) clearTimeout(this.readTimer);
6278
+ this.readTimer = null;
6129
6279
  for (const link of this.links.values()) await link.client.dispose();
6130
6280
  this.links.clear();
6131
6281
  });
@@ -6598,6 +6748,8 @@ var HaExportAddon = class extends BaseAddon {
6598
6748
  const startedAt = Date.now();
6599
6749
  try {
6600
6750
  await this.refreshBrokers();
6751
+ const fullResync = this.resync.take(reason);
6752
+ let readsHeld = false;
6601
6753
  await this.refreshMediaBaseUrl();
6602
6754
  await this.refreshComponentSupport();
6603
6755
  const enabled = this.enabledBrokerIds();
@@ -6660,16 +6812,22 @@ var HaExportAddon = class extends BaseAddon {
6660
6812
  * used to call the deprecated `getCameraSwitches` separately for the
6661
6813
  * same answer.
6662
6814
  */
6663
- const cameraFunctions = device.type === "camera" ? deriveCameraFunctions(await this.readCameraFunctions({
6815
+ const sourceNodeId = this.cameraStatusByDeviceId.get(numericId)?.assignment.sourceNodeId ?? null;
6816
+ const functionReads = device.type === "camera" ? await this.readCameraFunctions({
6664
6817
  deviceId: numericId,
6665
6818
  deviceType: device.type,
6666
6819
  deviceDisabled: device.disabled,
6667
6820
  privacySlice: toSnapshot(snapshots[String(numericId)])["privacy-mask"],
6668
- sourceNodeId: this.cameraStatusByDeviceId.get(numericId)?.assignment.sourceNodeId ?? null,
6821
+ sourceNodeId,
6669
6822
  bindableCapsByType,
6670
6823
  mutedDeviceIds
6671
- })) : null;
6672
- const catalogDevice = await this.buildCatalogDevice(device, snapshots[String(numericId)], cameraFunctions);
6824
+ }) : null;
6825
+ const cameraFunctions = functionReads !== null ? deriveCameraFunctions(functionReads) : null;
6826
+ const functionsFailure = functionReads !== null ? cameraFunctionReadsFailed(functionReads, sourceNodeId) : null;
6827
+ const built = await this.buildCatalogDevice(device, snapshots[String(numericId)], cameraFunctions, functionsFailure);
6828
+ if (built.heldBack) readsHeld = true;
6829
+ const catalogDevice = built.device;
6830
+ if (catalogDevice === null) continue;
6673
6831
  for (const cap of catalogDevice.boundCaps) allCaps.add(cap);
6674
6832
  const brokerIds = brokersExposing(this.config.membership, deviceIdStr).filter((id) => enabled.includes(id));
6675
6833
  /**
@@ -6740,7 +6898,8 @@ var HaExportAddon = class extends BaseAddon {
6740
6898
  this.entityCount = entityCount;
6741
6899
  this.unclassified = unclassifiedCaps([...allCaps]);
6742
6900
  const fingerprint = structureFingerprint(exported);
6743
- if (shouldSkipAnnounce(reason, fingerprint, this.lastStructureFingerprint)) {
6901
+ if (readsHeld) this.scheduleReadRetry();
6902
+ if (!fullResync && shouldSkipAnnounce(reason, fingerprint, this.lastStructureFingerprint)) {
6744
6903
  this.ctx.logger.debug("ha-export: structure unchanged, skipping announce", { meta: {
6745
6904
  devices: exported.size,
6746
6905
  entities: entityCount
@@ -6758,11 +6917,17 @@ var HaExportAddon = class extends BaseAddon {
6758
6917
  entities: line.entities
6759
6918
  }
6760
6919
  });
6761
- await this.announce(exported);
6920
+ const delivered = await this.announce(exported, fullResync);
6762
6921
  await this.pushFullState(exported, snapshots);
6763
6922
  if (synthetic !== null) await this.pushSynthetic(projectSynthetic(synthetic, Date.now()), [...enabled]);
6923
+ if (!delivered || [...this.links.values()].some((link) => !link.client.healthy)) {
6924
+ this.resync.markGap("push-not-delivered");
6925
+ this.lastStructureFingerprint = void 0;
6926
+ this.ctx.logger.warn("ha-export: a push was not delivered, the next pass is a full resync");
6927
+ }
6764
6928
  this.ctx.logger.info("ha-export: reconciled", { meta: {
6765
6929
  reason,
6930
+ fullResync,
6766
6931
  brokers: enabled.length,
6767
6932
  devices: exported.size,
6768
6933
  entities: entityCount,
@@ -6773,6 +6938,7 @@ var HaExportAddon = class extends BaseAddon {
6773
6938
  this.lastError = void 0;
6774
6939
  } catch (err) {
6775
6940
  this.lastError = errMsg(err);
6941
+ this.resync.markGap("reconcile-failed");
6776
6942
  this.ctx.logger.warn("ha-export: reconcile failed", { meta: { error: this.lastError } });
6777
6943
  }
6778
6944
  }
@@ -6971,7 +7137,7 @@ var HaExportAddon = class extends BaseAddon {
6971
7137
  online: node.isOnline,
6972
7138
  cpuPercent: node.cpuPercent,
6973
7139
  memoryPercent: node.memoryPercent,
6974
- uptime: node.uptime,
7140
+ uptimeMs: node.uptime,
6975
7141
  isHub: node.isHub,
6976
7142
  footageUsedBytes: footage.footageUsedBytes,
6977
7143
  footageFreePercent: footage.footageFreePercent,
@@ -6990,13 +7156,16 @@ var HaExportAddon = class extends BaseAddon {
6990
7156
  for (const value of values) link.client.publishState(value.topic, value.value);
6991
7157
  }
6992
7158
  }
6993
- async announce(exported) {
7159
+ /** `false` when any announce was not delivered. */
7160
+ async announce(exported, full) {
7161
+ let allDelivered = true;
6994
7162
  for (const entry of exported.values()) for (const brokerId of entry.brokerIds) {
6995
7163
  const link = this.links.get(brokerId);
6996
7164
  if (link === void 0) continue;
6997
7165
  try {
6998
- await link.client.publishEntityChange(entry.plan.deviceKey, entry.plan.cmps, entry.plan.dev);
7166
+ if (!await link.client.publishEntityChange(entry.plan.deviceKey, entry.plan.cmps, entry.plan.dev, full)) allDelivered = false;
6999
7167
  } catch (err) {
7168
+ allDelivered = false;
7000
7169
  this.ctx.logger.warn("ha-export: could not announce a device, skipping it", {
7001
7170
  tags: { deviceId: entry.deviceId },
7002
7171
  meta: {
@@ -7006,6 +7175,7 @@ var HaExportAddon = class extends BaseAddon {
7006
7175
  });
7007
7176
  }
7008
7177
  }
7178
+ return allDelivered;
7009
7179
  }
7010
7180
  async pushFullState(exported, snapshots) {
7011
7181
  for (const entry of exported.values()) try {
@@ -7123,46 +7293,105 @@ var HaExportAddon = class extends BaseAddon {
7123
7293
  }
7124
7294
  return values;
7125
7295
  }
7126
- async buildCatalogDevice(device, snapshotRaw, cameraFunctions) {
7296
+ /**
7297
+ * `device: null` = a part of this device's component set cannot be read and
7298
+ * nothing was ever exported for it, so the device is not announced this pass
7299
+ * (an empty part would make Home Assistant delete what it holds). `heldBack`
7300
+ * = at least one part is not a fresh answer, so a retry is worth arming.
7301
+ */
7302
+ async buildCatalogDevice(device, snapshotRaw, cameraFunctions, functionsFailure) {
7127
7303
  const snapshot = toSnapshot(snapshotRaw);
7128
- const boundCaps = await this.loadBoundCaps(device.id);
7129
- const zones = device.type === "camera" ? await this.loadZones(device.id) : [];
7304
+ let heldBack = false;
7305
+ let unknown = false;
7306
+ const settle = (part, mirror, outcome) => {
7307
+ const resolution = mirror.resolve(device.id, outcome);
7308
+ if (resolution.status !== "fresh") heldBack = true;
7309
+ if (resolution.status === "unknown") unknown = true;
7310
+ this.logHeldRead(device.id, part, resolution);
7311
+ return resolution.status === "unknown" ? [] : resolution.items;
7312
+ };
7313
+ const boundCaps = settle("boundCaps", this.boundCapsMirror, await this.loadBoundCaps(device.id));
7314
+ const zones = device.type === "camera" ? settle("zones", this.zonesMirror, await this.loadZones(device.id)) : [];
7130
7315
  /**
7131
7316
  * Every camera function, available or not — `entity-catalog.ts` is the
7132
7317
  * one place that filters to `available`, exactly as it did for
7133
- * `getCameraSwitches`'s answer.
7318
+ * `getCameraSwitches`'s answer. A function that did not answer is
7319
+ * "unavailable", which removes its entity, so the set goes through the
7320
+ * mirror: held when a source failed, shrunk only on two agreeing passes.
7134
7321
  */
7135
- const switches = (cameraFunctions ?? []).map((fn) => ({
7322
+ const mappedSwitches = (cameraFunctions ?? []).map((fn) => ({
7136
7323
  id: fn.id,
7137
7324
  label: fn.label,
7138
7325
  available: fn.available
7139
7326
  }));
7140
- const ptzPresets = device.type === "camera" && boundCaps.includes("ptz") ? await this.loadPresets(device.id) : [];
7141
- const streams = device.type === "camera" && boundCaps.includes("webrtc-session") ? await this.loadStreamChoices(device.id) : [];
7327
+ const switches = cameraFunctions === null ? mappedSwitches : settle("cameraFunctions", this.switchesMirror, functionsFailure !== null ? {
7328
+ kind: "failed",
7329
+ reason: functionsFailure
7330
+ } : {
7331
+ kind: "answered",
7332
+ items: mappedSwitches.filter((fn) => fn.available)
7333
+ });
7334
+ const ptzPresets = device.type === "camera" ? settle("ptzPresets", this.presetsMirror, boundCaps.includes("ptz") ? await this.loadPresets(device.id) : {
7335
+ kind: "answered",
7336
+ items: []
7337
+ }) : [];
7338
+ const streams = device.type === "camera" ? settle("streams", this.streamsMirror, boundCaps.includes("webrtc-session") ? await this.loadStreamChoices(device.id) : {
7339
+ kind: "answered",
7340
+ items: []
7341
+ }) : [];
7342
+ if (unknown) return {
7343
+ device: null,
7344
+ heldBack
7345
+ };
7142
7346
  const manufacturer = readString(device.metadata ?? {}, "manufacturer");
7143
7347
  const model = readString(device.metadata ?? {}, "model");
7144
7348
  return {
7145
- id: device.id,
7146
- name: device.name,
7147
- type: device.type,
7148
- ...manufacturer !== null ? { manufacturer } : {},
7149
- ...model !== null ? { model } : {},
7150
- features: device.features,
7151
- boundCaps,
7152
- zones,
7153
- switches,
7154
- ptzPresets,
7155
- streams,
7156
- slices: Object.keys(snapshot),
7157
- /**
7158
- * The slice VALUES, for the native platforms only: a native
7159
- * `climate` announces the modes the device accepts, and
7160
- * `availableModes` is on the slice and nowhere else. See
7161
- * `CatalogDevice.capSlices`.
7162
- */
7163
- capSlices: snapshot
7349
+ device: {
7350
+ id: device.id,
7351
+ name: device.name,
7352
+ type: device.type,
7353
+ ...manufacturer !== null ? { manufacturer } : {},
7354
+ ...model !== null ? { model } : {},
7355
+ features: device.features,
7356
+ boundCaps,
7357
+ zones,
7358
+ switches,
7359
+ ptzPresets,
7360
+ streams,
7361
+ slices: Object.keys(snapshot),
7362
+ /**
7363
+ * The slice VALUES, for the native platforms only: a native
7364
+ * `climate` announces the modes the device accepts, and
7365
+ * `availableModes` is on the slice and nowhere else. See
7366
+ * `CatalogDevice.capSlices`.
7367
+ */
7368
+ capSlices: snapshot
7369
+ },
7370
+ heldBack
7164
7371
  };
7165
7372
  }
7373
+ /** One WARN per held part, with the device tag and the reason. */
7374
+ logHeldRead(deviceId, part, resolution) {
7375
+ if (resolution.status === "fresh") return;
7376
+ const kept = resolution.status === "unknown" ? 0 : resolution.items.length;
7377
+ const message = resolution.status === "unknown" ? "ha-export: a read did not answer and nothing was exported yet, the device is not announced this pass" : resolution.status === "held-failed" ? "ha-export: a read did not answer, keeping the last exported components" : "ha-export: a read dropped components, keeping them until a second answer agrees";
7378
+ this.ctx.logger.warn(message, {
7379
+ tags: { deviceId },
7380
+ meta: {
7381
+ part,
7382
+ reason: resolution.reason,
7383
+ kept
7384
+ }
7385
+ });
7386
+ }
7387
+ /** Retry soon after a held-back read: the provider usually registers seconds later. */
7388
+ scheduleReadRetry() {
7389
+ if (this.readTimer !== null) return;
7390
+ this.readTimer = setTimeout(() => {
7391
+ this.readTimer = null;
7392
+ this.reconcile("read-retry");
7393
+ }, READ_RETRY_MS);
7394
+ }
7166
7395
  /**
7167
7396
  * Every stream of the camera, as the hub's own picker lists them.
7168
7397
  *
@@ -7180,45 +7409,54 @@ var HaExportAddon = class extends BaseAddon {
7180
7409
  */
7181
7410
  async loadStreamChoices(deviceId) {
7182
7411
  try {
7183
- return (await this.ctx.api.webrtcSession.listStreams.query({ deviceId })).map((choice) => ({
7184
- id: choice.id,
7185
- label: choice.label,
7186
- target: choice.target,
7187
- ...choice.resolution === null ? {} : { height: choice.resolution.height }
7188
- }));
7412
+ return {
7413
+ kind: "answered",
7414
+ items: (await this.ctx.api.webrtcSession.listStreams.query({ deviceId })).map((choice) => ({
7415
+ id: choice.id,
7416
+ label: choice.label,
7417
+ target: choice.target,
7418
+ ...choice.resolution === null ? {} : { height: choice.resolution.height }
7419
+ }))
7420
+ };
7189
7421
  } catch (err) {
7190
- this.ctx.logger.warn("ha-export: could not read the camera streams, no camera entity", {
7191
- tags: { deviceId },
7192
- meta: { error: errMsg(err) }
7193
- });
7194
- return [];
7422
+ return {
7423
+ kind: "failed",
7424
+ reason: errMsg(err)
7425
+ };
7195
7426
  }
7196
7427
  }
7197
7428
  async loadBoundCaps(deviceId) {
7198
7429
  try {
7199
- return (await this.ctx.api.deviceManager.getBindings.query({ deviceId })).entries.map((entry) => entry.capName);
7430
+ return {
7431
+ kind: "answered",
7432
+ items: (await this.ctx.api.deviceManager.getBindings.query({ deviceId })).entries.map((entry) => entry.capName)
7433
+ };
7200
7434
  } catch (err) {
7201
- this.ctx.logger.warn("ha-export: could not read bindings, exporting without them", {
7202
- tags: { deviceId },
7203
- meta: { error: errMsg(err) }
7204
- });
7205
- return [];
7435
+ return {
7436
+ kind: "failed",
7437
+ reason: errMsg(err)
7438
+ };
7206
7439
  }
7207
7440
  }
7208
7441
  async loadZones(deviceId) {
7209
7442
  try {
7210
7443
  const snapshot = await this.ctx.api.zoneAnalytics.getCurrentSnapshot.query({ deviceId });
7211
- if (snapshot === null) return [];
7212
- return snapshot.zones.map((zone) => ({
7213
- id: zone.zoneId,
7214
- name: zone.zoneName
7215
- }));
7444
+ if (snapshot === null) return {
7445
+ kind: "answered",
7446
+ items: []
7447
+ };
7448
+ return {
7449
+ kind: "answered",
7450
+ items: snapshot.zones.map((zone) => ({
7451
+ id: zone.zoneId,
7452
+ name: zone.zoneName
7453
+ }))
7454
+ };
7216
7455
  } catch (err) {
7217
- this.ctx.logger.warn("ha-export: could not read zones, exporting no zone entities", {
7218
- tags: { deviceId },
7219
- meta: { error: errMsg(err) }
7220
- });
7221
- return [];
7456
+ return {
7457
+ kind: "failed",
7458
+ reason: errMsg(err)
7459
+ };
7222
7460
  }
7223
7461
  }
7224
7462
  /**
@@ -7337,14 +7575,19 @@ var HaExportAddon = class extends BaseAddon {
7337
7575
  async loadPresets(deviceId) {
7338
7576
  try {
7339
7577
  const device = await this.ctx.fetchDevice(deviceId);
7340
- if (device.ptz === void 0) return [];
7341
- return (await device.ptz.getPresets({})).map((preset) => preset.name);
7578
+ if (device.ptz === void 0) return {
7579
+ kind: "answered",
7580
+ items: []
7581
+ };
7582
+ return {
7583
+ kind: "answered",
7584
+ items: (await device.ptz.getPresets({})).map((preset) => preset.name)
7585
+ };
7342
7586
  } catch (err) {
7343
- this.ctx.logger.debug("ha-export: could not probe PTZ presets", {
7344
- tags: { deviceId },
7345
- meta: { error: errMsg(err) }
7346
- });
7347
- return [];
7587
+ return {
7588
+ kind: "failed",
7589
+ reason: errMsg(err)
7590
+ };
7348
7591
  }
7349
7592
  }
7350
7593
  /**
@@ -7423,6 +7666,7 @@ var HaExportAddon = class extends BaseAddon {
7423
7666
  onLinkRestored: (brokerId) => this.reachability.request(brokerId)
7424
7667
  });
7425
7668
  client.start();
7669
+ this.resync.markGap("broker-connected");
7426
7670
  this.links.set(broker.id, {
7427
7671
  brokerId: broker.id,
7428
7672
  name: broker.name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.602",
3
+ "version": "1.2.603",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",