@camstack/addon-provider-homeassistant 1.2.159 → 1.2.161

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-Ck6d87Ov.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-B1FEp9mY.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)`.
@@ -4526,6 +4540,23 @@ function brokerRefreshPatch(current, next) {
4526
4540
  ...membershipChanged ? { membership: next.membership } : {}
4527
4541
  };
4528
4542
  }
4543
+ //#endregion
4544
+ //#region src/ha-export/node-reads.ts
4545
+ var UNREAD = {
4546
+ footageUsedBytes: null,
4547
+ footageFreePercent: null
4548
+ };
4549
+ function footageForNode(nodeId, usage) {
4550
+ if (usage === null || usage.nodeId !== nodeId) return UNREAD;
4551
+ const freePercents = (usage.locations ?? []).filter((loc) => loc.availableBytes !== null && loc.totalBytes !== null && loc.totalBytes > 0).map((loc) => loc.availableBytes / loc.totalBytes * 100);
4552
+ return {
4553
+ footageUsedBytes: usage.totalUsedBytes,
4554
+ footageFreePercent: freePercents.length > 0 ? Math.min(...freePercents) : null
4555
+ };
4556
+ }
4557
+ function shouldReadPackageStatus(node) {
4558
+ return !node.isHub && node.isOnline;
4559
+ }
4529
4560
  /**
4530
4561
  * What a linked Home Assistant NEEDS to function — one entry per thing it
4531
4562
  * actually calls with this token, each named. Not a blast radius, not a
@@ -4816,6 +4847,54 @@ var ReachabilityReconciler = class {
4816
4847
  }
4817
4848
  };
4818
4849
  //#endregion
4850
+ //#region src/ha-export/reconcile-fingerprint.ts
4851
+ function structureFingerprint(exported) {
4852
+ return JSON.stringify([...exported.keys()].sort().map((key) => {
4853
+ const entry = exported.get(key);
4854
+ return [
4855
+ key,
4856
+ entry?.brokerIds,
4857
+ entry?.plan
4858
+ ];
4859
+ }));
4860
+ }
4861
+ /** Only an event-debounced pass with an unchanged structure may skip. */
4862
+ function shouldSkipAnnounce(reason, fingerprint, lastFingerprint) {
4863
+ return reason === "structure-changed" && fingerprint === lastFingerprint;
4864
+ }
4865
+ //#endregion
4866
+ //#region src/ha-export/rule-triggers.ts
4867
+ function reduceOne(row) {
4868
+ return {
4869
+ ruleId: row.ruleId,
4870
+ ruleName: row.ruleName,
4871
+ deviceId: row.deviceId,
4872
+ at: row.createdAt,
4873
+ label: row.subject.label ?? row.subject.className,
4874
+ status: row.status,
4875
+ artifactId: row.artifactIds?.[0] ?? null
4876
+ };
4877
+ }
4878
+ /**
4879
+ * Reduce a newest-first page of history rows.
4880
+ *
4881
+ * A rule missing from `rows` (never fired, or its last fire fell outside the
4882
+ * page this pass could afford) is simply absent from `byRule` — the caller
4883
+ * renders that as `unknown`, never as an invented value.
4884
+ */
4885
+ function reduceLastTriggers(rows) {
4886
+ const byRule = /* @__PURE__ */ new Map();
4887
+ let overall = null;
4888
+ for (const row of rows) {
4889
+ if (overall === null) overall = reduceOne(row);
4890
+ if (!byRule.has(row.ruleId)) byRule.set(row.ruleId, reduceOne(row));
4891
+ }
4892
+ return {
4893
+ overall,
4894
+ byRule
4895
+ };
4896
+ }
4897
+ //#endregion
4819
4898
  //#region src/ha-export/state-projector.ts
4820
4899
  /**
4821
4900
  * Pipeline / notification-center output → `{topic, value}`.
@@ -5864,84 +5943,6 @@ function projectSynthetic(input, nowMs) {
5864
5943
  return values;
5865
5944
  }
5866
5945
  //#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
5946
  //#region src/ha-export/ha-export.addon.ts
5946
5947
  /**
5947
5948
  * Native Home Assistant export — a custom component fed by HTTP push.
@@ -5991,8 +5992,6 @@ var DOORBELL_PULSE_MS = 5e3;
5991
5992
  * instant. Year 2000 predates every device this addon exports.
5992
5993
  */
5993
5994
  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
5995
  /** One PTZ button press, as a relative move. */
5997
5996
  var PTZ_MOVE_BY_DIRECTION = {
5998
5997
  up: { tilt: .1 },
@@ -7880,14 +7879,15 @@ var HaExportAddon = class extends BaseAddon {
7880
7879
  return;
7881
7880
  }
7882
7881
  try {
7883
- const { endpoints } = await this.ctx.api.localNetwork.getConnectionEndpoints.query({ port: HUB_API_PORT });
7884
7882
  /**
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.
7883
+ * Home Assistant is a LAN device pulling from the hub: the `agents`
7884
+ * role (the address flagged "Agents" on the Network page, else the
7885
+ * detected LAN address), never loopback — a link Home Assistant
7886
+ * cannot open is worse than no image, because it renders as a broken
7887
+ * entity rather than an empty one. Ranked by `local-network`, never here.
7888
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;
7889
+ const { roles } = await this.ctx.api.localNetwork.getAddresses.query();
7890
+ this.mediaBaseUrl = roles.agents.resolved;
7891
7891
  } catch (err) {
7892
7892
  this.mediaBaseUrl = null;
7893
7893
  this.ctx.logger.debug("ha-export: could not resolve a media base URL", { meta: { error: errMsg(err) } });
package/dist/index.js CHANGED
@@ -13,6 +13,5 @@ exports.HaWsClient = require_addon.HaWsClient;
13
13
  exports.authToSettings = require_addon.authToSettings;
14
14
  exports.buildHaAuth = require_addon.buildHaAuth;
15
15
  exports.processEvent = require_addon.processEvent;
16
- exports.resolveAuthConfig = require_addon.resolveAuthConfig;
17
16
  exports.settingsToAuth = require_addon.settingsToAuth;
18
17
  exports.subscribeEntities = require_addon.subscribeEntities;
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { HaProviderAddon, a as HaIntegrationManager, c as HaWsClient, d as HaAuthConfigSchema, f as HaDomain, i as settingsToAuth, l as buildHaAuth, n as HaBrokerRegistry, o as processEvent, p as HaEntityStateSchema, r as authToSettings, s as subscribeEntities, t as HaBrokerEntrySchema, u as resolveAuthConfig } from "./addon.mjs";
1
+ import { HaProviderAddon, a as HaIntegrationManager, c as HaWsClient, d as HaDomain, f as HaEntityStateSchema, i as settingsToAuth, l as buildHaAuth, n as HaBrokerRegistry, o as processEvent, r as authToSettings, s as subscribeEntities, t as HaBrokerEntrySchema, u as HaAuthConfigSchema } from "./addon.mjs";
2
2
  import { HaExportAddon } from "./ha-export/ha-export.addon.mjs";
3
- export { HaAuthConfigSchema, HaBrokerEntrySchema, HaBrokerRegistry, HaDomain, HaEntityStateSchema, HaExportAddon, HaIntegrationManager, HaProviderAddon, HaWsClient, authToSettings, buildHaAuth, processEvent, resolveAuthConfig, settingsToAuth, subscribeEntities };
3
+ export { HaAuthConfigSchema, HaBrokerEntrySchema, HaBrokerRegistry, HaDomain, HaEntityStateSchema, HaExportAddon, HaIntegrationManager, HaProviderAddon, HaWsClient, authToSettings, buildHaAuth, processEvent, settingsToAuth, subscribeEntities };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.159",
3
+ "version": "1.2.161",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",