Shelly device management app with mDNS/subnet discovery, inventory, configuration, and mass operations for Gen1/Gen2+ devices. Includes .gitignore excluding runtime data (device DB, user config), AI conversation history, build artifacts, and common Python/OS patterns.
Shelly Manager
Manage Shelly Gen1 and Gen2+ devices on your LAN: discovery (mDNS + subnet scan), inventory, configuration, and mass operations.
Requirements
- Python 3.11+
- uv recommended
Install
cd shelly-ui
uv sync
Tests
uv sync
uv run pytest
Pytest is configured to disable the unraisableexception plugin: Streamlit’s AppTest runs pages that call asyncio.run(), and teardown can emit ResourceWarning for sockets/event loops that would otherwise make pytest exit with an ExceptionGroup even when all tests pass.
- Unit / async tests: models; subnet scan (
fetch_shelly_jsonmocked); discovery helpers (IPv6 URL host brackets,probe_ipport); SQLite + Markdown storage;DeviceManager(mocked mDNS + enrich). Real mDNS/zeroconf is not run in CI (no devices/network dependency). - Streamlit UI tests:
streamlit.testing.v1.AppTestruns the home page and multipage scripts in-process — no live browser or runningstreamlit runrequired.
To add browser E2E tests against your local UI (http://localhost:8501), use Playwright or Selenium in a separate optional suite; AppTest already covers UI structure without flakiness from a real server.
CLI (Textual TUI)
uv run shelly-manager
# or
uv run python -m shelly_manager.cli
Options:
uv run shelly-manager --help
uv run shelly-manager --storage sqlite --db-path ./data/devices.db
uv run shelly-manager --storage markdown --markdown-dir ./data/devices_md
The Textual TUI uses the same DeviceFilter as the web UI: generation, online/auth, combined name search, model & firmware substrings, IP prefix & MAC, comma-separated tags and capabilities, and dropdown presets for cached settings and status JSON. The Streamlit app adds exact model multiselect, tag multiselect, and custom dot- or |-separated paths for settings/status (use | when a key contains : e.g. switch:0|output).
Web UI (Streamlit)
uv run shelly-manager-ui
# or
uv run streamlit run src/shelly_manager/ui/Dashboard.py
Hot reload: shelly-manager-ui enables Streamlit’s Run on save (server.runOnSave), so edits to the app or imported modules trigger an automatic rerun. To disable (manual “Rerun” only), set SHELLY_UI_NO_RELOAD=1. Project .streamlit/config.toml also sets runOnSave when you use streamlit run from the repo root.
Settings persistence: The Settings page writes data/shelly_ui_config.json (by default, relative to the process working directory — run from the repo root so data/ is stable). Override the path with env SHELLY_UI_CONFIG. Without this file, settings lived only in browser session state and were lost on restart.
Text fields: Single-line inputs use a ✕ control on the right of the input row to clear the field in one click (Streamlit does not render a native in-field clear icon; multi-line JSON fields show ✕ on the top-right of the block). Implementation: shelly_manager.ui.components.clearable_input.
Each device shows an Open device web UI link using the device’s HTTP URL (http://…/), with correct bracketing for IPv6. Non-default ports from mDNS discovery are stored on the device as http_port.
Discovery: The app probes mDNS (_shelly._tcp / _http._tcp) and an optional subnet CIDR (GET http://IP/shelly on each address — use 192.168.x.0/24 for a whole LAN, or 192.168.x.y/32 for one IP). To add a single device by address, use Dashboard → Add Shelly device manually (IP only). Each discovery run uses the latest saved settings (get_config()), not only the in-memory DeviceManager snapshot. Dashboard → Refresh inventory from network only re-fetches devices already in inventory; it does not scan the subnet and ignores Subnet CIDR for finding new addresses. Discovery details on the Dashboard shows a text log (devices found + source) and JSON stats. The Python logger shelly_manager.core.device_manager also emits INFO lines per device. Devices that require login may not expose /shelly without auth.
The Dashboard page (Dashboard.py) is the main inventory: an Inventory overview with metrics (totals, online/offline, Gen1/2/3, auth, reboot, firmware-check buckets, tags) plus bar charts for generation and firmware status, top models and capabilities tables. Click the numbers in the overview to set URL query parameters (?preset=…, or ?preset=model&model=… / ?cap=… for model/cap rows) so Filters match that slice (applied once, then cleared from the URL). Then discovery, Add Shelly device manually (single IP), the same inline filters as Mass Config (including Tag filter: any / has tags / untagged), visible columns (shared preference), and a read-only table (including Needs reboot from Shelly.GetStatus → sys.restart_required after a live refresh, plus Reboot filtered devices that need reboot) (device name / IP links, Config snapshots column with the last N stored GetConfig snapshot labels per device — N is Settings → Config snapshots shown per device). FW update uses Shelly.CheckForUpdate when you run Check firmware, and otherwise falls back to Shelly.GetStatus → sys.available_updates (same information the device web UI uses; updated periodically on the device). Stored CheckForUpdate results are kept across refresh (they used to be cleared). Use Check firmware or Refresh so the column stays current. Filter FW update = Has stable update (default “offered” meaning) for devices with a stable channel build; use Beta only or Stable or beta to include beta-only offers. Additional pages: Device, Mass Config, Settings (see src/shelly_manager/ui/pages/). Dark theme: .streamlit/config.toml (Streamlit theming).
Device page: Firmware section shows the installed version, runs Shelly.CheckForUpdate, and can start Shelly.Update OTA (stable or beta) on Gen2+ without auth (device must reach the internet; the device reboots when the install finishes). Builds a dynamic editor from the last Shelly.GetConfig snapshot (one expander per top-level key: sys, wifi, switch:0, …). Edit scalars and nested objects in the form; lists are edited as JSON. Save to device (Gen2+) sends each changed section via Component.SetConfig RPC; then the inventory is refreshed. The Last config apply expander lists each section with a Restart required column when the RPC response asks for a reboot; you can Reboot device now from the UI. Gen1 only saves to local inventory (apply on the device separately). Discard resets the form widgets. Authenticated-only devices show raw JSON until credentials are supported. From the Dashboard or Mass Config inventory table, click the device name to open the in-app Device page, or click the IP to open the device’s http:// web UI (typically in a new tab). You can also open a device via ?device=<id> in the URL. Configuration history compares stored GetConfig snapshots as a unified diff (snapshot count per device is capped in Settings).
Mass configuration: Table-first: filters narrow which devices appear (same layout as the Dashboard). Include checkboxes + Select / deselect all choose which filtered rows receive an action — e.g. filter Needs reboot = yes, include those rows, then Bulk actions → Device control → Reboot devices. Action category groups Device control (reboot, identify), Diagnostics (live refresh, firmware check), and RPC configuration (BLE / MQTT / Cloud). Custom section config lets you paste a JSON object for one top-level GetConfig key (mqtt, wifi, coiot, sntp, sys, …) with merge or replace, for advanced bulk edits (same Component.SetConfig path as the Device page). Tags apply to selected rows only. Latest bulk operation results stays until dismissed. Refresh table live-refreshes listed devices. Settings → Mass Config: refresh table after bulk operation still applies after bulk runs. Name / IP are links. Gen2+-only actions skip Gen1 where appropriate.
Project layout
shelly_manager.core— models, discovery, device managershelly_manager.api— aioshelly-based Gen1/Gen2 clientsshelly_manager.storage— SQLite or Markdown persistenceshelly_manager.cli— Textual appshelly_manager.ui— Streamlit multipage app
Docs
Note
Authentication on devices is not implemented in this version; unauthenticated devices are supported.