├── sample.json ├── README.md ├── dbus-values.py ├── .gitignore └── dbus-service.py /sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "/Yield/System": 457.82, 3 | "/Load/I": 0.0, 4 | "/Pv/I": 0.0, 5 | "/Load/State": 0, 6 | "/Yield/Power": 0.0, 7 | "/Connected": 1, 8 | "/Yield/User": 457.82, 9 | "/Pv/V": 9.08, 10 | "/Dc/0/Current": 0.0, 11 | "/Mode": 1, 12 | "/State": 0, 13 | "/ErrorCode": 0, 14 | "/Dc/0/Voltage": 12.32 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # victron-json-mppt 2 | Python DBUS json mppt solar charger scripts for VenusOS. 3 | 4 | ### Tested on RPi 3 CCGX 5 | ### Tested on RPi 4 CCGX With python 3 and Venus_OS 3.10 6 | 7 | 8 | Publishes an MPPT solarcharger on the DBUS based on the dbus definitions here: https://github.com/victronenergy/venus/wiki/dbus#solar-chargers 9 | 10 | Both scripts run on startup on the CCGX https://www.victronenergy.com/live/ccgx:root_access#hooks_to_install_run_own_code_at_boot. 11 | 12 | example rc.local: 13 | ```su root -c "screen -dm -S epever-dbus ~/dbus-service.py"``` 14 | ```su root -c "screen -dm -S epever-dbus ~/dbus-values.py"``` 15 | 16 | 17 | ## Why? 18 | I have an Epever Tracer MPPT which I needed to integrate with my victron multiplus & CCGX. I didnt want to spend lots of money on a new mppt when my current one works fine (it just doesn't communicate with the victron software) 19 | 20 | Initially I tried getting the Tracer MPPT readings directly on the CCGX but turns out modbus would crash a lot and the Epever MPPT wasn't happy with the high rate of readings required to run the service well on DBUS. 21 | 22 | ## Known issues: 23 | This script 100% relies on keeping the remote json file up to date. I run a separate script every 60 seconds to grab the required modbus values from my Tracer MPPT and publish them to a json file. On smaller arrays, solar isn't likely to fluctuate massively within 60 seconds so an average is fine. This process is stable and that's what counts. 24 | 25 | ## Special thanks: 26 | I started with this script as a base for how to get it working with CCGX: https://github.com/CarlosBornay/venus-bornaywind 27 | -------------------------------------------------------------------------------- /dbus-values.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -u 2 | 3 | __author__ = "Michael Gane" 4 | __version__ = "1.5.8" 5 | __maintainer__ = __author__ 6 | __email__ = "victron@ganey.co.uk" 7 | 8 | import time # Library to use delays 9 | import os # Library to detect import libraries 10 | import sys # system command library 11 | import urllib.request as ur 12 | import json 13 | 14 | 15 | # importing dbus complements 16 | sys.path.insert(1, os.path.join(os.path.dirname(__file__), './ext/velib_python')) 17 | 18 | # default start values 19 | values = { 20 | '/State': '2', # fault 21 | '/Connected': 0, 22 | '/Pv/V': 0, 23 | '/Pv/I': 0, 24 | '/Dc/0/Voltage': 0, 25 | '/Dc/0/Current': 0, 26 | '/Yield/Power': 0, 27 | '/Yield/User': 129, 28 | '/Yield/System': 129, 29 | } 30 | 31 | 32 | def update_dbus(items): 33 | for key in items: 34 | print ("Setting: ", key, items[key]) 35 | os.system('dbus --system com.victronenergy.solarcharger.epever_ttyUSB3 ' + key + ' SetValue %' + str(items[key])) 36 | 37 | 38 | def merge_two_dicts(x, y): 39 | z = x.copy() # start with x's keys and values 40 | for key, value in x.items(): 41 | print("key is: ", key + "Value is : ", value) 42 | if key in x and key in y: 43 | z[key] = y[key] 44 | return z 45 | 46 | 47 | while 1: 48 | #print(sys.version) 49 | 50 | # set the remote address of json file here: 51 | url = "http://raspberrypi.local/solar.json" 52 | print ("Reading from remote :", url) 53 | 54 | #import urllib.request as ur 55 | #s = ur.urlopen("http://www.google.com") 56 | #sl = s.read() 57 | #print(sl) 58 | 59 | try: 60 | response = ur.urlopen(url) 61 | print("response is : ", response) 62 | data = json.loads(response.read()) 63 | print("json data is : ", data) 64 | 65 | if data: 66 | print ("Got data: " % data) 67 | time.sleep(1) 68 | update_dbus(merge_two_dicts(values, data)) 69 | else: 70 | print ("Failed, using defaults: " % values) 71 | update_dbus(values) 72 | time.sleep(2) 73 | except Exception as error: 74 | # handle the exception 75 | print("An exception occurred:", error) # An exception occurred: division by zero 76 | update_dbus(values) 77 | time.sleep(2) 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /dbus-service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from pymodbus.exceptions import ModbusIOException 4 | 5 | __author__ = "Michael Gane" 6 | __version__ = "1.5.8" 7 | __maintainer__ = __author__ 8 | __email__ = "victron@ganey.co.uk" 9 | 10 | import time # Library to use delays 11 | from argparse import ArgumentParser 12 | import os # Library to detect import libraries 13 | import sys # system command library 14 | from dbus.mainloop.glib import DBusGMainLoop 15 | 16 | # importing dbus complements 17 | # Victron packages 18 | sys.path.insert( 19 | 1, 20 | os.path.join( 21 | os.path.dirname(__file__), 22 | "/opt/victronenergy/dbus-systemcalc-py/ext/velib_python", 23 | ), 24 | ) 25 | from vedbus import VeDbusService # VeDbusItemImportObject paths that are mandatory for services representing products 26 | 27 | from gi.repository import GObject as gobject 28 | #import gobject 29 | #from gobject import idle_add 30 | 31 | DBusGMainLoop(set_as_default=True) 32 | dbusservice = VeDbusService('com.victronenergy.solarcharger.epever_ttyUSB3') 33 | # 34 | # mainloop = gobject.MainLoop() 35 | # mainloop.run() 36 | 37 | try: 38 | print ("trying something") 39 | dbusservice.add_path('/Management/ProcessName', __file__) 40 | dbusservice.add_path('/Management/ProcessVersion', 41 | 'Version {} running on Python {}'.format(1, sys.version)) 42 | dbusservice.add_path('/Management/Connection', 'ModBus RTU') 43 | 44 | # Create the mandatory objects 45 | dbusservice.add_path('/DeviceInstance', 291) 46 | dbusservice.add_path('/ProductId', 0) 47 | dbusservice.add_path('/ProductName', 'Epever Tracer 3210N XDS') 48 | dbusservice.add_path('/FirmwareVersion', 1) 49 | dbusservice.add_path('/HardwareVersion', 1) 50 | dbusservice.add_path('/Connected', 1) 51 | dbusservice.add_path('/Serial', '01234abcde') 52 | 53 | except: 54 | print ("Json mppt schema has been created before!") 55 | 56 | try: 57 | dbusservice.add_path('/State', 0, writeable=True) 58 | dbusservice.add_path('/Pv/V', 0, writeable=True) 59 | dbusservice.add_path('/Pv/I', 0, writeable=True) 60 | dbusservice.add_path('/Dc/0/Voltage', 0, writeable=True) 61 | dbusservice.add_path('/Dc/0/Current', 0, writeable=True) 62 | # dbusservice.add_path('/DC/0/Power', 0, writeable=True) 63 | dbusservice.add_path('/Load/State', 0, writeable=True) 64 | dbusservice.add_path('/Load/I', 0, writeable=True) 65 | dbusservice.add_path('/ErrorCode', 0, writeable=True) 66 | dbusservice.add_path('/Yield/Power', 0, writeable=True) # Actual input power (Watts) 67 | dbusservice.add_path('/Yield/User', 0, writeable=True) # Total kWh produced (user resettable) 68 | dbusservice.add_path('/Yield/System', 0, writeable=True) # Total kWh produced (not resettable) 69 | dbusservice.add_path('/Mode', 0, writeable=True) 70 | dbusservice.add_path('/MppOperationMode', 0, writeable=True) 71 | except: 72 | print ("Json mppt values created before!2") 73 | 74 | 75 | print ("Starting main loop...") 76 | mainloop = gobject.MainLoop() 77 | mainloop.run() 78 | 79 | exit(0) 80 | --------------------------------------------------------------------------------