homebridge-dyson-solarcycle-morph 0.1.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/CHANGELOG.md +52 -0
- package/LICENSE +22 -0
- package/README.md +259 -0
- package/config.schema.json +118 -0
- package/dist/accessory.d.ts +41 -0
- package/dist/accessory.js +129 -0
- package/dist/accessory.js.map +1 -0
- package/dist/cli/pair.d.ts +2 -0
- package/dist/cli/pair.js +144 -0
- package/dist/cli/pair.js.map +1 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -0
- package/dist/dyson/cloud.d.ts +67 -0
- package/dist/dyson/cloud.js +147 -0
- package/dist/dyson/cloud.js.map +1 -0
- package/dist/dyson/credentials.d.ts +32 -0
- package/dist/dyson/credentials.js +73 -0
- package/dist/dyson/credentials.js.map +1 -0
- package/dist/dyson/crypto.d.ts +38 -0
- package/dist/dyson/crypto.js +98 -0
- package/dist/dyson/crypto.js.map +1 -0
- package/dist/dyson/debounce.d.ts +33 -0
- package/dist/dyson/debounce.js +59 -0
- package/dist/dyson/debounce.js.map +1 -0
- package/dist/dyson/lamp.d.ts +193 -0
- package/dist/dyson/lamp.js +692 -0
- package/dist/dyson/lamp.js.map +1 -0
- package/dist/dyson/protocol.d.ts +91 -0
- package/dist/dyson/protocol.js +156 -0
- package/dist/dyson/protocol.js.map +1 -0
- package/dist/dyson/queue.d.ts +24 -0
- package/dist/dyson/queue.js +51 -0
- package/dist/dyson/queue.js.map +1 -0
- package/dist/dyson/reconcile.d.ts +38 -0
- package/dist/dyson/reconcile.js +30 -0
- package/dist/dyson/reconcile.js.map +1 -0
- package/dist/dyson/settle.d.ts +30 -0
- package/dist/dyson/settle.js +56 -0
- package/dist/dyson/settle.js.map +1 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +20 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +27 -0
- package/dist/platform.js +106 -0
- package/dist/platform.js.map +1 -0
- package/dist/settings.d.ts +4 -0
- package/dist/settings.js +5 -0
- package/dist/settings.js.map +1 -0
- package/docs/PROTOCOL.md +153 -0
- package/homebridge-ui/public/index.html +396 -0
- package/homebridge-ui/server.js +297 -0
- package/package.json +68 -0
- package/tools/ble-probe.js +132 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] — 2026-09-17
|
|
9
|
+
|
|
10
|
+
First release. Verified end to end against a Solarcycle Morph desk light
|
|
11
|
+
(model CD06) on Raspberry Pi OS Bookworm, BlueZ 5.66, Homebridge 2.4.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- A settings page in the Homebridge UI that scans for nearby lights, authorises
|
|
16
|
+
a MyDyson account and pairs them, replacing the need to run anything by hand.
|
|
17
|
+
- Credentials are stored in the plugin's storage directory rather than in
|
|
18
|
+
`config.json`, so the file the UI displays holds nothing sensitive. Setting
|
|
19
|
+
`ltk` and `accountId` in the config still works and takes precedence.
|
|
20
|
+
- Signal strength is logged on connect, with a warning below −80 dBm — the point
|
|
21
|
+
where BLE supervision timeouts make a link drop repeatedly.
|
|
22
|
+
|
|
23
|
+
- HomeKit lightbulb with on/off, brightness and colour temperature (2700–6500 K).
|
|
24
|
+
- Optional motion sensor from the lamp's built-in detector.
|
|
25
|
+
- Offline LTK re-authentication, run on every connect. No network access after
|
|
26
|
+
pairing.
|
|
27
|
+
- `dyson-morph-pair`, the same flow from a terminal, for setups without the
|
|
28
|
+
Homebridge UI.
|
|
29
|
+
- `tools/ble-probe.js`, a standalone GATT dump for inspecting a lamp without
|
|
30
|
+
installing the plugin.
|
|
31
|
+
- Reconnect with backoff, state followed through notifications, and the lamp
|
|
32
|
+
reported as unreachable in HomeKit while there is no link.
|
|
33
|
+
- Power commands are checked and resent if they did not land. Brightness and
|
|
34
|
+
colour temperature are not: the lamp ramps and trims them itself, so a reading
|
|
35
|
+
that differs from the request is usually the lamp working.
|
|
36
|
+
|
|
37
|
+
### Requirements
|
|
38
|
+
|
|
39
|
+
- Homebridge 2.x. The plugin is ESM, matching Homebridge 2's own module format.
|
|
40
|
+
|
|
41
|
+
### Notes on the protocol
|
|
42
|
+
|
|
43
|
+
Probing real hardware contradicted the published protocol notes in two places,
|
|
44
|
+
both handled here rather than assumed away:
|
|
45
|
+
|
|
46
|
+
- Characteristics are spread across three GATT services, not gathered under one.
|
|
47
|
+
Discovery sweeps every service and matches on characteristic UUID.
|
|
48
|
+
- Acknowledged writes are described as mandatory, but this lamp advertises no
|
|
49
|
+
`write` flag at all and unacknowledged writes do take effect. The write mode
|
|
50
|
+
is taken from each characteristic's advertised flags.
|
|
51
|
+
|
|
52
|
+
See [docs/PROTOCOL.md](docs/PROTOCOL.md).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oliver Rummeyer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# homebridge-dyson-solarcycle-morph
|
|
2
|
+
|
|
3
|
+
Homebridge plugin for the **Dyson Solarcycle Morph** and **Lightcycle Morph**
|
|
4
|
+
task lights (model CD06).
|
|
5
|
+
|
|
6
|
+
These lamps have no Wi-Fi. Everything goes over Bluetooth LE, so the machine
|
|
7
|
+
running Homebridge has to be within Bluetooth range of the lamp — there is no
|
|
8
|
+
cloud path to fall back on.
|
|
9
|
+
|
|
10
|
+
Exposes a HomeKit lightbulb with on/off, brightness and colour temperature
|
|
11
|
+
(2700–6500 K), plus the lamp's built-in motion sensor if you want it.
|
|
12
|
+
|
|
13
|
+
## Before you start
|
|
14
|
+
|
|
15
|
+
**Your lamp must already be registered in the MyDyson app, on the account you
|
|
16
|
+
are going to log in with.** This is not optional and it is worth explaining why,
|
|
17
|
+
because it is the one thing that can stop you dead.
|
|
18
|
+
|
|
19
|
+
The lamp ignores every command until a challenge-response handshake proves you
|
|
20
|
+
hold its *long-term key* (LTK). That key is issued by Dyson, tied to the account
|
|
21
|
+
the lamp is registered to, and there is no way to derive it from the device
|
|
22
|
+
itself. Pairing here fetches it once from the Dyson cloud. If the lamp is not on
|
|
23
|
+
your account, the endpoint returns 404 and there is nothing this plugin can do.
|
|
24
|
+
|
|
25
|
+
The only other way to get an LTK is Dyson's "fresh pairing" handshake, which
|
|
26
|
+
needs a physical button press on the lamp *and* a working cloud session — this
|
|
27
|
+
plugin does not implement it. So: open the MyDyson app, make sure the lamp is
|
|
28
|
+
there, then continue.
|
|
29
|
+
|
|
30
|
+
Everything after pairing is local. The plugin never touches the network again.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- A Linux host with BlueZ — a Raspberry Pi running Homebridge is the usual case.
|
|
35
|
+
This uses [`node-ble`](https://github.com/chrvadala/node-ble), which talks to
|
|
36
|
+
BlueZ over D-Bus, so **macOS and Windows will not work**.
|
|
37
|
+
- Node.js 20.19+ / 22.12+ / 24+.
|
|
38
|
+
- Homebridge 2.x.
|
|
39
|
+
|
|
40
|
+
Developed and verified against a Solarcycle Morph desk light on Raspberry Pi OS
|
|
41
|
+
Bookworm, BlueZ 5.66, Homebridge 2.4.
|
|
42
|
+
|
|
43
|
+
## 1. Find the lamp
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
sudo bluetoothctl --timeout 15 scan le
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Look for a device whose name is a Dyson serial, something like
|
|
50
|
+
`ABC-EU-XXXXXXXX`. **The lamp advertises its serial number as its Bluetooth
|
|
51
|
+
name**, so this one command gives you both values you need:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
bluetoothctl info AA:BB:CC:DD:EE:FF
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
Device AA:BB:CC:DD:EE:FF (random)
|
|
59
|
+
Name: ABC-EU-XXXXXXXX ← serial number
|
|
60
|
+
UUID: Vendor specific (2dd10010-1c37-452d-8979-d1b4a787d0a4)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
That `2dd10010` service UUID confirms it is a Dyson light speaking the protocol
|
|
64
|
+
this plugin implements.
|
|
65
|
+
|
|
66
|
+
## 2. Install
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
sudo npm install -g homebridge-dyson-solarcycle-morph
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
On a Homebridge installation managed by `hb-service` (the official Raspberry Pi
|
|
73
|
+
image and apt package), plugins live in the Homebridge storage directory rather
|
|
74
|
+
than in npm's global prefix. Install it there instead:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
cd /var/lib/homebridge
|
|
78
|
+
sudo -u homebridge /opt/homebridge/bin/npm install homebridge-dyson-solarcycle-morph
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Installing into the wrong prefix is a quiet failure: npm reports success and
|
|
82
|
+
Homebridge never sees the plugin.
|
|
83
|
+
|
|
84
|
+
## 3. Pair, in the Homebridge UI
|
|
85
|
+
|
|
86
|
+
Open the plugin's settings. The page walks through three things:
|
|
87
|
+
|
|
88
|
+
1. **Scan for lights.** Finds nearby Dyson lights and adds them to the
|
|
89
|
+
configuration, filling in the address and serial number for you. You can skip
|
|
90
|
+
this and type them by hand if you already did step 1.
|
|
91
|
+
2. **Authorise your MyDyson account.** Enter your email, password and country,
|
|
92
|
+
request a code, and enter the code Dyson emails you.
|
|
93
|
+
3. **Check the result.** Paired lights are listed with the date they were
|
|
94
|
+
authorised, and can be un-paired again.
|
|
95
|
+
|
|
96
|
+
Save the configuration and restart Homebridge.
|
|
97
|
+
|
|
98
|
+
**The key never enters `config.json`.** It is written to the plugin's own
|
|
99
|
+
storage directory, so the file you can open in the UI — and paste into a support
|
|
100
|
+
thread — contains nothing sensitive.
|
|
101
|
+
|
|
102
|
+
### Without the Homebridge UI
|
|
103
|
+
|
|
104
|
+
`dyson-morph-pair` does the same thing from a terminal:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
dyson-morph-pair --serial ABC-EU-XXXXXXXX --country DE
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Pass `--storage` if your Homebridge storage directory is not `~/.homebridge`,
|
|
111
|
+
and `--debug` to see every request and response.
|
|
112
|
+
|
|
113
|
+
**Run it in a real terminal.** It reads from stdin interactively, so launching
|
|
114
|
+
it from an editor, an agent or a CI shell prints the prompts but never receives
|
|
115
|
+
your answers — every field arrives empty and Dyson rejects the login with a
|
|
116
|
+
misleading `401 Unable to authenticate user`.
|
|
117
|
+
|
|
118
|
+
## 4. Configure
|
|
119
|
+
|
|
120
|
+
Scanning writes this for you; by hand it looks like:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"platform": "DysonSolarcycleMorph",
|
|
125
|
+
"lights": [
|
|
126
|
+
{
|
|
127
|
+
"name": "Desk Light",
|
|
128
|
+
"mac": "AA:BB:CC:DD:EE:FF",
|
|
129
|
+
"serial": "ABC-EU-XXXXXXXX",
|
|
130
|
+
"motionSensor": true
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| Field | |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `name` | What the lamp is called in HomeKit. |
|
|
139
|
+
| `mac` | BLE address from the scan. |
|
|
140
|
+
| `serial` | Identifies the accessory to HomeKit and keys its stored credentials. |
|
|
141
|
+
| `motionSensor` | Expose the lamp's motion detector as a HomeKit sensor. Default `false`. |
|
|
142
|
+
|
|
143
|
+
Credentials are not configured here. For an unattended setup you may still set
|
|
144
|
+
`ltk` and `accountId` explicitly, and they then take precedence over anything
|
|
145
|
+
stored — supply both or neither.
|
|
146
|
+
|
|
147
|
+
**Running it as a child bridge is recommended.** Bluetooth links drop and
|
|
148
|
+
reconnect; in a child bridge that churn stays isolated from your other
|
|
149
|
+
accessories. In the Homebridge UI it is one toggle under the plugin's settings.
|
|
150
|
+
|
|
151
|
+
## Troubleshooting
|
|
152
|
+
|
|
153
|
+
**`le-connection-abort-by-local` a few times at startup.** Expected. BlueZ
|
|
154
|
+
commonly aborts the first two or three attempts before one sticks. The plugin
|
|
155
|
+
retries with backoff (2 s, 5 s, 15 s, 30 s, then 60 s) and normally connects
|
|
156
|
+
within a minute. Only worry if it never succeeds.
|
|
157
|
+
|
|
158
|
+
**It connects, drops after a few seconds, and reconnects, over and over.**
|
|
159
|
+
Usually the radio link, not the software. The plugin logs the signal strength
|
|
160
|
+
when it connects and warns below about −80 dBm; at that level BLE hits its
|
|
161
|
+
supervision timeout and the link dies. Check it directly with:
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
sudo bluetoothctl info AA:BB:CC:DD:EE:FF | grep RSSI
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
−60 dBm is comfortable, −75 is workable, −85 will not hold. Move the lamp or the
|
|
168
|
+
Homebridge host closer to each other, or put a Bluetooth adapter nearer the lamp.
|
|
169
|
+
|
|
170
|
+
**It keeps failing with `le-connection-abort-by-local` and never connects.**
|
|
171
|
+
The lamp's Bluetooth stack can reach a state where it advertises normally — a
|
|
172
|
+
scan finds it, at a good signal strength — but refuses every connection.
|
|
173
|
+
**Disconnecting the lamp from power for ten seconds clears it**, reliably, and
|
|
174
|
+
nothing on the Homebridge side does. The plugin keeps retrying and thins the
|
|
175
|
+
attempts out to five minutes apart, so it will pick the lamp up again by itself
|
|
176
|
+
once you have done that.
|
|
177
|
+
|
|
178
|
+
**Everything reports `Operation Not Authorized`.** The handshake did not
|
|
179
|
+
complete. Look for `Handshake with … complete` in the log.
|
|
180
|
+
|
|
181
|
+
**`… is not paired yet`.** No key is stored for that serial. Open the plugin's
|
|
182
|
+
settings in the Homebridge UI and authorise your account there. Check the serial
|
|
183
|
+
matches the lamp exactly — it is what the stored key is filed under.
|
|
184
|
+
|
|
185
|
+
**`PayloadB failed HMAC verification`.** The stored LTK is wrong or corrupted.
|
|
186
|
+
Re-run `dyson-morph-pair`.
|
|
187
|
+
|
|
188
|
+
**404 when fetching the LTK.** The lamp is not registered to that Dyson account.
|
|
189
|
+
See *Before you start*.
|
|
190
|
+
|
|
191
|
+
**`Not authorized` from D-Bus, or nothing connects at all.** Only relevant if
|
|
192
|
+
your distribution restricts BlueZ access — Debian Bookworm and Raspberry Pi OS
|
|
193
|
+
already allow it for all users, so this is usually *not* your problem. If it is,
|
|
194
|
+
create `/etc/dbus-1/system.d/node-ble.conf`, replacing `homebridge` with the
|
|
195
|
+
user Homebridge runs as, then `sudo systemctl restart dbus`:
|
|
196
|
+
|
|
197
|
+
```xml
|
|
198
|
+
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
|
|
199
|
+
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
|
200
|
+
<busconfig>
|
|
201
|
+
<policy user="homebridge">
|
|
202
|
+
<allow own="org.bluez"/>
|
|
203
|
+
<allow send_destination="org.bluez"/>
|
|
204
|
+
<allow send_interface="org.bluez.GattCharacteristic1"/>
|
|
205
|
+
<allow send_interface="org.bluez.GattDescriptor1"/>
|
|
206
|
+
<allow send_interface="org.freedesktop.DBus.ObjectManager"/>
|
|
207
|
+
<allow send_interface="org.freedesktop.DBus.Properties"/>
|
|
208
|
+
</policy>
|
|
209
|
+
</busconfig>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**The Home app shows the lamp as "No Response".** There is no Bluetooth link
|
|
213
|
+
right now. Commands are not queued while that is the case: they fail, so you
|
|
214
|
+
know they did not happen. The plugin reconnects on its own and reads the lamp's
|
|
215
|
+
real state when it does.
|
|
216
|
+
|
|
217
|
+
**The brightness I set is not exactly what the lamp shows.** Expected. The lamp
|
|
218
|
+
ramps towards a value rather than jumping, and trims it to track daylight from
|
|
219
|
+
its location and the time. Only power is checked and resent, because it is the
|
|
220
|
+
only value whose outcome is unambiguous — see
|
|
221
|
+
[docs/PROTOCOL.md](docs/PROTOCOL.md).
|
|
222
|
+
|
|
223
|
+
**Daylight mode changed at the lamp is not reflected.** The lamp lets you write
|
|
224
|
+
that setting but not read it back, so the plugin cannot know. It re-asserts
|
|
225
|
+
manual mode before writes to keep control predictable.
|
|
226
|
+
|
|
227
|
+
### Inspecting the lamp yourself
|
|
228
|
+
|
|
229
|
+
`tools/ble-probe.js` dumps every service, characteristic, flag and notification,
|
|
230
|
+
with no build step and no plugin install:
|
|
231
|
+
|
|
232
|
+
```sh
|
|
233
|
+
npm install node-ble
|
|
234
|
+
node tools/ble-probe.js AA:BB:CC:DD:EE:FF
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Useful for checking a model this plugin has not been tried against.
|
|
238
|
+
|
|
239
|
+
## Protocol
|
|
240
|
+
|
|
241
|
+
See [docs/PROTOCOL.md](docs/PROTOCOL.md) for the GATT layout, the message
|
|
242
|
+
framing and the handshake, including where a real lamp disagreed with the
|
|
243
|
+
published notes.
|
|
244
|
+
|
|
245
|
+
Reverse-engineered from the MyDyson Android app by S-Termi and documented in
|
|
246
|
+
[cmgrayb/hass-dyson](https://github.com/cmgrayb/hass-dyson). This plugin is an
|
|
247
|
+
independent TypeScript implementation, cross-checked against that reference with
|
|
248
|
+
`npm test`.
|
|
249
|
+
|
|
250
|
+
## Development
|
|
251
|
+
|
|
252
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the build, how to try a change on
|
|
253
|
+
real hardware, and how releases are published.
|
|
254
|
+
|
|
255
|
+
Not affiliated with or endorsed by Dyson.
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "DysonSolarcycleMorph",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": true,
|
|
5
|
+
"headerDisplay": "Controls the Bluetooth-only Dyson Solarcycle / Lightcycle Morph.\n\n**The lamp must be registered in the MyDyson app.** Its key is issued by Dyson against the account the lamp belongs to and cannot be read from the device itself.",
|
|
6
|
+
"schema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"name": {
|
|
10
|
+
"title": "Name",
|
|
11
|
+
"type": "string",
|
|
12
|
+
"default": "Dyson Solarcycle Morph",
|
|
13
|
+
"required": true,
|
|
14
|
+
"description": "Shown in the Homebridge log in front of this plugin\u2019s messages."
|
|
15
|
+
},
|
|
16
|
+
"adapter": {
|
|
17
|
+
"title": "Bluetooth adapter",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"required": false,
|
|
20
|
+
"placeholder": "hci0",
|
|
21
|
+
"description": "Leave empty to use the system default adapter."
|
|
22
|
+
},
|
|
23
|
+
"lights": {
|
|
24
|
+
"title": "Lights",
|
|
25
|
+
"type": "array",
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"name": {
|
|
30
|
+
"title": "Name",
|
|
31
|
+
"type": "string",
|
|
32
|
+
"required": true,
|
|
33
|
+
"default": "Dyson Morph"
|
|
34
|
+
},
|
|
35
|
+
"mac": {
|
|
36
|
+
"title": "BLE MAC address",
|
|
37
|
+
"type": "string",
|
|
38
|
+
"required": true,
|
|
39
|
+
"placeholder": "AA:BB:CC:DD:EE:FF",
|
|
40
|
+
"pattern": "^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$"
|
|
41
|
+
},
|
|
42
|
+
"serial": {
|
|
43
|
+
"title": "Serial number",
|
|
44
|
+
"type": "string",
|
|
45
|
+
"required": true,
|
|
46
|
+
"description": "Printed on the lamp base and shown in the MyDyson app."
|
|
47
|
+
},
|
|
48
|
+
"motionSensor": {
|
|
49
|
+
"title": "Expose the motion sensor",
|
|
50
|
+
"type": "boolean",
|
|
51
|
+
"default": false,
|
|
52
|
+
"description": "Adds the lamp's built-in motion detector as a HomeKit motion sensor."
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"dysonAccount": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"properties": {
|
|
60
|
+
"email": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"title": "MyDyson email"
|
|
63
|
+
},
|
|
64
|
+
"password": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"title": "MyDyson password"
|
|
67
|
+
},
|
|
68
|
+
"country": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"title": "Country",
|
|
71
|
+
"maxLength": 2,
|
|
72
|
+
"default": "GB"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"customUi": true,
|
|
79
|
+
"customUiPath": "./homebridge-ui",
|
|
80
|
+
"footerDisplay": "Lamp keys are stored in the plugin's storage directory, never in config.json. Your MyDyson password is only needed while authorising \u2014 clear it afterwards.",
|
|
81
|
+
"layout": [
|
|
82
|
+
"name",
|
|
83
|
+
{
|
|
84
|
+
"type": "fieldset",
|
|
85
|
+
"title": "Lights",
|
|
86
|
+
"expandable": true,
|
|
87
|
+
"expanded": true,
|
|
88
|
+
"items": [
|
|
89
|
+
{
|
|
90
|
+
"key": "lights",
|
|
91
|
+
"type": "array",
|
|
92
|
+
"add": "Add light",
|
|
93
|
+
"items": [
|
|
94
|
+
{
|
|
95
|
+
"type": "div",
|
|
96
|
+
"displayFlex": true,
|
|
97
|
+
"flex-direction": "column",
|
|
98
|
+
"items": [
|
|
99
|
+
"lights[].name",
|
|
100
|
+
"lights[].mac",
|
|
101
|
+
"lights[].serial",
|
|
102
|
+
"lights[].motionSensor"
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"type": "fieldset",
|
|
111
|
+
"title": "Advanced",
|
|
112
|
+
"expandable": true,
|
|
113
|
+
"items": [
|
|
114
|
+
"adapter"
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PlatformAccessory } from 'homebridge';
|
|
2
|
+
import type { ResolvedLightConfig } from './config.js';
|
|
3
|
+
import type { MorphPlatform } from './platform.js';
|
|
4
|
+
/**
|
|
5
|
+
* Bridges one lamp's BLE session to its HomeKit services.
|
|
6
|
+
*
|
|
7
|
+
* Writes are optimistic — HomeKit's 10-second characteristic timeout is shorter
|
|
8
|
+
* than a BLE reconnect, so a command issued while the lamp is away would
|
|
9
|
+
* otherwise surface as an error in the Home app. Instead we let the lamp's own
|
|
10
|
+
* state events correct us once it comes back.
|
|
11
|
+
*/
|
|
12
|
+
export declare class MorphAccessory {
|
|
13
|
+
private readonly platform;
|
|
14
|
+
private readonly accessory;
|
|
15
|
+
private readonly config;
|
|
16
|
+
private readonly lightbulb;
|
|
17
|
+
private readonly motion?;
|
|
18
|
+
private readonly lamp;
|
|
19
|
+
constructor(platform: MorphPlatform, accessory: PlatformAccessory, config: ResolvedLightConfig);
|
|
20
|
+
start(): Promise<void>;
|
|
21
|
+
stop(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Answer only while the lamp is actually reachable.
|
|
24
|
+
*
|
|
25
|
+
* Without this HomeKit keeps showing whatever was last known — a lamp that
|
|
26
|
+
* has been unreachable for minutes still reads as on at 100%, which is worse
|
|
27
|
+
* than saying nothing. Throwing a communication failure is how an accessory
|
|
28
|
+
* reports itself as unavailable, and the Home app shows "No Response".
|
|
29
|
+
*/
|
|
30
|
+
private live;
|
|
31
|
+
/** Push "No Response" to every characteristic at once. */
|
|
32
|
+
private markUnreachable;
|
|
33
|
+
/**
|
|
34
|
+
* Apply a write, reporting failure to HomeKit rather than swallowing it.
|
|
35
|
+
*
|
|
36
|
+
* A command that cannot be delivered is not retried later, so the Home app
|
|
37
|
+
* has to say so — otherwise it shows the new value as though it had taken
|
|
38
|
+
* effect and the user has no way to tell that nothing happened.
|
|
39
|
+
*/
|
|
40
|
+
private handleSet;
|
|
41
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describeError } from './errors.js';
|
|
2
|
+
import { DysonMorphLamp } from './dyson/lamp.js';
|
|
3
|
+
import { kelvinToMired, miredToKelvin, MAX_KELVIN, MIN_KELVIN } from './dyson/protocol.js';
|
|
4
|
+
/**
|
|
5
|
+
* Bridges one lamp's BLE session to its HomeKit services.
|
|
6
|
+
*
|
|
7
|
+
* Writes are optimistic — HomeKit's 10-second characteristic timeout is shorter
|
|
8
|
+
* than a BLE reconnect, so a command issued while the lamp is away would
|
|
9
|
+
* otherwise surface as an error in the Home app. Instead we let the lamp's own
|
|
10
|
+
* state events correct us once it comes back.
|
|
11
|
+
*/
|
|
12
|
+
export class MorphAccessory {
|
|
13
|
+
platform;
|
|
14
|
+
accessory;
|
|
15
|
+
config;
|
|
16
|
+
lightbulb;
|
|
17
|
+
motion;
|
|
18
|
+
lamp;
|
|
19
|
+
constructor(platform, accessory, config) {
|
|
20
|
+
this.platform = platform;
|
|
21
|
+
this.accessory = accessory;
|
|
22
|
+
this.config = config;
|
|
23
|
+
const { Service, Characteristic } = this.platform.api.hap;
|
|
24
|
+
this.lamp = new DysonMorphLamp({
|
|
25
|
+
mac: config.mac,
|
|
26
|
+
ltk: config.ltk,
|
|
27
|
+
accountId: config.accountId,
|
|
28
|
+
adapter: platform.config.adapter,
|
|
29
|
+
log: platform.log,
|
|
30
|
+
});
|
|
31
|
+
(this.accessory.getService(Service.AccessoryInformation) ?? this.accessory.addService(Service.AccessoryInformation))
|
|
32
|
+
.setCharacteristic(Characteristic.Manufacturer, 'Dyson')
|
|
33
|
+
.setCharacteristic(Characteristic.Model, 'Solarcycle Morph')
|
|
34
|
+
.setCharacteristic(Characteristic.SerialNumber, config.serial);
|
|
35
|
+
this.lightbulb =
|
|
36
|
+
this.accessory.getService(Service.Lightbulb) ?? this.accessory.addService(Service.Lightbulb, config.name);
|
|
37
|
+
this.lightbulb.setCharacteristic(Characteristic.Name, config.name);
|
|
38
|
+
this.lightbulb
|
|
39
|
+
.getCharacteristic(Characteristic.On)
|
|
40
|
+
.onGet(() => this.live(() => this.lamp.getState().on))
|
|
41
|
+
.onSet((value) => this.handleSet('power', () => this.lamp.setPower(value)));
|
|
42
|
+
this.lightbulb
|
|
43
|
+
.getCharacteristic(Characteristic.Brightness)
|
|
44
|
+
.onGet(() => this.live(() => this.lamp.getState().brightness))
|
|
45
|
+
.onSet((value) => this.handleSet('brightness', () => this.lamp.setBrightness(value)));
|
|
46
|
+
this.lightbulb
|
|
47
|
+
.getCharacteristic(Characteristic.ColorTemperature)
|
|
48
|
+
// HomeKit works in mireds, which run inverse to Kelvin.
|
|
49
|
+
.setProps({ minValue: kelvinToMired(MAX_KELVIN), maxValue: kelvinToMired(MIN_KELVIN) })
|
|
50
|
+
.onGet(() => this.live(() => kelvinToMired(this.lamp.getState().kelvin)))
|
|
51
|
+
.onSet((value) => this.handleSet('colour temperature', () => this.lamp.setColorTemperature(miredToKelvin(value))));
|
|
52
|
+
// Tell HomeKit the moment the link goes, rather than leaving it showing the
|
|
53
|
+
// last value as though it were current.
|
|
54
|
+
this.lamp.on('disconnected', () => this.markUnreachable());
|
|
55
|
+
this.lamp.on('connected', () => {
|
|
56
|
+
const state = this.lamp.getState();
|
|
57
|
+
this.lightbulb.updateCharacteristic(Characteristic.On, state.on);
|
|
58
|
+
this.lightbulb.updateCharacteristic(Characteristic.Brightness, state.brightness);
|
|
59
|
+
this.lightbulb.updateCharacteristic(Characteristic.ColorTemperature, kelvinToMired(state.kelvin));
|
|
60
|
+
});
|
|
61
|
+
if (config.motionSensor) {
|
|
62
|
+
this.motion =
|
|
63
|
+
this.accessory.getService(Service.MotionSensor) ??
|
|
64
|
+
this.accessory.addService(Service.MotionSensor, `${config.name} Motion`);
|
|
65
|
+
this.motion
|
|
66
|
+
.getCharacteristic(Characteristic.MotionDetected)
|
|
67
|
+
.onGet(() => this.live(() => this.motion?.getCharacteristic(Characteristic.MotionDetected).value ?? false));
|
|
68
|
+
this.lamp.on('motion', (detected) => {
|
|
69
|
+
this.motion?.updateCharacteristic(Characteristic.MotionDetected, detected);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
this.lamp.on('state', (state) => {
|
|
73
|
+
this.lightbulb.updateCharacteristic(Characteristic.On, state.on);
|
|
74
|
+
this.lightbulb.updateCharacteristic(Characteristic.Brightness, state.brightness);
|
|
75
|
+
this.lightbulb.updateCharacteristic(Characteristic.ColorTemperature, kelvinToMired(state.kelvin));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async start() {
|
|
79
|
+
try {
|
|
80
|
+
await this.lamp.start();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
this.platform.log.error(`Could not start BLE session for ${this.config.name}: ${describeError(error)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async stop() {
|
|
87
|
+
await this.lamp.stop();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Answer only while the lamp is actually reachable.
|
|
91
|
+
*
|
|
92
|
+
* Without this HomeKit keeps showing whatever was last known — a lamp that
|
|
93
|
+
* has been unreachable for minutes still reads as on at 100%, which is worse
|
|
94
|
+
* than saying nothing. Throwing a communication failure is how an accessory
|
|
95
|
+
* reports itself as unavailable, and the Home app shows "No Response".
|
|
96
|
+
*/
|
|
97
|
+
live(read) {
|
|
98
|
+
if (!this.lamp.isConnected()) {
|
|
99
|
+
throw new this.platform.api.hap.HapStatusError(-70402 /* this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE */);
|
|
100
|
+
}
|
|
101
|
+
return read();
|
|
102
|
+
}
|
|
103
|
+
/** Push "No Response" to every characteristic at once. */
|
|
104
|
+
markUnreachable() {
|
|
105
|
+
const { Characteristic } = this.platform.api.hap;
|
|
106
|
+
const error = new Error(`${this.config.name} is not reachable over Bluetooth`);
|
|
107
|
+
for (const characteristic of [Characteristic.On, Characteristic.Brightness, Characteristic.ColorTemperature]) {
|
|
108
|
+
this.lightbulb.updateCharacteristic(characteristic, error);
|
|
109
|
+
}
|
|
110
|
+
this.motion?.updateCharacteristic(Characteristic.MotionDetected, error);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Apply a write, reporting failure to HomeKit rather than swallowing it.
|
|
114
|
+
*
|
|
115
|
+
* A command that cannot be delivered is not retried later, so the Home app
|
|
116
|
+
* has to say so — otherwise it shows the new value as though it had taken
|
|
117
|
+
* effect and the user has no way to tell that nothing happened.
|
|
118
|
+
*/
|
|
119
|
+
async handleSet(what, apply) {
|
|
120
|
+
try {
|
|
121
|
+
await apply();
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
this.platform.log.warn(`Setting ${what} on ${this.config.name} failed: ${describeError(error)}`);
|
|
125
|
+
throw new this.platform.api.hap.HapStatusError(-70402 /* this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE */);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=accessory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessory.js","sourceRoot":"","sources":["../src/accessory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAI5C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAG3F;;;;;;;GAOG;AACH,MAAM,OAAO,cAAc;IAMN;IACA;IACA;IAPF,SAAS,CAAU;IACnB,MAAM,CAAW;IACjB,IAAI,CAAiB;IAEtC,YACmB,QAAuB,EACvB,SAA4B,EAC5B,MAA2B;QAF3B,aAAQ,GAAR,QAAQ,CAAe;QACvB,cAAS,GAAT,SAAS,CAAmB;QAC5B,WAAM,GAAN,MAAM,CAAqB;QAE5C,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;QAE1D,IAAI,CAAC,IAAI,GAAG,IAAI,cAAc,CAAC;YAC7B,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO;YAChC,GAAG,EAAE,QAAQ,CAAC,GAAG;SAClB,CAAC,CAAC;QAEH,CAAC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;aACjH,iBAAiB,CAAC,cAAc,CAAC,YAAY,EAAE,OAAO,CAAC;aACvD,iBAAiB,CAAC,cAAc,CAAC,KAAK,EAAE,kBAAkB,CAAC;aAC3D,iBAAiB,CAAC,cAAc,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAEjE,IAAI,CAAC,SAAS;YACZ,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QAC5G,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QAEnE,IAAI,CAAC,SAAS;aACX,iBAAiB,CAAC,cAAc,CAAC,EAAE,CAAC;aACpC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;aACrD,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAgB,CAAC,CAAC,CAAC,CAAC;QAEzF,IAAI,CAAC,SAAS;aACX,iBAAiB,CAAC,cAAc,CAAC,UAAU,CAAC;aAC5C,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,CAAC;aAC7D,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,KAAe,CAAC,CAAC,CAAC,CAAC;QAElG,IAAI,CAAC,SAAS;aACX,iBAAiB,CAAC,cAAc,CAAC,gBAAgB,CAAC;YACnD,wDAAwD;aACvD,QAAQ,CAAC,EAAE,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC;aACtF,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;aACxE,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,oBAAoB,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,aAAa,CAAC,KAAe,CAAC,CAAC,CAAC,CAAC,CAAC;QAE/H,4EAA4E;QAC5E,wCAAwC;QACxC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,CAAC;QAC3D,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,GAAG,EAAE;YAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;YACjE,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,cAAc,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;YACjF,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,cAAc,CAAC,gBAAgB,EAAE,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACpG,CAAC,CAAC,CAAC;QAEH,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;YACxB,IAAI,CAAC,MAAM;gBACT,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC;oBAC/C,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,MAAM,CAAC,IAAI,SAAS,CAAC,CAAC;YAC3E,IAAI,CAAC,MAAM;iBACR,iBAAiB,CAAC,cAAc,CAAC,cAAc,CAAC;iBAChD,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,iBAAiB,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC;YAC9G,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,EAAE;gBAClC,IAAI,CAAC,MAAM,EAAE,oBAAoB,CAAC,cAAc,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;YAC7E,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC9B,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;YACjE,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,cAAc,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;YACjF,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,cAAc,CAAC,gBAAgB,EAAE,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACpG,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAC1B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,mCAAmC,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC1G,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC;IAED;;;;;;;OAOG;IACK,IAAI,CAAI,IAAa;QAC3B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YAC7B,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,cAAc,4EAE7C,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC;IAED,0DAA0D;IAClD,eAAe;QACrB,MAAM,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;QACjD,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,kCAAkC,CAAC,CAAC;QAC/E,KAAK,MAAM,cAAc,IAAI,CAAC,cAAc,CAAC,EAAE,EAAE,cAAc,CAAC,UAAU,EAAE,cAAc,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAC7G,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;QAC7D,CAAC;QACD,IAAI,CAAC,MAAM,EAAE,oBAAoB,CAAC,cAAc,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;IAC1E,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,SAAS,CAAC,IAAY,EAAE,KAA0B;QAC9D,IAAI,CAAC;YACH,MAAM,KAAK,EAAE,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,YAAY,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACjG,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,cAAc,4EAE7C,CAAC;QACJ,CAAC;IACH,CAAC;CACF"}
|