#!/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()