@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.
- package/dist/addon.js +1 -21
- package/dist/addon.mjs +2 -21
- package/dist/{dist-Ck6d87Ov.mjs → dist-B1FEp9mY.mjs} +255 -8
- package/dist/{dist-BE_WMCdp.js → dist-C3T2aMHG.js} +255 -8
- package/dist/ha-export/ha-export.addon.js +302 -302
- package/dist/ha-export/ha-export.addon.mjs +302 -302
- package/dist/index.js +0 -1
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
|
@@ -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-
|
|
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
|
-
*
|
|
4335
|
+
* The ONE derivation of "a signed, expiring URL".
|
|
4127
4336
|
*
|
|
4128
|
-
*
|
|
4129
|
-
*
|
|
4130
|
-
*
|
|
4131
|
-
*
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
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
|
-
*
|
|
7886
|
-
*
|
|
7887
|
-
*
|
|
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
|
|
7890
|
-
this.mediaBaseUrl =
|
|
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
|
|
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,
|
|
3
|
+
export { HaAuthConfigSchema, HaBrokerEntrySchema, HaBrokerRegistry, HaDomain, HaEntityStateSchema, HaExportAddon, HaIntegrationManager, HaProviderAddon, HaWsClient, authToSettings, buildHaAuth, processEvent, settingsToAuth, subscribeEntities };
|