@khalido/home 0.1.3 → 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 +41 -0
- package/README.md +7 -2
- package/dist/config.d.ts +5 -3
- package/dist/config.js +5 -1
- package/dist/connectors/cast/index.d.ts +1 -1
- package/dist/connectors/cast/index.js +1 -1
- package/dist/connectors/cast/state.d.ts +17 -3
- package/dist/connectors/cast/state.js +26 -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 +2 -2
- package/dist/connectors/daikin/index.js +4 -3
- 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 +15 -4
- package/dist/connectors/hue/index.js +25 -8
- 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/spotify/changes.d.ts +19 -5
- package/dist/connectors/spotify/changes.js +29 -7
- package/dist/connectors/spotify/index.d.ts +1 -1
- package/dist/connectors/spotify/index.js +8 -2
- package/dist/devices.d.ts +24 -0
- package/dist/devices.js +56 -2
- 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/home.d.ts +11 -2
- package/dist/home.js +98 -34
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -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 +11 -3
- package/docs/decisions.md +12 -0
- package/docs/hue.md +161 -0
- package/docs/using.md +98 -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 +3 -3
- package/src/cli/cast.ts +51 -5
- package/src/cli/context.ts +2 -0
- package/src/cli/daikin.ts +71 -0
- 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 +61 -10
- package/src/config.ts +8 -2
- package/src/connectors/cast/README.md +6 -3
- package/src/connectors/cast/index.ts +7 -1
- package/src/connectors/cast/state.ts +32 -5
- package/src/connectors/daikin/README.md +16 -4
- package/src/connectors/daikin/client.ts +104 -3
- package/src/connectors/daikin/index.ts +8 -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 +44 -11
- package/src/connectors/hue/schemas.ts +26 -0
- package/src/connectors/hue/state.ts +27 -1
- package/src/connectors/spotify/README.md +8 -2
- package/src/connectors/spotify/changes.ts +38 -9
- package/src/connectors/spotify/index.ts +9 -1
- package/src/devices.ts +83 -3
- package/src/discover.ts +1 -2
- package/src/finders-entry.ts +1 -1
- package/src/finders.ts +18 -6
- package/src/home.ts +99 -39
- package/src/index.ts +7 -1
- package/src/node-fetch.ts +6 -2
package/.env.example
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
HUE_BRIDGE_HOST=hue-bridge.local # optional: found by mDNS (dns-sd) when unset
|
|
5
5
|
HUE_BRIDGE_ID=0017880000000000 # optional: the certificate's CN must match it; mDNS finds it when unset
|
|
6
6
|
HUE_APPLICATION_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # from `home pair`
|
|
7
|
-
HUE_CA_FILE=/path/to/hue-root-ca.pem #
|
|
8
|
-
DAIKIN_HOST=192.0.2.20 # the unit's address;
|
|
7
|
+
HUE_CA_FILE=/path/to/hue-root-ca.pem # optional: a root CA to trust instead of Signify's two, which the package carries
|
|
8
|
+
DAIKIN_HOST=192.0.2.20 # optional: the unit's address; found by UDP broadcast when unset (1.5 s a run)
|
|
9
9
|
IQ_GATEWAY_HOST=envoy.local # the IQ Gateway on the LAN (a scheme is tolerated)
|
|
10
10
|
IQ_GATEWAY_SERIAL_NUMBER=000000000000 # optional: for `home enphase login`; from the Enphase app: System > Devices > Gateway
|
|
11
11
|
ENPHASE_USERNAME=you@example.com # optional: for `home enphase login`, once a year: the Enphase account
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ All notable changes to home.ts (`@khalido/home` on npm), in [Keep a Changelog](h
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-11-12
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- **spotify: music on another Spotify device no longer reads as this speaker playing.** When Soloist says `playing` (or `buffering`) while it isn't the account's active device, `speakerState` now gives `elsewhere` (it gave `playing`), `spotifyText` says `silent · "Song" by Band is playing on another Spotify device`, and `spotifyChanges` treats it as silence: music moving off this speaker is `stopped`, music moving onto it is `started` then `track`, and nothing fires for its tracks or pauses meanwhile (it fired `resumed` with the box silent). `home devices --json` carries the new `state`. What to do: add `elsewhere` wherever you switch on `speakerState`; `state.status` is still what Soloist said.
|
|
12
|
+
- **cli: `home scene <name>` no longer takes the first match** when several rooms have a scene by that name: it lists them and exits 2, and `home scene <name> in <room>` picks one. A wrong or shared room or scene name is a usage error (exit 2), where it was exit 1. What to do: a script that recalls a shared scene name adds `in <room>`.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **spotify:** the stall watch no longer looks at, or repairs, music playing on another device.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **cli:** `home room <name> <n>` and `home scene` print the room's line after the write, where they printed nothing.
|
|
21
|
+
- **hue:** a light's line shows its white temperature or colour when on (`Desk: on · 40% · 2700 K`), in `home hue`, `home devices` and `homeDevices`.
|
|
22
|
+
- **docs, for a consumer:** `docs/using.md` has each write and what it resolves with in one table, an announce-where-free example, and what a part's `status` and `reason` and an error's `code` mean. `docs/config.md`'s `loadHome` call now passes what Cast needs (`finders`, `castConnect`). `llms.txt` says what to call.
|
|
23
|
+
- **docs:** `castStatus` and `castFree` take the Cast part's state (`home.part("cast")?.state`), which the docs called "the snapshot".
|
|
24
|
+
- **cli:** `home` and `home daikin` find the Daikin by UDP broadcast when `DAIKIN_HOST` is unset, as the write commands do, so `DAIKIN_HOST` is optional. With no unit on the network it reads "not set up".
|
|
25
|
+
- **cli:** `home cast volume | mute | stop` print the device's line once it reports the change, where they printed nothing; after 2 s without it, "sent; the device has not reported it yet".
|
|
26
|
+
- **cli:** "no route to host" on macOS now adds that the terminal may need the Local Network permission.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Dry run on Hue's and the Daikin's writes**: `{ dryRun: true }` as the last argument of `light`, `room`, `scene`, `allOff`, `power` and `mode`. The read and every check made before sending run, and no write is sent; the result's `wouldSend` is the request it would make (`HueRequest`, `DaikinRequest` with `sets`), `state` how things stand, `confirmed: false`. `--dry-run` on the same commands (`home light | room | scene | lights off`, `home hue …`, `home daikin …`); every other command refuses the flag.
|
|
31
|
+
- **hue: `client.allOff()`**: every light in the home off with one write, resolving `{ state, confirmed }` with every light as the bridge then reported it. `home lights off` uses it and now prints the result.
|
|
32
|
+
- **`byRoom(snapshot, config)`** from `@khalido/home`, for a map of the home: each room of the config with its devices, `noData` for a connector the config places there that has nothing to list, and `unplaced` for the rest.
|
|
33
|
+
- **config: `photos` on a room**, a list of paths. The library carries them and reads nothing.
|
|
34
|
+
- **cast: `castFreeSpeakers(state)`**: every free speaker and display in one call, a whole group where all of it is free, never a TV; `free` goes straight to `announceAll`.
|
|
35
|
+
- **cast: `castFree(state, targets, { forms })`**: with `forms: ["speaker", "display"]` a TV is left out (`why: "form"`, only when `forms` is given), and a group holding one becomes its other free members.
|
|
36
|
+
- **`home.add(integration)` and `home.remove(name)`** on the home `createHome` returns: add one while the rest keep running (a bridge paired just now), replace one by name (the old run has ended before the new one starts; how an `unavailable` part comes back after a re-key), or stop one and drop its part. `start` now also waits for those added.
|
|
37
|
+
- **spotify: `playingElsewhere(state)`**, and **`spotifyElsewhere(snapshot)`** from `@khalido/home`: the names of the Cast devices and groups playing that track in Spotify, matched by the track's name since Soloist doesn't say where the music is. Empty for a phone or a laptop. `homeDevices` and `home devices` put it in the speaker's line (`silent · "Song" is playing on Front room speaker`), and `spotifyText(state, on)` takes it. Tested on built snapshots only.
|
|
38
|
+
- **hue: lights, rooms and scenes by name.** `client.light(name, change)`, `client.room(name, change)` and `client.scene(name, room?)`, each resolving `{ state, confirmed }` (`HueApplied`). `change` (`LightChange`) is `{ on?, brightness?, color?, kelvin?, transitionMs? }`: a colour as hex or `{ r, g, b }`, sent as `xy` inside the light's own gamut; a white temperature in kelvin, checked against the light's range. An unknown name or a value the light can't take throws `HueError` with `code: "invalid"` before anything is sent, and a light the bridge has lost throws with `code: "offline"`.
|
|
39
|
+
- **hue: colour in the state.** Each light's `kelvin` and `kelvinRange`, and on a colour light its `xy`, `hex` and `gamut`. `colorToXy`, `xyToHex`, `kelvinToMirek`, `mirekToKelvin` and `hueText` are exported.
|
|
40
|
+
- **`home light <name> …`**, and `home room` and `home scene` now take a brightness, a temperature (`2700k`) and a colour (`ff8800`) together, name a scene's room (`in <room>`), and print what the bridge then reported.
|
|
41
|
+
- **hue: Signify's root certificates are in the package**, as `HUE_BRIDGE_ROOT_CA` from `@khalido/home/hue`: both roots, the bundle from Signify's "Using HTTPS" page. `ca` is now optional everywhere it was required: `hue({ host, bridgeId, key })`, `bunPinnedFetch(bridgeId)`, `nodePinnedFetch(bridgeId)`, the config's `ca` and the CLI's `HUE_CA_FILE`. One given still replaces the bundle. `home probe` reads the bridge without setup, and `home discover --write` no longer writes a `ca` line.
|
|
42
|
+
- **hue: `pairWhenPressed`**: `pair`, asked every 2 s for 30 s until the bridge's button is pressed, with `onWaiting` for the moment to prompt. `pair` takes a `signal`. `home pair` now waits 60 s for the button, counting down, and says when no press was detected.
|
|
43
|
+
- **daikin: `client.power(on)` and `client.mode(mode, targetC?)`**, resolving with `{ state, confirmed }` (`DaikinApplied`) once the unit reports what was asked, or after 5 s (`CONFIRM_MS`) without it. `mode` sends the mode, power on and the setpoint as one request: the one given, else the library's defaults, `DEFAULT_TARGET_C` (cool 25, heat 27).
|
|
44
|
+
- **`home daikin on | off`** (power only, as it was last left) and **`home daikin cool | heat | auto [°C] | dry | fan`**, each printing what the unit then reports.
|
|
45
|
+
- **`MissingTool`** from `@khalido/home/finders`: thrown when the discovery tool (`dns-sd` on macOS, `avahi-browse` on Linux) isn't installed.
|
|
46
|
+
- **`home devices`:** every device the connectors read, one line each from `homeDevices`: its state, its room and name from the config when there is one, and why when it isn't available. `--json` prints the records. A connector that isn't connected is named on stderr.
|
|
47
|
+
|
|
7
48
|
## [0.1.3] - 2026-11-10
|
|
8
49
|
|
|
9
50
|
### Added
|
package/README.md
CHANGED
|
@@ -13,7 +13,9 @@ Small, typed home automation for TypeScript, starting with the devices in my hou
|
|
|
13
13
|
bun add @khalido/home # or: npm install @khalido/home
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
The library runs on Bun 1.4+ and Node 24+, and is ESM only. The `home` command needs Bun
|
|
16
|
+
The library runs on Bun 1.4+ and Node 24+, and is ESM only. The `home` command needs Bun on your `PATH`; in a project that added the package, run it as `bunx home`.
|
|
17
|
+
|
|
18
|
+
Pointing a coding agent at it? Give it [llms.txt](llms.txt): what to call and where each doc is. [Using it from an app](docs/using.md) is the same for people.
|
|
17
19
|
|
|
18
20
|
## Try it
|
|
19
21
|
|
|
@@ -26,6 +28,7 @@ bun link # puts `home` on your PATH
|
|
|
26
28
|
home discover # what answers on this network, and which of it this package supports
|
|
27
29
|
home probe # read-only checks, no keys
|
|
28
30
|
home # each connector: connected, or what it still needs
|
|
31
|
+
home devices # every device, one line each
|
|
29
32
|
```
|
|
30
33
|
|
|
31
34
|
Copy [.env.example](.env.example) to `.env` and fill in the connectors you have, or describe the home in a [config file](docs/config.md) ([example](examples/home.jsonc)).
|
|
@@ -66,9 +69,11 @@ Each connector's README says what has run against a real device and what has not
|
|
|
66
69
|
|
|
67
70
|
## More
|
|
68
71
|
|
|
72
|
+
- [Using it from an app](docs/using.md): the wiring, each write and what it returns
|
|
69
73
|
- [The `home` command](docs/cli.md) and [the home config](docs/config.md)
|
|
74
|
+
- [Hue](docs/hue.md): Signify's guidance on certificates, pairing and write rates
|
|
70
75
|
- [Why it is shaped this way](docs/philosophy.md) and [decisions](docs/decisions.md)
|
|
71
|
-
- To add a connector, follow [AGENTS.md](AGENTS.md)
|
|
76
|
+
- To add a connector, follow [AGENTS.md](https://github.com/khalido/home.ts/blob/main/AGENTS.md)
|
|
72
77
|
- For agents: [llms.txt](llms.txt)
|
|
73
78
|
|
|
74
79
|
I use it from jaldi, a Mac launcher, and chota-bot, a family bot on a Raspberry Pi, both on Bun; the docs name both.
|
package/dist/config.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export declare const Room: z.ZodObject<{
|
|
|
16
16
|
name: z.ZodString;
|
|
17
17
|
floor: z.ZodOptional<z.ZodString>;
|
|
18
18
|
hueRoom: z.ZodOptional<z.ZodString>;
|
|
19
|
+
photos: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
19
20
|
}, z.core.$loose>;
|
|
20
21
|
/** JSONC (comments and trailing commas) through jsonc-parser, which runs on Node too; a syntax error throws rather than parsing to part of the file. */
|
|
21
22
|
export declare function parseJsonc(text: string): unknown;
|
|
@@ -25,7 +26,7 @@ export declare const Device: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
25
26
|
host: z.ZodString;
|
|
26
27
|
bridgeId: z.ZodString;
|
|
27
28
|
key: z.ZodString;
|
|
28
|
-
ca: z.ZodString
|
|
29
|
+
ca: z.ZodOptional<z.ZodString>;
|
|
29
30
|
id: z.ZodString;
|
|
30
31
|
room: z.ZodOptional<z.ZodString>;
|
|
31
32
|
name: z.ZodOptional<z.ZodString>;
|
|
@@ -123,13 +124,14 @@ export declare const HomeConfig: z.ZodObject<{
|
|
|
123
124
|
name: z.ZodString;
|
|
124
125
|
floor: z.ZodOptional<z.ZodString>;
|
|
125
126
|
hueRoom: z.ZodOptional<z.ZodString>;
|
|
127
|
+
photos: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
126
128
|
}, z.core.$loose>>>;
|
|
127
129
|
devices: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
128
130
|
connector: z.ZodLiteral<"hue">;
|
|
129
131
|
host: z.ZodString;
|
|
130
132
|
bridgeId: z.ZodString;
|
|
131
133
|
key: z.ZodString;
|
|
132
|
-
ca: z.ZodString
|
|
134
|
+
ca: z.ZodOptional<z.ZodString>;
|
|
133
135
|
id: z.ZodString;
|
|
134
136
|
room: z.ZodOptional<z.ZodString>;
|
|
135
137
|
name: z.ZodOptional<z.ZodString>;
|
|
@@ -246,7 +248,7 @@ export type LoadOptions = {
|
|
|
246
248
|
fetch?: typeof fetch;
|
|
247
249
|
/** Hue's pinned fetch and Enphase's unverified one: Bun's by default, `NODE_TLS` from `@khalido/home/node` under Node. */
|
|
248
250
|
tls?: {
|
|
249
|
-
hue(bridgeId: string, ca
|
|
251
|
+
hue(bridgeId: string, ca?: string): typeof fetch;
|
|
250
252
|
enphase(): typeof fetch;
|
|
251
253
|
};
|
|
252
254
|
/** BOM's transport: `ftpFetch()` from `@khalido/home/weather/ftp`. Without it a listed `bom` source stops and says why. */
|
package/dist/config.js
CHANGED
|
@@ -25,6 +25,10 @@ export const Room = z.looseObject({
|
|
|
25
25
|
.string()
|
|
26
26
|
.optional()
|
|
27
27
|
.describe("the Hue room, zone or grouped_light id whose lights this room's marker shows"),
|
|
28
|
+
photos: z
|
|
29
|
+
.array(filePath)
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("photos of the room, as paths relative to the config or from ~/; the library never reads them, a drawing agent or an app does"),
|
|
28
32
|
});
|
|
29
33
|
/** JSONC (comments and trailing commas) through jsonc-parser, which runs on Node too; a syntax error throws rather than parsing to part of the file. */
|
|
30
34
|
export function parseJsonc(text) {
|
|
@@ -266,7 +270,7 @@ export function loadHome(input, options) {
|
|
|
266
270
|
function one(d, at) {
|
|
267
271
|
switch (d.connector) {
|
|
268
272
|
case "hue": {
|
|
269
|
-
const ca = file(d.ca);
|
|
273
|
+
const ca = d.ca === undefined ? undefined : file(d.ca);
|
|
270
274
|
return hue({
|
|
271
275
|
...start("hue", d.bridgeId, d.host),
|
|
272
276
|
bridgeId: d.bridgeId,
|
|
@@ -7,7 +7,7 @@ import { type CastConnect } from "./codec.ts";
|
|
|
7
7
|
import { type CastDevice, type CastSnapshot, type Resolved, type CastForm, type CastStatus } from "./state.ts";
|
|
8
8
|
import { type CastActions, type ServeClip } from "./writes.ts";
|
|
9
9
|
export type { CastConnect } from "./codec.ts";
|
|
10
|
-
export { castDevices, castForm, castFree, castStatus } from "./state.ts";
|
|
10
|
+
export { castDevices, castForm, castFree, castFreeSpeakers, castStatus, } from "./state.ts";
|
|
11
11
|
export type { CastDevice, CastForm, CastSkipped, CastSnapshot, CastStatus, Resolved, } from "./state.ts";
|
|
12
12
|
export type { AnnounceResult, CastActions, CastClip, ServeClip, } from "./writes.ts";
|
|
13
13
|
/** A Cast device or speaker group in a home config. No host: a group's moves with its leader, so every address comes from discovery. */
|
|
@@ -8,7 +8,7 @@ import { NS } from "./codec.js";
|
|
|
8
8
|
import { CastPayload, KNOWN_TYPES } from "./schemas.js";
|
|
9
9
|
import { castKey, connection, found, mediaApp, reduce, castDevices, castStatus, } from "./state.js";
|
|
10
10
|
import { castActions, } from "./writes.js";
|
|
11
|
-
export { castDevices, castForm, castFree, castStatus } from "./state.js";
|
|
11
|
+
export { castDevices, castForm, castFree, castFreeSpeakers, castStatus, } from "./state.js";
|
|
12
12
|
/** A Cast device or speaker group in a home config. No host: a group's moves with its leader, so every address comes from discovery. */
|
|
13
13
|
export const configSchema = z.looseObject({
|
|
14
14
|
connector: z.literal("cast"),
|
|
@@ -97,10 +97,10 @@ export type CastStatus = {
|
|
|
97
97
|
};
|
|
98
98
|
/** Every device and group in the snapshot as a `CastStatus`, devices first, each by name. */
|
|
99
99
|
export declare function castStatus(s: CastSnapshot): CastStatus[];
|
|
100
|
-
/** A target `castFree` left out: `standby` is a TV that is off, which a clip may wake. */
|
|
100
|
+
/** A target `castFree` left out: `standby` is a TV that is off, which a clip may wake; `form` is one whose form wasn't asked for. */
|
|
101
101
|
export type CastSkipped = {
|
|
102
102
|
name: string;
|
|
103
|
-
why: "busy" | "offline" | "standby" | "unknown";
|
|
103
|
+
why: "busy" | "offline" | "standby" | "unknown" | "form";
|
|
104
104
|
app?: string;
|
|
105
105
|
};
|
|
106
106
|
/**
|
|
@@ -111,8 +111,22 @@ export type CastSkipped = {
|
|
|
111
111
|
* becomes its free members, named one by one (they then play out of step with each other).
|
|
112
112
|
* - A device is never named beside a free group that holds it, nor two groups that share a member: the second becomes its
|
|
113
113
|
* members not yet covered.
|
|
114
|
+
* - With `forms` (`["speaker", "display"]` keeps an announcement off TVs), a device of another form, or of a model this
|
|
115
|
+
* library doesn't know, is skipped with `why: "form"`, and a group holding one (or a member the snapshot doesn't have)
|
|
116
|
+
* becomes its other free members. An empty list leaves every device out.
|
|
114
117
|
*/
|
|
115
|
-
export declare function castFree(s: CastSnapshot, targets: string[]
|
|
118
|
+
export declare function castFree(s: CastSnapshot, targets: string[], options?: {
|
|
119
|
+
forms?: readonly CastForm[];
|
|
120
|
+
}): {
|
|
121
|
+
free: string[];
|
|
122
|
+
skipped: CastSkipped[];
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Where an announcement can play right now, in one call: every speaker and display that is free, a whole group where all
|
|
126
|
+
* of it is, and never a TV. `castFree` over everything Cast has found, with `forms: ["speaker", "display"]`; `free` goes
|
|
127
|
+
* straight to `announceAll`.
|
|
128
|
+
*/
|
|
129
|
+
export declare const castFreeSpeakers: (s: CastSnapshot) => {
|
|
116
130
|
free: string[];
|
|
117
131
|
skipped: CastSkipped[];
|
|
118
132
|
};
|
|
@@ -216,8 +216,18 @@ export function castStatus(s) {
|
|
|
216
216
|
* becomes its free members, named one by one (they then play out of step with each other).
|
|
217
217
|
* - A device is never named beside a free group that holds it, nor two groups that share a member: the second becomes its
|
|
218
218
|
* members not yet covered.
|
|
219
|
+
* - With `forms` (`["speaker", "display"]` keeps an announcement off TVs), a device of another form, or of a model this
|
|
220
|
+
* library doesn't know, is skipped with `why: "form"`, and a group holding one (or a member the snapshot doesn't have)
|
|
221
|
+
* becomes its other free members. An empty list leaves every device out.
|
|
219
222
|
*/
|
|
220
|
-
export function castFree(s, targets) {
|
|
223
|
+
export function castFree(s, targets, options = {}) {
|
|
224
|
+
/** A device of a form asked for. A group has no form of its own: its members decide. */
|
|
225
|
+
const wanted = (id) => {
|
|
226
|
+
const d = s.devices[id];
|
|
227
|
+
if (!options.forms || d?.kind === "group")
|
|
228
|
+
return true;
|
|
229
|
+
return d?.form !== undefined && options.forms.includes(d.form);
|
|
230
|
+
};
|
|
221
231
|
const status = new Map(castStatus(s).map((c) => [c.id, c]));
|
|
222
232
|
const free = new Map();
|
|
223
233
|
const covered = new Set(); // members of the groups in `free`
|
|
@@ -225,14 +235,16 @@ export function castFree(s, targets) {
|
|
|
225
235
|
const ok = (c) => (c.state === "idle" || c.state === "restorable") && !c.standby;
|
|
226
236
|
const skip = (c) => skipped.set(c.id, {
|
|
227
237
|
name: c.name,
|
|
228
|
-
why: c.
|
|
229
|
-
? "
|
|
230
|
-
: c.
|
|
231
|
-
? "
|
|
232
|
-
: "busy"
|
|
238
|
+
why: !wanted(c.id)
|
|
239
|
+
? "form"
|
|
240
|
+
: c.state === "offline"
|
|
241
|
+
? "offline"
|
|
242
|
+
: c.standby && c.state !== "busy"
|
|
243
|
+
? "standby"
|
|
244
|
+
: "busy",
|
|
233
245
|
...(c.app ? { app: c.app } : {}),
|
|
234
246
|
});
|
|
235
|
-
const device = (c) =>
|
|
247
|
+
const device = (c) => ok(c) && wanted(c.id) ? free.set(c.id, c) : skip(c);
|
|
236
248
|
for (const target of targets) {
|
|
237
249
|
const d = findCast(s.devices, target);
|
|
238
250
|
const c = d && status.get(d.id);
|
|
@@ -248,6 +260,7 @@ export function castFree(s, targets) {
|
|
|
248
260
|
skip(c);
|
|
249
261
|
else if (own !== "offline" &&
|
|
250
262
|
up.every(ok) &&
|
|
263
|
+
(d.members ?? []).every(wanted) &&
|
|
251
264
|
!members.some((m) => covered.has(m.id))) {
|
|
252
265
|
free.set(c.id, c);
|
|
253
266
|
for (const m of members)
|
|
@@ -265,3 +278,9 @@ export function castFree(s, targets) {
|
|
|
265
278
|
skipped: [...skipped.values()],
|
|
266
279
|
};
|
|
267
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Where an announcement can play right now, in one call: every speaker and display that is free, a whole group where all
|
|
283
|
+
* of it is, and never a TV. `castFree` over everything Cast has found, with `forms: ["speaker", "display"]`; `free` goes
|
|
284
|
+
* straight to `announceAll`.
|
|
285
|
+
*/
|
|
286
|
+
export const castFreeSpeakers = (s) => castFree(s, castDevices(s).map((d) => d.id), { forms: ["speaker", "display"] });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type
|
|
2
|
+
import { type Clock } from "../../clock.ts";
|
|
3
3
|
import { ConnectorError } from "../../connector.ts";
|
|
4
4
|
/** A failed Daikin call. `status` is the HTTP status, or the dsiot `rsc` when HTTP succeeded. */
|
|
5
5
|
export declare class DaikinError extends ConnectorError {
|
|
@@ -23,6 +23,41 @@ type Node = {
|
|
|
23
23
|
pch?: Node[];
|
|
24
24
|
};
|
|
25
25
|
declare const Node: z.ZodType<Node>;
|
|
26
|
+
/**
|
|
27
|
+
* The setpoint a mode gets when the caller names none: this library's opinion (KO's: not too cold, properly warm).
|
|
28
|
+
* Without one, "cool" would mean whatever the remote last left. Auto keeps the unit's own.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DEFAULT_TARGET_C: {
|
|
31
|
+
readonly cool: 25;
|
|
32
|
+
readonly heat: 27;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* What a write left: the state once the unit reported the change (`confirmed`), or as it stood when `CONFIRM_MS` ran out
|
|
36
|
+
* without it. With `{ dryRun: true }` nothing is sent: `wouldSend` is the request and what it sets, `state` the unit as it
|
|
37
|
+
* reads now, and `confirmed` is false. Check `wouldSend` first.
|
|
38
|
+
*/
|
|
39
|
+
export type DaikinApplied = {
|
|
40
|
+
state: DaikinState;
|
|
41
|
+
confirmed: boolean;
|
|
42
|
+
wouldSend?: DaikinRequest;
|
|
43
|
+
};
|
|
44
|
+
/** One write: `POST /dsiot/multireq` with `body`; `sets` is what it changes, in this library's words. */
|
|
45
|
+
export type DaikinRequest = {
|
|
46
|
+
method: "POST";
|
|
47
|
+
path: "/dsiot/multireq";
|
|
48
|
+
body: WriteRequest;
|
|
49
|
+
sets: {
|
|
50
|
+
on?: boolean;
|
|
51
|
+
mode?: Mode;
|
|
52
|
+
targetC?: number;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/** `dryRun`: every check a write makes (the mode, the setpoint's range), one read for the state, and nothing sent. */
|
|
56
|
+
export type DaikinWriteOptions = {
|
|
57
|
+
dryRun?: boolean;
|
|
58
|
+
};
|
|
59
|
+
/** How long `power` and `mode` wait for the unit to report what was asked, reading once a second. */
|
|
60
|
+
export declare const CONFIRM_MS = 5000;
|
|
26
61
|
/** Little-endian hex to an integer, two's complement when `signed`: "2700" → 39, "EEFF" → -18. */
|
|
27
62
|
export declare function hexLE(hex: string, signed?: boolean): number | undefined;
|
|
28
63
|
/** Map the indoor and outdoor `dgc_status` trees to a state. Unknown codes go to `anomaly`, never to a guess. */
|
|
@@ -48,7 +83,7 @@ export declare const setMode: (mode: Mode) => WriteRequest;
|
|
|
48
83
|
export declare function setTarget(c: number, mode: Mode): WriteRequest;
|
|
49
84
|
/** Set the fan speed of `mode` (usually the current one). */
|
|
50
85
|
export declare function setFan(fan: Fan, mode: Mode): WriteRequest;
|
|
51
|
-
/** A client for one unit. Reads are op 2; `write` sends
|
|
86
|
+
/** A client for one unit. Reads are op 2. `power` and `mode` are the writes most callers want; `write` sends any body built by setPower, setMode, setTarget or setFan. */
|
|
52
87
|
export declare function daikinClient(options: {
|
|
53
88
|
host: string;
|
|
54
89
|
fetch: typeof fetch;
|
|
@@ -61,5 +96,13 @@ export declare function daikinClient(options: {
|
|
|
61
96
|
* unconfirmed; re-read to be sure). Writes waiting their turn merge (`mergeWrites`) and resolve with the send that carried them.
|
|
62
97
|
*/
|
|
63
98
|
write(request: WriteRequest): Promise<void>;
|
|
99
|
+
/** Turn the unit on or off, as it was last left. Only when a person asks. Resolves with what the unit then reported. */
|
|
100
|
+
power: (on: boolean, options?: DaikinWriteOptions) => Promise<DaikinApplied>;
|
|
101
|
+
/**
|
|
102
|
+
* Select a mode, turn the unit on and set its setpoint, in one request: `targetC`, else `DEFAULT_TARGET_C` (cool 25,
|
|
103
|
+
* heat 27), else (auto) the unit's own. Dry and fan take none, and a `targetC` for them throws before anything is
|
|
104
|
+
* sent, as one off the unit's range does. Only when a person asks. Resolves with what the unit then reported.
|
|
105
|
+
*/
|
|
106
|
+
mode(mode: Mode, targetC?: number, options?: DaikinWriteOptions): Promise<DaikinApplied>;
|
|
64
107
|
};
|
|
65
108
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { realClock } from "../../clock.js";
|
|
2
3
|
import { ConnectorError, getJson } from "../../connector.js";
|
|
3
4
|
import { writeLane } from "../../lane.js";
|
|
4
5
|
// Daikin's JSON dsiot API (the BRP084 family): POST /dsiot/multireq, plain HTTP, no key. Field paths and value
|
|
@@ -56,6 +57,16 @@ const TARGET_RANGE = {
|
|
|
56
57
|
heat: [10, 30],
|
|
57
58
|
auto: [18, 30],
|
|
58
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* The setpoint a mode gets when the caller names none: this library's opinion (KO's: not too cold, properly warm).
|
|
62
|
+
* Without one, "cool" would mean whatever the remote last left. Auto keeps the unit's own.
|
|
63
|
+
*/
|
|
64
|
+
export const DEFAULT_TARGET_C = {
|
|
65
|
+
cool: 25,
|
|
66
|
+
heat: 27,
|
|
67
|
+
};
|
|
68
|
+
/** How long `power` and `mode` wait for the unit to report what was asked, reading once a second. */
|
|
69
|
+
export const CONFIRM_MS = 5000;
|
|
59
70
|
/** Little-endian hex to an integer, two's complement when `signed`: "2700" → 39, "EEFF" → -18. */
|
|
60
71
|
export function hexLE(hex, signed = false) {
|
|
61
72
|
if (!/^(?:[0-9a-f]{2})+$/i.test(hex))
|
|
@@ -173,7 +184,7 @@ export function setFan(fan, mode) {
|
|
|
173
184
|
throw new DaikinError(`${mode} mode has no fan speed`);
|
|
174
185
|
return write([["e_3001", FAN[mode], key(FANS, fan)]]);
|
|
175
186
|
}
|
|
176
|
-
/** A client for one unit. Reads are op 2; `write` sends
|
|
187
|
+
/** A client for one unit. Reads are op 2. `power` and `mode` are the writes most callers want; `write` sends any body built by setPower, setMode, setTarget or setFan. */
|
|
177
188
|
export function daikinClient(options) {
|
|
178
189
|
const lane = writeLane({
|
|
179
190
|
gapMs: WRITE_GAP_MS,
|
|
@@ -190,7 +201,8 @@ export function daikinClient(options) {
|
|
|
190
201
|
schema: Multi,
|
|
191
202
|
error: DaikinError,
|
|
192
203
|
});
|
|
193
|
-
|
|
204
|
+
const clock = options.clock ?? realClock;
|
|
205
|
+
const client = {
|
|
194
206
|
/** The indoor and outdoor status in one request. A failed outdoor read leaves `outsideC` empty. */
|
|
195
207
|
async read(signal) {
|
|
196
208
|
const m = await multireq({
|
|
@@ -218,4 +230,57 @@ export function daikinClient(options) {
|
|
|
218
230
|
}, mergeWrites);
|
|
219
231
|
},
|
|
220
232
|
};
|
|
233
|
+
/** Send `request`, then read once a second until the unit reports `asked` or `CONFIRM_MS` passes. Rejects if the write is refused, or if no read after it succeeds. */
|
|
234
|
+
async function apply(request, sets, asked, options) {
|
|
235
|
+
if (options?.dryRun)
|
|
236
|
+
return {
|
|
237
|
+
state: await client.read(),
|
|
238
|
+
confirmed: false,
|
|
239
|
+
wouldSend: {
|
|
240
|
+
method: "POST",
|
|
241
|
+
path: "/dsiot/multireq",
|
|
242
|
+
body: request,
|
|
243
|
+
sets,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
await client.write(request);
|
|
247
|
+
const until = clock.now() + CONFIRM_MS;
|
|
248
|
+
let state;
|
|
249
|
+
let failed;
|
|
250
|
+
// The write was accepted, so one dropped read must not turn it into a failure: keep reading until the time is up.
|
|
251
|
+
for (;;) {
|
|
252
|
+
try {
|
|
253
|
+
state = await client.read();
|
|
254
|
+
if (asked(state))
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
failed = error;
|
|
259
|
+
}
|
|
260
|
+
if (clock.now() >= until)
|
|
261
|
+
break;
|
|
262
|
+
await clock.sleep(1000);
|
|
263
|
+
}
|
|
264
|
+
if (!state)
|
|
265
|
+
throw failed; // sent, but the unit answered no read since: nothing to report
|
|
266
|
+
return { state, confirmed: asked(state) };
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
...client,
|
|
270
|
+
/** Turn the unit on or off, as it was last left. Only when a person asks. Resolves with what the unit then reported. */
|
|
271
|
+
power: (on, options) => apply(setPower(on), { on }, (s) => s.on === on, options),
|
|
272
|
+
/**
|
|
273
|
+
* Select a mode, turn the unit on and set its setpoint, in one request: `targetC`, else `DEFAULT_TARGET_C` (cool 25,
|
|
274
|
+
* heat 27), else (auto) the unit's own. Dry and fan take none, and a `targetC` for them throws before anything is
|
|
275
|
+
* sent, as one off the unit's range does. Only when a person asks. Resolves with what the unit then reported.
|
|
276
|
+
*/
|
|
277
|
+
async mode(mode, targetC, options) {
|
|
278
|
+
const c = targetC ?? DEFAULT_TARGET_C[mode];
|
|
279
|
+
return apply(c === undefined
|
|
280
|
+
? setMode(mode)
|
|
281
|
+
: mergeWrites(setMode(mode), setTarget(c, mode)), { on: true, mode, ...(c === undefined ? {} : { targetC: c }) }, (s) => s.on === true &&
|
|
282
|
+
s.mode === mode &&
|
|
283
|
+
(c === undefined || s.targetC === c), options);
|
|
284
|
+
},
|
|
285
|
+
};
|
|
221
286
|
}
|
|
@@ -4,7 +4,7 @@ import { type DeviceRecord, type Integration, type Part } from "../../home.ts";
|
|
|
4
4
|
import { type Finder } from "../../discover.ts";
|
|
5
5
|
import type { Manifest } from "../../manifest.ts";
|
|
6
6
|
import { daikinClient, type DaikinState, type Mode } from "./client.ts";
|
|
7
|
-
export { DaikinError, type DaikinState, daikinClient, type Fan, type Mode, setFan, setMode, setPower, setTarget, type WriteRequest, } from "./client.ts";
|
|
7
|
+
export { CONFIRM_MS, type DaikinApplied, type DaikinRequest, type DaikinWriteOptions, DaikinError, type DaikinState, DEFAULT_TARGET_C, daikinClient, type Fan, type Mode, setFan, setMode, setPower, setTarget, type WriteRequest, } from "./client.ts";
|
|
8
8
|
/** Poll every 120 s, as Home Assistant's daikin integration does (its coordinator's update_interval). */
|
|
9
9
|
export declare const POLL_MS = 120000;
|
|
10
10
|
/** A Daikin AC in a home config: its last known address, and the MAC its UDP discovery reports, which is how it is recognised. */
|
|
@@ -30,7 +30,7 @@ export type DaikinSnapshot = DaikinState & {
|
|
|
30
30
|
/** What `daikin().client` is: the reads and writes of the unit, at wherever it is now. */
|
|
31
31
|
export type DaikinClient = ReturnType<typeof daikinClient>;
|
|
32
32
|
/**
|
|
33
|
-
* One Daikin AC as a home integration: one read now, then every 120 s. Its writes are `client.
|
|
33
|
+
* One Daikin AC as a home integration: one read now, then every 120 s. Its writes are `client.power(on)` and `client.mode(mode, targetC?)`: one
|
|
34
34
|
* client for the integration's life, so the writes share its lane and follow the unit when it moves.
|
|
35
35
|
*/
|
|
36
36
|
export declare function daikin(config: DaikinConfig): Integration<DaikinSnapshot, "daikin"> & {
|
|
@@ -4,7 +4,7 @@ import { deviceRecords, } from "../../home.js";
|
|
|
4
4
|
import { lostDetail, relocator } from "../../discover.js";
|
|
5
5
|
import { poller } from "../../poll.js";
|
|
6
6
|
import { daikinClient, DaikinError, } from "./client.js";
|
|
7
|
-
export { DaikinError, daikinClient, setFan, setMode, setPower, setTarget, } from "./client.js";
|
|
7
|
+
export { CONFIRM_MS, DaikinError, DEFAULT_TARGET_C, daikinClient, setFan, setMode, setPower, setTarget, } from "./client.js";
|
|
8
8
|
/** Poll every 120 s, as Home Assistant's daikin integration does (its coordinator's update_interval). */
|
|
9
9
|
export const POLL_MS = 120_000;
|
|
10
10
|
/** A Daikin AC in a home config: its last known address, and the MAC its UDP discovery reports, which is how it is recognised. */
|
|
@@ -17,7 +17,7 @@ export const configSchema = z.looseObject({
|
|
|
17
17
|
.describe("the unit's MAC as its UDP discovery reports it: its identity"),
|
|
18
18
|
});
|
|
19
19
|
/**
|
|
20
|
-
* One Daikin AC as a home integration: one read now, then every 120 s. Its writes are `client.
|
|
20
|
+
* One Daikin AC as a home integration: one read now, then every 120 s. Its writes are `client.power(on)` and `client.mode(mode, targetC?)`: one
|
|
21
21
|
* client for the integration's life, so the writes share its lane and follow the unit when it moves.
|
|
22
22
|
*/
|
|
23
23
|
export function daikin(config) {
|
|
@@ -102,7 +102,8 @@ export const manifest = {
|
|
|
102
102
|
{
|
|
103
103
|
name: "DAIKIN_HOST",
|
|
104
104
|
example: "192.0.2.20",
|
|
105
|
-
|
|
105
|
+
optional: true,
|
|
106
|
+
what: "the unit's address; found by UDP broadcast when unset (1.5 s a run)",
|
|
106
107
|
},
|
|
107
108
|
],
|
|
108
109
|
lines: ({ state }) => (state ? [acText(state)] : []),
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Signify's root certificates for Hue bridges (PEM, two certificates): the default `ca` everywhere one is taken. */
|
|
2
|
+
export declare const HUE_BRIDGE_ROOT_CA = "-----BEGIN CERTIFICATE-----\nMIICMjCCAdigAwIBAgIUO7FSLbaxikuXAljzVaurLXWmFw4wCgYIKoZIzj0EAwIw\nOTELMAkGA1UEBhMCTkwxFDASBgNVBAoMC1BoaWxpcHMgSHVlMRQwEgYDVQQDDAty\nb290LWJyaWRnZTAiGA8yMDE3MDEwMTAwMDAwMFoYDzIwMzgwMTE5MDMxNDA3WjA5\nMQswCQYDVQQGEwJOTDEUMBIGA1UECgwLUGhpbGlwcyBIdWUxFDASBgNVBAMMC3Jv\nb3QtYnJpZGdlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjNw2tx2AplOf9x86\naTdvEcL1FU65QDxziKvBpW9XXSIcibAeQiKxegpq8Exbr9v6LBnYbna2VcaK0G22\njOKkTqOBuTCBtjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNV\nHQ4EFgQUZ2ONTFrDT6o8ItRnKfqWKnHFGmQwdAYDVR0jBG0wa4AUZ2ONTFrDT6o8\nItRnKfqWKnHFGmShPaQ7MDkxCzAJBgNVBAYTAk5MMRQwEgYDVQQKDAtQaGlsaXBz\nIEh1ZTEUMBIGA1UEAwwLcm9vdC1icmlkZ2WCFDuxUi22sYpLlwJY81Wrqy11phcO\nMAoGCCqGSM49BAMCA0gAMEUCIEBYYEOsa07TH7E5MJnGw557lVkORgit2Rm1h3B2\nsFgDAiEA1Fj/C3AN5psFMjo0//mrQebo0eKd3aWRx+pQY08mk48=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBzDCCAXOgAwIBAgICEAAwCgYIKoZIzj0EAwIwPDELMAkGA1UEBhMCTkwxFDAS\nBgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTAgFw0y\nNTAyMjUwMDAwMDBaGA8yMDUwMTIzMTIzNTk1OVowPDELMAkGA1UEBhMCTkwxFDAS\nBgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTBZMBMG\nByqGSM49AgEGCCqGSM49AwEHA0IABFfOO0jfSAUXGQ9kjEDzyBrcMQ3ItyA5krE+\ncyvb1Y3xFti7KlAad8UOnAx0FBLn7HZrlmIwm1QnX0fK3LPM13mjYzBhMB0GA1Ud\nDgQWBBTF1pSpsCASX/z0VHLigxU2CAaqoTAfBgNVHSMEGDAWgBTF1pSpsCASX/z0\nVHLigxU2CAaqoTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAKBggq\nhkjOPQQDAgNHADBEAiAk7duT+IHbOGO4UUuGLAEpyYejGZK9Z7V9oSfnvuQ5BQIg\nIYSgwwxHXm73/JgcU9lAM6c8Bmu3UE3kBIUwBs1qXFw=\n-----END CERTIFICATE-----\n";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Signify's two root certificates for Hue bridges, so pinning needs no file: `bunPinnedFetch(bridgeId)` and
|
|
2
|
+
// `nodePinnedFetch(bridgeId)` trust these and nothing else. Public certificates, as Signify's "Using HTTPS" page gives
|
|
3
|
+
// them (login-gated); these bytes are the ones openHAB, hb-hue-tools, openhue-go and Raycast's Hue extension ship.
|
|
4
|
+
//
|
|
5
|
+
// CN=root-bridge, O=Philips Hue to 2038-01-19 the one in use: KO's square bridge (BSB002) chains to it
|
|
6
|
+
// SHA-256 F0:BD:8E:65:09:E8:2F:77:4D:63:BC:00:9D:53:88:C9:69:FE:3D:CF:7D:6D:54:1D:63:51:B7:2B:89:8D:8A:CF
|
|
7
|
+
// CN=Hue Root CA 01, O=Signify Hue to 2050-12-31 Signify's spare, likely behind an intermediate: untried here
|
|
8
|
+
// SHA-256 D8:B8:94:48:B2:AF:8E:16:76:18:5A:C0:72:19:EE:9D:CB:C8:F0:1C:12:2A:02:6A:2A:4B:7B:5C:FE:03:28:B8
|
|
9
|
+
//
|
|
10
|
+
// test/hue.test.ts checks both fingerprints, so an edit here fails.
|
|
11
|
+
/** Signify's root certificates for Hue bridges (PEM, two certificates): the default `ca` everywhere one is taken. */
|
|
12
|
+
export const HUE_BRIDGE_ROOT_CA = `-----BEGIN CERTIFICATE-----
|
|
13
|
+
MIICMjCCAdigAwIBAgIUO7FSLbaxikuXAljzVaurLXWmFw4wCgYIKoZIzj0EAwIw
|
|
14
|
+
OTELMAkGA1UEBhMCTkwxFDASBgNVBAoMC1BoaWxpcHMgSHVlMRQwEgYDVQQDDAty
|
|
15
|
+
b290LWJyaWRnZTAiGA8yMDE3MDEwMTAwMDAwMFoYDzIwMzgwMTE5MDMxNDA3WjA5
|
|
16
|
+
MQswCQYDVQQGEwJOTDEUMBIGA1UECgwLUGhpbGlwcyBIdWUxFDASBgNVBAMMC3Jv
|
|
17
|
+
b3QtYnJpZGdlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjNw2tx2AplOf9x86
|
|
18
|
+
aTdvEcL1FU65QDxziKvBpW9XXSIcibAeQiKxegpq8Exbr9v6LBnYbna2VcaK0G22
|
|
19
|
+
jOKkTqOBuTCBtjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNV
|
|
20
|
+
HQ4EFgQUZ2ONTFrDT6o8ItRnKfqWKnHFGmQwdAYDVR0jBG0wa4AUZ2ONTFrDT6o8
|
|
21
|
+
ItRnKfqWKnHFGmShPaQ7MDkxCzAJBgNVBAYTAk5MMRQwEgYDVQQKDAtQaGlsaXBz
|
|
22
|
+
IEh1ZTEUMBIGA1UEAwwLcm9vdC1icmlkZ2WCFDuxUi22sYpLlwJY81Wrqy11phcO
|
|
23
|
+
MAoGCCqGSM49BAMCA0gAMEUCIEBYYEOsa07TH7E5MJnGw557lVkORgit2Rm1h3B2
|
|
24
|
+
sFgDAiEA1Fj/C3AN5psFMjo0//mrQebo0eKd3aWRx+pQY08mk48=
|
|
25
|
+
-----END CERTIFICATE-----
|
|
26
|
+
-----BEGIN CERTIFICATE-----
|
|
27
|
+
MIIBzDCCAXOgAwIBAgICEAAwCgYIKoZIzj0EAwIwPDELMAkGA1UEBhMCTkwxFDAS
|
|
28
|
+
BgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTAgFw0y
|
|
29
|
+
NTAyMjUwMDAwMDBaGA8yMDUwMTIzMTIzNTk1OVowPDELMAkGA1UEBhMCTkwxFDAS
|
|
30
|
+
BgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTBZMBMG
|
|
31
|
+
ByqGSM49AgEGCCqGSM49AwEHA0IABFfOO0jfSAUXGQ9kjEDzyBrcMQ3ItyA5krE+
|
|
32
|
+
cyvb1Y3xFti7KlAad8UOnAx0FBLn7HZrlmIwm1QnX0fK3LPM13mjYzBhMB0GA1Ud
|
|
33
|
+
DgQWBBTF1pSpsCASX/z0VHLigxU2CAaqoTAfBgNVHSMEGDAWgBTF1pSpsCASX/z0
|
|
34
|
+
VHLigxU2CAaqoTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAKBggq
|
|
35
|
+
hkjOPQQDAgNHADBEAiAk7duT+IHbOGO4UUuGLAEpyYejGZK9Z7V9oSfnvuQ5BQIg
|
|
36
|
+
IYSgwwxHXm73/JgcU9lAM6c8Bmu3UE3kBIUwBs1qXFw=
|
|
37
|
+
-----END CERTIFICATE-----
|
|
38
|
+
`;
|