├── version ├── docs ├── EVSE.pdf ├── OpenEVSE_schema.pdf ├── Level 1 and Level 2 Electric Vehicle Service Equipment(EVSE) Reference Design.pdf └── OpenEVSE-status-sample.json ├── img ├── 2-EVSE.png ├── 3-Device.png ├── 1-DeviceList.png ├── 4-VRM_Portal.png ├── 6-VRM_Graph.png └── 5-VRM_Devices.png ├── config.ini ├── restart.sh ├── service └── run ├── uninstall.sh ├── install.sh ├── README.md └── dbus-evsecharger.py /version: -------------------------------------------------------------------------------- 1 | v1.0 2 | -------------------------------------------------------------------------------- /docs/EVSE.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuWorkshop/dbus-evsecharger/HEAD/docs/EVSE.pdf -------------------------------------------------------------------------------- /img/2-EVSE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuWorkshop/dbus-evsecharger/HEAD/img/2-EVSE.png -------------------------------------------------------------------------------- /img/3-Device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuWorkshop/dbus-evsecharger/HEAD/img/3-Device.png -------------------------------------------------------------------------------- /img/1-DeviceList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuWorkshop/dbus-evsecharger/HEAD/img/1-DeviceList.png -------------------------------------------------------------------------------- /img/4-VRM_Portal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuWorkshop/dbus-evsecharger/HEAD/img/4-VRM_Portal.png -------------------------------------------------------------------------------- /img/6-VRM_Graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuWorkshop/dbus-evsecharger/HEAD/img/6-VRM_Graph.png -------------------------------------------------------------------------------- /img/5-VRM_Devices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuWorkshop/dbus-evsecharger/HEAD/img/5-VRM_Devices.png -------------------------------------------------------------------------------- /docs/OpenEVSE_schema.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuWorkshop/dbus-evsecharger/HEAD/docs/OpenEVSE_schema.pdf -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | AccessType = OnPremise 3 | SignOfLifeLog = 1 4 | Deviceinstance = 43 5 | 6 | [ONPREMISE] 7 | Host=192.168.178.97:3000 8 | -------------------------------------------------------------------------------- /restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 3 | 4 | kill $(pgrep -f "python $SCRIPT_DIR/dbus-evsecharger.py") 5 | -------------------------------------------------------------------------------- /service/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 3 | 4 | exec 2>&1 5 | python $(realpath $SCRIPT_DIR/../dbus-evsecharger.py) 6 | -------------------------------------------------------------------------------- /docs/Level 1 and Level 2 Electric Vehicle Service Equipment(EVSE) Reference Design.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuWorkshop/dbus-evsecharger/HEAD/docs/Level 1 and Level 2 Electric Vehicle Service Equipment(EVSE) Reference Design.pdf -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 3 | SERVICE_NAME=$(basename $SCRIPT_DIR) 4 | 5 | rm /service/$SERVICE_NAME 6 | kill $(pgrep -f 'supervise dbus-evsecharger') 7 | chmod a-x $SCRIPT_DIR/service/run 8 | ./restart.sh 9 | -------------------------------------------------------------------------------- /docs/OpenEVSE-status-sample.json: -------------------------------------------------------------------------------- 1 | {"mode":"Wired","wifi_client_connected":0,"srssi":0,"ipaddress":"XXX.XXX.XXX.XXX","network_manager":"external","emoncms_connected":0,"mqtt_connected":1,"ohm_hour":"NotConnected","ohm_started_charge":false,"free_heap":7559100,"comm_sent":84296,"comm_success":67935,"amp":0,"pilot":8,"temp1":250,"temp2":-2560,"temp3":-2560,"state":1,"elapsed":5896,"wattsec":10698532,"watthour":1079531,"gfcicount":0,"nogndcount":0,"stuckcount":0,"divertmode":1,"solar":0,"grid_ie":0,"charge_rate":0,"divert_update":241384} 2 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 3 | SERVICE_NAME=$(basename $SCRIPT_DIR) 4 | 5 | # set permissions for script files 6 | chmod a+x $SCRIPT_DIR/restart.sh 7 | chmod 744 $SCRIPT_DIR/restart.sh 8 | 9 | chmod a+x $SCRIPT_DIR/uninstall.sh 10 | chmod 744 $SCRIPT_DIR/uninstall.sh 11 | 12 | chmod a+x $SCRIPT_DIR/service/run 13 | chmod 755 $SCRIPT_DIR/service/run 14 | 15 | # create sym-link to run script in deamon 16 | ln -s $SCRIPT_DIR/service /service/$SERVICE_NAME 17 | 18 | # add install-script to rc.local to be ready for firmware update 19 | filename=/data/rc.local 20 | if [ ! -f $filename ] 21 | then 22 | touch $filename 23 | chmod 755 $filename 24 | echo "#!/bin/bash" >> $filename 25 | echo >> $filename 26 | fi 27 | 28 | grep -qxF "$SCRIPT_DIR/install.sh" $filename || echo "$SCRIPT_DIR/install.sh" >> $filename 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dbus-open-evse 2 | Integrate Open_EVSE charger into Victron Energies Venus OS 3 | 4 | ## Purpose 5 | This script supports reading EV charger values from openEVSE base charger. Writing values is supported for "Enable charging"and "Charging current" 6 | 7 | ### Pictures 8 | ![Remote Console - Overview](img/1-DeviceList.png) 9 | ![](img/2-EVSE.png) 10 | ![](img/3-Device.png) 11 | ![](img/4-VRM_Portal.png) 12 | ![](img/5-VRM_Devices.png) 13 | ![](img/6-VRM_Graph.png) 14 | 15 | ## Install & Configuration 16 | ### Get the code 17 | Just grap a copy of the main branche and copy them to a folder under `/data/` e.g. `/data/dbus-evsecharger`. 18 | After that call the install.sh script. 19 | 20 | The following script should do everything for you: 21 | ``` 22 | wget https://github.com/JuWorkshop/dbus-evsecharger/archive/refs/heads/main.zip 23 | unzip main.zip "dbus-evsecharger-main/*" -d /data 24 | mv /data/dbus-evsecharger-main /data/dbus-evsecharger 25 | chmod a+x /data/dbus-evsecharger/install.sh 26 | /data/dbus-evsecharger/install.sh 27 | rm main.zip 28 | ``` 29 | ⚠️ Check configuration after that - because service is already installed an running and with wrong connection data (host) you will spam the log-file 30 | 31 | ### Change config.ini 32 | Within the project there is a file `/data/dbus-evsecharger/config.ini` - just change the values - most important is the deviceinstance under "DEFAULT" and host in section "ONPREMISE". More details below: 33 | 34 | | Section | Config vlaue | Explanation | 35 | | ------------- | ------------- | ------------- | 36 | | DEFAULT | AccessType | Fixed value 'OnPremise' | 37 | | DEFAULT | SignOfLifeLog | Time in minutes how often a status is added to the log-file `current.log` with log-level INFO | 38 | | DEFAULT | Deviceinstance | Unique ID identifying the shelly 1pm in Venus OS | 39 | | ONPREMISE | Host | IP or hostname of on-premise Shelly 3EM web-interface | 40 | 41 | 42 | ## Usefull links 43 | Many thanks. @vikt0rm, @fabian-lauer and @trixing project: 44 | - https://github.com/trixing/venus.dbus-twc3 45 | - https://github.com/fabian-lauer/dbus-shelly-3em-smartmeter 46 | - https://github.com/vikt0rm/dbus-goecharger 47 | -------------------------------------------------------------------------------- /dbus-evsecharger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # import normal packages 4 | import platform 5 | import logging 6 | import os 7 | import sys 8 | 9 | if sys.version_info.major == 2: 10 | import gobject 11 | else: 12 | from gi.repository import GLib as gobject 13 | import sys 14 | import time 15 | import requests # for http GET 16 | import configparser # for config/ini file 17 | 18 | # our own packages from victron 19 | sys.path.insert(1, os.path.join(os.path.dirname(__file__), '/opt/victronenergy/dbus-systemcalc-py/ext/velib_python')) 20 | from vedbus import VeDbusService 21 | 22 | 23 | class DbusEvseChargerService: 24 | def __init__(self, servicename, paths, productname='EVSE-Charger', connection='OpenEVSE JSON RAPI'): 25 | config = self._getConfig() 26 | deviceinstance = int(config['DEFAULT']['Deviceinstance']) 27 | 28 | self._dbusservice = VeDbusService("{}.http_{:02d}".format(servicename, deviceinstance)) 29 | self._paths = paths 30 | 31 | logging.debug("%s /DeviceInstance = %d" % (servicename, deviceinstance)) 32 | 33 | paths_wo_unit = [ 34 | '/Status', 35 | # value 'state' EVSE State - 1 Not Connected - 2 Connected - 3 Charging - 4 Error, 254 - sleep, 255 - disabled 36 | # old_goecharger 1: charging station ready, no vehicle 2: vehicle loads 3: Waiting for vehicle 4: Charge finished, vehicle still connected 37 | '/Mode' 38 | ] 39 | 40 | # get data from go-eCharger 41 | data = self._getEvseChargerData() 42 | 43 | # Create the management objects, as specified in the ccgx dbus-api document 44 | self._dbusservice.add_path('/Mgmt/ProcessName', __file__) 45 | self._dbusservice.add_path('/Mgmt/ProcessVersion', 46 | 'Unkown version, and running on Python ' + platform.python_version()) 47 | self._dbusservice.add_path('/Mgmt/Connection', connection) 48 | 49 | # Create the mandatory objects 50 | self._dbusservice.add_path('/DeviceInstance', deviceinstance) 51 | self._dbusservice.add_path('/ProductId', 0xFFFF) # 52 | self._dbusservice.add_path('/ProductName', productname) 53 | self._dbusservice.add_path('/CustomName', productname) 54 | self._dbusservice.add_path('/FirmwareVersion', int(data['divert_update'])) 55 | self._dbusservice.add_path('/HardwareVersion', 2) 56 | self._dbusservice.add_path('/Serial', data['comm_success']) 57 | self._dbusservice.add_path('/Connected', 1) 58 | self._dbusservice.add_path('/UpdateIndex', 0) 59 | 60 | # add paths without units 61 | for path in paths_wo_unit: 62 | self._dbusservice.add_path(path, None) 63 | 64 | # add path values to dbus 65 | for path, settings in self._paths.items(): 66 | self._dbusservice.add_path( 67 | path, settings['initial'], gettextcallback=settings['textformat'], writeable=True, 68 | onchangecallback=self._handlechangedvalue) 69 | 70 | # last update 71 | self._lastUpdate = 0 72 | 73 | # charging time in float 74 | self._chargingTime = 0.0 75 | 76 | # add _update function 'timer' 77 | gobject.timeout_add(2000, self._update) # pause 2sec before the next request 78 | 79 | # add _signOfLife 'timer' to get feedback in log every 5minutes 80 | gobject.timeout_add(self._getSignOfLifeInterval() * 60 * 1000, self._signOfLife) 81 | 82 | def _getConfig(self): 83 | config = configparser.ConfigParser() 84 | config.read("%s/config.ini" % (os.path.dirname(os.path.realpath(__file__)))) 85 | return config 86 | 87 | def _getSignOfLifeInterval(self): 88 | config = self._getConfig() 89 | value = config['DEFAULT']['SignOfLifeLog'] 90 | 91 | if not value: 92 | value = 0 93 | 94 | return int(value) 95 | 96 | def _getEvseChargerStatusUrl(self): 97 | config = self._getConfig() 98 | accessType = config['DEFAULT']['AccessType'] 99 | 100 | if accessType == 'OnPremise': 101 | URL = "http://%s/status" % (config['ONPREMISE']['Host']) 102 | else: 103 | raise ValueError("AccessType %s is not supported" % (config['DEFAULT']['AccessType'])) 104 | 105 | return URL 106 | 107 | def _getEvseChargerMqttPayloadUrl(self, parameter, value): 108 | config = self._getConfig() 109 | accessType = config['DEFAULT']['AccessType'] 110 | 111 | if accessType == 'OnPremise': 112 | URL = "http://%s/r?json=1&rapi=$%s%s" % (config['ONPREMISE']['Host'], parameter, value) 113 | else: 114 | raise ValueError("AccessType %s is not supported" % (config['DEFAULT']['AccessType'])) 115 | 116 | return URL 117 | 118 | def _setEvseChargerValue(self, parameter, value): 119 | URL = self._getEvseChargerMqttPayloadUrl(parameter, str(value)) 120 | request_data = requests.get(url=URL) 121 | 122 | # check for response 123 | if not request_data: 124 | raise ConnectionError("No response from Evse-Charger - %s" % (URL)) 125 | 126 | json_data = request_data.json() 127 | 128 | # check for Json 129 | if not json_data: 130 | raise ValueError("Converting response to JSON failed") 131 | 132 | if json_data[parameter] == str(value): 133 | return True 134 | else: 135 | logging.warning("Evse-Charger parameter %s not set to %s" % (parameter, str(value))) 136 | return False 137 | 138 | def _getEvseChargerData(self): 139 | URL = self._getEvseChargerStatusUrl() 140 | request_data = requests.get(url=URL) 141 | 142 | # check for response 143 | if not request_data: 144 | raise ConnectionError("No response from Evse-Charger - %s" % (URL)) 145 | 146 | json_data = request_data.json() 147 | 148 | # check for Json 149 | if not json_data: 150 | raise ValueError("Converting response to JSON failed") 151 | 152 | return json_data 153 | 154 | def _signOfLife(self): 155 | logging.info("--- Start: sign of life ---") 156 | logging.info("Last _update() call: %s" % (self._lastUpdate)) 157 | logging.info("Last '/Ac/Power': %s" % (self._dbusservice['/Ac/Power'])) 158 | logging.info("--- End: sign of life ---") 159 | return True 160 | 161 | def _update(self): 162 | try: 163 | # get data from go-eCharger 164 | data = self._getEvseChargerData() 165 | 166 | # send data to DBus 167 | voltage = int(data['voltage']) 168 | self._dbusservice['/Ac/L1/Power'] = int(data['amp'] * voltage / 1000) 169 | self._dbusservice['/Ac/L2/Power'] = 0 170 | self._dbusservice['/Ac/L3/Power'] = 0 171 | self._dbusservice['/Ac/Power'] = int(data['amp'] * voltage / 1000) 172 | self._dbusservice['/Ac/Voltage'] = voltage 173 | self._dbusservice['/Current'] = float(data['amp'] / 1000) 174 | self._dbusservice['/Ac/Energy/Forward'] = float(data['wattsec'] / 3600000) # int(float(data['eto']) / 10.0) 175 | if int(data['state']) == 1 or int(data['state']) == 3: 176 | self._dbusservice['/StartStop'] = 1 177 | else: 178 | self._dbusservice['/StartStop'] = 0 179 | 180 | # self._dbusservice['/StartStop'] = int(data['divertmode']) 181 | self._dbusservice['/SetCurrent'] = int(data['pilot']) 182 | self._dbusservice['/MaxCurrent'] = 32 # int(data['ama']) 183 | 184 | # update chargingTime, increment charge time only on active charging (2), reset when no car connected (1) 185 | timeDelta = time.time() - self._lastUpdate 186 | if int(data['state']) == 3 and self._lastUpdate > 0: # vehicle loads 187 | self._chargingTime += timeDelta 188 | elif int(data['state']) == 1: # charging station ready, no vehicle 189 | self._chargingTime = 0 190 | self._dbusservice['/ChargingTime'] = int(self._chargingTime) 191 | 192 | self._dbusservice['/Mode'] = 0 # Manual, no control 193 | self._dbusservice['/MCU/Temperature'] = int(data['temp1']) 194 | 195 | # 'state' EVSE State - 1 Not Connected - 2 Connected - 3 Charging - 4 Error, 254 - sleep, 255 - disabled 196 | # value 'car' 1: charging station ready, no vehicle 2: vehicle loads 3: Waiting for vehicle 4: Charge finished, vehicle still connected 197 | # 0:EVdisconnected; 1:Connected; 2:Charging; 3:Charged; 4:Wait sun; 5:Wait RFID; 6:Wait enable; 7:Low SOC; 8:Ground error; 9:Welded contacts error; defaut:Unknown; 198 | status = 0 199 | if int(data['state']) == 1: 200 | status = 0 201 | elif int(data['state']) == 2: 202 | status = 1 203 | elif int(data['state']) == 3: 204 | status = 2 205 | elif int(data['state']) ==4: 206 | status = 8 207 | elif int(data['state']) ==255: 208 | status = 6 209 | elif int(data['state']) ==254: 210 | status = 4 211 | self._dbusservice['/Status'] = status 212 | 213 | # logging 214 | logging.debug("Wallbox Consumption (/Ac/Power): %s" % (self._dbusservice['/Ac/Power'])) 215 | logging.debug("Wallbox Forward (/Ac/Energy/Forward): %s" % (self._dbusservice['/Ac/Energy/Forward'])) 216 | logging.debug("---") 217 | 218 | # increment UpdateIndex - to show that new data is available 219 | index = self._dbusservice['/UpdateIndex'] + 1 # increment index 220 | if index > 255: # maximum value of the index 221 | index = 0 # overflow from 255 to 0 222 | self._dbusservice['/UpdateIndex'] = index 223 | 224 | # update lastupdate vars 225 | self._lastUpdate = time.time() 226 | except Exception as e: 227 | logging.critical('Error at %s', '_update', exc_info=e) 228 | 229 | # return true, otherwise add_timeout will be removed from GObject - see docs http://library.isr.ist.utl.pt/docs/pygtk2reference/gobject-functions.html#function-gobject--timeout-add 230 | return True 231 | 232 | def _handlechangedvalue(self, path, value): 233 | logging.info("someone else updated %s to %s" % (path, value)) 234 | 235 | if path == '/SetCurrent': 236 | return self._setEvseChargerValue('SC+', value) 237 | elif path == '/StartStop': 238 | return self._setEvseChargerValue('F', '1') #F1 239 | elif path == '/MaxCurrent': 240 | return self._setEvseChargerValue('ama', value) 241 | else: 242 | logging.info("mapping for evcharger path %s does not exist" % (path)) 243 | return False 244 | 245 | 246 | def main(): 247 | # configure logging 248 | logging.basicConfig(format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s', 249 | datefmt='%Y-%m-%d %H:%M:%S', 250 | level=logging.INFO, 251 | handlers=[ 252 | logging.FileHandler("%s/current.log" % (os.path.dirname(os.path.realpath(__file__)))), 253 | logging.StreamHandler() 254 | ]) 255 | 256 | try: 257 | logging.info("Start") 258 | 259 | from dbus.mainloop.glib import DBusGMainLoop 260 | # Have a mainloop, so we can send/receive asynchronous calls to and from dbus 261 | DBusGMainLoop(set_as_default=True) 262 | 263 | # formatting 264 | _kwh = lambda p, v: (str(round(v, 2)) + 'kWh') 265 | _a = lambda p, v: (str(round(v, 1)) + 'A') 266 | _w = lambda p, v: (str(round(v, 1)) + 'W') 267 | _v = lambda p, v: (str(round(v, 1)) + 'V') 268 | _degC = lambda p, v: (str(v) + '°C') 269 | _s = lambda p, v: (str(v) + 's') 270 | 271 | # start our main-service 272 | pvac_output = DbusEvseChargerService( 273 | servicename='com.victronenergy.evcharger', 274 | paths={ 275 | '/Ac/Power': {'initial': 0, 'textformat': _w}, 276 | '/Ac/L1/Power': {'initial': 0, 'textformat': _w}, 277 | '/Ac/L2/Power': {'initial': 0, 'textformat': _w}, 278 | '/Ac/L3/Power': {'initial': 0, 'textformat': _w}, 279 | '/Ac/Energy/Forward': {'initial': 0, 'textformat': _kwh}, 280 | '/ChargingTime': {'initial': 0, 'textformat': _s}, 281 | 282 | '/Ac/Voltage': {'initial': 0, 'textformat': _v}, 283 | '/Current': {'initial': 0, 'textformat': _a}, 284 | '/SetCurrent': {'initial': 0, 'textformat': _a}, 285 | '/MaxCurrent': {'initial': 0, 'textformat': _a}, 286 | '/MCU/Temperature': {'initial': 0, 'textformat': _degC}, 287 | '/StartStop': {'initial': 0, 'textformat': lambda p, v: (str(v))} 288 | } 289 | ) 290 | 291 | logging.info('Connected to dbus, and switching over to gobject.MainLoop() (= event based)') 292 | mainloop = gobject.MainLoop() 293 | mainloop.run() 294 | except Exception as e: 295 | logging.critical('Error at %s', 'main', exc_info=e) 296 | 297 | 298 | if __name__ == "__main__": 299 | main() 300 | --------------------------------------------------------------------------------