A small, fully-typed Python CLI + TUI to discover and manage your Shelly Gen2 / Gen3 devices on the LAN.
- Scan the network with mDNS, pick which devices to track
- Live status table: firmware, on/off for switches, open/closed + position for covers
- Bulk reboot and OTA firmware update with multi-select
- All state in plain TOML under your platform config dir
(
~/Library/Application Support/lly/on macOS,~/.config/lly/on Linux)
Gen1 (
shelly1,shelly2.5, originalshellyDimmer, ...) is intentionally out of scope — Gen2/Gen3 only.
- macOS or Linux
- Your Shellies reachable on the same L2 network (mDNS must not be blocked between your machine and the devices — VLAN segregation often breaks this)
brew tap lailo/tap
brew install lly
lly --helpNo Python required. The binary is self-contained.
Grab lly-darwin-arm64.tar.gz from the
latest release, extract, and
put lly on your $PATH. Intel Macs are not currently built — install from
source.
Requires uv:
curl -LsSf https://astral.sh/uv/install.sh | sh # if you don't have uv yet
git clone https://github.com/lailo/lly.git
cd lly
uv sync # installs deps into .venv
uv run lly --help # check the CLI is wired upFor a bare lly command on your $PATH (Linux):
uv tool install --editable .
lly --helpEvery uv run lly ... example below also works as plain lly ... after
either install path.
Most home setups have one shared password on all Shelly devices (username
is always admin on Gen2/3). Save it once:
uv run lly config set-password
# Default Shelly password (empty to clear): ********This writes (encrypted-at-rest only by your filesystem permissions, not
encrypted at the app level) to config.toml in your platform config dir (see
"File layout" below).
You can override per-device later by editing devices.toml in the config dir and
adding password_override = "..." to a specific entry.
Inspect your config any time:
uv run lly config showuv run llyIn the TUI:
| Key | Action |
|---|---|
s |
Open the mDNS scan modal |
space |
Toggle selection on the row under the cursor |
a |
Select all / clear selection |
R |
Refresh status now |
r |
Bulk reboot selected (or all if none selected) |
u |
Bulk update selected (or all if none selected) |
q |
Quit |
Inside the scan modal:
| Key | Action |
|---|---|
space |
Toggle pick on the row under cursor |
a |
Pick all / none |
enter |
Import picked rows |
escape |
Cancel |
Imported devices are merged into devices.toml in your config dir. Your custom
names and per-device password overrides are preserved across re-imports.
uv run lly scan # one-shot scan, prints a table, doesn't save
uv run lly add --yes # scan and import everything found
uv run lly add # scan, then prompt for which ids to import
uv run lly list # show the saved fleetuv run lly status # live state of every saved device
uv run lly status shellyplus1pm-aabbccddeeff # one device
uv run lly action reboot # reboot all
uv run lly action reboot shellyplus1pm-aabb # reboot one
uv run lly action update shellyplus1pm-aabb # OTA via official Shelly serverThe TUI auto-refreshes status every 5 s (configurable in config.toml).
OTA note: Shelly.Update against the official firmware server is a
no-op when a device is already on the latest stable firmware — that
surfaces as a [red]err[/] row with message no update available. That's
not a real failure; expect it most of the time.
# macOS: ~/Library/Application Support/lly/
# Linux: ~/.config/lly/
<config-dir>/
├── config.toml # default password, scan timeout, refresh interval
├── devices.toml # the curated fleet
└── lly.log # JSON-Lines log, rotated at 1 MB
The exact location follows platformdirs.user_config_dir("lly"); both
config.toml and devices.toml are written 0600 because they contain
plaintext device passwords.
devices.toml is plain TOML — feel free to hand-edit, rename, or remove
entries. The format:
[[devices]]
id = "shellyplus1pm-08b61fcaaabb"
name = "kitchen light"
host = "192.168.1.42"
model = "Plus1PM"
gen = 2
kind = "switch"
fw_version = "1.4.4"
# password_override = "device-specific-password" # optionaluv run ruff check . # lint
uv run ruff format . # format
uv run mypy # strict typecheck
uv run pytest -q # tests (25 of them, all offline)The codebase is from __future__ import annotations everywhere, mypy --strict, and tested with respx mocks for the HTTP layer and MagicMock
fakes for zeroconf. No real network is needed to run the test suite.
Scan finds nothing. mDNS uses UDP/5353. Check:
- The Shellies and your machine are on the same VLAN/SSID
- mDNS isn't filtered by your router or AP (Unifi calls it "IGMP snooping" / "multicast enhancement"; aruba/cisco have similar)
- The devices are actually Gen2/3 —
genmust appear in their TXT record
You can sanity-check with:
dns-sd -B _shelly._tcp . # macOS
avahi-browse -r _shelly._tcp # Linux401 Unauthorized on every RPC. Your device has a web UI password set
and lly config set-password either wasn't run or the password is wrong.
The username is always admin (Shelly Gen2+) — you cannot change it.
OTA always says "no update available". That's not an error — it's
Shelly.Update reporting your firmware is current. Run
uv run lly status to confirm fw_version matches the latest Shelly
stable channel.
MIT, with the Commons Clause condition layered
on top — see LICENSE. In plain English: use it, fork it, modify
it, distribute it freely; just don't sell it (or a hosted/consulting
service whose value derives substantially from it). Personal and internal use
at your company is fine.
Note: the Commons Clause makes this source-available, not OSI-approved open source.