Self-hosted LAN control surface for TP-Link Kasa, Sonos, and GoTailwind devices, with a tile-based web UI.
Project description
domesti-bot
A self-hosted home-automation control surface for the devices on your home
network. domesti-bot discovers and controls TP-Link Kasa smart plugs/lights,
Sonos zones, and GoTailwind garage-door controllers, then exposes them through
a small tile-based web UI for one-tap control from any phone or laptop on the
same LAN.
The project is intentionally narrow in scope: no cloud round-trips, no external accounts beyond the ones each device family already requires, and no heavyweight rules engine. Everything runs on a single machine inside the network the devices are on — typically the same Linux server that already hosts other always-on services.
Features
- TP-Link Kasa / Tapo (
python-kasa) — auto-discovery, on/off toggle per device, per-family "Turn off all", with sticky exclusions for devices you don't want bulk-actions to touch. Handles newer KLAP-encrypted devices via an interactivekasa-credsREPL command. - Sonos (
soco) — zone discovery, per-zone pause/resume, per-family "Pause all". Gracefully handles UPnP 701 ("Transition not available", e.g. empty queue) with a surfaced action-error toast in the UI. - GoTailwind garage doors — open/close per door, "Close all", and idempotent operations so a "Close everything" bulk action survives doors that are already closed. The Tailwind Local Control Key can be stored encrypted in the discovery SQLite database (see Encrypted secrets).
- Encrypted secrets — Fernet-encrypted values in SQLite (Tailwind token
today); master key in gitignored
domesti-secrets.jsonat the repo root. Create it with thesetup-secretsREPL command or copydomesti-secrets.json.example. - Web UI (
/) — tile-based control, family-color frames, optimistic UI updates with an 8-second grace window, backend-connectivity status, mobile viewport support, and standardized colour rules (green active, red per-tile off, orange bulk actions). Talks to a stable, OpenAPI-typed HTTP surface under/v1/…. - REPL CLI (
scripts/domesti-bot) — same discovery / control surface exposed as an interactiveprompt_toolkitshell for scripting and troubleshooting, includingsetup-secretsto createdomesti-secrets.json. - Continuous state monitoring — background pollers keep the UI's view of Kasa, Sonos, and Tailwind state in sync without manual refresh.
Quick start
Requires Python ≥ 3.11 (3.14 is the targeted runtime).
Install from PyPI (recommended)
Install from PyPI (pipx recommended):
pipx install domesti-bot
domesti-bot-server # HTTP API + web UI on a free loopback port
domesti-bot-server --listen-all # LAN-visible bind for phone / tablet testing
domesti-bot # interactive REPL for troubleshooting
domesti-bot --version # package version and source commit
Set DOMESTI_API_KEY when binding to the LAN or any network you do not fully
trust. See Configuration below.
PyPI releases are built with the web bundle included; no Node.js is required at
runtime. See docs/RELEASING.md for how maintainers publish.
Develop from a git checkout
Uses uv for dependency management.
git clone https://github.com/the-hcma/domesti-bot.git
cd domesti-bot
uv sync --group dev
cd web && pnpm install --frozen-lockfile && pnpm run build && cd ..
# Start the HTTP server (binds 127.0.0.1 on a free port; auto-opens browser)
./scripts/domesti-bot-server
# Or expose to the LAN so you can validate the UI from a phone
./scripts/domesti-bot-server --listen-all
The startup banner prints the URL the server is listening on, including one
[http] network: http://<lan-ip>:<port> line per non-loopback interface when
--listen-all is passed.
Need the device-control REPL instead of the HTTP API? Run
./scripts/domesti-bot and follow the prompts.
Configuration
Most operation is zero-config — devices are discovered on the LAN via mDNS /
broadcast probes, and discovered configurations are persisted in an SQLite
cache (~/.config/domesti-bot/kasa_discovery.sqlite3 by default) so subsequent
startups are fast.
Optional environment variables:
| Variable | Effect |
|---|---|
DOMESTI_API_KEY |
When set, every /v1/… endpoint requires the X-Domesti-Api-Key header. Unset = unauthenticated (intended for trusted LAN only). |
DOMESTI_LISTEN_HOST |
Default bind address for the HTTP server. Overridden by --listen-host / --listen-all. |
DOMESTI_LISTEN_PORT |
Default TCP port. 0 = OS-allocated (the dev default). |
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. |
TAILWIND_TOKEN |
GoTailwind Local Control Key (six-digit code from the Tailwind dashboard). Overrides the encrypted DB copy when set. |
DOMESTI_SECRETS_KEY |
Fernet master key for encrypted SQLite secrets. Overrides domesti-secrets.json when set. |
DOMESTI_SECRETS_FILE |
Override path to the secrets JSON file (default: ./domesti-secrets.json at repo root). |
Pass --help to either script for the complete flag list.
Encrypted secrets
Discovery state (device configs, display names, UI preferences, cached Tailwind
host, and similar) lives in a single SQLite file. Upgrading domesti-bot does
not wipe that file — existing rows keep working; new tables (such as
app_secrets for encrypted values) are added automatically on first access.
To encrypt secrets at rest (for example the Tailwind token saved from the web UI), configure a Fernet master key:
-
Copy the template and generate a key (or use the REPL helper):
cp domesti-secrets.json.example domesti-secrets.json # in the REPL: setup-secrets
setup-secretscan generate a new key or accept an existing one, writesdomesti-secrets.jsonwith mode0600, and reminds you to restart the server. The file is listed in.gitignore— never commit it. -
Restart
domesti-bot-serverso the process reads the file. -
On desktop browsers, open the ☰ menu → Settings and paste the six-digit Tailwind Local Control Key. It is stored encrypted in SQLite and is never shown again. Restart once more so discovery picks up the token.
Precedence for the Tailwind token: --tailwind-token → TAILWIND_TOKEN
env → encrypted row in SQLite. Precedence for the Fernet key:
DOMESTI_SECRETS_KEY env → domesti_secrets_key in domesti-secrets.json.
For systemd, you can still use EnvironmentFile= for TAILWIND_TOKEN instead
of the database path; see docs/AGENTS.md for security notes.
Web UI overview
After starting the server, the landing page hydrates a tile UI:
Compact mobile layout on a phone: family sections (green frame = connected), per-device tiles (green = on / playing, red = off / paused), and the global Turn off / pause / close everything control at the top.
- One section per device family (
Lights & plugs,Sonos zones,Garage doors) with a family-coloured icon and frame. - One tile per device. Tap to toggle (on/off, play/pause, open/close); the tile updates optimistically and reconciles with the next background poll (every 5 seconds).
- Per-family bulk button (
Turn off all,Pause all,Close all) and a globalTurn off / pause / close everythingbutton at the top (warm orange, distinct from red per-tile off controls). - On desktop viewports, a ☰ menu with Settings (Tailwind token). The menu is hidden on mobile form factors.
- Per-tile "Exclude from all-off" (and analogous) checkbox so the top-of-page bulk action skips devices you don't want it touching.
- Connectivity indicator: family frames turn red when the backend is unreachable; all controls grey out until the next poll succeeds.
Progressive Web App (PWA)
The landing page is installable as a PWA on phones and desktops that support
it. Assets live under app/api/static/:
manifest.webmanifest— name, icons,display: standalonesw.js— service worker (also served atGET /sw.jsso scope covers/)icons/— launcher icons referenced by the manifest
The TypeScript bundle registers the worker on load. After you deploy a new
version, the service worker cache version in sw.js (for example
domesti-bot-pwa-v15) must be bumped so installed clients pick up HTML, CSS,
and dist/main.js changes.
Install requirements: Chromium-based browsers need a secure context
(https:// or http://127.0.0.1). On a plain HTTP LAN URL, you still get
manifest metadata in some browsers, but the install prompt may not appear until
you terminate TLS or use loopback. When the server is reachable with
--listen-all, open the dashboard from your phone at
http://<server-lan-ip>:<port>/ and use the in-app install banner when
offered.
Project layout
domesti-bot/
├── app/ Domain code (device managers, rule engine)
│ ├── *_device_manager.py One per family (kasa, sonos, gotailwind, …)
│ ├── db/ SQLAlchemy models + encrypted secrets
│ ├── kasa_discovery_store.py SQLite cache facade (shared by all managers)
│ └── api/ FastAPI HTTP surface (subpackage)
├── config/serve.py uvicorn entrypoint
├── tests/python/ pytest suite (hermetic + LAN-integration)
├── web/src/ TypeScript source for the tile UI
├── scripts/domesti-bot REPL CLI
├── scripts/domesti-bot-server HTTP server launcher
├── production/ systemd unit template + on-deploy hooks
├── docs/images/ README screenshots and other doc assets
└── docs/AGENTS.md Developer reference (canonical)
AGENTS.md at the root is a symlink to docs/AGENTS.md — both paths point
to the same canonical developer reference.
Development
# One-time setup
uv sync
# The full set of CI gates, in the order they run on every PR:
uv run pyright # type errors over app/, config/, scripts/, tests/
uv run pytest -m "not integration" -n auto # hermetic (parallel; matches CI)
shellcheck $(git ls-files scripts production/scripts | grep -Ev '\.(py|md|txt|yml|yaml|json|toml)$')
# Frontend, when web/src/ is touched:
cd web
pnpm install --frozen-lockfile
pnpm run typecheck
pnpm run build # writes app/api/static/dist/main.js
The full set of code-style, testing, security, and Git workflow conventions is
documented in docs/AGENTS.md. Notable rules:
- Python 3.14 targeted, modern typing only (
list[str], notList[str]), every public function annotated,pyrightenforced. uvfor dependency management — neverpipdirectly.- Methods and module-level functions sorted alphabetically inside each class.
- Sigs require
from __future__ import annotations. - All commits via Graphite-stacked PRs;
mainis protected, direct pushes are blocked at the server. - Conventional Commit messages, GPG-signed.
Production deployment
The production target is a systemd user unit. The template at
etc/systemd/domesti-bot.service is what
repository-helpers
setup-service installs (same @@REPO_DIR@@ contract as fpdf). It passes
--listen-all --listen-port 8003 so the API listens on all interfaces (use
DOMESTI_API_KEY on untrusted LANs). ExecStartPost curls GET /health on
loopback until the process answers. The deploy hook scripts/on-deploy
runs uv sync, rebuilds the web bundle when needed, and lets setup-service
restart the unit. For a system-level unit with a dedicated user, see
production/systemd/domesti-bot-server.service.template.
docs/AGENTS.md has the deployment-specific details — auth keys, log paths,
service management commands.
Contributing
Contributions are welcome and appreciated. Issues, bug reports, feature requests, and PRs are all on the table — whether you've spotted a typo, hit an edge case with your specific Kasa/Sonos/Tailwind hardware, or want to add a brand-new device family, the door is open.
The project uses Graphite for stacked PRs. The practical workflow is:
# 1. Start a stack from main
gt create feat/your-idea
# 2. Make the change, run the local gates (pyright + pytest, see Development)
# Each gate is also enforced in CI.
# 3. Commit + open PR
gt create --all --message "feat: short description"
gt submit --no-interactive --publish
For larger changes, stack the work into focused PRs so each one is independently reviewable. The stack of PRs visible on this repo is itself an example of the pattern.
The full Git / commit / PR conventions, including the merge-it label flow and
the protected-main ruleset, live in docs/AGENTS.md under
the Commits, Stacking & Pull Requests section.
License
MIT License — see LICENSE for the full text. Copyright (c) 2026 Henrique Andrade.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file domesti_bot-0.1.4.tar.gz.
File metadata
- Download URL: domesti_bot-0.1.4.tar.gz
- Upload date:
- Size: 175.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
406502b7bf1376271bd63376081c6a0ce9c29e41a4bc475a05d2dc07e8269cde
|
|
| MD5 |
91d9c4400fcb48f95f23cf2594b9b267
|
|
| BLAKE2b-256 |
63a320d461c1942e29ad06f513039816304733b2b0780b7d37d3ed0c9afc64a2
|
Provenance
The following attestation bundles were made for domesti_bot-0.1.4.tar.gz:
Publisher:
release-please.yml on the-hcma/domesti-bot
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
domesti_bot-0.1.4.tar.gz -
Subject digest:
406502b7bf1376271bd63376081c6a0ce9c29e41a4bc475a05d2dc07e8269cde - Sigstore transparency entry: 1593014329
- Sigstore integration time:
-
Permalink:
the-hcma/domesti-bot@8653bdfb9b2ed0ed6784a1839e2f7879724c87e0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/the-hcma
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@8653bdfb9b2ed0ed6784a1839e2f7879724c87e0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file domesti_bot-0.1.4-py3-none-any.whl.
File metadata
- Download URL: domesti_bot-0.1.4-py3-none-any.whl
- Upload date:
- Size: 203.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
18288ec6a7b428808310723e7584f8651137bc5edc107dd0acb1ffac7ea202fd
|
|
| MD5 |
dfe484bad6427bb8d3c24ceacf27fa4f
|
|
| BLAKE2b-256 |
4b337e33f41274f856c955d4b4cd2388d45dc257935374f2da4b893878b7626d
|
Provenance
The following attestation bundles were made for domesti_bot-0.1.4-py3-none-any.whl:
Publisher:
release-please.yml on the-hcma/domesti-bot
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
domesti_bot-0.1.4-py3-none-any.whl -
Subject digest:
18288ec6a7b428808310723e7584f8651137bc5edc107dd0acb1ffac7ea202fd - Sigstore transparency entry: 1593014504
- Sigstore integration time:
-
Permalink:
the-hcma/domesti-bot@8653bdfb9b2ed0ed6784a1839e2f7879724c87e0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/the-hcma
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@8653bdfb9b2ed0ed6784a1839e2f7879724c87e0 -
Trigger Event:
push
-
Statement type: