@khalido/home 0.1.2 → 0.2.0
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/.env.example +2 -2
- package/CHANGELOG.md +74 -2
- package/README.md +7 -2
- package/dist/clock.js +2 -0
- package/dist/config.d.ts +5 -3
- package/dist/config.js +5 -1
- package/dist/connector.d.ts +9 -2
- package/dist/connectors/amber/index.js +1 -0
- package/dist/connectors/cast/index.d.ts +8 -6
- package/dist/connectors/cast/index.js +30 -4
- package/dist/connectors/cast/state.d.ts +66 -0
- package/dist/connectors/cast/state.js +141 -0
- package/dist/connectors/cast/writes.d.ts +0 -2
- package/dist/connectors/cast/writes.js +25 -7
- package/dist/connectors/daikin/client.d.ts +45 -2
- package/dist/connectors/daikin/client.js +67 -2
- package/dist/connectors/daikin/index.d.ts +8 -4
- package/dist/connectors/daikin/index.js +25 -4
- package/dist/connectors/enphase/index.d.ts +5 -1
- package/dist/connectors/enphase/index.js +34 -2
- package/dist/connectors/hue/ca.d.ts +2 -0
- package/dist/connectors/hue/ca.js +38 -0
- package/dist/connectors/hue/client.d.ts +81 -6
- package/dist/connectors/hue/client.js +217 -6
- package/dist/connectors/hue/color.d.ts +30 -0
- package/dist/connectors/hue/color.js +92 -0
- package/dist/connectors/hue/index.d.ts +21 -6
- package/dist/connectors/hue/index.js +59 -9
- package/dist/connectors/hue/schemas.d.ts +27 -0
- package/dist/connectors/hue/schemas.js +26 -0
- package/dist/connectors/hue/state.d.ts +10 -1
- package/dist/connectors/hue/state.js +20 -1
- package/dist/connectors/printers/index.d.ts +6 -2
- package/dist/connectors/printers/index.js +23 -0
- package/dist/connectors/spotify/changes.d.ts +19 -5
- package/dist/connectors/spotify/changes.js +29 -7
- package/dist/connectors/spotify/index.d.ts +7 -2
- package/dist/connectors/spotify/index.js +34 -3
- package/dist/connectors/up/index.js +1 -0
- package/dist/connectors/weather/bom.js +1 -0
- package/dist/connectors/weather/google.js +52 -9
- package/dist/connectors/weather/index.d.ts +2 -1
- package/dist/connectors/weather/index.js +7 -0
- package/dist/connectors/weather/source.d.ts +2 -0
- package/dist/devices.d.ts +51 -0
- package/dist/devices.js +115 -0
- package/dist/discover.js +1 -1
- package/dist/finders-entry.d.ts +1 -1
- package/dist/finders-entry.js +1 -1
- package/dist/finders.d.ts +2 -1
- package/dist/finders.js +16 -5
- package/dist/health.d.ts +2 -1
- package/dist/health.js +2 -1
- package/dist/home.d.ts +52 -5
- package/dist/home.js +114 -33
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/node-fetch.d.ts +2 -2
- package/dist/node-fetch.js +3 -2
- package/docs/cli.md +13 -7
- package/docs/config.md +18 -5
- package/docs/decisions.md +16 -0
- package/docs/hue.md +161 -0
- package/docs/using.md +115 -3
- package/examples/home.jsonc +2 -1
- package/home.schema.json +10 -3
- package/llms.txt +19 -6
- package/package.json +1 -1
- package/skills/draw-home/SKILL.md +1 -1
- package/skills/use-home/SKILL.md +10 -10
- package/src/cli/cast.ts +51 -5
- package/src/cli/context.ts +2 -0
- package/src/cli/daikin.ts +71 -0
- package/src/cli/enphase.ts +5 -1
- package/src/cli/hue.ts +147 -59
- package/src/cli/main.ts +35 -10
- package/src/cli/probe.ts +21 -17
- package/src/cli/status.ts +87 -13
- package/src/clock.ts +1 -0
- package/src/config.ts +8 -2
- package/src/connector.ts +18 -2
- package/src/connectors/amber/index.ts +1 -0
- package/src/connectors/cast/README.md +10 -3
- package/src/connectors/cast/index.ts +65 -7
- package/src/connectors/cast/state.ts +204 -0
- package/src/connectors/cast/writes.ts +29 -8
- package/src/connectors/daikin/README.md +20 -4
- package/src/connectors/daikin/client.ts +104 -3
- package/src/connectors/daikin/index.ts +49 -4
- package/src/connectors/enphase/index.ts +49 -2
- package/src/connectors/hue/README.md +62 -17
- package/src/connectors/hue/ca.ts +39 -0
- package/src/connectors/hue/client.ts +362 -15
- package/src/connectors/hue/color.ts +123 -0
- package/src/connectors/hue/index.ts +94 -12
- package/src/connectors/hue/schemas.ts +26 -0
- package/src/connectors/hue/state.ts +27 -1
- package/src/connectors/printers/README.md +4 -2
- package/src/connectors/printers/index.ts +43 -1
- package/src/connectors/spotify/README.md +21 -3
- package/src/connectors/spotify/changes.ts +38 -9
- package/src/connectors/spotify/index.ts +55 -3
- package/src/connectors/up/index.ts +1 -0
- package/src/connectors/weather/README.md +1 -1
- package/src/connectors/weather/bom.ts +1 -0
- package/src/connectors/weather/google.ts +66 -10
- package/src/connectors/weather/index.ts +16 -2
- package/src/connectors/weather/source.ts +2 -0
- package/src/devices.ts +182 -0
- package/src/discover.ts +1 -2
- package/src/finders-entry.ts +1 -1
- package/src/finders.ts +18 -6
- package/src/health.ts +4 -2
- package/src/home.ts +166 -39
- package/src/index.ts +11 -1
- package/src/node-fetch.ts +6 -2
package/src/home.ts
CHANGED
|
@@ -9,15 +9,18 @@ export const envName = z
|
|
|
9
9
|
/^[A-Z_][A-Z0-9_]*$/,
|
|
10
10
|
"an environment variable name (e.g. AMBER_KEY), not the secret itself",
|
|
11
11
|
);
|
|
12
|
-
/** A file a connector needs (a CA, a token), relative to the config file
|
|
12
|
+
/** A file a connector needs (a CA, a token), relative to the config file or from `~/`. The caller reads it, and expands `~/`; the library reads no files. */
|
|
13
13
|
export const filePath = z.string().min(1);
|
|
14
14
|
|
|
15
15
|
/** How an integration is doing. "unavailable" means it stopped and will not retry by itself. */
|
|
16
16
|
export type Status =
|
|
17
17
|
"connecting" | "connected" | "disconnected" | "unavailable";
|
|
18
18
|
|
|
19
|
+
/** Why a part is `unavailable`, for code to switch on; `detail` is the text for people. `auth`: a key or token was rejected or is unusable, so a person must re-key. `config`: a setting or a file is wrong. `crashed`: its `run` threw. */
|
|
20
|
+
export type Reason = "auth" | "config" | "crashed";
|
|
21
|
+
|
|
19
22
|
/**
|
|
20
|
-
* One integration's slice of the home: its status, a human detail, anything odd the device reported, and its state.
|
|
23
|
+
* One integration's slice of the home: its status, a human detail, `reason` (why it is `unavailable`, for code to switch on), anything odd the device reported, and its state.
|
|
21
24
|
* `createHome` stamps the times (UTC ISO). `lastGoodAt`: its last report while connected, or the read time a connected report
|
|
22
25
|
* gives when it repeats an older read (weather's 5-minute sun tick). `downSince`: when it stopped being connected (or started,
|
|
23
26
|
* if it never has); none while connected. Any other time an integration sets is ignored.
|
|
@@ -25,12 +28,68 @@ export type Status =
|
|
|
25
28
|
export type Part<S = unknown> = {
|
|
26
29
|
status: Status;
|
|
27
30
|
detail?: string;
|
|
31
|
+
reason?: Reason;
|
|
28
32
|
anomaly?: string;
|
|
29
33
|
state?: S;
|
|
30
34
|
lastGoodAt?: string;
|
|
31
35
|
downSince?: string;
|
|
32
36
|
};
|
|
33
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The few things every device answers, whatever it is, in the same words; a connector's `<name>HomeDevices(part)` returns
|
|
40
|
+
* these and `homeDevices(snapshot)` collects them. Everything particular to its kind is in `detail`, the connector's own
|
|
41
|
+
* typed row, and `connector` (with `kind`) says which type that is. Unknown stays absent, never guessed.
|
|
42
|
+
* - `id`: the device's own stable id (a Hue light id, a castId, a printer's UUID), or the connector's name when it has one device.
|
|
43
|
+
* - `kind`: light, group, speaker, display, tv, ac, printer, solar (and `cast` for a Cast model this library doesn't know).
|
|
44
|
+
* - `available`: it answered and can be used now. When it is false, `reason` says why in one word: `offline`, or the
|
|
45
|
+
* connector's own reason for being `unavailable` (`auth`, `config`, `crashed`).
|
|
46
|
+
* - `asOf`: when the device last reported (a push connector) or was last read (a polled one). A quiet light can be hours
|
|
47
|
+
* old and current: `available` says whether it is.
|
|
48
|
+
* - `state`: one short word in that kind's vocabulary: on, off, idle, busy, cool, printing.
|
|
49
|
+
* - `text`: one line for people, "Kitchen: on · 60%".
|
|
50
|
+
* - `room`: a room id from the home config, when one places it.
|
|
51
|
+
* - `members`: a group's device ids.
|
|
52
|
+
*/
|
|
53
|
+
export type DeviceRecord<
|
|
54
|
+
C extends string = string,
|
|
55
|
+
K extends string = string,
|
|
56
|
+
S extends string = string,
|
|
57
|
+
D = unknown,
|
|
58
|
+
> = {
|
|
59
|
+
connector: C;
|
|
60
|
+
id: string;
|
|
61
|
+
name: string;
|
|
62
|
+
kind: K;
|
|
63
|
+
available?: boolean;
|
|
64
|
+
reason?: Reason | "offline";
|
|
65
|
+
asOf?: string;
|
|
66
|
+
state?: S;
|
|
67
|
+
text: string;
|
|
68
|
+
room?: string;
|
|
69
|
+
members?: string[];
|
|
70
|
+
detail: D;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A connector's device records from its part. While the connector isn't connected none is `available`, and each keeps its
|
|
75
|
+
* last values, as the part's state does. A device that isn't available gets its `reason`: the part's when it stopped for
|
|
76
|
+
* good, else `offline`.
|
|
77
|
+
*/
|
|
78
|
+
export const deviceRecords = <R extends DeviceRecord>(
|
|
79
|
+
part: Part | undefined,
|
|
80
|
+
records: R[],
|
|
81
|
+
): R[] => {
|
|
82
|
+
const up = part?.status === "connected";
|
|
83
|
+
// A part that stopped for good without saying why is a custom integration's: `config` is the nearest true word, since it won't come back by itself.
|
|
84
|
+
const why =
|
|
85
|
+
part?.status === "unavailable" ? (part.reason ?? "config") : "offline";
|
|
86
|
+
return records.map((r) =>
|
|
87
|
+
up && r.available !== false
|
|
88
|
+
? r
|
|
89
|
+
: { ...r, available: false, reason: up ? "offline" : why },
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
34
93
|
/**
|
|
35
94
|
* An integration is a folder in `src/` that exports a factory returning this.
|
|
36
95
|
* `run` keeps its state current until `signal` aborts, reporting each change through `update`.
|
|
@@ -81,7 +140,7 @@ function stamped(part: Part, before: Part | undefined, now: string): Part {
|
|
|
81
140
|
|
|
82
141
|
/**
|
|
83
142
|
* Load a home: run each integration, keep one snapshot, call `onChange` with it and what changed after every report.
|
|
84
|
-
* `clock` stamps each part's `lastGoodAt` and `downSince`.
|
|
143
|
+
* `clock` stamps each part's `lastGoodAt` and `downSince`. `add` and `remove` change the set while it runs.
|
|
85
144
|
*/
|
|
86
145
|
export function createHome<I extends Integration<any, string>>(
|
|
87
146
|
integrations: readonly I[],
|
|
@@ -89,22 +148,80 @@ export function createHome<I extends Integration<any, string>>(
|
|
|
89
148
|
options: { clock?: Clock } = {},
|
|
90
149
|
) {
|
|
91
150
|
type Name = I["name"];
|
|
151
|
+
type Run = { stop: AbortController; done: Promise<void> };
|
|
92
152
|
const clock = options.clock ?? realClock;
|
|
153
|
+
const list: I[] = [...integrations]; // the integrations now: `add` and `remove` change it
|
|
154
|
+
const parts = () => snapshot as Record<string, Part>;
|
|
93
155
|
let snapshot = Object.fromEntries(
|
|
94
|
-
|
|
156
|
+
list.map((i) => [i.name, { status: "connecting" }]),
|
|
95
157
|
) as HomeSnapshot<I>;
|
|
96
158
|
const waiting = new Map<string, (() => void)[]>(); // refresh() callers, resolved by the integration's next report
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
159
|
+
const runs = new Map<I, Run>(); // the ones running: before start or after its run ended, a refresh resolves at once, nothing will report
|
|
160
|
+
const latest = new WeakMap<I, Run>(); // each integration's newest run: a report from an older run of the same object is dropped
|
|
161
|
+
let started: AbortSignal | undefined; // start()'s signal, while it runs
|
|
162
|
+
let changing: Promise<unknown> = Promise.resolve(); // add and remove, one at a time: two on one name must not overlap
|
|
163
|
+
let changes = 0; // those under way or queued: start outlasts a replace of its only integration
|
|
164
|
+
const change = <T>(fn: () => Promise<T>): Promise<T> => {
|
|
165
|
+
changes++;
|
|
166
|
+
const done = changing.then(fn).finally(() => changes--);
|
|
167
|
+
changing = done.catch(() => {});
|
|
168
|
+
return done;
|
|
169
|
+
};
|
|
170
|
+
const heard = (name: string) => {
|
|
171
|
+
for (const resolve of waiting.get(name) ?? []) resolve();
|
|
172
|
+
waiting.delete(name);
|
|
173
|
+
};
|
|
174
|
+
// A report counts only from the integration now listed under its name: one replaced or removed may still be finishing.
|
|
175
|
+
const set = (i: I, run: Run, reported: Part) => {
|
|
176
|
+
const name: Name = i.name;
|
|
177
|
+
const before = parts()[name];
|
|
178
|
+
if (latest.get(i) !== run || !list.includes(i) || !before) return;
|
|
100
179
|
const part = stamped(reported, before, asOf(clock));
|
|
101
180
|
snapshot = { ...snapshot, [name]: part };
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
181
|
+
Promise.try(() =>
|
|
182
|
+
onChange?.(snapshot, { name, before, after: part } as Change<I>),
|
|
183
|
+
).catch((error) =>
|
|
184
|
+
console.error(`onChange threw on ${name}: ${errorText(error)}`),
|
|
185
|
+
); // the caller's bug, thrown or async, not the device's: the integration keeps running
|
|
186
|
+
heard(name);
|
|
187
|
+
};
|
|
188
|
+
/** Run one integration until the home's signal or its own `stop` aborts. One failing marks itself unavailable. */
|
|
189
|
+
const launch = (i: I, signal: AbortSignal) => {
|
|
190
|
+
const stop = new AbortController();
|
|
191
|
+
const run: Run = { stop, done: Promise.resolve() };
|
|
192
|
+
runs.set(i, run);
|
|
193
|
+
latest.set(i, run);
|
|
194
|
+
run.done = Promise.try(() =>
|
|
195
|
+
i.run(AbortSignal.any([signal, stop.signal]), (part) =>
|
|
196
|
+
set(i, run, part),
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
.catch((error) =>
|
|
200
|
+
set(i, run, {
|
|
201
|
+
status: "unavailable",
|
|
202
|
+
reason: "crashed",
|
|
203
|
+
detail: errorText(error),
|
|
204
|
+
state: parts()[i.name]?.state, // the last good state stays, as it does through a failed read
|
|
205
|
+
}),
|
|
206
|
+
)
|
|
207
|
+
.finally(() => {
|
|
208
|
+
if (runs.get(i) !== run) return;
|
|
209
|
+
runs.delete(i);
|
|
210
|
+
if (list.includes(i)) heard(i.name); // nothing more will report
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
/** Take one out of the list and the snapshot, and wait for its run to end. */
|
|
214
|
+
const drop = async (name: string) => {
|
|
215
|
+
const at = list.findIndex((i) => i.name === name);
|
|
216
|
+
if (at < 0) return false;
|
|
217
|
+
const [old] = list.splice(at, 1);
|
|
218
|
+
const { [name]: _, ...rest } = parts();
|
|
219
|
+
snapshot = rest as HomeSnapshot<I>;
|
|
220
|
+
heard(name);
|
|
221
|
+
const run = runs.get(old!);
|
|
222
|
+
run?.stop.abort();
|
|
223
|
+
await run?.done;
|
|
224
|
+
return true;
|
|
108
225
|
};
|
|
109
226
|
return {
|
|
110
227
|
/** The latest snapshot. Never touches a device. */
|
|
@@ -115,57 +232,67 @@ export function createHome<I extends Integration<any, string>>(
|
|
|
115
232
|
integration: <N extends Name>(
|
|
116
233
|
name: N,
|
|
117
234
|
): Extract<I, { name: N }> | undefined =>
|
|
118
|
-
|
|
119
|
-
Extract<I, { name: N }> | undefined,
|
|
235
|
+
list.find((i) => i.name === name) as Extract<I, { name: N }> | undefined,
|
|
120
236
|
/**
|
|
121
237
|
* Read now: one integration by name, or every one. Results arrive through the snapshot and `onChange` as usual; the
|
|
122
|
-
* promise resolves once each target has reported (its next update, good or bad), or at once for one
|
|
238
|
+
* promise resolves once each target has reported (its next update, good or bad), or at once for one that isn't running.
|
|
123
239
|
*/
|
|
124
240
|
refresh(name?: Name): Promise<void> {
|
|
125
241
|
const targets =
|
|
126
|
-
name === undefined
|
|
127
|
-
|
|
128
|
-
: integrations.filter((i) => i.name === name);
|
|
129
|
-
if (!targets.length)
|
|
242
|
+
name === undefined ? [...list] : list.filter((i) => i.name === name);
|
|
243
|
+
if (!targets.length && name !== undefined)
|
|
130
244
|
throw new Error(
|
|
131
|
-
`no integration "${name}"; have ${
|
|
245
|
+
`no integration "${name}"; have ${list.map((i) => i.name).join(", ")}`,
|
|
132
246
|
);
|
|
133
247
|
const reported = targets.map(
|
|
134
248
|
(i) =>
|
|
135
249
|
new Promise<void>((resolve) => {
|
|
136
|
-
if (
|
|
250
|
+
if (!runs.has(i)) return resolve();
|
|
137
251
|
waiting.set(i.name, [...(waiting.get(i.name) ?? []), resolve]);
|
|
138
252
|
}),
|
|
139
253
|
);
|
|
140
254
|
for (const i of targets) i.refresh();
|
|
141
255
|
return Promise.all(reported).then(() => undefined);
|
|
142
256
|
},
|
|
143
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* Add an integration while the home runs, or before it starts: a bridge paired just now, a key that arrived late. One
|
|
259
|
+
* already there under that name is stopped first and its part dropped, so the new one starts from `connecting` and
|
|
260
|
+
* the two never run together. Resolves once the new one is running; its reports arrive as any other's. Calls on
|
|
261
|
+
* one home run one at a time. Once `start`'s signal has aborted, an added one waits for the next `start`.
|
|
262
|
+
*/
|
|
263
|
+
add: (integration: I): Promise<void> =>
|
|
264
|
+
change(async () => {
|
|
265
|
+
await drop(integration.name);
|
|
266
|
+
list.push(integration);
|
|
267
|
+
const live = started && !started.aborted ? started : undefined;
|
|
268
|
+
snapshot = {
|
|
269
|
+
...snapshot,
|
|
270
|
+
[integration.name]: {
|
|
271
|
+
status: "connecting",
|
|
272
|
+
...(live ? { downSince: asOf(clock) } : {}),
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
if (live) launch(integration, live);
|
|
276
|
+
}),
|
|
277
|
+
/** Stop one integration and drop its part from the snapshot. Resolves once its run has ended; false if there was none by that name. */
|
|
278
|
+
remove: (name: Name): Promise<boolean> => change(() => drop(name)),
|
|
279
|
+
/** Run every integration, and any added meanwhile, until `signal` aborts. One failing marks itself unavailable; the rest carry on. */
|
|
144
280
|
async start(signal: AbortSignal): Promise<void> {
|
|
145
|
-
|
|
281
|
+
started = signal;
|
|
146
282
|
const now = asOf(clock); // the wait for a first report counts from here, not from createHome
|
|
147
283
|
snapshot = Object.fromEntries(
|
|
148
|
-
Object.entries(
|
|
284
|
+
Object.entries(parts()).map(([name, p]) => [
|
|
149
285
|
name,
|
|
150
286
|
p.status === "connecting"
|
|
151
287
|
? { ...p, downSince: p.downSince ?? now }
|
|
152
288
|
: p,
|
|
153
289
|
]),
|
|
154
290
|
) as HomeSnapshot<I>;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
set(i.name, { status: "unavailable", detail: errorText(error) }),
|
|
161
|
-
)
|
|
162
|
-
.finally(() => {
|
|
163
|
-
finished.add(i.name);
|
|
164
|
-
for (const resolve of waiting.get(i.name) ?? []) resolve(); // nothing more will report
|
|
165
|
-
waiting.delete(i.name);
|
|
166
|
-
}),
|
|
167
|
-
),
|
|
168
|
-
);
|
|
291
|
+
for (const i of list) launch(i, signal); // a second start, after the first has stopped, runs them all again
|
|
292
|
+
// Until every run has ended, those added meanwhile too.
|
|
293
|
+
while (runs.size || changes)
|
|
294
|
+
await Promise.all([...[...runs.values()].map((r) => r.done), changing]);
|
|
295
|
+
// `started` stays: one added after every run has ended still runs, until the signal aborts.
|
|
169
296
|
},
|
|
170
297
|
};
|
|
171
298
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
export {
|
|
4
4
|
type Change,
|
|
5
5
|
createHome,
|
|
6
|
+
type DeviceRecord,
|
|
7
|
+
deviceRecords,
|
|
6
8
|
envName,
|
|
7
9
|
filePath,
|
|
8
10
|
settle,
|
|
9
11
|
type HomeSnapshot,
|
|
10
12
|
type Integration,
|
|
11
13
|
type Part,
|
|
14
|
+
type Reason,
|
|
12
15
|
type StateOf,
|
|
13
16
|
type Status,
|
|
14
17
|
} from "./home.ts";
|
|
@@ -19,8 +22,15 @@ export {
|
|
|
19
22
|
PUSH_DOWN_MS,
|
|
20
23
|
STALE_POLLS,
|
|
21
24
|
} from "./health.ts";
|
|
25
|
+
export {
|
|
26
|
+
byRoom,
|
|
27
|
+
type HomeDevice,
|
|
28
|
+
homeDevices,
|
|
29
|
+
type RoomDevices,
|
|
30
|
+
spotifyElsewhere,
|
|
31
|
+
} from "./devices.ts";
|
|
22
32
|
export { asOf, type Clock, realClock } from "./clock.ts";
|
|
23
|
-
export { ConnectorError } from "./connector.ts";
|
|
33
|
+
export { type ConnectorCode, ConnectorError } from "./connector.ts";
|
|
24
34
|
export {
|
|
25
35
|
bestWindow,
|
|
26
36
|
type EnergyNow,
|
package/src/node-fetch.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
2
2
|
import type { LoadOptions } from "./config.ts";
|
|
3
|
+
import { HUE_BRIDGE_ROOT_CA } from "./connectors/hue/ca.ts";
|
|
3
4
|
|
|
4
5
|
// `@khalido/home/node`: the fetches Hue and Enphase need under Node, where Bun's `tls` fetch option does not exist. Node's
|
|
5
6
|
// global `fetch` takes no CA, so these use undici's own `fetch` and `Agent` (one undici on both sides). Outside the
|
|
@@ -13,8 +14,11 @@ const through = (dispatcher: Agent) =>
|
|
|
13
14
|
{ ...init, dispatcher } as never,
|
|
14
15
|
)) as unknown as typeof fetch;
|
|
15
16
|
|
|
16
|
-
/** A `fetch` that trusts only `ca` and accepts only a certificate whose CN is `bridgeId`: Node's `bunPinnedFetch`. */
|
|
17
|
-
export function nodePinnedFetch(
|
|
17
|
+
/** A `fetch` that trusts only `ca` (Signify's roots when left out) and accepts only a certificate whose CN is `bridgeId`: Node's `bunPinnedFetch`. */
|
|
18
|
+
export function nodePinnedFetch(
|
|
19
|
+
bridgeId: string,
|
|
20
|
+
ca: string = HUE_BRIDGE_ROOT_CA,
|
|
21
|
+
): typeof fetch {
|
|
18
22
|
const checkServerIdentity = (
|
|
19
23
|
_host: string,
|
|
20
24
|
cert: { subject?: { CN?: string | string[] } },
|