├── service ├── down ├── log │ ├── down │ └── run └── run ├── image.png ├── .gitmodules ├── start-dbus-mppsolar.sh ├── test.py ├── LICENSE ├── README.md ├── serial-starter.sh └── dbus-mppsolar.py /service/down: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service/log/down: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkZeros/dbus-mppsolar/HEAD/image.png -------------------------------------------------------------------------------- /service/log/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec 2>&1 3 | exec multilog t s25000 n4 /var/log/dbus-mppsolar.TTY 4 | -------------------------------------------------------------------------------- /service/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "*** starting dbus-mppsolar ***" 3 | exec 2>&1 4 | exec /data/etc/dbus-mppsolar/start-dbus-mppsolar.sh TTY 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "velib_python"] 2 | path = velib_python 3 | url = https://github.com/victronenergy/velib_python 4 | [submodule "mpp-solar"] 5 | path = mpp-solar 6 | url = https://github.com/DarkZeros/mpp-solar 7 | -------------------------------------------------------------------------------- /start-dbus-mppsolar.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Start script for gps_dbus 4 | # First parameter: tty device to use 5 | # 6 | # Keep this script running with daemon tools. If it exits because the 7 | # connection crashes, or whatever, daemon tools will start a new one. 8 | # 9 | 10 | . /opt/victronenergy/serial-starter/run-service.sh 11 | 12 | app=/data/etc/dbus-mppsolar/dbus-mppsolar.py 13 | 14 | # Baudrates to use 15 | start -b 2400 -s /dev/$tty 16 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import datetime 4 | import serial 5 | 6 | def send_and_receive(): 7 | response_line = None 8 | try: 9 | with serial.Serial('/dev/ttyUSB2', 2400, timeout=1, write_timeout=1) as s: 10 | print(datetime.datetime.now().time(), "Executing command via serialio...") 11 | s.flushInput() 12 | s.flushOutput() 13 | s.write(b'QVFWb\x99\r') 14 | response_line = s.read_until(b"\r") 15 | s.read_all() 16 | print(datetime.datetime.now().time(),"serial response was: %s", response_line) 17 | #s.close() 18 | print(datetime.datetime.now().time(), "closed") 19 | return response_line 20 | except Exception as e: 21 | print(f"Serial read error: {e}") 22 | 23 | for i in range(10): 24 | print(datetime.datetime.now().time(), '--------------------') 25 | send_and_receive() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 DarkZeros 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dbus-mppsolar 2 | 3 | DBus VenusOS driver for MPPSolar inverter or any compatible one. 4 | 5 | ## What does it do? 6 | 7 | After installing this to VenusOS. Venus will be able to auto-connect 8 | to inverters that follow MPPSolar protocols though a USB-Serial interface. 9 | 10 | It will automatically spawn the inverter in a very similar way to Victron 11 | Multi inverters, and will support controlling the inverter trough VRM. 12 | 13 | ![Example](image.png) 14 | 15 | # Install Instructions 16 | 17 | ## Prepare Venus OS (optional): 18 | 19 | You may need to install GIT and python by doing: 20 | 21 | ``` 22 | /opt/victronenergy/swupdate-scripts/set-feed.sh release 23 | opkg update 24 | opkg install python3-pip git 25 | pip3 instal mpp-solar # Not needed because this repo uses it as a submodule 26 | ``` 27 | 28 | ## Clone GIT & submodules on Venus OS: 29 | 30 | ``` 31 | git clone --recurse-submodules https://github.com/DarkZeros/dbus-mppsolar /data/etc/dbus-mppsolar 32 | ``` 33 | 34 | Now install the service to the VenusOS to allow starting it when needed: 35 | ``` 36 | cp -R /data/etc/dbus-mppsolar/service /opt/victronenergy/service-templates/dbus-mppsolar 37 | ``` 38 | 39 | Add the service to the starter rules, this will autostart the service on new USB-Serial connections with any compatible inverter. Modify `/etc/venus/serial-starter.conf` add a new service in the list of services, and add it to the default list of service handlers at the start (`mppsolar:gps:...:vedirect`). 40 | 41 | Here how it should look like: 42 | ``` 43 | ... 44 | service mppsolar dbus-mppsolar 45 | ... 46 | alias rs485 mppsolar:cgwacs:fzsonick:imt:modbus 47 | alias default mppsolar:gps:vedirect 48 | ... 49 | ``` 50 | 51 | # What does this repo depend on 52 | 53 | * Need velib_python for execution of the service 54 | * Need mpp-solar to communicate with Inverter 55 | 56 | 57 | # What inverters are supported? 58 | 59 | * Inverters that follow the PI30 protocol, or PI30MAX 60 | * The rest, are not yet parsed, but feel free to implement the update()/change() functions for them :) 61 | -------------------------------------------------------------------------------- /serial-starter.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | exec 2>&1 3 | 4 | . $(dirname $0)/functions.sh 5 | 6 | BASE_DIR='/opt/victronenergy' 7 | CACHE_DIR='/data/var/lib/serial-starter' 8 | SERVICE_DIR='/var/volatile/services' 9 | SS_CONFIG='/etc/venus/serial-starter.conf' 10 | 11 | # Remove stale service symlinks 12 | find -L /service -maxdepth 1 -type l -delete 13 | 14 | mkdir -p "$CACHE_DIR" 15 | mkdir -p "$SERVICE_DIR" 16 | 17 | get_property() { 18 | udevadm info --query=property --name="$1" | sed -n "s/^$2=//p" 19 | } 20 | 21 | get_product() { 22 | tty=$1 23 | dev=/sys/class/tty/$tty/device 24 | 25 | if [ ! -L $dev ]; then 26 | # no device, probably a virtual terminal 27 | echo ignore 28 | return 29 | fi 30 | 31 | ve_product=$(get_property $tty VE_PRODUCT) 32 | 33 | if [ -n "$ve_product" ]; then 34 | echo $ve_product 35 | return 36 | fi 37 | 38 | subsys=$(basename $(readlink $dev/subsystem)) 39 | 40 | case $subsys in 41 | usb*) 42 | get_property $tty ID_MODEL 43 | ;; 44 | *) 45 | echo ignore 46 | ;; 47 | esac 48 | } 49 | 50 | get_program() { 51 | tty=$1 52 | product=$2 53 | 54 | ve_service=$(get_property $tty VE_SERVICE) 55 | 56 | if [ -n "$ve_service" ]; then 57 | # If a seperate debug console is lacking a ve-direct port can be used instead.. 58 | if [ "$ve_service" = "vedirect-or-vegetty" ]; then 59 | if [ -e /service/vegetty ]; then 60 | echo ignore 61 | else 62 | echo vedirect 63 | fi 64 | return 65 | fi 66 | 67 | echo $ve_service 68 | return 69 | fi 70 | 71 | case $product in 72 | builtin-mkx) 73 | echo mkx 74 | ;; 75 | builtin-vedirect) 76 | echo vedirect 77 | ;; 78 | ignore) 79 | echo ignore 80 | ;; 81 | *) 82 | echo default 83 | ;; 84 | esac 85 | } 86 | 87 | create_service() { 88 | service=$1 89 | tty=$2 90 | 91 | # check if service already exists 92 | test -d "/service/$SERVICE" && return 0 93 | 94 | tmpl=$BASE_DIR/service-templates/$service 95 | 96 | # check existence of service template 97 | if [ ! -d "$tmpl" ]; then 98 | echo "ERROR: no service template for $service" 99 | return 1 100 | fi 101 | 102 | echo "INFO: Create daemontools service $SERVICE" 103 | 104 | # copy service 105 | cp -a "$tmpl" "$SERVICE_DIR/$SERVICE" 106 | 107 | # Patch run files for tty device 108 | sed -i "s:TTY:$TTY:" "$SERVICE_DIR/$SERVICE/run" 109 | sed -i "s:TTY:$TTY:" "$SERVICE_DIR/$SERVICE/log/run" 110 | 111 | # Create symlink to /service 112 | ln -sf "$SERVICE_DIR/$SERVICE" "/service/$SERVICE" 113 | 114 | # wait for svscan to find service 115 | sleep 6 116 | } 117 | 118 | start_service() { 119 | eval service="\$svc_$1" 120 | tty=$2 121 | 122 | if [ -z "$service" ]; then 123 | echo "ERROR: unknown service $1" 124 | return 1 125 | fi 126 | 127 | SERVICE="$service.$tty" 128 | 129 | if ! create_service $service $tty; then 130 | unlock_tty $tty 131 | return 1 132 | fi 133 | 134 | # update product string 135 | sed -i "s:PRODUCT\(=[^ ]*\)*:PRODUCT=$PRODUCT:" "$SERVICE_DIR/$SERVICE/run" 136 | 137 | svc -u "/service/$SERVICE/log" 138 | 139 | if [ $AUTOPROG = n ]; then 140 | echo "INFO: Start service $SERVICE" 141 | svc -u "/service/$SERVICE" 142 | else 143 | echo "INFO: Start service $SERVICE once" 144 | svc -o "/service/$SERVICE" 145 | fi 146 | } 147 | 148 | # recursively expand aliases, removing duplicates 149 | expand_alias() { 150 | set -- $(echo $1 | tr : ' ') 151 | 152 | for v; do 153 | eval x="\$exp_$v" 154 | test -n "$x" && continue 155 | 156 | eval e="\$alias_$v" 157 | eval "exp_$v=1" 158 | 159 | if [ -n "$e" ]; then 160 | expand_alias "$e" 161 | else 162 | echo $v 163 | fi 164 | done 165 | } 166 | 167 | # expand aliases and return colon separated list 168 | get_alias() ( 169 | set -- $(expand_alias $1) 170 | IFS=: 171 | echo "$*" 172 | ) 173 | 174 | check_val() { 175 | if echo "$1" | grep -Eqv "^$2+\$"; then 176 | echo "ERROR: $3 ${1:+'$1'}" >&2 177 | fi 178 | } 179 | 180 | load_config() { 181 | cfg=$1 182 | 183 | test -r "$cfg" || return 184 | 185 | echo "INFO: loading config file $cfg" >&2 186 | 187 | sed 's/#.*//' "$cfg" | while read keyword name value; do 188 | # ignore blank lines 189 | test -z "$keyword" && continue 190 | 191 | case $keyword in 192 | service) 193 | check_val "$name" '[[:alnum:]_]' 'invalid service name' 194 | check_val "$value" '[[:alnum:]_:.-]' 'invalid service value' 195 | echo "svc_$name=$value" 196 | ;; 197 | alias) 198 | check_val "$name" '[[:alnum:]_]' 'invalid alias name' 199 | check_val "$value" '[[:alnum:]_:-]' 'invalid alias value' 200 | echo "alias_$name=$value" 201 | ;; 202 | include) 203 | check_val "$name" . 'include: name required' 204 | if [ -d "$name" ]; then 205 | for file in "$name"/*.conf; do 206 | load_config "$file" 207 | done 208 | else 209 | load_config "$name" 210 | fi 211 | ;; 212 | *) 213 | echo "ERROR: unknown keyword $keyword" >&2 214 | ;; 215 | esac 216 | done 217 | } 218 | 219 | echo "serstart starting" 220 | 221 | eval $(load_config "$SS_CONFIG") 222 | 223 | while true; do 224 | TTYS=$(ls /dev/serial-starter/ 2>/dev/null) 225 | for TTY in $TTYS; do 226 | CACHE_FILE="$CACHE_DIR/$TTY" 227 | PROG_FILE="/tmp/$TTY.prog" 228 | 229 | lock_tty $TTY || continue 230 | 231 | # device may have vanished while running for loop 232 | if ! test -e /dev/serial-starter/$TTY; then 233 | unlock_tty $TTY 234 | continue 235 | fi 236 | 237 | # check for a known device 238 | PRODUCT=$(get_product $TTY) 239 | PROGRAMS=$(get_program $TTY $PRODUCT) 240 | PROGRAMS=$(get_alias $PROGRAMS) 241 | 242 | if [ "$PROGRAMS" = ignore ]; then 243 | rm /dev/serial-starter/$TTY 244 | unlock_tty $TTY 245 | continue 246 | elif [ "${PROGRAMS%%:*}" = "${PROGRAMS}" ]; then 247 | AUTOPROG=n 248 | PROGRAM=$PROGRAMS 249 | rm /dev/serial-starter/$TTY 250 | else 251 | AUTOPROG=y 252 | 253 | if [ -f "$PROG_FILE" ]; then 254 | # next entry in probe cycle 255 | PROGRAM=$(cat $PROG_FILE) 256 | elif [ -f "$CACHE_FILE" ]; then 257 | # last used program 258 | PROGRAM=$(cat $CACHE_FILE) 259 | fi 260 | 261 | if ! echo ":$PROGRAMS:" | grep -q ":$PROGRAM:"; then 262 | # invalid cache, reset 263 | PROGRAM=${PROGRAMS%%:*} 264 | fi 265 | fi 266 | 267 | for n in $(echo $PROGRAMS | tr : ' '); do 268 | mkdir -p /run/serial-starter/$n 269 | ln -sf /dev/$TTY /run/serial-starter/$n/$TTY 270 | done 271 | 272 | echo "$PROGRAM" >"$CACHE_FILE" 273 | 274 | start_service $PROGRAM $TTY & 275 | 276 | if [ $AUTOPROG = y ]; then 277 | NEXT=${PROGRAMS#*${PROGRAM}:} 278 | NEXT=${NEXT%%:*} 279 | echo "$NEXT" >"$PROG_FILE" 280 | fi 281 | 282 | sleep 1 283 | done 284 | 285 | sleep 2 286 | done 287 | -------------------------------------------------------------------------------- /dbus-mppsolar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Handle automatic connection with MPP Solar inverter compatible device (VEVOR) 5 | This will output 2 dbus services, one for Inverter data another one for control 6 | via VRM of the features. 7 | """ 8 | VERSION = 'v0.2' 9 | 10 | from gi.repository import GLib 11 | import platform 12 | import argparse 13 | import logging 14 | import sys 15 | import os 16 | import subprocess as sp 17 | import json 18 | from enum import Enum 19 | import datetime 20 | import dbus 21 | import dbus.service 22 | 23 | logging.basicConfig(level=logging.WARNING) 24 | 25 | # our own packages 26 | sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'velib_python')) 27 | from vedbus import VeDbusService, VeDbusItemExport, VeDbusItemImport 28 | 29 | # Workarounds for some inverter specific problem I saw 30 | INVERTER_OFF_ASSUME_BYPASS = True 31 | GUESS_AC_CHARGING = True 32 | 33 | # Should we import and call manually, to use our version 34 | USE_SYSTEM_MPPSOLAR = False 35 | if USE_SYSTEM_MPPSOLAR: 36 | try: 37 | import mppsolar 38 | except: 39 | USE_SYSTEM_MPPSOLAR = FALSE 40 | if not USE_SYSTEM_MPPSOLAR: 41 | sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'mpp-solar')) 42 | import mppsolar 43 | 44 | # Inverter commands to read from the serial 45 | def runInverterCommands(commands, protocol="PI30"): 46 | global args 47 | global mainloop 48 | if USE_SYSTEM_MPPSOLAR: 49 | output = [sp.getoutput("mpp-solar -b {} -P {} -p {} -o json -c {}".format(args.baudrate, protocol, args.serial, c)).split('\n')[0] for c in commands] 50 | parsed = [json.loads(o) for o in output] 51 | else: 52 | dev = mppsolar.helpers.get_device_class("mppsolar")(port=args.serial, protocol=protocol, baud=args.baudrate) 53 | results = [dev.run_command(command=c) for c in commands] 54 | parsed = [mppsolar.outputs.to_json(r, False, None, None) for r in results] 55 | return parsed 56 | 57 | def setOutputSource(source): 58 | #POP: Setting device output source priority 59 | # NN = 00 for utility first, 01 for solar first, 02 for SBU priority 60 | return runInverterCommands(['POP{:02d}'.format(source)]) 61 | 62 | def setChargerPriority(priority): 63 | #PCP: Setting device charger priority 64 | # For KS: 00 for utility first, 01 for solar first, 02 for solar and utility, 03 for only solar charging 65 | # For MKS: 00 for utility first, 01 for solar first, 03 for only solar charging 66 | return runInverterCommands(['PCP{:02d}'.format(priority)]) 67 | 68 | def setMaxChargingCurrent(current): 69 | #MNCHGC: Setting max charging current (More than 100A) 70 | # Setting value can be gain by QMCHGCR command. 71 | # nnn is max charging current, m is parallel number. 72 | return runInverterCommands(['MNCHGC0{:04d}'.format(current)]) 73 | 74 | def setMaxUtilityChargingCurrent(current): 75 | #MUCHGC: Setting utility max charging current 76 | # Setting value can be gain by QMCHGCR command. 77 | # nnn is max charging current, m is parallel number. 78 | return runInverterCommands(['MUCHGC{:03d}'.format(current)]) 79 | 80 | def isNaN(num): 81 | return num != num 82 | 83 | 84 | # Allow to have multiple DBUS connections 85 | class SystemBus(dbus.bus.BusConnection): 86 | def __new__(cls): 87 | return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) 88 | class SessionBus(dbus.bus.BusConnection): 89 | def __new__(cls): 90 | return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) 91 | def dbusconnection(): 92 | return SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() 93 | 94 | # Our MPP solar service that conencts to 2 dbus services (multi & vebus) 95 | class DbusMppSolarService(object): 96 | def __init__(self, tty, deviceinstance, productname='MPPSolar', connection='MPPSolar interface'): 97 | self._tty = tty 98 | self._queued_updates = [] 99 | 100 | # Try to get the protocol version of the inverter 101 | try: 102 | self._invProtocol = runInverterCommands(['QPI'])[0].get('protocol_id', 'PI30') 103 | except: 104 | try: 105 | self._invProtocol = runInverterCommands(['PI'])[0].get('protocol_id', 'PI17') 106 | except: 107 | logging.error("Protocol detection error, will probably fail now in the next steps") 108 | self._invProtocol = "QPI" 109 | 110 | # Refine the protocol received, it may be the inverter is lying 111 | if self._invProtocol == 'PI30': 112 | try: 113 | raw = runInverterCommands(['QPIGS','QMOD','QPIWS']) 114 | except: 115 | logging.warning(f"Protocol PI30 is failing, switching to PI30MAX") 116 | self._invProtocol = 'PI30MAX' 117 | 118 | # Get inverter data based on protocol 119 | if self._invProtocol == 'PI17': 120 | self._invData = runInverterCommands(['ID','VFW'], self._invProtocol) 121 | elif self._invProtocol == 'PI30' or self._invProtocol == 'PI30MAX': 122 | self._invData = runInverterCommands(['QID','QVFW'], self._invProtocol) 123 | else: 124 | logging.error(f"Detected inverter on {tty} ({self._invProtocol}), protocol not supported, using PI30 as fallback") 125 | self._invProtocol = 'PI30' 126 | logging.warning(f"Connected to inverter on {tty} ({self._invProtocol}), setting up dbus with /DeviceInstance = {deviceinstance}") 127 | 128 | # Create a listener to the DC system power, we need it to give some values 129 | self._systemDcPower = None 130 | self._dcLast = 0 131 | self._chargeLast = 0 132 | 133 | # Create the services 134 | self._dbusmulti = VeDbusService(f'com.victronenergy.multi.mppsolar.{tty}', dbusconnection(), register=False) 135 | self._dbusvebus = VeDbusService(f'com.victronenergy.acsystem.mppsolar.{tty}', dbusconnection(), register=False) 136 | 137 | # Set up default paths 138 | self.setupDefaultPaths(self._dbusmulti, connection, deviceinstance, f"Inverter {productname}") 139 | self.setupDefaultPaths(self._dbusvebus, connection, deviceinstance, f"Vebus {productname}") 140 | 141 | # Create paths for 'multi' 142 | self._dbusmulti.add_path('/Ac/In/1/L1/V', 0) 143 | self._dbusmulti.add_path('/Ac/In/1/L1/I', 0) 144 | self._dbusmulti.add_path('/Ac/In/1/L1/P', 0) 145 | self._dbusmulti.add_path('/Ac/In/1/L1/F', 0) 146 | #self._dbusmulti.add_path('/Ac/In/2/L1/V', 0) 147 | #self._dbusmulti.add_path('/Ac/In/2/L1/I', 0) 148 | #self._dbusmulti.add_path('/Ac/In/2/L1/P', 0) 149 | #self._dbusmulti.add_path('/Ac/In/2/L1/F', 0) 150 | self._dbusmulti.add_path('/Ac/Out/L1/V', 0) 151 | self._dbusmulti.add_path('/Ac/Out/L1/I', 0) 152 | self._dbusmulti.add_path('/Ac/Out/L1/P', 0) 153 | self._dbusmulti.add_path('/Ac/Out/L1/S', 0) 154 | self._dbusmulti.add_path('/Ac/Out/L1/F', 0) 155 | self._dbusmulti.add_path('/Ac/In/1/Type', 1) #0=Unused;1=Grid;2=Genset;3=Shore 156 | #self._dbusmulti.add_path('/Ac/In/2/Type', 1) #0=Unused;1=Grid;2=Genset;3=Shore 157 | self._dbusmulti.add_path('/Ac/In/1/CurrentLimit', 20) 158 | #self._dbusmulti.add_path('/Ac/In/2/CurrentLimit', 20) 159 | self._dbusmulti.add_path('/Ac/NumberOfPhases', 1) 160 | self._dbusmulti.add_path('/Ac/ActiveIn/ActiveInput', 0) 161 | self._dbusmulti.add_path('/Ac/ActiveIn/Type', 1) 162 | self._dbusmulti.add_path('/Dc/0/Voltage', 0) 163 | self._dbusmulti.add_path('/Dc/0/Current', 0) 164 | #self._dbusmulti.add_path('/Dc/0/Temperature', 10) 165 | self._dbusmulti.add_path('/Soc', None) 166 | self._dbusmulti.add_path('/State', 9) #0=Off;1=Low Power;2=Fault;3=Bulk;4=Absorption;5=Float;6=Storage;7=Equalize;8=Passthru;9=Inverting;10=Power assist;11=Power supply;252=External control 167 | self._dbusmulti.add_path('/Mode', 0, writeable=True, onchangecallback=self._change) #1=Charger Only;2=Inverter Only;3=On;4=Off 168 | self._dbusmulti.add_path('/Alarms/HighTemperature', 0) 169 | self._dbusmulti.add_path('/Alarms/HighVoltage', 0) 170 | self._dbusmulti.add_path('/Alarms/HighVoltageAcOut', 0) 171 | self._dbusmulti.add_path('/Alarms/LowTemperature', 0) 172 | self._dbusmulti.add_path('/Alarms/LowVoltage', 0) 173 | self._dbusmulti.add_path('/Alarms/LowVoltageAcOut', 0) 174 | self._dbusmulti.add_path('/Alarms/Overload', 0) 175 | self._dbusmulti.add_path('/Alarms/Ripple', 0) 176 | self._dbusmulti.add_path('/Yield/Power', 0) 177 | self._dbusmulti.add_path('/Yield/User', 0) 178 | self._dbusmulti.add_path('/Relay/0/State', None) 179 | self._dbusmulti.add_path('/MppOperationMode', 0) #0=Off;1=Voltage/current limited;2=MPPT active;255=Not available 180 | self._dbusmulti.add_path('/Pv/V', 0) 181 | self._dbusmulti.add_path('/ErrorCode', 0) 182 | self._dbusmulti.add_path('/Energy/AcIn1ToAcOut', 0) 183 | self._dbusmulti.add_path('/Energy/AcIn1ToInverter', 0) 184 | #self._dbusmulti.add_path('/Energy/AcIn2ToAcOut', 0) 185 | #self._dbusmulti.add_path('/Energy/AcIn2ToInverter', 0) 186 | self._dbusmulti.add_path('/Energy/AcOutToAcIn1', 0) 187 | #self._dbusmulti.add_path('/Energy/AcOutToAcIn2', 0) 188 | self._dbusmulti.add_path('/Energy/InverterToAcIn1', 0) 189 | #self._dbusmulti.add_path('/Energy/InverterToAcIn2', 0) 190 | self._dbusmulti.add_path('/Energy/InverterToAcOut', 0) 191 | self._dbusmulti.add_path('/Energy/OutToInverter', 0) 192 | self._dbusmulti.add_path('/Energy/SolarToAcIn1', 0) 193 | #self._dbusmulti.add_path('/Energy/SolarToAcIn2', 0) 194 | self._dbusmulti.add_path('/Energy/SolarToAcOut', 0) 195 | self._dbusmulti.add_path('/Energy/SolarToBattery', 0) 196 | self._dbusmulti.add_path('/History/Daily/0/Yield', 0) 197 | self._dbusmulti.add_path('/History/Daily/0/MaxPower', 0) 198 | self._dbusmulti.add_path('/History/Daily/0/Pv/0/Yield', 0) 199 | self._dbusmulti.add_path('/History/Daily/0/Pv/0/MaxPower', 0) 200 | self._dbusmulti.add_path('/Pv/0/V', 0) 201 | self._dbusmulti.add_path('/Pv/0/P', 0) 202 | self._dbusmulti.add_path('/Temperature', 123) 203 | self._dbusmulti.add_path('/Alarms/LowSoc', 0) 204 | self._dbusmulti.add_path('/Alarms/HighDcVoltage', 0) 205 | self._dbusmulti.add_path('/Alarms/LowDcVoltage', 0) 206 | self._dbusmulti.add_path('/Alarms/LineFail', 0) 207 | self._dbusmulti.add_path('/Alarms/GridLost', 0) 208 | self._dbusmulti.add_path('/Alarms/Connection', 0) 209 | 210 | # Create paths for 'vebus' 211 | self._dbusvebus.add_path('/Ac/ActiveIn/L1/F', 0) 212 | self._dbusvebus.add_path('/Ac/ActiveIn/L1/I', 0) 213 | self._dbusvebus.add_path('/Ac/ActiveIn/L1/V', 0) 214 | self._dbusvebus.add_path('/Ac/ActiveIn/L1/P', 0) 215 | self._dbusvebus.add_path('/Ac/ActiveIn/L1/S', 0) 216 | self._dbusvebus.add_path('/Ac/ActiveIn/P', 0) 217 | self._dbusvebus.add_path('/Ac/ActiveIn/S', 0) 218 | self._dbusvebus.add_path('/Ac/ActiveIn/ActiveInput', 0) 219 | self._dbusvebus.add_path('/Ac/Out/L1/V', 0) 220 | self._dbusvebus.add_path('/Ac/Out/L1/I', 0) 221 | self._dbusvebus.add_path('/Ac/Out/L1/P', 0) 222 | self._dbusvebus.add_path('/Ac/Out/L1/S', 0) 223 | self._dbusvebus.add_path('/Ac/Out/L1/F', 0) 224 | self._dbusvebus.add_path('/Ac/NumberOfPhases', 1) 225 | self._dbusvebus.add_path('/Dc/0/Voltage', 0) 226 | self._dbusvebus.add_path('/Dc/0/Current', 0) 227 | self._dbusvebus.add_path('/Ac/In/1/CurrentLimit', 20, writeable=True, onchangecallback=self._change) 228 | self._dbusvebus.add_path('/Ac/In/1/CurrentLimitIsAdjustable', 1) 229 | self._dbusvebus.add_path('/Settings/SystemSetup/AcInput1', 1) 230 | self._dbusvebus.add_path('/Settings/SystemSetup/AcInput2', 0) 231 | self._dbusvebus.add_path('/Ac/In/1/Type', 1) #0=Unused;1=Grid;2=Genset;3=Shore 232 | self._dbusvebus.add_path('/Ac/In/2/Type', 0) #0=Unused;1=Grid;2=Genset;3=Shore 233 | self._dbusvebus.add_path('/Ac/State/IgnoreAcIn1', 0) 234 | self._dbusvebus.add_path('/Ac/State/IgnoreAcIn2', 1) 235 | self._dbusvebus.add_path('/Mode', 0, writeable=True, onchangecallback=self._change) 236 | self._dbusvebus.add_path('/ModeIsAdjustable', 1) 237 | self._dbusvebus.add_path('/State', 0) 238 | self._dbusvebus.add_path('/Ac/In/1/L1/V', 0, writeable=False, onchangecallback=self._change) 239 | 240 | # Register on the bus 241 | self._dbusmulti.register() 242 | self._dbusvebus.register() # Comment to not add it to the path 243 | 244 | GLib.timeout_add(10000 if USE_SYSTEM_MPPSOLAR else 2000, self._update) 245 | 246 | def setupDefaultPaths(self, service, connection, deviceinstance, productname): 247 | # self._dbusmulti.add_mandatory_paths(__file__, 'version f{VERSION}, and running on Python ' + platform.python_version(), connection, 248 | # deviceinstance, self._invData[0].get('serial_number', 0), productname, self._invData[1].get('main_cpu_firmware_version', 0), 0, 1) 249 | 250 | # Create the management objects, as specified in the ccgx dbus-api document 251 | service.add_path('/Mgmt/ProcessName', __file__) 252 | service.add_path('/Mgmt/ProcessVersion', 'version f{VERSION}, and running on Python ' + platform.python_version()) 253 | service.add_path('/Mgmt/Connection', connection) 254 | 255 | # Create the mandatory objects 256 | service.add_path('/DeviceInstance', deviceinstance) 257 | service.add_path('/ProductId', self._invData[0].get('serial_number', 0)) 258 | service.add_path('/ProductName', productname) 259 | service.add_path('/FirmwareVersion', self._invData[1].get('main_cpu_firmware_version', 0)) 260 | service.add_path('/HardwareVersion', 0) 261 | service.add_path('/Connected', 1) 262 | 263 | # Create the paths for modifying the system manually 264 | service.add_path('/Settings/Reset', None, writeable=True, onchangecallback=self._change) 265 | service.add_path('/Settings/Charger', None, writeable=True, onchangecallback=self._change) 266 | service.add_path('/Settings/Output', None, writeable=True, onchangecallback=self._change) 267 | 268 | def _updateInternal(self): 269 | # Store in the paths all values that were updated from _handleChangedValue 270 | with self._dbusmulti as m:# self._dbusvebus as v: 271 | for path, value, in self._queued_updates: 272 | m[path] = value 273 | # v[path] = value 274 | self._queued_updates = [] 275 | 276 | def _connectToDc(self): 277 | if self._systemDcPower is None: 278 | try: 279 | self._systemDcPower = VeDbusItemImport(dbusconnection(), 'com.victronenergy.system', '/Dc/System/Power') 280 | logging.warning("Connected to DC at {}".format(datetime.datetime.now().time())) 281 | except: 282 | pass 283 | 284 | def _update(self): 285 | global mainloop 286 | self._connectToDc() 287 | logging.info("{} updating".format(datetime.datetime.now().time())) 288 | try: 289 | if self._invProtocol == 'PI30' or self._invProtocol == 'PI30MAX': 290 | return self._update_PI30() 291 | elif self._invProtocol == 'PI17': 292 | return self._update_PI17() 293 | else: 294 | return True #self._update_def() 295 | except: 296 | logging.exception('Error in update loop', exc_info=True) 297 | mainloop.quit() 298 | return False 299 | 300 | def _change(self, path, value): 301 | global mainloop 302 | logging.warning("updated %s to %s" % (path, value)) 303 | if path == '/Settings/Reset': 304 | logging.info("Restarting!") 305 | mainloop.quit() 306 | exit 307 | try: 308 | if self._invProtocol == 'PI30' or self._invProtocol == 'PI30MAX': 309 | return self._change_PI30(path, value) 310 | elif self._invProtocol == 'PI17': 311 | return self._change_PI17(path, value) 312 | else: 313 | return True #self._change_def() 314 | except: 315 | logging.exception('Error in change loop', exc_info=True) 316 | mainloop.quit() 317 | return False 318 | 319 | def _update_PI30(self): 320 | raw = runInverterCommands(['QPIGS','QMOD','QPIWS']) 321 | data, mode, warnings = raw 322 | dcSystem = None 323 | if self._systemDcPower != None: 324 | dcSystem = self._systemDcPower.get_value() 325 | logging.debug(dcSystem) 326 | logging.debug(raw) 327 | with self._dbusmulti as m, self._dbusvebus as v: 328 | # 1=Charger Only;2=Inverter Only;3=On;4=Off -> Control from outside 329 | if 'error' in data and 'short' in data['error']: 330 | m['/State'] = 0 331 | m['/Alarms/Connection'] = 2 332 | 333 | # 0=Off;1=Low Power;2=Fault;3=Bulk;4=Absorption;5=Float;6=Storage;7=Equalize;8=Passthru;9=Inverting;10=Power assist;11=Power supply;252=External control 334 | invMode = mode.get('device_mode', None) 335 | if invMode == 'Battery': 336 | m['/State'] = 9 # Inverting 337 | elif invMode == 'Line': 338 | if data.get('is_charging_on', 0) == 1: 339 | m['/State'] = 3 # Passthru + Charging? = Bulk 340 | else: 341 | m['/State'] = 8 # Passthru 342 | elif invMode == 'Standby': 343 | m['/State'] = data.get('is_charging_on', 0) * 6 # Standby = 0 -> OFF, Stanby + Charging = 6 -> "Storage" Storing power 344 | else: 345 | m['/State'] = 0 # OFF 346 | v['/State'] = m['/State'] 347 | 348 | # Normal operation, read data 349 | v['/Dc/0/Voltage'] = m['/Dc/0/Voltage'] = data.get('battery_voltage', None) 350 | m['/Dc/0/Current'] = -data.get('battery_discharge_current', 0) 351 | v['/Dc/0/Current'] = -m['/Dc/0/Current'] 352 | charging_ac_current = data.get('battery_charging_current', 0) 353 | load_on = data.get('is_load_on', 0) 354 | charging_ac = data.get('is_charging_on', 0) 355 | 356 | v['/Ac/Out/L1/V'] = m['/Ac/Out/L1/V'] = data.get('ac_output_voltage', None) 357 | v['/Ac/Out/L1/F'] = m['/Ac/Out/L1/F'] = data.get('ac_output_frequency', None) 358 | v['/Ac/Out/L1/P'] = m['/Ac/Out/L1/P'] = data.get('ac_output_active_power', None) 359 | v['/Ac/Out/L1/S'] = m['/Ac/Out/L1/S'] = data.get('ac_output_aparent_power', None) 360 | 361 | # For some reason, the system does not detect small values 362 | if (m['/Ac/Out/L1/P'] == 0) and load_on == 1 and m['/Dc/0/Current'] != None and m['/Dc/0/Voltage'] != None and dcSystem != None: 363 | dcPower = dcSystem + self._dcLast + 27 364 | power = 27 if dcPower < 27 else dcPower 365 | power = 100 if power > 100 else power 366 | m['/Ac/Out/L1/P'] = power - 27 367 | self._dcLast = m['/Ac/Out/L1/P'] or 0 368 | else: 369 | self._dcLast = 0 370 | 371 | # Also, due to a bug (?), is not possible to get the battery charging current from AC 372 | if GUESS_AC_CHARGING and dcSystem != None and charging_ac == 1: 373 | chargePower = dcSystem + self._chargeLast 374 | self._chargeLast = chargePower - 30 375 | charging_ac_current = -(chargePower - 30) / m['/Dc/0/Voltage'] 376 | else: 377 | self._chargeLast = 0 378 | 379 | # For my installation specific case: 380 | # - When the load is off the output is unkonwn, the AC1/OUT are connected directly, and inverter is bypassed 381 | if INVERTER_OFF_ASSUME_BYPASS and load_on == 0: 382 | m['/Ac/Out/L1/P'] = m['/Ac/Out/L1/S'] = None 383 | 384 | # Charger input, same as AC1 but separate line data 385 | v['/Ac/ActiveIn/L1/V'] = m['/Ac/In/1/L1/V'] = data.get('ac_input_voltage', None) 386 | v['/Ac/ActiveIn/L1/F'] = m['/Ac/In/1/L1/F'] = data.get('ac_input_frequency', None) 387 | 388 | # It does not give us power of AC in, we need to compute it from the current state + Output power + Charging on + Current 389 | if m['/State'] == 0: 390 | m['/Ac/In/1/L1/P'] = None # Unkown if inverter is off 391 | else: 392 | m['/Ac/In/1/L1/P'] = 0 if invMode == 'Battery' else m['/Ac/Out/L1/P'] 393 | m['/Ac/In/1/L1/P'] = (m['/Ac/In/1/L1/P'] or 0) + charging_ac * charging_ac_current * m['/Dc/0/Voltage'] 394 | v['/Ac/ActiveIn/L1/P'] = m['/Ac/In/1/L1/P'] 395 | 396 | # Solar charger 397 | m['/Pv/0/V'] = data.get('pv_input_voltage', None) 398 | m['/Pv/0/P'] = data.get('pv_input_power', None) 399 | m['/MppOperationMode'] = 2 if (m['/Pv/0/P'] != None and m['/Pv/0/P'] > 0) else 0 400 | 401 | m['/Dc/0/Current'] = m['/Dc/0/Current'] + charging_ac * charging_ac_current - self._dcLast / (m['/Dc/0/Voltage'] or 27) 402 | # Compute the currents as well? 403 | # m['/Ac/Out/L1/I'] = m['/Ac/Out/L1/P'] / m['/Ac/Out/L1/V'] 404 | # m['/Ac/In/1/L1/I'] = m['/Ac/In/1/L1/P'] / m['/Ac/In/1/L1/V'] 405 | 406 | # Update some Alarms 407 | def getWarning(string): 408 | val = warnings.get(string, None) 409 | if val is None: 410 | return 1 411 | return int(val) * 2 412 | m['/Alarms/Connection'] = 0 413 | m['/Alarms/HighTemperature'] = getWarning('over_temperature_fault') 414 | m['/Alarms/Overload'] = getWarning('overload_fault') 415 | m['/Alarms/HighVoltage'] = getWarning('bus_over_fault') 416 | m['/Alarms/LowVoltage'] = getWarning('bus_under_fault') 417 | m['/Alarms/HighVoltageAcOut'] = getWarning('inverter_voltage_too_high_fault') 418 | m['/Alarms/LowVoltageAcOut'] = getWarning('inverter_voltage_too_low_fault') 419 | m['/Alarms/HighDcVoltage'] = getWarning('battery_voltage_to_high_fault') 420 | m['/Alarms/LowDcVoltage'] = getWarning('battery_low_alarm_warning') 421 | m['/Alarms/LineFail'] = getWarning('line_fail_warning') 422 | 423 | # Misc 424 | m['/Temperature'] = data.get('inverter_heat_sink_temperature', None) 425 | 426 | # Execute updates of previously updated values 427 | self._updateInternal() 428 | 429 | logging.info("{} done".format(datetime.datetime.now().time())) 430 | return True 431 | 432 | def _change_PI30(self, path, value): 433 | if path == '/Ac/In/1/CurrentLimit' or path == '/Ac/In/2/CurrentLimit': 434 | logging.warning("setting max utility charging current to = {} ({})".format(value, setMaxUtilityChargingCurrent(value))) 435 | self._queued_updates.append((path, value)) 436 | 437 | if path == '/Mode': # 1=Charger Only;2=Inverter Only;3=On;4=Off(?) 438 | if value == 1: 439 | #logging.warning("setting mode to 'Charger Only'(Charger=Util & Output=Util->solar) ({},{})".format(setChargerPriority(0), setOutputSource(0))) 440 | logging.warning("setting mode to 'Charger Only'(Charger=Util) ({})".format(setChargerPriority(0))) 441 | elif value == 2: 442 | logging.warning("setting mode to 'Inverter Only'(Charger=Solar & Output=SBU) ({},{})".format(setChargerPriority(3), setOutputSource(2))) 443 | elif value == 3: 444 | logging.warning("setting mode to 'ON=Charge+Invert'(Charger=Util & Output=SBU) ({},{})".format(setChargerPriority(0), setOutputSource(2))) 445 | elif value == 4: 446 | #logging.warning("setting mode to 'OFF'(Charger=Solar & Output=Util->solar) ({},{})".format(setChargerPriority(3), setOutputSource(0))) 447 | logging.warning("setting mode to 'OFF'(Charger=Solar) ({})".format(setChargerPriority(3))) 448 | else: 449 | logging.warning("setting mode not understood ({})".format(value)) 450 | self._queued_updates.append((path, value)) 451 | # Debug nodes 452 | if path == '/Settings/Charger': 453 | if value == 0: 454 | logging.warning("setting charger priority to utility first ({})".format(setChargerPriority(value))) 455 | elif value == 1: 456 | logging.warning("setting charger priority to solar first ({})".format(setChargerPriority(value))) 457 | elif value == 2: 458 | logging.warning("setting charger priority to solar and utility ({})".format(setChargerPriority(value))) 459 | else: 460 | logging.warning("setting charger priority to only solar ({})".format(setChargerPriority(3))) 461 | self._queued_updates.append((path, value)) 462 | if path == '/Settings/Output': 463 | if value == 0: 464 | logging.warning("setting output Utility->Solar priority ({})".format(setOutputSource(value))) 465 | elif value == 1: 466 | logging.warning("setting output solar->Utility priority ({})".format(setOutputSource(value))) 467 | else: 468 | logging.warning("setting output SBU priority ({})".format(setOutputSource(2))) 469 | self._queued_updates.append((path, value)) 470 | return True # accept the change 471 | 472 | # THIS IS COMPLETELY UNTESTED 473 | def _update_PI17(self): 474 | raw = runInverterCommands(['GS','MOD','WS']) 475 | data, mode, warnings = raw 476 | with self._dbusmulti as m:#, self._dbusvebus as v: 477 | # 1=Charger Only;2=Inverter Only;3=On;4=Off -> Control from outside 478 | if 'error' in data and 'short' in data['error']: 479 | m['/State'] = 0 480 | m['/Alarms/Connection'] = 2 481 | 482 | # 0=Off;1=Low Power;2=Fault;3=Bulk;4=Absorption;5=Float;6=Storage;7=Equalize;8=Passthru;9=Inverting;10=Power assist;11=Power supply;252=External control 483 | invMode = mode.get('device_mode', None) 484 | if invMode == 'Battery': 485 | m['/State'] = 9 # Inverting 486 | elif invMode == 'Line': 487 | if data.get('is_charging_on', 0) == 1: 488 | m['/State'] = 3 # Passthru + Charging? = Bulk 489 | else: 490 | m['/State'] = 8 # Passthru 491 | elif invMode == 'Standby': 492 | m['/State'] = data.get('is_charging_on', 0) * 6 # Standby = 0 -> OFF, Stanby + Charging = 6 -> "Storage" Storing power 493 | else: 494 | m['/State'] = 0 # OFF 495 | # v['/State'] = m['/State'] 496 | 497 | # Normal operation, read data 498 | #v['/Dc/0/Voltage'] = 499 | m['/Dc/0/Voltage'] = data.get('battery_voltage', None) 500 | m['/Dc/0/Current'] = -data.get('battery_discharge_current', 0) 501 | #v['/Dc/0/Current'] = -m['/Dc/0/Current'] 502 | charging_ac_current = data.get('battery_charging_current', 0) 503 | load_on = data.get('is_load_on', 0) 504 | charging_ac = data.get('is_charging_on', 0) 505 | 506 | #v['/Ac/Out/L1/V'] = 507 | m['/Ac/Out/L1/V'] = data.get('ac_output_voltage', None) 508 | #v['/Ac/Out/L1/F'] = 509 | m['/Ac/Out/L1/F'] = data.get('ac_output_frequency', None) 510 | #v['/Ac/Out/L1/P'] =1 511 | m['/Ac/Out/L1/P'] = data.get('ac_output_active_power', None) 512 | #v['/Ac/Out/L1/S'] = 513 | m['/Ac/Out/L1/S'] = data.get('ac_output_aparent_power', None) 514 | 515 | # For my installation specific case: 516 | # - When the load is off the output is unkonwn, the AC1/OUT are connected directly, and inverter is bypassed 517 | if INVERTER_OFF_ASSUME_BYPASS and load_on == 0: 518 | m['/Ac/Out/L1/P'] = m['/Ac/Out/L1/S'] = None 519 | 520 | # Charger input, same as AC1 but separate line data 521 | #v['/Ac/ActiveIn/L1/V'] = 522 | m['/Ac/In/1/L1/V'] = data.get('ac_input_voltage', None) 523 | #v['/Ac/ActiveIn/L1/F'] = 524 | m['/Ac/In/1/L1/F'] = data.get('ac_input_frequency', None) 525 | 526 | # It does not give us power of AC in, we need to compute it from the current state + Output power + Charging on + Current 527 | if m['/State'] == 0: 528 | m['/Ac/In/1/L1/P'] = None # Unkown if inverter is off 529 | else: 530 | m['/Ac/In/1/L1/P'] = 0 if invMode == 'Battery' else m['/Ac/Out/L1/P'] 531 | m['/Ac/In/1/L1/P'] = (m['/Ac/In/1/L1/P'] or 0) + charging_ac * charging_ac_current * m['/Dc/0/Voltage'] 532 | #v['/Ac/ActiveIn/L1/P'] = m['/Ac/In/1/L1/P'] 533 | 534 | # Solar charger 535 | m['/Pv/0/V'] = data.get('pv_input_voltage', None) 536 | m['/Pv/0/P'] = data.get('pv_input_power', None) 537 | m['/MppOperationMode'] = 2 if (m['/Pv/0/P'] != None and m['/Pv/0/P'] > 0) else 0 538 | 539 | m['/Dc/0/Current'] = m['/Dc/0/Current'] + charging_ac * charging_ac_current - self._dcLast / (m['/Dc/0/Voltage'] or 27) 540 | # Compute the currents as well? 541 | # m['/Ac/Out/L1/I'] = m['/Ac/Out/L1/P'] / m['/Ac/Out/L1/V'] 542 | # m['/Ac/In/1/L1/I'] = m['/Ac/In/1/L1/P'] / m['/Ac/In/1/L1/V'] 543 | 544 | # Update some Alarms 545 | def getWarning(string): 546 | val = warnings.get(string, None) 547 | if val is None: 548 | return 1 549 | return int(val) * 2 550 | m['/Alarms/Connection'] = 0 551 | m['/Alarms/HighTemperature'] = getWarning('over_temperature_fault') 552 | m['/Alarms/Overload'] = getWarning('overload_fault') 553 | m['/Alarms/HighVoltage'] = getWarning('bus_over_fault') 554 | m['/Alarms/LowVoltage'] = getWarning('bus_under_fault') 555 | m['/Alarms/HighVoltageAcOut'] = getWarning('inverter_voltage_too_high_fault') 556 | m['/Alarms/LowVoltageAcOut'] = getWarning('inverter_voltage_too_low_fault') 557 | m['/Alarms/HighDcVoltage'] = getWarning('battery_voltage_to_high_fault') 558 | m['/Alarms/LowDcVoltage'] = getWarning('battery_low_alarm_warning') 559 | m['/Alarms/LineFail'] = getWarning('line_fail_warning') 560 | 561 | # Misc 562 | m['/Temperature'] = data.get('inverter_heat_sink_temperature', None) 563 | 564 | # Execute updates of previously updated values 565 | self._updateInternal() 566 | 567 | return True 568 | 569 | def _change_PI17(self, path, value): 570 | # if path == '/Ac/In/1/CurrentLimit' or path == '/Ac/In/2/CurrentLimit': 571 | # logging.warning("setting max utility charging current to = {} ({})".format(value, setMaxUtilityChargingCurrent(value))) 572 | # self._queued_updates.append((path, value)) 573 | 574 | # if path == '/Mode': # 1=Charger Only;2=Inverter Only;3=On;4=Off(?) 575 | # if value == 1: 576 | # #logging.warning("setting mode to 'Charger Only'(Charger=Util & Output=Util->solar) ({},{})".format(setChargerPriority(0), setOutputSource(0))) 577 | # logging.warning("setting mode to 'Charger Only'(Charger=Util) ({})".format(setChargerPriority(0))) 578 | # elif value == 2: 579 | # logging.warning("setting mode to 'Inverter Only'(Charger=Solar & Output=SBU) ({},{})".format(setChargerPriority(3), setOutputSource(2))) 580 | # elif value == 3: 581 | # logging.warning("setting mode to 'ON=Charge+Invert'(Charger=Util & Output=SBU) ({},{})".format(setChargerPriority(0), setOutputSource(2))) 582 | # elif value == 4: 583 | # #logging.warning("setting mode to 'OFF'(Charger=Solar & Output=Util->solar) ({},{})".format(setChargerPriority(3), setOutputSource(0))) 584 | # logging.warning("setting mode to 'OFF'(Charger=Solar) ({})".format(setChargerPriority(3))) 585 | # else: 586 | # logging.warning("setting mode not understood ({})".format(value)) 587 | # self._queued_updates.append((path, value)) 588 | # # Debug nodes 589 | # if path == '/Settings/Charger': 590 | # if value == 0: 591 | # logging.warning("setting charger priority to utility first ({})".format(setChargerPriority(value))) 592 | # elif value == 1: 593 | # logging.warning("setting charger priority to solar first ({})".format(setChargerPriority(value))) 594 | # elif value == 2: 595 | # logging.warning("setting charger priority to solar and utility ({})".format(setChargerPriority(value))) 596 | # else: 597 | # logging.warning("setting charger priority to only solar ({})".format(setChargerPriority(3))) 598 | # self._queued_updates.append((path, value)) 599 | # if path == '/Settings/Output': 600 | # if value == 0: 601 | # logging.warning("setting output Utility->Solar priority ({})".format(setOutputSource(value))) 602 | # elif value == 1: 603 | # logging.warning("setting output solar->Utility priority ({})".format(setOutputSource(value))) 604 | # else: 605 | # logging.warning("setting output SBU priority ({})".format(setOutputSource(2))) 606 | # self._queued_updates.append((path, value)) 607 | 608 | return True # accept the change 609 | 610 | def main(): 611 | parser = argparse.ArgumentParser() 612 | parser.add_argument("--baudrate","-b", default=2400, type=int) 613 | parser.add_argument("--serial","-s", required=True, type=str) 614 | global args 615 | args = parser.parse_args() 616 | 617 | from dbus.mainloop.glib import DBusGMainLoop 618 | # Have a mainloop, so we can send/receive asynchronous calls to and from dbus 619 | DBusGMainLoop(set_as_default=True) 620 | 621 | mppservice = DbusMppSolarService(tty=args.serial.strip("/dev/"), deviceinstance=0) 622 | logging.warning('Created service & connected to dbus, switching over to GLib.MainLoop() (= event based)') 623 | 624 | global mainloop 625 | mainloop = GLib.MainLoop() 626 | mainloop.run() 627 | 628 | 629 | if __name__ == "__main__": 630 | main() 631 | --------------------------------------------------------------------------------