Initial commit
This commit is contained in:
commit
56e75d9cde
25 files changed
+8285
No files matched your search
+232
@@ -0,0 +1,232 @@
|
||||
# Power profiles
|
||||
|
||||
A power profile is a plain YAML file linking one Anker SOLIX device to one
|
||||
Shelly switch channel. You can edit it in any text editor.
|
||||
|
||||
The Anker device is **read-only**. The engine reads its telemetry and never
|
||||
sends it a command. The only thing that gets switched is the Shelly.
|
||||
|
||||
## Layout
|
||||
|
||||
device-profiles/anker/ generated, one file per Anker device
|
||||
device-profiles/shelly/ generated, one file per Shelly device
|
||||
power-profiles/ yours, hand-edited
|
||||
state/runtime.json last known target state per profile
|
||||
logs/automation.log action history
|
||||
|
||||
## Workflow
|
||||
|
||||
solixauto discover-anker
|
||||
solixauto discover-shelly
|
||||
solixauto new-profile solar-failover --template solar
|
||||
solixauto fields A1782-<serial>
|
||||
solixauto run solar-failover --test
|
||||
solixauto run solar-failover
|
||||
|
||||
## Anatomy of a rule
|
||||
|
||||
- name: grid assist when solar drops
|
||||
when: pv_total < 150
|
||||
for: 90s
|
||||
then: target.on
|
||||
priority: 0
|
||||
|
||||
`when` is an expression over any field listed in the Anker device profile,
|
||||
under `readable` or `derived`. `solixauto fields <device>` prints them with
|
||||
current sample values.
|
||||
|
||||
`for` is the dwell time. The condition must stay true for this long before
|
||||
anything happens. Without it, a cloud passing over your array would cycle the
|
||||
relay repeatedly.
|
||||
|
||||
`then` is `target.on`, `target.off`, or `none`.
|
||||
|
||||
`priority` breaks ties. If two rules are ready at the same moment and disagree,
|
||||
the higher number wins. A low-battery override should outrank normal solar
|
||||
logic.
|
||||
|
||||
## Use a deadband
|
||||
|
||||
This is the single most important thing to get right:
|
||||
|
||||
# WRONG - will chatter around 200W
|
||||
- when: pv_total < 200
|
||||
then: target.on
|
||||
- when: pv_total > 200
|
||||
then: target.off
|
||||
|
||||
# RIGHT - 150W of deadband between the two
|
||||
- when: pv_total < 150
|
||||
then: target.on
|
||||
- when: pv_total > 300
|
||||
then: target.off
|
||||
|
||||
`--test` warns when it detects the same threshold used for both directions.
|
||||
|
||||
## Useful automations
|
||||
|
||||
**Solar failover.** Solar covers the load most of the day; pull from the wall
|
||||
only when production drops.
|
||||
|
||||
- name: grid assist when solar drops
|
||||
when: pv_total < 150
|
||||
for: 90s
|
||||
then: target.on
|
||||
|
||||
- name: release grid when solar recovers
|
||||
when: pv_total > 300
|
||||
for: 2m
|
||||
then: target.off
|
||||
|
||||
**Low-battery charge.** No solar involved.
|
||||
|
||||
- name: charge when battery is low
|
||||
when: battery_soc <= 15
|
||||
for: 30s
|
||||
then: target.on
|
||||
|
||||
- name: stop charging when battery is healthy
|
||||
when: battery_soc >= 60
|
||||
for: 2m
|
||||
then: target.off
|
||||
|
||||
**Overnight top-up.** Combine conditions.
|
||||
|
||||
- name: cheap overnight charging
|
||||
when: battery_soc < 80 and pv_total < 20
|
||||
for: 5m
|
||||
then: target.on
|
||||
|
||||
**Load shedding.** Cut a non-essential circuit when the battery is draining.
|
||||
|
||||
- name: shed load
|
||||
when: battery_soc < 30 and ac_input_power == 0
|
||||
for: 2m
|
||||
then: target.off
|
||||
|
||||
**Thermal guard.** Higher priority so it outranks everything else.
|
||||
|
||||
- name: stop charging when hot
|
||||
when: temperature >= 45
|
||||
for: 60s
|
||||
then: target.off
|
||||
priority: 200
|
||||
|
||||
## Safety settings
|
||||
|
||||
`stale_after` and `on_stale` control what happens when telemetry stops
|
||||
arriving. Rules are never evaluated against stale data.
|
||||
|
||||
- `hold` keeps the relay wherever it is. Default and safest.
|
||||
- `safe_state` drives the relay to the `safe_state:` value.
|
||||
- `stop` exits the engine.
|
||||
|
||||
`limits` is a hard backstop independent of dwell times:
|
||||
|
||||
limits:
|
||||
min_seconds_between_actions: 60
|
||||
max_actions_per_hour: 20
|
||||
|
||||
If a rule somehow oscillates, this caps the damage.
|
||||
|
||||
## Notifications
|
||||
|
||||
Turn them on in the profile:
|
||||
|
||||
notifications:
|
||||
enabled: true
|
||||
channels: [ntfy]
|
||||
title: "{profile}"
|
||||
template: >-
|
||||
{source_name} battery {battery_soc}%, solar {pv_total}W.
|
||||
{target_name} turned {action}.
|
||||
throttle: 5m
|
||||
on:
|
||||
- action
|
||||
|
||||
Then per rule, `notify: on` or `notify: off` to include or exclude it. Omit it
|
||||
to inherit. A rule can also carry its own wording:
|
||||
|
||||
- name: emergency charge on low battery
|
||||
when: battery_soc <= 15
|
||||
for: 30s
|
||||
then: target.on
|
||||
priority: 100
|
||||
notify:
|
||||
template: >-
|
||||
{source_name} has {battery_soc}% battery remaining.
|
||||
{target_name} turned on AC power.
|
||||
priority: high
|
||||
|
||||
### Template fields
|
||||
|
||||
Any field from the device profile works, plus:
|
||||
|
||||
{profile} power profile name
|
||||
{rule} rule that fired
|
||||
{condition} the rule's when expression
|
||||
{action} ON or OFF
|
||||
{action_word} on or off
|
||||
{source_name} Anker device name, falling back to model
|
||||
{source_model} e.g. SOLIX F3000
|
||||
{source_serial}
|
||||
{target_name} Shelly device name, falling back to model
|
||||
{target_model}
|
||||
{target_host}
|
||||
{time}
|
||||
|
||||
`--test` checks every field name in your templates against the device profile,
|
||||
so a typo is caught before it ships a message reading `battery ?%`.
|
||||
|
||||
### Channels
|
||||
|
||||
Credentials live in `../notifications.yaml`, which is created with owner-only
|
||||
permissions. Power profiles stay free of secrets.
|
||||
|
||||
- **ntfy** - recommended. Free, no account. Install the app, pick an
|
||||
unguessable topic name, subscribe. Anyone who knows the topic can read your
|
||||
alerts, so make it long.
|
||||
- **pushover** - $5 once per platform. Priority 2 alerts repeat until you
|
||||
acknowledge them.
|
||||
- **email** - SMTP. Gmail requires an App Password.
|
||||
- **telegram** - free bot.
|
||||
- **webhook** - Slack, Discord, or generic JSON.
|
||||
- **desktop** - local notification on the machine running the engine.
|
||||
Uses osascript on macOS, notify-send on Linux, PowerShell on Windows.
|
||||
Run `solixauto doctor` to see which backend was detected.
|
||||
|
||||
The Anker app and the Shelly app cannot receive custom push messages from an
|
||||
external program, so neither is an option here.
|
||||
|
||||
Test a channel:
|
||||
|
||||
solixauto notify-test
|
||||
solixauto notify-test --channel ntfy
|
||||
|
||||
### Throttling
|
||||
|
||||
`throttle` is per rule. An identical repeated message inside the window is
|
||||
dropped. This is separate from the switching rate limits, so a stuck condition
|
||||
cannot flood your phone even if the relay is behaving.
|
||||
|
||||
### Other events
|
||||
|
||||
on:
|
||||
- action
|
||||
- stale
|
||||
|
||||
`stale` fires once when telemetry stops arriving and is worth enabling if you
|
||||
depend on the automation.
|
||||
|
||||
## Testing
|
||||
|
||||
solixauto run <profile> --test
|
||||
|
||||
Validates syntax, checks that every field you reference actually exists on the
|
||||
device, warns about missing deadbands and short dwell times, connects to both
|
||||
devices, and prints what each rule would do right now. Nothing is switched.
|
||||
|
||||
solixauto run <profile> --test --offline
|
||||
|
||||
Same checks without connecting. Rules are evaluated against the sample values
|
||||
captured during discovery.
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
# Testing before you publish
|
||||
|
||||
Work down this list. Each stage only depends on the ones above it, so a failure
|
||||
tells you exactly which layer is broken. Nothing switches real hardware until
|
||||
stage 5.
|
||||
|
||||
## What is already verified
|
||||
|
||||
Rule parsing, the expression sandbox, dwell timing, priority resolution, rate
|
||||
limiting, profile validation, template rendering, and notification throttling
|
||||
all have test coverage.
|
||||
|
||||
## What has never run against real hardware
|
||||
|
||||
Every Anker library call, every Shelly HTTP call, every notification channel,
|
||||
and the engine loop end to end. Those are what these stages exercise.
|
||||
|
||||
---
|
||||
|
||||
## Stage 1 - environment
|
||||
|
||||
python solixauto.py doctor
|
||||
|
||||
Expect: no PROBLEM lines. Warnings about optional packages are fine.
|
||||
|
||||
If `anker-solix-api` shows MISSING, you are running the wrong interpreter. Use
|
||||
the venv that already works with mqtt_monitor.
|
||||
|
||||
---
|
||||
|
||||
## Stage 2 - Anker discovery
|
||||
|
||||
python solixauto.py discover-anker
|
||||
|
||||
Expect: one profile per owned device, each reporting a field count.
|
||||
|
||||
python solixauto.py devices
|
||||
python solixauto.py fields A1782-<serial>
|
||||
|
||||
Check specifically:
|
||||
|
||||
- `pv_total` appears under derived, and equals `pv_1_power + pv_2_power`
|
||||
- `battery_soc` is present and matches the Anker app right now
|
||||
- the field count is roughly what mqtt_monitor showed you
|
||||
|
||||
**If a device reports no telemetry:** it is powered off, asleep, off WiFi, or
|
||||
Bluetooth-only.
|
||||
|
||||
Some models pair with the Anker app over Bluetooth only. You press a button on
|
||||
the unit to make it discoverable and it holds no persistent cloud connection.
|
||||
The F2000 / PowerHouse 767 works this way. Those devices publish nothing to
|
||||
Anker's MQTT broker and the broker refuses the subscription with
|
||||
`Unspecified error(128)`. They cannot be automation sources here — local
|
||||
Bluetooth access would need a different project (SolixBLE).
|
||||
|
||||
Discovery skips devices the cloud reports as disconnected before subscribing,
|
||||
so they cost no time. To keep one out permanently:
|
||||
|
||||
python solixauto.py discover-anker --skip <serial>
|
||||
|
||||
For a device that is genuinely cloud connected but reported otherwise, force it
|
||||
with `--include-offline`.
|
||||
|
||||
Otherwise, confirm it shows as online in the Anker app, then rerun for that
|
||||
device alone with `--sn <serial>`.
|
||||
|
||||
Discovery never overwrites a good profile with an empty one, so re-running with
|
||||
a device switched off is harmless.
|
||||
|
||||
**Most likely failure:** an `AttributeError` or `TypeError` from
|
||||
`update_sites`, `get_bind_devices`, `startMqttSession`, or the device factory.
|
||||
I derived those calls from the library's C1000X example rather than running
|
||||
them. If one breaks, send me the traceback and the exact line.
|
||||
|
||||
**Also check:** verify `pv_total` against the Anker app using LIVE data, not the
|
||||
samples in the profile. `fields` shows the snapshot captured at discovery time;
|
||||
use `status` for a live read:
|
||||
|
||||
python solixauto.py status <anker-profile> --fields pv battery --watch
|
||||
|
||||
Open the Anker app side by side and compare the combined solar watts. If they
|
||||
disagree, the field mapping is wrong and every solar rule you write will be
|
||||
wrong with it. This is community-reverse-engineered, not documented by Anker.
|
||||
|
||||
Worth watching for a few minutes across a change in conditions, so you can see
|
||||
`pv_total` track the app rather than matching once by coincidence.
|
||||
|
||||
---
|
||||
|
||||
## Stage 3 - Shelly discovery
|
||||
|
||||
python solixauto.py discover-shelly
|
||||
|
||||
Expect: one profile per Shelly, with the right host and channel count.
|
||||
|
||||
If mDNS finds nothing, fall back:
|
||||
|
||||
python solixauto.py discover-shelly --host 192.168.1.50
|
||||
python solixauto.py discover-shelly --network 192.168.1.0/24
|
||||
|
||||
Give every Shelly a DHCP reservation before going further. If a plug changes IP,
|
||||
automation silently stops working.
|
||||
|
||||
---
|
||||
|
||||
## Stage 4 - manual switch
|
||||
|
||||
This is the first stage that moves a relay. **Plug the Shelly into a lamp, not
|
||||
into anything that matters.**
|
||||
|
||||
python solixauto.py switch <shelly-profile> status
|
||||
python solixauto.py switch <shelly-profile> on
|
||||
python solixauto.py switch <shelly-profile> off
|
||||
|
||||
Expect: state reads back correctly, and the command is confirmed by re-reading
|
||||
the device rather than trusting the HTTP response.
|
||||
|
||||
If this fails, control is broken and no rule will work. Check auth in the
|
||||
profile if the device has a password set.
|
||||
|
||||
---
|
||||
|
||||
## Stage 4b - check for competing automation
|
||||
|
||||
python solixauto.py conflicts <shelly-profile>
|
||||
|
||||
A Shelly can hold schedules, auto-off timers, and webhooks on the device itself.
|
||||
They run independently of this tool and will silently override it. Remove them
|
||||
in the Shelly app before going further, or you will spend a long time debugging
|
||||
rules that were working correctly.
|
||||
|
||||
Shelly Cloud scenes are not visible locally, so also check the app's scenes.
|
||||
|
||||
## Stage 5 - notifications
|
||||
|
||||
python solixauto.py notify-setup
|
||||
# enable one channel, then
|
||||
python solixauto.py notify-test
|
||||
|
||||
Expect: `ok` per channel and a message on your phone.
|
||||
|
||||
ntfy tip: use a long random topic name. Anyone who knows it can read your
|
||||
alerts.
|
||||
|
||||
---
|
||||
|
||||
## Stage 6 - profile validation
|
||||
|
||||
python solixauto.py new-profile solar-failover --template solar
|
||||
python solixauto.py run solar-failover --test --offline
|
||||
|
||||
Expect: syntax OK, no PROBLEM lines, and a rendered notification per rule with
|
||||
your real device names in it.
|
||||
|
||||
---
|
||||
|
||||
## Stage 7 - forced rule firing
|
||||
|
||||
Prove the logic without waiting for the sun to set:
|
||||
|
||||
python solixauto.py run solar-failover --test --simulate battery_soc=12 pv_total=0
|
||||
|
||||
Expect: the low-battery rule reports TRUE, the release rule reports false, and
|
||||
the notification text reads the way you want it to read. Nothing is switched.
|
||||
|
||||
Try a few combinations. This is where you catch a rule that is inverted or a
|
||||
threshold that is off by a decimal place.
|
||||
|
||||
---
|
||||
|
||||
## Stage 8 - live dry run
|
||||
|
||||
python solixauto.py run solar-failover --test
|
||||
|
||||
Now it connects to both devices with real telemetry. Expect: Shelly reachable,
|
||||
real values, dwell countdowns.
|
||||
|
||||
Then run the real loop with switching still disabled:
|
||||
|
||||
python solixauto.py run solar-failover --dry-run
|
||||
|
||||
Leave it for an hour. Expect log lines saying what it *would* do. Confirm the
|
||||
decisions match what you would have made by hand.
|
||||
|
||||
---
|
||||
|
||||
## Stage 9 - live, on a lamp
|
||||
|
||||
python solixauto.py run solar-failover
|
||||
|
||||
Still on the lamp. Watch for a full day, ideally one with variable cloud, which
|
||||
is what exposes missing deadbands.
|
||||
|
||||
Check `logs/automation.log` for:
|
||||
|
||||
- action counts that look sane, not dozens per hour
|
||||
- no `suppressed` lines hitting the hourly cap, which means a rule is
|
||||
oscillating
|
||||
- notifications arriving when you expect and not otherwise
|
||||
|
||||
---
|
||||
|
||||
## Stage 10 - real load
|
||||
|
||||
Only after stage 9 has been clean for a full day. Move the Shelly to the real
|
||||
circuit and keep watching the log for another day.
|
||||
|
||||
---
|
||||
|
||||
## Stage 11 - run it unattended
|
||||
|
||||
python solixauto.py service <profile>
|
||||
python solixauto.py service <profile> --status
|
||||
|
||||
Installs a LaunchAgent on macOS or a systemd user unit on Linux, starting at
|
||||
login and restarting on crash. Check the log after an hour, and again after a
|
||||
reboot, before trusting it.
|
||||
|
||||
Removing the service leaves the Shelly in whatever state it was last set to.
|
||||
Check the plug before you walk away.
|
||||
|
||||
## Before publishing
|
||||
|
||||
- [ ] `device-profiles/` and `state/` are gitignored (the generated `.gitignore`
|
||||
covers this, but verify — profiles contain your serial numbers)
|
||||
- [ ] `notifications.yaml` is not committed
|
||||
- [ ] no `.env` in the repo
|
||||
- [ ] `git log -p | grep -i -E "password|token|@gmail|user_key"` comes back empty
|
||||
- [ ] README states the project is unofficial and unaffiliated with Anker or
|
||||
Allterco
|
||||
- [ ] no Anker or Shelly logos in the repo
|
||||
- [ ] the example profiles reference placeholder serials, not yours
|
||||
Reference in new issue
Block a user