@khalido/home 0.1.3 → 0.2.1
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 +56 -1
- package/README.md +7 -2
- package/dist/config.d.ts +5 -3
- package/dist/config.js +5 -1
- package/dist/connector.d.ts +2 -0
- package/dist/connector.js +3 -0
- package/dist/connectors/amber/client.d.ts +1 -1
- package/dist/connectors/amber/client.js +1 -1
- package/dist/connectors/amber/index.js +2 -2
- 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/enphase/client.d.ts +1 -1
- package/dist/connectors/enphase/client.js +1 -1
- package/dist/connectors/enphase/index.js +2 -1
- 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/events.d.ts +0 -2
- package/dist/connectors/hue/events.js +1 -2
- package/dist/connectors/hue/index.d.ts +15 -4
- package/dist/connectors/hue/index.js +27 -10
- 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/client.d.ts +41 -6
- package/dist/connectors/printers/client.js +81 -8
- package/dist/connectors/printers/codec.d.ts +17 -3
- package/dist/connectors/printers/codec.js +32 -8
- package/dist/connectors/printers/index.d.ts +2 -0
- package/dist/connectors/printers/index.js +4 -0
- package/dist/connectors/printers/raster.d.ts +30 -0
- package/dist/connectors/printers/raster.js +175 -0
- package/dist/connectors/printers/state.d.ts +2 -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 +1 -1
- package/dist/connectors/spotify/index.js +20 -3
- package/dist/connectors/up/client.d.ts +1 -1
- package/dist/connectors/up/client.js +1 -1
- package/dist/connectors/up/index.js +2 -2
- package/dist/connectors/weather/google.js +2 -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 +15 -7
- package/docs/config.md +11 -3
- package/docs/decisions.md +19 -0
- package/docs/hue.md +161 -0
- package/docs/ideas.md +2 -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/README.md +2 -0
- 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/enphase.ts +31 -5
- package/src/cli/hue.ts +147 -59
- package/src/cli/main.ts +43 -13
- package/src/cli/printers.ts +254 -3
- package/src/cli/probe.ts +21 -17
- package/src/cli/status.ts +61 -10
- package/src/config.ts +8 -2
- package/src/connector.ts +5 -0
- package/src/connectors/amber/README.md +1 -1
- package/src/connectors/amber/client.ts +1 -1
- package/src/connectors/amber/index.ts +2 -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/enphase/README.md +3 -1
- package/src/connectors/enphase/client.ts +1 -1
- package/src/connectors/enphase/index.ts +2 -1
- 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/events.ts +1 -4
- package/src/connectors/hue/index.ts +46 -13
- package/src/connectors/hue/schemas.ts +26 -0
- package/src/connectors/hue/state.ts +27 -1
- package/src/connectors/printers/README.md +9 -4
- package/src/connectors/printers/client.ts +186 -12
- package/src/connectors/printers/codec.ts +44 -10
- package/src/connectors/printers/index.ts +17 -0
- package/src/connectors/printers/raster.ts +215 -0
- package/src/connectors/printers/state.ts +2 -0
- package/src/connectors/spotify/README.md +10 -2
- package/src/connectors/spotify/changes.ts +38 -9
- package/src/connectors/spotify/index.ts +21 -2
- package/src/connectors/up/README.md +1 -1
- package/src/connectors/up/client.ts +1 -1
- package/src/connectors/up/index.ts +2 -2
- package/src/connectors/weather/google.ts +6 -2
- 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,59 @@ 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.1] - 2026-11-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **printers:** printing. `identifyPrinter`, `validateJob`, `printJob` and `cancelJob` over IPP, and `pwgRaster` for a printer that takes no PDF (8-bit grey PWG Raster). The snapshot's `Printer` gains `port` and `rp`, so a write can reach it. `home printers print <file.pdf> [printer]`, `identify` and `cancel`; `print --dry-run` has the printer validate the job and sends no job; a printer with no PDF needs poppler's `pdftoppm`. Live on a Brother HL-L2350DW.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **cli, enphase:** a stale `IQ_GATEWAY_HOST` no longer leaves `home enphase` refused for good: with the gateway's serial from the address cache, a failed read is followed by mDNS discovery, and the line says where the gateway moved. Without `IQ_GATEWAY_HOST` the address comes from the cache, then from mDNS.
|
|
16
|
+
- **amber, enphase, up:** a 403 is a rejected key (`unavailable`, `reason: "auth"`), as a 401 already was and as Hue and Google weather already read both. It was `disconnected` and retried forever. One check, `isRejectedKey` in `src/connector.ts`, now serves every connector and the CLI.
|
|
17
|
+
- **spotify:** after the first-play lowering ([spotify/soloist#23](https://github.com/spotify/soloist/issues/23)), `anomaly` no longer says only "lowered it" when the volume is back above `maxVolume`: it adds `it is back at <n>`. Soloist puts its own level back about 90 ms after `playing` when it goes active (live, 2026-11-11), and the cap leaves that level alone.
|
|
18
|
+
|
|
19
|
+
## [0.2.0] - 2026-11-12
|
|
20
|
+
|
|
21
|
+
### Breaking
|
|
22
|
+
|
|
23
|
+
- **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.
|
|
24
|
+
- **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>`.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **spotify:** the stall watch no longer looks at, or repairs, music playing on another device.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **cli:** `home room <name> <n>` and `home scene` print the room's line after the write, where they printed nothing.
|
|
33
|
+
- **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`.
|
|
34
|
+
- **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.
|
|
35
|
+
- **docs:** `castStatus` and `castFree` take the Cast part's state (`home.part("cast")?.state`), which the docs called "the snapshot".
|
|
36
|
+
- **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".
|
|
37
|
+
- **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".
|
|
38
|
+
- **cli:** "no route to host" on macOS now adds that the terminal may need the Local Network permission.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **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.
|
|
43
|
+
- **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.
|
|
44
|
+
- **`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.
|
|
45
|
+
- **config: `photos` on a room**, a list of paths. The library carries them and reads nothing.
|
|
46
|
+
- **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`.
|
|
47
|
+
- **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.
|
|
48
|
+
- **`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.
|
|
49
|
+
- **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 Den speaker`), and `spotifyText(state, on)` takes it. Tested on built snapshots only.
|
|
50
|
+
- **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"`.
|
|
51
|
+
- **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.
|
|
52
|
+
- **`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.
|
|
53
|
+
- **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.
|
|
54
|
+
- **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.
|
|
55
|
+
- **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).
|
|
56
|
+
- **`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.
|
|
57
|
+
- **`MissingTool`** from `@khalido/home/finders`: thrown when the discovery tool (`dns-sd` on macOS, `avahi-browse` on Linux) isn't installed.
|
|
58
|
+
- **`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.
|
|
59
|
+
|
|
7
60
|
## [0.1.3] - 2026-11-10
|
|
8
61
|
|
|
9
62
|
### Added
|
|
@@ -98,7 +151,9 @@ First release. Nine connectors: Hue, Daikin, Enphase, Google Cast, AirPrint prin
|
|
|
98
151
|
- **Agent skills** in `skills/`: `use-home` (reading safely, connecting, the rules for writes) and `draw-home` (an SVG plan of a home and its rooms). `llms.txt` points agents at the docs.
|
|
99
152
|
- **Node.** Hue and Enphase take the fetches in `@khalido/home/node`. The library runs on Bun 1.4+ and Node 24+; the `home` command needs Bun.
|
|
100
153
|
|
|
101
|
-
[Unreleased]: https://github.com/khalido/home.ts/compare/v0.1
|
|
154
|
+
[Unreleased]: https://github.com/khalido/home.ts/compare/v0.2.1...HEAD
|
|
155
|
+
[0.2.1]: https://github.com/khalido/home.ts/compare/v0.2.0...v0.2.1
|
|
156
|
+
[0.2.0]: https://github.com/khalido/home.ts/compare/v0.1.3...v0.2.0
|
|
102
157
|
[0.1.3]: https://github.com/khalido/home.ts/compare/v0.1.2...v0.1.3
|
|
103
158
|
[0.1.2]: https://github.com/khalido/home.ts/compare/v0.1.1...v0.1.2
|
|
104
159
|
[0.1.1]: https://github.com/khalido/home.ts/compare/v0.1.0...v0.1.1
|
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,
|
package/dist/connector.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export declare class ConnectorError extends Error {
|
|
|
23
23
|
cause?: unknown;
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
+
/** A 401 or 403 from a device or service: the key or token is wrong or revoked. Every connector reads it as `reason: "auth"`. */
|
|
27
|
+
export declare const isRejectedKey: (error: unknown) => boolean;
|
|
26
28
|
/** `fetch` that gives up after `timeoutMs`, or as soon as `signal` (a run's) aborts. Returns the response whatever its status. */
|
|
27
29
|
export declare function fetchTimeout(fetcher: typeof fetch, url: string | URL, init?: Omit<RequestInit, "signal">, options?: {
|
|
28
30
|
timeoutMs?: number;
|
package/dist/connector.js
CHANGED
|
@@ -24,6 +24,9 @@ export class ConnectorError extends Error {
|
|
|
24
24
|
this.code = options.code;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
/** A 401 or 403 from a device or service: the key or token is wrong or revoked. Every connector reads it as `reason: "auth"`. */
|
|
28
|
+
export const isRejectedKey = (error) => error instanceof ConnectorError &&
|
|
29
|
+
(error.status === 401 || error.status === 403);
|
|
27
30
|
/** `fetch` that gives up after `timeoutMs`, or as soon as `signal` (a run's) aborts. Returns the response whatever its status. */
|
|
28
31
|
export function fetchTimeout(fetcher, url, init = {}, options = {}) {
|
|
29
32
|
const timeout = AbortSignal.timeout(options.timeoutMs ?? TIMEOUT_MS);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ConnectorError } from "../../connector.ts";
|
|
3
|
-
/** A failed Amber call. `status` 401 means the key is wrong or revoked; 429 means the rate limit (50 per 5 min). */
|
|
3
|
+
/** A failed Amber call. `status` 401 or 403 (`isRejectedKey`) means the key is wrong or revoked; 429 means the rate limit (50 per 5 min). */
|
|
4
4
|
export declare class AmberError extends ConnectorError {
|
|
5
5
|
}
|
|
6
6
|
/** One 5-minute price interval on one channel. Prices are c/kWh including GST, as a cost: negative feed-in means you are paid. */
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { ConnectorError, getJson } from "../../connector.js";
|
|
3
3
|
// Amber Electric's public API v1 (https://app.amber.com.au/developers; the published spec is 1.0, there is no v2). Shapes checked against live replies,
|
|
4
4
|
// 2026-10-13; every field optional so a change degrades a reading rather than throwing.
|
|
5
|
-
/** A failed Amber call. `status` 401 means the key is wrong or revoked; 429 means the rate limit (50 per 5 min). */
|
|
5
|
+
/** A failed Amber call. `status` 401 or 403 (`isRejectedKey`) means the key is wrong or revoked; 429 means the rate limit (50 per 5 min). */
|
|
6
6
|
export class AmberError extends ConnectorError {
|
|
7
7
|
}
|
|
8
8
|
/** One 5-minute price interval on one channel. Prices are c/kWh including GST, as a cost: negative feed-in means you are paid. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { asOf, realClock } from "../../clock.js";
|
|
2
|
-
import { errorText } from "../../connector.js";
|
|
2
|
+
import { errorText, isRejectedKey } from "../../connector.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { envName } from "../../home.js";
|
|
5
5
|
import { energyNow } from "../../energy.js";
|
|
@@ -41,7 +41,7 @@ export function amber(config) {
|
|
|
41
41
|
catch (error) {
|
|
42
42
|
if (signal.aborted)
|
|
43
43
|
return; // stopped mid-read: not a failure
|
|
44
|
-
if (error
|
|
44
|
+
if (isRejectedKey(error))
|
|
45
45
|
return update({
|
|
46
46
|
status: "unavailable",
|
|
47
47
|
reason: "auth",
|
|
@@ -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)] : []),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConnectorError } from "../../connector.ts";
|
|
2
|
-
/** A failed Enphase call. `status` 401 means the token is wrong or expired. */
|
|
2
|
+
/** A failed Enphase call. `status` 401 or 403 (`isRejectedKey`) means the token is wrong or expired. */
|
|
3
3
|
export declare class EnphaseError extends ConnectorError {
|
|
4
4
|
}
|
|
5
5
|
/** Solar right now and today. Watts and watt-hours as the gateway reports them, negatives included. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ConnectorError, fetchTimeout, getJson } from "../../connector.js";
|
|
3
3
|
import { Login, Meters, Production, Readings, } from "./schemas.js";
|
|
4
|
-
/** A failed Enphase call. `status` 401 means the token is wrong or expired. */
|
|
4
|
+
/** A failed Enphase call. `status` 401 or 403 (`isRejectedKey`) means the token is wrong or expired. */
|
|
5
5
|
export class EnphaseError extends ConnectorError {
|
|
6
6
|
}
|
|
7
7
|
/** A daily counter equal to the lifetime one is the software 8 bug seen on KO's gateway (D8.3.5528): not today's energy. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { localDay, realClock } from "../../clock.js";
|
|
2
|
+
import { isRejectedKey } from "../../connector.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { deviceRecords, filePath, } from "../../home.js";
|
|
4
5
|
import { lostDetail, relocator } from "../../discover.js";
|
|
@@ -113,7 +114,7 @@ export function enphase(config) {
|
|
|
113
114
|
catch (error) {
|
|
114
115
|
if (signal.aborted)
|
|
115
116
|
return; // stopped mid-read: not a failure
|
|
116
|
-
if (error
|
|
117
|
+
if (isRejectedKey(error))
|
|
117
118
|
return update({
|
|
118
119
|
status: "unavailable",
|
|
119
120
|
reason: "auth",
|
|
@@ -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";
|