an invention by tserong
Project Description
At my home we have solar PV hooked up to Victron inverter/chargers and a Redflow ZCell battery for energy storage. The setup is described in detail on my blog. By their nature, ZCell flow batteries needs to undergo a maintenance cycle at least every three days, where they are discharged completely for a few hours. Having only one battery, this means we can't use the "minimum state of charge" feature of the Victron kit to always keep some charge in the battery in case of outages, because doing so conflicts with the ZCell maintenance cycles. This isn't a problem if you have more than one ZCell, because the maintenance cycles interleave in that case, but so far we only have one of these things. If I want to keep charge in the battery for emergency purposes on non-maintenance days, I can do that by configuring scheduled charge settings manually on the Victron Cerbo GX console, but then I have to remember to turn those things back off (or otherwise adjust the settings) for the next maintenance day. For this hack week, I'm going to see if I can automate that piece somehow.
Goal for this Hackweek
Figure out how to interrogate the ZCell Battery Management System's REST API to find out when the battery will be doing maintenance, and then figure out how to automatically update the scheduled charge settings on the Victron Cerbo GX to keep the battery reasonably full on non-maintenance days, and to let it drain again on maintenance days. Also, the battery still needs to be allowed to discharge during peak (i.e. more expensive) electricity times, which are 07:00-10:00 and 16:00-21:00 weekdays, regardless of whether it's a maintenance cycle day.
Bonus points for implementing this in Go and/or Rust, to force me to learn those languages.
Resources
- Go With The Flow (my blog post describing our current setup)
- Victron Energy Open Source
- Venus OS: Root Access (to get root on the Cerbo in case I need that)
- The ZCell BMS REST API is documented in its online help, i.e. in my case this is available only on my internal home network (apologies to everyone reading this who doesn't have access to my house)
Looking for hackers with the skills:
Nothing? Add some keywords!
This project is part of:
Hack Week 21
Comments
-
over 2 years ago by tserong | Reply
Progress! The following script actually seems to do the trick, and is at least a reasonable first cut, with some caveats:
- Almost no error handling
- Assumes maintenance is due based on maintenance time limit. This will usually be true, unless someone (i.e.: me) has messed with the discharge cycle allowed days settings on the BMS (if all days are allowed, you're good - if some days are not allowed you may get maintenance before the time limit).
- The charge schedules are probably sane for winter (which is now, for me) but likely won't make sense for summer when there's a lot more sun. That's OK, I can tweak this later (or just disable it altogether in the summer months).
The advantage of using python is I can run this directly on the GX itself. If I want to do it in rust or go, I'd have to figure out how to build for an armv7l system, which might mean going down a cross-compilation rat hole, and I'm not sure I want to do that.
Another possibility is to enable MQTT on the GX, and potentially poke at the schedule settings remotely that way. Or automate ssh'ing in and running
dbus -y -- com.victronenergy.settings /Settings/CGwacs/BatteryLife/Schedule/Charge/$INDEX/$PARAM SetValue $WHATEVER
a few times.```
!/usr/bin/env python3
Set scheduled charges on a single ZCell, to generally keep the
battery full except for peak electricity usage times, and maintenance
cycle days.
This is designed to be run on the GX from cron at 07:00 each day
(just as peak electricity starts), because that makes the scheduled
charges as simple as I could imagine.
Run on the GX with:
TZ=$(dbus -y com.victronenergy.settings /Settings/System/TimeZone GetValue | tr -d "'") sched.py
to make sure datetime.now().weekday() is working off the local timezone,
which is what the scheduler uses, vs. UTC, which is what the GX itself
runs in.
import argparse import dbus import requests from datetime import datetime
HOURS = 60 * 60 DAYS = 60 * 60 * 24
CGX days are:
0 Sunday
1 Monday
2 Tuesday
3 Wednesday
4 Thursday
5 Friday
6 Saturday
7 Every Day
8 Weekdays
9 Weekends
python's datetime.weekday() is Monday 0 - Sunday 6
So to get from python to CGX, we add 1 modulo 7
def cgx_day(day): return (day + 1) % 7
Yeah, I know this is the same code as the above function, but the
semantics are different ;-)
def next_day(day): return (day + 1) % 7
def ismaintenanceday(): # If it's less than 24 hours until maintenance is due when # this script is run, we know today is a maintenance day status = requests.get(f'http://{args.bmsip}:3000/rest/1.0/status').json() strippumpruntarget = status['list'][0]['strippumpruntarget'] strippumpruntimer = status['list'][0]['strippumpruntimer'] nextstrip = strippumpruntarget - strippumpruntimer return next_strip / DAYS < 1
def setchargeschedule(index, day, start, duration, soc): bus = dbus.SystemBus() # SetValue returns dbus.Int32(0) on success or dbus.Int32(-1) on failure o = bus.getobject('com.victronenergy.settings', f'/Settings/CGwacs/BatteryLife/Schedule/Charge/{index}/Day') o.SetValue(day) o = bus.getobject('com.victronenergy.settings', f'/Settings/CGwacs/BatteryLife/Schedule/Charge/{index}/Start') o.SetValue(start) o = bus.getobject('com.victronenergy.settings', f'/Settings/CGwacs/BatteryLife/Schedule/Charge/{index}/Duration') o.SetValue(duration) o = bus.getobject('com.victronenergy.settings', f'/Settings/CGwacs/BatteryLife/Schedule/Charge/{index}/Soc') o.SetValue(soc)
def setscheduledcharges(): today = cgxday(datetime.now().weekday()) tomorrow = nextday(today) try: if ismaintenanceday(): # On maintenance days, set a scheduled charge from 13:00 for 3 # hours with a 50% SoC limit... setchargeschedule(3, today, 13 * HOURS, 3 * HOURS, 50) # ...and set an overnight charge starting at 03:00 for 4 hours # the next day (maintenance should be complete by then) setchargeschedule(4, tomorrow, 3 * HOURS, 4 * HOURS, 100) else: # On non-maintenance days, set scheduled charges from 10:00 for # 6 hours and 21:00 for 10 hours, so we're basically keeping the # battery charged all the time, except for peak electricity hours # (07:00-10:00 and 16:00-21:00). Note that for simplicity this # assumes that all days have peak times. That's not actually # true on Weekends, but it's just simpler to pretend it's true # rather than trying to set up schedules that account for the # difference between weekdays and weekends. setchargeschedule(3, today, 10 * HOURS, 6 * HOURS, 100) setchargeschedule(4, today, 21 * HOURS, 10 * HOURS, 100) except Exception as e: print(e) # If something breaks (e.g. can't talk to the BMS) fall back to # disabling our scheduled charges as the least worst course of # action setchargeschedule(3, -today, 10 * HOURS, 6 * HOURS, 100) setchargeschedule(4, -today, 21 * HOURS, 10 * HOURS, 100)
if name == "main": parser = argparse.ArgumentParser("Set scheduled charges based on ZCell maintenance cycle") parser.addargument("bmsip", help="ZCell BMS IP address") args = parser.parseargs() setscheduled_charges() ```
-
over 2 years ago by tserong | Reply
The Cerbo GX doesn't preserve crontabs over firmware updates (see https://www.victronenergy.com/live/ccgx:rootaccess#partitionsread-onlyrootfsandavailabledisk_space). We can work around this with the following
/data/rc.local
script:```
!/bin/sh
if ! grep -q sched-v1.py /etc/crontab; then mount | grep -q 'on / .*ro' isro=$? if [ $isro -eq 0 ]; then mount -o remount,rw / fi cat >> /etc/crontab <<EOF
Set scheduled charges based on zcell maintenance cycle
0 21 * * * root TZ=\$(dbus -y com.victronenergy.settings /Settings/System/TimeZone GetValue | tr -d "'") /home/root/sched-v1.py
EOF if [ $is_ro -eq 0 ]; then mount -o remount,ro / fi fi ```The script we've added to crontab (/home/root/sched-v1.py) is actually on the data partition because /home/root is a symlink to /data/home/root. It's just the crontab itself that needs setting up after a firmware update.
-
over 2 years ago by tserong | Reply
Alright, I think I'm done :-) Here's a blog post with details: https://ourobengr.com/2022/07/keeping-the-battery-full/
I didn't get the bonus points for implementing in go or rust (see earlier comment on cross compiling). I would still like to experiment with external control/access via MQTT, so maybe that's what I should try in those languages...
Similar Projects
This project is one of its kind!