2022-04-01 10:05:18 +02:00
#!/usr/bin/env python
# import normal packages
import platform
import logging
2025-02-27 11:40:57 +01:00
from logging . handlers import RotatingFileHandler
2022-04-01 10:05:18 +02:00
import sys
import os
import sys
if sys . version_info . major == 2 :
import gobject
else :
from gi . repository import GLib as gobject
import sys
import time
import requests # for http GET
import configparser # for config/ini file
# our own packages from victron
sys . path . insert ( 1 , os . path . join ( os . path . dirname ( __file__ ) , ' /opt/victronenergy/dbus-systemcalc-py/ext/velib_python ' ) )
from vedbus import VeDbusService
class DbusGoeChargerService :
def __init__ ( self , servicename , paths , productname = ' go-eCharger ' , connection = ' go-eCharger HTTP JSON service ' ) :
config = self . _getConfig ( )
deviceinstance = int ( config [ ' DEFAULT ' ] [ ' Deviceinstance ' ] )
2023-05-17 22:54:28 +02:00
hardwareVersion = int ( config [ ' DEFAULT ' ] [ ' HardwareVersion ' ] )
2025-02-27 12:05:32 +01:00
acPosition = int ( config [ ' DEFAULT ' ] [ ' AcPosition ' ] )
2024-04-18 14:33:41 +02:00
pauseBetweenRequests = int ( config [ ' ONPREMISE ' ] [ ' PauseBetweenRequests ' ] ) # in ms
if pauseBetweenRequests < = 20 :
raise ValueError ( " Pause between requests must be greater than 20 " )
2025-02-27 11:40:57 +01:00
self . _dbusservice = VeDbusService ( " {} .http_ {:02d} " . format ( servicename , deviceinstance ) , register = False )
2022-04-01 10:05:18 +02:00
self . _paths = paths
logging . debug ( " %s /DeviceInstance = %d " % ( servicename , deviceinstance ) )
paths_wo_unit = [
2022-04-03 20:43:22 +02:00
' /Status ' , # value 'car' 1: charging station ready, no vehicle 2: vehicle loads 3: Waiting for vehicle 4: Charge finished, vehicle still connected
' /Mode '
2022-04-01 10:05:18 +02:00
]
2022-04-03 20:43:22 +02:00
#get data from go-eCharger
2025-02-27 11:40:57 +01:00
data = self . _getGoeChargerData ( ' sse,fwv ' )
2022-04-03 20:43:22 +02:00
2022-04-01 10:05:18 +02:00
# Create the management objects, as specified in the ccgx dbus-api document
self . _dbusservice . add_path ( ' /Mgmt/ProcessName ' , __file__ )
self . _dbusservice . add_path ( ' /Mgmt/ProcessVersion ' , ' Unkown version, and running on Python ' + platform . python_version ( ) )
self . _dbusservice . add_path ( ' /Mgmt/Connection ' , connection )
# Create the mandatory objects
self . _dbusservice . add_path ( ' /DeviceInstance ' , deviceinstance )
self . _dbusservice . add_path ( ' /ProductId ' , 0xFFFF ) #
self . _dbusservice . add_path ( ' /ProductName ' , productname )
self . _dbusservice . add_path ( ' /CustomName ' , productname )
2023-11-05 12:42:29 +01:00
if data :
2024-09-20 17:06:11 +02:00
fwv = data [ ' fwv ' ]
try :
fwv = int ( data [ ' fwv ' ] . replace ( ' . ' , ' ' ) )
except :
pass
self . _dbusservice . add_path ( ' /FirmwareVersion ' , fwv )
2023-11-05 12:42:29 +01:00
self . _dbusservice . add_path ( ' /Serial ' , data [ ' sse ' ] )
2023-05-17 22:54:28 +02:00
self . _dbusservice . add_path ( ' /HardwareVersion ' , hardwareVersion )
2022-04-01 10:05:18 +02:00
self . _dbusservice . add_path ( ' /Connected ' , 1 )
self . _dbusservice . add_path ( ' /UpdateIndex ' , 0 )
2025-03-02 11:18:11 +00:00
self . _dbusservice . add_path ( ' /Position ' , acPosition )
2022-04-01 10:05:18 +02:00
# add paths without units
for path in paths_wo_unit :
self . _dbusservice . add_path ( path , None )
# add path values to dbus
for path , settings in self . _paths . items ( ) :
self . _dbusservice . add_path (
path , settings [ ' initial ' ] , gettextcallback = settings [ ' textformat ' ] , writeable = True , onchangecallback = self . _handlechangedvalue )
2025-02-27 11:40:57 +01:00
# register the service
self . _dbusservice . register ( )
2022-04-01 10:05:18 +02:00
# last update
self . _lastUpdate = 0
2022-04-05 13:12:29 +02:00
2022-04-10 19:56:12 +02:00
# charging time in float
self . _chargingTime = 0.0
2022-04-01 10:05:18 +02:00
# add _update function 'timer'
2024-04-18 14:33:41 +02:00
gobject . timeout_add ( pauseBetweenRequests , self . _update )
2022-04-01 10:05:18 +02:00
# add _signOfLife 'timer' to get feedback in log every 5minutes
gobject . timeout_add ( self . _getSignOfLifeInterval ( ) * 60 * 1000 , self . _signOfLife )
def _getConfig ( self ) :
config = configparser . ConfigParser ( )
config . read ( " %s /config.ini " % ( os . path . dirname ( os . path . realpath ( __file__ ) ) ) )
2022-04-03 20:43:22 +02:00
return config
2022-04-01 10:05:18 +02:00
def _getSignOfLifeInterval ( self ) :
config = self . _getConfig ( )
value = config [ ' DEFAULT ' ] [ ' SignOfLifeLog ' ]
if not value :
value = 0
return int ( value )
def _getGoeChargerStatusUrl ( self ) :
config = self . _getConfig ( )
accessType = config [ ' DEFAULT ' ] [ ' AccessType ' ]
if accessType == ' OnPremise ' :
2025-02-27 12:49:17 +00:00
URL = " http:// %s /api/status " % ( config [ ' ONPREMISE ' ] [ ' Host ' ] )
2022-04-01 10:05:18 +02:00
else :
2025-02-27 12:57:17 +00:00
raise ValueError ( " AccessType %s is not supported " % ( config [ ' DEFAULT ' ] [ ' AccessType ' ] ) )
2022-04-01 10:05:18 +02:00
return URL
2022-04-05 21:19:43 +02:00
def _getGoeChargerMqttPayloadUrl ( self , parameter , value ) :
config = self . _getConfig ( )
accessType = config [ ' DEFAULT ' ] [ ' AccessType ' ]
if accessType == ' OnPremise ' :
URL = " http:// %s /mqtt?payload= %s = %s " % ( config [ ' ONPREMISE ' ] [ ' Host ' ] , parameter , value )
else :
raise ValueError ( " AccessType %s is not supported " % ( config [ ' DEFAULT ' ] [ ' AccessType ' ] ) )
return URL
def _setGoeChargerValue ( self , parameter , value ) :
URL = self . _getGoeChargerMqttPayloadUrl ( parameter , str ( value ) )
request_data = requests . get ( url = URL )
# check for response
if not request_data :
raise ConnectionError ( " No response from go-eCharger - %s " % ( URL ) )
json_data = request_data . json ( )
# check for Json
if not json_data :
raise ValueError ( " Converting response to JSON failed " )
if json_data [ parameter ] == str ( value ) :
return True
else :
logging . warning ( " go-eCharger parameter %s not set to %s " % ( parameter , str ( value ) ) )
return False
2022-04-01 10:05:18 +02:00
2025-02-27 11:40:57 +01:00
def _getGoeChargerData ( self , filter ) :
URL = " %s ?filter= %s " % ( self . _getGoeChargerStatusUrl ( ) , filter )
2023-11-05 12:42:29 +01:00
try :
2025-02-27 11:40:57 +01:00
request_data = requests . get ( url = URL , timeout = 1 )
2023-11-05 12:42:29 +01:00
except Exception :
return None
2022-04-01 10:05:18 +02:00
# check for response
if not request_data :
raise ConnectionError ( " No response from go-eCharger - %s " % ( URL ) )
json_data = request_data . json ( )
# check for Json
if not json_data :
raise ValueError ( " Converting response to JSON failed " )
return json_data
def _signOfLife ( self ) :
logging . info ( " --- Start: sign of life --- " )
logging . info ( " Last _update() call: %s " % ( self . _lastUpdate ) )
logging . info ( " Last ' /Ac/Power ' : %s " % ( self . _dbusservice [ ' /Ac/Power ' ] ) )
logging . info ( " --- End: sign of life --- " )
return True
def _update ( self ) :
try :
#get data from go-eCharger
2025-02-27 12:49:17 +00:00
data = self . _getGoeChargerData ( ' nrg,eto,wh,alw,amp,ama,car,tmp,tma ' )
2022-04-01 10:05:18 +02:00
2023-11-05 12:42:29 +01:00
if data is not None :
2024-04-18 14:33:41 +02:00
'''
data [ ' nrg ' ]
0 = U L1
1 = U L2
2 = U L3
3 = U N
4 = I L1
5 = I L2
6 = I L3
7 = P L1
8 = P L2
9 = P L3
10 = P N
11 = P Total
12 = PF L1
13 = PF L2
14 = PF L3
15 = PF N
'''
2025-02-27 12:49:17 +00:00
config = self . _getConfig ( )
hardwareVersion = int ( config [ ' DEFAULT ' ] [ ' HardwareVersion ' ] )
2024-04-18 14:33:41 +02:00
2023-11-05 12:42:29 +01:00
#send data to DBus
self . _dbusservice [ ' /Ac/Voltage ' ] = int ( data [ ' nrg ' ] [ 0 ] )
2025-03-02 11:18:11 +00:00
self . _dbusservice [ ' /Ac/L1/Power ' ] = int ( data [ ' nrg ' ] [ 7 ] )
self . _dbusservice [ ' /Ac/L2/Power ' ] = int ( data [ ' nrg ' ] [ 8 ] )
self . _dbusservice [ ' /Ac/L3/Power ' ] = int ( data [ ' nrg ' ] [ 9 ] )
self . _dbusservice [ ' /Ac/Power ' ] = int ( data [ ' nrg ' ] [ 11 ] )
self . _dbusservice [ ' /Current ' ] = max ( data [ ' nrg ' ] [ 4 ] , data [ ' nrg ' ] [ 5 ] , data [ ' nrg ' ] [ 6 ] )
if int ( hardwareVersion ) < 4 :
2025-02-27 12:49:17 +00:00
self . _dbusservice [ ' /Ac/Energy/Forward ' ] = int ( float ( data [ ' eto ' ] ) / 1000.0 )
2025-03-02 11:18:11 +00:00
else :
2025-02-27 12:57:17 +00:00
self . _dbusservice [ ' /Ac/Energy/Forward ' ] = round ( data [ ' wh ' ] / 1000 , 2 )
2023-11-05 12:42:29 +01:00
self . _dbusservice [ ' /StartStop ' ] = int ( data [ ' alw ' ] )
self . _dbusservice [ ' /SetCurrent ' ] = int ( data [ ' amp ' ] )
self . _dbusservice [ ' /MaxCurrent ' ] = int ( data [ ' ama ' ] )
2025-02-27 13:01:39 +00:00
2023-11-05 12:42:29 +01:00
# update chargingTime, increment charge time only on active charging (2), reset when no car connected (1)
timeDelta = time . time ( ) - self . _lastUpdate
if int ( data [ ' car ' ] ) == 2 and self . _lastUpdate > 0 : # vehicle loads
self . _chargingTime + = timeDelta
elif int ( data [ ' car ' ] ) == 1 : # charging station ready, no vehicle
self . _chargingTime = 0
self . _dbusservice [ ' /ChargingTime ' ] = int ( self . _chargingTime )
2022-04-03 20:43:22 +02:00
2023-11-05 12:42:29 +01:00
self . _dbusservice [ ' /Mode ' ] = 0 # Manual, no control
2025-03-06 08:38:16 +01:00
config = self . _getConfig ( )
hardwareVersion = int ( config [ ' DEFAULT ' ] [ ' HardwareVersion ' ] )
if ' /MCU/Temperature ' in self . _dbusservice : # check if path exists, at some point it was removed
if hardwareVersion > = 3 :
self . _dbusservice [ ' /MCU/Temperature ' ] = int ( data [ ' tma ' ] [ 0 ] if data [ ' tma ' ] [ 0 ] else 0 )
else :
self . _dbusservice [ ' /MCU/Temperature ' ] = int ( data [ ' tmp ' ] )
2022-04-03 20:43:22 +02:00
2025-02-27 11:40:57 +01:00
# carState, null if internal error (Unknown/Error=0, Idle=1, Charging=2, WaitCar=3, Complete=4, Error=5)
# status 0=Disconnected; 1=Connected; 2=Charging; 3=Charged; 4=Waiting for sun; 5=Waiting for RFID; 6=Waiting for start; 7=Low SOC; 8=Ground fault; 9=Welded contacts; 10=CP Input shorted; 11=Residual current detected; 12=Under voltage detected; 13=Overvoltage detected; 14=Overheating detected
2023-11-05 12:42:29 +01:00
status = 0
if int ( data [ ' car ' ] ) == 1 :
status = 0
elif int ( data [ ' car ' ] ) == 2 :
status = 2
elif int ( data [ ' car ' ] ) == 3 :
status = 6
elif int ( data [ ' car ' ] ) == 4 :
status = 3
self . _dbusservice [ ' /Status ' ] = status
2022-04-01 10:05:18 +02:00
2023-11-05 12:42:29 +01:00
#logging
logging . debug ( " Wallbox Consumption (/Ac/Power): %s " % ( self . _dbusservice [ ' /Ac/Power ' ] ) )
logging . debug ( " Wallbox Forward (/Ac/Energy/Forward): %s " % ( self . _dbusservice [ ' /Ac/Energy/Forward ' ] ) )
logging . debug ( " --- " )
# increment UpdateIndex - to show that new data is available
index = self . _dbusservice [ ' /UpdateIndex ' ] + 1 # increment index
if index > 255 : # maximum value of the index
index = 0 # overflow from 255 to 0
self . _dbusservice [ ' /UpdateIndex ' ] = index
#update lastupdate vars
self . _lastUpdate = time . time ( )
else :
logging . debug ( " Wallbox is not available " )
2022-04-01 10:05:18 +02:00
except Exception as e :
logging . critical ( ' Error at %s ' , ' _update ' , exc_info = e )
# 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
return True
def _handlechangedvalue ( self , path , value ) :
2022-04-05 20:25:39 +02:00
logging . info ( " someone else updated %s to %s " % ( path , value ) )
2022-04-05 21:19:43 +02:00
if path == ' /SetCurrent ' :
return self . _setGoeChargerValue ( ' amp ' , value )
elif path == ' /StartStop ' :
return self . _setGoeChargerValue ( ' alw ' , value )
elif path == ' /MaxCurrent ' :
return self . _setGoeChargerValue ( ' ama ' , value )
else :
logging . info ( " mapping for evcharger path %s does not exist " % ( path ) )
return False
2022-04-01 10:05:18 +02:00
def main ( ) :
#configure logging
2024-04-18 14:33:41 +02:00
config = configparser . ConfigParser ( )
config . read ( f " { ( os . path . dirname ( os . path . realpath ( __file__ ) ) ) } /config.ini " )
logging_level = config [ " DEFAULT " ] [ " Logging " ] . upper ( )
2022-04-01 10:05:18 +02:00
logging . basicConfig ( format = ' %(asctime)s , %(msecs)d %(name)s %(levelname)s %(message)s ' ,
datefmt = ' % Y- % m- %d % H: % M: % S ' ,
2024-04-18 14:33:41 +02:00
level = logging_level ,
2022-04-01 10:05:18 +02:00
handlers = [
2025-02-27 11:40:57 +01:00
RotatingFileHandler ( " %s /current.log " % ( os . path . dirname ( os . path . realpath ( __file__ ) ) ) , maxBytes = 10000 ) ,
2022-04-01 10:05:18 +02:00
logging . StreamHandler ( )
] )
try :
2022-04-03 20:43:22 +02:00
logging . info ( " Start " )
2022-04-01 10:05:18 +02:00
from dbus . mainloop . glib import DBusGMainLoop
# Have a mainloop, so we can send/receive asynchronous calls to and from dbus
DBusGMainLoop ( set_as_default = True )
#formatting
2022-04-14 10:42:22 +02:00
_kwh = lambda p , v : ( str ( round ( v , 2 ) ) + ' kWh ' )
2022-04-01 10:05:18 +02:00
_a = lambda p , v : ( str ( round ( v , 1 ) ) + ' A ' )
_w = lambda p , v : ( str ( round ( v , 1 ) ) + ' W ' )
2022-04-03 20:43:22 +02:00
_v = lambda p , v : ( str ( round ( v , 1 ) ) + ' V ' )
_degC = lambda p , v : ( str ( v ) + ' °C ' )
2022-04-14 10:42:22 +02:00
_s = lambda p , v : ( str ( v ) + ' s ' )
2022-04-01 10:05:18 +02:00
#start our main-service
pvac_output = DbusGoeChargerService (
servicename = ' com.victronenergy.evcharger ' ,
2022-04-03 20:43:22 +02:00
paths = {
2022-04-01 10:05:18 +02:00
' /Ac/Power ' : { ' initial ' : 0 , ' textformat ' : _w } ,
' /Ac/L1/Power ' : { ' initial ' : 0 , ' textformat ' : _w } ,
' /Ac/L2/Power ' : { ' initial ' : 0 , ' textformat ' : _w } ,
' /Ac/L3/Power ' : { ' initial ' : 0 , ' textformat ' : _w } ,
' /Ac/Energy/Forward ' : { ' initial ' : 0 , ' textformat ' : _kwh } ,
2022-04-14 10:42:22 +02:00
' /ChargingTime ' : { ' initial ' : 0 , ' textformat ' : _s } ,
2022-04-01 10:05:18 +02:00
' /Ac/Voltage ' : { ' initial ' : 0 , ' textformat ' : _v } ,
' /Current ' : { ' initial ' : 0 , ' textformat ' : _a } ,
2022-04-04 18:18:36 +02:00
' /SetCurrent ' : { ' initial ' : 0 , ' textformat ' : _a } ,
2022-04-03 20:43:22 +02:00
' /MaxCurrent ' : { ' initial ' : 0 , ' textformat ' : _a } ,
2022-04-05 21:19:43 +02:00
' /MCU/Temperature ' : { ' initial ' : 0 , ' textformat ' : _degC } ,
2022-04-05 21:25:51 +02:00
' /StartStop ' : { ' initial ' : 0 , ' textformat ' : lambda p , v : ( str ( v ) ) }
2022-04-03 20:43:22 +02:00
}
)
2022-04-01 10:05:18 +02:00
logging . info ( ' Connected to dbus, and switching over to gobject.MainLoop() (= event based) ' )
mainloop = gobject . MainLoop ( )
mainloop . run ( )
except Exception as e :
logging . critical ( ' Error at %s ' , ' main ' , exc_info = e )
if __name__ == " __main__ " :
main ( )