Added batrium native protocol implementation, not working atm

This commit is contained in:
Carsten Schmiemann 2023-05-20 17:44:18 +02:00
parent ebea91d930
commit 1c7eef7089
4 changed files with 438 additions and 0 deletions

View File

@ -0,0 +1,158 @@
#!/usr/bin/env python3
import logging
import itertools
from time import time
from datetime import datetime
import can
import struct
from argparse import ArgumentParser
VERSION = '1.0'
class BatriumBattery(can.Listener):
def __init__(self, capacity, connection):
self.capacity = capacity
self.maxChargeVoltage = 0
self.numberOfModules = 3
self.chargeComplete = 0
self.soc = 0
self.voltage = 0
self.current = 0
self.temperature = 0
self.maxCellTemperature = 0
self.minCellTemperature = 0
self.cellVoltages_min =[(0,0,0,0) for i in range(self.numberOfModules)]
self.cellVoltages_max =[(0,0,0,0) for i in range(self.numberOfModules)]
self.cellTemperatures =[(0,0,0,0) for i in range(self.numberOfModules)]
self.cellBypassTemperatures =[(0,0,0,0) for i in range(self.numberOfModules)]
self.cellBypassPWM =[(0,0,0,0) for i in range(self.numberOfModules)]
self.maxCellVoltage = 0
self.maxCellVoltageId = 0
self.minCellVoltage = 0
self.minCellVoltageId = 0
self.maxChargeCurrent = 0
self.maxDischargeCurrent = 0
self.firmwareVersion = '2.17.39'
self.updated = -1
self.cyclicModeTask = None
self.TimeToEmpty = 0
self.TimeToFull = 0
self._ci = can.interface.Bus(channel=connection, bustype='socketcan')
# check connection and that reported system voltage roughly matches configuration
found = False
msg = None
while True:
try:
msg = self._ci.recv(timeout=10)
except can.CanError:
logging.error("Canbus error")
if msg == None:
#timeout no system connected
logging.error("No messages on canbus %s received. Check connection and speed." % connection)
break;
elif msg.arbitration_id == 0x00111500:
logging.info("Found Batrium BMS on %s" % connection)
self.soc = (msg.data[0] * 0.5) - 5
self.voltage = ((msg.data[3] * 256) + msg.data[2]) * 0.01
self.current = (struct.unpack('>f', bytearray([msg.data[7], msg.data[6], msg.data[5], msg.data[4]])))[0] * 0.001
self.temperature = msg.data[1] - 40
logging.debug("SOC: %2d, I: %2.2fA, U: %2.2fV, T:%2.1fC" % (self.soc, self.current, self.voltage, self.temperature))
found = True
break;
def on_message_received(self, msg):
self.updated = msg.timestamp
logging.debug("CAN message arrived")
if msg.arbitration_id == 0x00111500:
self.soc = (msg.data[0] * 0.5) - 5
self.voltage = ((msg.data[3] * 256) + msg.data[2]) * 0.01
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 == 0x00111800:
self.TimeToEmpty = ((msg.data[2] * 256) + msg.data[1])
elif msg.arbitration_id == 0x00111700:
self.TimeToFull = ((msg.data[2] * 256) + msg.data[1])
elif msg.arbitration_id == 0x00140400:
self.maxDischargeCurrent = ((msg.data[3] * 256) + msg.data[2]) * 0.1
logging.debug("Idmax %dA", self.maxDischargeCurrent)
logging.debug("I: %dA U: %dV",self.current, self.voltage)
elif msg.arbitration_id == 0x00140300:
self.maxChargeVoltage = ((msg.data[7] * 256) + msg.data[6]) * 0.01
self.maxChargeCurrent = ((msg.data[3] * 256) + msg.data[2]) * 0.01
logging.debug("Icmax %dA, Ivmax %dA", self.maxChargeCurrent, self.maxChargeVoltage)
logging.debug("I: %dA U: %dV",self.current, self.voltage)
elif msg.arbitration_id == 0x00161700:
self.minCellTemperature = msg.data[0] - 40
self.maxCellTemperature = msg.data[1] - 40
logging.debug("Min Cell Temp: %d, Max Cell Temp: %d", self.minCellTemperature, self.maxCellTemperature)
elif msg.arbitration_id == 0x00161100:
self.minCellVoltage = ((msg.data[1] * 256) + msg.data[0]) * 0.001
self.maxCellVoltage = ((msg.data[3] * 256) + msg.data[2]) * 0.001
self.minCellVoltageId = msg.data[4]
self.maxCellVoltageId = msg.data[5]
logging.debug("Umin %1.3fV Umax %1.3fV", self.minCellVoltage, self.maxCellVoltage)
elif ((msg.arbitration_id >= 0x001D1101) and (msg.arbitration_id <= 0x001D11F9)):
cell_id = msg.arbitration_id & 0xff
logging.debug("Cell ID: %d", cell_id)
#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]
def prnt(self):
print("SOC: %2d, I: %2.2fA, U: %2.2fV, T:%2.1fC" % (self.soc, self.current, self.voltage, self.maxCellTemperature))
print("Umin: %1.2f, Umax: %1.2f Udelta: %1.2f" % (self.minCellVoltage, self.maxCellVoltage, self.maxCellVoltage-self.minCellVoltage))
print("CCL: %4.1f A CVL: %2.2f V" % (self.maxChargeCurrent, self.maxChargeVoltage))
# === All code below is to simply run it from the commandline for debugging purposes ===
def main():
logging.info('Starting dbus_batrium_native listener')
#logger = can.Logger('logfile.asc')
bat = BatriumBattery(capacity=300, connection='can0')
listeners = [
#logger, # Regular Listener object
bat
]
notifier = can.Notifier(bat._ci, listeners)
for msg in bat._ci:
if msg.arbitration_id == 0x00140100:
bat.prnt()
# Clean-up
notifier.stop()
bat._ci.shutdown()
logging.basicConfig( format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
level=(logging.DEBUG),
handlers=[
logging.FileHandler("%s/current.log" % (os.path.dirname(os.path.realpath(__file__)))),
logging.StreamHandler()
])
if __name__ == "__main__":
main()

View File

@ -0,0 +1,274 @@
#!/usr/bin/env python3
"""
A class to put a battery service on the dbus, according to victron standards, with constantly updating
paths.
"""
from gi.repository import GLib
import platform
import argparse
import logging
import sys
import os
import dbus
import itertools
import math
from time import time
from datetime import datetime
import struct
from argparse import ArgumentParser
from batrium import *
# our own packages
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'
def handle_changed_setting(setting, oldvalue, newvalue):
logging.debug('setting changed, setting: %s, old: %s, new: %s' % (setting, oldvalue, newvalue))
class DbusBatteryService:
def __init__(self, servicename, deviceinstance, capacity, productname='Batrium BMS', connection='can0'):
self.minUpdateDone = 0
self.dailyResetDone = 0
self.lastUpdated = 0
self._bat = BatriumBattery(capacity=capacity, connection=connection)
try:
self._dbusservice = VeDbusService(servicename+'.socketcan_'+connection+'_di'+str(deviceinstance))
except:
exit
# 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', VERSION + ' 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', 0)
self._dbusservice.add_path('/ProductName', productname)
self._dbusservice.add_path('/FirmwareVersion', 'unknown')
self._dbusservice.add_path('/HardwareVersion', 'unknown')
self._dbusservice.add_path('/Connected', 0)
# Create battery specific objects
self._dbusservice.add_path('/Status', 0)
self._dbusservice.add_path('/Soh', 100)
self._dbusservice.add_path('/Capacity', int(capacity))
self._dbusservice.add_path('/InstalledCapacity', int(capacity))
self._dbusservice.add_path('/Dc/0/Temperature', 20)
self._dbusservice.add_path('/Info/MaxChargeCurrent', 5)
self._dbusservice.add_path('/Info/MaxDischargeCurrent', 5)
self._dbusservice.add_path('/Info/MaxChargeVoltage', 0)
self._dbusservice.add_path('/Info/BatteryLowVoltage', 48.0)
self._dbusservice.add_path('/Alarms/CellImbalance', 0)
self._dbusservice.add_path('/Alarms/LowVoltage', 0)
self._dbusservice.add_path('/Alarms/HighVoltage', 0)
self._dbusservice.add_path('/Alarms/HighDischargeCurrent', 0)
self._dbusservice.add_path('/Alarms/HighChargeCurrent', 0)
self._dbusservice.add_path('/Alarms/LowSoc', 0)
self._dbusservice.add_path('/Alarms/LowTemperature', 0)
self._dbusservice.add_path('/Alarms/HighTemperature', 0)
self._dbusservice.add_path('/Balancing', 0)
self._dbusservice.add_path('/System/HasTemperature', 1)
self._dbusservice.add_path('/System/NrOfBatteries', 1)
self._dbusservice.add_path('/System/NrOfModulesOnline', 1)
self._dbusservice.add_path('/System/NrOfModulesOffline', 0)
self._dbusservice.add_path('/System/NrOfModulesBlockingDischarge', 0)
self._dbusservice.add_path('/System/NrOfModulesBlockingCharge', 0)
self._dbusservice.add_path('/System/NrOfBatteriesBalancing', 0)
self._dbusservice.add_path('/System/BatteriesParallel', 1)
self._dbusservice.add_path('/System/BatteriesSeries', 1)
self._dbusservice.add_path('/System/NrOfCellsPerBattery', 4)
self._dbusservice.add_path('/System/MinVoltageCellId', 0)
self._dbusservice.add_path('/System/MaxVoltageCellId', 0)
self._dbusservice.add_path('/System/MinCellTemperature', 10.0)
self._dbusservice.add_path('/System/MaxCellTemperature', 10.0)
self._settings = SettingsDevice(
bus=dbus.SystemBus() if (platform.machine() == 'armv7l') else dbus.SessionBus(),
supportedSettings={
'AvgDischarge': ['/Settings/Batrium/AvgerageDischarge', 0.0,0,0],
'TotalAhDrawn': ['/Settings/Batrium/TotalAhDrawn', 0.0,0,0],
'TimeLastFull': ['/Settings/Batrium/TimeLastFull', 0.0 ,0,0],
'MinCellVoltage': ['/Settings/Batrium/MinCellVoltage', 4.0,2.0,4.2],
'MaxCellVoltage': ['/Settings/Batrium/MaxCellVoltage', 2.0,2.0,4.2],
'interval': ['/Settings/Batrium/Interval', 10, 10, 10]
},
eventCallback=handle_changed_setting)
self._summeditems = {
'/System/MaxCellVoltage': {'gettext': '%.2F V'},
'/System/MinCellVoltage': {'gettext': '%.2F V'},
'/Dc/0/Voltage': {'gettext': '%.2F V'},
'/Dc/0/Current': {'gettext': '%.1F A'},
'/Dc/0/Power': {'gettext': '%.0F W'},
'/Soc': {'gettext': '%.0F %%'},
'/History/TotalAhDrawn': {'gettext': '%.0F Ah'},
'/History/DischargedEnergy': {'gettext': '%.2F kWh'},
'/History/ChargedEnergy': {'gettext': '%.2F kWh'},
'/History/AverageDischarge': {'gettext': '%.2F kWh'},
'/TimeToGo': {'gettext': '%.0F s'},
'/ConsumedAmphours': {'gettext': '%.1F Ah'}
}
for path in self._summeditems.keys():
self._dbusservice.add_path(path, value=None, gettextcallback=self._gettext)
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'])
self._dbusservice['/ConsumedAmphours'] = 0
logging.info("History cell voltage min: %.3f, max: %.3f, totalAhDrawn: %d",
self._settings['MinCellVoltage'], self._settings['MaxCellVoltage'], self._settings['TotalAhDrawn'])
self._dbusservice['/History/ChargedEnergy'] = 0
self._dbusservice['/History/DischargedEnergy'] = 0
GLib.timeout_add( self._settings['interval'], exit_on_error, self._update)
def _gettext(self, path, value):
item = self._summeditems.get(path)
if item is not None:
return item['gettext'] % value
return str(value)
def __del__(self):
self._safe_history()
logging.info('Stopping dbus_Batrium')
def _safe_history(self):
logging.debug('Saving history to localsettings')
self._settings['AvgDischarge'] = self._dbusservice['/History/AverageDischarge']
self._settings['TotalAhDrawn'] = self._dbusservice['/History/TotalAhDrawn']
self._settings['MinCellVoltage'] = self._dbusservice['/History/MinCellVoltage']
self._settings['MaxCellVoltage'] = self._dbusservice['/History/MaxCellVoltage']
def _daily_stats(self):
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'])
self._dbusservice['/History/AverageDischarge'] = (6*self._dbusservice['/History/AverageDischarge'] + self._dbusservice['/History/DischargedEnergy'])/7 #rolling week
self._dbusservice['/History/ChargedEnergy'] = 0
self._dbusservice['/History/DischargedEnergy'] = 0
dt = datetime.now() - datetime.fromtimestamp( float(self._settings['TimeLastFull']) )
#if full within the last 24h and more than *0% consumed, estimate actual capacity and SOH based on consumed amphours from full and SOC reported
if dt.total_seconds() < 24*3600 and self._bat.soc < 70:
self._dbusservice['/Capacity'] = int(-self._dbusservice['/ConsumedAmphours'] * 100 / (100-self._bat.soc))
self._dbusservice['/Soh'] = int(self._dbusservice['/Capacity']*100 / self._dbusservice['/InstalledCapacity'])
logging.info("SOH: %d, Capacity: %d ", self._dbusservice['/Soh'], self._dbusservice['/Capacity'])
self.dailyResetDone = datetime.now().day
def _update(self):
if (self._bat.updated != -1 and self.lastUpdated == 0) or ((self._bat.updated - self.lastUpdated) < 1000):
self.lastUpdated = self._bat.updated
self._dbusservice['/Connected'] = 1
else:
self._dbusservice['/Connected'] = 0
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['/System/MaxVoltageCellId'] = self._bat.maxCellVoltageId
self._dbusservice['/System/MinVoltageCellId'] = self._bat.minCellVoltageId
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)
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)
self._dbusservice['/System/MinCellTemperature'] = self._bat.minCellTemperature
self._dbusservice['/System/MaxCellTemperature'] = self._bat.maxCellTemperature
self._dbusservice['/Info/MaxChargeCurrent'] = self._bat.maxChargeCurrent
self._dbusservice['/Info/MaxDischargeCurrent'] = self._bat.maxDischargeCurrent
self._dbusservice['/Info/MaxChargeVoltage'] = self._bat.maxChargeVoltage
self._dbusservice['/System/NrOfModulesOnline'] = 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()
now = datetime.now().time()
if now.minute != self.minUpdateDone:
self.minUpdateDone = now.minute
if self._bat.current > 0:
#charging
self._dbusservice['/TimeToGo'] = self._bat.TimeToFull
else :
#discharging
self._dbusservice['/TimeToGo'] = self._bat.TimeToEmpty
self._safe_history()
return True
def main():
parser = ArgumentParser(description='dbus_batrium', 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('-c', '--capacity', help='capacity in Ah')
parser.add_argument('-p', '--print', help='print only')
args = parser.parse_args()
logging.basicConfig( format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
level=(logging.DEBUG if args.debug else logging.INFO),
handlers=[
logging.FileHandler("%s/current.log" % (os.path.dirname(os.path.realpath(__file__)))),
logging.StreamHandler()
])
if not args.interface:
logging.info('No CAN interface specified, using default can0')
args.interface = 'can0'
if not args.capacity:
logging.warning('Battery capacity not specified, using default (100Ah)')
args.capacity = 100
logging.info('Starting dbus_batrium %s on %s ' %
(VERSION, args.interface))
logging.debug('Setup main loop')
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,
deviceinstance = 0,
capacity = int(args.capacity),
)
logging.debug('Connected to dbus, and switching over to GLib.MainLoop() (= event based)')
mainloop = GLib.MainLoop()
mainloop.run()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,3 @@
opkg update
opkg install python3-pip
pip3 install python-can

View File

@ -0,0 +1,3 @@
#!/bin/sh
exec 2>&1
exec softlimit -d 100000000 -s 1000000 -a 100000000 python /data/dbus_batrium_native/dbus_batrium_native.py -i can0 -c 300