Metadata-Version: 2.4
Name: shelly-backup
Version: 0.2.0
Summary: Verified local backup & restore for Shelly Gen2+ devices, scripts included
Author: Germain Masse
License-Expression: MIT
Project-URL: Homepage, https://github.com/gmasse/shelly-backup
Project-URL: Repository, https://github.com/gmasse/shelly-backup
Project-URL: Issues, https://github.com/gmasse/shelly-backup/issues
Keywords: shelly,gen2,gen3,backup,restore,home-automation,rpc
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Home Automation
Classifier: Topic :: System :: Archiving :: Backup
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28
Requires-Dist: PyYAML>=6.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: mypy>=1.11; extra == "dev"
Requires-Dist: types-requests; extra == "dev"
Requires-Dist: types-PyYAML; extra == "dev"
Dynamic: license-file

# shelly-backup

**Verified local backup & restore for Shelly Gen2+ devices — scripts included.**

Backs up the full configuration *and the complete source of every script* from
Shelly Gen2/Gen3/Gen4 devices over the local RPC API, and restores it. No cloud,
no account, no agent on the device.

> **Unofficial project.** Not affiliated with, endorsed by, or supported by
> Allterco Robotics EOOD. "Shelly" is their trademark, used here only to say what
> these tools talk to. Use at your own risk — see the warranty disclaimer in
> [LICENSE](LICENSE).

## Why this exists

Shelly Cloud does not back up locally-stored scripts. So people reach for a
local tool — and the ones I looked at get scripts subtly, dangerously wrong.

`Script.GetCode` is a **chunked** RPC. It returns a slice of the source plus a
`left` field saying how many bytes remain. Call it once and read `data`, and you
get a perfectly well-formed response containing a plausible-looking prefix of
your script. Every script longer than one RPC frame is **silently truncated**:

```
real script size:             13489 bytes
naive single GetCode gives:    1024 bytes   <-- looks fine, is not
shelly-backup gives:          13489 bytes
```

The restore path has the mirror bug: pushing the whole body in one
`Script.PutCode` fails for anything over the frame limit, and if the tool
discards that error, your script restores **empty**.

A backup you trust that contains half a script is worse than no backup at all.
So this tool is built around one guarantee:

**Every script is read in full, checksummed, and — on restore — read back off
the device and compared before we call it a success. Anything less is an error,
loudly, with a non-zero exit code.**

## Install

Requires Python 3.11+.

```bash
uv tool install shelly-backup     # recommended -- installs the CLI in its own env
pip install shelly-backup         # fallback
```

From source:

```bash
git clone https://github.com/gmasse/shelly-backup
cd shelly-backup
uv sync --extra dev               # fallback: pip install -e ".[dev]"
uv run shelly-backup --help
```

## Use

```bash
cp devices.example.yaml devices.yaml   # add your device IPs
shelly-backup backup                   # -> backups/<device>/
shelly-backup verify                   # re-check checksums on disk
shelly-backup restore backups/kitchen --host 192.168.1.50        # dry run
shelly-backup restore backups/kitchen --host 192.168.1.50 --yes  # apply
```

### Passwords

If you enabled device auth, `-p` asks for the password once and uses it for every
device that needs one (the username is always `admin`):

```bash
shelly-backup backup -p                                   # asks once, echoes nothing
pass show shelly | shelly-backup backup -p                # reads one line from the pipe
SHELLY_PASSWORD=... shelly-backup backup                  # for cron
shelly-backup restore backups/kitchen --host 192.168.1.50 -p
```

Where a password comes from, highest first:

| Source | Applies to |
|---|---|
| `password:` in `devices.yaml` | that device only — a per-device override |
| `-p` / `--ask-password` | every device without one in the inventory |
| `SHELLY_PASSWORD` | same, without asking — the non-interactive path |

Without `-p`, a device that turns out to need a password is asked about only if
you are at a terminal. Under cron there is nobody to ask, so it fails with a
non-zero exit code and a message rather than blocking the backup forever. There
is deliberately no flag that takes a password as a value: that would put it in
your shell history and in `ps` output for every user on the machine. Passwords
are never echoed, logged, or written to a backup.

## What a backup looks like

Git-friendly by design — scripts are real `.js` files, so `git diff` shows what
actually changed in your automation:

```
backups/kitchen/
├── manifest.json      device info, per-script SHA-256, timestamp
├── config.json        Shelly.GetConfig — every component
├── kvs.json           key-value store, if present
└── scripts/
    ├── 01_solar.js        complete source
    ├── 01_solar.json      id, name, enable, running, sha256, bytes
    └── 02_boiler.js
```

Commit `backups/` to a private repo and you get versioned config history for
free.

## Safety decisions on restore

| Behaviour | Why |
|---|---|
| **Dry run by default** | `--yes` required to write anything. |
| **Network components skipped** | Restoring `wifi`/`eth`/`cloud`/`mqtt` can lock you out — and since the API redacts passwords, a restored `wifi` block would carry an empty one. `--include-network` to override. |
| **Scripts matched by name, updated in place** | Blindly calling `Script.Create` duplicates every script on a device that already has them. |
| **Refuses on model mismatch** | Won't push a Pro 2PM config onto a Mini 1PM. |
| **Checksums verified before send, and after write** | Catches a corrupt backup *and* a bad upload. |
| **No silent failures** | Every error is reported and sets the exit code. |

## What is *not* in a backup

Passwords. The device API redacts the Wi-Fi password and the auth password, so
they cannot be captured — or restored. Record them separately.

## Supported

Gen2+ devices sharing the JSON-RPC API — Pro 1PM/2PM, Plus series, Mini 1PM G3,
Gen4. Gen1 devices use a different API and are out of scope.

## Development

```bash
uv sync --extra dev
uv run pytest -v

# the lint gate CI enforces
uv run ruff check .
uv run ruff format --check .
uv run mypy
```

Without uv: `pip install -e ".[dev]"`, then run the same commands directly.

The test suite runs against an in-memory Shelly that chunks exactly like real
firmware. `test_naive_single_call_truncates` pins the upstream bug explicitly,
so nobody can "simplify" the chunking away later.

## License

MIT
