2022-08-10 00:38:46 +02:00
#!/usr/bin/env python
2022-08-15 15:28:07 +02:00
# import normal packages
import platform
2022-08-10 00:38:46 +02:00
import logging
import sys
import os
2022-08-15 15:28:07 +02:00
import sys
if sys . version_info . major == 2 :
import gobject
else :
from gi . repository import GLib as gobject
import sys
import time
2022-08-10 00:38:46 +02:00
import requests # for http GET
2022-08-15 15:28:07 +02:00
# our own packages from victron
sys . path . insert ( 1 , os . path . join ( os . path . dirname ( __file__ ) , ' /opt/victronenergy/dbus-systemcalc-py/ext/velib_python ' ) )
2022-08-10 00:38:46 +02:00
from vedbus import VeDbusService
2023-07-15 11:47:45 +02:00
class NodeRedGridMeter :
2023-09-20 21:54:53 +02:00
def __init__ ( self , servicename , deviceinstance , paths , productname = ' Zähler Einspeisung ' , connection = ' Node RED HTTP JSON service ' ) :
2022-08-15 15:28:07 +02:00
self . _dbusservice = VeDbusService ( " {} .http_ {:02d} " . format ( servicename , deviceinstance ) )
2022-08-10 00:38:46 +02:00
self . _paths = paths
2022-08-15 15:28:07 +02:00
2022-08-10 00:38:46 +02:00
logging . debug ( " %s /DeviceInstance = %d " % ( servicename , deviceinstance ) )
2022-08-15 15:28:07 +02:00
2022-08-10 00:38:46 +02:00
# Create the management objects, as specified in the ccgx dbus-api document
self . _dbusservice . add_path ( ' /Mgmt/ProcessName ' , __file__ )
2022-08-19 23:17:24 +02:00
self . _dbusservice . add_path ( ' /Mgmt/ProcessVersion ' , ' Python ' + platform . python_version ( ) )
2022-08-10 00:38:46 +02:00
self . _dbusservice . add_path ( ' /Mgmt/Connection ' , connection )
2022-08-15 15:28:07 +02:00
2022-08-10 00:38:46 +02:00
# Create the mandatory objects
self . _dbusservice . add_path ( ' /DeviceInstance ' , deviceinstance )
2022-08-15 15:28:07 +02:00
self . _dbusservice . add_path ( ' /ProductId ' , 45069 )
2023-05-09 00:57:56 +02:00
self . _dbusservice . add_path ( ' /DeviceType ' , 1744 ) # EM540 for faster ESS control loop
2022-08-10 00:38:46 +02:00
self . _dbusservice . add_path ( ' /ProductName ' , productname )
2022-08-15 15:28:07 +02:00
self . _dbusservice . add_path ( ' /CustomName ' , productname )
self . _dbusservice . add_path ( ' /Latency ' , None )
2022-08-10 00:38:46 +02:00
self . _dbusservice . add_path ( ' /FirmwareVersion ' , 1.0 )
self . _dbusservice . add_path ( ' /HardwareVersion ' , 0 )
2023-05-26 12:14:29 +02:00
self . _dbusservice . add_path ( ' /Connected ' , 0 )
2022-08-15 15:28:07 +02:00
self . _dbusservice . add_path ( ' /Role ' , ' grid ' )
self . _dbusservice . add_path ( ' /Position ' , 0 )
self . _dbusservice . add_path ( ' /Serial ' , 12345678 )
self . _dbusservice . add_path ( ' /UpdateIndex ' , 0 )
# add path values to dbus
2022-08-10 00:38:46 +02:00
for path , settings in self . _paths . items ( ) :
self . _dbusservice . add_path (
2022-08-15 15:28:07 +02:00
path , settings [ ' initial ' ] , gettextcallback = settings [ ' textformat ' ] , writeable = True , onchangecallback = self . _handlechangedvalue )
# last update
self . _lastUpdate = 0
# add _update function 'timer'
gobject . timeout_add ( 500 , self . _update ) # pause 500ms before the next request
2022-08-10 12:09:36 +02:00
2022-08-19 23:17:24 +02:00
# add _Status 'timer' to get feedback in log every 5minutes
gobject . timeout_add ( self . _getStatusInterval ( ) * 60 * 1000 , self . _Status )
2022-08-15 15:28:07 +02:00
2022-08-19 23:17:24 +02:00
def _getStatusInterval ( self ) :
2022-08-15 15:28:07 +02:00
value = 1
if not value :
value = 0
return int ( value )
2023-07-15 11:44:22 +02:00
# if there is a grid meter disconnect or some other error, set mandatory values to None to stop ESS and set MPs to passthrough
def _errorState ( self ) :
self . _dbusservice [ ' /Connected ' ] = 0
self . _dbusservice [ ' /Ac/Power ' ] = None
self . _dbusservice [ ' /Ac/Current ' ] = None
self . _dbusservice [ ' /Ac/L1/Voltage ' ] = None
self . _dbusservice [ ' /Ac/L2/Voltage ' ] = None
self . _dbusservice [ ' /Ac/L3/Voltage ' ] = None
self . _dbusservice [ ' /Ac/L1/Current ' ] = None
self . _dbusservice [ ' /Ac/L2/Current ' ] = None
self . _dbusservice [ ' /Ac/L3/Current ' ] = None
self . _dbusservice [ ' /Ac/L1/Power ' ] = None
self . _dbusservice [ ' /Ac/L2/Power ' ] = None
self . _dbusservice [ ' /Ac/L3/Power ' ] = None
return True
2022-08-15 15:28:07 +02:00
def _getNodeRedData ( self ) :
URL = " http://localhost:1880/meters "
2023-05-26 12:14:29 +02:00
try :
meter_r = requests . get ( url = URL , timeout = 5 )
except requests . exceptions . RequestException as e :
2023-07-15 11:44:22 +02:00
self . _errorState ( )
2022-08-15 15:28:07 +02:00
raise ConnectionError ( " No response from NodeRed - %s " % ( URL ) )
2023-05-26 12:14:29 +02:00
2022-08-15 15:28:07 +02:00
meter_data = meter_r . json ( )
# check for Json
if not meter_data :
2023-07-15 11:44:22 +02:00
self . _errorState ( )
2022-08-15 15:28:07 +02:00
raise ValueError ( " Converting response to JSON failed " )
2022-08-10 12:09:36 +02:00
2023-07-15 11:47:45 +02:00
# if there is not error parsing data, set connected to true
2023-05-26 12:14:29 +02:00
self . _dbusservice [ ' /Connected ' ] = 1
2022-08-15 15:28:07 +02:00
return meter_data
2022-08-19 23:17:24 +02:00
def _Status ( self ) :
2023-04-22 13:27:03 +02:00
logging . debug ( " Last update: %s " % ( self . _lastUpdate ) )
logging . debug ( " Last ' /Ac/Power ' : %s " % ( self . _dbusservice [ ' /Ac/Power ' ] ) )
2022-08-10 00:38:46 +02:00
return True
2022-08-15 15:28:07 +02:00
def _update ( self ) :
try :
2023-07-15 11:47:45 +02:00
#get parsed data from NodeRed fuction
2022-08-15 15:28:07 +02:00
meter_data = self . _getNodeRedData ( )
#send data to DBus
2023-09-20 21:54:53 +02:00
self . _dbusservice [ ' /Ac/Power ' ] = meter_data [ ' grid_meter ' ] [ ' total_power ' ] # positive: consumption, negative: feed into grid
self . _dbusservice [ ' /Ac/Current ' ] = meter_data [ ' grid_meter ' ] [ ' total_current ' ]
self . _dbusservice [ ' /Ac/L1/Voltage ' ] = meter_data [ ' grid_meter ' ] [ ' l1_voltage ' ]
self . _dbusservice [ ' /Ac/L2/Voltage ' ] = meter_data [ ' grid_meter ' ] [ ' l2_voltage ' ]
self . _dbusservice [ ' /Ac/L3/Voltage ' ] = meter_data [ ' grid_meter ' ] [ ' l3_voltage ' ]
self . _dbusservice [ ' /Ac/L1/Current ' ] = meter_data [ ' grid_meter ' ] [ ' l1_current ' ]
self . _dbusservice [ ' /Ac/L2/Current ' ] = meter_data [ ' grid_meter ' ] [ ' l2_current ' ]
self . _dbusservice [ ' /Ac/L3/Current ' ] = meter_data [ ' grid_meter ' ] [ ' l3_current ' ]
self . _dbusservice [ ' /Ac/L1/Power ' ] = meter_data [ ' grid_meter ' ] [ ' l1_power ' ]
self . _dbusservice [ ' /Ac/L2/Power ' ] = meter_data [ ' grid_meter ' ] [ ' l2_power ' ]
self . _dbusservice [ ' /Ac/L3/Power ' ] = meter_data [ ' grid_meter ' ] [ ' l3_power ' ]
self . _dbusservice [ ' /Ac/L1/Energy/Forward ' ] = meter_data [ ' grid_meter ' ] [ ' l1_import ' ]
self . _dbusservice [ ' /Ac/L2/Energy/Forward ' ] = meter_data [ ' grid_meter ' ] [ ' l2_import ' ]
self . _dbusservice [ ' /Ac/L3/Energy/Forward ' ] = meter_data [ ' grid_meter ' ] [ ' l3_import ' ]
self . _dbusservice [ ' /Ac/L1/Energy/Reverse ' ] = meter_data [ ' grid_meter ' ] [ ' l1_export ' ]
self . _dbusservice [ ' /Ac/L2/Energy/Reverse ' ] = meter_data [ ' grid_meter ' ] [ ' l2_export ' ]
self . _dbusservice [ ' /Ac/L3/Energy/Reverse ' ] = meter_data [ ' grid_meter ' ] [ ' l3_export ' ]
self . _dbusservice [ ' /Ac/Energy/Forward ' ] = meter_data [ ' grid_meter ' ] [ ' total_import ' ]
self . _dbusservice [ ' /Ac/Energy/Reverse ' ] = meter_data [ ' grid_meter ' ] [ ' total_export ' ]
2022-08-15 15:28:07 +02:00
#logging
logging . debug ( " House Consumption (/Ac/Power): %s " % ( self . _dbusservice [ ' /Ac/Power ' ] ) )
logging . debug ( " House Forward (/Ac/Energy/Forward): %s " % ( self . _dbusservice [ ' /Ac/Energy/Forward ' ] ) )
logging . debug ( " House Reverse (/Ac/Energy/Revers): %s " % ( self . _dbusservice [ ' /Ac/Energy/Reverse ' ] ) )
logging . debug ( " --- " ) ;
# increment UpdateIndex - to show that new data is available
2023-07-16 13:14:29 +02:00
if self . _dbusservice [ ' /Connected ' ] == 1 :
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
2022-08-15 15:28:07 +02:00
#update lastupdate vars
2023-07-15 11:47:45 +02:00
self . _lastUpdate = time . time ( )
2022-08-15 15:28:07 +02:00
except Exception as e :
2023-07-15 11:44:22 +02:00
self . _errorState ( )
2022-08-15 15:28:07 +02:00
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
2022-08-10 00:38:46 +02:00
def _handlechangedvalue ( self , path , value ) :
logging . debug ( " someone else updated %s to %s " % ( path , value ) )
return True # accept the change
def main ( ) :
2022-08-15 15:28:07 +02:00
#configure logging
2022-08-10 12:09:36 +02:00
logging . basicConfig ( format = ' %(asctime)s , %(msecs)d %(name)s %(levelname)s %(message)s ' ,
datefmt = ' % Y- % m- %d % H: % M: % S ' ,
2022-08-15 15:28:07 +02:00
level = logging . INFO ,
2023-07-15 11:49:58 +02:00
#handlers=[
# logging.FileHandler("%s/current.log" % (os.path.dirname(os.path.realpath(__file__)))),
# logging.StreamHandler()
#]
)
2022-08-15 15:28:07 +02:00
2022-08-10 12:09:36 +02:00
try :
2022-08-15 15:28:07 +02:00
logging . info ( " Start " ) ;
2022-08-10 12:09:36 +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 )
2022-08-15 15:28:07 +02:00
#formatting
2022-08-10 12:09:36 +02:00
_kwh = lambda p , v : ( str ( round ( v , 2 ) ) + ' KWh ' )
2022-08-15 15:28:07 +02:00
_a = lambda p , v : ( str ( round ( v , 1 ) ) + ' A ' )
_w = lambda p , v : ( str ( round ( v , 1 ) ) + ' W ' )
_v = lambda p , v : ( str ( round ( v , 1 ) ) + ' V ' )
#start our main-service
2023-07-15 11:47:45 +02:00
pvac_output = NodeRedGridMeter (
2022-08-15 15:28:07 +02:00
servicename = ' com.victronenergy.grid ' ,
deviceinstance = 40 ,
paths = {
2023-04-22 13:27:03 +02:00
' /Ac/Energy/Forward ' : { ' initial ' : 0 , ' textformat ' : _kwh } , # energy bought from the grid, summ'd over all phases
' /Ac/Energy/Reverse ' : { ' initial ' : 0 , ' textformat ' : _kwh } , # energy sold to the grid, summ'd over all phases
' /Ac/Power ' : { ' initial ' : 0 , ' textformat ' : _w } , # power accumulated over all phases for ess regulation
' /Ac/Current ' : { ' initial ' : 0 , ' textformat ' : _a } , # current accumulated over all phases
2022-08-15 15:28:07 +02:00
' /Ac/L1/Voltage ' : { ' initial ' : 0 , ' textformat ' : _v } ,
' /Ac/L2/Voltage ' : { ' initial ' : 0 , ' textformat ' : _v } ,
' /Ac/L3/Voltage ' : { ' initial ' : 0 , ' textformat ' : _v } ,
' /Ac/L1/Current ' : { ' initial ' : 0 , ' textformat ' : _a } ,
' /Ac/L2/Current ' : { ' initial ' : 0 , ' textformat ' : _a } ,
' /Ac/L3/Current ' : { ' initial ' : 0 , ' textformat ' : _a } ,
' /Ac/L1/Power ' : { ' initial ' : 0 , ' textformat ' : _w } ,
' /Ac/L2/Power ' : { ' initial ' : 0 , ' textformat ' : _w } ,
' /Ac/L3/Power ' : { ' initial ' : 0 , ' textformat ' : _w } ,
2022-08-10 12:09:36 +02:00
' /Ac/L1/Energy/Forward ' : { ' initial ' : 0 , ' textformat ' : _kwh } ,
' /Ac/L2/Energy/Forward ' : { ' initial ' : 0 , ' textformat ' : _kwh } ,
' /Ac/L3/Energy/Forward ' : { ' initial ' : 0 , ' textformat ' : _kwh } ,
' /Ac/L1/Energy/Reverse ' : { ' initial ' : 0 , ' textformat ' : _kwh } ,
' /Ac/L2/Energy/Reverse ' : { ' initial ' : 0 , ' textformat ' : _kwh } ,
' /Ac/L3/Energy/Reverse ' : { ' initial ' : 0 , ' textformat ' : _kwh } ,
2022-08-15 15:28:07 +02:00
} )
2022-08-10 12:09:36 +02:00
logging . info ( ' Connected to dbus, and switching over to gobject.MainLoop() (= event based) ' )
mainloop = gobject . MainLoop ( )
2022-08-15 15:28:07 +02:00
mainloop . run ( )
2022-08-10 12:09:36 +02:00
except Exception as e :
logging . critical ( ' Error at %s ' , ' main ' , exc_info = e )
2022-08-10 00:38:46 +02:00
if __name__ == " __main__ " :
2023-04-22 13:27:03 +02:00
main ( )