domesti-bot 0.1.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 (77) hide show
  1. domesti_bot-0.1.0/.gitignore +29 -0
  2. domesti_bot-0.1.0/LICENSE +21 -0
  3. domesti_bot-0.1.0/PKG-INFO +329 -0
  4. domesti_bot-0.1.0/README.md +286 -0
  5. domesti_bot-0.1.0/app/__init__.py +1 -0
  6. domesti_bot-0.1.0/app/_build_metadata.py +6 -0
  7. domesti_bot-0.1.0/app/androidtv_device_manager.py +703 -0
  8. domesti_bot-0.1.0/app/api/__init__.py +1 -0
  9. domesti_bot-0.1.0/app/api/app.py +629 -0
  10. domesti_bot-0.1.0/app/api/schemas.py +268 -0
  11. domesti_bot-0.1.0/app/api/settings_routes.py +170 -0
  12. domesti_bot-0.1.0/app/api/static/.gitkeep +0 -0
  13. domesti_bot-0.1.0/app/api/static/compact-layout-prototype.html +550 -0
  14. domesti_bot-0.1.0/app/api/static/dist/main.js +2 -0
  15. domesti_bot-0.1.0/app/api/static/dist/main.js.map +7 -0
  16. domesti_bot-0.1.0/app/api/static/icons/app-icon-192x192.png +0 -0
  17. domesti_bot-0.1.0/app/api/static/icons/app-icon-512x512.png +0 -0
  18. domesti_bot-0.1.0/app/api/static/icons/app-icon.svg +23 -0
  19. domesti_bot-0.1.0/app/api/static/icons/compact/bulb.svg +6 -0
  20. domesti_bot-0.1.0/app/api/static/icons/compact/desk.svg +8 -0
  21. domesti_bot-0.1.0/app/api/static/icons/compact/fan.svg +12 -0
  22. domesti_bot-0.1.0/app/api/static/icons/compact/garage_closed.svg +10 -0
  23. domesti_bot-0.1.0/app/api/static/icons/compact/garage_open.svg +5 -0
  24. domesti_bot-0.1.0/app/api/static/icons/compact/lamp.svg +7 -0
  25. domesti_bot-0.1.0/app/api/static/icons/compact/lantern.svg +7 -0
  26. domesti_bot-0.1.0/app/api/static/icons/compact/led.svg +8 -0
  27. domesti_bot-0.1.0/app/api/static/icons/compact/light.svg +12 -0
  28. domesti_bot-0.1.0/app/api/static/icons/compact/outlet.svg +6 -0
  29. domesti_bot-0.1.0/app/api/static/icons/compact/pendant.svg +11 -0
  30. domesti_bot-0.1.0/app/api/static/icons/compact/plug.svg +6 -0
  31. domesti_bot-0.1.0/app/api/static/icons/compact/room_attic.svg +7 -0
  32. domesti_bot-0.1.0/app/api/static/icons/compact/room_basement.svg +7 -0
  33. domesti_bot-0.1.0/app/api/static/icons/compact/room_bathroom.svg +7 -0
  34. domesti_bot-0.1.0/app/api/static/icons/compact/room_bedroom.svg +7 -0
  35. domesti_bot-0.1.0/app/api/static/icons/compact/room_dining.svg +7 -0
  36. domesti_bot-0.1.0/app/api/static/icons/compact/room_garage.svg +6 -0
  37. domesti_bot-0.1.0/app/api/static/icons/compact/room_guest.svg +7 -0
  38. domesti_bot-0.1.0/app/api/static/icons/compact/room_hall.svg +7 -0
  39. domesti_bot-0.1.0/app/api/static/icons/compact/room_kitchen.svg +6 -0
  40. domesti_bot-0.1.0/app/api/static/icons/compact/room_laundry.svg +7 -0
  41. domesti_bot-0.1.0/app/api/static/icons/compact/room_living.svg +8 -0
  42. domesti_bot-0.1.0/app/api/static/icons/compact/room_office.svg +10 -0
  43. domesti_bot-0.1.0/app/api/static/icons/compact/room_porch.svg +7 -0
  44. domesti_bot-0.1.0/app/api/static/icons/compact/speaker.svg +6 -0
  45. domesti_bot-0.1.0/app/api/static/icons/compact/speaker_paused.svg +8 -0
  46. domesti_bot-0.1.0/app/api/static/icons/compact/speaker_playing.svg +7 -0
  47. domesti_bot-0.1.0/app/api/static/icons/compact/speaker_unknown.svg +8 -0
  48. domesti_bot-0.1.0/app/api/static/icons/compact/strip.svg +7 -0
  49. domesti_bot-0.1.0/app/api/static/icons/compact/table.svg +8 -0
  50. domesti_bot-0.1.0/app/api/static/index.html +696 -0
  51. domesti_bot-0.1.0/app/api/static/manifest.webmanifest +24 -0
  52. domesti_bot-0.1.0/app/api/static/sw.js +74 -0
  53. domesti_bot-0.1.0/app/api/ui_state.py +644 -0
  54. domesti_bot-0.1.0/app/build_info.py +91 -0
  55. domesti_bot-0.1.0/app/db/__init__.py +37 -0
  56. domesti_bot-0.1.0/app/db/base.py +9 -0
  57. domesti_bot-0.1.0/app/db/engine.py +40 -0
  58. domesti_bot-0.1.0/app/db/legacy_migrations.py +37 -0
  59. domesti_bot-0.1.0/app/db/models.py +71 -0
  60. domesti_bot-0.1.0/app/db/schema.py +27 -0
  61. domesti_bot-0.1.0/app/db/secrets.py +125 -0
  62. domesti_bot-0.1.0/app/db/secrets_key.py +96 -0
  63. domesti_bot-0.1.0/app/db/session.py +25 -0
  64. domesti_bot-0.1.0/app/device_manager.py +109 -0
  65. domesti_bot-0.1.0/app/device_state_watcher.py +279 -0
  66. domesti_bot-0.1.0/app/domesti_bot_cli.py +2516 -0
  67. domesti_bot-0.1.0/app/gotailwind_device_manager.py +375 -0
  68. domesti_bot-0.1.0/app/kasa_device_manager.py +700 -0
  69. domesti_bot-0.1.0/app/kasa_discovery_store.py +379 -0
  70. domesti_bot-0.1.0/app/logging_config.py +221 -0
  71. domesti_bot-0.1.0/app/rule_engine.py +276 -0
  72. domesti_bot-0.1.0/app/sonos_device_manager.py +393 -0
  73. domesti_bot-0.1.0/app/tailwind_credentials.py +30 -0
  74. domesti_bot-0.1.0/app/ui_compact_icon.py +173 -0
  75. domesti_bot-0.1.0/config/__init__.py +1 -0
  76. domesti_bot-0.1.0/config/serve.py +367 -0
  77. domesti_bot-0.1.0/pyproject.toml +103 -0
@@ -0,0 +1,29 @@
1
+ .DS_Store
2
+ .idea/
3
+ .vscode/
4
+
5
+ # Local git worktrees; never commit as nested repo entries.
6
+ .worktrees/
7
+
8
+ *.sqlite
9
+ *.sqlite-journal
10
+ __pycache__/
11
+ *.py[cod]
12
+ .pytest_cache/
13
+ .mypy_cache/
14
+ .ruff_cache/
15
+ .venv/
16
+
17
+ # Runtime artifacts that should never be committed.
18
+ logs/
19
+ *.log
20
+
21
+ # Fernet master key for encrypted SQLite secrets (see domesti-secrets.json.example).
22
+ domesti-secrets.json
23
+
24
+ # Web bundle: source lives under web/, built output under app/api/static/dist/.
25
+ # The dist/ directory is rebuilt by `pnpm run build` (locally, in CI, and via
26
+ # scripts/on-deploy in production); it must not be committed.
27
+ web/node_modules/
28
+ .pnpm-store/
29
+ app/api/static/dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Henrique Andrade
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,329 @@
1
+ Metadata-Version: 2.4
2
+ Name: domesti-bot
3
+ Version: 0.1.0
4
+ Summary: Self-hosted LAN control surface for TP-Link Kasa, Sonos, and GoTailwind devices, with a tile-based web UI.
5
+ Project-URL: Homepage, https://github.com/the-hcma/domesti-bot
6
+ Project-URL: Repository, https://github.com/the-hcma/domesti-bot
7
+ Project-URL: Issues, https://github.com/the-hcma/domesti-bot/issues
8
+ Author-email: Henrique Andrade <thehcma@users.noreply.github.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: domesti-bot,fastapi,home-automation,kasa,sonos,tailwind
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Intended Audience :: End Users/Desktop
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Home Automation
25
+ Requires-Python: >=3.11
26
+ Requires-Dist: click==8.4.0
27
+ Requires-Dist: cryptography>=48.0.0
28
+ Requires-Dist: fastapi>=0.136.1
29
+ Requires-Dist: gotailwind[cli]
30
+ Requires-Dist: httpx>=0.28.1
31
+ Requires-Dist: idna==3.15
32
+ Requires-Dist: prompt-toolkit>=3.0.43
33
+ Requires-Dist: pychromecast>=14.0.10
34
+ Requires-Dist: pyproj
35
+ Requires-Dist: python-kasa
36
+ Requires-Dist: requests>=2.34.2
37
+ Requires-Dist: soco>=0.31.0
38
+ Requires-Dist: sqlalchemy>=2.0.49
39
+ Requires-Dist: uvicorn[standard]>=0.47.0
40
+ Requires-Dist: watchfiles==1.2.0
41
+ Requires-Dist: zeroconf>=0.149.7
42
+ Description-Content-Type: text/markdown
43
+
44
+ # domesti-bot
45
+
46
+ [![CI](https://github.com/the-hcma/domesti-bot/actions/workflows/ci.yml/badge.svg)](https://github.com/the-hcma/domesti-bot/actions/workflows/ci.yml)
47
+ [![Python ≥ 3.11](https://img.shields.io/badge/python-%E2%89%A53.11-brightgreen)](https://www.python.org/)
48
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
49
+
50
+ A self-hosted home-automation control surface for the devices on your home
51
+ network. `domesti-bot` discovers and controls TP-Link Kasa smart plugs/lights,
52
+ Sonos zones, and GoTailwind garage-door controllers, then exposes them through
53
+ a small tile-based web UI for one-tap control from any phone or laptop on the
54
+ same LAN.
55
+
56
+ The project is intentionally narrow in scope: no cloud round-trips, no
57
+ external accounts beyond the ones each device family already requires, and no
58
+ heavyweight rules engine. Everything runs on a single machine inside the
59
+ network the devices are on — typically the same Linux server that already
60
+ hosts other always-on services.
61
+
62
+ ## Features
63
+
64
+ - **TP-Link Kasa / Tapo (`python-kasa`)** — auto-discovery, on/off toggle per
65
+ device, per-family "Turn off all", with sticky exclusions for devices you
66
+ don't want bulk-actions to touch. Handles newer KLAP-encrypted devices via
67
+ an interactive `kasa-creds` REPL command.
68
+ - **Sonos (`soco`)** — zone discovery, per-zone pause/resume, per-family
69
+ "Pause all". Gracefully handles UPnP 701 ("Transition not available", e.g.
70
+ empty queue) with a surfaced action-error toast in the UI.
71
+ - **GoTailwind garage doors** — open/close per door, "Close all", and
72
+ idempotent operations so a "Close everything" bulk action survives doors
73
+ that are already closed. The Tailwind Local Control Key can be stored
74
+ encrypted in the discovery SQLite database (see [Encrypted secrets](#encrypted-secrets)).
75
+ - **Encrypted secrets** — Fernet-encrypted values in SQLite (Tailwind token
76
+ today); master key in gitignored `domesti-secrets.json` at the repo root.
77
+ Create it with the `setup-secrets` REPL command or copy
78
+ `domesti-secrets.json.example`.
79
+ - **Web UI** (`/`) — tile-based control, family-color frames, optimistic UI
80
+ updates with an 8-second grace window, backend-connectivity status, mobile
81
+ viewport support, and standardized colour rules (green active, red per-tile
82
+ off, orange bulk actions). Talks to a stable, OpenAPI-typed HTTP surface under `/v1/…`.
83
+ - **REPL CLI** (`scripts/domesti-bot`) — same discovery / control surface
84
+ exposed as an interactive `prompt_toolkit` shell for scripting and
85
+ troubleshooting, including `setup-secrets` to create `domesti-secrets.json`.
86
+ - **Continuous state monitoring** — background pollers keep the UI's view of
87
+ Kasa, Sonos, and Tailwind state in sync without manual refresh.
88
+
89
+ ## Quick start
90
+
91
+ Requires **Python ≥ 3.11** (3.14 is the targeted runtime).
92
+
93
+ ### Install from PyPI (recommended)
94
+
95
+ ```bash
96
+ pipx install domesti-bot
97
+ domesti-bot-server # HTTP API + web UI on a free loopback port
98
+ domesti-bot-server --listen-all # LAN-visible bind for phone / tablet testing
99
+ domesti-bot # interactive REPL for troubleshooting
100
+ domesti-bot --version # package version and source commit
101
+ ```
102
+
103
+ Set `DOMESTI_API_KEY` when binding to the LAN or any network you do not fully
104
+ trust. See [Configuration](#configuration) below.
105
+
106
+ PyPI releases are built with the web bundle included; no Node.js is required at
107
+ runtime. See [`docs/RELEASING.md`](docs/RELEASING.md) for how maintainers publish.
108
+
109
+ ### Develop from a git checkout
110
+
111
+ Uses [`uv`](https://docs.astral.sh/uv/) for dependency management.
112
+
113
+ ```bash
114
+ git clone https://github.com/the-hcma/domesti-bot.git
115
+ cd domesti-bot
116
+ uv sync --group dev
117
+ cd web && pnpm install --frozen-lockfile && pnpm run build && cd ..
118
+
119
+ # Start the HTTP server (binds 127.0.0.1 on a free port; auto-opens browser)
120
+ ./scripts/domesti-bot-server
121
+
122
+ # Or expose to the LAN so you can validate the UI from a phone
123
+ ./scripts/domesti-bot-server --listen-all
124
+ ```
125
+
126
+ The startup banner prints the URL the server is listening on, including one
127
+ `[http] network: http://<lan-ip>:<port>` line per non-loopback interface when
128
+ `--listen-all` is passed.
129
+
130
+ Need the device-control REPL instead of the HTTP API? Run
131
+ `./scripts/domesti-bot` and follow the prompts.
132
+
133
+ ## Configuration
134
+
135
+ Most operation is zero-config — devices are discovered on the LAN via mDNS /
136
+ broadcast probes, and discovered configurations are persisted in an SQLite
137
+ cache (`~/.config/domesti-bot/kasa_discovery.sqlite3` by default) so subsequent
138
+ startups are fast.
139
+
140
+ Optional environment variables:
141
+
142
+ | Variable | Effect |
143
+ |---|---|
144
+ | `DOMESTI_API_KEY` | When set, every `/v1/…` endpoint requires the `X-Domesti-Api-Key` header. Unset = unauthenticated (intended for trusted LAN only). |
145
+ | `DOMESTI_LISTEN_HOST` | Default bind address for the HTTP server. Overridden by `--listen-host` / `--listen-all`. |
146
+ | `DOMESTI_LISTEN_PORT` | Default TCP port. `0` = OS-allocated (the dev default). |
147
+ | `KASA_USERNAME` / `KASA_PASSWORD` | TP-Link cloud credentials for KLAP-encrypted devices (Tapo / newer Kasa). Required only if you have at least one such device. |
148
+ | `TAILWIND_TOKEN` | GoTailwind Local Control Key (six-digit code from the Tailwind dashboard). Overrides the encrypted DB copy when set. |
149
+ | `DOMESTI_SECRETS_KEY` | Fernet master key for encrypted SQLite secrets. Overrides `domesti-secrets.json` when set. |
150
+ | `DOMESTI_SECRETS_FILE` | Override path to the secrets JSON file (default: `./domesti-secrets.json` at repo root). |
151
+
152
+ Pass `--help` to either script for the complete flag list.
153
+
154
+ ## Encrypted secrets
155
+
156
+ Discovery state (device configs, display names, UI preferences, cached Tailwind
157
+ host, and similar) lives in a single SQLite file. **Upgrading domesti-bot does
158
+ not wipe that file** — existing rows keep working; new tables (such as
159
+ `app_secrets` for encrypted values) are added automatically on first access.
160
+
161
+ To encrypt secrets at rest (for example the Tailwind token saved from the web
162
+ UI), configure a Fernet master key:
163
+
164
+ 1. Copy the template and generate a key (or use the REPL helper):
165
+
166
+ ```bash
167
+ cp domesti-secrets.json.example domesti-secrets.json
168
+ # in the REPL: setup-secrets
169
+ ```
170
+
171
+ `setup-secrets` can generate a new key or accept an existing one, writes
172
+ `domesti-secrets.json` with mode `0600`, and reminds you to restart the
173
+ server. The file is listed in `.gitignore` — never commit it.
174
+
175
+ 2. Restart `domesti-bot-server` so the process reads the file.
176
+
177
+ 3. On **desktop** browsers, open the **☰** menu → **Settings** and paste the
178
+ six-digit Tailwind Local Control Key. It is stored encrypted in SQLite and
179
+ is never shown again. Restart once more so discovery picks up the token.
180
+
181
+ **Precedence for the Tailwind token:** `--tailwind-token` → `TAILWIND_TOKEN`
182
+ env → encrypted row in SQLite. **Precedence for the Fernet key:**
183
+ `DOMESTI_SECRETS_KEY` env → `domesti_secrets_key` in `domesti-secrets.json`.
184
+
185
+ For systemd, you can still use `EnvironmentFile=` for `TAILWIND_TOKEN` instead
186
+ of the database path; see [`docs/AGENTS.md`](docs/AGENTS.md) for security notes.
187
+
188
+ ## Web UI overview
189
+
190
+ After starting the server, the landing page hydrates a tile UI:
191
+
192
+ - One section per device family (`Lights & plugs`, `Sonos zones`,
193
+ `Garage doors`) with a family-coloured icon and frame.
194
+ - One tile per device. Tap to toggle (on/off, play/pause, open/close); the
195
+ tile updates optimistically and reconciles with the next background poll
196
+ (every 5 seconds).
197
+ - Per-family bulk button (`Turn off all`, `Pause all`, `Close all`) and a
198
+ global `Turn off / pause / close everything` button at the top (warm orange,
199
+ distinct from red per-tile off controls).
200
+ - On **desktop** viewports, a **☰** menu with **Settings** (Tailwind token).
201
+ The menu is hidden on mobile form factors.
202
+ - Per-tile "Exclude from all-off" (and analogous) checkbox so the top-of-page
203
+ bulk action skips devices you don't want it touching.
204
+ - Connectivity indicator: family frames turn red when the backend is
205
+ unreachable; all controls grey out until the next poll succeeds.
206
+
207
+ ## Progressive Web App (PWA)
208
+
209
+ The landing page is installable as a PWA on phones and desktops that support
210
+ it. Assets live under `app/api/static/`:
211
+
212
+ - `manifest.webmanifest` — name, icons, `display: standalone`
213
+ - `sw.js` — service worker (also served at `GET /sw.js` so scope covers `/`)
214
+ - `icons/` — launcher icons referenced by the manifest
215
+
216
+ The TypeScript bundle registers the worker on load. After you deploy a new
217
+ version, the service worker cache version in `sw.js` (for example
218
+ `domesti-bot-pwa-v15`) must be bumped so installed clients pick up HTML, CSS,
219
+ and `dist/main.js` changes.
220
+
221
+ **Install requirements:** Chromium-based browsers need a secure context
222
+ (`https://` or `http://127.0.0.1`). On a plain HTTP LAN URL, you still get
223
+ manifest metadata in some browsers, but the install prompt may not appear until
224
+ you terminate TLS or use loopback. When the server is reachable with
225
+ `--listen-all`, open the dashboard from your phone at
226
+ `http://<server-lan-ip>:<port>/` and use the in-app install banner when
227
+ offered.
228
+
229
+ ## Project layout
230
+
231
+ ```
232
+ domesti-bot/
233
+ ├── app/ Domain code (device managers, rule engine)
234
+ │ ├── *_device_manager.py One per family (kasa, sonos, gotailwind, …)
235
+ │ ├── db/ SQLAlchemy models + encrypted secrets
236
+ │ ├── kasa_discovery_store.py SQLite cache facade (shared by all managers)
237
+ │ └── api/ FastAPI HTTP surface (subpackage)
238
+ ├── config/serve.py uvicorn entrypoint
239
+ ├── tests/python/ pytest suite (hermetic + LAN-integration)
240
+ ├── web/src/ TypeScript source for the tile UI
241
+ ├── scripts/domesti-bot REPL CLI
242
+ ├── scripts/domesti-bot-server HTTP server launcher
243
+ ├── production/ systemd unit template + on-deploy hooks
244
+ └── docs/AGENTS.md Developer reference (canonical)
245
+ ```
246
+
247
+ `AGENTS.md` at the root is a symlink to `docs/AGENTS.md` — both paths point
248
+ to the same canonical developer reference.
249
+
250
+ ## Development
251
+
252
+ ```bash
253
+ # One-time setup
254
+ uv sync
255
+
256
+ # The full set of CI gates, in the order they run on every PR:
257
+ uv run pyright # type errors over app/, config/, scripts/, tests/
258
+ uv run pytest -m "not integration" -n auto # hermetic (parallel; matches CI)
259
+ shellcheck $(git ls-files scripts production/scripts | grep -Ev '\.(py|md|txt|yml|yaml|json|toml)$')
260
+
261
+ # Frontend, when web/src/ is touched:
262
+ cd web
263
+ pnpm install --frozen-lockfile
264
+ pnpm run typecheck
265
+ pnpm run build # writes app/api/static/dist/main.js
266
+ ```
267
+
268
+ The full set of code-style, testing, security, and Git workflow conventions is
269
+ documented in [`docs/AGENTS.md`](docs/AGENTS.md). Notable rules:
270
+
271
+ - Python 3.14 targeted, modern typing only (`list[str]`, not `List[str]`),
272
+ every public function annotated, `pyright` enforced.
273
+ - `uv` for dependency management — never `pip` directly.
274
+ - Methods and module-level functions sorted alphabetically inside each class.
275
+ - Sigs require `from __future__ import annotations`.
276
+ - All commits via Graphite-stacked PRs; `main` is protected, direct pushes
277
+ are blocked at the server.
278
+ - Conventional Commit messages, GPG-signed.
279
+
280
+ ## Production deployment
281
+
282
+ The production target is a **systemd user unit**. The template at
283
+ [`etc/systemd/domesti-bot.service`](etc/systemd/domesti-bot.service) is what
284
+ [`repository-helpers`](https://github.com/the-hcma/repository-helpers)
285
+ `setup-service` installs (same `@@REPO_DIR@@` contract as fpdf). It passes
286
+ `--listen-all --listen-port 8003` so the API listens on all interfaces (use
287
+ `DOMESTI_API_KEY` on untrusted LANs). `ExecStartPost` curls `GET /health` on
288
+ loopback until the process answers. The deploy hook [`scripts/on-deploy`](scripts/on-deploy)
289
+ runs `uv sync`, rebuilds the web bundle when needed, and lets `setup-service`
290
+ restart the unit. For a **system**-level unit with a dedicated user, see
291
+ [`production/systemd/domesti-bot-server.service.template`](production/systemd/domesti-bot-server.service.template).
292
+
293
+ `docs/AGENTS.md` has the deployment-specific details — auth keys, log paths,
294
+ service management commands.
295
+
296
+ ## Contributing
297
+
298
+ **Contributions are welcome and appreciated.** Issues, bug reports, feature
299
+ requests, and PRs are all on the table — whether you've spotted a typo,
300
+ hit an edge case with your specific Kasa/Sonos/Tailwind hardware, or want to
301
+ add a brand-new device family, the door is open.
302
+
303
+ The project uses [Graphite](https://graphite.dev) for stacked PRs. The
304
+ practical workflow is:
305
+
306
+ ```bash
307
+ # 1. Start a stack from main
308
+ gt create feat/your-idea
309
+
310
+ # 2. Make the change, run the local gates (pyright + pytest, see Development)
311
+ # Each gate is also enforced in CI.
312
+
313
+ # 3. Commit + open PR
314
+ gt create --all --message "feat: short description"
315
+ gt submit --no-interactive --publish
316
+ ```
317
+
318
+ For larger changes, stack the work into focused PRs so each one is
319
+ independently reviewable. The [stack of PRs](https://github.com/the-hcma/domesti-bot/pulls)
320
+ visible on this repo is itself an example of the pattern.
321
+
322
+ The full Git / commit / PR conventions, including the merge-it label flow and
323
+ the protected-`main` ruleset, live in [`docs/AGENTS.md`](docs/AGENTS.md) under
324
+ the *Commits, Stacking & Pull Requests* section.
325
+
326
+ ## License
327
+
328
+ MIT License — see [LICENSE](LICENSE) for the full text. Copyright (c) 2026
329
+ Henrique Andrade.
@@ -0,0 +1,286 @@
1
+ # domesti-bot
2
+
3
+ [![CI](https://github.com/the-hcma/domesti-bot/actions/workflows/ci.yml/badge.svg)](https://github.com/the-hcma/domesti-bot/actions/workflows/ci.yml)
4
+ [![Python ≥ 3.11](https://img.shields.io/badge/python-%E2%89%A53.11-brightgreen)](https://www.python.org/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
6
+
7
+ A self-hosted home-automation control surface for the devices on your home
8
+ network. `domesti-bot` discovers and controls TP-Link Kasa smart plugs/lights,
9
+ Sonos zones, and GoTailwind garage-door controllers, then exposes them through
10
+ a small tile-based web UI for one-tap control from any phone or laptop on the
11
+ same LAN.
12
+
13
+ The project is intentionally narrow in scope: no cloud round-trips, no
14
+ external accounts beyond the ones each device family already requires, and no
15
+ heavyweight rules engine. Everything runs on a single machine inside the
16
+ network the devices are on — typically the same Linux server that already
17
+ hosts other always-on services.
18
+
19
+ ## Features
20
+
21
+ - **TP-Link Kasa / Tapo (`python-kasa`)** — auto-discovery, on/off toggle per
22
+ device, per-family "Turn off all", with sticky exclusions for devices you
23
+ don't want bulk-actions to touch. Handles newer KLAP-encrypted devices via
24
+ an interactive `kasa-creds` REPL command.
25
+ - **Sonos (`soco`)** — zone discovery, per-zone pause/resume, per-family
26
+ "Pause all". Gracefully handles UPnP 701 ("Transition not available", e.g.
27
+ empty queue) with a surfaced action-error toast in the UI.
28
+ - **GoTailwind garage doors** — open/close per door, "Close all", and
29
+ idempotent operations so a "Close everything" bulk action survives doors
30
+ that are already closed. The Tailwind Local Control Key can be stored
31
+ encrypted in the discovery SQLite database (see [Encrypted secrets](#encrypted-secrets)).
32
+ - **Encrypted secrets** — Fernet-encrypted values in SQLite (Tailwind token
33
+ today); master key in gitignored `domesti-secrets.json` at the repo root.
34
+ Create it with the `setup-secrets` REPL command or copy
35
+ `domesti-secrets.json.example`.
36
+ - **Web UI** (`/`) — tile-based control, family-color frames, optimistic UI
37
+ updates with an 8-second grace window, backend-connectivity status, mobile
38
+ viewport support, and standardized colour rules (green active, red per-tile
39
+ off, orange bulk actions). Talks to a stable, OpenAPI-typed HTTP surface under `/v1/…`.
40
+ - **REPL CLI** (`scripts/domesti-bot`) — same discovery / control surface
41
+ exposed as an interactive `prompt_toolkit` shell for scripting and
42
+ troubleshooting, including `setup-secrets` to create `domesti-secrets.json`.
43
+ - **Continuous state monitoring** — background pollers keep the UI's view of
44
+ Kasa, Sonos, and Tailwind state in sync without manual refresh.
45
+
46
+ ## Quick start
47
+
48
+ Requires **Python ≥ 3.11** (3.14 is the targeted runtime).
49
+
50
+ ### Install from PyPI (recommended)
51
+
52
+ ```bash
53
+ pipx install domesti-bot
54
+ domesti-bot-server # HTTP API + web UI on a free loopback port
55
+ domesti-bot-server --listen-all # LAN-visible bind for phone / tablet testing
56
+ domesti-bot # interactive REPL for troubleshooting
57
+ domesti-bot --version # package version and source commit
58
+ ```
59
+
60
+ Set `DOMESTI_API_KEY` when binding to the LAN or any network you do not fully
61
+ trust. See [Configuration](#configuration) below.
62
+
63
+ PyPI releases are built with the web bundle included; no Node.js is required at
64
+ runtime. See [`docs/RELEASING.md`](docs/RELEASING.md) for how maintainers publish.
65
+
66
+ ### Develop from a git checkout
67
+
68
+ Uses [`uv`](https://docs.astral.sh/uv/) for dependency management.
69
+
70
+ ```bash
71
+ git clone https://github.com/the-hcma/domesti-bot.git
72
+ cd domesti-bot
73
+ uv sync --group dev
74
+ cd web && pnpm install --frozen-lockfile && pnpm run build && cd ..
75
+
76
+ # Start the HTTP server (binds 127.0.0.1 on a free port; auto-opens browser)
77
+ ./scripts/domesti-bot-server
78
+
79
+ # Or expose to the LAN so you can validate the UI from a phone
80
+ ./scripts/domesti-bot-server --listen-all
81
+ ```
82
+
83
+ The startup banner prints the URL the server is listening on, including one
84
+ `[http] network: http://<lan-ip>:<port>` line per non-loopback interface when
85
+ `--listen-all` is passed.
86
+
87
+ Need the device-control REPL instead of the HTTP API? Run
88
+ `./scripts/domesti-bot` and follow the prompts.
89
+
90
+ ## Configuration
91
+
92
+ Most operation is zero-config — devices are discovered on the LAN via mDNS /
93
+ broadcast probes, and discovered configurations are persisted in an SQLite
94
+ cache (`~/.config/domesti-bot/kasa_discovery.sqlite3` by default) so subsequent
95
+ startups are fast.
96
+
97
+ Optional environment variables:
98
+
99
+ | Variable | Effect |
100
+ |---|---|
101
+ | `DOMESTI_API_KEY` | When set, every `/v1/…` endpoint requires the `X-Domesti-Api-Key` header. Unset = unauthenticated (intended for trusted LAN only). |
102
+ | `DOMESTI_LISTEN_HOST` | Default bind address for the HTTP server. Overridden by `--listen-host` / `--listen-all`. |
103
+ | `DOMESTI_LISTEN_PORT` | Default TCP port. `0` = OS-allocated (the dev default). |
104
+ | `KASA_USERNAME` / `KASA_PASSWORD` | TP-Link cloud credentials for KLAP-encrypted devices (Tapo / newer Kasa). Required only if you have at least one such device. |
105
+ | `TAILWIND_TOKEN` | GoTailwind Local Control Key (six-digit code from the Tailwind dashboard). Overrides the encrypted DB copy when set. |
106
+ | `DOMESTI_SECRETS_KEY` | Fernet master key for encrypted SQLite secrets. Overrides `domesti-secrets.json` when set. |
107
+ | `DOMESTI_SECRETS_FILE` | Override path to the secrets JSON file (default: `./domesti-secrets.json` at repo root). |
108
+
109
+ Pass `--help` to either script for the complete flag list.
110
+
111
+ ## Encrypted secrets
112
+
113
+ Discovery state (device configs, display names, UI preferences, cached Tailwind
114
+ host, and similar) lives in a single SQLite file. **Upgrading domesti-bot does
115
+ not wipe that file** — existing rows keep working; new tables (such as
116
+ `app_secrets` for encrypted values) are added automatically on first access.
117
+
118
+ To encrypt secrets at rest (for example the Tailwind token saved from the web
119
+ UI), configure a Fernet master key:
120
+
121
+ 1. Copy the template and generate a key (or use the REPL helper):
122
+
123
+ ```bash
124
+ cp domesti-secrets.json.example domesti-secrets.json
125
+ # in the REPL: setup-secrets
126
+ ```
127
+
128
+ `setup-secrets` can generate a new key or accept an existing one, writes
129
+ `domesti-secrets.json` with mode `0600`, and reminds you to restart the
130
+ server. The file is listed in `.gitignore` — never commit it.
131
+
132
+ 2. Restart `domesti-bot-server` so the process reads the file.
133
+
134
+ 3. On **desktop** browsers, open the **☰** menu → **Settings** and paste the
135
+ six-digit Tailwind Local Control Key. It is stored encrypted in SQLite and
136
+ is never shown again. Restart once more so discovery picks up the token.
137
+
138
+ **Precedence for the Tailwind token:** `--tailwind-token` → `TAILWIND_TOKEN`
139
+ env → encrypted row in SQLite. **Precedence for the Fernet key:**
140
+ `DOMESTI_SECRETS_KEY` env → `domesti_secrets_key` in `domesti-secrets.json`.
141
+
142
+ For systemd, you can still use `EnvironmentFile=` for `TAILWIND_TOKEN` instead
143
+ of the database path; see [`docs/AGENTS.md`](docs/AGENTS.md) for security notes.
144
+
145
+ ## Web UI overview
146
+
147
+ After starting the server, the landing page hydrates a tile UI:
148
+
149
+ - One section per device family (`Lights & plugs`, `Sonos zones`,
150
+ `Garage doors`) with a family-coloured icon and frame.
151
+ - One tile per device. Tap to toggle (on/off, play/pause, open/close); the
152
+ tile updates optimistically and reconciles with the next background poll
153
+ (every 5 seconds).
154
+ - Per-family bulk button (`Turn off all`, `Pause all`, `Close all`) and a
155
+ global `Turn off / pause / close everything` button at the top (warm orange,
156
+ distinct from red per-tile off controls).
157
+ - On **desktop** viewports, a **☰** menu with **Settings** (Tailwind token).
158
+ The menu is hidden on mobile form factors.
159
+ - Per-tile "Exclude from all-off" (and analogous) checkbox so the top-of-page
160
+ bulk action skips devices you don't want it touching.
161
+ - Connectivity indicator: family frames turn red when the backend is
162
+ unreachable; all controls grey out until the next poll succeeds.
163
+
164
+ ## Progressive Web App (PWA)
165
+
166
+ The landing page is installable as a PWA on phones and desktops that support
167
+ it. Assets live under `app/api/static/`:
168
+
169
+ - `manifest.webmanifest` — name, icons, `display: standalone`
170
+ - `sw.js` — service worker (also served at `GET /sw.js` so scope covers `/`)
171
+ - `icons/` — launcher icons referenced by the manifest
172
+
173
+ The TypeScript bundle registers the worker on load. After you deploy a new
174
+ version, the service worker cache version in `sw.js` (for example
175
+ `domesti-bot-pwa-v15`) must be bumped so installed clients pick up HTML, CSS,
176
+ and `dist/main.js` changes.
177
+
178
+ **Install requirements:** Chromium-based browsers need a secure context
179
+ (`https://` or `http://127.0.0.1`). On a plain HTTP LAN URL, you still get
180
+ manifest metadata in some browsers, but the install prompt may not appear until
181
+ you terminate TLS or use loopback. When the server is reachable with
182
+ `--listen-all`, open the dashboard from your phone at
183
+ `http://<server-lan-ip>:<port>/` and use the in-app install banner when
184
+ offered.
185
+
186
+ ## Project layout
187
+
188
+ ```
189
+ domesti-bot/
190
+ ├── app/ Domain code (device managers, rule engine)
191
+ │ ├── *_device_manager.py One per family (kasa, sonos, gotailwind, …)
192
+ │ ├── db/ SQLAlchemy models + encrypted secrets
193
+ │ ├── kasa_discovery_store.py SQLite cache facade (shared by all managers)
194
+ │ └── api/ FastAPI HTTP surface (subpackage)
195
+ ├── config/serve.py uvicorn entrypoint
196
+ ├── tests/python/ pytest suite (hermetic + LAN-integration)
197
+ ├── web/src/ TypeScript source for the tile UI
198
+ ├── scripts/domesti-bot REPL CLI
199
+ ├── scripts/domesti-bot-server HTTP server launcher
200
+ ├── production/ systemd unit template + on-deploy hooks
201
+ └── docs/AGENTS.md Developer reference (canonical)
202
+ ```
203
+
204
+ `AGENTS.md` at the root is a symlink to `docs/AGENTS.md` — both paths point
205
+ to the same canonical developer reference.
206
+
207
+ ## Development
208
+
209
+ ```bash
210
+ # One-time setup
211
+ uv sync
212
+
213
+ # The full set of CI gates, in the order they run on every PR:
214
+ uv run pyright # type errors over app/, config/, scripts/, tests/
215
+ uv run pytest -m "not integration" -n auto # hermetic (parallel; matches CI)
216
+ shellcheck $(git ls-files scripts production/scripts | grep -Ev '\.(py|md|txt|yml|yaml|json|toml)$')
217
+
218
+ # Frontend, when web/src/ is touched:
219
+ cd web
220
+ pnpm install --frozen-lockfile
221
+ pnpm run typecheck
222
+ pnpm run build # writes app/api/static/dist/main.js
223
+ ```
224
+
225
+ The full set of code-style, testing, security, and Git workflow conventions is
226
+ documented in [`docs/AGENTS.md`](docs/AGENTS.md). Notable rules:
227
+
228
+ - Python 3.14 targeted, modern typing only (`list[str]`, not `List[str]`),
229
+ every public function annotated, `pyright` enforced.
230
+ - `uv` for dependency management — never `pip` directly.
231
+ - Methods and module-level functions sorted alphabetically inside each class.
232
+ - Sigs require `from __future__ import annotations`.
233
+ - All commits via Graphite-stacked PRs; `main` is protected, direct pushes
234
+ are blocked at the server.
235
+ - Conventional Commit messages, GPG-signed.
236
+
237
+ ## Production deployment
238
+
239
+ The production target is a **systemd user unit**. The template at
240
+ [`etc/systemd/domesti-bot.service`](etc/systemd/domesti-bot.service) is what
241
+ [`repository-helpers`](https://github.com/the-hcma/repository-helpers)
242
+ `setup-service` installs (same `@@REPO_DIR@@` contract as fpdf). It passes
243
+ `--listen-all --listen-port 8003` so the API listens on all interfaces (use
244
+ `DOMESTI_API_KEY` on untrusted LANs). `ExecStartPost` curls `GET /health` on
245
+ loopback until the process answers. The deploy hook [`scripts/on-deploy`](scripts/on-deploy)
246
+ runs `uv sync`, rebuilds the web bundle when needed, and lets `setup-service`
247
+ restart the unit. For a **system**-level unit with a dedicated user, see
248
+ [`production/systemd/domesti-bot-server.service.template`](production/systemd/domesti-bot-server.service.template).
249
+
250
+ `docs/AGENTS.md` has the deployment-specific details — auth keys, log paths,
251
+ service management commands.
252
+
253
+ ## Contributing
254
+
255
+ **Contributions are welcome and appreciated.** Issues, bug reports, feature
256
+ requests, and PRs are all on the table — whether you've spotted a typo,
257
+ hit an edge case with your specific Kasa/Sonos/Tailwind hardware, or want to
258
+ add a brand-new device family, the door is open.
259
+
260
+ The project uses [Graphite](https://graphite.dev) for stacked PRs. The
261
+ practical workflow is:
262
+
263
+ ```bash
264
+ # 1. Start a stack from main
265
+ gt create feat/your-idea
266
+
267
+ # 2. Make the change, run the local gates (pyright + pytest, see Development)
268
+ # Each gate is also enforced in CI.
269
+
270
+ # 3. Commit + open PR
271
+ gt create --all --message "feat: short description"
272
+ gt submit --no-interactive --publish
273
+ ```
274
+
275
+ For larger changes, stack the work into focused PRs so each one is
276
+ independently reviewable. The [stack of PRs](https://github.com/the-hcma/domesti-bot/pulls)
277
+ visible on this repo is itself an example of the pattern.
278
+
279
+ The full Git / commit / PR conventions, including the merge-it label flow and
280
+ the protected-`main` ruleset, live in [`docs/AGENTS.md`](docs/AGENTS.md) under
281
+ the *Commits, Stacking & Pull Requests* section.
282
+
283
+ ## License
284
+
285
+ MIT License — see [LICENSE](LICENSE) for the full text. Copyright (c) 2026
286
+ Henrique Andrade.
@@ -0,0 +1 @@
1
+ """domesti-bot application package: device managers, rules, REPL CLI, HTTP API."""
@@ -0,0 +1,6 @@
1
+ """Autogenerated by scripts/embed_build_metadata.py — do not hand-edit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ EMBEDDED_COMMIT: str = 'a114242d9faf'
6
+ EMBEDDED_VERSION: str = '0.1.0'