Files
jonas 71803418e5 Initial commit: Shelly Manager with Textual CLI, Streamlit UI, and comprehensive .gitignore
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.
2026-03-23 21:51:59 +01:00

89 lines
9.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: Streamlits `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 Streamlits **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 devices 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 devices **`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 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.