@camstack/addon-provider-homeassistant 1.2.160 → 1.2.162

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,5 +1,216 @@
1
- import { $ as oauthIntegrationCapability, C as buildAddonRouteProvider, Ft as EventCategory, I as deviceExportCapability, St as nodePin, a as COCO_TO_MACRO, bt as BaseAddon, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, jt as string, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema } from "../dist-H458ct4y.mjs";
1
+ import { $ as oauthIntegrationCapability, C as buildAddonRouteProvider, Ft as EventCategory, I as deviceExportCapability, St as nodePin, a as COCO_TO_MACRO, bt as BaseAddon, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, jt as string, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema } from "../dist-Cw91Z1Ew.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
+ /** The `BrokerInfo.kind` tag the Home Assistant provider stamps. */
4
+ var HA_BROKER_KIND = "home-assistant";
5
+ /**
6
+ * Every Home Assistant broker currently registered, cluster-wide.
7
+ *
8
+ * Unpinned by design (see the module docblock) and filtered by `kind`, which is
9
+ * the contract check that matters: whatever mix of providers answered the
10
+ * union, only `home-assistant` brokers are ever exported to over Home
11
+ * Assistant's transport.
12
+ */
13
+ async function listHomeAssistantBrokers(query) {
14
+ return (await query({})).filter((broker) => broker.kind === HA_BROKER_KIND);
15
+ }
16
+ //#endregion
17
+ //#region src/ha-export/camera-function-writes.ts
18
+ /**
19
+ * Performs the WRITE of a camera function on the authority that owns it.
20
+ *
21
+ * `deriveCameraFunctions`/`resolveCameraFunctionWrite` (`camera-functions.ts`)
22
+ * decide; this module acts. It is the file that
23
+ * `scripts/check-switch-successor-surfaces.ts` points to as the successor of the
24
+ * deprecated pair `getCameraSwitches` / `setCameraSwitch` (D113 phase 4).
25
+ */
26
+ async function applyCameraFunctionWrite(api, deviceId, write, sourceNodeId) {
27
+ switch (write.kind) {
28
+ case "device-disabled":
29
+ await api.deviceManager.setDisabled.mutate({
30
+ deviceId,
31
+ disabled: write.disabled
32
+ });
33
+ return;
34
+ case "wrapper-binding":
35
+ await api.deviceManager.setWrapperActive.mutate({
36
+ deviceId,
37
+ capName: write.capName,
38
+ wrapperAddonId: write.wrapperAddonId,
39
+ active: write.active
40
+ });
41
+ return;
42
+ case "recording-enabled": {
43
+ const config = await api.recordingArchive.getDeviceConfig.query({ deviceId });
44
+ await api.recordingArchive.setDeviceConfig.mutate({
45
+ deviceId,
46
+ config: {
47
+ ...config,
48
+ enabled: write.enabled
49
+ }
50
+ });
51
+ return;
52
+ }
53
+ case "notification-mute":
54
+ await api.notificationRules.setDeviceMuted.mutate({
55
+ deviceId,
56
+ muted: write.muted
57
+ });
58
+ return;
59
+ case "camera-mask":
60
+ await api.privacyMask.setMask.mutate({
61
+ deviceId,
62
+ patch: { enabled: write.enabled }
63
+ });
64
+ return;
65
+ case "camera-audio":
66
+ await api.privacyMask.setAudioEnabled.mutate({
67
+ deviceId,
68
+ enabled: write.enabled
69
+ });
70
+ return;
71
+ case "broker-audio-mute":
72
+ if (sourceNodeId === null) throw new Error(`refusing an unrouted broker audio write for device ${String(deviceId)}: no source node`);
73
+ await api.streamBroker.setDeviceAudioMute.mutate({
74
+ deviceId,
75
+ muted: write.muted
76
+ }, nodePin(sourceNodeId));
77
+ return;
78
+ }
79
+ }
80
+ //#endregion
81
+ //#region src/ha-export/camera-functions.ts
82
+ var DETECTION_CAP = "detection-pipeline";
83
+ var AUDIO_ANALYSIS_CAP = "audio-analysis";
84
+ /** The wrapper that WRITES the history: tracks, events and their media. */
85
+ var PIPELINE_ANALYTICS_CAP = "pipeline-analytics";
86
+ /** Render order: widest reach first, source before consumer. */
87
+ var ORDER = [
88
+ "stream-broker",
89
+ "object-detection",
90
+ "privacy-mask",
91
+ "device-audio",
92
+ "broker-audio",
93
+ "audio-analysis",
94
+ "event-saving",
95
+ "recording",
96
+ "notifications"
97
+ ];
98
+ var LABELS = {
99
+ "stream-broker": "Camera",
100
+ "object-detection": "Object detection & tracking",
101
+ "privacy-mask": "Privacy mask",
102
+ "device-audio": "Camera microphone",
103
+ "broker-audio": "Audio distribution",
104
+ "audio-analysis": "Audio detection & classification",
105
+ "event-saving": "Event saving",
106
+ recording: "Recording",
107
+ notifications: "Notifications"
108
+ };
109
+ /**
110
+ * Which cap stands behind each wrapper-backed switch. A map instead of a
111
+ * ternary: with three entries the ternary became a chain, and the next entry
112
+ * would have silently landed on the wrong branch.
113
+ */
114
+ var WRAPPER_CAP_BY_ID = {
115
+ "object-detection": DETECTION_CAP,
116
+ "audio-analysis": AUDIO_ANALYSIS_CAP,
117
+ "event-saving": PIPELINE_ANALYTICS_CAP
118
+ };
119
+ var UNAVAILABLE = {
120
+ available: false,
121
+ enabled: true
122
+ };
123
+ function resolveWrapper(capName, reads) {
124
+ if (reads.bindableCapNames === null || reads.wrappedCapNames === null) return UNAVAILABLE;
125
+ if (!reads.bindableCapNames.includes(capName)) return UNAVAILABLE;
126
+ if (reads.wrapperAddonIdByCap.get(capName) === void 0) return UNAVAILABLE;
127
+ return {
128
+ available: true,
129
+ enabled: reads.wrappedCapNames.includes(capName)
130
+ };
131
+ }
132
+ function resolveOne(id, reads) {
133
+ switch (id) {
134
+ case "stream-broker": return reads.deviceDisabled === null ? UNAVAILABLE : {
135
+ available: true,
136
+ enabled: !reads.deviceDisabled
137
+ };
138
+ case "object-detection": return resolveWrapper(DETECTION_CAP, reads);
139
+ case "audio-analysis": return resolveWrapper(AUDIO_ANALYSIS_CAP, reads);
140
+ case "event-saving": return resolveWrapper(PIPELINE_ANALYTICS_CAP, reads);
141
+ case "recording": return reads.recordingEnabled === null ? UNAVAILABLE : {
142
+ available: true,
143
+ enabled: reads.recordingEnabled
144
+ };
145
+ case "notifications": return reads.notificationsMuted === null ? UNAVAILABLE : {
146
+ available: true,
147
+ enabled: !reads.notificationsMuted
148
+ };
149
+ case "privacy-mask": return reads.privacy === null ? UNAVAILABLE : {
150
+ available: true,
151
+ enabled: reads.privacy.maskEnabled
152
+ };
153
+ case "device-audio": return reads.privacy === null || reads.privacy.audioEnabled === null ? UNAVAILABLE : {
154
+ available: true,
155
+ enabled: reads.privacy.audioEnabled
156
+ };
157
+ case "broker-audio": return reads.brokerAudioMuted === null ? UNAVAILABLE : {
158
+ available: true,
159
+ enabled: !reads.brokerAudioMuted
160
+ };
161
+ }
162
+ }
163
+ function deriveCameraFunctions(reads) {
164
+ return ORDER.map((id) => ({
165
+ id,
166
+ label: LABELS[id],
167
+ ...resolveOne(id, reads)
168
+ }));
169
+ }
170
+ /** `null` = not available: the write must be REFUSED, not approximated. */
171
+ function resolveCameraFunctionWrite(id, on, reads) {
172
+ if (!resolveOne(id, reads).available) return null;
173
+ switch (id) {
174
+ case "stream-broker": return {
175
+ kind: "device-disabled",
176
+ disabled: !on
177
+ };
178
+ case "object-detection":
179
+ case "audio-analysis":
180
+ case "event-saving": {
181
+ const capName = WRAPPER_CAP_BY_ID[id];
182
+ const wrapperAddonId = reads.wrapperAddonIdByCap.get(capName);
183
+ if (wrapperAddonId === void 0) return null;
184
+ return {
185
+ kind: "wrapper-binding",
186
+ capName,
187
+ wrapperAddonId,
188
+ active: on
189
+ };
190
+ }
191
+ case "recording": return {
192
+ kind: "recording-enabled",
193
+ enabled: on
194
+ };
195
+ case "notifications": return {
196
+ kind: "notification-mute",
197
+ muted: !on
198
+ };
199
+ case "privacy-mask": return {
200
+ kind: "camera-mask",
201
+ enabled: on
202
+ };
203
+ case "device-audio": return {
204
+ kind: "camera-audio",
205
+ enabled: on
206
+ };
207
+ case "broker-audio": return {
208
+ kind: "broker-audio-mute",
209
+ muted: !on
210
+ };
211
+ }
212
+ }
213
+ //#endregion
3
214
  //#region src/ha-export/topics.ts
4
215
  /**
5
216
  * The topic grammar. Every topic in the export is built HERE.
@@ -4120,223 +4331,26 @@ async function call(slice, capName, make) {
4120
4331
  }
4121
4332
  return { ok: true };
4122
4333
  }
4123
- //#endregion
4124
- //#region src/ha-export/camera-function-writes.ts
4125
4334
  /**
4126
- * Performs the WRITE of a camera function on the authority that owns it.
4335
+ * The ONE derivation of "a signed, expiring URL".
4127
4336
  *
4128
- * `deriveCameraFunctions`/`resolveCameraFunctionWrite` (`camera-functions.ts`)
4129
- * decide; this module acts. It is the file that
4130
- * `scripts/check-switch-successor-surfaces.ts` points to as the successor of the
4131
- * deprecated pair `getCameraSwitches` / `setCameraSwitch` (D113 phase 4).
4132
- */
4133
- async function applyCameraFunctionWrite(api, deviceId, write, sourceNodeId) {
4134
- switch (write.kind) {
4135
- case "device-disabled":
4136
- await api.deviceManager.setDisabled.mutate({
4137
- deviceId,
4138
- disabled: write.disabled
4139
- });
4140
- return;
4141
- case "wrapper-binding":
4142
- await api.deviceManager.setWrapperActive.mutate({
4143
- deviceId,
4144
- capName: write.capName,
4145
- wrapperAddonId: write.wrapperAddonId,
4146
- active: write.active
4147
- });
4148
- return;
4149
- case "recording-enabled": {
4150
- const config = await api.recordingArchive.getDeviceConfig.query({ deviceId });
4151
- await api.recordingArchive.setDeviceConfig.mutate({
4152
- deviceId,
4153
- config: {
4154
- ...config,
4155
- enabled: write.enabled
4156
- }
4157
- });
4158
- return;
4159
- }
4160
- case "notification-mute":
4161
- await api.notificationRules.setDeviceMuted.mutate({
4162
- deviceId,
4163
- muted: write.muted
4164
- });
4165
- return;
4166
- case "camera-mask":
4167
- await api.privacyMask.setMask.mutate({
4168
- deviceId,
4169
- patch: { enabled: write.enabled }
4170
- });
4171
- return;
4172
- case "camera-audio":
4173
- await api.privacyMask.setAudioEnabled.mutate({
4174
- deviceId,
4175
- enabled: write.enabled
4176
- });
4177
- return;
4178
- case "broker-audio-mute":
4179
- if (sourceNodeId === null) throw new Error(`refusing an unrouted broker audio write for device ${String(deviceId)}: no source node`);
4180
- await api.streamBroker.setDeviceAudioMute.mutate({
4181
- deviceId,
4182
- muted: write.muted
4183
- }, nodePin(sourceNodeId));
4184
- return;
4185
- }
4186
- }
4187
- //#endregion
4188
- //#region src/ha-export/camera-functions.ts
4189
- var DETECTION_CAP = "detection-pipeline";
4190
- var AUDIO_ANALYSIS_CAP = "audio-analysis";
4191
- /** The wrapper that WRITES the history: tracks, events and their media. */
4192
- var PIPELINE_ANALYTICS_CAP = "pipeline-analytics";
4193
- /** Render order: widest reach first, source before consumer. */
4194
- var ORDER = [
4195
- "stream-broker",
4196
- "object-detection",
4197
- "privacy-mask",
4198
- "device-audio",
4199
- "broker-audio",
4200
- "audio-analysis",
4201
- "event-saving",
4202
- "recording",
4203
- "notifications"
4204
- ];
4205
- var LABELS = {
4206
- "stream-broker": "Camera",
4207
- "object-detection": "Object detection & tracking",
4208
- "privacy-mask": "Privacy mask",
4209
- "device-audio": "Camera microphone",
4210
- "broker-audio": "Audio distribution",
4211
- "audio-analysis": "Audio detection & classification",
4212
- "event-saving": "Event saving",
4213
- recording: "Recording",
4214
- notifications: "Notifications"
4215
- };
4216
- /**
4217
- * Which cap stands behind each wrapper-backed switch. A map instead of a
4218
- * ternary: with three entries the ternary became a chain, and the next entry
4219
- * would have silently landed on the wrong branch.
4220
- */
4221
- var WRAPPER_CAP_BY_ID = {
4222
- "object-detection": DETECTION_CAP,
4223
- "audio-analysis": AUDIO_ANALYSIS_CAP,
4224
- "event-saving": PIPELINE_ANALYTICS_CAP
4225
- };
4226
- var UNAVAILABLE = {
4227
- available: false,
4228
- enabled: true
4229
- };
4230
- function resolveWrapper(capName, reads) {
4231
- if (reads.bindableCapNames === null || reads.wrappedCapNames === null) return UNAVAILABLE;
4232
- if (!reads.bindableCapNames.includes(capName)) return UNAVAILABLE;
4233
- if (reads.wrapperAddonIdByCap.get(capName) === void 0) return UNAVAILABLE;
4234
- return {
4235
- available: true,
4236
- enabled: reads.wrappedCapNames.includes(capName)
4237
- };
4238
- }
4239
- function resolveOne(id, reads) {
4240
- switch (id) {
4241
- case "stream-broker": return reads.deviceDisabled === null ? UNAVAILABLE : {
4242
- available: true,
4243
- enabled: !reads.deviceDisabled
4244
- };
4245
- case "object-detection": return resolveWrapper(DETECTION_CAP, reads);
4246
- case "audio-analysis": return resolveWrapper(AUDIO_ANALYSIS_CAP, reads);
4247
- case "event-saving": return resolveWrapper(PIPELINE_ANALYTICS_CAP, reads);
4248
- case "recording": return reads.recordingEnabled === null ? UNAVAILABLE : {
4249
- available: true,
4250
- enabled: reads.recordingEnabled
4251
- };
4252
- case "notifications": return reads.notificationsMuted === null ? UNAVAILABLE : {
4253
- available: true,
4254
- enabled: !reads.notificationsMuted
4255
- };
4256
- case "privacy-mask": return reads.privacy === null ? UNAVAILABLE : {
4257
- available: true,
4258
- enabled: reads.privacy.maskEnabled
4259
- };
4260
- case "device-audio": return reads.privacy === null || reads.privacy.audioEnabled === null ? UNAVAILABLE : {
4261
- available: true,
4262
- enabled: reads.privacy.audioEnabled
4263
- };
4264
- case "broker-audio": return reads.brokerAudioMuted === null ? UNAVAILABLE : {
4265
- available: true,
4266
- enabled: !reads.brokerAudioMuted
4267
- };
4268
- }
4269
- }
4270
- function deriveCameraFunctions(reads) {
4271
- return ORDER.map((id) => ({
4272
- id,
4273
- label: LABELS[id],
4274
- ...resolveOne(id, reads)
4275
- }));
4276
- }
4277
- /** `null` = not available: the write must be REFUSED, not approximated. */
4278
- function resolveCameraFunctionWrite(id, on, reads) {
4279
- if (!resolveOne(id, reads).available) return null;
4280
- switch (id) {
4281
- case "stream-broker": return {
4282
- kind: "device-disabled",
4283
- disabled: !on
4284
- };
4285
- case "object-detection":
4286
- case "audio-analysis":
4287
- case "event-saving": {
4288
- const capName = WRAPPER_CAP_BY_ID[id];
4289
- const wrapperAddonId = reads.wrapperAddonIdByCap.get(capName);
4290
- if (wrapperAddonId === void 0) return null;
4291
- return {
4292
- kind: "wrapper-binding",
4293
- capName,
4294
- wrapperAddonId,
4295
- active: on
4296
- };
4297
- }
4298
- case "recording": return {
4299
- kind: "recording-enabled",
4300
- enabled: on
4301
- };
4302
- case "notifications": return {
4303
- kind: "notification-mute",
4304
- muted: !on
4305
- };
4306
- case "privacy-mask": return {
4307
- kind: "camera-mask",
4308
- enabled: on
4309
- };
4310
- case "device-audio": return {
4311
- kind: "camera-audio",
4312
- enabled: on
4313
- };
4314
- case "broker-audio": return {
4315
- kind: "broker-audio-mute",
4316
- muted: !on
4317
- };
4318
- }
4319
- }
4320
- /**
4321
- * The ONE derivation of "a signed, expiring URL".
4322
- *
4323
- * This repo mints unguessable, self-expiring links in three places now — the
4324
- * notification artifact plane, the Home Assistant media plane, and the snapshot
4325
- * link plane. The first two grew independently and are byte-identical logic
4326
- * (`hmac(secret, "<id>:<exp>")`, expiry checked before a constant-time compare),
4327
- * each restating the crypto locally because **addons never import each other**.
4328
- *
4329
- * That reason is real, and the conclusion drawn from it was wrong. Two copies of
4330
- * a signing scheme is how one of them quietly ends up with a different TTL, a
4331
- * different compare, or a missing expiry check, and nothing fails until a link
4332
- * that should have died keeps working. The fix is the same one D52 applies to
4333
- * crop geometry: one derivation, in a place every addon may depend on. A
4334
- * framework package is exactly that place — `@camstack/types/node`, off the root
4335
- * entry because `node:crypto` must never be traversed by a browser bundler.
4336
- *
4337
- * What this module deliberately does NOT decide: the TTL, the base URL, the
4338
- * shape of `id`, and the access level of the route. Those are per-plane policy
4339
- * and each caller states them where a reader can see them.
4337
+ * This repo mints unguessable, self-expiring links in three places now — the
4338
+ * notification artifact plane, the Home Assistant media plane, and the snapshot
4339
+ * link plane. The first two grew independently and are byte-identical logic
4340
+ * (`hmac(secret, "<id>:<exp>")`, expiry checked before a constant-time compare),
4341
+ * each restating the crypto locally because **addons never import each other**.
4342
+ *
4343
+ * That reason is real, and the conclusion drawn from it was wrong. Two copies of
4344
+ * a signing scheme is how one of them quietly ends up with a different TTL, a
4345
+ * different compare, or a missing expiry check, and nothing fails until a link
4346
+ * that should have died keeps working. The fix is the same one D52 applies to
4347
+ * crop geometry: one derivation, in a place every addon may depend on. A
4348
+ * framework package is exactly that place — `@camstack/types/node`, off the root
4349
+ * entry because `node:crypto` must never be traversed by a browser bundler.
4350
+ *
4351
+ * What this module deliberately does NOT decide: the TTL, the base URL, the
4352
+ * shape of `id`, and the access level of the route. Those are per-plane policy
4353
+ * and each caller states them where a reader can see them.
4340
4354
  */
4341
4355
  /**
4342
4356
  * The signature over `(id, expMs)`.
@@ -4395,6 +4409,16 @@ function verifyExpiringUrl(input) {
4395
4409
  * an expired link is refused whether or not its signature is valid, so a leaked
4396
4410
  * URL stops working on its own.
4397
4411
  */
4412
+ /**
4413
+ * The base a media link is minted on when the operator left the addon's own
4414
+ * field empty: the hub's `internal` role (D734) — the address flagged
4415
+ * "Internal" on the Network page, else the detected LAN address. Home
4416
+ * Assistant is a LAN device pulling from the hub, so never the public
4417
+ * endpoint, and never loopback (`null` when nothing qualifies).
4418
+ */
4419
+ function haMediaBaseUrl(book) {
4420
+ return book.roles.internal.resolved;
4421
+ }
4398
4422
  /** How long a minted link stays valid. Long enough for HA to render it. */
4399
4423
  var MEDIA_URL_TTL_MS = 360 * 60 * 1e3;
4400
4424
  function signMedia(secret, id, expMs) {
@@ -4526,6 +4550,23 @@ function brokerRefreshPatch(current, next) {
4526
4550
  ...membershipChanged ? { membership: next.membership } : {}
4527
4551
  };
4528
4552
  }
4553
+ //#endregion
4554
+ //#region src/ha-export/node-reads.ts
4555
+ var UNREAD = {
4556
+ footageUsedBytes: null,
4557
+ footageFreePercent: null
4558
+ };
4559
+ function footageForNode(nodeId, usage) {
4560
+ if (usage === null || usage.nodeId !== nodeId) return UNREAD;
4561
+ const freePercents = (usage.locations ?? []).filter((loc) => loc.availableBytes !== null && loc.totalBytes !== null && loc.totalBytes > 0).map((loc) => loc.availableBytes / loc.totalBytes * 100);
4562
+ return {
4563
+ footageUsedBytes: usage.totalUsedBytes,
4564
+ footageFreePercent: freePercents.length > 0 ? Math.min(...freePercents) : null
4565
+ };
4566
+ }
4567
+ function shouldReadPackageStatus(node) {
4568
+ return !node.isHub && node.isOnline;
4569
+ }
4529
4570
  /**
4530
4571
  * What a linked Home Assistant NEEDS to function — one entry per thing it
4531
4572
  * actually calls with this token, each named. Not a blast radius, not a
@@ -4816,6 +4857,54 @@ var ReachabilityReconciler = class {
4816
4857
  }
4817
4858
  };
4818
4859
  //#endregion
4860
+ //#region src/ha-export/reconcile-fingerprint.ts
4861
+ function structureFingerprint(exported) {
4862
+ return JSON.stringify([...exported.keys()].sort().map((key) => {
4863
+ const entry = exported.get(key);
4864
+ return [
4865
+ key,
4866
+ entry?.brokerIds,
4867
+ entry?.plan
4868
+ ];
4869
+ }));
4870
+ }
4871
+ /** Only an event-debounced pass with an unchanged structure may skip. */
4872
+ function shouldSkipAnnounce(reason, fingerprint, lastFingerprint) {
4873
+ return reason === "structure-changed" && fingerprint === lastFingerprint;
4874
+ }
4875
+ //#endregion
4876
+ //#region src/ha-export/rule-triggers.ts
4877
+ function reduceOne(row) {
4878
+ return {
4879
+ ruleId: row.ruleId,
4880
+ ruleName: row.ruleName,
4881
+ deviceId: row.deviceId,
4882
+ at: row.createdAt,
4883
+ label: row.subject.label ?? row.subject.className,
4884
+ status: row.status,
4885
+ artifactId: row.artifactIds?.[0] ?? null
4886
+ };
4887
+ }
4888
+ /**
4889
+ * Reduce a newest-first page of history rows.
4890
+ *
4891
+ * A rule missing from `rows` (never fired, or its last fire fell outside the
4892
+ * page this pass could afford) is simply absent from `byRule` — the caller
4893
+ * renders that as `unknown`, never as an invented value.
4894
+ */
4895
+ function reduceLastTriggers(rows) {
4896
+ const byRule = /* @__PURE__ */ new Map();
4897
+ let overall = null;
4898
+ for (const row of rows) {
4899
+ if (overall === null) overall = reduceOne(row);
4900
+ if (!byRule.has(row.ruleId)) byRule.set(row.ruleId, reduceOne(row));
4901
+ }
4902
+ return {
4903
+ overall,
4904
+ byRule
4905
+ };
4906
+ }
4907
+ //#endregion
4819
4908
  //#region src/ha-export/state-projector.ts
4820
4909
  /**
4821
4910
  * Pipeline / notification-center output → `{topic, value}`.
@@ -5864,84 +5953,6 @@ function projectSynthetic(input, nowMs) {
5864
5953
  return values;
5865
5954
  }
5866
5955
  //#endregion
5867
- //#region src/ha-export/node-reads.ts
5868
- var UNREAD = {
5869
- footageUsedBytes: null,
5870
- footageFreePercent: null
5871
- };
5872
- function footageForNode(nodeId, usage) {
5873
- if (usage === null || usage.nodeId !== nodeId) return UNREAD;
5874
- const freePercents = (usage.locations ?? []).filter((loc) => loc.availableBytes !== null && loc.totalBytes !== null && loc.totalBytes > 0).map((loc) => loc.availableBytes / loc.totalBytes * 100);
5875
- return {
5876
- footageUsedBytes: usage.totalUsedBytes,
5877
- footageFreePercent: freePercents.length > 0 ? Math.min(...freePercents) : null
5878
- };
5879
- }
5880
- function shouldReadPackageStatus(node) {
5881
- return !node.isHub && node.isOnline;
5882
- }
5883
- //#endregion
5884
- //#region src/ha-export/rule-triggers.ts
5885
- function reduceOne(row) {
5886
- return {
5887
- ruleId: row.ruleId,
5888
- ruleName: row.ruleName,
5889
- deviceId: row.deviceId,
5890
- at: row.createdAt,
5891
- label: row.subject.label ?? row.subject.className,
5892
- status: row.status,
5893
- artifactId: row.artifactIds?.[0] ?? null
5894
- };
5895
- }
5896
- /**
5897
- * Reduce a newest-first page of history rows.
5898
- *
5899
- * A rule missing from `rows` (never fired, or its last fire fell outside the
5900
- * page this pass could afford) is simply absent from `byRule` — the caller
5901
- * renders that as `unknown`, never as an invented value.
5902
- */
5903
- function reduceLastTriggers(rows) {
5904
- const byRule = /* @__PURE__ */ new Map();
5905
- let overall = null;
5906
- for (const row of rows) {
5907
- if (overall === null) overall = reduceOne(row);
5908
- if (!byRule.has(row.ruleId)) byRule.set(row.ruleId, reduceOne(row));
5909
- }
5910
- return {
5911
- overall,
5912
- byRule
5913
- };
5914
- }
5915
- //#endregion
5916
- //#region src/ha-export/reconcile-fingerprint.ts
5917
- function structureFingerprint(exported) {
5918
- return JSON.stringify([...exported.keys()].sort().map((key) => {
5919
- const entry = exported.get(key);
5920
- return [
5921
- key,
5922
- entry?.brokerIds,
5923
- entry?.plan
5924
- ];
5925
- }));
5926
- }
5927
- /** Only an event-debounced pass with an unchanged structure may skip. */
5928
- function shouldSkipAnnounce(reason, fingerprint, lastFingerprint) {
5929
- return reason === "structure-changed" && fingerprint === lastFingerprint;
5930
- }
5931
- /** The `BrokerInfo.kind` tag the Home Assistant provider stamps. */
5932
- var HA_BROKER_KIND = "home-assistant";
5933
- /**
5934
- * Every Home Assistant broker currently registered, cluster-wide.
5935
- *
5936
- * Unpinned by design (see the module docblock) and filtered by `kind`, which is
5937
- * the contract check that matters: whatever mix of providers answered the
5938
- * union, only `home-assistant` brokers are ever exported to over Home
5939
- * Assistant's transport.
5940
- */
5941
- async function listHomeAssistantBrokers(query) {
5942
- return (await query({})).filter((broker) => broker.kind === HA_BROKER_KIND);
5943
- }
5944
- //#endregion
5945
5956
  //#region src/ha-export/ha-export.addon.ts
5946
5957
  /**
5947
5958
  * Native Home Assistant export — a custom component fed by HTTP push.
@@ -5991,8 +6002,6 @@ var DOORBELL_PULSE_MS = 5e3;
5991
6002
  * instant. Year 2000 predates every device this addon exports.
5992
6003
  */
5993
6004
  var MIN_PLAUSIBLE_TIMESTAMP_MS = Date.UTC(2e3, 0, 1);
5994
- /** The hub's API port — where the data plane is reverse-proxied. */
5995
- var HUB_API_PORT = 4443;
5996
6005
  /** One PTZ button press, as a relative move. */
5997
6006
  var PTZ_MOVE_BY_DIRECTION = {
5998
6007
  up: { tilt: .1 },
@@ -7880,14 +7889,7 @@ var HaExportAddon = class extends BaseAddon {
7880
7889
  return;
7881
7890
  }
7882
7891
  try {
7883
- const { endpoints } = await this.ctx.api.localNetwork.getConnectionEndpoints.query({ port: HUB_API_PORT });
7884
- /**
7885
- * Lowest priority wins, loopback never: a link Home Assistant
7886
- * cannot open is worse than no image, because it renders as a
7887
- * broken entity rather than an empty one.
7888
- */
7889
- const usable = [...endpoints].filter((endpoint) => endpoint.kind !== "loopback" && endpoint.plausible).sort((a, b) => a.priority - b.priority)[0];
7890
- this.mediaBaseUrl = usable?.baseUrl ?? null;
7892
+ this.mediaBaseUrl = haMediaBaseUrl(await this.ctx.api.localNetwork.getAddresses.query());
7891
7893
  } catch (err) {
7892
7894
  this.mediaBaseUrl = null;
7893
7895
  this.ctx.logger.debug("ha-export: could not resolve a media base URL", { meta: { error: errMsg(err) } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.160",
3
+ "version": "1.2.162",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",