thermoslop 0.2.0__tar.gz

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.
Files changed (36) hide show
  1. thermoslop-0.2.0/LICENSE +21 -0
  2. thermoslop-0.2.0/PKG-INFO +386 -0
  3. thermoslop-0.2.0/README.md +361 -0
  4. thermoslop-0.2.0/pyproject.toml +40 -0
  5. thermoslop-0.2.0/setup.cfg +4 -0
  6. thermoslop-0.2.0/src/thermoslop/__init__.py +5 -0
  7. thermoslop-0.2.0/src/thermoslop/__main__.py +3 -0
  8. thermoslop-0.2.0/src/thermoslop/audit.py +123 -0
  9. thermoslop-0.2.0/src/thermoslop/cli.py +511 -0
  10. thermoslop-0.2.0/src/thermoslop/config.py +203 -0
  11. thermoslop-0.2.0/src/thermoslop/daemon.py +160 -0
  12. thermoslop-0.2.0/src/thermoslop/demo.py +52 -0
  13. thermoslop-0.2.0/src/thermoslop/discovery.py +260 -0
  14. thermoslop-0.2.0/src/thermoslop/guard.py +148 -0
  15. thermoslop-0.2.0/src/thermoslop/homekit.py +485 -0
  16. thermoslop-0.2.0/src/thermoslop/mcp_server.py +318 -0
  17. thermoslop-0.2.0/src/thermoslop/ops.py +76 -0
  18. thermoslop-0.2.0/src/thermoslop/setup.py +191 -0
  19. thermoslop-0.2.0/src/thermoslop/tailnet.py +165 -0
  20. thermoslop-0.2.0/src/thermoslop.egg-info/PKG-INFO +386 -0
  21. thermoslop-0.2.0/src/thermoslop.egg-info/SOURCES.txt +34 -0
  22. thermoslop-0.2.0/src/thermoslop.egg-info/dependency_links.txt +1 -0
  23. thermoslop-0.2.0/src/thermoslop.egg-info/entry_points.txt +2 -0
  24. thermoslop-0.2.0/src/thermoslop.egg-info/requires.txt +5 -0
  25. thermoslop-0.2.0/src/thermoslop.egg-info/top_level.txt +1 -0
  26. thermoslop-0.2.0/tests/test_audit_fixes.py +169 -0
  27. thermoslop-0.2.0/tests/test_changelog.py +200 -0
  28. thermoslop-0.2.0/tests/test_config.py +85 -0
  29. thermoslop-0.2.0/tests/test_demo.py +45 -0
  30. thermoslop-0.2.0/tests/test_guard.py +125 -0
  31. thermoslop-0.2.0/tests/test_mcp.py +280 -0
  32. thermoslop-0.2.0/tests/test_paths.py +107 -0
  33. thermoslop-0.2.0/tests/test_release.py +68 -0
  34. thermoslop-0.2.0/tests/test_setup.py +119 -0
  35. thermoslop-0.2.0/tests/test_state.py +162 -0
  36. thermoslop-0.2.0/tests/test_tailnet.py +90 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 John Martell
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.
@@ -0,0 +1,386 @@
1
+ Metadata-Version: 2.4
2
+ Name: thermoslop
3
+ Version: 0.2.0
4
+ Summary: Unofficial local HomeKit control for ecobee thermostats: CLI, MCP server, Omarchy bar widget
5
+ Author: John Martell
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/martellj/thermoslop
8
+ Project-URL: Issues, https://github.com/martellj/thermoslop/issues
9
+ Project-URL: Changelog, https://github.com/martellj/thermoslop/blob/main/CHANGELOG.md
10
+ Keywords: homekit,ecobee,thermostat,mcp,omarchy
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Home Automation
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: aiohomekit>=3.2
21
+ Requires-Dist: mcp<3,>=2.3
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest; extra == "test"
24
+ Dynamic: license-file
25
+
26
+ <p align="center"><img src="assets/thermoslop-logo.svg" alt="ThermoSlop" width="420"></p>
27
+
28
+ # ThermoSlop
29
+
30
+ Your thermostats in the terminal, the status bar, and your AI agents' hands.
31
+ ThermoSlop controls ecobee thermostats locally over HomeKit:
32
+
33
+ - `thermoslop`, a CLI for the terminal, scripts and AI tools
34
+ - an MCP server, with guardrails
35
+ - an Omarchy bar widget
36
+
37
+ No cloud and no ecobee developer key, and it works while the internet is down.
38
+
39
+ > **Unofficial.** ThermoSlop is an independent project. It is not affiliated
40
+ > with, endorsed by, or supported by ecobee Inc. or Apple Inc. "ecobee" and
41
+ > "HomeKit" are trademarks of their respective owners, and are used here only
42
+ > to say what this works with. It talks to your thermostat over the local
43
+ > HomeKit protocol. Use it at your own risk (see [LICENSE](LICENSE)).
44
+
45
+ ```
46
+ $ thermoslop status
47
+ Main Floor: 69.8°F · ▲ heating · set 68°F (heat) · home · 42% RH
48
+ Bedroom: 67.1°F, occupied
49
+ Upstairs: 71.2°F · idle · set 67°F–75°F (auto) · home · 40% RH
50
+ ```
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ uv tool install thermoslop # or: pipx install thermoslop
56
+ thermoslop setup # starts the watcher service; on Omarchy, offers the bar widget
57
+ thermoslop discover # lists thermostats and whether they're ready to pair
58
+ thermoslop pair <device id> # asks for the HomeKit code shown on the thermostat
59
+ ```
60
+
61
+ - **Linux:** `setup` installs systemd user services.
62
+ - **macOS:** `setup` installs launchd agents. The CLI, watcher and MCP server
63
+ work there; the bar widget is Omarchy-only.
64
+ - **Removing the services:** `thermoslop setup --remove` takes them out and
65
+ keeps your pairings and history.
66
+ - **The Omarchy plugin** can also be added on its own:
67
+ `omarchy plugin add https://github.com/martellj/thermoslop-omarchy --enable`.
68
+
69
+ From a checkout instead (for development), `./install.sh` makes an editable
70
+ venv, runs `thermoslop setup`, and symlinks the widget from `omarchy/`.
71
+
72
+ The HomeKit code is on the thermostat under **Menu → Settings → HomeKit**
73
+ (the exact menu path varies with firmware). ecobee shows a **new code once
74
+ pairing starts**, so `thermoslop pair` starts first and then asks for the code
75
+ on the screen at that moment; a code read beforehand is rejected. An ecobee pairs with **one**
76
+ HomeKit controller, so it has to be removed from Apple Home before pairing
77
+ here, and pairing it back to Apple Home takes it away from this box.
78
+
79
+ Upgrading from before the rename (when the project was called `ecobee`):
80
+ rerun `./install.sh`. It moves the config, pairings, cache and change log to
81
+ the `thermoslop` locations, swaps the services, and renames the bar entry in
82
+ place, so nothing has to be paired again.
83
+
84
+ The watcher service restarts on its own when `pairing.json` changes, so
85
+ newly paired thermostats show up in the bar without a manual restart.
86
+
87
+ ## Commands
88
+
89
+ All of them take `--json` (machine-readable; errors come back as
90
+ `{"error": ...}` with exit code 1) and `-t NAME` (alias, prefix ok).
91
+
92
+ Changes return once the thermostat reads back what was asked. If it hasn't
93
+ within 8 seconds, the command prints what it does read and exits **2** ("sent
94
+ but not confirmed"). Exit 0 means confirmed.
95
+
96
+ House-wide `home|away|sleep|resume` tries every thermostat even if one fails,
97
+ reports each, and exits 1 if any failed. With `--json`, failures appear as
98
+ `{"alias": ..., "error": ...}` beside the states.
99
+
100
+ `pair` takes a device id, an address or a name. If two devices share the name
101
+ (a "Main Floor" in each house), it lists them so you can pair by id.
102
+
103
+ | Command | What it does |
104
+ |---|---|
105
+ | `thermoslop status [--live]` | Temperature, humidity, mode, setpoints, comfort setting, sensors. Uses the watcher's cache when fresh; `--live` asks the thermostat. |
106
+ | `thermoslop set 70` / `set +1` / `set -2` | Target temperature in heat/cool mode. Relative changes also shift both setpoints in auto mode. |
107
+ | `thermoslop set --heat 68 --cool 75` | Auto-mode setpoints. |
108
+ | `thermoslop mode heat\|cool\|auto\|off` | HVAC mode. |
109
+ | `thermoslop home\|away\|sleep [--all]` | Hold a comfort setting. |
110
+ | `thermoslop resume [--all]` | Clear holds and go back to the schedule. |
111
+ | `thermoslop discover [--all]` | Find HomeKit thermostats (or every HomeKit accessory with `--all`). |
112
+ | `thermoslop pair NAME [--demo]`, `thermoslop unpair -t NAME` | Manage pairings (`--demo`: a pretend thermostat, nothing saved). |
113
+ | `thermoslop watch` | The daemon behind the widget (run by `thermoslop-watch.service`). |
114
+ | `thermoslop log [--hours 24] [--source S]` | Recent changes and where they came from (see [Change log](#change-log)). |
115
+ | `thermoslop mcp [--read-only]` | MCP server for AI clients (see below). |
116
+ | `thermoslop dump -t NAME` | Raw HomeKit accessory data, for debugging. |
117
+
118
+ When more than one thermostat is paired, commands that change something need
119
+ `-t`, `--all`, or `default = "alias"` in the config.
120
+
121
+ Writing a setpoint creates a hold, just like changing it on the thermostat.
122
+ `thermoslop resume` clears it.
123
+
124
+ ## MCP server for AI clients
125
+
126
+ `thermoslop mcp` serves the thermostats as MCP tools over stdio:
127
+
128
+ | Tool | Does |
129
+ |---|---|
130
+ | `list_thermostats`, `get_status` | Read-only. Same JSON as `thermoslop status --json`. |
131
+ | `set_temperature` | Absolute setpoint (`target`, or `heat`/`cool` in auto mode). |
132
+ | `adjust_temperature` | Relative change, −5 to +5. |
133
+ | `set_mode` | heat / cool / auto (off only if allowed). |
134
+ | `set_comfort`, `resume_schedule` | Per thermostat, or a whole `house`. Reports per thermostat whether it took effect. |
135
+
136
+ Pairing isn't exposed. Each tool takes `thermostat` (alias or prefix) and/or `house`.
137
+
138
+ **Claude Code:**
139
+
140
+ ```bash
141
+ claude mcp add --scope user thermoslop -- ~/.local/bin/thermoslop mcp
142
+ ```
143
+
144
+ **Claude Desktop**, or any client that takes a JSON config:
145
+
146
+ ```json
147
+ { "mcpServers": { "thermoslop": { "command": "/home/you/.local/bin/thermoslop", "args": ["mcp"] } } }
148
+ ```
149
+
150
+ ### Other machines on your tailnet
151
+
152
+ `thermoslop mcp --http` serves streamable HTTP for MCP clients on other machines
153
+ (a Mac, a laptop). Nothing is reachable from the LAN or the internet:
154
+
155
+ - It listens only on this machine's Tailscale addresses, and refuses
156
+ `--bind 0.0.0.0` or any non-Tailscale address.
157
+ - Each request's source is looked up with `tailscale whois`. It's answered only
158
+ if the caller is in `[mcp] allow`, which defaults to this node's owner.
159
+ Anyone else gets 403, and the refusal is logged. There are no tokens: the
160
+ tailnet already vouches for the device. A lookup is reused for up to 30
161
+ seconds; failures never are.
162
+ - Requests must be addressed to this node's tailnet names, and requests carrying
163
+ a browser `Origin` are refused, so a web page can't reach it by rebinding a
164
+ domain to this address.
165
+
166
+ ```bash
167
+ systemctl --user enable --now thermoslop-mcp # installed by install.sh, off by default
168
+ ```
169
+
170
+ ```toml
171
+ [mcp]
172
+ http_port = 8765
173
+ allow = [
174
+ "alice@example.com", # everyone signed in as this exact login
175
+ "node:nAbC123DEF4CNTRL@bob@example.com", # or just one device, while bob owns it
176
+ ]
177
+ ```
178
+
179
+ A device's stable ID is `Node.StableID` in `tailscale whois --json <its tailnet IP>`.
180
+ Machine names and login prefixes aren't accepted: renaming a machine, or
181
+ registering a look-alike login, could otherwise get someone in.
182
+
183
+ On the other machine, point the client at `http://<node>:8765/mcp`, where
184
+ `<node>` is this machine's MagicDNS name:
185
+
186
+ ```bash
187
+ claude mcp add --scope user --transport http thermoslop http://omarchy:8765/mcp
188
+ ```
189
+
190
+ The change log records these callers as `login on node`.
191
+
192
+ ### Guardrails
193
+
194
+ The CLI is unrestricted. Changes through MCP are checked against `[mcp]` in the
195
+ config:
196
+
197
+ ```toml
198
+ [mcp]
199
+ read_only = false # true (or `thermoslop mcp --read-only`) offers only the read tools
200
+ min_setpoint = "55F" # setpoints outside this range are refused ("13C" works too)
201
+ max_setpoint = "80F"
202
+ allow_off = false # set_mode off is refused unless true
203
+ max_changes_per_hour = 20 # each thermostat changed counts once; shared by all processes; survives restarts
204
+ units = "F" # unit of temperatures in tool calls that don't set `unit` (default: top-level units, else F)
205
+ ```
206
+
207
+ Temperatures an agent sends are in `[mcp] units` unless the call sets
208
+ `unit: "C"` or `"F"`, and they're converted to each thermostat's own units
209
+ before the limits are checked. Readings come back in each thermostat's units
210
+ (its `units` field).
211
+
212
+ Comfort holds and resuming the schedule switch to the thermostat's own preset
213
+ temperatures, which the setpoint range doesn't cover. A house-wide comfort or
214
+ resume reports, logs and counts each thermostat separately, so one that fails
215
+ doesn't hide the others.
216
+
217
+ Refusals come back as tool errors that explain why, so the agent can tell you
218
+ instead of retrying. Every attempted change, refusals and failures included,
219
+ goes to the [change log](#change-log) with source `mcp`. The rate limit counts
220
+ only MCP changes.
221
+
222
+ Comfort changes are verified by reading the thermostat back. An ecobee3 lite
223
+ (firmware 4.8.70240) does apply away, home and resume, but takes 2–4 s to
224
+ report them. Writes wait up to 8 s for the thermostat to report the change.
225
+
226
+ ## Change log
227
+
228
+ Every change to a thermostat is recorded in
229
+ `~/.local/state/thermoslop/changes.jsonl`, whatever made it, with the time, the
230
+ caller, the outcome, and the mode, setpoints and comfort setting before and
231
+ after. `thermoslop log` shows it:
232
+
233
+ ```
234
+ $ thermoslop log
235
+ Oct 22 19:55:04 cli set_temperature by terminal (alice)
236
+ upstairs: target 60→61, heat 60→61
237
+ Oct 22 19:58:40 external external_change by thermostat, app or schedule
238
+ upstairs: target 61→62, heat 61→62
239
+ ```
240
+
241
+ | Source | Where the change came from |
242
+ |---|---|
243
+ | `mcp` | An AI client through `thermoslop mcp` (caller: the local client, or `login on node` over the tailnet) |
244
+ | `cli` | `thermoslop` in a terminal or script |
245
+ | `widget` | The bar widget's panel buttons, scroll or clicks |
246
+ | `external` | Not ThermoSlop: the thermostat's screen, the ecobee app, or its schedule |
247
+
248
+ `external` entries come from the watcher, which compares each thermostat's
249
+ settings on every update.
250
+ - **Attribution:** a change counts as ThermoSlop's only if a ThermoSlop entry
251
+ for that thermostat, from about a minute before to 20 seconds after it,
252
+ explains the new values. So an app change made right after a widget change
253
+ is still caught.
254
+ - **No source detail:** HomeKit doesn't say who made a change, so a screen,
255
+ app or schedule change all show as `external`, logged with the time the
256
+ watcher saw them.
257
+ - **Restarts:** when the watcher starts, it compares its first reading with
258
+ the last cached state. A change made while it was stopped is logged then,
259
+ unless a ThermoSlop change since that cached reading explains it.
260
+
261
+ This is a heuristic for your own records, not forensic proof.
262
+
263
+ ## Bar widget (`omarchy/martellj.thermoslop`)
264
+
265
+ The bar shows an icon (flame while heating, snowflake while cooling, away icon
266
+ when every thermostat is away) and each thermostat's temperature.
267
+
268
+ - **left click**: the ThermoSlop panel, with one card per thermostat:
269
+ - temperature on a ring gauge, and humidity
270
+ - a SET box with − / + (presses are batched and sent together)
271
+ - a trend line (24 hours by default), and how far the room is from its target
272
+
273
+ The mode bar along the bottom (heat, cool, eco = away, hold = resume the
274
+ schedule) acts on every thermostat the panel shows. Colors come from your
275
+ Omarchy theme.
276
+ - **+ ADD THERMOSTAT** (in the panel): pair a new thermostat without a terminal.
277
+ 1. It scans the network and lists HomeKit thermostats as ready, paired here or
278
+ paired elsewhere.
279
+ 2. PAIR starts pairing.
280
+ 3. It asks for the code the thermostat shows *now*, which changes once pairing
281
+ starts.
282
+ 4. It shows success, or the error with Try Again.
283
+
284
+ It runs the same `thermoslop discover` / `thermoslop pair` as the terminal.
285
+ - **right click**: away hold on all thermostats, or resume the schedule if any is away
286
+ - **middle click**: resume the schedule
287
+ - **scroll**: setpoint ±1° on the first thermostat, sent once scrolling stops
288
+
289
+ It reads `~/.cache/thermoslop/state.json`, which `thermoslop watch` keeps
290
+ current with HomeKit event subscriptions. The watcher also records one
291
+ temperature per thermostat every 5 minutes for the trend line, kept for
292
+ `trend_hours` (24 by default; 0 turns the trend off). Each point is a real
293
+ reading, not an average, and gaps while a thermostat is unreachable are drawn
294
+ straight across. The widget never polls the thermostats itself, and dims when the data
295
+ is more than 10 minutes old.
296
+
297
+ To try the pairing flow without touching your pairings, set
298
+ `"pairingDemo": true` on the widget's entry in `~/.config/omarchy/shell.json`.
299
+ "Add thermostat" then uses `--demo`: a pretend "Basement" thermostat that shows
300
+ code `314-15-926` (any other code is rejected) and a "Garage" paired elsewhere.
301
+ Nothing is paired, saved or logged. The same works from a terminal:
302
+ `thermoslop discover --demo` and `thermoslop pair Basement --demo`.
303
+
304
+ From scripts or a keybinding: `omarchy-shell thermoslop open` opens the panel,
305
+ `omarchy-shell thermoslop add` opens it on "Add thermostat", and
306
+ `omarchy-shell thermoslop pair NAME` starts pairing with a discovered device.
307
+
308
+ To show or control just one thermostat, set `"thermostat": "upstairs"` on its
309
+ entry in `~/.config/omarchy/shell.json`. `allowMultiple` is on, so you can
310
+ add one widget per thermostat.
311
+
312
+ The plugin directory is symlinked into `~/.config/omarchy/plugins/`, and the
313
+ shell doesn't hot-reload through that symlink. A rescan keeps the compiled
314
+ widget, so after editing the QML restart the shell with `omarchy restart shell`.
315
+ `omarchy-shell shell summon martellj.thermoslop` then opens the panel.
316
+
317
+ ## Configuration: `~/.config/thermoslop/config.toml`
318
+
319
+ ```toml
320
+ subnets = ["192.168.1.0/24"] # unicast discovery fallback (see below)
321
+ default = "main-floor" # thermostat for commands without -t
322
+ units = "F" # override each thermostat's display units
323
+ poll_interval = 60 # watcher's full refresh, between events
324
+ trend_hours = 24 # widget trend line window, up to 168; 0 = off
325
+
326
+ [houses] # optional; subnets here are scanned too
327
+ home = ["192.168.1.0/24"]
328
+ cabin = ["192.168.2.0/24"]
329
+ ```
330
+
331
+ ## Several houses
332
+
333
+ A thermostat belongs to the house whose subnet holds its IP. One controller
334
+ can run every house as long as it can route to each house's LAN. Here that's
335
+ Tailscale, with each house's OpenWrt router advertising its LAN as a subnet
336
+ route, and `tailscale set --accept-routes` on this machine.
337
+
338
+ - `thermoslop status` groups thermostats under a heading per house.
339
+ - `--house cabin` limits any command to one house; `thermoslop away --house cabin`
340
+ holds away on all of them.
341
+ - When a thermostat's name is already taken (two "Main Floor"s), `pair`
342
+ prefixes the alias with its house: `cabin-main-floor`.
343
+ - The widget groups temperatures per house (`68° 70° │ 55°`).
344
+ `"house": "cabin"` on a widget entry limits it to one house.
345
+
346
+ If the tunnel drops, that house's thermostats show as unavailable and keep
347
+ running their own schedules.
348
+
349
+ ## Networking notes
350
+
351
+ HomeKit discovery normally uses multicast mDNS, which doesn't cross NAT,
352
+ routers or VLANs. A machine that can't see the thermostats' multicast (a VM
353
+ behind NAT, another VLAN, another house over Tailscale) can list their subnets
354
+ in `subnets`. `discover` then also sends **unicast DNS-SD queries** to every
355
+ host there, and those do get through. Connections then go straight to the stored IP:port.
356
+
357
+ If a thermostat's IP or HAP port changes, the next failed connection
358
+ re-locates it by device id (first at its last IP, then by scanning `subnets`)
359
+ and updates `pairing.json`. A DHCP reservation for each thermostat on the
360
+ router makes this rarely needed.
361
+
362
+ ## Files
363
+
364
+ | Path | Contents |
365
+ |---|---|
366
+ | `~/.local/share/thermoslop/pairing.json` | HomeKit pairing keys (mode 600 in a 700 directory; treat it like an SSH key, see [SECURITY.md](SECURITY.md)) |
367
+ | `~/.local/share/thermoslop/charmap.json` | aiohomekit accessory cache |
368
+ | `~/.local/state/thermoslop/changes.jsonl` | [Change log](#change-log): every change, from any source (mode 600) |
369
+ | `~/.cache/thermoslop/state.json` | Latest state and trend history, written atomically by the watcher and CLI (mode 600 in a 700 directory: it reveals occupancy and when you're away) |
370
+
371
+ ## Development
372
+
373
+ ```bash
374
+ ~/.local/share/thermoslop/venv/bin/pip install -e '.[test]'
375
+ ~/.local/share/thermoslop/venv/bin/python -m pytest
376
+ ```
377
+
378
+ `./install.sh` sets up a development install from the checkout.
379
+ `scripts/check-version`, `scripts/check-dist` and `scripts/check-plugin` are
380
+ the release checks, which CI runs too. Releases are cut with
381
+ `scripts/release X.Y.Z`; see [RELEASING.md](RELEASING.md).
382
+
383
+ Built on [aiohomekit](https://github.com/Jc2k/aiohomekit), the library behind
384
+ Home Assistant's HomeKit Controller. ecobee's comfort settings and hold
385
+ handling use vendor characteristics, written the same way Home Assistant
386
+ writes them.