diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f46e5f --- /dev/null +++ b/README.md @@ -0,0 +1,433 @@ +# anker-shelly-bridge + +

+ Live monitor dashboard showing battery level, solar input, grid input and load for an Anker SOLIX F3000, with Shelly switch states and a rolling history chart +

+ +Automate a Shelly smart plug from live Anker SOLIX telemetry. + +Your SOLIX device reports battery level, solar input, and load over Anker's +cloud MQTT service. This reads that telemetry and switches a Shelly plug on +your local network according to rules you write in a plain text file. + +The common use is controlling grid charging: plug the SOLIX into a Shelly, and +let the battery level decide when to draw from the wall. + +**The SOLIX device is read-only.** This tool never sends it a command. The +Shelly plug is the only thing it ever switches. + +> Unofficial and unaffiliated with Anker or Allterco Robotics. SOLIX and Shelly +> are trademarks of their respective owners. Built on the community +> [anker-solix-api](https://github.com/thomluther/anker-solix-api) library. + +--- + +## Quick start + +You need a SOLIX device that connects to WiFi, a Shelly plug on the same +network, and your Anker account login. + +```bash +git clone https://github.com/YOURNAME/anker-shelly-bridge.git +cd anker-shelly-bridge +./start.sh +``` + +On Windows, double-click `start.bat`. + +That is the whole install. `start.sh` creates its own Python environment, +installs everything, and hands over to a guided setup with eight steps: + +1. **Dependencies**: installs whatever is missing +2. **Anker account**: your login, stored locally with owner-only permissions +3. **Find your SOLIX device**: connects and captures every field it reports +4. **Find your Shelly plug**: scans the local network +5. **Check the plug**: finds schedules and timers already on it that would + conflict, and offers to remove them +6. **Notifications**: optional push to your phone, with a QR code to scan +7. **Rules**: pick a strategy and answer a few questions in plain language +8. **Test, then start**: a live dry run against your hardware that switches + nothing, then offers to install a background service + +Nothing touches your hardware until step 8 asks. Ctrl-C is safe at any point. + +Needs Python 3.12+ and git. If they are missing, `start.sh` tells you how to +install them for your platform. + +--- + +## What it does + +### Reads your SOLIX device + +Connects to Anker's cloud MQTT broker, captures a live telemetry snapshot, and +writes a device profile you can read. A portable power station typically +reports around 44 fields: battery percentage, per-string solar input, AC and DC +output, temperature, port states. + +It also computes derived fields, so rules can use `pv_total` instead of adding +`pv_1_power` and `pv_2_power` by hand: + +| Field | Meaning | +|---|---| +| `pv_total` | all solar strings added together, in watts | +| `pv_surplus` | solar minus load; positive means the surplus is charging the battery | +| `pv_covers_load` | true when solar alone is carrying everything | +| `usb_total` | combined USB output | +| `soc_headroom` | percentage points above the configured floor | + +```bash +solixauto fields # every usable field name +solixauto status --watch # live values +``` + +### Controls your Shelly plug + +Local HTTP only, no cloud. Gen1 and Gen2+ including Gen4. Discovery uses mDNS +plus a subnet scan across every private network it can see, so machines with +several interfaces work. + +```bash +solixauto switch on|off|status +``` + +Every command reads the state back from the device afterwards rather than +trusting the HTTP response. + +### Runs your rules + +A power profile is a YAML file linking one SOLIX device to one Shelly channel: + +```yaml +rules: + - name: top up from grid when low + when: battery_soc <= 35 + for: 2m + then: target.on + + - name: stop charging when full enough + when: battery_soc >= 85 + for: 2m + then: target.off +``` + +`when` is an expression over any field the device reports. `for` is how long +the condition must hold before anything happens, so a passing cloud or a +momentary load spike cannot toggle the relay. `priority` breaks ties when rules +disagree. + +Expressions run in a sandbox allowing only comparisons, `and`/`or`/`not`, and +arithmetic. Function calls and attribute access are rejected at parse time, so +a power profile cannot execute code. + +Full rule reference with worked examples: [docs/RULES.md](docs/RULES.md). +The same document is written into your data directory during setup. + +--- + +## Live monitor + +```bash +solixauto monitor +``` + +Opens a dashboard at `http://127.0.0.1:8765` showing every device you have +saved: battery level, solar in, grid in, load out, and each Shelly's switch +state and draw. It refreshes every five seconds and keeps a rolling chart. + +Each device gets a **power bus**: solar and grid drawn from the left, load +from the right, all to one scale. If the left outweighs the right, the battery +is filling. Light and dark themes, remembered between visits. + +Add `--host 0.0.0.0` to reach it from a phone on the same network. + +Anker readings come from a running automation rather than a second cloud +connection, so start a service first. Two MQTT sessions on one Anker account +invite rate limiting. Shelly devices are polled directly over local HTTP and +always show live. + +--- + +## Safety + +Controlling the power supply to a battery has a specific failure mode: turn +charging off, let the battery run flat, and the device drops off the network, +at which point nothing can turn charging back on. The design is built around +preventing that. + +### The battery floor + +```yaml +safety: + battery_floor: + at_or_below: 20 + release_at: 40 + then: target.on +``` + +Checked before any rule. It **bypasses the rate limits**, **outranks every +rule**, and **latches** once tripped, so nothing can turn the plug off again +until the battery reaches `release_at`. Without the latch, a solar rule could +release it at 21% straight back into whatever drained it. + +`release_at` must be meaningfully above `at_or_below`; the profile is rejected +otherwise. + +### Failing in the safe direction + +```yaml +source: + stale_after: 300s + on_stale: safe_state +safe_state: on +``` + +If telemetry stops arriving, rules are never evaluated against stale values. +`hold` leaves the plug alone, `safe_state` drives it to a known state, `stop` +exits. When the plug supplies power to the SOLIX device, `safe_state: on` means +losing sight of it fails toward charging. + +Rules are also never evaluated against **partial** telemetry. If a field a rule +needs has not arrived yet, nothing is evaluated at all, including the floor. + +### Rate limits + +```yaml +limits: + min_seconds_between_actions: 60 + max_actions_per_hour: 20 +``` + +A hard backstop independent of dwell times. If a rule somehow oscillates, this +caps the damage. The battery floor deliberately ignores it. + +### Conflict detection + +A Shelly can hold schedules, auto-on/auto-off timers, and webhooks on the +device itself. They run whether or not this tool is running, and they silently +override it. + +```bash +solixauto conflicts # list them +solixauto conflicts --fix # remove them, asking before each change +``` + +Checked at discovery, at `--test`, and again at engine startup. + +One limitation: schedules created as Shelly Cloud **scenes** live in the cloud +rather than on the device and cannot be seen over local HTTP. If behaviour +still looks wrong after `conflicts` is clean, check the app's scenes. + +--- + +## Testing before you trust it + +```bash +solixauto run --test +``` + +Validates syntax, checks every field name against what the device actually +reports, warns about missing deadbands and short dwell times, connects to both +devices, and prints what each rule would do right now. Switches nothing. + +```bash +solixauto run --test --offline +``` + +Same checks without connecting, evaluated against values captured at discovery. + +```bash +solixauto run --test --simulate battery_soc=12 +``` + +Force a rule to fire without waiting for real conditions. Prove your +low-battery logic works at 2pm on a sunny day, and see the exact notification +text it would send. + +```bash +solixauto run --dry-run +``` + +The full engine loop with real telemetry, narrating every cycle, but no switch +command and no notification. + +--- + +## Notifications + +```bash +solixauto notify-setup +``` + +Interactive: pick a channel, it configures and tests it. For ntfy it generates +a random private topic, shows QR codes for the app store and the topic, and +sends a test push. + +Supported: **ntfy** (free, no account), **Pushover**, **email**, **Telegram**, +**webhook** (Slack/Discord/custom), and **desktop** (osascript on macOS, +notify-send on Linux, PowerShell on Windows). + +Credentials live in `notifications.yaml` with owner-only permissions, never in +power profiles, so profiles stay safe to share. + +Battery floor alerts fire **even when notifications are otherwise disabled**, +at elevated priority, bypassing the throttle. Muting routine chatter should not +silence a battery emergency. + +The Anker app and the Shelly app cannot receive custom push messages from an +external program, so neither is an option. + +--- + +## Running it unattended + +```bash +solixauto service # install and start +solixauto service --status +solixauto service --uninstall +``` + +macOS gets a LaunchAgent, Linux a systemd user unit, both starting at login and +restarting on crash. Windows prints the Task Scheduler command. + +If the machine sleeps, the automation sleeps with it and the safety floor +cannot protect anything. Use an always-on machine, or disable sleep. + +--- + +## Layout + +Code lives where you cloned it. Everything else lives in `~/solix-automation`: + +``` +~/solix-automation/ +├── venv/ Python environment created by start.sh +├── device-profiles/ +│ ├── anker/ what your SOLIX device reports +│ └── shelly/ your plugs and their capabilities +├── power-profiles/ your rules, hand-editable +│ └── README.md full rule reference with worked examples +├── notifications.yaml channel credentials, mode 0600 +├── state/runtime.json last known target state +└── logs/automation.log every action taken +``` + +Override the root with `SOLIXAUTO_HOME`. + +--- + +## Commands + +``` +solixauto setup guided setup, start here +solixauto doctor check this machine is set up correctly + +solixauto discover-anker find and profile SOLIX devices +solixauto discover-shelly find and profile Shelly devices +solixauto devices list saved device profiles +solixauto name "" name a device, rename its file +solixauto fields field names usable in rules +solixauto status live telemetry +solixauto switch on|off manual control +solixauto conflicts automation set on the plug itself +solixauto monitor live dashboard in your browser + +solixauto new-profile scaffold a power profile +solixauto profiles list power profiles +solixauto validate check without connecting +solixauto run --test test against real devices, switch nothing +solixauto run run it + +solixauto notify-setup configure push notifications +solixauto notify-test send a test +solixauto notify-qr show the ntfy topic QR again + +solixauto service run in the background at login +``` + +Profiles resolve by friendly name, serial, ID, MAC, or IP, so all of these +reach the same device: + +```bash +solixauto switch "Garage Plug" on +solixauto switch 192.168.1.50 on +solixauto switch garage-plug on +``` + +--- + +## Device support + +Anything the upstream +[anker-solix-api](https://github.com/thomluther/anker-solix-api) library +supports **and** that holds a cloud connection. + +Models that pair with the Anker app over Bluetooth only, where you press a +button on the unit to make it discoverable, publish nothing to the MQTT broker +and cannot be used. The F2000 / PowerHouse 767 works this way. Discovery detects and skips +them. + +Shelly Gen1 and Gen2+ including Gen4, over local HTTP. + +Developed against a SOLIX F3000 (A1782) and a Shelly Plug US Gen4. Other +combinations should work; reports welcome. + +--- + +## Known limitations + +- Field mappings come from a community reverse-engineering effort, not from + Anker. Fields ending in `?` or starting with `unknown_` are unconfirmed, so + do not build rules on them. +- **Verify any field against the Anker app before trusting it.** The solar + fields in particular should be watched in daylight and compared to the app + before writing solar rules. +- Shelly Cloud scenes and app-set device names are invisible over local HTTP. +- Requires a machine that stays awake. +- Anker rate-limits logins; avoid restarting the service in a tight loop. + +--- + +## Troubleshooting + +**A device reports no telemetry.** It is powered off, asleep, off WiFi, or +Bluetooth-only. Confirm it shows online in the Anker app, then retry that +device alone with `--sn `. + +**Shelly discovery finds nothing.** Try `--host 192.168.1.50` or +`--network 192.168.1.0/24`. Give your plugs DHCP reservations; `access.host` is +a fixed address in the profile. + +**The automation does nothing.** Check `solixauto service --status` +and the log. Rules only act after their `for:` dwell has fully elapsed. + +**Something switches the plug unexpectedly.** Run `solixauto conflicts `, +then check the Shelly app for cloud scenes. + +**Anything else.** `solixauto doctor` reports platform, dependencies, +credentials, network, and notification status in one place. + +--- + +## Documentation + +- [docs/RULES.md](docs/RULES.md): power profile format, every option, worked + automation examples +- [docs/TESTING.md](docs/TESTING.md): staged checklist for validating a new + setup before trusting it with real hardware +- [examples/](examples/): a solar failover profile and an annotated + notifications config + +## Contributing + +Bug reports welcome, particularly for device models not listed above. Include +the output of `solixauto doctor` and the relevant section of +`logs/automation.log`. + +**Never paste `notifications.yaml`, your `.env`, or an ntfy topic** into an +issue. Device profiles contain serial numbers; redact them if that matters to +you. + +## License + +MIT. See [LICENSE](LICENSE). diff --git a/screenshot.jpg b/screenshot.jpg new file mode 100644 index 0000000..625daa2 Binary files /dev/null and b/screenshot.jpg differ