Compare commits

...

6 Commits

3 changed files with 74 additions and 81 deletions

View File

@ -38,7 +38,7 @@ I try to integrate every datapoint from Batriums BMS to fill Victrons registers.
- Ah, kWh charged and discharged is calcualated by script (see above)
- Capacity of battery must be entered into script batrium.py (see above)
- dbus script is started by rc.local which gets executed last, so all mppts and multiplus' showing temporarily "BMS missing" and need to be cleared after (re-)boot of Venus OS
- include calculation of State Of Health, based on Ah discharge and SoC State after full charge, becuase Batrium does not have
- include calculation of State Of Health, based on Ah discharge and SoC State after full charge, because Batrium does not have
### Victrons datapoints
This script will emulate one of Victrons own BMS, therefore are many registers to fill, but I dont know how to fill all.

View File

@ -15,10 +15,10 @@ VERSION = '1.0'
class BatriumBattery(can.Listener):
def __init__(self, connection):
##Capacity of battery need to set here, because Batrium not transmitting
self.capacity = 300
self.maxChargeVoltage = 0
self.numberOfModules = 0
self.chargeComplete = 0
self.soc = 0
self.voltage = 0
self.current = 0
@ -28,10 +28,10 @@ class BatriumBattery(can.Listener):
self.minCellTemperature = 0
self.minCellTemperatureId = 0
self.cellVoltages_min = dict()
self.cellVoltages_max = dict()
self.cellTemperatures = dict()
self.cellBypassTemperatures = dict()
self.cellBypassPWM = dict()
#self.cellVoltages_max = dict()
#self.cellTemperatures = dict()
#self.cellBypassTemperatures = dict()
#self.cellBypassPWM = dict()
self.maxCellVoltage = 0
self.maxCellVoltageId = 0
self.minCellVoltage = 0
@ -39,17 +39,15 @@ class BatriumBattery(can.Listener):
self.maxChargeCurrent = 0
self.maxDischargeCurrent = 0
self.updated = -1
self.cyclicModeTask = None
self.TimeToEmpty = 0
self.TimeToFull = 0
self.AhToEmpty = 0
self.AhToFull = 0
#self.AhToEmpty = 0
#self.AhToFull = 0
self.NumberInBypass = 0
self.alarm_high_temperature = 0
self.alarm_high_voltage = 0
self.alarm_low_temperature = 0
self.alarm_low_voltage = 0
self.alarm_low_soc = 0
self.alarm_high_charge_current = 0
self.alarm_high_discharge_current = 0
@ -83,10 +81,10 @@ class BatriumBattery(can.Listener):
self.current = round((struct.unpack('>f', bytearray([msg.data[7], msg.data[6], msg.data[5], msg.data[4]])))[0] * 0.001, 2)
self.temperature = msg.data[1] - 40
elif msg.arbitration_id == 0x00111900:
logging.debug("Ah 0x00111900 received")
#elif msg.arbitration_id == 0x00111900:
#logging.debug("Ah 0x00111900 received")
# message never arrived, batrium fw bug?
# message never arrived, batrium fw bug? contacted Batrium I hope they fix it too
#self.AhToFull = round((struct.unpack('>f', bytearray([msg.data[3], msg.data[2], msg.data[1], msg.data[0]])))[0] * 0.001, 2)
#self.AhToEmpty = round((struct.unpack('>f', bytearray([msg.data[7], msg.data[6], msg.data[5], msg.data[4]])))[0] * 0.001, 2)
@ -150,21 +148,15 @@ class BatriumBattery(can.Listener):
elif ((msg.arbitration_id >= 0x001D1101) and (msg.arbitration_id <= 0x001D11F9)):
cell_id = msg.arbitration_id & 0xff
#logging.debug("Cell ID: %d, Voltage_min: %2.2f, Voltage_max: %2.2f, Temperature: %d, Bypass_Temperature: %d, BypassPWM: %d", cell_id, ((msg.data[1] * 256) + msg.data[0]) * 0.001, ((msg.data[3] * 256) + msg.data[2]) * 0.001, msg.data[4] - 40, msg.data[5] - 40, msg.data[6])
logging.debug("Cell ID: %d, Voltage_min: %2.2f, Voltage_max: %2.2f, Temperature: %d, Bypass_Temperature: %d, BypassPWM: %d", cell_id, ((msg.data[1] * 256) + msg.data[0]) * 0.001, ((msg.data[3] * 256) + msg.data[2]) * 0.001, msg.data[4] - 40, msg.data[5] - 40, msg.data[6])
self.cellVoltages_min[cell_id] = ((msg.data[1] * 256) + msg.data[0]) * 0.001
self.cellVoltages_max[cell_id] = ((msg.data[3] * 256) + msg.data[2]) * 0.001
self.cellTemperatures[cell_id] = msg.data[4] - 40
self.cellBypassTemperatures[cell_id] = msg.data[5] - 40
self.cellBypassPWM[cell_id] = msg.data[6]
#self.cellVoltages_max[cell_id] = ((msg.data[3] * 256) + msg.data[2]) * 0.001
#self.cellTemperatures[cell_id] = msg.data[4] - 40
#self.cellBypassTemperatures[cell_id] = msg.data[5] - 40
#self.cellBypassPWM[cell_id] = msg.data[6]
self.numberOfModules = len(self.cellVoltages_min.keys())
def prnt(self):
print("SOC: %2d, I: %2.2fA, U: %2.2fV, T:%2.1fC" % (self.soc, self.current, self.voltage, self.maxCellTemperature))
print("MinCellVolt: %1.2f, MaxCellVolt: %1.2f CellVoltDelta: %1.2f" % (self.minCellVoltage, self.maxCellVoltage, self.maxCellVoltage-self.minCellVoltage))
print("MaxChargeCurrent (CCL): %4.1f A MaxChargeVoltage (CVL): %2.2f V" % (self.maxChargeCurrent, self.maxChargeVoltage))
print("Number of modules found: %1.0f" % (self.numberOfModules))
# === All code below is to simply run it from the commandline for debugging purposes ===
## All code below is to simply run it from the commandline for debugging purposes ##
def main():
logging.basicConfig( format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
@ -172,23 +164,20 @@ def main():
stream=sys.stdout,
)
logging.info('Starting dbus_batrium_native listener')
logging.info('Starting dbus-batrium-native listener')
#logger = can.Logger('logfile.asc')
#logger = can.Logger('logfile.asc') # CAN raw data logging
bat = BatriumBattery(connection='can1')
listeners = [
#logger, # Regular Listener object
#logger, # CAN raw data logging
bat
]
notifier = can.Notifier(bat._ci, listeners)
for msg in bat._ci:
if msg.arbitration_id == 0x00140100:
bat.prnt()
# Clean-up
# Shutdown
notifier.stop()
bat._ci.shutdown()

View File

@ -23,13 +23,13 @@ from argparse import ArgumentParser
from batrium import *
# our own packages
# Victron energy libraries
sys.path.insert(1, os.path.join(os.path.dirname(__file__), '/opt/victronenergy/dbus-systemcalc-py/ext/velib_python'))
from vedbus import VeDbusService
from ve_utils import exit_on_error
from settingsdevice import SettingsDevice
VERSION = '1.0'
VERSION = '1.5'
def handle_changed_setting(setting, oldvalue, newvalue):
logging.debug('setting changed, setting: %s, old: %s, new: %s' % (setting, oldvalue, newvalue))
@ -53,7 +53,7 @@ class DbusBatteryService:
except:
exit
# Create the management objects, as specified in the ccgx dbus-api document
# Create the management objects, as specified in the Venus OS dbus-api document https://github.com/victronenergy/venus/wiki/dbus-api
self._dbusservice.add_path('/Mgmt/ProcessName', __file__)
self._dbusservice.add_path('/Mgmt/ProcessVersion', VERSION + ' running on Python ' + platform.python_version())
self._dbusservice.add_path('/Mgmt/Connection', connection)
@ -66,7 +66,7 @@ class DbusBatteryService:
self._dbusservice.add_path('/HardwareVersion', 'unknown')
self._dbusservice.add_path('/Connected', 0)
# Create battery specific objects
self._dbusservice.add_path('/Status', None, writeable=True)
self._dbusservice.add_path('/Status', 0, writeable=False)
self._dbusservice.add_path('/Dc/0/Temperature', None, writeable=True)
self._dbusservice.add_path('/Info/BatteryLowVoltage', None, writeable=True)
self._dbusservice.add_path('/Alarms/CellImbalance', None, writeable=True)
@ -104,7 +104,6 @@ class DbusBatteryService:
},
eventCallback=handle_changed_setting)
self._summeditems = {
'/System/MaxCellVoltage': {'gettext': '%.3F V'},
'/System/MinCellVoltage': {'gettext': '%.3F V'},
@ -131,24 +130,27 @@ class DbusBatteryService:
for path in self._summeditems.keys():
self._dbusservice.add_path(path, value=None, gettextcallback=self._gettext)
## Load settings from Venus OS config xml ##
self._dbusservice['/History/AverageDischarge'] = self._settings['AvgDischarge']
self._dbusservice['/History/TotalAhDrawn'] = self._settings['TotalAhDrawn']
self._dbusservice.add_path('/History/TimeSinceLastFullCharge', 0)
self._dbusservice.add_path('/History/MinCellVoltage', self._settings['MinCellVoltage'])
self._dbusservice.add_path('/History/MaxCellVoltage', self._settings['MaxCellVoltage'])
## Set variables I will use later to standard values ##
self._dbusservice['/Soh'] = 100
self._dbusservice['/Status'] = 0
self._dbusservice['/ConsumedAmphours'] = 0
## Set CCL to 1 amp temporarily, it will overwritte immediately after first message, suppresing BMS missing after script crash ##
self._dbusservice['/Info/MaxChargeCurrent'] = 1
logging.info("History cell voltage min: %.3f, max: %.3f, totalAhDrawn: %d", self._settings['MinCellVoltage'], self._settings['MaxCellVoltage'], self._settings['TotalAhDrawn'])
## Set SOC according to Batrium as init values, if Cerbo or script is restarted at 100% SOC
if self._bat.soc <= 99:
self._dbusservice['/Soc'] = self._bat.soc
else:
self._dbusservice['/Soc'] = 100
## Log values written to history/XML ##
logging.debug("History cell voltage min: %.3f, max: %.3f, totalAhDrawn: %d", self._settings['MinCellVoltage'], self._settings['MaxCellVoltage'], self._settings['TotalAhDrawn'])
GLib.timeout_add( self._settings['interval'], exit_on_error, self._update)
def _gettext(self, path, value):
@ -159,7 +161,7 @@ class DbusBatteryService:
def __del__(self):
self._safe_history()
logging.info('Stopping dbus_Batrium')
logging.info('Stopping dbus-batrium-native integration')
def _safe_history(self):
logging.debug('Saving history to localsettings')
@ -170,22 +172,31 @@ class DbusBatteryService:
self._settings['TargetChargeVoltage'] = self._dbusservice['/Info/MaxChargeVoltage']
def _daily_stats(self):
## Update daily statistics and reset energy counters
if (self._dbusservice['/History/DischargedEnergy'] == 0): return
logging.info("Updating stats, SOC: %d, Discharged: %.2f, Charged: %.2f ",self._bat.soc, self._dbusservice['/History/DischargedEnergy'], self._dbusservice['/History/ChargedEnergy'])
logging.debug("Updating stats, SOC: %d, Discharged: %.2f, Charged: %.2f ",self._bat.soc, self._dbusservice['/History/DischargedEnergy'], self._dbusservice['/History/ChargedEnergy'])
self._dbusservice['/History/AverageDischarge'] = (6*self._dbusservice['/History/AverageDischarge'] + self._dbusservice['/History/DischargedEnergy'])/7 #rolling week
self._dbusservice['/History/ChargedEnergy'] = 0
self.ChargedEnergy = 0
self._dbusservice['/History/DischargedEnergy'] = 0
self.DischargedEnergy = 0
dt = datetime.now() - datetime.fromtimestamp( float(self._settings['TimeLastFull']) )
self.dailyResetDone = datetime.now().day
def _update(self):
#logging.debug("Update CAN %d, Time Now: %d", self._bat.updated, self.lastUpdated)
time = datetime.now()
self.lastUpdated = time.timestamp()
## Monitor communication ##
if (self._bat.updated != -1 and self.lastUpdated == 0) or ((self.lastUpdated - self._bat.updated) < 10):
self._dbusservice['/Connected'] = 1
else:
self._dbusservice['/Connected'] = 0
## Call daily statistics and reset energy counters function
if datetime.now().hour == 6 and datetime.now().minute == 0 and datetime.now().day != self.dailyResetDone :
self._daily_stats()
## Alarms ##
if self._bat.alarm_high_temperature:
self._dbusservice['/Alarms/HighTemperature'] = 2
else:
@ -210,78 +221,73 @@ class DbusBatteryService:
self._dbusservice['/Alarms/HighDischargeCurrent'] = 2
else:
self._dbusservice['/Alarms/HighDischargeCurrent'] = 0
if (self._bat.updated != -1 and self.lastUpdated == 0) or ((self.lastUpdated - self._bat.updated) < 10):
self._dbusservice['/Connected'] = 1
else:
self._dbusservice['/Connected'] = 0
## State of Charge, Batrium couting SoC above 100, stay at 99 till all cells are balanced, then set timestamp and SoC to 100 ##
if self._bat.soc <= 99:
self._dbusservice['/Soc'] = self._bat.soc
# calculate time since last full charge
dt = datetime.now() - datetime.fromtimestamp( float(self._settings['TimeLastFull']) )
self._dbusservice['/History/TimeSinceLastFullCharge'] = (dt.seconds + dt.days * 24 * 3600)
elif (self._bat.soc > 99) and (self.cell_balanced):
self._dbusservice['/Soc'] = 100
self._dbusservice['/ConsumedAmphours'] = 0
# set timestamp last full charge
#if datetime.fromtimestamp(time()).day != datetime.fromtimestamp(float(self._settings['TimeLastFull'])).day:
# self._settings['TimeLastFull'] = time()
self._settings['TimeLastFull'] = time()
## Cells are balancing or not ##
if self._bat.NumberInBypass != 0:
self._dbusservice['/Balancing'] = 1
else:
self._dbusservice['/Balancing'] = 0
self._dbusservice['/InstalledCapacity'] = self._bat.capacity
self._dbusservice['/Capacity'] = self._bat.capacity
## Write Batrium values direct to Venus OS registers ##
self._dbusservice['/Dc/0/Current'] = self._bat.current
self._dbusservice['/Dc/0/Voltage'] = self._bat.voltage
power = self._bat.voltage * self._bat.current
self._dbusservice['/Dc/0/Power'] = power
self._dbusservice['/Dc/0/Temperature'] = self._bat.maxCellTemperature
self._dbusservice['/Info/MaxChargeCurrent'] = self._bat.maxChargeCurrent
self._dbusservice['/Info/MaxDischargeCurrent'] = self._bat.maxDischargeCurrent
self._dbusservice['/System/NrOfModulesOnline'] = self._bat.numberOfModules
self._dbusservice['/System/NrOfBatteriesBalancing'] = self._bat.NumberInBypass
self._dbusservice['/System/BatteriesSeries'] = self._bat.numberOfModules
self._dbusservice['/InstalledCapacity'] = self._bat.capacity
self._dbusservice['/Capacity'] = self._bat.capacity
## Set Charge Voltage Limit according to Batrium, if charging is disabled, Batrium sent 0.0V. Stay at last CVL and save it to Venus OS config xml, to workaround if restarted with charge off ##
if self._bat.maxChargeVoltage != 0:
self._dbusservice['/Info/MaxChargeVoltage'] = self._bat.maxChargeVoltage
self.maxChargeVoltage = self._bat.maxChargeVoltage
# Workaround lower charge voltage a bit, because Batrium sends TCL 0V if charge is disabled
# If all cells are balanced, lower CVL 0.1V to spare cells mons
elif self.maxChargeVoltage != 0:
self._dbusservice['/Info/MaxChargeVoltage'] = self.maxChargeVoltage - 0.1
elif self.maxChargeVoltage == 0 and self._bat.maxChargeVoltage == 0:
self._dbusservice['/Info/MaxChargeVoltage'] = self._settings['TargetChargeVoltage']
self._dbusservice['/System/NrOfModulesOnline'] = self._bat.numberOfModules
self._dbusservice['/System/NrOfBatteriesBalancing'] = self._bat.NumberInBypass
self._dbusservice['/System/BatteriesSeries'] = self._bat.numberOfModules
#update energy statistics daily at 6:00,
if datetime.now().hour == 6 and datetime.now().minute == 0 and datetime.now().day != self.dailyResetDone :
self._daily_stats()
## Calculation every minute ##
now = datetime.now().time()
if now.minute != self.minUpdateDone:
self.minUpdateDone = now.minute
## Display min and max cell temperatures every minute, because of better readability
self._dbusservice['/System/MinCellTemperature'] = self._bat.minCellTemperature
self._dbusservice['/System/MaxCellTemperature'] = self._bat.maxCellTemperature
self._dbusservice['/System/MinTemperatureCellId'] = self._bat.minCellTemperatureId
self._dbusservice['/System/MaxTemperatureCellId'] = self._bat.maxCellTemperatureId
## Same for cell voltages and saving to history
self._dbusservice['/System/MaxCellVoltage'] = self._bat.maxCellVoltage
if (self._bat.maxCellVoltage > self._dbusservice['/History/MaxCellVoltage'] ):
self._dbusservice['/History/MaxCellVoltage'] = self._bat.maxCellVoltage
logging.info("New maximum cell voltage: %f", self._bat.maxCellVoltage)
logging.debug("New maximum cell voltage: %f", self._bat.maxCellVoltage)
self._dbusservice['/System/MinCellVoltage'] = self._bat.minCellVoltage
if (0 < self._bat.minCellVoltage < self._dbusservice['/History/MinCellVoltage'] ):
self._dbusservice['/History/MinCellVoltage'] = self._bat.minCellVoltage
logging.info("New minimum cell voltage: %f", self._bat.minCellVoltage)
logging.debug("New minimum cell voltage: %f", self._bat.minCellVoltage)
self._dbusservice['/System/MaxVoltageCellId'] = self._bat.maxCellVoltageId
self._dbusservice['/System/MinVoltageCellId'] = self._bat.minCellVoltageId
# Time remaining to full and to empty, show None if under 1 amp and under 5 days left for nicer looking readings
if self._bat.current > 1:
#charging
if self._bat.TimeToFull < 432000:
@ -299,16 +305,19 @@ class DbusBatteryService:
else :
self._dbusservice['/TimeToGo'] = None
## Check if all cells are balancing
if self._bat.NumberInBypass == self._bat.numberOfModules:
self.cell_balanced = True
elif self._bat.NumberInBypass == 0:
self.cell_balanced = False
## Call history save function to write history items to Venus OS config xml
self._safe_history()
## Calculation every second ##
if now.second != self.secUpdateDone:
self.secUpdateDone = now.second
#Workaround because of missing messages of Batrium BMS
# Calculated charged and discharged energy and save it to history, because Batrium not sending
if power > 0:
self.ChargedEnergy += (power / 3600.0) / 1000
if power < 0:
@ -319,9 +328,9 @@ class DbusBatteryService:
return True
def main():
parser = ArgumentParser(description='dbus_batrium', add_help=True)
parser.add_argument('-d', '--debug', help='enable debug logging',
action='store_true')
## Cli parser to set interface and for debbuging purposes
parser = ArgumentParser(description='dbus-batrium-native integration', add_help=True)
parser.add_argument('-d', '--debug', help='enable debug logging', action='store_true')
parser.add_argument('-i', '--interface', help='CAN interface')
parser.add_argument('-p', '--print', help='print only')
@ -339,17 +348,12 @@ def main():
logging.info('No CAN interface specified, using default can0')
args.interface = 'can0'
logging.info('Starting dbus_batrium %s on %s ' %
(VERSION, args.interface))
logging.debug('Setup main loop')
logging.info('Starting dbus-batrium-native integration %s on %s ' % (VERSION, args.interface))
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)
logging.debug('Setup dbus service')
battery_output = DbusBatteryService(
servicename='com.victronenergy.battery',
connection = args.interface,