# 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](https://docs.astral.sh/uv/) recommended ## Install ```bash cd shelly-ui uv sync ``` ## Tests ```bash 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_json` mocked); **discovery helpers** (IPv6 URL host brackets, `probe_ip` port); 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.AppTest`](https://docs.streamlit.io/develop/api-reference/app-testing) runs the home page and multipage scripts in-process — **no live browser or running `streamlit run` required**. 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) ```bash uv run shelly-manager # or uv run python -m shelly_manager.cli ``` Options: ```bash 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) ```bash 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`](.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/config.toml) ([Streamlit theming](https://docs.streamlit.io/develop/concepts/configuration/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=`** 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 manager - `shelly_manager.api` — aioshelly-based Gen1/Gen2 clients - `shelly_manager.storage` — SQLite or Markdown persistence - `shelly_manager.cli` — Textual app - `shelly_manager.ui` — Streamlit multipage app ## Docs - [Shelly Gen1 API](https://shelly-api-docs.shelly.cloud/gen1/) - [Shelly Gen2+ API](https://shelly-api-docs.shelly.cloud/gen2/) ## Note Authentication on devices is not implemented in this version; unauthenticated devices are supported.