├── evcc ├── version ├── service │ ├── log │ │ └── run │ └── run ├── up ├── down ├── dbus-evcc-check ├── setup └── evcc-autoconfig.py ├── evcc.dist.yaml ├── rc └── post-hook.sh ├── .gitignore ├── .gitmodules └── README.md /evcc/version: -------------------------------------------------------------------------------- 1 | 0.124.4 -------------------------------------------------------------------------------- /evcc.dist.yaml: -------------------------------------------------------------------------------- 1 | sponsortoken: '' 2 | -------------------------------------------------------------------------------- /evcc/service/log/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec multilog t s25000 n4 /var/log/evcc -------------------------------------------------------------------------------- /rc/post-hook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Setup evcc 4 | exec /data/evcc/setup -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | evcc/evcc 3 | venus-data.tar.gz 4 | evcc.yaml 5 | evcc/**/evcc.dist.yaml 6 | evcc/evcc.yaml 7 | evcc/pyyaml* 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "evcc/ext/velib_python"] 2 | path = evcc/ext/velib_python 3 | url = https://github.com/victronenergy/velib_python.git 4 | -------------------------------------------------------------------------------- /evcc/up: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sh /data/evcc/dbus-evcc-check 4 | if [[ $? -eq 0 ]]; then 5 | 6 | dbus-send --system --print-reply --dest=com.victronenergy.settings /Settings/Services/Evcc com.victronenergy.BusItem.SetValue variant:int32:1 > /dev/null 7 | 8 | else 9 | 10 | rm /data/.evcc_disabled 11 | svc -u /service/evcc* 12 | 13 | fi -------------------------------------------------------------------------------- /evcc/down: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sh /data/evcc/dbus-evcc-check 4 | if [[ $? -eq 0 ]]; then 5 | 6 | dbus-send --system --print-reply --dest=com.victronenergy.settings /Settings/Services/Evcc com.victronenergy.BusItem.SetValue variant:int32:0 > /dev/null 7 | 8 | else 9 | 10 | touch /data/.evcc_disabled 11 | svc -d /service/evcc* 12 | 13 | fi -------------------------------------------------------------------------------- /evcc/service/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ -f '/data/.evcc_disabled' ]] ; then 4 | echo "*** evcc is disabled, not starting ***" 5 | svc -d /service/evcc* 6 | exit 0 7 | fi 8 | 9 | echo "*** starting evcc ***" 10 | 11 | until netstat -an | grep 0.0.0.0:1883.*LISTEN > /dev/null; do 12 | echo "waiting for MQTT broker to start ..." 13 | sleep 1 14 | done 15 | 16 | # create config file 17 | /data/evcc/evcc-autoconfig.py 18 | 19 | exec 2>&1 20 | exec /data/evcc/evcc -c /data/evcc/evcc.ve.yaml -------------------------------------------------------------------------------- /evcc/dbus-evcc-check: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | VERSION="$(head -n 1 /opt/victronenergy/version | tr -d 'v')" 4 | 5 | MAJOR=`echo $VERSION | cut -d. -f1` 6 | MINOR=`echo $VERSION | cut -d. -f2 | cut -d~ -f1` 7 | 8 | # controlling evcc service via dbus available from v3.30~5 9 | if [[ $MAJOR -lt 3 ]]; then 10 | exit 1 11 | elif [[ $MAJOR -gt 3 ]]; then 12 | exit 0 13 | else 14 | if [[ $MINOR -lt 30 ]]; then 15 | exit 1 16 | elif [[ $MINOR -gt 30 ]]; then 17 | exit 0 18 | elif [[ $VERSION =~ "~" ]]; then 19 | BUILD=`echo $VERSION | cut -d. -f2 | cut -d~ -f2` 20 | if [[ $BUILD -lt 5 ]]; then 21 | exit 1 22 | else 23 | exit 0 24 | fi 25 | else 26 | exit 0 27 | fi 28 | fi -------------------------------------------------------------------------------- /evcc/setup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | FILE='/data/rc.local' 4 | DESCRIPTION='Auto-reinstall evcc' 5 | COMMAND='ln -sf /data/evcc/service/ /service/evcc' 6 | 7 | # STEP 1: Install pyyaml if missing 8 | 9 | export PYTHONPATH="${PYTHONPATH}:/data/evcc/ext" 10 | python3 -c "import yaml" 2> /dev/null 11 | if [ $? -ne 0 ]; then 12 | PWD=$(pwd) 13 | cd /data/evcc/pyyaml* 14 | python3 setup.py --without-libyaml install --install-lib /data/evcc/ext 15 | cd $PWD 16 | fi 17 | 18 | # STEP 2: Copy evcc service to /service/evcc to run on startup 19 | 20 | rm -rf /service/evcc 21 | $COMMAND 22 | 23 | # STEP 3: Install evcc service controls 24 | 25 | # Create file if not exists 26 | [ -f $FILE ] || { 27 | echo "#!/bin/sh" > $FILE 28 | chmod +x $FILE 29 | } 30 | 31 | # Check if controlling via evcc is possible 32 | sh /data/evcc/dbus-evcc-check 33 | if [[ $? -eq 0 ]]; then 34 | 35 | # Remove previously set disable file 36 | rm -f /data/.evcc_disabled 37 | 38 | # Remove evcc auto-reinstall, if present 39 | sed -i "/# $DESCRIPTION/,/${COMMAND//\//\\/}/d" $FILE 40 | 41 | else 42 | 43 | # Use evcc auto-reinstall, if not already present 44 | grep -qF -- "$COMMAND" "$FILE" || { 45 | echo "# ${DESCRIPTION}" >> $FILE 46 | echo "${COMMAND}" >> $FILE 47 | } 48 | 49 | fi 50 | 51 | sync -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EVCC VenusOS Installer 2 | 3 | **DISCLAIMER: This is a proof of concept and bound to change** 4 | 5 | This project creates a `venus-data.tar.gz` archive for easy installation of the [evcc](https://github.com/evcc-io/evcc) project on a Victron Energy GX device running Venus OS. Within the installation process, it automatically detects and configures all Victron Energy EV Charging Stations connected to the GX device for use with evcc. 6 | 7 | **Please note:** Currently, when evcc is used together with a Victron EV Charging Station, it will overrule any manual input from the GX device, VRM portal or even from the web interface and the display of the EV Charging Station itself. We are working on making it possible to enable and disable evcc from the GX device in order to restore manual control. 8 | 9 | ## Get the repository 10 | 11 | Clone the repository and initialize the git submodule: 12 | ```sh 13 | git clone https://github.com/victronenergy/evcc-venusos-installer.git 14 | cd evcc-venusos-installer 15 | git submodule update --init --recursive 16 | ``` 17 | 18 | ## Add your configuration 19 | 20 | If you have a Victron system together with an Victron Energy EV Charging Station, you can skip this step, as your EVCS is automatically detected when evcc starts. 21 | 22 | Otherwise, copy and rename the file `evcc.dist.yaml` to `evcc.yaml`. Then, add your custom configurations there, like sponsor token, EV charger configurations, etc. 23 | 24 | Please note the following: 25 | * An error in the configuration will prevent evcc from starting – In this case, the configuration must be corrected and the archive built and installed again (see steps below) 26 | * On evcc startup, the entries for `database` and `mqtt` will always be replaced with the correct parameters for the GX device 27 | * The entries for `interval`, `network`, `meters`, `site`, `chargers` and `loadpoints` will be added automatically on evcc startup, as long as they are not already present in your `evcc.yaml` 28 | * If you add `loadpoints` and/or `chargers` to `evcc.yaml`, Victron Energy EV Charging Stations are no longer auto-detected and will also have to be configured manually 29 | 30 | For general instructions on how to configure `evcc.yaml`, please refer to the evcc [documentation](https://docs.evcc.io/en/docs/Home/) and [community](https://github.com/evcc-io/evcc/discussions/). 31 | 32 | ## Install evcc using a USB stick or SD card 33 | 34 | Now, run the following command to load the evcc binary and pack a `venus-data.tar.gz` archive: 35 | 36 | ```sh 37 | sh build.sh 38 | ``` 39 | 40 | Put the `venus-data.tar.gz` archive on a USB stick or SD card, connect it to the GX device and reboot the device. 41 | 42 | After some seconds, evcc will get available at port `7070` of your GX device, e.g. [http://venus.local:7070/](http://venus.local:7070/). 43 | 44 | Please don't forget to remove the USB stick after the installation process. 45 | 46 | ## Frequently Asked Questions (FAQs) 47 | 48 | ### How to disable the evcc service? 49 | 50 | We are working on controls via the user interface of the GX device. Until then, this must be done via SSH by executing `/data/evcc/down` to disable and `/data/evcc/up` for re-enabling it. -------------------------------------------------------------------------------- /evcc/evcc-autoconfig.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 -u 2 | 3 | import os 4 | import sys 5 | import dbus 6 | import logging 7 | 8 | 9 | # configure logging 10 | logging.basicConfig(stream=sys.stdout, level=logging.INFO) 11 | logger = logging.getLogger('evcc-autoconfig.py') 12 | 13 | 14 | app_dir = os.path.dirname(os.path.realpath(__file__)) 15 | 16 | # Locally installed packages, namingly pyyaml 17 | sys.path.insert(1, os.path.join(app_dir, 'ext')) 18 | EGG_DIR = os.path.join(app_dir, 'ext') 19 | for filename in os.listdir(EGG_DIR): 20 | if filename.endswith(".egg"): 21 | sys.path.insert(2, os.path.join(EGG_DIR, filename)) 22 | 23 | # Victron packages 24 | sys.path.insert(3, os.path.join(app_dir, 'ext', 'velib_python')) 25 | 26 | import yaml 27 | from ve_utils import get_vrm_portal_id, wrap_dbus_value 28 | 29 | software_version = '0.1' 30 | 31 | 32 | class EvccDbusConfig: 33 | 34 | def __init__(self): 35 | self._bus = dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus() 36 | 37 | self.evcc_hostname = 'venus.local' 38 | self.evcc_port = 7070 39 | 40 | self.mqtt_hostname = '127.0.0.1:1883' 41 | self.mqtt_topic = f"N/{ get_vrm_portal_id() }/evcc" 42 | 43 | self.gx_modbus_hostname = '127.0.0.1' 44 | self.gx_modbus_port = 502 45 | 46 | self.interval = 30 # in seconds 47 | self.system_name = self._get_dbus_value('com.victronenergy.settings', '/Settings/SystemSetup/SystemName') 48 | 49 | self.meter_refs = { 50 | # usage: name 51 | 'grid': 've-grid', 52 | 'pv': 've-pv', 53 | 'battery': 've-battery' 54 | } 55 | 56 | def _get_dbus_value(self, service, path, type=str, fallback=None): 57 | try: 58 | val = self._bus.call_blocking(service, path, None, 'GetValue', '', []) 59 | except dbus.exceptions.DBusException: 60 | val = None 61 | if isinstance(val, dbus.Array) and len(val) == 0: 62 | val = None 63 | return type(val) if val is not None else fallback 64 | 65 | def _set_dbus_value(self, service, path, value): 66 | value = wrap_dbus_value(value) 67 | return self._bus.call_blocking(service, path, None, 'SetValue', 'v', [value]) 68 | 69 | def _enable_gx_modbus_server(self): 70 | service = 'com.victronenergy.settings' 71 | try: 72 | self._set_dbus_value(service, '/Settings/Services/Modbus', 1) 73 | logger.info(f"enabled GX Modbus server") 74 | except: 75 | logger.exception('enabling GX Modbus server failed') 76 | 77 | def _switch_ev_charger_to_manual(self, unique_name): 78 | service = 'com.victronenergy.evcharger.' + unique_name 79 | try: 80 | self._set_dbus_value(service, '/Mode', 0) 81 | logger.info(f"switched evcharger ({ unique_name }) to manual mode") 82 | except: 83 | logger.exception('setting evcharger to manual mode failed') 84 | 85 | def _find_evchargers(self): 86 | services = self._bus.list_names() 87 | chargers = [] 88 | loadpoints = [] 89 | 90 | for service in services: 91 | if service.startswith('com.victronenergy.evcharger.'): 92 | 93 | if not self._get_dbus_value(service, '/Connected', bool): 94 | continue 95 | 96 | unique_name = service.replace('com.victronenergy.evcharger.', '') 97 | device_instance = self._get_dbus_value(service, '/DeviceInstance', int) 98 | name = self._get_dbus_value(service, '/CustomName') \ 99 | or self._get_dbus_value(service, '/ProductName') 100 | 101 | if isinstance(name, dbus.Array): 102 | name = name.pop() \ 103 | or self._get_dbus_value(service, '/ProductName') 104 | 105 | logger.info(f"found evcharger '{ name }' ({ unique_name })") 106 | 107 | max_current = self._get_dbus_value(service, '/MaxCurrent', int) 108 | min_current = self._get_dbus_value(service, '/MinCurrent', int, fallback=6) 109 | num_phases = self._get_dbus_value(service, '/NrOfPhases', int, fallback=3) 110 | 111 | chargers.append({ 112 | 'type': 'template', 113 | 'template': 'victron', 114 | 'host': self.gx_modbus_hostname, 115 | 'port': self.gx_modbus_port, 116 | 'id': device_instance, 117 | 'modbus': 'tcpip', 118 | 'name': unique_name 119 | }) 120 | 121 | loadpoints.append({ 122 | 'title': name, 123 | 'charger': unique_name, 124 | 'mode': 'pv', 125 | 'phases': num_phases, 126 | 'mincurrent': min_current, 127 | 'maxcurrent': max_current 128 | }) 129 | 130 | return chargers, loadpoints 131 | 132 | def get_network(self): 133 | return { 134 | 'schema': 'http', 135 | 'host': self.evcc_hostname, 136 | 'port': self.evcc_port 137 | } 138 | 139 | def get_meters(self): 140 | meters = [] 141 | for usage, name in self.meter_refs.items(): 142 | meters.append({ 143 | 'type': 'template', 144 | 'template': 'victron-energy', 145 | 'usage': usage, 146 | 'host': self.gx_modbus_hostname, 147 | 'port': self.gx_modbus_port, 148 | 'name': name 149 | }) 150 | return meters 151 | 152 | def get_site(self): 153 | return { 154 | 'title': self.system_name or "Victron Energy", 155 | 'meters': self.meter_refs, 156 | 'residualPower': 100 157 | } 158 | 159 | def get_mqtt(self): 160 | return { 161 | 'broker': self.mqtt_hostname, 162 | 'topic': self.mqtt_topic 163 | } 164 | 165 | def get_interval(self): 166 | return f"{ self.interval }s" 167 | 168 | def get_config(self, config:dict={}): 169 | 170 | # add or override, if user-defined 171 | config.update({ 172 | 'database': { 173 | 'type': 'sqlite', 174 | 'dsn': '/data/evcc/evcc.db' 175 | }, 176 | 'mqtt': self.get_mqtt(), 177 | }) 178 | 179 | # add, if not user-defined 180 | config['interval'] = config.get('interval', self.get_interval()) 181 | config['network'] = config.get('network', self.get_network()) 182 | config['meters'] = config.get('meters', self.get_meters()) 183 | config['site'] = config.get('site', self.get_site()) 184 | 185 | # add auto-detected chargers or loadpoints, if not user-defined 186 | if config.get('chargers', []) or config.get('loadpoints', []): 187 | config.update({ 188 | 'chargers': config['chargers'], 189 | 'loadpoints': config['loadpoints'] 190 | }) 191 | else: 192 | chargers, loadpoints = self._find_evchargers() 193 | config.update({ 194 | 'chargers': chargers, 195 | 'loadpoints': loadpoints 196 | }) 197 | 198 | for charger in chargers: 199 | self._switch_ev_charger_to_manual(charger['name']) 200 | 201 | self._enable_gx_modbus_server() 202 | 203 | return config 204 | 205 | @staticmethod 206 | def get(custom_config={}): 207 | return EvccDbusConfig().get_config(custom_config) 208 | 209 | if __name__ == "__main__": 210 | 211 | logger.info("creating evcc.yaml ...") 212 | 213 | CUSTOM_CONFIG_PATH = os.path.join(app_dir, 'evcc.yaml') 214 | CONFIG_PATH = os.path.join(app_dir, 'evcc.ve.yaml') 215 | 216 | add_cfg = {} 217 | if os.path.isfile(CUSTOM_CONFIG_PATH): 218 | with open(CUSTOM_CONFIG_PATH, 'r') as f: 219 | add_cfg = yaml.safe_load(f) 220 | 221 | data = EvccDbusConfig.get(add_cfg) 222 | 223 | with open(CONFIG_PATH, 'w') as f: 224 | yaml.dump(data, f) 225 | 226 | logger.info(f"written config to { CONFIG_PATH }") 227 | --------------------------------------------------------------------------------