├── .gitignore ├── AtoDs.pdf ├── BMSbarePCBback.jpg ├── BMSbarePCBfront.jpg ├── BMScabling.jpg ├── BMSdecoded.pdf ├── BMSdecoded.xlsx ├── BMSsingle.jpg ├── BMSsingle.pdf ├── BMStoBattery.jpg ├── BalanceResistorCalculations.xlsx ├── BatteryMonitor.jpg ├── BatteryMonitor.pdf ├── LICENSE ├── README.md ├── ResistorDivider.jpg ├── ResistorDivider.pdf ├── SummaryGenerator.py ├── ads1115.py ├── alarms.py ├── batday.jpg ├── batteries.py ├── battery12cellBMS.cfg ├── battery15cellBMSPIP.cfg ├── battery16cell.cfg ├── battery16cellBMS.cfg ├── battery1cell.cfg ├── battery1cellPIP.cfg ├── battery4cell.cfg ├── battery8cell.cfg ├── battery8cellADS1115.cfg ├── batterybuggy.cfg ├── batterygeoff.cfg ├── batterykarrak.cfg ├── batterymike.cfg ├── beagle.py ├── bms.py ├── bmscore.py ├── bmstest.py ├── calibrate.py ├── caligain.py ├── calvcourse.py ├── calvfine.py ├── calvfineall.py ├── config.py ├── demandmanager.py ├── doc └── .gitkeep ├── filter.pdf ├── getatod.py ├── getbms.py ├── getdata.py ├── html ├── SampleBatday.html ├── SampleBattery.html ├── bargraph.plot ├── batday.php ├── battery.php ├── excesspwr.php └── strip.sh ├── logger.py ├── mqtt.py ├── pi.py ├── pip.py ├── piptest.py ├── pylon.py ├── pylontest.py ├── summary.py └── system ├── batman.service ├── piptest.sh └── startpython.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor Backup files and others 2 | *~ 3 | *old 4 | #battery.cfg 5 | testing 6 | summary 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | bin/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.mo 46 | 47 | # Mr Developer 48 | .mr.developer.cfg 49 | .project 50 | .pydevproject 51 | 52 | # Rope 53 | .ropeproject 54 | 55 | # Django stuff: 56 | *.log 57 | *.pot 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | -------------------------------------------------------------------------------- /AtoDs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/AtoDs.pdf -------------------------------------------------------------------------------- /BMSbarePCBback.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/BMSbarePCBback.jpg -------------------------------------------------------------------------------- /BMSbarePCBfront.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/BMSbarePCBfront.jpg -------------------------------------------------------------------------------- /BMScabling.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/BMScabling.jpg -------------------------------------------------------------------------------- /BMSdecoded.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/BMSdecoded.pdf -------------------------------------------------------------------------------- /BMSdecoded.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/BMSdecoded.xlsx -------------------------------------------------------------------------------- /BMSsingle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/BMSsingle.jpg -------------------------------------------------------------------------------- /BMSsingle.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/BMSsingle.pdf -------------------------------------------------------------------------------- /BMStoBattery.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/BMStoBattery.jpg -------------------------------------------------------------------------------- /BalanceResistorCalculations.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/BalanceResistorCalculations.xlsx -------------------------------------------------------------------------------- /BatteryMonitor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/BatteryMonitor.jpg -------------------------------------------------------------------------------- /BatteryMonitor.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/BatteryMonitor.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BatteryMonitor 2 | ============== 3 | 4 | This project provides the information and software to make a low cost ($50-150) and flexible device to monitor and log individual battery cell voltages and other battery data either using a cheap Chinese Bluetooth BMS board or a Beaglebone Black Linux computer. The monitoring software can also set and reset I/O pins on the Beaglebone in response to cell voltages going out of programmed limits. Programming of the I/O is done via the config file. I have also written some webpages to display batery information on any web browser via the internet. For those that don't need or want all the IO and additional memory that the Beaglebone Black offers I see no reason why this project could not be run on the various models of the Raspberry Pi including maybe even the Raspberry Pi Zero which could cut the cost to below $50, or any Linux computer which has an accessable built in I2C bus or if using the Bluetooth BMS board a USB port. It would even be possible to run it on your router running OpenWRT or something similar. 5 | 6 | The primary use for this would be to monitor large Lithium based battery banks of up to 48 volts but it would also be useful for other technologies like Lead Acid batteries. In my case I am using it to monitor and log data from our 24 volt 360ah LiFePO4 battery bank used in our off grid power system. 7 | 8 | 9 | The main documentation for this project can be found in the wiki here https://github.com/simat/BatteryMonitor/wiki 10 | 11 | My thanks to Adafruit for the ADS1X15 a/d drivers from here, https://github.com/adafruit/Adafruit_ADS1X15 12 | 13 | I have made this an open source project with the hope that it maybe useful to others and if found useful that others will get involved and broaden the scope of the project. Please feel free to make suggestions, report bugs or provide positive or negative feedback. I am new to using the Beaglebone, Git and Python so suggestions on how to use features of the Beaglebone, Git and Python that I might not be aware of could also be useful. 14 | -------------------------------------------------------------------------------- /ResistorDivider.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/ResistorDivider.jpg -------------------------------------------------------------------------------- /ResistorDivider.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/ResistorDivider.pdf -------------------------------------------------------------------------------- /SummaryGenerator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor Summary Generator***** 3 | # Copyright (C) 2020 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | """this program generates the summary file from the config data in battery.cfg""" 18 | 19 | from config import config 20 | from os import path,remove 21 | 22 | zerovolts=[0.0 for i in range(config['battery']['numcells']+1)] 23 | fivevolts=[5.0 for i in range(config['battery']['numcells']+1)] 24 | fivevolts[-1]=5.0*config['battery']['numcells'] 25 | zeroamps=[0.0 for i in range(len(config['CurrentInputs']))] 26 | inverterstate=['Offline' for i in range(config['Inverters']['numinverters'])] 27 | 28 | section={} 29 | section['timestamp']=20140101000000 30 | section['maxvoltages'],section['maxnocharge']=zerovolts,zerovolts 31 | section['minnoload'],section['minvoltages']=fivevolts,fivevolts 32 | section['deltav']=[5.0,0.0,0.0] 33 | section['ioutmax'],section['kwoutmax'],section['kwhout']=zeroamps,zeroamps,zeroamps 34 | section['iinmax'],section['kwinmax'],section['kwhin']=zeroamps,zeroamps,zeroamps 35 | section['ah']=[10000.0, 0.0, -1000.0, 0.0, 0.0, 0.0, 0.0] 36 | section['dod']=[10000.0, 0.0, -1000.0, 0] 37 | section['power']=[0.0, 0.0, 0.0, 0] 38 | section['tmax']=[-60.0 for i in range(len(config['TemperatureInputs']))] 39 | section['tmin']=[120.0 for i in range(len(config['TemperatureInputs']))] 40 | section['baltime']=[ 0.0 for i in range(config['battery']['numcells'])] 41 | 42 | order='timestamp',\ 43 | 'maxvoltages',\ 44 | 'maxnocharge',\ 45 | 'minnoload',\ 46 | 'minvoltages',\ 47 | 'deltav',\ 48 | 'ioutmax',\ 49 | 'kwoutmax',\ 50 | 'kwhout',\ 51 | 'iinmax',\ 52 | 'kwinmax',\ 53 | 'kwhin',\ 54 | 'ah',\ 55 | 'dod',\ 56 | 'power',\ 57 | 'tmax',\ 58 | 'tmin',\ 59 | 'baltime' 60 | 61 | endcurrent="""state = {} 62 | batpwr1hrav = [0] 63 | excesssolar = [0] 64 | minmaxdemandpwr = [0, {}] 65 | """.format(inverterstate,config['DemandManager']['maxdemandpwr']) 66 | 67 | template=['[current]','[hour]','[currentday]','[prevday]','[monthtodate]', 68 | '[yeartodate]','[alltime]'] 69 | 70 | def writesummaryfile(): 71 | """ writes summary file to disk""" 72 | with open (config['files']['summaryfile'],'x') as summaryfile: 73 | for i in (template): 74 | summaryfile.write('{}\n'.format(i)) 75 | for items in range(len(order)): 76 | summaryfile.write('{}={}\n'.format(order[items],section[order[items]])) 77 | if i=='[current]': 78 | summaryfile.write(endcurrent) 79 | summaryfile.write('\n') 80 | 81 | 82 | try: 83 | with open (config['files']['summaryfile'],'x') as summaryfile: 84 | writesummaryfile() 85 | except FileExistsError: 86 | x=input('Summary file exists, do you want to overwrite it?[y]') 87 | if not x or x=='y' or x=='Y': 88 | remove(config['files']['summaryfile']) 89 | writesummaryfile() 90 | else: 91 | exit() 92 | -------------------------------------------------------------------------------- /ads1115.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor Getdata from battery cells getdata.py***** 3 | # Copyright (C) 2017 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | from time import sleep 18 | from config import config 19 | #import smbus 20 | #from Adafruit_I2C import Adafruit_I2C 21 | import Adafruit_ADS1x15 as AtoD 22 | for i in config['AtoDs']: 23 | exec(i + '=' + config['AtoDs'][i]) 24 | 25 | import logger 26 | log = logger.logging.getLogger(__name__) 27 | log.setLevel(logger.logging.DEBUG) 28 | log.addHandler(logger.errfile) 29 | 30 | 31 | class Rawdat: 32 | # compile analog capture code to save CPU time 33 | vin = [] 34 | for i in sorted(config['VChannels']): 35 | vin = vin + [compile(config['VChannels'][i], '', 'eval')] 36 | # vin = vin + [config['VChannels'][i]] 37 | #for i in config['IChannels']: 38 | # config['IChannels'][i] = compile(config['IChannels'][i], '', 'eval') 39 | iin = [] 40 | for i in sorted(config['IChannels']): 41 | iin = iin + [compile(config['IChannels'][i], '', 'eval')] 42 | 43 | rawi = [0.0 for i in iin] 44 | rawv = [ 0.0 for i in range(len(vin)+1)] 45 | 46 | def getdata(self): 47 | """ Get data for A/Ds, calibrate, covert and place in list variables""" 48 | for i in range(5): 49 | try: 50 | for i in range(len(self.vin)): 51 | self.rawv[i+1] = eval(self.vin[i])/1000 # A to D 1 to 4 in volts 52 | for i in range(len(self.iin)): 53 | self.rawi[i] = eval(self.iin[i]) 54 | break 55 | except Exception as err: 56 | log.error(err) 57 | sleep(0.5) 58 | if i==4: 59 | raise 60 | -------------------------------------------------------------------------------- /alarms.py: -------------------------------------------------------------------------------- 1 | # *****BatteryMonitor main file batteries.py***** 2 | # Copyright (C) 2014 Simon Richard Matthews 3 | # Project loaction https://github.com/simat/BatteryMonitor 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # any later version. 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # You should have received a copy of the GNU General Public License along 13 | # with this program; if not, write to the Free Software Foundation, Inc., 14 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 15 | 16 | 17 | #!/usr/bin/python 18 | import sys 19 | from time import localtime 20 | import serial 21 | import binascii 22 | from config import config 23 | 24 | #self.summary=summary 25 | 26 | import logger 27 | log = logger.logging.getLogger(__name__) 28 | log.setLevel(logger.logging.INFO) 29 | log.addHandler(logger.alarmfile) 30 | 31 | 32 | class Alarms: 33 | # Initialise and compile alarms 34 | def __init__(self,batdata,summary): 35 | self.summary=summary 36 | self.alarmtriggered={} 37 | for i in config['alarms']: 38 | self.alarmtriggered[i]=False 39 | exec(config['alarms'][i][0]) 40 | log.info('alarm {} initialised'.format(i)) 41 | # config['alarms'][i][1] = compile(config['alarms'][i][1], '', 'exec') 42 | # config['alarms'][i][2] = compile(config['alarms'][i][2], '', 'exec') 43 | # config['alarms'][i][3] = compile(config['alarms'][i][3], '', 'exec') 44 | # config['alarms'][i][4] = compile(config['alarms'][i][4], '', 'exec') 45 | 46 | def scanalarms(self,batdata): 47 | # self.timedate = localtime() 48 | # print (self.timedate.tm_hour) 49 | for i in config['alarms']: 50 | if not self.alarmtriggered[i]: 51 | exec(config['alarms'][i][1]) 52 | # log.debug('{}{}{}'.format(self.test,batdata.maxcellv,batdata.lastmaxcellv)) 53 | if self.test: 54 | # sys.stderr.write('Alarm 1 triggered') 55 | log.info('alarm {} triggered'.format(i)) 56 | self.alarmtriggered[i]=True 57 | exec(config['alarms'][i][2]) 58 | 59 | if self.alarmtriggered[i]: 60 | exec(config['alarms'][i][3]) 61 | if self.test: 62 | log.info('alarm {} reset'.format(i)) 63 | self.alarmtriggered[i]=False 64 | exec(config['alarms'][i][4]) 65 | -------------------------------------------------------------------------------- /batday.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/batday.jpg -------------------------------------------------------------------------------- /batteries.py: -------------------------------------------------------------------------------- 1 | # *****BatteryMonitor main file batteries.py***** 2 | # Copyright (C) 2014 Simon Richard Matthews 3 | # Project loaction https://github.com/simat/BatteryMonitor 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # any later version. 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # You should have received a copy of the GNU General Public License along 13 | # with this program; if not, write to the Free Software Foundation, Inc., 14 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 15 | 16 | 17 | #!/usr/bin/python 18 | import sys 19 | #import smbus 20 | #import Adafruit_ADS1x15 21 | import time 22 | from shutil import copy as filecopy 23 | from config import loadconfig, config, editbatconfig 24 | from configparser import SafeConfigParser 25 | numcells = config['battery']['numcells'] 26 | import logger 27 | log = logger.logging.getLogger(__name__) 28 | log.setLevel(logger.logging.DEBUG) 29 | log.addHandler(logger.errfile) 30 | from demandmanager import solaravailable, initdemand 31 | from getdata import Readings 32 | from alarms import Alarms 33 | import mqtt 34 | """if "telsa" in config: 35 | teslaenabled=config['tesla']['enable'] 36 | if teslaenabled: 37 | import tesla""" 38 | maxtries=3 # number of error retries befor abort 39 | #exec(config['files']['alarms']) # import alarm code 40 | #import Adafruit_BBIO.GPIO as GPIO 41 | 42 | def initmain(soc): 43 | """ initialise main loop, soc is SOC to start battery DOD to""" 44 | summary = logsummary.summary 45 | printtime = int(time.strftime("%Y%m%d%H%M%S ", time.localtime())) 46 | # print (int(summary['current']['timestamp']),printtime) 47 | while printtime < int(summary['current']['timestamp']): 48 | print(printtime,summary['current']['timestamp']) 49 | print ("Error: Current time before last sample time") 50 | time.sleep(30) 51 | printtime = int(time.strftime("%Y%m%d%H%M%S", time.localtime())) 52 | 53 | filecopy(config['files']['summaryfile'],config['files']['summaryfile']+"R" + str(int(printtime))) 54 | if soc > config['battery']['capacity']: 55 | print ("Battery DOD must be less than Battery Capacity") 56 | 57 | else: 58 | if soc < 0: 59 | batdata.soc = summary['current']['ah'][0] 60 | batdata.socadj = summary['current']['dod'][0] 61 | else: 62 | batdata.soc = soc 63 | batdata.socadj = soc 64 | summary['current']['dod'][3] = 0 65 | summary['current']['dod'][3] = -100 # flag don't adjust leakage current 66 | initdemand(1-batdata.socadj/config['battery']['capacity']) 67 | prevtime = logsummary.currenttime 68 | # logsummary.startday(summary) 69 | # logsummary.starthour(summary) 70 | 71 | 72 | 73 | def mainloop(): 74 | """ Main loop, gets battery data, gets summary.py to do logging""" 75 | 76 | # global prevbatvoltage 77 | summary = logsummary.summary 78 | for i in range(config['sampling']['samplesav']): 79 | # printvoltage = '' 80 | # for i in range(numcells+1): 81 | # printvoltage = printvoltage + str(round(batdata.batvolts[i],3)).ljust(5,'0') + ' ' 82 | # print (printvoltage) 83 | batdata.getraw() 84 | 85 | # if batdata.batvoltsav[numcells] >= 55.2 and prevbatvoltage < 55.2: # reset SOC counter? 86 | # print batdata.socadj/(float(summary['current']['dod'][3])*24.0) 87 | 88 | if batdata.batvoltsav[numcells] > config['battery']['vreset'] \ 89 | and -batdata.currentav[-3] < config['battery']['ireset']: # reset SOC counter? 90 | 91 | if summary['current']['dod'][3] <= 0 : 92 | socerr=0 93 | else: 94 | socerr=batdata.socadj/(float(summary['current']['dod'][3])*24.0) 95 | socerr=max(socerr,-0.01) 96 | socerr=min(socerr,0.01) 97 | config['battery']['ahloss']=config['battery']['ahloss']-socerr/2 98 | editbatconfig('battery','ahloss',str(config['battery']['ahloss'])) 99 | batdata.soc = config['battery']['socreset'] 100 | batdata.socadj = batdata.soc 101 | summary['current']['dod'][3] = 0 102 | else: 103 | batdata.soc = batdata.soc + batdata.batah 104 | batdata.socadj = batdata.socadj +batdata.batahadj 105 | 106 | batdata.prevbatvoltage = batdata.batvoltsav[numcells] 107 | batdata.ah = batdata.ah + batdata.batah 108 | batdata.inahtot = batdata.inahtot + batdata.inah 109 | batdata.pwrbattot = batdata.pwrbattot + batdata.pwrbat 110 | batdata.pwrintot = batdata.pwrintot + batdata.pwrin 111 | # check alarms 112 | alarms.scanalarms(batdata) 113 | # update summaries 114 | batdata.pwravailable,batdata.minmaxdemandpwr=solaravailable(batdata) 115 | # if teslaenabled: 116 | # tesla.updatecharge(batdata) 117 | logsummary.update(summary, batdata) 118 | currenttime=str(logsummary.currenttime) 119 | prevtime=str(logsummary.prevtime) 120 | if currenttime[10:12] != prevtime[10:12]: # new minute 121 | loadconfig() 122 | logsummary.updatesection(summary, 'hour', 'current') 123 | logsummary.updatesection(summary, 'alltime','current') 124 | logsummary.updatesection(summary, 'currentday','current') 125 | logsummary.updatesection(summary, 'monthtodate', 'current') 126 | logsummary.updatesection(summary, 'yeartodate', 'current') 127 | logsummary.writesummary() 128 | for item in config['mqtt']['payload']: 129 | config['mqtt']['payload'][item]=eval(config['mqtt']['payload'][item]) 130 | mqtt.publish(config['mqtt']['topic'],f"{config['mqtt']['payload']}".replace("'",'"')) 131 | # print(f'{config["mqtt"]["payload"]}') 132 | 133 | batdata.ah = 0.0 134 | batdata.ahadj = 0.0 135 | batdata.inahtot = 0.0 136 | batdata.pwrbattot = 0.0 137 | batdata.pwrintot = 0.0 138 | for i in range(batdata.numiins): 139 | batdata.kWhin[i] = 0.0 140 | batdata.kWhout[i] = 0.0 141 | for i in range(numcells): 142 | batdata.baltime[i]=0 143 | 144 | if currenttime[8:10] != prevtime[8:10]: # new hour 145 | logsummary.starthour(summary) 146 | 147 | if currenttime[6:8] != prevtime[6:8]: # newday 148 | logsummary.startday(summary) 149 | 150 | if currenttime[4:6] != prevtime[4:6]: # new month 151 | logsummary.startmonth(summary) 152 | 153 | if currenttime[0:4] != prevtime[0:4]: # new year 154 | logsummary.startyear(summary) 155 | 156 | import summary 157 | logsummary=summary.Summary() 158 | batdata = Readings() # initialise batdata 159 | alarms = Alarms(batdata,summary) # initialise alarms 160 | def deamon(soc=-1): 161 | """Battery Management deamon to run in background""" 162 | 163 | numtries=0 164 | while True: 165 | try: 166 | initmain(soc) 167 | while True: 168 | mainloop() 169 | numtries=0 170 | except KeyboardInterrupt: 171 | numtries=maxtries 172 | sys.stdout.write('\n') 173 | logsummary.close() 174 | raise 175 | # sys.exit(9) 176 | except Exception as err: 177 | log.critical(err) 178 | numtries+=1 179 | soc=-1 180 | if numtries==maxtries: 181 | logsummary.close() 182 | raise 183 | 184 | 185 | if __name__ == "__main__": 186 | print (sys.argv) 187 | if len(sys.argv) > 1: 188 | deamon(float(sys.argv[1])) 189 | else: 190 | deamon() 191 | -------------------------------------------------------------------------------- /battery12cellBMS.cfg: -------------------------------------------------------------------------------- 1 | # *****BATTERY LOGGER CONFIG FILE***** 2 | # Copyright (C) 2018 Simon Richard Matthews 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # any later version. 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # You should have received a copy of the GNU General Public License along 12 | # with this program; if not, write to the Free Software Foundation, Inc., 13 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 14 | 15 | [files] 16 | # logfile is location and name of file used for logging all data 17 | logfile = 'data/log' 18 | # errfile is location and name of error log 19 | errfile = 'data/errorlog' 20 | # summaryfile is location and name of a summary of the data 21 | summaryfile = 'data/summary' 22 | # hoursummaryfile is the location of the hourly summary file 23 | hoursummaryfile = '/dev/null' 24 | # daysummaryfile is the location and name of daily summary file 25 | daysummaryfile = '/dev/null' 26 | # monthsummaryfile is the location and name of monthly summary file 27 | monthsummaryfile = '/dev/null' 28 | # yearsummaryfile is the location and name of yearly summary file 29 | yearsummaryfile = '/dev/null' 30 | 31 | [logging] 32 | data = "self.printtime+batdata.vbat+batdata.vdelta \ 33 | +batdata.iall+batdata.soctxt+batdata.socadjtxt+str(batdata.pip.rawdat)" 34 | 35 | [battery] 36 | name = 'My Bike Battery' 37 | numcells = 12 38 | capacity = 20 39 | ahloss = 0.1 40 | vreset = 48.0 # voltage to reset DOD counter 41 | ireset = 0.5 # current to reset DOD counter 42 | ilowcurrent = 1.0 # C/20 lowcurrent voltage logging current 43 | 44 | [VoltageInputs] 45 | v01 = 'self.bms.rawdat["V01"]' 46 | v02 = 'self.bms.rawdat["V02"]' 47 | v03 = 'self.bms.rawdat["V03"]' 48 | v04 = 'self.bms.rawdat["V04"]' 49 | v05 = 'self.bms.rawdat["V05"]' 50 | v06 = 'self.bms.rawdat["V06"]' 51 | v07 = 'self.bms.rawdat["V07"]' 52 | v08 = 'self.bms.rawdat["V08"]' 53 | v09 = 'self.bms.rawdat["V09"]' 54 | v10 = 'self.bms.rawdat["V10"]' 55 | v11 = 'self.bms.rawdat["V11"]' 56 | v12 = 'self.bms.rawdat["V12"]' 57 | vbat = 'self.bms.rawdat["V12"]' 58 | 59 | 60 | 61 | [CurrentInputs] 62 | iin1 = 'self.bms.rawdat["Ibat"]' 63 | 64 | [TemperatureInputs] 65 | tin1 = 'self.bms.rawdat["T1"]' 66 | tin2 = 'self.bms.rawdat["T2"]' 67 | 68 | [BalanceFlags] 69 | balf01 = 'self.bms.rawdat["Bal"] & 1' 70 | balf02 = 'self.bms.rawdat["Bal"] >> 1 & 1' 71 | balf03 = 'self.bms.rawdat["Bal"] >> 2 & 1' 72 | balf04 = 'self.bms.rawdat["Bal"] >> 3 & 1' 73 | balf05 = 'self.bms.rawdat["Bal"] >> 4 & 1' 74 | balf06 = 'self.bms.rawdat["Bal"] >> 5 & 1' 75 | balf07 = 'self.bms.rawdat["Bal"] >> 6 & 1' 76 | balf08 = 'self.bms.rawdat["Bal"] >> 7 & 1' 77 | balf09 = 'self.bms.rawdat["Bal"] >> 8 & 1' 78 | balf10 = 'self.bms.rawdat["Bal"] >> 9 & 1' 79 | balf11 = 'self.bms.rawdat["Bal"] >> 10 & 1' 80 | balf12 = 'self.bms.rawdat["Bal"] >> 11 & 1' 81 | balf13 = 'self.bms.rawdat["Bal"] >> 12 & 1' 82 | balf14 = 'self.bms.rawdat["Bal"] >> 13 & 1' 83 | balf15 = 'self.bms.rawdat["Bal"] >> 14 & 1' 84 | 85 | [Status] 86 | chargestate = "self.pip.rawdat['ChgStat']" 87 | 88 | [sampling] 89 | # sampletime is time between samples in seconds 90 | sampletime = 1.002 91 | # samplesav is number of samples in running average 92 | samplesav = 10 93 | 94 | [calibrate] 95 | currentgain = [-0.01, 1.0, 1.0] 96 | currentoffset = [-0.0, 0.0, 0.0] 97 | batvgain = 1 98 | measured = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 36.0] 99 | displayed = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 36.0] 100 | measureddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 101 | displayeddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 102 | 103 | delta = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 104 | 105 | [alarms] 106 | # each alarm has five attributes, the first is the alarm initialisation code 107 | # if the second attribute test is true the third attribute will be executed 108 | # if the fourth attribute is true the fith attribute will be executed 109 | #alarm = ['GPIO.setup("P8_10", GPIO.OUT)', 'test=minvolts<2.8 or maxvolts>3.60', 'GPIO.output("P8_10", GPIO.HIGH)', \ 110 | # 'test=minvolts>2.9 and maxvolts<3.59', 'GPIO.output("P8_10", GPIO.LOW)'] 111 | -------------------------------------------------------------------------------- /battery15cellBMSPIP.cfg: -------------------------------------------------------------------------------- 1 | [files] 2 | logfile = '/home/pi/data/log' 3 | # errfile is location and name of error log 4 | errfile = 'data/errorlog' 5 | summaryfile = '/home/pi/data/summary' 6 | errfile = '/home/pi/data/baterrlog' 7 | alarmfile = '/home/pi/data/alarmlog' 8 | hoursummaryfile = '/home/pi/data/hoursummary' 9 | daysummaryfile = '/home/pi/data/daysummary' 10 | monthsummaryfile = '/home/pi/data/monthsummary' 11 | yearsummaryfile = '/home/pi/data/yearsummary' 12 | 13 | [logging] 14 | data = "self.printtime+batdata.vbat+batdata.vdelta \ 15 | +batdata.iall+batdata.soctxt+batdata.socadjtxt+str(batdata.pip.rawdat)" 16 | 17 | [battery] 18 | name = 'Mikes' 19 | numcells = 15 20 | capacity = 220 21 | overvoltage = 3.600 22 | undervoltage = 2.900 23 | ahloss = 0.10500000000000001 24 | vreset = 51.5 # voltage to reset DOD counter 25 | ireset = 5.0 # current to reset DOD counter 26 | ilowcurrent = 10.0 # C/20 lowcurrent voltage logging current 27 | 28 | [Interfaces] 29 | pip = 'pip(92931710100542)' # number in brackets is SN of PIP) 30 | bms = 'bms(LH-SP15S001-P15S-30A)' # string in brackets is harware name, from read command 05 31 | pi = 'pi' # add raspberry pi 32 | 33 | [VoltageInputs] 34 | v01 = 'self.bms.rawdat["V01"]' 35 | v02 = 'self.bms.rawdat["V02"]' 36 | v03 = 'self.bms.rawdat["V03"]' 37 | v04 = 'self.bms.rawdat["V04"]' 38 | v05 = 'self.bms.rawdat["V05"]' 39 | v06 = 'self.bms.rawdat["V06"]' 40 | v07 = 'self.bms.rawdat["V07"]' 41 | v08 = 'self.bms.rawdat["V08"]' 42 | v09 = 'self.bms.rawdat["V09"]' 43 | v10 = 'self.bms.rawdat["V10"]' 44 | v11 = 'self.bms.rawdat["V11"]' 45 | v12 = 'self.bms.rawdat["V12"]' 46 | v13 = 'self.bms.rawdat["V13"]' 47 | v14 = 'self.bms.rawdat["V14"]' 48 | v15 = 'self.bms.rawdat["V15"]' 49 | vbat = 'self.bms.rawdat["V15"]' 50 | 51 | 52 | [CurrentInputs] 53 | iin1 = 'self.bms.rawdat["Ibat"]' 54 | iin2 = 'self.pip.rawdat["PVI"]' 55 | iin3 = 'self.current[0]-self.current[1]' 56 | 57 | [TemperatureInputs] 58 | tin1='self.bms.rawdat["T1"]' 59 | tin2='self.bms.rawdat["T2"]' 60 | 61 | [BalanceFlags] 62 | balf01 = 'self.bms.rawdat["Bal"] & 1' 63 | balf02 = 'self.bms.rawdat["Bal"] >> 1 & 1' 64 | balf03 = 'self.bms.rawdat["Bal"] >> 2 & 1' 65 | balf04 = 'self.bms.rawdat["Bal"] >> 3 & 1' 66 | balf05 = 'self.bms.rawdat["Bal"] >> 4 & 1' 67 | balf06 = 'self.bms.rawdat["Bal"] >> 5 & 1' 68 | balf07 = 'self.bms.rawdat["Bal"] >> 6 & 1' 69 | balf08 = 'self.bms.rawdat["Bal"] >> 7 & 1' 70 | balf09 = 'self.bms.rawdat["Bal"] >> 8 & 1' 71 | balf10 = 'self.bms.rawdat["Bal"] >> 9 & 1' 72 | balf11 = 'self.bms.rawdat["Bal"] >> 10 & 1' 73 | balf12 = 'self.bms.rawdat["Bal"] >> 11 & 1' 74 | balf13 = 'self.bms.rawdat["Bal"] >> 12 & 1' 75 | balf14 = 'self.bms.rawdat["Bal"] >> 13 & 1' 76 | balf15 = 'self.bms.rawdat["Bal"] >> 14 & 1' 77 | 78 | [Status] 79 | chargestate = "self.pip.rawdat['ChgStat']" 80 | 81 | [sampling] 82 | sampletime = 10 83 | samplesav = 1 84 | 85 | [calibrate] 86 | currentgain = [-0.01, -1.2, 1.0] 87 | currentoffset = [-0.2, -0.3, 0.0] 88 | batvgain = 1 89 | measured = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 45.0] 90 | displayed = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 45.0] 91 | measureddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 92 | displayeddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 93 | delta = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 94 | 95 | [alarms] 96 | # each alarm has five attributes, the first is the alarm initialisation code 97 | # if the second attribute test is true and the alarm has not been triggered 98 | # the third attribute will be executed 99 | # if the fourth attribute is true and the alarm has been triggered 100 | # the fith attribute will be executed 101 | 102 | overv = ['batdata.pip.setparamnoerror("PCVV51.8")', \ 103 | 'self.test=batdata.maxcellv>3.60', \ 104 | 'batdata.pip.setparamnoerror("PCVV50.2")', \ 105 | 'self.test=batdata.maxcellv<3.40', \ 106 | 'batdata.pip.setparamnoerror("PCVV51.8")'] 107 | underv = ['batdata.pip.setparamnoerror("PSDV45.0")', \ 108 | 'self.test=batdata.mincellv<2.90', \ 109 | 'batdata.pip.setparamnoerror("PSDV48.0")', \ 110 | 'self.test=batdata.mincellv>3.20', \ 111 | 'batdata.pip.setparamnoerror("PSDV45.0")'] 112 | #swapinverter = ['batdata.pi.gpio.setup(7, batdata.pi.gpio.OUT, initial = 1) \nbatdata.pi.gpio.setup(11, batdata.pi.gpio.OUT, initial = 0)', \ 113 | # 'self.test=localtime().tm_hour==18', \ 114 | # 'batdata.pi.swapinverter(on=7,off=11)', \ 115 | # 'self.test=localtime().tm_hour==6', \ 116 | # 'batdata.pi.swapinverter(on=11,off=7)'] 117 | 118 | # float, gets around premature float bug with PIP inverters 119 | #float = ['batdata.pip.setparamnoerror("PBFT55.2")', \ 120 | # 'self.test=batdata.batvoltsav[16]>55.1 and batdata.currentav[0]>-3.6', \ 121 | # 'batdata.pip.setparamnoerror("PBFT53.6")', \ 122 | # 'self.test=localtime().tm_hour==0', \ 123 | # 'batdata.pip.setparamnoerror("PBFT55.2")'] 124 | 125 | # makes charge voltage high enough to reset SOC counter on Monday 126 | #resetSOC = ['batdata.pip.setparamnoerror("PCVV55.3")', \ 127 | # 'self.test=localtime().tm_wday==0', \ 128 | # 'batdata.pip.setparamnoerror("PCVV55.2")', \ 129 | # 'self.test=summary["current"]["dod"][-1]==0', \ 130 | # 'batdata.pip.setparamnoerror("PCVV55.3")'] 131 | -------------------------------------------------------------------------------- /battery16cell.cfg: -------------------------------------------------------------------------------- 1 | # *****BATTERY LOGGER CONFIG FILE***** 2 | # Copyright (C) 2014 Simon Richard Matthews 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # any later version. 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # You should have received a copy of the GNU General Public License along 12 | # with this program; if not, write to the Free Software Foundation, Inc., 13 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 14 | 15 | [files] 16 | interface = 'from getatod import Raw' 17 | alarms = 'from alarmsnull import Alarms' 18 | # logfile is location and name of file used for logging all data 19 | logfile = '/dev/null' 20 | # summaryfile is location and name of a summary of the data 21 | # errfile is location and name of error log 22 | errfile = 'data/errorlog' 23 | summaryfile = 'summary' 24 | # hoursummaryfile is the location of the hourly summary file 25 | hoursummaryfile = '/dev/null' 26 | # daysummaryfile is the location and name of daily summary file 27 | daysummaryfile = '/dev/null' 28 | # monthsummaryfile is the location and name of monthly summary file 29 | monthsummaryfile = '/dev/null' 30 | # yearsummaryfile is the location and name of yearly summary file 31 | yearsummaryfile = '/dev/null' 32 | 33 | [battery] 34 | name = 'My' 35 | numcells = 16 36 | capacity = 180 37 | ahloss = 0.1 38 | vreset = 55.2 # voltage to reset DOD counter 39 | ireset = 9.0 # current to reset DOD counter 40 | ilowcurrent = 7.2 # C/50 lowcurrent voltage logging current 41 | 42 | [AtoDs] 43 | atod0 = 'AtoD.ADS1115(address=0x48)' 44 | atod1 = 'AtoD.ADS1115(address=0x49)' 45 | atod2 = 'AtoD.ADS1115(address=0x4A)' 46 | atod3 = 'AtoD.ADS1115(address=0x4B)' 47 | atod4 = 'AtoD.ADS1115(address=0x48, busnum=2)' 48 | 49 | [VoltageInputs] 50 | v01 = 'atod0.read_adc(0, gain=2)*0.06250197' 51 | v02 = 'atod0.read_adc(1, gain=2)*0.06250197' 52 | v03 = 'atod0.read_adc(2, gain=2)*0.06250197' 53 | v04 = 'atod0.read_adc(3, gain=2)*0.06250197' 54 | v05 = 'atod1.read_adc(0, gain=2)*0.06250197' 55 | v06 = 'atod1.read_adc(1, gain=2)*0.06250197' 56 | v07 = 'atod1.read_adc(2, gain=2)*0.06250197' 57 | v08 = 'atod1.read_adc(3, gain=2)*0.06250197' 58 | v09 = 'atod2.read_adc(0, gain=2)*0.06250197' 59 | v10 = 'atod2.read_adc(1, gain=2)*0.06250197' 60 | v11 = 'atod2.read_adc(2, gain=2)*0.06250197' 61 | v12 = 'atod2.read_adc(3, gain=2)*0.06250197' 62 | v13 = 'atod3.read_adc(0, gain=2)*0.06250197' 63 | v14 = 'atod3.read_adc(1, gain=2)*0.06250197' 64 | v15 = 'atod3.read_adc(2, gain=2)*0.06250197' 65 | v16 = 'atod3.read_adc(3, gain=2)*0.06250197' 66 | 67 | [CurrentInputs] 68 | iin1 = 'atod4.read_adc_difference(0, gain=16)' 69 | iin2 = 'atod4.read_adc_difference(3, gain=16)' 70 | iin3 = 'self.current[0] - self.current[1]' 71 | 72 | [sampling] 73 | # sampletime is time between samples in seconds 74 | sampletime = 1.002 75 | # samplesav is number of samples in running average 76 | samplesav = 10 77 | 78 | [calibrate] 79 | ibatoffset = 0.0 # 0 offset current in amps 80 | ibatgain = 2.666667 # 200/75 gain to multiply AtoD output in mV to A 81 | iinoffset = 0.0 # 0 offset current in amps 82 | iingain = 2.666667 # 200/75 gain to multiply AtoD output in mV to A 83 | pcurrent = .063 # parasitic current in A not measured (battery monitor power) 84 | 85 | batvgain = 27.66667 # resistor divider (2.49kohm+33.2x2kohm)/2.49kohm 86 | 87 | delta = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 88 | 89 | measured = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 48.0, 48.0] 90 | displayed = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 48.0, 48.0] 91 | 92 | measureddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 93 | displayeddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 94 | 95 | [alarms] 96 | # each alarm has five attributes, the first is the alarm initialisation code 97 | # if the second attribute test is true the third attribute will be executed 98 | # if the fourth attribute is true the fith attribute will be executed 99 | alarm = ['GPIO.setup("P8_10", GPIO.OUT)', 'test=minvolts<2.8 or maxvolts>3.60', 'GPIO.output("P8_10", GPIO.HIGH)', \ 100 | 'test=minvolts>2.9 and maxvolts<3.59', 'GPIO.output("P8_10", GPIO.LOW)'] 101 | -------------------------------------------------------------------------------- /battery16cellBMS.cfg: -------------------------------------------------------------------------------- 1 | [files] 2 | logfile = '/batdat/log' 3 | summaryfile = '/batdat/summary' 4 | errfile = '/batdat/baterrlog' 5 | alarmfile = '/home/pi/data/alarmlog' 6 | hoursummaryfile = '/batdat/hoursummary' 7 | daysummaryfile = '/batdat/daysummary' 8 | monthsummaryfile = '/batdat/monthsummary' 9 | yearsummaryfile = '/batdat/yearsummary' 10 | 11 | [logging] 12 | data = "self.printtime+batdata.vbat+batdata.vdelta \ 13 | +batdata.iall+batdata.soctxt+batdata.socadjtxt+str(batdata.pip.rawdat)" 14 | 15 | [battery] 16 | name = 'My Battery' 17 | numcells = 16 18 | capacity = 180 19 | overvoltage = 3.600 20 | undervoltage = 2.900 21 | ahloss = -0.005 22 | vreset = 51.5 # voltage to reset DOD counter 23 | ireset = 5.0 # current to reset DOD counter 24 | ilowcurrent = 9.0 # C/50 lowcurrent voltage logging current 25 | 26 | [Interfaces] 27 | pip = 'pip(92931710100542)' # number in brackets is SN of PIP) 28 | bms = 'bms(LH-SP15S001-P15S-30A)' # string in brackets is harware name, from read command 05 29 | 30 | [Ports] 31 | pipport = '/dev/ttyU*' # where to look for pipport 32 | bmsport = '/dev/ttyO*' # where to look for bmsport 33 | 34 | [VoltageInputs] 35 | v01 = 'self.bms.rawdat["V01"]' 36 | v02 = 'self.bms.rawdat["V02"]' 37 | v03 = 'self.bms.rawdat["V03"]' 38 | v04 = 'self.bms.rawdat["V04"]' 39 | v05 = 'self.bms.rawdat["V05"]' 40 | v06 = 'self.bms.rawdat["V06"]' 41 | v07 = 'self.bms.rawdat["V07"]' 42 | v08 = 'self.bms.rawdat["V08"]' 43 | v09 = 'self.bms.rawdat["V09"]' 44 | v10 = 'self.bms.rawdat["V10"]' 45 | v11 = 'self.bms.rawdat["V11"]' 46 | v12 = 'self.bms.rawdat["V12"]' 47 | v13 = 'self.bms.rawdat["V13"]' 48 | v14 = 'self.bms.rawdat["V14"]' 49 | v15 = 'self.bms.rawdat["V15"]' 50 | v16 = 'self.bms.rawdat["V16"]' 51 | vbat = 'self.bms.rawdat["V16"]' 52 | 53 | [CurrentInputs] 54 | iin1 = 'self.bms.rawdat["Ibat"]' 55 | iin2 = 'self.pip.rawdat["PVI"]' 56 | iin3 = 'self.current[0]-self.current[1]' 57 | 58 | [TemperatureInputs] 59 | tin1='self.bms.rawdat["T1"]' 60 | tin2='self.bms.rawdat["T2"]' 61 | 62 | [BalanceFlags] 63 | balf01 = 'self.bms.rawdat["Bal"] & 1' 64 | balf02 = 'self.bms.rawdat["Bal"] >> 1 & 1' 65 | balf03 = 'self.bms.rawdat["Bal"] >> 2 & 1' 66 | balf04 = 'self.bms.rawdat["Bal"] >> 3 & 1' 67 | balf05 = 'self.bms.rawdat["Bal"] >> 4 & 1' 68 | balf06 = 'self.bms.rawdat["Bal"] >> 5 & 1' 69 | balf07 = 'self.bms.rawdat["Bal"] >> 6 & 1' 70 | balf08 = 'self.bms.rawdat["Bal"] >> 7 & 1' 71 | balf09 = 'self.bms.rawdat["Bal"] >> 8 & 1' 72 | balf10 = 'self.bms.rawdat["Bal"] >> 9 & 1' 73 | balf11 = 'self.bms.rawdat["Bal"] >> 10 & 1' 74 | balf12 = 'self.bms.rawdat["Bal"] >> 11 & 1' 75 | balf13 = 'self.bms.rawdat["Bal"] >> 12 & 1' 76 | balf14 = 'self.bms.rawdat["Bal"] >> 13 & 1' 77 | balf15 = 'self.bms.rawdat["Bal"] >> 14 & 1' 78 | balf16 = 'self.bms.rawdat["Bal"] >> 15 & 1' 79 | 80 | [Status] 81 | chargestate = "self.pip.rawdat['ChgStat']" 82 | 83 | [sampling] 84 | sampletime = 10 85 | samplesav = 1 86 | 87 | [calibrate] 88 | currentgain = [-0.01, -1.2, 1.0] 89 | currentoffset = [-0.2, -0.3, 0.0] 90 | batvgain = 1 91 | measured = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 45.0] 92 | displayed = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 45.0] 93 | measureddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 94 | displayeddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 95 | delta = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 96 | 97 | [alarms] 98 | # each alarm has five attributes, the first is the alarm initialisation code 99 | # if the second attribute test is true and the alarm has not been triggered 100 | # the third attribute will be executed 101 | # if the fourth attribute is true and the alarm has been triggered 102 | # the fith attribute will be executed 103 | 104 | overv = ['batdata.pip.setparamnoerror("PCVV55.2")', \ 105 | 'self.test=batdata.maxcellv>3.60', \ 106 | 'batdata.pip.setparamnoerror("PCVV53.6")', \ 107 | 'self.test=batdata.maxcellv<3.40', \ 108 | 'batdata.pip.setparamnoerror("PCVV55.2")'] 109 | underv = ['batdata.pip.setparamnoerror("PSDV48.0")', \ 110 | 'self.test=batdata.mincellv<2.90', \ 111 | 'batdata.pip.setparamnoerror("PSDV51.2")', \ 112 | 'self.test=batdata.mincellv>3.20', \ 113 | 'batdata.pip.setparamnoerror("PSDV48.0")'] 114 | #swapinverter = ['batdata.pi.gpio.setup(7, batdata.pi.gpio.OUT, initial = 1) \nbatdata.pi.gpio.setup(11, batdata.pi.gpio.OUT, initial = 0)', \ 115 | # 'self.test=localtime().tm_hour==18', \ 116 | # 'batdata.pi.swapinverter(on=7,off=11)', \ 117 | # 'self.test=localtime().tm_hour==6', \ 118 | # 'batdata.pi.swapinverter(on=11,off=7)'] 119 | 120 | 121 | #float = ['batdata.pip.setparamnoerror("PBFT55.2")', \ 122 | # 'self.test=batdata.batvoltsav[16]>55.1 and batdata.currentav[0]>-3.6', \ 123 | # 'batdata.pip.setparamnoerror("PBFT53.6")', \ 124 | # 'self.test=localtime().tm_hour==0', \ 125 | # 'batdata.pip.setparamnoerror("PBFT55.2")'] 126 | 127 | # makes charge voltage high enough to reset SOC counter on Monday 128 | #resetSOC = ['batdata.pip.setparamnoerror("PCVV55.3")', \ 129 | # 'self.test=localtime().tm_wday==0', \ 130 | # 'batdata.pip.setparamnoerror("PCVV55.2")', \ 131 | # 'self.test=summary["current"]["dod"][-1]==0', \ 132 | # 'batdata.pip.setparamnoerror("PCVV55.3")'] 133 | -------------------------------------------------------------------------------- /battery1cell.cfg: -------------------------------------------------------------------------------- 1 | [files] 2 | interface = 'from getatod import Raw' 3 | alarms = 'from alarmsnull import Alarms' 4 | logfile = 'log' 5 | # errfile is location and name of error log 6 | errfile = 'data/errorlog' 7 | summaryfile = 'summary' 8 | hoursummaryfile = '/dev/null' 9 | daysummaryfile = '/dev/null' 10 | monthsummaryfile = '/dev/null' 11 | yearsummaryfile = '/dev/null' 12 | 13 | [battery] 14 | name = 'My' 15 | numcells = 1 16 | capacity = 360 17 | ahloss = 3.0 18 | vreset = 27.6 # voltage to reset DOD counter 19 | ireset = 9.0 # current to reset DOD counter 20 | ilowcurrent = 7.2 # C/50 lowcurrent voltage logging current 21 | 22 | [AtoDs] 23 | atod0 = 'AtoD.ADS1115(address=0x48)' 24 | 25 | [VoltageInputs] 26 | v1 = 'atod0.read_adc(0, gain=2)*0.06250197' 27 | vbat = 'v1' 28 | 29 | 30 | [CurrentInputs] 31 | iin1 = 'atod0.read_adc_difference(2, gain=16)' 32 | iin2 = 'atod0.read_adc_difference(3, gain=16)' 33 | iin3 = 'self.current[0] - self.current[1]' 34 | 35 | [sampling] 36 | sampletime = 1.002 37 | samplesav = 10 38 | 39 | [calibrate] 40 | pcurrent = 0 # parasitic current in A not measured (battery monitor power) 41 | batvgain = 1 # resistor divider (2.49kohm+33.2kohm)/2.49kohm 42 | 43 | # count of 32767=256mV, so 1mV=128 counts 44 | # so gain=current@shuntvoltage/128*shuntvoltage 45 | currentgain = [0.010417, 0.005208, 0.0010417] 46 | currentoffset = [-0.0, 0.0, 0.0] 47 | 48 | delta = [0.0, 0.0, 0.0, 0.0] 49 | measured = [0.0, 3.0, 6.0, 9.0, 12.0] 50 | displayed = [0.0, 3.0, 6.0, 9.0, 12.0] 51 | 52 | measureddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000] 53 | displayeddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000] 54 | 55 | [alarms] 56 | # each alarm has five attributes, the first is the alarm initialisation code 57 | # if the second attribute test is true the third attribute will be executed 58 | # if the fourth attribute is true the fith attribute will be executed 59 | 60 | # hvc = ['GPIO.setup("P8_10", GPIO.OUT)', 3.590, 'GPIO.output("P8_10", GPIO.HIGH)', 3.600, 'GPIO.output("P8_10", GPIO.LOW)'] 61 | -------------------------------------------------------------------------------- /battery1cellPIP.cfg: -------------------------------------------------------------------------------- 1 | [files] 2 | pipport = '/dev/ttyUSB1' 3 | bmsport = '/dev/ttyUSB0' 4 | alarms = 'from pip import Alarms' 5 | logfile = '/home/pi/data/log' 6 | summaryfile = '/home/pi/data/summary' 7 | errfile = '/home/pi/data/baterrlog' 8 | hoursummaryfile = '/home/pi/data/hoursummary' 9 | daysummaryfile = '/home/pi/data/daysummary' 10 | monthsummaryfile = '/home/pi/data/monthsummary' 11 | yearsummaryfile = '/home/pi/data/yearsummary' 12 | 13 | [logging] 14 | data="self.printtime+batdata.vbat+batdata.vdelta \ 15 | +batdata.iall+batdata.soctxt+batdata.socadjtxt+str(batdata.pip.rawdat)" 16 | 17 | [battery] 18 | name = 'Pip4048' 19 | numcells = 1 20 | capacity = 220 21 | overvoltage = 54.00 22 | undervoltage = 45.00 23 | ahloss = 0.10500000000000001 24 | vreset = 51.5 # voltage to reset DOD counter 25 | ireset = 5.0 # current to reset DOD counter 26 | ilowcurrent = 10.0 # C/20 lowcurrent voltage logging current 27 | 28 | [Interfaces] 29 | pip = 'pip' 30 | 31 | [VoltageInputs] 32 | v1 = 'pip.rawdat["BV"]' 33 | vbat= 'pip.rawdat["BV"]' 34 | 35 | [CurrentInputs] 36 | iin1 = 'pip.rawdat["ibat"]' 37 | iin2 = 'pip.rawdat["ipv"]' 38 | iin3 = 'pip.rawdat["iload"]' 39 | 40 | [sampling] 41 | sampletime = 10 42 | samplesav = 1 43 | 44 | [calibrate] 45 | currentgain = [1.0, 1.0, 1.0] 46 | currentoffset = [-0.0, 0.0, 0.0] 47 | batvgain = 1 48 | measured = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0] 49 | displayed = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0] 50 | measureddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 51 | displayeddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 52 | delta = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 53 | 54 | [alarms] 55 | # each alarm has five attributes, the first is the alarm initialisation code 56 | # if the second attribute test is true the third attribute will be executed 57 | # if the fourth attribute is true the fith attribute will be executed 58 | -------------------------------------------------------------------------------- /battery4cell.cfg: -------------------------------------------------------------------------------- 1 | # *****BATTERY LOGGER CONFIG FILE***** 2 | # Copyright (C) 2014 Simon Richard Matthews 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # any later version. 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # You should have received a copy of the GNU General Public License along 12 | # with this program; if not, write to the Free Software Foundation, Inc., 13 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 14 | 15 | [files] 16 | interface = 'from getatod import Raw' 17 | alarms = 'from alarmsnull import Alarms' 18 | # logfile is location and name of file used for logging all data 19 | logfile = '/dev/null' 20 | # errfile is location and name of error log 21 | errfile = 'data/errorlog' 22 | # summaryfile is location and name of a summary of the data 23 | summaryfile = 'summary' 24 | # hoursummaryfile is the location of the hourly summary file 25 | hoursummaryfile = '/dev/null' 26 | # daysummaryfile is the location and name of daily summary file 27 | daysummaryfile = '/dev/null' 28 | # monthsummaryfile is the location and name of monthly summary file 29 | monthsummaryfile = '/dev/null' 30 | # yearsummaryfile is the location and name of yearly summary file 31 | yearsummaryfile = '/dev/null' 32 | 33 | [battery] 34 | name = 'My' 35 | numcells = 4 36 | capacity = 360 37 | # coulomb losses in battery expressed as offset current 38 | # subtracted from battery current to get adjusted ah count 39 | ahloss = 0.1 40 | vreset = 13.8 # voltage to reset DOD counter 41 | ireset = 9.0 # current to reset DOD counter 42 | ilowcurrent = 7.2 # C/50 lowcurrent voltage logging current 43 | 44 | 45 | [AtoDs] 46 | atod0 = 'AtoD.ADS1115(address=0x48)' 47 | atod1 = 'AtoD.ADS1115(address=0x49)' 48 | 49 | [VoltageInputs] 50 | v1 = 'atod0.read_adc(0, gain=2)*0.06250197' 51 | v2 = 'atod0.read_adc(1, gain=2)*0.06250197' 52 | v3 = 'atod0.read_adc(2, gain=2)*0.06250197' 53 | v4 = 'atod0.read_adc(3, gain=2)*0.06250197' 54 | 55 | [CurrentInputs] 56 | iin1 = 'atod1.read_adc_difference(0, gain=16)' 57 | iin2 = 'atod1.read_adc_difference(3, gain=16)' 58 | iin3 = 'self.current[0] - self.current[1]' 59 | 60 | [sampling] 61 | # sampletime is time between samples in seconds 62 | sampletime = 1.002 63 | # samplesav is number of samples in running average 64 | samplesav = 10 65 | 66 | [calibrate] 67 | batvgain = 14.33333 # resistor divider gain (2.49+33.2)/2.49 68 | ibatoffset = 0.0 # 0 offset current in amps 69 | ibatgain = 2.666667 # 200/75 gain to multiply AtoD output in mV to A 70 | iinoffset = 0.0 # 0 offset current in amps 71 | iingain = 2.666667 # 200/75 gain to multiply AtoD output in mV to A 72 | pcurrent = 0 # parasitic current in A not measured (battery monitor power) 73 | 74 | delta = [0.0, 0.0, 0.0, 0.0] 75 | measured = [0.0, 3.0, 6.0, 9.0, 12.0] 76 | displayed = [0.0, 3.0, 6.0, 9.0, 12.0] 77 | 78 | measureddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000] 79 | displayeddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000] 80 | 81 | [alarms] 82 | # each alarm has five attributes, the first is the alarm initialisation code 83 | # if the second attribute test is true the third attribute will be executed 84 | # if the fourth attribute is true the fith attribute will be executed 85 | alarm = ['GPIO.setup("P8_10", GPIO.OUT)', 'test=minvolts<2.8 or maxvolts>3.60', 'GPIO.output("P8_10", GPIO.HIGH)', \ 86 | 'test=minvolts>2.9 and maxvolts<3.59', 'GPIO.output("P8_10", GPIO.LOW)'] 87 | -------------------------------------------------------------------------------- /battery8cell.cfg: -------------------------------------------------------------------------------- 1 | # *****BATTERY LOGGER CONFIG FILE***** 2 | # Copyright (C) 2014 Simon Richard Matthews 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # any later version. 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # You should have received a copy of the GNU General Public License along 12 | # with this program; if not, write to the Free Software Foundation, Inc., 13 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 14 | 15 | [files] 16 | interface = 'from getatod import Raw' 17 | alarms = 'from alarmsnull import Alarms' 18 | # logfile is location and name of file used for logging all data 19 | logfile = '/batdat/log' 20 | # errfile is location and name of error log 21 | errfile = 'data/errorlog' 22 | # summaryfile is location and name of a summary of the data 23 | summaryfile = '/batdat/summary' 24 | # hoursummaryfile is the location of the hourly summary file 25 | hoursummaryfile = '/dev/null' 26 | # daysummaryfile is the location and name of daily summary file 27 | daysummaryfile = '/batdat/daysummary' 28 | # monthsummaryfile is the location and name of monthly summary file 29 | monthsummaryfile = '/batdat/monthsummary' 30 | # yearsummaryfile is the location and name of yearly summary file 31 | yearsummaryfile = '/batdat/yearsummary' 32 | 33 | 34 | [battery] 35 | name = 'My' 36 | numcells = 8 37 | capacity = 360 38 | ahloss = 0.1 39 | vreset = 27.6 # voltage to reset DOD counter 40 | ireset = 9.0 # current to reset DOD counter 41 | ilowcurrent = 7.2 # C/50 lowcurrent voltage logging current 42 | 43 | 44 | [AtoDs] 45 | atod0 = 'AtoD.ADS1115(address=0x48)' 46 | atod1 = 'AtoD.ADS1115(address=0x49)' 47 | atod2 = 'AtoD.ADS1115(address=0x4A)' 48 | 49 | [VoltageInputs] 50 | v1 = 'atod0.read_adc(0, gain=2)*0.06250197' 51 | v2 = 'atod0.read_adc(1, gain=2)*0.06250197' 52 | v3 = 'atod0.read_adc(2, gain=2)*0.06250197' 53 | v4 = 'atod0.read_adc(3, gain=2)*0.06250197' 54 | v5 = 'atod1.read_adc(0, gain=2)*0.06250197' 55 | v6 = 'atod1.read_adc(1, gain=2)*0.06250197' 56 | v7 = 'atod1.read_adc(2, gain=2)*0.06250197' 57 | v8 = 'atod1.read_adc(3, gain=2)*0.06250197' 58 | 59 | [CurrentInputs] 60 | iin1 = 'atod2.read_adc_difference(0, gain=16)' 61 | iin2 = 'atod2.read_adc_difference(3, gain=16)' 62 | iin3 = 'self.current[0] - self.current[1]' 63 | 64 | [sampling] 65 | # sampletime is time between samples in seconds 66 | sampletime = 1.002 67 | # samplesav is number of samples in running average 68 | samplesav = 10 69 | 70 | [calibrate] 71 | ibatoffset = 0.0 # 0 offset current in amps 72 | ibatgain = 3.33333 # 250/75 gain to multiply AtoD output in mV to A 73 | iinoffset = 0.0 # 0 offset current in amps 74 | iingain = 1.33333 # 100/75 gain to multiply AtoD output in mV to A 75 | pcurrent = .063 # parasitic current in A not measured (battery monitor power) 76 | 77 | batvgain = 14.33333 # resistor divider (2.49kohm+33.2kohm)/2.49kohm 78 | 79 | delta = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 80 | 81 | measured = [0.0, 3.320, 6.638, 9.958, 13.278, 16.599, 19.92, 23.242, 26.564, 26.0] 82 | displayed = [0.000, 3.283, 6.597, 9.872, 13.158, 16.497, 19.755, 23.138, 26.415, 26.0] 83 | 84 | measureddelta = [ 0.0, 3.320, 3.318, 3.320, 3.320, 3.321, 3.321, 3.322, 3.322, 3.000] 85 | displayeddelta = [ 0.0, 3.321, 3.317, 3.322, 3.312, 3.324, 3.316, 3.324, 3.324, 3.000] 86 | 87 | [alarms] 88 | # each alarm has five attributes, the first is the alarm initialisation code 89 | # if the second attribute test is true the third attribute will be executed 90 | # if the fourth attribute is true the fith attribute will be executed 91 | alarm = ['GPIO.setup("P8_10", GPIO.OUT)', 'test=minvolts<2.8 or maxvolts>3.60', 'GPIO.output("P8_10", GPIO.HIGH)', \ 92 | 'test=minvolts>2.9 and maxvolts<3.59', 'GPIO.output("P8_10", GPIO.LOW)'] 93 | -------------------------------------------------------------------------------- /battery8cellADS1115.cfg: -------------------------------------------------------------------------------- 1 | [files] 2 | logfile = '/batdat/log' 3 | summaryfile = '/batdat/summary' 4 | errfile = '/batdat/baterrlog' 5 | alarms = 'from alarmsnull import Alarms' 6 | hoursummaryfile = '/batdat/hoursummary' 7 | daysummaryfile = '/batdat/daysummary' 8 | monthsummaryfile = '/batdat/monthsummary' 9 | yearsummaryfile = '/batdat/yearsummary' 10 | 11 | [logging] 12 | data="self.printtime+batdata.vcells+batdata.vbat+batdata.vdelta \ 13 | +batdata.iall+batdata.soctxt+batdata.socadjtxt" 14 | 15 | 16 | [battery] 17 | name = 'Karrak' 18 | numcells = 8 19 | capacity = 330 20 | ahloss = -0.005 21 | vreset = 27.55 # voltage to reset DOD counter 22 | ireset = 9.0 # current to reset DOD counter 23 | ilowcurrent = 7.2 # C/50 lowcurrent voltage logging current 24 | 25 | [Interfaces] 26 | ads1115= 'ads1115' 27 | 28 | [AtoDs] 29 | atod0 = 'AtoD.ADS1115(address=0x48)' 30 | atod1 = 'AtoD.ADS1115(address=0x49)' 31 | atod2 = 'AtoD.ADS1115(address=0x4A)' 32 | 33 | [VChannels] 34 | v1 = 'atod0.read_adc(0, gain=2)*0.06250197' 35 | v2 = 'atod0.read_adc(1, gain=2)*0.06250197' 36 | v3 = 'atod0.read_adc(2, gain=2)*0.06250197' 37 | v4 = 'atod0.read_adc(3, gain=2)*0.06250197' 38 | v5 = 'atod1.read_adc(0, gain=2)*0.06250197' 39 | v6 = 'atod1.read_adc(1, gain=2)*0.06250197' 40 | v7 = 'atod1.read_adc(2, gain=2)*0.06250197' 41 | v8 = 'atod1.read_adc(3, gain=2)*0.06250197' 42 | 43 | [IChannels] 44 | iin1 = 'atod2.read_adc_difference(0, gain=16)' 45 | iin2 = 'atod2.read_adc_difference(3, gain=16)' 46 | 47 | [VoltageInputs] 48 | v01 = 'self.ads1115.rawv[1]' 49 | v02 = 'self.ads1115.rawv[2]' 50 | v03 = 'self.ads1115.rawv[3]' 51 | v04 = 'self.ads1115.rawv[4]' 52 | v05 = 'self.ads1115.rawv[5]' 53 | v06 = 'self.ads1115.rawv[6]' 54 | v07 = 'self.ads1115.rawv[7]' 55 | v08 = 'self.ads1115.rawv[8]' 56 | 57 | [CurrentInputs] 58 | iin1 = 'self.ads1115.rawi[0]' 59 | iin2 = 'self.ads1115.rawi[1]' 60 | iin3 = 'self.current[0]+self.current[1]' 61 | 62 | [sampling] 63 | sampletime = 1.002 64 | samplesav = 10 65 | 66 | [calibrate] 67 | currentgain = [0.02604, 0.01094, 1.0] 68 | currentoffset = [-0.063, 0.0, 0.0] 69 | batvgain = 14.33333 # resistor divider (2.49kohm+33.2kohm)/2.49kohm 70 | measured = [0.0, 3.326, 6.655, 9.985, 13.316, 16.642, 19.972, 23.303, 26.633] 71 | displayed = [0.0, 3.326, 6.655, 9.985, 13.316, 16.642, 19.972, 23.303, 26.633] 72 | #displayed = [0.0, 3.282, 6.587, 9.834, 13.094, 16.44, 19.644, 23.102, 26.328] 73 | measureddelta = [ 0.0, 3.333, 3.33, 3.331, 3.33, 3.331, 3.331, 3.332, 3.331] 74 | displayeddelta = [ 0.0, 3.333, 3.33, 3.331, 3.33, 3.331, 3.331, 3.332, 3.331] 75 | delta = [0.008, -0.002, 0.002, 0.0, 0.0, 0.001, 0.005, 0.004] 76 | 77 | [alarms] 78 | -------------------------------------------------------------------------------- /batterybuggy.cfg: -------------------------------------------------------------------------------- 1 | [files] 2 | logfile = '/media/sdcard/log' 3 | summaryfile = '/media/sdcard/summary' 4 | errfile = '/media/sdcard/baterrlog' 5 | alarmfile = '/media/sdcard/alarmlog' 6 | hoursummaryfile = '/media/sdcard/hoursummary' 7 | daysummaryfile = '/media/sdcard/daysummary' 8 | monthsummaryfile = '/media/sdcard/monthsummary' 9 | yearsummaryfile = '/media/sdcard/yearsummary' 10 | 11 | [logging] 12 | data = "self.printtime+batdata.vbat+batdata.vdelta \ 13 | +batdata.iall+batdata.soctxt+batdata.socadjtxt+str(batdata.pip.rawdat)" 14 | 15 | [battery] 16 | name = 'Buggy' 17 | numcells = 15 18 | capacity = 80 19 | maxchargerate = 50 20 | targetsoc = 0.70 # target SOC for demand management 21 | minsocdif = 0.03 # difference between target SOC and demand shutdown SOC 22 | overvoltage = 3.650 23 | undervoltage = 2.900 24 | float = 50.2 # battery float Voltage 25 | bulk = 52.5 # battery bulk/CV voltage 26 | ahloss = 0.027090130542655324 27 | vreset = 51.8 # voltage to reset DOD counter 28 | ireset = 4.0 # current to reset DOD counter 29 | ilowcurrent = 4.0 # C/20 lowcurrent voltage logging current 30 | 31 | [Interfaces] 32 | bms = 'bms(KarrakBMS)' # string in brackets is harware name, from read command 05 33 | beagle ='beagle' 34 | pwmcontroller='pwmcontroller' 35 | [Ports] 36 | bmsport = '/dev/ttyS1' # where to look for bmsport 37 | 38 | [VoltageInputs] 39 | v01 = 'self.bms.rawdat["V01"]' 40 | v02 = 'self.bms.rawdat["V02"]' 41 | v03 = 'self.bms.rawdat["V03"]' 42 | v04 = 'self.bms.rawdat["V04"]' 43 | v05 = 'self.bms.rawdat["V05"]' 44 | v06 = 'self.bms.rawdat["V06"]' 45 | v07 = 'self.bms.rawdat["V07"]' 46 | v08 = 'self.bms.rawdat["V08"]' 47 | v09 = 'self.bms.rawdat["V09"]' 48 | v10 = 'self.bms.rawdat["V10"]' 49 | v11 = 'self.bms.rawdat["V11"]' 50 | v12 = 'self.bms.rawdat["V12"]' 51 | v13 = 'self.bms.rawdat["V13"]' 52 | v14 = 'self.bms.rawdat["V14"]' 53 | v15 = 'self.bms.rawdat["V15"]' 54 | vbat = 'self.bms.rawdat["V15"]' 55 | vcharger = 'self.beagle.rawdat["ADCP9_39"]*(680+47000)/680' 56 | 57 | [CurrentInputs] 58 | iin1 = 'self.pip.rawdat["PVI"]' 59 | iin2 = 'self.pip2.rawdat["PVI"]' 60 | iin3 = 'self.bms.rawdat["Ibat"]' 61 | iin4 = 'self.current[0]+self.current[1]' 62 | iin5 = 'self.current[2]-self.current[3]' 63 | 64 | [TemperatureInputs] 65 | tin1 = 'self.bms.rawdat["T1"]' 66 | tin2 = 'self.bms.rawdat["T2"]' 67 | 68 | [BalanceFlags] 69 | balf01 = 'self.bms.rawdat["Bal"] & 1' 70 | balf02 = 'self.bms.rawdat["Bal"] >> 1 & 1' 71 | balf03 = 'self.bms.rawdat["Bal"] >> 2 & 1' 72 | balf04 = 'self.bms.rawdat["Bal"] >> 3 & 1' 73 | balf05 = 'self.bms.rawdat["Bal"] >> 4 & 1' 74 | balf06 = 'self.bms.rawdat["Bal"] >> 5 & 1' 75 | balf07 = 'self.bms.rawdat["Bal"] >> 6 & 1' 76 | balf08 = 'self.bms.rawdat["Bal"] >> 7 & 1' 77 | balf09 = 'self.bms.rawdat["Bal"] >> 8 & 1' 78 | balf10 = 'self.bms.rawdat["Bal"] >> 9 & 1' 79 | balf11 = 'self.bms.rawdat["Bal"] >> 10 & 1' 80 | balf12 = 'self.bms.rawdat["Bal"] >> 11 & 1' 81 | balf13 = 'self.bms.rawdat["Bal"] >> 12 & 1' 82 | balf14 = 'self.bms.rawdat["Bal"] >> 13 & 1' 83 | balf15 = 'self.bms.rawdat["Bal"] >> 14 & 1' 84 | 85 | [Status] 86 | chargestate1 = "self.pip.rawdat['ChgStat']" 87 | chargestate2 = "self.pip2.rawdat['ChgStat']" 88 | 89 | [sampling] 90 | sampletime = 10 91 | samplesav = 1 92 | 93 | [calibrate] 94 | currentgain = [-1.0, -1.0, -0.02, 1.0, 1.0] 95 | currentoffset = [-0.0, -0.0, 0.0, 0.0, 0.0] 96 | batvgain = 1 97 | measured = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 48.0, 48.0] 98 | displayed = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 48.0, 48.0] 99 | measureddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 100 | displayeddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 101 | delta = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 102 | 103 | [alarms] 104 | overv = ['batdata.pip.setparamnoerror("PCVV" +str(config["battery"]["bulk"]))', \ 105 | 'self.test=batdata.maxcellv>3.65', \ 106 | 'batdata.pip.setparamnoerror("PCVV" +str(config["battery"]["float"]))', \ 107 | 'self.test=batdata.maxcellv<3.40', \ 108 | 'batdata.pip.setparamnoerror("PCVV" +str(config["battery"]["bulk"]))'] 109 | underv = ['batdata.pip.setparamnoerror("PSDV45.0")', \ 110 | 'self.test=batdata.mincellv<2.90', \ 111 | 'batdata.pip.setparamnoerror("MNCHGC0497")', \ 112 | 'self.test=batdata.mincellv>3.20', \ 113 | 'batdata.pip.setparamnoerror("MNCHGC0498")'] 114 | swapinverter = ['pass', \ 115 | 'self.test=localtime().tm_hour==8', \ 116 | 'batdata.pip.swapinverter()', \ 117 | 'self.test=localtime().tm_hour==9', \ 118 | 'pass'] 119 | acoverload = ['pass', \ 120 | 'self.test=batdata.pip.acloadav+batdata.pip2.acloadav>config["Inverters"]["turnonslave"]', \ 121 | 'batdata.pip.slaveinvon()', \ 122 | 'self.test=batdata.pip.timeoverload==0.0', \ 123 | 'batdata.pip.slaveinvoff()'] 124 | -------------------------------------------------------------------------------- /batterygeoff.cfg: -------------------------------------------------------------------------------- 1 | [files] 2 | logfile = 'data/log' 3 | summaryfile = '/home/debian/BatteryMonitor/data/summary' 4 | errfile = 'data/baterrlog' 5 | alarmfile = 'data/alarmlog' 6 | hoursummaryfile = 'data/hoursummary' 7 | daysummaryfile = 'data/daysummary' 8 | monthsummaryfile = 'data/monthsummary' 9 | yearsummaryfile = 'data/yearsummary' 10 | 11 | [logging] 12 | data = "self.printtime+batdata.vbat+batdata.vdelta \ 13 | +batdata.iall+batdata.soctxt+batdata.socadjtxt" 14 | 15 | [battery] 16 | name = 'Geoff' 17 | numcells = 16 18 | capacity = 180 19 | overvoltage = 3.600 20 | undervoltage = 2.900 21 | float = 53.6 # battery float Voltage 22 | bulk = 55.2 # battery bulk/CV voltage 23 | ahloss = 0.10988877987905027 24 | vreset = 55.0 # voltage to reset DOD counter 25 | ireset = 5.0 # current to reset DOD counter 26 | ilowcurrent = 9.0 # C/20 lowcurrent voltage logging current 27 | 28 | [Interfaces] 29 | #bms = 'bms(LH-SP15S001-P15S-30A)' # string in brackets is harware name, from read command 05 30 | ads1115 = 'ads1115' 31 | 32 | [Ports] 33 | bmsport = '/dev/ttyS1' # where to look for bmsport 34 | 35 | [AtoDs] 36 | atod2 = 'AtoD.ADS1115(address=0x4A, busnum=2)' 37 | 38 | [VChannels] 39 | 40 | [IChannels] 41 | iin1 = 'atod2.read_adc_difference(3, gain=16)' 42 | iin2 = 'atod2.read_adc_difference(0, gain=16)' 43 | 44 | [VoltageInputs] 45 | #v01 = 'self.bms.rawdat["V01"]' 46 | #v02 = 'self.bms.rawdat["V02"]' 47 | #v03 = 'self.bms.rawdat["V03"]' 48 | #v04 = 'self.bms.rawdat["V04"]' 49 | #v05 = 'self.bms.rawdat["V05"]' 50 | #v06 = 'self.bms.rawdat["V06"]' 51 | #v07 = 'self.bms.rawdat["V07"]' 52 | #v08 = 'self.bms.rawdat["V08"]' 53 | #v09 = 'self.bms.rawdat["V09"]' 54 | #v10 = 'self.bms.rawdat["V10"]' 55 | #v11 = 'self.bms.rawdat["V11"]' 56 | #v12 = 'self.bms.rawdat["V12"]' 57 | #v13 = 'self.bms.rawdat["V13"]' 58 | #v14 = 'self.bms.rawdat["V14"]' 59 | #v15 = 'self.bms.rawdat["V15"]' 60 | #v16 = 'self.bms.rawdat["V16"]' 61 | 62 | [CurrentInputs] 63 | iin1 = 'self.ads1115.rawi[0]' 64 | iin2 = 'self.ads1115.rawi[1]' 65 | iin3 = 'self.current[0]-self.current[1]' 66 | 67 | [TemperatureInputs] 68 | #tin1 = 'self.bms.rawdat["T4"]' 69 | #tin2 = 'self.bms.rawdat["T3"]' 70 | #tin3 = 'self.bms.rawdat["T1"]' 71 | #tin4 = 'self.bms.rawdat["T2"]' 72 | 73 | [BalanceFlags] 74 | #balf01 = 'self.bms.rawdat["Bal"] & 1' 75 | #balf02 = 'self.bms.rawdat["Bal"] >> 1 & 1' 76 | #balf03 = 'self.bms.rawdat["Bal"] >> 2 & 1' 77 | #balf04 = 'self.bms.rawdat["Bal"] >> 3 & 1' 78 | #balf05 = 'self.bms.rawdat["Bal"] >> 4 & 1' 79 | #balf06 = 'self.bms.rawdat["Bal"] >> 5 & 1' 80 | #balf07 = 'self.bms.rawdat["Bal"] >> 6 & 1' 81 | #balf08 = 'self.bms.rawdat["Bal"] >> 7 & 1' 82 | #balf09 = 'self.bms.rawdat["Bal"] >> 8 & 1' 83 | #balf10 = 'self.bms.rawdat["Bal"] >> 9 & 1' 84 | #balf11 = 'self.bms.rawdat["Bal"] >> 10 & 1' 85 | #balf12 = 'self.bms.rawdat["Bal"] >> 11 & 1' 86 | #balf13 = 'self.bms.rawdat["Bal"] >> 12 & 1' 87 | #balf14 = 'self.bms.rawdat["Bal"] >> 13 & 1' 88 | #balf15 = 'self.bms.rawdat["Bal"] >> 14 & 1' 89 | #balf16 = 'self.bms.rawdat["Bal"] >> 15 & 1' 90 | 91 | [Status] 92 | 93 | [sampling] 94 | sampletime = 10 95 | samplesav = 1 96 | 97 | [calibrate] 98 | currentgain = [0.02188, 0.01175, 1.0] 99 | currentoffset = [-0.063, 0.0, 0.0] 100 | batvgain = 1 101 | measured = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 48.0] 102 | displayed = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 48.0] 103 | measureddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 104 | displayeddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 105 | delta = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 106 | 107 | [alarms] 108 | 109 | -------------------------------------------------------------------------------- /batterykarrak.cfg: -------------------------------------------------------------------------------- 1 | [files] 2 | logfile = '/media/sdcard/log' 3 | summaryfile = '/media/sdcard/summary' 4 | errfile = '/media/sdcard/baterrlog' 5 | alarmfile = '/media/sdcard/alarmlog' 6 | hoursummaryfile = '/media/sdcard/hoursummary' 7 | daysummaryfile = '/media/sdcard/daysummary' 8 | monthsummaryfile = '/media/sdcard/monthsummary' 9 | yearsummaryfile = '/media/sdcard/yearsummary' 10 | 11 | [logging] 12 | data = "self.printtime+batdata.vbat+batdata.vdelta \ 13 | +batdata.iall+batdata.soctxt+batdata.socadjtxt+str(batdata.pip.rawdat)" 14 | 15 | [battery] 16 | name = 'Karrak' 17 | numcells = 16 18 | capacity = 180 19 | maxchargerate = 50 20 | targetsoc = 0.95 # target maximum SOC 21 | overvoltage = 3.650 22 | undervoltage = 2.900 23 | float = 53.6 # battery float Voltage 24 | bulk = 56.0 # battery bulk/CV voltage 25 | ahloss = 0.26824555354228935 26 | vreset = 55.6 # voltage to reset DOD counter 27 | ireset = 5.0 # current to reset DOD counter 28 | ilowcurrent = 9.0 # C/20 lowcurrent voltage logging current 29 | 30 | [Inverters] 31 | numinverters = 2 # number of phisical inverters 32 | turnonslave = 3000.0 # power output to turn slave inverter on 33 | minruntime = 1800.0 # mimimum runtime of slave inverter in seconds 34 | 35 | [Interfaces] 36 | pip = 'pip(92931805102299)' # number in brackets is SN of PIP) 37 | pip2 = 'pip(92931906104223)' # number in brackets is SN of PIP) 38 | bms = 'bms(LH-SP15S001-P15S-30A)' # string in brackets is harware name, from read command 05 39 | 40 | [Ports] 41 | pipport = '/dev/ttyU*' # where to look for pipport 42 | bmsport = '/dev/ttyS1' # where to look for bmsport 43 | 44 | [VoltageInputs] 45 | v01 = 'self.bms.rawdat["V01"]' 46 | v02 = 'self.bms.rawdat["V02"]' 47 | v03 = 'self.bms.rawdat["V03"]' 48 | v04 = 'self.bms.rawdat["V04"]' 49 | v05 = 'self.bms.rawdat["V05"]' 50 | v06 = 'self.bms.rawdat["V06"]' 51 | v07 = 'self.bms.rawdat["V07"]' 52 | v08 = 'self.bms.rawdat["V08"]' 53 | v09 = 'self.bms.rawdat["V09"]' 54 | v10 = 'self.bms.rawdat["V10"]' 55 | v11 = 'self.bms.rawdat["V11"]' 56 | v12 = 'self.bms.rawdat["V12"]' 57 | v13 = 'self.bms.rawdat["V13"]' 58 | v14 = 'self.bms.rawdat["V14"]' 59 | v15 = 'self.bms.rawdat["V15"]' 60 | v16 = 'self.bms.rawdat["V16"]' 61 | vbat = 'self.bms.rawdat["V16"]' 62 | vinv = 'self.pip.rawdat["BV"]' 63 | 64 | [CurrentInputs] 65 | iin1 = 'self.pip.rawdat["PVI"]' 66 | iin2 = 'self.pip2.rawdat["PVI"]' 67 | iin3 = 'self.bms.rawdat["Ibat"]' 68 | iin4 = 'self.current[0]+self.current[1]' 69 | iin5 = 'self.current[2]-self.current[3]' 70 | 71 | [TemperatureInputs] 72 | tin1 = 'self.bms.rawdat["T1"]' 73 | tin2 = 'self.bms.rawdat["T2"]' 74 | tin3 = 'self.bms.rawdat["T3"]' 75 | tin4 = 'self.bms.rawdat["T4"]' 76 | 77 | [BalanceFlags] 78 | balf01 = 'self.bms.rawdat["Bal"] & 1' 79 | balf02 = 'self.bms.rawdat["Bal"] >> 1 & 1' 80 | balf03 = 'self.bms.rawdat["Bal"] >> 2 & 1' 81 | balf04 = 'self.bms.rawdat["Bal"] >> 3 & 1' 82 | balf05 = 'self.bms.rawdat["Bal"] >> 4 & 1' 83 | balf06 = 'self.bms.rawdat["Bal"] >> 5 & 1' 84 | balf07 = 'self.bms.rawdat["Bal"] >> 6 & 1' 85 | balf08 = 'self.bms.rawdat["Bal"] >> 7 & 1' 86 | balf09 = 'self.bms.rawdat["Bal"] >> 8 & 1' 87 | balf10 = 'self.bms.rawdat["Bal"] >> 9 & 1' 88 | balf11 = 'self.bms.rawdat["Bal"] >> 10 & 1' 89 | balf12 = 'self.bms.rawdat["Bal"] >> 11 & 1' 90 | balf13 = 'self.bms.rawdat["Bal"] >> 12 & 1' 91 | balf14 = 'self.bms.rawdat["Bal"] >> 13 & 1' 92 | balf15 = 'self.bms.rawdat["Bal"] >> 14 & 1' 93 | balf16 = 'self.bms.rawdat["Bal"] >> 15 & 1' 94 | 95 | [Status] 96 | chargestate1 = "self.pip.rawdat['ChgStat']" 97 | chargestate2 = "self.pip2.rawdat['ChgStat']" 98 | 99 | [sampling] 100 | sampletime = 10 101 | samplesav = 1 102 | 103 | [calibrate] 104 | currentgain = [-1.0, -1.0, -0.01606, 1.0, 1.0] 105 | currentoffset = [-0.0, -0.0, 0.0, 0.0, 0.0] 106 | batvgain = 1 107 | measured = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 48.0, 48.0, 48.0] 108 | displayed = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 48.0, 48.0, 48.0] 109 | measureddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 110 | displayeddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 111 | delta = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 112 | 113 | [alarms] 114 | overv = ['batdata.pip.setparamnoerror("PCVV" +str(config["battery"]["bulk"]))', \ 115 | 'self.test=batdata.maxcellv>3.65', \ 116 | 'batdata.pip.setparamnoerror("PCVV" +str(config["battery"]["float"]))', \ 117 | 'self.test=batdata.maxcellv<3.40', \ 118 | 'batdata.pip.setparamnoerror("PCVV" +str(config["battery"]["bulk"]))'] 119 | underv = ['batdata.pip.setparamnoerror("PSDV48.0")', \ 120 | 'self.test=batdata.mincellv<2.90', \ 121 | 'batdata.pip.setparamnoerror("MNCHGC0497")', \ 122 | 'self.test=batdata.mincellv>3.20', \ 123 | 'batdata.pip.setparamnoerror("MNCHGC0498")'] 124 | swapinverter = ['pass', \ 125 | 'self.test=localtime().tm_hour==8', \ 126 | 'batdata.pip.swapinverter()', \ 127 | 'self.test=localtime().tm_hour==9', \ 128 | 'pass'] 129 | acoverload = ['pass', \ 130 | 'self.test=batdata.pip.acloadav+batdata.pip2.acloadav>config["Inverters"]["turnonslave"]', \ 131 | 'batdata.pip.slaveinvon()', \ 132 | 'self.test=batdata.pip.timeoverload==0.0', \ 133 | 'batdata.pip.slaveinvoff()'] 134 | -------------------------------------------------------------------------------- /batterymike.cfg: -------------------------------------------------------------------------------- 1 | [files] 2 | logfile = '/home/pi/data/log' 3 | summaryfile = '/home/pi/data/summary' 4 | errfile = '/home/pi/data/baterrlog' 5 | alarmfile = '/home/pi/data/alarmlog' 6 | hoursummaryfile = '/home/pi/data/hoursummary' 7 | daysummaryfile = '/home/pi/data/daysummary' 8 | monthsummaryfile = '/home/pi/data/monthsummary' 9 | yearsummaryfile = '/home/pi/data/yearsummary' 10 | 11 | [logging] 12 | data = "self.printtime+batdata.vbat+batdata.vdelta \ 13 | +batdata.iall+batdata.soctxt+batdata.socadjtxt+str(batdata.pip.rawdat)+str(batdata.pip2.rawdat)" 14 | 15 | [battery] 16 | name = 'Mikes' 17 | numcells = 15 18 | capacity = 200 19 | maxchargerate = 50 20 | targetsoc = 0.95 # target maximum SOC 21 | minsocdif=0.03 22 | float = 50.2 23 | bulk = 52.2 24 | overvoltage = 3.650 25 | undervoltage = 2.900 26 | ahloss = 0.29498831864316155 27 | vreset = 51.8 # voltage to reset DOD counter 28 | ireset = 5.0 # current to reset DOD counter 29 | ilowcurrent = 10.0 # C/20 lowcurrent voltage logging current 30 | 31 | [DemandManager] 32 | socfeedback = 1.0 # multiplier for SOC difference feedback 33 | currentfeedback = 0.70 # multiplier for current diffenence feedback 34 | 35 | [Inverters] 36 | numinverters = 2 # number of phisical inverters 37 | turnonslave = 3000.0 # power output to turn slave inverter on 38 | minruntime = 1800.0 # mimimum runtime of slave inverter in seconds 39 | 40 | [Interfaces] 41 | pip = 'pip(92931710100542)' # number in brackets is SN of PIP) 42 | pip2 = 'pip(92931712101484)' # number in brackets is SN of PIP) 43 | bms = 'bms(SP15S001-L15S-30A)' # string in brackets is harware name, from read command 05 44 | pi = 'pi' # add raspberry pi 45 | 46 | [Ports] 47 | pipport = '/dev/ttyU*' # where to look for pipport 48 | bmsport = '/dev/ttyU*' # where to look for bmsport 49 | 50 | [VoltageInputs] 51 | v01 = 'self.bms.rawdat["V01"]' 52 | v02 = 'self.bms.rawdat["V02"]' 53 | v03 = 'self.bms.rawdat["V03"]' 54 | v04 = 'self.bms.rawdat["V04"]' 55 | v05 = 'self.bms.rawdat["V05"]' 56 | v06 = 'self.bms.rawdat["V06"]' 57 | v07 = 'self.bms.rawdat["V07"]' 58 | v08 = 'self.bms.rawdat["V08"]' 59 | v09 = 'self.bms.rawdat["V09"]' 60 | v10 = 'self.bms.rawdat["V10"]' 61 | v11 = 'self.bms.rawdat["V11"]' 62 | v12 = 'self.bms.rawdat["V12"]' 63 | v13 = 'self.bms.rawdat["V13"]' 64 | v14 = 'self.bms.rawdat["V14"]' 65 | v15 = 'self.bms.rawdat["V15"]' 66 | vinv = 'self.pip2.rawdat["BV"]' 67 | 68 | [CurrentInputs] 69 | iin1 = 'self.pip.rawdat["PVI"]' 70 | iin2 = 'self.pip2.rawdat["PVI"]' 71 | iin3 = 'self.pip.rawdat["BOutI"]' 72 | iin4 = 'self.pip.rawdat["BInI"]' 73 | iin5 = 'self.pip2.rawdat["BOutI"]' 74 | iin6 = 'self.pip2.rawdat["BInI"]' 75 | iin7 = 'self.current[2]+self.current[3]+self.current[4]+self.current[5]' 76 | iin8 = 'self.current[0]+self.current[1]' 77 | iin9 = 'self.current[6]-self.current[7]' 78 | 79 | [TemperatureInputs] 80 | tin1 = 'self.bms.rawdat["T2"]' 81 | tin2 = 'self.bms.rawdat["T1"]' 82 | 83 | [BalanceFlags] 84 | balf01 = 'self.bms.rawdat["Bal"] & 1' 85 | balf02 = 'self.bms.rawdat["Bal"] >> 1 & 1' 86 | balf03 = 'self.bms.rawdat["Bal"] >> 2 & 1' 87 | balf04 = 'self.bms.rawdat["Bal"] >> 3 & 1' 88 | balf05 = 'self.bms.rawdat["Bal"] >> 4 & 1' 89 | balf06 = 'self.bms.rawdat["Bal"] >> 5 & 1' 90 | balf07 = 'self.bms.rawdat["Bal"] >> 6 & 1' 91 | balf08 = 'self.bms.rawdat["Bal"] >> 7 & 1' 92 | balf09 = 'self.bms.rawdat["Bal"] >> 8 & 1' 93 | balf10 = 'self.bms.rawdat["Bal"] >> 9 & 1' 94 | balf11 = 'self.bms.rawdat["Bal"] >> 10 & 1' 95 | balf12 = 'self.bms.rawdat["Bal"] >> 11 & 1' 96 | balf13 = 'self.bms.rawdat["Bal"] >> 12 & 1' 97 | balf14 = 'self.bms.rawdat["Bal"] >> 13 & 1' 98 | balf15 = 'self.bms.rawdat["Bal"] >> 14 & 1' 99 | 100 | [Status] 101 | chargestate1 = "self.pip.rawdat['ChgStat']" 102 | chargestate2 = "self.pip2.rawdat['ChgStat']" 103 | 104 | [sampling] 105 | sampletime = 10 106 | samplesav = 1 107 | 108 | [calibrate] 109 | currentgain = [-1.0, -1.0, 1.0, -1.0, 1.0, -1.0 ,1.0, 1.0, 1.0] 110 | currentoffset = [-0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 111 | batvgain = 1 112 | measured = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 45.0] 113 | displayed = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 39.0, 42.0, 45.0, 45.0] 114 | measureddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 115 | displayeddelta = [ 0.0, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000, 3.000] 116 | delta = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 117 | 118 | [alarms] 119 | overv = ['batdata.pip.setparamnoerror("PCVV" +str(config["battery"]["bulk"]))', \ 120 | 'self.test=batdata.maxcellv>3.60', \ 121 | 'batdata.pip.setparamnoerror("PCVV" +str(config["battery"]["float"]))', \ 122 | 'self.test=batdata.maxcellv<3.40', \ 123 | 'batdata.pip.setparamnoerror("PCVV" +str(config["battery"]["bulk"]))'] 124 | underv = ['batdata.pip.setparamnoerror("PSDV46.0")', \ 125 | 'self.test=batdata.mincellv<2.90', \ 126 | 'batdata.pip.setparamnoerror("PSDV48.0")', \ 127 | 'self.test=batdata.mincellv>3.20', \ 128 | 'batdata.pip.setparamnoerror("PSDV46.0")'] 129 | waterheater = ['batdata.pi.gpio.setup(15, batdata.pi.gpio.OUT, initial = 1)', \ 130 | 'self.test=batdata.batpwr1hrav*-1000/batdata.batvoltsav[-1] - batdata.socadj+10 > 0 and localtime().tm_min==5', \ 131 | 'batdata.pi.gpio.output(15,False)', \ 132 | 'self.test=batdata.batpwr1hrav*-1000/batdata.batvoltsav[-1] - batdata.socadj+20 < 0 and localtime().tm_min==35', \ 133 | 'batdata.pi.gpio.output(15,True)'] 134 | 135 | swapinverter = ['batdata.pi.gpio.setup(11, batdata.pi.gpio.OUT, initial = 1) \nbatdata.pi.gpio.setup(13, batdata.pi.gpio.OUT, initial = 0)', \ 136 | 'self.test=localtime().tm_hour==18', \ 137 | 'batdata.pi.swapinverter(on=7,off=11)', \ 138 | 'self.test=localtime().tm_hour==6', \ 139 | 'batdata.pi.swapinverter(on=11,off=7)'] 140 | acoverload = ['pass', \ 141 | 'self.test=batdata.pip.acloadav+batdata.pip2.acloadav>config["Inverters"]["turnonslave"]', \ 142 | 'batdata.pi.allinvon((11,13))', \ 143 | 'self.test=batdata.pip.timeoverload==0.0', \ 144 | 'batdata.pi.restoreinverters()'] 145 | 146 | 147 | -------------------------------------------------------------------------------- /beagle.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/python 3 | # *****BatteryMonitor Getdata from battery cells getdata.py***** 4 | # Copyright (C) 2020 Simon Richard Matthews 5 | # Project loaction https://github.com/simat/BatteryMonitor 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # any later version. 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # You should have received a copy of the GNU General Public License along 15 | # with this program; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | import Adafruit_BBIO.GPIO as gpio 19 | import Adafruit_BBIO.ADC as adc 20 | adc.setup() 21 | 22 | class Rawdat(): 23 | """class for using the raspberry Pi for IO""" 24 | 25 | def __init__(self): 26 | gpio.setup("P9_41", gpio.OUT) 27 | adc.setup() 28 | self.rawdat={'ADCP9_39':0.0} 29 | 30 | def chargeonoff(self,onoroff): 31 | gpio.output("P9_41",onoroff) 32 | 33 | def getdata(self): 34 | self.rawdat['ADCP9_39'] = adc.read("P9_39") 35 | print (self.rawdat['ADCP9_39']) 36 | -------------------------------------------------------------------------------- /bms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor Getdata from battery cells getdata.py***** 3 | # Copyright (C) 2017 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | import time 18 | import serial 19 | import binascii 20 | import glob 21 | from config import config 22 | numcells = config['battery']['numcells'] 23 | import logger 24 | log = logger.logging.getLogger(__name__) 25 | log.setLevel(logger.logging.DEBUG) 26 | log.addHandler(logger.errfile) 27 | 28 | class Bms: 29 | """Generic Chinese BMS comms class""" 30 | 31 | # def __init__(self,sn): 32 | 33 | 34 | def findbms(self,interfacesinuse): 35 | """Scan ports to find BMS port""" 36 | 37 | self.bmsport="" 38 | for dev in glob.glob(config['Ports']['bmsport']): 39 | for i in range(2): 40 | if dev not in interfacesinuse: 41 | print(dev) 42 | try: 43 | self.openbms(dev) 44 | reply=self.getbmsdat(self.port,b'\x05\x00') 45 | if reply.decode('ascii','strict')==self.sn: 46 | self.bmsport=dev 47 | break 48 | except serial.serialutil.SerialException: 49 | pass 50 | finally: 51 | self.port.close() 52 | if self.bmsport!="": 53 | break 54 | if self.bmsport=="": 55 | raise Exception("Couldn't find BMS hardware name {}".format(self.sn)) 56 | 57 | 58 | 59 | def crccalc(self,data): 60 | """returns crc as integer from byte stream""" 61 | crc=0x10000 62 | for i in data: 63 | crc=crc-int(i) 64 | return crc 65 | 66 | def openbms(self,port): 67 | self.port = serial.Serial(port,timeout=1) # open serial port 68 | 69 | def getbmsdat(self,port,command): 70 | """ Issue BMS command and return data as byte data """ 71 | """ assumes data port is open and configured """ 72 | packet=b'' 73 | crc=self.crccalc(command) 74 | packet=b'\xDD\xA5'+command+crc.to_bytes(2, byteorder='big')+b'\x77' 75 | # print (packet) 76 | data=self.sendbms(self.port,packet) 77 | return data 78 | 79 | 80 | def sendbms(self,port,packet): 81 | """Send complete command string with crc to BMS""" 82 | 83 | for i in range(3): 84 | try: 85 | self.port.flushInput() 86 | self.port.write(packet) 87 | reply = self.port.read(4) 88 | # raise serial.serialutil.SerialException('hithere') 89 | 90 | # print (reply) 91 | x = int.from_bytes(reply[3:5], byteorder = 'big') 92 | # print (x) 93 | data = self.port.read(x) 94 | end = self.port.read(3) 95 | # print (data,end,self.crccalc(reply[2:4]+data),end[0:2]) 96 | if data == b'': 97 | raise serial.serialutil.SerialException('No Reply') 98 | 99 | if self.crccalc(reply[2:4]+data)!=int.from_bytes(end[0:2],byteorder='big'): 100 | raise serial.serialutil.SerialException('CRC data= {} calCRC={} CRC={}'.format(data,self.crccalc(reply[2:4]+data),int.from_bytes(end[0:2],byteorder='big'))) 101 | # print (data) 102 | break 103 | except serial.serialutil.SerialException: 104 | if i==2: 105 | raise 106 | return data 107 | 108 | class Rawdat(Bms): 109 | 110 | def __init__(self,sn,interfacesinuse): 111 | 112 | self.rawdat={'DataValid':False,'V00':0.0} 113 | self.sn=sn 114 | self.findbms(interfacesinuse) 115 | """port=self.openbms(config['files']['usbport']) 116 | data=self.getbmsdat(port,b'\x04\x00') # get BMS voltages 117 | port.close() 118 | self.rawdat={'V'+str(x+1):0.0 for x in range(int(len(data)/2))} 119 | self.rawdat['Ibat']=0.0""" 120 | 121 | def getdata(self): 122 | self.rawdat['DataValid']=False 123 | for i in range(5): 124 | try: 125 | self.openbms(self.bmsport) 126 | data=self.getbmsdat(self.port,b'\x04\x00') # get BMS Voltage 127 | # print(data) 128 | for i in range(int(len(data)/2)): 129 | self.rawdat['V{0:0=2}'.format(i+1)]=int.from_bytes(data[i*2:i*2+2], byteorder = 'big')/1000 \ 130 | +self.rawdat['V{0:0=2}'.format(i)] 131 | # convert from cell voltage to total voltages 132 | data=self.getbmsdat(self.port,b'\x03\x00') # get other BMS data 133 | self.rawdat['Ibat']=int.from_bytes(data[2:4], byteorder = 'big',signed=True) 134 | self.rawdat['Bal']=int.from_bytes(data[12:14],byteorder = 'big',signed=False) 135 | for i in range(int.from_bytes(data[22:23],'big')): # read temperatures 136 | self.rawdat['T{0:0=1}'.format(i+1)]=(int.from_bytes(data[23+i*2:i*2+25],'big')-2731)/10 137 | # print (self.rawdat) 138 | self.rawdat['DataValid']=True 139 | break 140 | except ValueError as err: 141 | log.error('{}\n{}'.format(err,reply)) 142 | time.sleep(1.0) 143 | if i==4: 144 | raise 145 | except Exception as err: 146 | log.error(err) 147 | time.sleep(0.5) 148 | if i==4: 149 | raise 150 | finally: 151 | self.port.close() 152 | -------------------------------------------------------------------------------- /bmscore.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****Core routines to retrieve and store data to BMS PCBs***** 3 | # Copyright (C) 2017 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | import time 18 | import serial 19 | import binascii 20 | import json 21 | 22 | def rdjson(file): 23 | with open(file, "r") as read_file: 24 | data=json.load(read_file) 25 | return data 26 | 27 | configinmem={} # template for bms data 28 | configinmem=rdjson('bms.json') 29 | fullconfiglist= [i for i in configinmem] 30 | 31 | def wrjson(file,data): 32 | with open(file, "w") as write_file: 33 | json.dump(data,write_file,indent=2) 34 | 35 | def crccalc(data): 36 | """returns crc as integer from byte stream""" 37 | crc=0x10000 38 | for i in data: 39 | crc=crc-int(i) 40 | return crc 41 | 42 | 43 | def rdallconfig(port='/dev/ttyUSB0'): 44 | "reads BMS data from BMS board and stores in template configinmem" 45 | 46 | configitems(list) 47 | 48 | def openbms(port='/dev/ttyUSB0'): 49 | ser = serial.Serial(port) # open serial port 50 | ser.timeout = 1 51 | return ser 52 | 53 | def getbmsdat(port,command): 54 | """ Issue BMS command and return data as byte data """ 55 | """ assumes data port is open and configured """ 56 | print ('command=',binascii.hexlify(command)) 57 | port.write(command) 58 | reply = port.read(4) 59 | x = int.from_bytes(reply[3:5], byteorder = 'big') 60 | data = port.read(x) 61 | end = port.read(3) 62 | if len(data)")) 24 | return command 25 | 26 | def switchfets(port='/dev/ttyUSB0'): 27 | """ switch charge and discharge fets """ 28 | print ('(03)=Both FETs off') 29 | print ('(01)=Discharge FET on, Charge FET off') 30 | print ('(02)=Discharge FET off, Charge FET on') 31 | print ('(00)=Both FETs on') 32 | usercmd = input("Enter numeric option> ") 33 | ser = bmscore.openbms(port) 34 | command = bytes.fromhex('DD A5 03 00 FF FD 77') 35 | print ('command=',binascii.hexlify(command)) 36 | data=bmscore.getbmsdat(ser,command) 37 | print ('reply=',binascii.hexlify(data)) 38 | command = bytes.fromhex('DD A5 04 00 FF FC 77') 39 | print ('command=',binascii.hexlify(command)) 40 | data=bmscore.getbmsdat(ser,command) 41 | print ('reply=',binascii.hexlify(data)) 42 | command = bytes.fromhex('DD 5A 00 02 56 78 FF 30 77') 43 | data=bmscore.getbmsdat(ser,command) 44 | print ('reply=',binascii.hexlify(data)) 45 | usercmd=b'\xE1\x02\x00'+bytes.fromhex(usercmd) 46 | command = b'\xDD\x5A'+usercmd+bmscore.crccalc(usercmd).to_bytes(2, byteorder='big')+b'\x77' 47 | print (binascii.hexlify(command)) 48 | bmscore.getbmsdat(ser,command) 49 | command = bytes.fromhex('DD 5A 01 02 00 00 FF FD 77') 50 | bmscore.getbmsdat(ser,command) 51 | 52 | def getdat(port='/dev/ttyUSB0'): 53 | """ Get data from BMS board""" 54 | ser = bmscore.openbms(port) 55 | command = bytes.fromhex('DD A5 03 00 FF FD 77') 56 | dat = bmscore.getbmsdat(ser,command) 57 | dat = bmscore.getbmsdat(ser,command) 58 | rawi = int.from_bytes(dat[2:4], byteorder = 'big',signed=True) 59 | rawv = int.from_bytes(dat[0:2], byteorder = 'big',signed=True) 60 | balance = bin(int.from_bytes(dat[12:14], byteorder = 'big',signed=True)) 61 | state = int.from_bytes(dat[16:18], byteorder = 'big',signed=True) 62 | fets = bin(int.from_bytes(dat[20:21], byteorder = 'big',signed=True)) 63 | print ("V={} I={} bal={} state={} fets={}".format(rawv,rawi,balance,bin(state),fets)) 64 | errors=['Cell Overvoltage','Cell Undervoltage','Battery Overvoltage', \ 65 | 'Battery Undervoltage','Charge Overtemp','Charge Undertemp', \ 66 | 'Discharge Overtemp','Discharge Undertemp','Charge Overcurrent' \ 67 | 'Discharge Overcurrent','Short Circuit','IC Fault','Software MOS lock'] 68 | for i in range(state.bit_length()): 69 | if 2**i & state: 70 | print (errors[i]) 71 | 72 | # line1 = [ 0 for i in range(int(len(dat)))] 73 | # for i in range(0,int(len(dat))): 74 | # print (dat[i*2:i*2+2]) 75 | # print (int.from_bytes(dat[i:i+1], byteorder = 'big')) 76 | # line1[i] = int.from_bytes(dat[i:i+1], byteorder = 'big') 77 | print (binascii.hexlify(dat)) 78 | # print (line1) 79 | 80 | 81 | # voltages 82 | command = bytes.fromhex('DD A5 04 00 FF FC 77') 83 | voltages = bmscore.getbmsdat(ser,command) 84 | ser.close 85 | print (binascii.hexlify(voltages)) 86 | rawv = [ 0.0 for i in range(15)] 87 | for i in range(15): 88 | rawv[i] = int.from_bytes(voltages[i*2:i*2+2], byteorder = 'big')/1000.00 89 | # rawv[i] = rawv[i]+rawv[i-1] 90 | print (rawv) 91 | 92 | command = bytes.fromhex('DD A5 05 00 FF FB 77') 93 | dat = bmscore.getbmsdat(ser,command) 94 | 95 | # line1 = [ 0 for i in range(int(len(dat)))] 96 | # for i in range(0,int(len(dat))): 97 | # print (dat[i*2:i*2+2]) 98 | # print (int.from_bytes(dat[i:i+1], byteorder = 'big')) 99 | # line1[i] = int.from_bytes(dat[i:i+1], byteorder = 'big') 100 | print (binascii.hexlify(dat)) 101 | # print (line1) 102 | 103 | def findregname(reg): 104 | """Find register name given register address""" 105 | 106 | regname=None 107 | for i in bmscore.configinmem: 108 | if bmscore.configinmem[i]['reg']==reg: 109 | regname=i 110 | break 111 | return regname 112 | 113 | def enterreg(): 114 | """Get user entry of register address and value""" 115 | 116 | cmd=int(input('By Name (1) or by register number (2)?>')) 117 | if cmd==1: 118 | item=input("Enter Config Item Name>") 119 | elif cmd==2: 120 | item=str.upper(input("Enter Config Register Address>")) 121 | item=findregname(item) 122 | value=input("{} = {}, Enter New Value, [return] for don't write>" \ 123 | .format(item,bmscore.configinmem[item]['value'])) 124 | reginfo ={item:value} 125 | return reginfo 126 | 127 | def chgreg(reginfo): 128 | """Stores Values in reginfo dictionary""" 129 | for reg in reginfo: 130 | if reginfo[reg]: 131 | if "valueint" in bmscore.configinmem[reg]['decode']: 132 | reginfo[reg]=float(reginfo[reg]) 133 | bmscore.configinmem[reg]['value']=reginfo[reg] 134 | 135 | def main(): 136 | print (sys.argv) 137 | if len(sys.argv) == 2: 138 | sendcmd(sys.argv[1]) 139 | elif len(sys.argv) == 3: 140 | sendcmd(sys.argv[1],sys.argv[2]) 141 | elif len(sys.argv) == 1: 142 | 143 | print ('Enter BMS port address option [3]') 144 | print ('(1) /dev/ttyUSB0') 145 | print ('(2) /dev/ttyUSB1') 146 | print ('(3) other') 147 | port=int(getcmd()) 148 | if port==1: 149 | port='/dev/ttyUSB0' 150 | elif port == 2: 151 | port='/dev/ttyUSB1' 152 | else: 153 | port=str(input("Enter port name>")) 154 | 155 | while True: 156 | bmscore.openbms(port) 157 | 158 | print('Enter option') 159 | print('(1) Load all config data from BMS to memory') 160 | print('(2) Read all config data from disk to memory') 161 | print('(3) Write all config data from memory to BMS') 162 | print('(4) Write all config data from memory to disk') 163 | print('(5) Dump all config data in memory') 164 | print('(6) Dump raw config data in memory') 165 | print('(7) Read/Write config item in memory') 166 | print('(8) Read/Write single register in memory and on BMS PCB') 167 | print('(9) Read BMS data') 168 | print('(10) Switch charge/discharge FETs') 169 | print('(11) Calibrate BMS') 170 | while True: 171 | try: 172 | cmd=int(getcmd()) 173 | except ValueError: 174 | print('Invalid input') 175 | break 176 | if cmd==1: 177 | bmscore.configitems(bmscore.fullconfiglist,port) 178 | elif cmd==2: 179 | file=str(input("Enter filename>")) 180 | bmscore.configinmem=bmscore.rdjson(file) 181 | elif cmd==3: 182 | bmscore.configitems(bmscore.fullconfiglist,port,write=True) 183 | elif cmd==4: 184 | file=str(input("Enter filename>")) 185 | bmscore.wrjson(file,bmscore.configinmem) 186 | elif cmd ==5: 187 | count = 0 188 | for i in bmscore.configinmem: 189 | print ('{}={}{} {}'.format \ 190 | (i,bmscore.configinmem[i]['value'],bmscore.configinmem[i]['units'],bmscore.configinmem[i]['comment'])) 191 | count=count+1 192 | if count%30==0: 193 | x=input("press return for next page") 194 | 195 | elif cmd==7: 196 | reg=enterreg() 197 | chgreg(reg) 198 | elif cmd==6: 199 | count=0 200 | for i in bmscore.configinmem: 201 | print (i,bmscore.configinmem[i]) 202 | count=count+1 203 | if count%30==0: 204 | x=input("press return for next page") 205 | elif cmd==8: 206 | reg=enterreg() 207 | val, =dict.values(reg) 208 | if val: 209 | chgreg(reg) 210 | bmscore.configitems(reg,port,write=True) 211 | elif cmd==9: 212 | getdat(port) 213 | elif cmd ==10: 214 | switchfets(port) 215 | elif cmd ==11: 216 | print ('Enter Item to Calibrate?') 217 | print ('(1) Cell Voltages') 218 | print ('(2) Battery Current') 219 | item=int(getcmd()) 220 | if item==1: 221 | item=input("Enter cell number/s e.g. '1-4,5'?> ") 222 | result=set() 223 | for part in item.split(','): 224 | x=part.split('-') 225 | result.update(range(int(x[0]),int(x[-1])+1)) 226 | celllist=sorted(result) 227 | cellvolts=int(input("Enter cell voltage/s in mV?> ")) 228 | reglist={} 229 | for i in range(len(celllist)): 230 | reg=findregname(str.upper(format(celllist[i]+0xAF,'02x'))) 231 | reglist[reg]=cellvolts 232 | chgreg(reglist) 233 | bmscore.configitems(reglist,port,write=True,calibrate=True) 234 | elif item ==2: 235 | print("Enter current type?> ") 236 | print ('(1) Idle Current') 237 | print ('(2) Charge Current') 238 | print ('(3) Discharge Current') 239 | item=int(getcmd()) 240 | if item==1: 241 | reg='CalibrateIdleA' 242 | elif item==2: 243 | reg='CalibrateChgA' 244 | elif item==3: 245 | reg='CalibrateDchgA' 246 | current=int(input("Enter Measured Current in A> ")) 247 | reginfo={reg:current} 248 | chgreg(reginfo) 249 | bmscore.configitems(reginfo,port,write=True,calibrate=True) 250 | else: 251 | print('No such Command') 252 | 253 | if __name__ == "__main__": 254 | """if run from command line""" 255 | main() 256 | -------------------------------------------------------------------------------- /calibrate.py: -------------------------------------------------------------------------------- 1 | # *****BatteryMonitor calibration file calibrate.py***** 2 | # Copyright (C) 2014 Simon Richard Matthews 3 | # Project loaction https://github.com/simat/BatteryMonitor 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # any later version. 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # You should have received a copy of the GNU General Public License along 13 | # with this program; if not, write to the Free Software Foundation, Inc., 14 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 15 | 16 | 17 | #!/usr/bin/python 18 | import sys 19 | #import smbus 20 | #from Adafruit_I2C import Adafruit_I2C 21 | from config import config 22 | numcells = config['battery']['numcells'] 23 | from getdata import Readings 24 | batdata = Readings() 25 | 26 | def calibrate(): 27 | 28 | while True: 29 | try: 30 | for i in range(config['sampling']['samplesav']): 31 | batdata.getraw() 32 | printvoltage = '' 33 | for i in range(numcells+1): 34 | printvoltage = printvoltage + str(round(batdata.uncalvolts[i],3)).ljust(5,'0') + ' ' 35 | printvoltage = printvoltage + '\n' 36 | for i in range(1, numcells+1): 37 | printvoltage = printvoltage + str(round(batdata.uncalvolts[i]-batdata.uncalvolts[i-1],3)).ljust(5,'0') + ' ' 38 | printvoltage = printvoltage +'\n' 39 | 40 | for i in range(numcells+1): 41 | printvoltage = printvoltage + str(round(batdata.batvolts[i],3)).ljust(5,'0') + ' ' 42 | printvoltage = printvoltage + '\n' 43 | for i in range(1, numcells+1): 44 | printvoltage = printvoltage + str(round(batdata.batvolts[i]-batdata.batvolts[i-1],3)).ljust(5,'0') + ' ' 45 | printvoltage = printvoltage +'\n' 46 | for i in range(1, numcells+1): 47 | printvoltage = printvoltage + str(round(batdata.batvolts[i]-batdata.batvolts[i-1]+batdata.calvolts[i],3)).ljust(5,'0') + ' ' 48 | printvoltage = printvoltage +'\n' 49 | print (printvoltage) 50 | 51 | 52 | except KeyboardInterrupt: 53 | break 54 | 55 | if __name__ == "__main__": 56 | calibrate() 57 | -------------------------------------------------------------------------------- /caligain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor store summary data summary.py***** 3 | # Copyright (C) 2014 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | import sys 18 | from os import rename 19 | import time 20 | from shutil import copy as filecopy 21 | from copy import deepcopy 22 | from ast import literal_eval 23 | from configparser import SafeConfigParser 24 | from config import config, loadconfig 25 | numiin = len(config['CurrentInputs']) 26 | numcells = config['battery']['numcells'] 27 | 28 | summaryfile = SafeConfigParser() 29 | 30 | def tail(file, n=1, bs=1024): 31 | f = open(file) 32 | f.seek(-1,2) 33 | l = 1-f.read(1).count('\n') # If file doesn't end in \n, count it anyway. 34 | B = f.tell() 35 | while n >= l and B > 0: 36 | block = min(bs, B) 37 | B -= block 38 | f.seek(B, 0) 39 | l += f.read(block).count('\n') 40 | f.seek(B, 0) 41 | l = min(l,n) # discard first (incomplete) line if l > n 42 | lines = f.readlines()[-l:] 43 | f.close() 44 | return lines 45 | 46 | def getavi(): 47 | istr="" 48 | avi = [ 0.0 for i in range(numiin)] 49 | endlog=tail(config['files']['logfile'],11) 50 | for line in range(10): 51 | lineargs = str(endlog[line]).split(' ') 52 | for iin in range(numiin): 53 | avi[iin] = avi[iin] + float(lineargs[1+numcells+2+iin])/10 54 | for iin in range(numiin): 55 | istr =istr+str(round(avi[iin],3)) + ", " 56 | print('Average readings for last 100 seconds: ' + istr) 57 | loadconfig() 58 | print('gain from config file: ' + str(config['calibrate']['currentgain'])) 59 | return avi 60 | 61 | 62 | def geti(): 63 | try: 64 | summaryfile.read(config['files']['summaryfile']) 65 | except IOError: 66 | pass 67 | iin=literal_eval(summaryfile.get('current','ioutmax')) 68 | iprint = '' 69 | for i in range(numiin): 70 | iprint = iprint + str(i+1) + '=' + str(iin[i]).ljust(5,'0') + ', ' 71 | print('Readings from summary: ' + iprint) 72 | 73 | 74 | def main(): 75 | print('Usage: At > prompt enter current input channel number to change then [Enter] key') 76 | print(' Pressing just the [Enter] key will display the latest current readings, type ^C to exit') 77 | print() 78 | print(' Make sure the Battery Monitoring software is running otherwise the summary file will not be current') 79 | print(' This program uses the data from the log file generated by the Battery Monitoring software') 80 | print(' Make sure that the Battery Monitoring software is logging to a real file and not /dev/null') 81 | print(' This program averages the data from the log file for the last 100 seconds.') 82 | print(' To make sure that the calibartion is accurate make sure the current on the input being calibrated is not varying') 83 | while True: 84 | try: 85 | # time.sleep(60.0) 86 | geti() 87 | what = input(">") 88 | if len(what)>0: 89 | avi=getavi() 90 | reali = eval(input("Input " + what + " multimeter reading ")) 91 | if reali == 0: 92 | print("Input Error: Zero not valid current input") 93 | else: 94 | what=int(what) 95 | config['calibrate']['currentgain'][what-1] = config['calibrate']['currentgain'][what-1]*reali/avi[what-1] 96 | print('Recalculated gain: ' + str(config['calibrate']['currentgain'])) 97 | batconfigdata=SafeConfigParser() 98 | batconfigdata.read('battery.cfg') 99 | batconfigdata.set('calibrate','currentgain',str(config['calibrate']['currentgain'])) 100 | with open('battery.cfg', 'w') as batconfig: 101 | batconfigdata.write(batconfig) 102 | batconfig.closed 103 | 104 | 105 | 106 | except KeyboardInterrupt: 107 | break 108 | 109 | if __name__ == "__main__": 110 | main() 111 | -------------------------------------------------------------------------------- /calvcourse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor store summary data summary.py***** 3 | # Copyright (C) 2014 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | import sys 18 | from os import rename 19 | import time 20 | from shutil import copy as filecopy 21 | from copy import deepcopy 22 | from ast import literal_eval 23 | from configparser import SafeConfigParser 24 | from config import config,loadconfig 25 | 26 | numcells = config['battery']['numcells'] 27 | voltages = [] 28 | avv = [] 29 | 30 | 31 | summaryfile = SafeConfigParser() 32 | 33 | def tail(file, n=1, bs=1024): 34 | f = open(file,'rb') 35 | f.seek(-1,2) 36 | l = 1-f.read(1).decode('utf-8').count('\n') # If file doesn't end in \n, count it anyway. 37 | B = f.tell() 38 | while n >= l and B > 0: 39 | block = min(bs, B) 40 | B -= block 41 | f.seek(B, 0) 42 | x = f.read(block).decode('utf-8') 43 | l += x.count('\n') 44 | f.seek(B, 0) 45 | l = min(l,n) # discard first (incomplete) line if l > n 46 | lines = f.readlines()[-l:] 47 | f.close() 48 | return lines 49 | 50 | def avv(): 51 | vstr="" 52 | v=[0.0 for i in range(numcells+1)] 53 | endlog=tail(config['files']['logfile'],11) 54 | for j in range(1,numcells+1): 55 | for i in range(10): 56 | x=float(endlog[i][(j-1)*6+15:(j-1)*6+20])+config['calibrate']['delta'][j-1] 57 | v[j]=v[j]+x 58 | for i in range(1,len(v)): 59 | v[i]=v[i]/10+v[i-1] 60 | vstr=vstr+str(i) + "=" +str(round(v[i],3)).ljust(5,'0') + ", " 61 | print(vstr) 62 | print(config['calibrate']['measured']) 63 | print(config['calibrate']['displayed']) 64 | return v 65 | 66 | def main(): 67 | print('Usage: Pressing just the [Enter] key will display the latest voltage readings, type ^C to exit') 68 | print(' Type in the cell # that you want to calibrate followed by [Enter]') 69 | print(' Then type in the mesured voltage for that cell followed by [Enter]') 70 | print() 71 | print(' Make sure the Battery Monitoring software is running otherwise the summary file will not be current') 72 | print(' This program uses the data from the log file generated by the Battery Monitoring software') 73 | print(' Make sure that the Battery Monitoring software is logging to a real file and not /dev/null') 74 | print(' This program averages the data from the log file for the last 100 seconds.') 75 | print(' To make sure that the calibration is accurate make sure the input voltages are not varying') 76 | avvolts=[] 77 | while True: 78 | try: 79 | loadconfig() 80 | avvolts=avv() 81 | # time.sleep(60.0) 82 | what = input(">") 83 | if len(what)>0: 84 | realvolts = eval(input("Cell " + what + " reading ")) 85 | what=int(what) 86 | config['calibrate']['measured'][what] = round(realvolts,3) 87 | config['calibrate']['displayed'][what] = round(avvolts[what],3) 88 | batconfigdata=SafeConfigParser() 89 | batconfigdata.read('battery.cfg') 90 | batconfigdata.set('calibrate','measured',str(config['calibrate']['measured'])) 91 | batconfigdata.set('calibrate','displayed',str(config['calibrate']['displayed'])) 92 | with open('battery.cfg', 'w') as batconfig: 93 | batconfigdata.write(batconfig) 94 | batconfig.closed 95 | 96 | 97 | except KeyboardInterrupt: 98 | break 99 | -------------------------------------------------------------------------------- /calvfine.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor store summary data summary.py***** 3 | # Copyright (C) 2014 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | import sys 18 | from os import rename 19 | import time 20 | from shutil import copy as filecopy 21 | from copy import deepcopy 22 | from ast import literal_eval 23 | from configparser import SafeConfigParser 24 | from config import config,loadconfig 25 | 26 | numcells = config['battery']['numcells'] 27 | voltages = [] 28 | avv = [] 29 | 30 | 31 | summaryfile = SafeConfigParser() 32 | 33 | def tail(file, n=1, bs=1024): 34 | f = open(file,'rb') 35 | f.seek(-1,2) 36 | l = 1-f.read(1).decode('utf-8').count('\n') # If file doesn't end in \n, count it anyway. 37 | B = f.tell() 38 | while n >= l and B > 0: 39 | block = min(bs, B) 40 | B -= block 41 | f.seek(B, 0) 42 | x = f.read(block).decode('utf-8') 43 | l += x.count('\n') 44 | f.seek(B, 0) 45 | l = min(l,n) # discard first (incomplete) line if l > n 46 | lines = f.readlines()[-l:] 47 | f.close() 48 | return lines 49 | 50 | def avv(): 51 | vstr="" 52 | v=[0.0 for i in range(numcells)] 53 | endlog=tail(config['files']['logfile'],11) 54 | for j in range(numcells): 55 | for i in range(10): 56 | x=float(endlog[i][j*6+15:j*6+20]) 57 | v[j]=v[j]+x 58 | for i in range(len(v)): 59 | v[i]=v[i]/10 60 | vstr=vstr+str(i+1) + "=" +str(round(v[i],3)).ljust(5,'0') + ", " 61 | print(vstr) 62 | print(config['calibrate']['delta']) 63 | return v 64 | 65 | 66 | 67 | def getv(): 68 | global voltages 69 | try: 70 | summaryfile.read(config['files']['summaryfile']) 71 | except IOError: 72 | pass 73 | voltages=literal_eval(summaryfile.get('current','maxvoltages')) 74 | vprint = '' 75 | for i in range(numcells+1): 76 | vprint = vprint + str(i+1) + '=' + str(voltages[i]).ljust(5,'0') + ', ' 77 | print(vprint) 78 | print(config['calibrate']['delta']) 79 | 80 | def main(): 81 | print('Usage: Pressing just the [Enter] key will display the latest voltage readings, type ^C to exit') 82 | print(' Type in the cell # that you want to calibrate followed by [Enter]') 83 | print(' Then type in the mesured voltage for that cell followed by [Enter]') 84 | print() 85 | print(' Make sure the Battery Monitoring software is running otherwise the summary file will not be current') 86 | print(' This program uses the data from the log file generated by the Battery Monitoring software') 87 | print(' Make sure that the Battery Monitoring software is logging to a real file and not /dev/null') 88 | print(' This program averages the data from the log file for the last 100 seconds.') 89 | print(' To make sure that the calibration is accurate make sure the input voltages are not varying') 90 | avvolts=[] 91 | while True: 92 | try: 93 | loadconfig() 94 | avvolts=avv() 95 | # time.sleep(60.0) 96 | what = input(">") 97 | if len(what)>0: 98 | realvolts = eval(input("Cell " + what + " reading ")) 99 | what=int(what) 100 | config['calibrate']['delta'][what-1] = round(avvolts[what-1]-realvolts+config['calibrate']['delta'][what-1],3) 101 | batconfigdata=SafeConfigParser() 102 | batconfigdata.read('battery.cfg') 103 | batconfigdata.set('calibrate','delta',str(config['calibrate']['delta'])) 104 | with open('battery.cfg', 'w') as batconfig: 105 | batconfigdata.write(batconfig) 106 | batconfig.closed 107 | 108 | 109 | except KeyboardInterrupt: 110 | break 111 | -------------------------------------------------------------------------------- /calvfineall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor store summary data summary.py***** 3 | # Copyright (C) 2014 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | import sys 18 | from os import rename 19 | import time 20 | from shutil import copy as filecopy 21 | from copy import deepcopy 22 | from ast import literal_eval 23 | from configparser import SafeConfigParser 24 | from config import config,loadconfig 25 | numcells = config['battery']['numcells'] 26 | voltages = [] 27 | avv = [] 28 | 29 | summaryfile = SafeConfigParser() 30 | 31 | def tail(file, n=1, bs=1024): 32 | f = open(file,'rb') 33 | f.seek(-1,2) 34 | l = 1-f.read(1).decode('utf-8').count('\n') # If file doesn't end in \n, count it anyway. 35 | B = f.tell() 36 | while n >= l and B > 0: 37 | block = min(bs, B) 38 | B -= block 39 | f.seek(B, 0) 40 | x = f.read(block).decode('utf-8') 41 | l += x.count('\n') 42 | f.seek(B, 0) 43 | l = min(l,n) # discard first (incomplete) line if l > n 44 | lines = f.readlines()[-l:] 45 | f.close() 46 | return lines 47 | 48 | def avv(): 49 | vstr="" 50 | v=[0.0 for i in range(numcells)] 51 | endlog=tail(config['files']['logfile'],11) 52 | for j in range(numcells): 53 | for i in range(10): 54 | x=float(endlog[i][j*6+15:j*6+20]) 55 | v[j]=v[j]+x 56 | for i in range(len(v)): 57 | v[i]=v[i]/10 58 | vstr=vstr+str(i+1) + "=" +str(round(v[i],3)).ljust(5,'0') + ", " 59 | print(vstr) 60 | print(config['calibrate']['delta']) 61 | return v 62 | 63 | 64 | 65 | def getv(): 66 | global voltages 67 | try: 68 | summaryfile.read(config['files']['summaryfile']) 69 | except IOError: 70 | pass 71 | voltages=literal_eval(summaryfile.get('current','maxvoltages')) 72 | vprint = '' 73 | for i in range(numcells+1): 74 | vprint = vprint + str(i+1) + '=' + str(voltages[i]).ljust(5,'0') + ', ' 75 | print(vprint) 76 | print(config['calibrate']['delta']) 77 | 78 | def main(): 79 | print('Usage: Pressing just the [Enter] key will display the latest voltage readings, type ^C to exit') 80 | print(' Press any other key to change the voltage calibration of all the voltage readings') 81 | print() 82 | print(' Make sure the Battery Monitoring software is running otherwise the summary file will not be current') 83 | print(' This program uses the data from the log file generated by the Battery Monitoring software') 84 | print(' Make sure that the Battery Monitoring software is logging to a real file and not /dev/null') 85 | print(' This program averages the data from the log file for the last 100 seconds.') 86 | print(' To make sure that the calibration is accurate make sure the input voltages are not varying') 87 | 88 | 89 | avvolts=[] 90 | while True: 91 | try: 92 | 93 | loadconfig() 94 | avvolts=avv() 95 | # time.sleep(60.0) 96 | what = input(">") 97 | if len(what)>0: 98 | realvolts = eval(input("All Cell readings ")) 99 | for i in range(numcells): 100 | config['calibrate']['delta'][i] = round(avvolts[i]-realvolts+config['calibrate']['delta'][i],3) 101 | print(config['calibrate']['delta']) 102 | batconfigdata=SafeConfigParser() 103 | batconfigdata.read('battery.cfg') 104 | batconfigdata.set('calibrate','delta',str(config['calibrate']['delta'])) 105 | with open('battery.cfg', 'w') as batconfig: 106 | batconfigdata.write(batconfig) 107 | batconfig.closed 108 | 109 | 110 | except KeyboardInterrupt: 111 | break 112 | 113 | if __name__ == "__main__": 114 | main() 115 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # *****BatteryMonitor parse Config data from battery config file***** 2 | # Copyright (C) 2014 Simon Richard Matthews 3 | # Project loaction https://github.com/simat/BatteryMonitor 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # any later version. 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # You should have received a copy of the GNU General Public License along 13 | # with this program; if not, write to the Free Software Foundation, Inc., 14 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 15 | 16 | from ast import literal_eval 17 | from configparser import SafeConfigParser 18 | 19 | def editbatconfig(section,item,itemvalue): 20 | """Change item in section in battery.cfg""" 21 | 22 | batconfigdata=SafeConfigParser() 23 | batconfigdata.read('battery.cfg') 24 | batconfigdata.set(section,item,itemvalue) 25 | with open('battery.cfg', 'w') as batconfig: 26 | batconfigdata.write(batconfig) 27 | batconfig.closed 28 | 29 | config ={} 30 | def loadconfig(): 31 | configfile = SafeConfigParser() 32 | configfile.read('battery.cfg') 33 | for section in configfile.sections(): 34 | config[section] = {} 35 | for key, val in configfile.items(section): 36 | config[section][key] = literal_eval(val) 37 | 38 | loadconfig() 39 | -------------------------------------------------------------------------------- /demandmanager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor Getdata from battery cells getdata.py***** 3 | # Copyright (C) 2020 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | from config import config 18 | from summary import summary 19 | IBatZero = 0.0 # 'Zero' battery current flow to calculate available power 20 | minmaxdemandpwr = [0,0] 21 | trackingsoc = -1.0 # target SOC if tracking battery SOC up and down, min=targetsoc 22 | 23 | def initdemand(initsoc): 24 | """Initialise trackingsoc, must be called after initial SOC determined""" 25 | 26 | global trackingsoc 27 | trackingsoc = max(config['battery']['targetsoc'],initsoc+config['battery']['minsocdif']) 28 | 29 | def solaravailable(batdata): 30 | """Returns max amount of surpus solar energy available without using battery Power 31 | Calculates the difference between amount of power being consumed vesus 32 | power that could be consumed to get battery current=IBatZero""" 33 | 34 | global minmaxdemandpwr, trackingsoc 35 | ibatminuteav=batdata.ibatminute/batdata.ibatnuminmin 36 | batdata.ibatminute = 0.0 37 | batdata.ibatnuminmin = 0 38 | 39 | soc=1-batdata.socadj/config['battery']['capacity'] 40 | 41 | # calcualte power available as current I plus amount to get SOC to target in one minute 42 | iavailable=60*((1-trackingsoc)*config['battery']['capacity']-batdata.socadj)\ 43 | -batdata.currentav[-3] 44 | pwravailable=iavailable*batdata.batvoltsav[config['battery']['numcells']+1] 45 | 46 | if config['battery']['tracksoc']: 47 | if soc < (trackingsoc - config['battery']['minsocdif'] - 0.01)\ 48 | and not config['battery']['trackdown'] or\ 49 | (soc < (trackingsoc - 0.01) and config['battery']['trackdown']): 50 | trackingsoc = max(config['battery']['targetsoc'],trackingsoc-0.01) 51 | elif soc>trackingsoc +config['battery']['minsocdif']: 52 | trackingsoc += 0.01 53 | else: 54 | trackingsoc=config['battery']['targetsoc'] 55 | 56 | if soc<(trackingsoc - config['battery']['minsocdif']) 57 | or (config['DemandManager']['float?'] and 58 | summary['current']['state'][0]<>'Float'): 59 | minmaxdemandpwr[1]=0 60 | elif soc>=trackingsoc: 61 | minmaxdemandpwr[1]=min(config['DemandManager']['maxdemandpwr'],\ 62 | config['Inverters']['ratedoutput']*(batdata.pip.numinvon-1)\ 63 | +config['Inverters']['turnonslave']+500) 64 | # print ("ibat {} iavailable {} soc {} sodadj {} pwravail {} minmax {} trackSOC {}"\ 65 | # .format(batdata.currentav[-3],iavailable,soc,batdata.socadj,pwravailable,minmaxdemandpwr,trackingsoc)) 66 | return pwravailable,minmaxdemandpwr 67 | -------------------------------------------------------------------------------- /doc/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /filter.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simat/BatteryMonitor/3e0ed06ef18793e6b06a1bcbabe8e200b344de27/filter.pdf -------------------------------------------------------------------------------- /getatod.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor Getdata from battery cells getdata.py***** 3 | # Copyright (C) 2017 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | 18 | from config import config 19 | #import smbus 20 | #from Adafruit_I2C import Adafruit_I2C 21 | import Adafruit_ADS1x15 22 | for i in config['AtoDs']: 23 | exec(i + '=' + config['AtoDs'][i]) 24 | 25 | class Raw: 26 | # compile analog capture code to save CPU time 27 | vin = [] 28 | for i in sorted(config['VoltageInputs']): 29 | vin = vin + [compile(config['VoltageInputs'][i], '', 'eval')] 30 | # vin = vin + [config['VoltageInputs'][i]] 31 | #for i in config['CurrentInputs']: 32 | # config['CurrentInputs'][i] = compile(config['CurrentInputs'][i], '', 'eval') 33 | iin = [] 34 | for i in sorted(config['CurrentInputs']): 35 | iin = iin + [compile(config['CurrentInputs'][i], '', 'eval')] 36 | 37 | rawi = [0.0 for i in iin] 38 | rawv = [ 0.0 for i in range(len(vin)+1)] 39 | 40 | def x(self): 41 | """ Get data for A/Ds, calibrate, covert and place in list variables""" 42 | 43 | for i in range(len(self.vin)): 44 | self.rawv[i+1] = eval(self.vin[i])/1000 # A to D 1 to 4 in volts 45 | for i in range(len(self.iin)): 46 | self.rawi[i] = eval(self.iin[i]) 47 | -------------------------------------------------------------------------------- /getbms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor Getdata from battery cells getdata.py***** 3 | # Copyright (C) 2017 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | import time 18 | import serial 19 | import binascii 20 | from config import config 21 | numcells = config['battery']['numcells'] 22 | 23 | class Raw: 24 | # compile analog capture code to save CPU time 25 | vin = [] 26 | for i in sorted(config['VoltageInputs']): 27 | vin = vin + [compile(config['VoltageInputs'][i], '', 'eval')] 28 | # vin = vin + [config['VoltageInputs'][i]] 29 | #for i in config['CurrentInputs']: 30 | # config['CurrentInputs'][i] = compile(config['CurrentInputs'][i], '', 'eval') 31 | iin = [] 32 | for i in sorted(config['CurrentInputs']): 33 | iin = iin + [compile(config['CurrentInputs'][i], '', 'eval')] 34 | 35 | line1 = [ 0 for i in range(20)] 36 | rawi = [0.0, 0.0, 0.0] 37 | rawv = [ 0.0 for i in range(numcells+1)] 38 | ser = serial.Serial(config['files']['usbport']) # open serial port 39 | ser.timeout = 3 40 | 41 | 42 | def getbmsdat(self,port,command): 43 | """ Issue BMS command and return data as byte data """ 44 | """ assumes data port is open and configured """ 45 | for i in range(5): 46 | try: 47 | port.write(command) 48 | reply = port.read(4) 49 | # raise serial.serialutil.SerialException('hithere') 50 | break 51 | except serial.serialutil.SerialException as err: 52 | errfile=open(config['files']['errfile'],'at') 53 | errfile.write(time.strftime("%Y%m%d%H%M%S ", time.localtime())+str(err.args)+'\n') 54 | errfile.close() 55 | 56 | # print (reply) 57 | x = int.from_bytes(reply[3:5], byteorder = 'big') 58 | # print (x) 59 | data = port.read(x) 60 | end = port.read(3) 61 | # print (data) 62 | return data 63 | 64 | def x(self): 65 | """ Get data from BMS board""" 66 | command = bytes.fromhex('DD A5 03 00 FF FD 77') 67 | dat = self.getbmsdat(self.ser,command) 68 | self.rawi[0] = int.from_bytes(dat[2:4], byteorder = 'big',signed=True) 69 | # print (self.rawi) 70 | # self.line1 = [ 0 for i in range(int(len(dat)))] 71 | # for i in range(0,int(len(dat))): 72 | # print (dat[i*2:i*2+2]) 73 | # print (int.from_bytes(dat[i:i+1], byteorder = 'big')) 74 | # self.line1[i] = int.from_bytes(dat[i:i+1], byteorder = 'big') 75 | # print (binascii.hexlify(dat)) 76 | # print (self.line1) 77 | 78 | 79 | # voltages 80 | command = bytes.fromhex('DD A5 04 00 FF FC 77') 81 | voltages = self.getbmsdat(self.ser,command) 82 | for i in range(0,numcells): 83 | self.rawv[i+1] = int.from_bytes(voltages[i*2:i*2+2], byteorder = 'big')\ 84 | /1000.00 85 | self.rawv[i+1] = self.rawv[i+1]+self.rawv[i] 86 | # print (self.rawv) 87 | # print (binascii.hexlify(voltages)) 88 | -------------------------------------------------------------------------------- /getdata.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/python 3 | # *****BatteryMonitor Getdata from battery cells getdata.py***** 4 | # Copyright (C) 2014 Simon Richard Matthews 5 | # Project loaction https://github.com/simat/BatteryMonitor 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # any later version. 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # You should have received a copy of the GNU General Public License along 15 | # with this program; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | #from test import config 19 | import re 20 | from config import config 21 | numcells = config['battery']['numcells'] 22 | import time 23 | #from x import Raw 24 | class Readings: 25 | """ get and manipulates readings from the real world""" 26 | 27 | 28 | def __init__(self): 29 | self.measured = config['calibrate']['measured'] 30 | self.displayed = config['calibrate']['displayed'] 31 | 32 | self.ratio = [ 1.0 for i in range(len(self.measured))] 33 | for i in range(1, len(self.measured)): 34 | self.ratio[i] = self.measured[i]/self.displayed[i] 35 | self.calvolts = [ 0.0 for i in range(len(self.measured))] 36 | 37 | # calvolts = [measureddelta[i] - displayeddelta[i] for i in range(numcells+1)] 38 | self.voltsav = [ 3.25 for i in range(len(self.measured)+1)] 39 | self.voltsav[0] = 0.0 40 | self.rawvolts = [ 0.0 for i in range(len(self.measured))] 41 | self.batvolts = [ i*3.25 for i in range(len(self.measured))] 42 | self.uncalvolts = [ i*3.25 for i in range(len(self.measured))] 43 | self.batvoltsav = [ i*3.25 for i in range(len(self.measured))] 44 | self.balflg = [ 0.0 for i in range(numcells)] 45 | self.baltime = [ 0.0 for i in range(numcells)] 46 | self.temp = [ 0.0 for i in range(len(config['TemperatureInputs']))] 47 | self.numiins = len(config['CurrentInputs']) 48 | self.current = [ 0.0 for i in range(self.numiins)] 49 | self.currentav = [ 0.0 for i in range(self.numiins)] 50 | self.ibatminute = 0.0 #sum of last minutes battery current readings 51 | self.ibatnuminmin = 0 # numvber of samples in last minute 52 | self.kWhin = [ 0.0 for i in range(self.numiins)] 53 | self.kWhout = [ 0.0 for i in range(self.numiins)] 54 | self.chargestates = [ b'00' for i in range(len(config['Status']))] 55 | self.pwravailable = 0.0 # amount of excess solar power available 56 | self.minmaxdemandpwr = [0.0,0.0] # min and max amount of excess power 57 | self.batcapresidual = config['battery']['capacity']*\ 58 | (1-(int(time.strftime("%Y", time.localtime()))\ 59 | -config['battery']['yearinstalled'])\ 60 | *(config['battery']['lossperyear']/100)) # residual battery capacity factor 61 | # rawcurrent = 0.0 62 | # batcurrent = 0.0 63 | # batcurrentav = 0.0 64 | # rawincurrent = 0.0 65 | # incurrent = 0.0 66 | # incurrentav = 0.0 67 | self.soc = 0.0 68 | self.socadj = 0.0 69 | self.batah = 0.0 70 | self.batahadj = 0.0 71 | self.inah = 0.0 72 | self.inahtot = 0.0 73 | self.ah = 0.0 74 | self.prevbatvoltage = 0.0 75 | 76 | self.mincellv=100.0 77 | self.maxcellv=0.0 78 | self.lastmaxv = 0.0 # previous sample maximum cell voltage 79 | self.lastminv = 100.0 # previous sample minimum cell voltage 80 | self.pwrbat = 0.0 # battery power units kW 81 | self.pwrbattot = 0.0 # total battery power units kWh 82 | self.pwrin = 0.0 # gross power in units kW 83 | self.pwrintot = 0.0 # total gross power in units kWh 84 | self.iall="" 85 | self.vdelta="" 86 | self.vcells="" 87 | self.soctxt="" 88 | self.socadjtxt="" 89 | self.sampleshr=3600/config['sampling']['sampletime'] 90 | self.interfacesinuse=[]# numder of sample / hour 91 | batpwr1hrav = 0.0 92 | 93 | for i in config['Interfaces']: 94 | interface=re.match(r'\w*',config['Interfaces'][i]).group() 95 | snstring=re.compile(r'[(].*[^)]') 96 | sn=snstring.search(config['Interfaces'][i]) 97 | if sn!=None: 98 | sn=sn.group() 99 | exec("import " + interface) 100 | if sn==None: 101 | # exec('self.'+i +'='+interface+'.Rawdat(self.interfacesinuse)') 102 | exec('self.{}={}.Rawdat({})'.format(i,interface,self.interfacesinuse)) 103 | else: 104 | # exec('self.'+i+'='+interface+".Rawdat('"+str(sn[1:])+"self.interfacesinuse')") 105 | exec('self.{}={}.Rawdat("{}",{})'.format(i,interface,sn[1:],self.interfacesinuse)) 106 | self.vin = [] 107 | for i in sorted(config['VoltageInputs']): 108 | self.vin = self.vin + [config['VoltageInputs'][i]] 109 | # self.vin = self.vin + [compile('self.'+config['VoltageInputs'][i], '', 'eval')] 110 | self.iin = [] 111 | for i in sorted(config['CurrentInputs']): 112 | self.iin = self.iin + [config['CurrentInputs'][i]] 113 | # self.iin = self.iin + [compile(config['CurrentInputs'][i], '', 'eval')] 114 | 115 | self.tin = [] # temperatures 116 | for i in sorted(config['TemperatureInputs']): 117 | self.tin = self.tin + [config['TemperatureInputs'][i]] 118 | 119 | self.balf = [] # balance flags 120 | for i in sorted(config['BalanceFlags']): 121 | self.balf = self.balf + [config['BalanceFlags'][i]] 122 | 123 | self.chgstat = [] # balance flags 124 | for i in sorted(config['Status']): 125 | self.chgstat = self.chgstat + [config['Status'][i]] 126 | 127 | self.sampletime = time.time() 128 | 129 | self.getvi() 130 | self.batvoltsav = self.batvolts 131 | self.batcurrentav = self.current[-3] 132 | self.incurrentav = self.current[-2] 133 | self.batpwr1hrav = self.batvoltsav[-1]*self.batcurrentav/1000 134 | for i in range(0,self.numiins): 135 | self.currentav[i] = self.current[i] 136 | # (self.batvoltsav, self.current) 137 | 138 | def getvi(self): 139 | """ Get raw data """ 140 | self.oldsampletime=self.sampletime 141 | sleeptime = max(config['sampling']['sampletime'] - (time.time()-self.oldsampletime), 0.0) 142 | # sleeptime 143 | time.sleep(sleeptime) 144 | self.sampletime = time.time() 145 | # get data from Interfaces 146 | for i in config['Interfaces']: 147 | exec('self.'+i+".getdata()") 148 | for i in range(len(self.iin)): 149 | self.current[i] = eval(self.iin[i]) \ 150 | *config['calibrate']['currentgain'][i] \ 151 | -config['calibrate']['currentoffset'][i] 152 | # self.batvolts[0] = self.rawdata.rawv[0] 153 | # self.uncalvolts[0] = self.rawdata.rawv[0] 154 | # self.batvolts[0] = 0.0 155 | # self.uncalvolts[0] = 0.0 156 | 157 | for i in range(len(self.vin)): 158 | self.uncalvolts[i+1] = eval(self.vin[i]) \ 159 | *config['calibrate']['batvgain'] # A/D to battery volts 160 | self.batvolts[i+1] = self.uncalvolts[i+1]*self.ratio[i] # calibrate values 161 | 162 | for i in range(len(self.tin)): # get temperatures 163 | self.temp[i] = eval(self.tin[i]) 164 | 165 | for i in range(numcells): # get balance flags 166 | self.balflg[i] = eval(self.balf[i]) 167 | 168 | for i in range(len(self.chgstat)): # get PIP charge states 169 | self.chargestates[i]=eval(self.chgstat[i]) 170 | 171 | def getraw(self): 172 | """ gets battery data, do averaging, voltage results in volts, current in amps""" 173 | self.getvi() 174 | samplesav = config['sampling']['samplesav'] 175 | self.deltatime=(self.sampletime-self.oldsampletime)/3600 176 | self.batah = self.currentav[-3]*self.deltatime 177 | self.batahadj = (self.currentav[-3]+config['battery']['ahloss'])*self.deltatime 178 | self.inah = self.currentav[-2]*self.deltatime 179 | for i in range(1,len(self.measured)): 180 | self.batvoltsav[i] = (self.batvoltsav[i]*(samplesav-1) \ 181 | + self.batvolts[i])/samplesav 182 | batvoltsav = self.batvoltsav[config['battery']['numcells']+1] 183 | self.pwrin = self.inah*batvoltsav/1000 # gross input energy 184 | self.pwrbat = self.batah*batvoltsav/1000 # battery energy in/out 185 | self.batpwr1hrav = self.batpwr1hrav \ 186 | +1.2*(self.currentav[-3]*batvoltsav/1000-self.batpwr1hrav)/self.sampleshr # caculate battery power 1hr running av in kW 187 | self.ibatminute = self.ibatminute+self.currentav[-3] 188 | self.ibatnuminmin += 1 189 | for i in range(0,self.numiins): 190 | self.currentav[i] = (self.currentav[i]*(samplesav-1)+self.current[i])/samplesav # running av current 191 | if self.currentav[i] < 0: 192 | self.kWhin[i] = self.kWhin[i]+self.currentav[i]*self.deltatime*batvoltsav/1000 193 | else: 194 | self.kWhout[i] = self.kWhout[i]+self.currentav[i]*self.deltatime*batvoltsav/1000 195 | 196 | for i in range(numcells+1): 197 | if self.balflg[i-1]: 198 | self.baltime[i-1]= self.baltime[i-1]+self.deltatime # update time balancers are on 199 | 200 | self.voltsav[0]=round(self.batvolts[0],3) 201 | self.lastmincellv=self.mincellv 202 | self.lastmaxcellv=self.maxcellv 203 | self.mincellv=100.0 204 | self.maxcellv=0.0 205 | for i in range(numcells,0,-1): # caculate cell voltages 206 | self.voltsav[i]=round((self.batvoltsav[i]-self.batvoltsav[i-1]-config['calibrate']['delta'][i-1]),3) 207 | self.mincellv = min(self.voltsav[i],self.mincellv) 208 | self.maxcellv = max(self.voltsav[i],self.maxcellv) 209 | 210 | for i in range(numcells+1,len(self.measured)): # do other voltages 211 | self.voltsav[i]=round(self.batvoltsav[i],2) 212 | -------------------------------------------------------------------------------- /html/SampleBatday.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |

8
3.269V
3.244V
3.269V
3.049V

9
3.262V
3.237V
3.262V
3.04V

10
3.268V
3.241V
3.268V
3.073V

11
3.265V
3.247V
3.265V
3.048V

12
3.246V
3.234V
3.246V
3.047V

13
3.24V
3.23V
3.24V
3.029V

14
3.237V
3.227V
3.237V
3.032V

15
3.237V
3.228V
3.237V
3.045V

7
3.264V
3.24V
3.264V
3.04V

6
3.274V
3.245V
3.274V
3.044V

5
3.266V
3.241V
3.266V
3.037V

4
3.268V
3.242V
3.268V
3.05V

3
3.266V
3.238V
3.266V
3.005V

2
3.262V
3.24V
3.262V
3.052V

1
3.263V
3.238V
3.263V
3.053V

96 |
97 |
98 |

99 | 15%

100 |

-14.5A

101 |

In -16.5A Out 2.0A

102 |

188.06Ah

103 |

48.89V

104 |

0.037V

105 |

TBat 12.7C TFet 17.2C

106 |
107 | 108 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /html/SampleBattery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Battery Stats

6 | [current] 7 |
timestamp = '20180601115708 ' 8 |
maxvoltages = [3.267, 3.267, 3.27, 3.273, 3.27, 3.278, 3.268, 3.273, 3.267, 3.273, 3.27, 3.25, 3.245, 3.241, 3.241, 48.95] 9 |
maxnocharge = [3.233, 3.236, 3.233, 3.237, 3.235, 3.24, 3.234, 3.237, 3.233, 3.236, 3.242, 3.232, 3.228, 3.226, 3.227, 48.51] 10 |
minnoload = [3.267, 3.267, 3.27, 3.273, 3.27, 3.278, 3.268, 3.273, 3.267, 3.273, 3.27, 3.25, 3.245, 3.241, 3.241, 48.95] 11 |
minvoltages = [3.267, 3.267, 3.27, 3.273, 3.27, 3.278, 3.268, 3.273, 3.267, 3.273, 3.27, 3.25, 3.245, 3.241, 3.241, 48.95] 12 |
deltav = [0.037, 0.037, 0.037] 13 |
ioutmax = [-14.3, -16.5, 2.2] 14 |
kwoutmax = [0.091, 0.014, 0.108] 15 |
kwhout = [0.0, 0.0, 0.002082] 16 |
iinmax = [-14.3, -16.5, 2.2] 17 |
kwinmax = [-0.7, -0.808, -0.004] 18 |
kwhin = [-0.011716, -0.013799, 0.0] 19 |
ah = [185.38, 185.38, 185.38, 0.0, -0.24, 0.0, -0.29] 20 |
dod = [185.91, 185.91, 185.91, -100] 21 |
power = [-0.011659, 0.0, -0.013962, 0.002303] 22 |
tmax = [17.0, 12.6] 23 |
tmin = [17.0, 12.6] 24 |
baltime = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 25 |
26 |
[hour] 27 |
timestamp = '20180601115708 ' 28 |
maxvoltages = [3.267, 3.267, 3.27, 3.273, 3.27, 3.278, 3.268, 3.273, 3.267, 3.273, 3.27, 3.25, 3.245, 3.241, 3.241, 48.95] 29 |
maxnocharge = [3.233, 3.236, 3.233, 3.237, 3.235, 3.24, 3.234, 3.237, 3.233, 3.236, 3.242, 3.232, 3.228, 3.226, 3.227, 48.51] 30 |
minnoload = [3.246, 3.246, 3.247, 3.25, 3.249, 3.253, 3.245, 3.25, 3.244, 3.247, 3.25, 3.226, 3.221, 3.219, 3.218, 48.62] 31 |
minvoltages = [3.246, 3.246, 3.247, 3.25, 3.249, 3.253, 3.245, 3.25, 3.244, 3.247, 3.25, 3.226, 3.221, 3.219, 3.218, 48.62] 32 |
deltav = [0.033, 0.039, 0.039] 33 |
ioutmax = [-11.5, -16.5, 6.0] 34 |
kwoutmax = [0.091, 0.014, 0.294] 35 |
kwhout = [0.0, 0.0, 0.18453] 36 |
iinmax = [-15.7, -17.7, 0.8] 37 |
kwinmax = [-0.765, -0.866, -0.004] 38 |
kwhin = [-0.6171, -0.80159, 0.0] 39 |
ah = [185.38, 191.40772, 197.79, 57.0, -12.66, 0.0, -16.57] 40 |
dod = [185.91, 191.877195, 198.19, -100] 41 |
power = [-0.617277, 0.0, -0.80161, 0.184333] 42 |
tmax = [17.2, 12.9] 43 |
tmin = [15.2, 12.6] 44 |
baltime = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 45 |
46 |
[currentday] 47 |
timestamp = '20180601115708 ' 48 |
maxvoltages = [3.267, 3.267, 3.27, 3.273, 3.27, 3.278, 3.268, 3.273, 3.267, 3.273, 3.27, 3.25, 3.245, 3.241, 3.241, 48.95] 49 |
maxnocharge = [3.238, 3.24, 3.238, 3.242, 3.241, 3.245, 3.24, 3.244, 3.237, 3.241, 3.247, 3.234, 3.23, 3.227, 3.228, 48.57] 50 |
minnoload = [3.053, 3.052, 3.005, 3.05, 3.037, 3.044, 3.04, 3.049, 3.04, 3.073, 3.048, 3.047, 3.029, 3.032, 3.045, 45.64] 51 |
minvoltages = [3.053, 3.052, 3.005, 3.05, 3.037, 3.044, 3.04, 3.049, 3.04, 3.073, 3.048, 3.047, 3.029, 3.032, 3.045, 45.64] 52 |
deltav = [0.01, 0.075, 0.075] 53 |
ioutmax = [4.8, 0.3, 6.0] 54 |
kwoutmax = [0.227, 0.014, 0.294] 55 |
kwhout = [0.39433, 0.11102, 0.66817] 56 |
iinmax = [-18.9, -20.1, -1.0] 57 |
kwinmax = [-0.916, -0.978, -0.048] 58 |
kwhin = [-1.22646, -1.60695, -0.00428] 59 |
ah = [160.75, 177.684557, 210.5, 716.0, -25.3, 10.28, -29.37] 60 |
dod = [171.3, 185.047446, 210.6, -98] 61 |
power = [-1.222535, 0.39212, -1.494031, 0.663616] 62 |
tmax = [17.2, 13.4] 63 |
tmin = [11.2, 10.4] 64 |
baltime = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 65 |
66 |
[prevday] 67 |
timestamp = 20180601000003 68 |
maxvoltages = [3.268, 3.272, 3.263, 3.268, 3.267, 3.271, 3.264, 3.269, 3.265, 3.27, 3.269, 3.262, 3.262, 3.26, 3.261, 48.94] 69 |
maxnocharge = [3.268, 3.272, 3.263, 3.268, 3.267, 3.271, 3.264, 3.269, 3.265, 3.27, 3.269, 3.262, 3.262, 3.26, 3.261, 48.94] 70 |
minnoload = [3.173, 3.174, 3.162, 3.17, 3.17, 3.168, 3.168, 3.169, 3.17, 3.172, 3.165, 3.143, 3.145, 3.145, 3.139, 47.49] 71 |
minvoltages = [3.144, 3.144, 3.11, 3.122, 3.124, 3.109, 3.113, 3.111, 3.123, 3.124, 3.12, 3.093, 3.106, 3.108, 3.101, 46.75] 72 |
deltav = [0.005, 0.044, 0.06] 73 |
ioutmax = [61.5, 0.3, 64.8] 74 |
kwoutmax = [2.887, 0.015, 3.042] 75 |
kwhout = [2.2303, 0.22889, 2.6666] 76 |
iinmax = [-11.3, -14.1, -0.6] 77 |
kwinmax = [-0.549, -0.682, -0.059] 78 |
kwhin = [-0.68826, -1.35227, -0.00128] 79 |
ah = [128.38, 142.787738, 160.75, 1440.0, -13.94, 48.18, -20.07] 80 |
dod = [135.81, 151.773093, 171.3, -99] 81 |
power = [-0.670102, 2.21372, -1.122085, 2.665703] 82 |
tmax = [22.8, 16.4] 83 |
tmin = [12.2, 12.4] 84 |
baltime = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 85 |
86 |
[monthtodate] 87 |
timestamp = '20180601115708 ' 88 |
maxvoltages = [3.267, 3.267, 3.27, 3.273, 3.27, 3.278, 3.268, 3.273, 3.267, 3.273, 3.27, 3.25, 3.245, 3.241, 3.241, 48.95] 89 |
maxnocharge = [3.238, 3.24, 3.238, 3.242, 3.241, 3.245, 3.24, 3.244, 3.237, 3.241, 3.247, 3.234, 3.23, 3.227, 3.228, 48.57] 90 |
minnoload = [3.053, 3.052, 3.005, 3.05, 3.037, 3.044, 3.04, 3.049, 3.04, 3.073, 3.048, 3.047, 3.029, 3.032, 3.045, 45.64] 91 |
minvoltages = [3.053, 3.052, 3.005, 3.05, 3.037, 3.044, 3.04, 3.049, 3.04, 3.073, 3.048, 3.047, 3.029, 3.032, 3.045, 45.64] 92 |
deltav = [0.01, 0.075, 0.075] 93 |
ioutmax = [4.8, 0.3, 6.0] 94 |
kwoutmax = [0.227, 0.014, 0.294] 95 |
kwhout = [0.39433, 0.11102, 0.66817] 96 |
iinmax = [-18.9, -20.1, -1.0] 97 |
kwinmax = [-0.916, -0.978, -0.048] 98 |
kwhin = [-1.22646, -1.60695, -0.00428] 99 |
ah = [160.75, 177.684557, 210.5, 716.0, -25.3, 10.28, -29.37] 100 |
dod = [171.3, 185.047446, 210.6, -98] 101 |
power = [-1.222535, 0.39212, -1.494031, 0.663616] 102 |
tmax = [17.2, 13.4] 103 |
tmin = [11.2, 10.4] 104 |
baltime = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 105 |
106 |
[yeartodate] 107 |
timestamp = '20180601115708 ' 108 |
maxvoltages = [3.482, 29.738, 3.656, 3.523, 3.792, 3.654, 3.472, 3.495, 3.464, 3.464, 3.602, 3.492, 3.772, 3.724, 3.674, 53.67] 109 |
maxnocharge = [3.482, 29.738, 3.656, 3.507, 3.792, 3.654, 3.459, 3.473, 3.463, 3.464, 3.563, 3.479, 3.772, 3.724, 3.66, 53.67] 110 |
minnoload = [2.951, 2.922, 2.848, 2.944, 2.927, 2.933, 2.91, -42.952, 2.931, 2.978, 2.939, 2.946, 2.903, 2.918, 2.921, 29.74] 111 |
minvoltages = [2.951, 2.922, 2.848, 2.944, 2.927, 2.923, 2.91, -42.952, 2.931, 2.977, 2.939, 2.939, 2.903, 2.918, 2.921, 29.74] 112 |
deltav = [0.0, 72.69, 72.69] 113 |
ioutmax = [93.1, 9.0, 96.4] 114 |
kwoutmax = [4.25, 0.448, 4.606] 115 |
kwhout = [86.68816, 9.8361, 111.12815] 116 |
iinmax = [-24.4, -25.9, -8.8] 117 |
kwinmax = [-1.22, -1.295, -0.45] 118 |
kwhin = [-98.49789, -134.11955, -2.03612] 119 |
ah = [-34.47, 53.398644, 220.0, 59505.0, -1945.88, 1797.52, -2387.72] 120 |
dod = [-25.3, 58.041585, 220.02, 3] 121 |
power = [-96.066522, 84.295106, -124.302816, 112.5314] 122 |
tmax = [43.1, 22.3] 123 |
tmin = [10.2, 10.4] 124 |
baltime = [0.0, 0.0306, 0.128, 0.0306, 0.1391, 0.0445, 0.0, 0.0, 0.0, 0.0, 0.1475, 0.0, 0.0529, 0.0279, 0.0613] 125 |
126 |
[alltime] 127 |
timestamp = '20180601115708 ' 128 |
maxvoltages = [3.482, 29.738, 3.656, 3.523, 3.792, 3.654, 3.472, 3.495, 3.464, 3.464, 3.602, 3.492, 3.772, 3.724, 3.674, 53.67] 129 |
maxnocharge = [3.482, 29.738, 3.656, 3.507, 3.792, 3.654, 3.459, 3.473, 3.463, 3.464, 3.563, 3.479, 3.772, 3.724, 3.66, 53.67] 130 |
minnoload = [2.951, 2.922, 2.848, 2.944, 2.927, 2.933, 2.91, -42.952, 2.931, 2.978, 2.939, 2.946, 2.903, 2.918, 2.921, 29.74] 131 |
minvoltages = [2.951, 2.922, 2.848, 2.944, 2.927, 2.923, 2.91, -42.952, 2.931, 2.977, 2.939, 2.939, 2.903, 2.918, 2.921, 29.74] 132 |
deltav = [0.0, 72.69, 72.69] 133 |
ioutmax = [93.1, 9.0, 96.4] 134 |
kwoutmax = [4.25, 0.448, 4.606] 135 |
kwhout = [86.68816, 9.8361, 111.12815] 136 |
iinmax = [-24.4, -25.9, -8.8] 137 |
kwinmax = [-1.22, -1.295, -0.45] 138 |
kwhin = [-98.49789, -134.11955, -2.03612] 139 |
ah = [-34.47, 53.398644, 220.0, 59505.0, -1945.88, 1797.52, -2387.72] 140 |
dod = [-25.3, 58.041585, 220.02, 3] 141 |
power = [-96.066522, 84.295106, -124.302816, 112.5314] 142 |
tmax = [43.1, 22.3] 143 |
tmin = [10.2, 10.4] 144 |
baltime = [0.0, 0.0306, 0.128, 0.0306, 0.1391, 0.0445, 0.0, 0.0, 0.0, 0.0, 0.1475, 0.0, 0.0529, 0.0279, 0.0613] 145 |
146 |

147 | 148 | -------------------------------------------------------------------------------- /html/bargraph.plot: -------------------------------------------------------------------------------- 1 | set terminal jpeg 2 | set output "plot.jpg" 3 | set xdata time 4 | set timefmt "%Y%m%d" 5 | set xrange ["20170301":"20170401"] 6 | set format x "%d/%m" 7 | set title "Daily Battery Energy in/out" 8 | set ylabel "Battery Energy (kWh)" 9 | #show title 10 | #show ylabel 11 | plot "plot" using 1:(-$2) title "Energy In" with histep,\ 12 | "plot" using 1:3 title "Energy Out" with histep 13 | 14 | -------------------------------------------------------------------------------- /html/batday.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Error: [$errno] $errstr"; 11 | } 12 | 13 | //set error handler 14 | set_error_handler("customError"); 15 | ?> 16 | 109 | 110 | "75") { 156 | $capcolour="green"; 157 | } elseif ($capacity > "50") { 158 | $capcolour="gold"; 159 | } elseif ($capacity > "25") { 160 | $capcolour="darkorange"; 161 | } else { 162 | $capcolour="red"; 163 | } 164 | $pos = strpos($summary,"[currentday]", $pos); 165 | $daymaxv = getdat("maxvoltages"); 166 | $daymaxnochargev = getdat("maxnocharge"); 167 | $dayminnoloadv = getdat("minnoload"); 168 | $dayminloadv = getdat("minvoltages"); 169 | $dayah = getdat("ah"); 170 | $power = getdat("power"); 171 | 172 | /*$numbercells = count($batvolts)-1;*/ 173 | $batwidth = $numbercells*38; 174 | $batcolour = array_fill(0,$numbercells,"yellow"); 175 | 176 | $minbatvolts = 100.0; 177 | $dayminmin = 100.0 ; 178 | $maxbatvolts = 0.0; 179 | $daymaxmax = 0.0; 180 | $dayminmax = 100.0 ; 181 | $daymaxnocharge = 0.0; 182 | $dayminnocharge = 100.0 ; 183 | $daymaxmin = 0.0; 184 | $dayminmin = 100.0 ; 185 | for ($x = 0; $x < $numbercells; $x++) { 186 | if ($batvolts[$x] >= $highv) { 187 | $batcolour[$x] = "red"; } 188 | if ($batvolts[$x] <= $lowv) { 189 | $batcolour[$x] = "fuchsia"; } 190 | $minbatvolts = min($minbatvolts,$batvolts[$x]); 191 | $maxbatvolts = max($maxbatvolts,$batvolts[$x]); 192 | $daymaxmax = max($daymaxmax,$daymaxv[$x]); 193 | $dayminmax = min($dayminmax,$daymaxv[$x]); 194 | $daymaxnocharge = max($daymaxnocharge,$daymaxnochargev[$x]); 195 | $dayminnocharge = min($dayminnocharge,$daymaxnochargev[$x]); 196 | $dayminv[$x] = min($dayminnoloadv[$x],$dayminloadv[$x]); 197 | $daymaxmin = max($daymaxmin,$dayminv[$x]); 198 | $dayminmin = min($dayminmin,$dayminv[$x]); 199 | } 200 | 201 | $batvoltstype = array_fill(0,$numbercells+1,"middle"); 202 | $batmaxtype = array_fill(0,$numbercells+1,"middle"); 203 | $batnochargetype = array_fill(0,$numbercells+1,"middle"); 204 | $batmintype = array_fill(0,$numbercells+1,"middle"); 205 | 206 | for ($x = 0; $x < $numbercells; $x++) { 207 | 208 | if ($batvolts[$x] == $maxbatvolts) { 209 | $batvoltstype[$x] = "highest"; } 210 | if ($batvolts[$x] == $minbatvolts) { 211 | $batvoltstype[$x] = "lowest"; } 212 | 213 | if ($daymaxv[$x] == $daymaxmax) { 214 | $batmaxtype[$x] = "highest"; } 215 | if ($daymaxv[$x] == $dayminmax) { 216 | $batmaxtype[$x] = "lowest"; } 217 | 218 | if ($daymaxnochargev[$x] == $daymaxnocharge) { 219 | $batnochargetype[$x] = "highest"; } 220 | if ($daymaxnochargev[$x] == $dayminnocharge) { 221 | $batnochargetype[$x] = "lowest"; } 222 | 223 | if ($dayminv[$x] == $daymaxmin) { 224 | $batmintype[$x] = "highest"; } 225 | if ($dayminv[$x] == $dayminmin) { 226 | $batmintype[$x] = "lowest"; } 227 | } 228 | 229 | /* 230 | $arrlength = count($batvoltstype); 231 | 232 | for($x = 0; $x < $arrlength; $x++) { 233 | echo ($x.$batvoltstype[$x]); 234 | } 235 | */ 236 | 237 | 238 | ?> 239 | 240 | 241 | 242 | 243 | 244 | 256 |
257 | 258 | 259 | 260 | '); 263 | echo ('

' . intval($x+1) . '
'); 264 | echo (' ' . $daymaxv[$x] . 'V
'); 265 | 266 | /* echo ($batmaxtype[$x]. $daymaxv[$x] ); */ 267 | 268 | echo (' ' . $daymaxnochargev[$x] . 'V
'); 269 | echo ('' . $batvolts[$x] .'V
'); 270 | echo (' ' . $dayminv[$x] . 'V
'); 271 | echo ('

'); 272 | } ?> 273 | 274 | 275 | = 0; $x--) { 276 | 277 | echo (''); 284 | } ?> 285 | 286 | 287 |
'); 278 | echo ('

' . intval($x+1) . '
'); 279 | echo (' ' . $daymaxv[$x] . 'V
'); 280 | echo (' ' . $daymaxnochargev[$x] . 'V
'); 281 | echo ('' . $batvolts[$x] .'V
'); 282 | echo (' ' . $dayminv[$x] . 'V
'); 283 | echo ('

288 |
289 |
290 |

291 |

292 |

293 |

294 |

295 |

"; 297 | } 298 | ?>

299 |

300 |

301 |

302 |

303 | 304 |
305 | 306 | 318 | 323 | 324 | 325 | 326 | -------------------------------------------------------------------------------- /html/battery.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Battery Stats

6 | "; 19 | } 20 | fclose($myfile); 21 | ?> 22 | 23 | 24 | -------------------------------------------------------------------------------- /html/excesspwr.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Excess Power 5 | 6 | 7 | 8 | 9 | Error: [$errno] $errstr"; 15 | } 16 | 17 | //set error handler 18 | set_error_handler("customError"); 19 | ?> 20 | 22 | 23 | 49 | 50 | 51 | 52 | 53 | 54 |

55 |

56 |

57 |

58 |

59 |

60 |

61 |

62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /html/strip.sh: -------------------------------------------------------------------------------- 1 | #! bash 2 | #Script to strip data from a period summary file and get into format for gnuplot 3 | 4 | # Usage: strip.sh infile outfile 5 | 6 | # infile is the period summary file generated by my battery monitor program 7 | # period could be hour, day, month or year 8 | # outfile is file to output the gnuplot ready data 9 | 10 | # The first command strips out datestamp and the 'power' information 11 | # The second command amagamates the datestamp and the 'power' information 12 | # The third command strips out the 2017 data 13 | # The forth command removes all the delimiters from the lines 14 | # The last command cuts the timestamp down to year,month and day and saves the file 15 | 16 | sed -n -e '/'timestamp'/p' -e "/'power'/p" $1 | \ 17 | paste -d ' ' - - | \ 18 | sed -n "/'2017/p" | \ 19 | sed -e "s/('timestamp',..'//" -e "s/'.).*('power',.'[[]//" -e "s/,//g" -e "s/[]]')//" | \ 20 | sed 's/\(2017....\)....../\1/' > $2 21 | -------------------------------------------------------------------------------- /logger.py: -------------------------------------------------------------------------------- 1 | # *****Logging Module logger.py***** 2 | # Copyright (C) 2014-2018 Simon Richard Matthews 3 | # Project loaction https://github.com/simat/BatteryMonitor 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # any later version. 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # You should have received a copy of the GNU General Public License along 13 | # with this program; if not, write to the Free Software Foundation, Inc., 14 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 15 | 16 | 17 | #!/usr/bin/python 18 | 19 | import logging 20 | #logging.config.fileConfig('battery.cfg') 21 | from config import loadconfig, config 22 | 23 | errfile=logging.FileHandler(config['files']['errfile']) 24 | errfile.setLevel(logging.DEBUG) 25 | alarmfile=logging.FileHandler(config['files']['alarmfile']) 26 | logfile=logging.FileHandler(config['files']['logfile']) 27 | 28 | 29 | # create formatter 30 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(filename)s,%(module)s,%(funcName)s,%(lineno)d') 31 | alarmformat = logging.Formatter('%(asctime)s - %(message)s') 32 | logformat = logging.Formatter('%(message)s') 33 | # add formatter to ch 34 | errfile.setFormatter(formatter) 35 | alarmfile.setFormatter(alarmformat) 36 | logfile.setFormatter(logformat) 37 | # add ch to logger 38 | -------------------------------------------------------------------------------- /mqtt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor MQTT client***** 3 | # Copyright (C) 2021 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | from paho.mqtt import client as mqtt_client 18 | from time import sleep 19 | from config import editbatconfig, config 20 | import logger 21 | log = logger.logging.getLogger(__name__) 22 | log.setLevel(logger.logging.DEBUG) 23 | log.addHandler(logger.errfile) 24 | 25 | 26 | def on_message(client,userata,message): 27 | """Executed when any subscribed MQTT message arives 28 | At present assumes payload is item to change in battery.config 29 | makes change and rewrites config file""" 30 | 31 | payload=eval(str(message.payload.decode("utf-8"))) 32 | print (payload) 33 | for section in payload: 34 | for item in payload[section]: 35 | # print (section,item,payload[section][item]) 36 | editbatconfig(section,item,payload[section][item]) 37 | 38 | def on_disconnect(client, userdata, rc): 39 | if rc: 40 | log.error('MQTT server disconnect, Restart MQTT') 41 | 42 | def connect_mqtt(): 43 | def on_connect(client, userdata, flags, rc): 44 | if rc == 0: 45 | print("Connected to MQTT Broker!") 46 | else: 47 | fail=f"Failed to connect, return code {rc}" 48 | print(fail) 49 | log.error(fail) 50 | 51 | client = mqtt_client.Client(config['mqtt']['clientname'],clean_session=False) 52 | if config['mqtt']['username']: 53 | client.username_pw_set(config['mqtt']['username'],config['mqtt']['password']) 54 | client.on_connect = on_connect 55 | client.connect(config['mqtt']['brokerip'],config['mqtt']['brokerport']) 56 | return client 57 | 58 | client=connect_mqtt() 59 | client.on_message=on_message 60 | client.on_disconnect=on_disconnect 61 | client.subscribe(config['mqtt']['subscribetopic'],qos=1) 62 | client.loop_start() 63 | 64 | def publish(topic,data): 65 | result = client.publish(topic, data) 66 | # result: [0, 1] 67 | status = result[0] 68 | if status == 0: 69 | print(f"Send `{data}` to topic `{topic}`") 70 | else: 71 | fail=f"MQQT Failed to send message to topic {topic}" 72 | print(fail) 73 | log.error(fail) 74 | 75 | 76 | def test(): 77 | client.loop_start() 78 | count =0 79 | while True: 80 | count+=1 81 | publish(client,'batpwr',data=f"SOC:{count},powerin:{count}") 82 | publish(client,'solarpwr',data=f"SOC:{count},powerin:{count}") 83 | publish(client,'loadpwr',data=f"SOC:{count},powerin:{count}") 84 | 85 | sleep(2) 86 | -------------------------------------------------------------------------------- /pi.py: -------------------------------------------------------------------------------- 1 | # *****BatteryMonitor main file batteries.py***** 2 | # Copyright (C) 2014 Simon Richard Matthews 3 | # Project loaction https://github.com/simat/BatteryMonitor 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # any later version. 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # You should have received a copy of the GNU General Public License along 13 | # with this program; if not, write to the Free Software Foundation, Inc., 14 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 15 | #!/usr/bin/python 16 | import RPi.GPIO as gpio 17 | import threading 18 | from time import sleep 19 | from time import time 20 | 21 | class Rawdat(): 22 | """class for using the raspberry Pi for IO""" 23 | 24 | savedinvertermap ={} 25 | def __init__(self,InterfacesInUse): 26 | self.gpio=gpio 27 | self.gpio.setmode(self.gpio.BOARD) 28 | self.gpio.setup(11, self.gpio.OUT, initial = 1) 29 | self.gpio.setup(13, self.gpio.OUT, initial = 0) 30 | 31 | def getdata(self): 32 | print (self.gpio.input(11),self.gpio.input(13)) 33 | 34 | def backgroundswapinv(self,on,off): 35 | print(on,off) 36 | self.gpio.output(on,1) 37 | sleep(20) # wait for second inverter to power up and syncronise 38 | self.gpio.output(off,0) 39 | 40 | def swapinverter(self,on,off): 41 | """turns on inverter controlled by Pi pin number if on arg and turns off 42 | inverter controlled by Pi pin number in off arg""" 43 | 44 | threading.Thread(target=self.backgroundswapinv,args=(self,on,off)).start() 45 | 46 | def allinvon(self,batdata,pins): 47 | """ turn on inverters in pins list, save current inverter map""" 48 | 49 | batdata.pip.timeoverload=time() 50 | for pin in pins: 51 | self.savedinvertermap[pin]=self.gpio.input(pin) 52 | self.gpio.output(pin,1) 53 | 54 | def restoreinverters(self): 55 | """ restore saved invertermap""" 56 | for pin in self.savedinvertermap: 57 | self.gpio.output(pin,self.savedinvertermap[pin]) 58 | -------------------------------------------------------------------------------- /pip.py: -------------------------------------------------------------------------------- 1 | # *****BatteryMonitor main file batteries.py***** 2 | # Copyright (C) 2014 Simon Richard Matthews 3 | # Project loaction https://github.com/simat/BatteryMonitor 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # any later version. 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # You should have received a copy of the GNU General Public License along 13 | # with this program; if not, write to the Free Software Foundation, Inc., 14 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 15 | 16 | 17 | #!/usr/bin/python 18 | import sys 19 | import time 20 | import serial 21 | import binascii 22 | import glob 23 | from threading import Thread 24 | from config import config 25 | numcells = config['battery']['numcells'] 26 | import logger 27 | log = logger.logging.getLogger(__name__) 28 | log.setLevel(logger.logging.DEBUG) 29 | log.addHandler(logger.errfile) 30 | initrawdat ={'DataValid':False,'BInI':0.0,'BOutI':0.0,'BV':0.0,'PVI':0.0,'PVW':0,\ 31 | 'ACW':0.0,'ibat':0.0,'ipv':0.0,'iload':0.0,'ChgStat':b'00'} 32 | 33 | 34 | 35 | class Rawdat(): 36 | """class for obtaining data from and controlling indidvdual PIP inverters. 37 | When class is instantiated the SN of the PIP is used to tie the instance of 38 | the class to the particular machine""" 39 | 40 | def __init__(self,sn,InterfacesInUse): 41 | self.rawdat = dict.copy(initrawdat) 42 | self.reply ='' # placeholder for reply from sendcmd 43 | self.pipdown=0.0 44 | self.sn=sn 45 | self.timeslaveon=0 # time slave inverter was turned on 46 | self.numinvon=1 # number of inverters currently on 47 | try: 48 | self.findpip(InterfacesInUse) 49 | except IOError as err: 50 | self.pipdown=time.time() # flag pip is down 51 | log.error(err) 52 | 53 | 54 | self.stashok=False 55 | self.floatv=48.0 56 | self.bulkv=48.0 57 | self.rechargev=48.0 58 | self.lowv=44.0 59 | self.stashok=False 60 | self.command=b'' # last command sent to PIP 61 | self.acloadav=0.0 62 | self.timeoverload=0.0 #time overload started 63 | self.time=0.0 64 | 65 | def findpip(self,InterfacesInUse): 66 | """Scan ports to find PIP port""" 67 | 68 | self.pipport="" 69 | for dev in glob.glob(config['Ports']['pipport']): 70 | if dev not in InterfacesInUse: 71 | # print(dev) 72 | for i in range(2): 73 | try: 74 | self.openpip(dev) 75 | self.sendcmd("QID") 76 | if self.reply[1:15].decode('ascii','strict')==str(self.sn): 77 | self.pipport=dev 78 | break 79 | except IOError: 80 | pass 81 | finally: 82 | self.port.close 83 | if self.pipport!="": 84 | break 85 | if self.pipport=="": 86 | raise IOError("Couldn't find PIP sn {}".format(self.sn)) 87 | 88 | def openpip(self,port): 89 | self.port = serial.Serial(port,baudrate=2400,timeout=1) # open serial port 90 | 91 | def crccalc(self,command): 92 | """returns crc as integer from binary string command""" 93 | 94 | crc=binascii.crc_hqx(command,0) 95 | crchi=crc>>8 96 | crclo=crc&255 97 | 98 | if crchi == 0x28 or crchi==0x0d or crchi==0x0a: 99 | crc+=256 100 | 101 | if crclo == 0x28 or crclo==0x0d or crclo==0x0a: 102 | crc+=1 103 | return crc 104 | 105 | def sendcmd(self,command): 106 | """send command/query to Pip4048, return reply""" 107 | self.command=command.encode('ascii','strict') 108 | crc=self.crccalc(self.command) 109 | self.command=self.command+crc.to_bytes(2, byteorder='big')+b'\r' 110 | self.port.reset_input_buffer() 111 | self.port.write(self.command) 112 | self.reply=self.port.readline() 113 | # print('command {} reply {}'.format(self.command,self.reply)) 114 | if self.crccalc(self.reply[0:-3]) != int.from_bytes(self.reply[-3:-1],byteorder='big'): 115 | raise IOError('CRC error in reply') 116 | 117 | def setparam(self,command): 118 | # time.sleep(5.0) 119 | self.sendcmd(command) 120 | if self.reply[1:4]!=b'ACK': 121 | log.error('Bad Reply {} to command {}'.format(self.reply,command)) 122 | raise IOError('Bad Parameters') 123 | 124 | 125 | def opensetparam(self,command): 126 | """open port, send set parameter command to pip, close port""" 127 | self.openpip(self.pipport) 128 | self.setparam(command) 129 | self.port.close 130 | 131 | def setparamnoerror(self,command): # set parameter, ignore errors 132 | try: 133 | self.opensetparam(command) 134 | except IOError: 135 | pass 136 | 137 | def backgroundswapinv(self): 138 | """Turns slave pip inverter on, waits for powerup and them turns 139 | master pip inverter off""" 140 | 141 | try: 142 | self.opensetparam('MNCHGC1498') 143 | except IOError: 144 | pass 145 | else: 146 | time.sleep(20) # wait for second inverter to power up and syncronise 147 | try: 148 | self.opensetparam('MNCHGC0497') 149 | except IOError: 150 | pass 151 | 152 | def swapinverter(self): 153 | """turns on inverter controlled by Pi pin number if on arg and turns off 154 | inverter controlled by Pi pin number in off arg""" 155 | 156 | if self.timeoverload==0: # only swap inverters if no overload 157 | Thread(target=self.backgroundswapinv).start() 158 | 159 | def slaveinvon(self): 160 | """turns on slave inverter, set overload timer, timer set to zero when 161 | power draw less than 60% of inverter load for half an hour""" 162 | 163 | self.timeoverload=time.time() 164 | try: 165 | self.opensetparam('MNCHGC1498') # turn on slave 166 | except IOError: 167 | pass 168 | else: 169 | self.numinvon=2 170 | 171 | def slaveinvoff(self): 172 | """turn slave inverter off""" 173 | try: 174 | self.opensetparam('MNCHGC1497') # turn off slave 175 | except IOError: 176 | pass 177 | else: 178 | self.numinvon=1 179 | 180 | 181 | 182 | def getdata(self): 183 | """returns dictionary with data from Pip4048""" 184 | # log.debug('open') 185 | self.rawdat = dict.copy(initrawdat) 186 | if self.pipdown==0.0: 187 | for i in range(5): 188 | try: 189 | self.openpip(self.pipport) 190 | self.sendcmd('QPIGS') 191 | self.rawdat['BInI']=float(self.reply[47:50].decode('ascii','strict')) 192 | self.rawdat['BOutI']=float(self.reply[77:82].decode('ascii','strict')) 193 | self.rawdat['PVI']=float(self.reply[60:64].decode('ascii','strict')) 194 | self.rawdat['BV']=float(self.reply[41:46].decode('ascii','strict')) 195 | self.rawdat['ACW']=float(self.reply[28:32].decode('ascii','strict')) \ 196 | *config['MPPSolar']['acwcal'] 197 | self.sendcmd('Q1') 198 | if len(self.reply) == 91: 199 | self.rawdat['ChgStat']=self.reply[69:71] 200 | self.rawdat['PVW']=float(self.reply[53:57].decode('ascii','strict')) 201 | elif len(self.reply) == 51: 202 | self.rawdat['ChgStat']=self.reply[46:48] 203 | self.rawdat['PVW']=float(self.reply[41:45].decode('ascii','strict')) 204 | 205 | self.rawdat['PVW']=self.rawdat['PVW']*config['MPPSolar']['pvwcal'] 206 | self.rawdat['ibat']=self.rawdat['BOutI']-self.rawdat['BInI'] 207 | self.rawdat['ipv']=self.rawdat['PVW']/self.rawdat['BV'] 208 | self.rawdat['iload']=self.rawdat['ACW']/self.rawdat['BV'] 209 | self.rawdat['DataValid']=True 210 | break 211 | except ValueError as err: 212 | log.error('PIP bad response{} to command {}'.format(self.reply,self.command)) 213 | time.sleep(0.5) 214 | if i==4: 215 | self.pipdown=time.time() # flag pip is down 216 | log.error("PIP sn {} interface down".format(self.sn)) 217 | except IOError as err: 218 | log.error('PIP interface error {}'.format(err)) 219 | time.sleep(0.5) 220 | if i==4: 221 | self.pipdown=time.time() # flag pip is down 222 | log.error("PIP sn {} interface down".format(self.sn)) 223 | finally: 224 | self.port.close() 225 | else: 226 | missedsamples=(time.time()-self.pipdown)//config['sampling']['sampletime'] 227 | if missedsamples%(600/config['sampling']['sampletime'])==0: #retry interface every 10 minutes 228 | try: 229 | self.findpip([]) 230 | except IOError: 231 | pass 232 | # if downtime>3600: # upgrade error if more than one hour 233 | # raise 234 | else: 235 | self.pipdown=0.0 236 | log.info("PIP sn {} interface back up".format(self.sn)) 237 | 238 | self.acloadav = (self.acloadav*2 + self.rawdat['ACW'])/3 # running average 239 | print ('acloadav {} ACW {}'.format(self.acloadav,self.rawdat['ACW'])) 240 | if self.timeoverload !=0.0: 241 | self.time=time.time() 242 | if self.acloadav*config['Inverters']['numinverters']>config['Inverters']['turnonslave'] \ 243 | or self.rawdat['ACW']*config['Inverters']['numinverters']>config['Inverters']['turnonslave']*1.3: 244 | self.timeoverload=self.time 245 | if self.time-self.timeoverload > config['Inverters']['minruntime']: 246 | self.timeoverload =0.0 247 | -------------------------------------------------------------------------------- /piptest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor Getdata from battery cells getdata.py***** 3 | # Copyright (C) 2017 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | import sys 18 | import serial 19 | import binascii 20 | from re import match 21 | from time import sleep 22 | #commands={'QPIGS':110,'Q':74,'QFLAG':15,'QPIRI':102,'PE':7,'PD':7,'PBCV':7, \ 23 | # 'PCVV':7,'PBFT':7,'PCVT':7} 24 | #commands={} 25 | def openpip(port): 26 | openport = serial.Serial(port,baudrate=2400,timeout=2.0) # open serial port 27 | return openport 28 | 29 | def sendcmd(command,port='/dev/ttyUSB1'): 30 | """send command/query to Pip4048, return reply""" 31 | 32 | # try: 33 | # x=match("[A-Z,a-z]*",command) 34 | # replylen=commands[x.group()] 35 | # except KeyError: 36 | # replylen=int(input("Enter Reply Length>")) 37 | 38 | try: 39 | cmd=command.encode('ascii','strict') 40 | crc=crccalc(cmd) 41 | cmd=cmd+crc.to_bytes(2, byteorder='big')+b'\r' 42 | openport=openpip(port) 43 | openport.reset_input_buffer() 44 | openport.write(cmd) 45 | # reply = openport.read(replylen) 46 | reply=openport.readline() 47 | print ('chars in buf {}'.format(openport.in_waiting)) 48 | if crccalc(reply[0:-3]) != int.from_bytes(reply[-3:-1],byteorder='big'): 49 | raise IOError("CRC error in Pip4048 return string") 50 | except IOError as err: 51 | print(err.args) 52 | finally: 53 | openport.close() 54 | 55 | print (len(reply),reply) 56 | print(binascii.hexlify(reply)) 57 | if command=='QPIGS': 58 | print ('AC output V =',reply[12:17]) 59 | print ('AC output VA =',reply[23:27]) 60 | print ('AC output W =',reply[28:32]) 61 | print ('Bus V =',reply[37:40]) 62 | print ('Battery V =',reply[41:46]) 63 | print ('Battery In I =',reply[47:50]) 64 | print ('Heat sink T =',reply[51:54]) 65 | print ('PV Input current for battery I =',reply[60:64]) 66 | print ('PV Input V =',reply[65:70]) 67 | print ('Battery V from SCC =',reply[71:76]) 68 | print ('Battery Discharge I =',reply[77:82]) 69 | print ('Status =',reply[83:91]) 70 | 71 | # (230.0 21.7 230.0 50.0 21.7 5000 4000 48.0 49.0 45.0 51.6 50.2 2 30 030 0 1 3 9 01 0 0 48.0 0 1 000\x19Q\r' 72 | 73 | elif command=='QPIRI': 74 | print ('Recharge V =',reply[43:47]) 75 | print ('LVD =',reply[48:52]) 76 | print ('Bulk V =',reply[53:57]) 77 | print ('Float V =',reply[58:62]) 78 | 79 | 80 | def setparam(command,port): 81 | reply=sendcmd(command,port) 82 | print (reply) 83 | if reply[1:4]!=b'ACK': 84 | raise IOError('Bad Parameters') 85 | 86 | def getcmd(): 87 | """gets command from user""" 88 | command=str(input("Enter Command>")) 89 | return command 90 | 91 | def loop(): 92 | while True: 93 | main() 94 | 95 | def main(port='/dev/ttyUSB1'): 96 | command=getcmd() 97 | sendcmd(command,port) 98 | 99 | def crccalc(command): 100 | """returns crc as integer from binary string command""" 101 | 102 | crc=binascii.crc_hqx(command,0) 103 | crchi=crc>>8 104 | crclo=crc&255 105 | 106 | if crchi == 0x28 or crchi==0x0d or crchi==0x0a: 107 | crc+=256 108 | 109 | if crclo == 0x28 or crclo==0x0d or crclo==0x0a: 110 | crc+=1 111 | return crc 112 | 113 | if __name__ == "__main__": 114 | """if run from command line, piptest [command] [port] 115 | default port /dev/ttyUSB1, if no command ask user""" 116 | 117 | print (sys.argv) 118 | if len(sys.argv) == 2: 119 | sendcmd(sys.argv[1]) 120 | else: 121 | if len(sys.argv) == 3: 122 | sendcmd(sys.argv[1],sys.argv[2]) 123 | else: 124 | loop() 125 | -------------------------------------------------------------------------------- /pylon.py: -------------------------------------------------------------------------------- 1 | # *****BatteryMonitor pylontech battery driver***** 2 | # Copyright (C) 2014 Simon Richard Matthews 3 | # Project loaction https://github.com/simat/BatteryMonitor 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # any later version. 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # You should have received a copy of the GNU General Public License along 13 | # with this program; if not, write to the Free Software Foundation, Inc., 14 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 15 | 16 | 17 | #!/usr/bin/python 18 | import numpy as np 19 | from copy import deepcopy 20 | import sys 21 | import time 22 | import serial 23 | import glob 24 | from config import config 25 | numcells = config['battery']['numcells'] 26 | import logger 27 | log = logger.logging.getLogger(__name__) 28 | log.setLevel(logger.logging.DEBUG) 29 | log.addHandler(logger.errfile) 30 | initrawdat ={'DataValid':False,'BatI':0.0,'SOC':100.0,\ 31 | 'CellV':[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],\ 32 | 'Temp':[-50.0,-50.0,-50.0,-50.0,-50.0,-50.0,-50.0,-50.0]} 33 | 34 | 35 | class Rawdat(): 36 | """class for obtaining data from and controlling indidvdual PIP inverters. 37 | When class is instantiated the SN of the PIP is used to tie the instance of 38 | the class to the particular machine""" 39 | 40 | def __init__(self,interfacesinuse): 41 | self.rawdat = deepcopy(initrawdat) 42 | self.reply ='' # placeholder for reply from sendcmd 43 | self.pylondown=0.0 44 | try: 45 | self.findpylon() 46 | except serial.serialutil.SerialException as err: 47 | self.pylondown=time.time() # flag pylontech is down 48 | log.error(err) 49 | 50 | 51 | self.floatv=48.0 52 | self.bulkv=48.0 53 | self.rechargev=48.0 54 | self.lowv=44.0 55 | # self.command=b'' # last command sent to pylontech 56 | self.time=0.0 57 | 58 | def findpylon(self): 59 | """Scan ports to find pylon port""" 60 | 61 | self.pylonport="" 62 | for dev in glob.glob(config['Ports']['pylonport']): 63 | try: 64 | self.openpylon(dev) 65 | reply=self.sendcmd('\n') 66 | # print (reply,dev) 67 | if b'pylon>' in reply: 68 | self.pylonport=dev 69 | break 70 | except (IOError, serial.serialutil.SerialException): 71 | pass 72 | finally: 73 | self.openport.close 74 | if self.pylonport=="": 75 | raise serial.serialutil.SerialException("Couldn't find Pylontech battery") 76 | 77 | def openpylon(self,port): 78 | self.openport = serial.Serial(port,baudrate=115200,timeout=1) # open serial port 79 | 80 | def sendcmd(self,command,replylen=2048): 81 | """send command/query to Pylontech battery, port must be open, return reply""" 82 | 83 | retries=2 84 | for tries in range(retries): 85 | try: 86 | cmd=command.encode('ascii','strict') 87 | # print ('command ={}'.format(cmd)) 88 | self.openport.write(cmd) 89 | reply = b'' 90 | starttime=time.time() 91 | for i in range(replylen): 92 | char=self.openport.read(1) 93 | if char==b'': #timeout 94 | break 95 | reply = reply + char 96 | # if char==b'$': 97 | # if self.openport.read(1)==b'$': 98 | # break 99 | if char==b'>': 100 | break 101 | if reply: 102 | break 103 | except IOError as err: 104 | print(err.args) 105 | if tries==retries-1: 106 | raise 107 | # print (reply) 108 | return reply 109 | 110 | batcmdidx ={'Volt':9,'current':18,'temp':27,'SOC':88} 111 | def getdata(self): 112 | """returns dictionary with data from Pylontech battery""" 113 | # log.debug('open') 114 | # print (initrawdat) 115 | self.rawdat =deepcopy(initrawdat) 116 | if self.pylondown==0.0: 117 | for i in range(5): 118 | try: 119 | self.openpylon(self.pylonport) 120 | cellv=np.empty((8,15), dtype=np.uint16) 121 | for bat in range(8): 122 | reply=self.sendcmd('bat {}\n'.format(bat+1)) 123 | reply=reply.decode() 124 | if 'Invalid command or fail to excute.' in reply: 125 | numbats=bat 126 | break 127 | else: 128 | idx=reply.index('Coulomb \r\r\n')+15 129 | for cell in range(15): 130 | cellv[bat,cell]=int(reply[idx+9:idx+13]) 131 | self.rawdat['BatI']+=float(reply[idx+14:idx+27]) 132 | self.rawdat['Temp'][bat]=max(self.rawdat['Temp'][bat],float(reply[idx+27:idx+33])/1000) 133 | self.rawdat['SOC']=min(self.rawdat['SOC'],float(reply[idx+88:idx+91])) 134 | idx+=110 135 | self.rawdat['BatI']=self.rawdat['BatI']/(15000) 136 | cellv.resize(numbats,15) 137 | # print (cellv) 138 | maxvs, minvs=np.amax(cellv,axis=0),np.amin(cellv,axis=0) 139 | maxv,minv=0,5000 140 | for cell in range(15): 141 | maxv,minv=max(maxv,maxvs[cell]),min(minv,minvs[cell]) 142 | cellavs=np.median(cellv,axis=0) 143 | # print (maxvs,maxv,minvs,minv,cellavs) 144 | #if max or min cell voltage in current cell # store that otherwise average 145 | for cell in range(15): 146 | for bat in range(numbats): 147 | curcell=cellv[bat][cell] 148 | if curcell==maxv or curcell==minv: 149 | self.rawdat['CellV'][cell]+=float(curcell)/1000.0 150 | if cell < 15-1: 151 | self.rawdat['CellV'][cell+1]=self.rawdat['CellV'][cell] 152 | break 153 | elif bat==numbats-1: 154 | self.rawdat['CellV'][cell]+=int(cellavs[cell])/1000.0 155 | if cell < 15-1: 156 | self.rawdat['CellV'][cell+1]=self.rawdat['CellV'][cell] 157 | self.rawdat['DataValid']=True 158 | break 159 | except ValueError as err: 160 | log.error('Pylon bad response{} to command {}'.format(self.reply,self.command)) 161 | time.sleep(0.5) 162 | if i==4: 163 | self.pylondown=time.time() # flag pylon is down 164 | log.error("Pylon interface down") 165 | except serial.serialutil.SerialException as err: 166 | log.error('PIP interface error {}'.format(err)) 167 | time.sleep(0.5) 168 | if i==4: 169 | self.pylondown=time.time() # flag pylon is down 170 | log.error("Pylon interface down") 171 | finally: 172 | self.openport.close() 173 | print (self.rawdat) 174 | else: 175 | downtime=time.time()-self.pylondown 176 | if downtime!=0 and downtime%6003600: # upgrade error if more than one hour 182 | # raise 183 | else: 184 | self.pylondown=0.0 185 | log.info("PIP sn {} interface back up".format(self.sn)) 186 | -------------------------------------------------------------------------------- /pylontest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor diagnostic program for Pylontech batteries***** 3 | # Copyright (C) 2021 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | import sys 18 | import serial 19 | import binascii 20 | from re import match 21 | 22 | #commands={'QPIGS':110,'Q':74,'QFLAG':15,'QPIRI':102,'PE':7,'PD':7,'PBCV':7, \ 23 | # 'PCVV':7,'PBFT':7,'PCVT':7} 24 | #commands={} 25 | def openpylon(port): 26 | openport = serial.Serial(port,baudrate=115200,timeout=1.0) # open serial port 27 | return openport 28 | 29 | def sendcmd(command,port='/dev/ttyUSB1'): 30 | """send command/query to Pip4048, return reply""" 31 | 32 | # try: 33 | # x=match("[A-Z,a-z]*",command) 34 | # replylen=commands[x.group()] 35 | # except KeyError: 36 | # replylen=int(input("Enter Reply Length>")) 37 | 38 | try: 39 | openport=openpylon(port) 40 | openport.write(command) 41 | # reply = openport.read(replylen) 42 | reply = b'' 43 | for i in range(200): 44 | char=openport.read(1) 45 | reply = reply + char 46 | if char==b'\r': 47 | break 48 | except IOError as err: 49 | print(err.args) 50 | finally: 51 | openport.close() 52 | 53 | print (len(reply),reply) 54 | print(binascii.hexlify(reply)) 55 | 56 | 57 | def setparam(command,port): 58 | reply=sendcmd(command,port) 59 | print (reply) 60 | if reply[1:4]!=b'ACK': 61 | raise IOError('Bad Parameters') 62 | 63 | def getcmd(): 64 | """gets command from user""" 65 | command=str(input("Enter Command>")) 66 | return command 67 | 68 | def loop(): 69 | while True: 70 | main() 71 | 72 | def main(port='/dev/ttyUSB1'): 73 | command=getcmd() 74 | sendcmd(command,port) 75 | 76 | def crccalc(command): 77 | """returns crc as integer from binary string command""" 78 | 79 | crc=binascii.crc_hqx(command,0) 80 | crchi=crc>>8 81 | crclo=crc&255 82 | 83 | if crchi == 0x28 or crchi==0x0d or crchi==0x0a: 84 | crc+=256 85 | 86 | if crclo == 0x28 or crclo==0x0d or crclo==0x0a: 87 | crc+=1 88 | return crc 89 | 90 | if __name__ == "__main__": 91 | """if run from command line, pylontest [command] [port] 92 | default port /dev/ttyUSB1, if no command ask user""" 93 | 94 | print (sys.argv) 95 | if len(sys.argv) == 2: 96 | sendcmd(sys.argv[1]) 97 | else: 98 | if len(sys.argv) == 3: 99 | sendcmd(sys.argv[1],sys.argv[2]) 100 | else: 101 | loop() 102 | -------------------------------------------------------------------------------- /summary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # *****BatteryMonitor store summary data summary.py***** 3 | # Copyright (C) 2014 Simon Richard Matthews 4 | # Project loaction https://github.com/simat/BatteryMonitor 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # any later version. 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | import sys 18 | from os import rename 19 | import time 20 | from shutil import copy as filecopy 21 | from copy import deepcopy 22 | from ast import literal_eval 23 | from configparser import SafeConfigParser 24 | from config import config 25 | numcells = config['battery']['numcells'] 26 | import logger 27 | log = logger.logging.getLogger(__name__) 28 | log.setLevel(logger.logging.DEBUG) 29 | log.addHandler(logger.errfile) 30 | 31 | 32 | summaryfile = SafeConfigParser() 33 | summaryfile.read(config['files']['summaryfile']) 34 | 35 | #logdata =logger.logging.getlogger() 36 | #logdata.setLevel(logger.logging.INFO) 37 | #log.addHandler(logger.logfile) 38 | 39 | class Summary: 40 | """Handles battery summary data""" 41 | 42 | def __init__(self): 43 | self.currenttime = time.strftime("%Y%m%d%H%M%S ", time.localtime()) 44 | printtime = str(self.currenttime) 45 | self.logfile = open(config['files']['logfile'],'at',buffering=1) 46 | self.sampletime = time.time() 47 | self.prevtime = self.currenttime 48 | self.summary=self.loadsummary() 49 | 50 | # summary = open('/media/75cc9171-4331-4f88-ac3f-0278d132fae9/summary','w') 51 | # pickle.dump(hivolts, summary) 52 | # pickle.dump(lowvolts, summary) 53 | # summary.close() 54 | if str(self.summary['hour']['timestamp'])[0:10] != printtime[0:10]: 55 | self.summary['hour'] = deepcopy(self.summary['current']) 56 | if str(self.summary['currentday']['timestamp'])[0:8] != printtime[0:8]: 57 | self.summary['currentday'] = deepcopy(self.summary['current']) 58 | if str(self.summary['monthtodate']['timestamp'])[0:6] != printtime[0:6]: 59 | self.summary['monthtodate'] = deepcopy(self.summary['current']) 60 | if str(self.summary['yeartodate']['timestamp'])[0:4] != printtime[0:4]: 61 | self.summary['yeartodate'] = deepcopy(self.summary['current']) 62 | 63 | def loadsummary(self): 64 | summary = {} 65 | for section in summaryfile.sections(): 66 | summary[section] = {} 67 | for key, val in summaryfile.items(section): 68 | summary[section][key] = literal_eval(val) 69 | # daysummaryfile = open('/media/75cc9171-4331-4f88-ac3f-0278d132fae9/daysummary','r') 70 | # self.daydata = literal_eval(daysummaryfile.read()) 71 | # daysummaryfile.close() 72 | print(summary['current']['timestamp']) 73 | return summary 74 | 75 | 76 | def update(self, summary, batdata): 77 | """ Update 'current' section of summary data with 'batdata' and write realtime log """ 78 | self.prevtime = self.currenttime 79 | self.currenttime = int(time.strftime("%Y%m%d%H%M%S ", time.localtime())) 80 | self.summary['current']['timestamp'] = str(self.currenttime) 81 | self.summary['current']['maxvoltages'][numcells] = round(batdata.batvoltsav[numcells],2) 82 | self.summary['current']['minvoltages'][numcells] = self.summary['current']['maxvoltages'][numcells] 83 | if batdata.currentav[-3] > -config['battery']['ilowcurrent']: 84 | self.summary['current']['maxnocharge'][numcells] = self.summary['current']['maxvoltages'][numcells] 85 | if batdata.currentav[-3] < config['battery']['ilowcurrent']: 86 | self.summary['current']['minnoload'][numcells] = self.summary['current']['minvoltages'][numcells] 87 | self.summary['current']['ah'][2] = round(batdata.soc,2) 88 | self.summary['current']['ah'][0] = self.summary['current']['ah'][2] 89 | self.summary['current']['ah'][1] = self.summary['current']['ah'][2] 90 | self.summary['current']['ah'][6] = round(batdata.inahtot,2) # current from solar etc 91 | self.summary['current']['dod'][2] = round(batdata.socadj,2) 92 | self.summary['current']['dod'][0] = self.summary['current']['dod'][2] 93 | self.summary['current']['dod'][1] = self.summary['current']['dod'][2] 94 | self.summary['current']['dod'][4] = round(\ 95 | 100-100*(self.summary["current"]["dod"][0])/(batdata.batcapresidual),1) 96 | 97 | # self.summary['current']['amps'][1] = round(batdata.currentav[0], 1) 98 | # self.summary['current']['amps'][0] = self.summary['current']['amps'][1] 99 | # self.summary['current']['amps'][2] = round(batdata.currentav[1], 1) 100 | if batdata.ah > 0.0: 101 | self.summary['current']['ah'][5] = round(batdata.ah,2) 102 | self.summary['current']['ah'][4] = 0.0 103 | else: 104 | self.summary['current']['ah'][4] = round(batdata.ah,2) 105 | self.summary['current']['ah'][5] = 0.0 106 | if batdata.pwrbat > 0.0: 107 | self.summary['current']['power'][1] = round(batdata.pwrbattot,6) 108 | self.summary['current']['power'][0] = 0.0 109 | else: 110 | self.summary['current']['power'][0] = round(batdata.pwrbattot,6) 111 | self.summary['current']['power'][1] = 0.0 112 | self.summary['current']['power'][2] = round(batdata.pwrintot,6) 113 | self.summary['current']['power'][3] = round(self.summary['current']['power'][0] + \ 114 | self.summary['current']['power'][2] + \ 115 | self.summary['current']['power'][1] ,6 ) # current to loads 116 | self.summary['current']['batpwr1hrav'][0]=round(batdata.batpwr1hrav,3) 117 | self.summary['current']['minmaxdemandpwr']=batdata.minmaxdemandpwr 118 | self.summary['current']['excesssolar'][0]=round(batdata.pwravailable) 119 | for i in range(len(batdata.chargestates)): 120 | if batdata.chargestates[i] == b'00': 121 | self.summary['current']['state'][i] ='Offline' 122 | elif batdata.chargestates[i] == b'10': 123 | self.summary['current']['state'][i] ='No Sun' 124 | elif batdata.chargestates[i] == b'11': 125 | self.summary['current']['state'][i] ='Bulk' 126 | elif batdata.chargestates[i] == b'12': 127 | self.summary['current']['state'][i] ='Absorb' 128 | elif batdata.chargestates[i] == b'13': 129 | self.summary['current']['state'][i] ='Float' 130 | 131 | vprint='' 132 | batdata.vcells='' 133 | batdata.iall='' 134 | maxmaxvoltage = 0.0 135 | minmaxvoltage = 5.0 136 | for i in range(len(self.summary['current']['maxvoltages'])): 137 | self.summary['current']['maxvoltages'][i] = round(batdata.voltsav[i+1],3) 138 | self.summary['current']['minvoltages'][i] = self.summary['current']['maxvoltages'][i] 139 | if batdata.currentav[-3] > -config['battery']['ilowcurrent']: 140 | self.summary['current']['maxnocharge'][i] = self.summary['current']['maxvoltages'][i] 141 | if batdata.currentav[-3] < config['battery']['ilowcurrent']: 142 | self.summary['current']['minnoload'][i] = self.summary['current']['minvoltages'][i] 143 | 144 | for i in range(numcells): 145 | maxmaxvoltage = max(maxmaxvoltage, self.summary['current']['maxvoltages'][i]) 146 | minmaxvoltage = min(minmaxvoltage, self.summary['current']['maxvoltages'][i]) 147 | self.summary['current']['baltime'][i]=round(batdata.baltime[i],4) 148 | batdata.vcells=batdata.vcells+str(round(batdata.voltsav[i+1],3)).ljust(5,'0')+' ' 149 | 150 | vprint=vprint + batdata.vcells 151 | self.summary['current']['deltav'][0] = round(maxmaxvoltage - minmaxvoltage, 3) 152 | if batdata.currentav[-3] < config['battery']['ilowcurrent']: 153 | self.summary['current']['deltav'][1] = self.summary['current']['deltav'][0] 154 | self.summary['current']['deltav'][2] = self.summary['current']['deltav'][0] 155 | batdata.vbat=str(round(batdata.batvoltsav[numcells],2)).ljust(5,'0')+' ' 156 | vprint = vprint +batdata.vbat 157 | batdata.vdelta= str(self.summary['current']['deltav'][0]).ljust(5,'0')+' ' 158 | vprint = vprint+batdata.vdelta 159 | 160 | for i in range(batdata.numiins): 161 | self.summary['current']['ioutmax'][i] = round(batdata.currentav[i],1) 162 | self.summary['current']['iinmax'][i] = self.summary['current']['ioutmax'][i] 163 | batdata.iall=batdata.iall+str(round(batdata.currentav[i],1)).ljust(5,'0')+' ' 164 | self.summary['current']['kwoutmax'][i] = round(batdata.currentav[i]*batdata.batvoltsav[numcells]/1000,3) 165 | self.summary['current']['kwinmax'][i] = self.summary['current']['kwoutmax'][i] 166 | self.summary['current']['kwhin'][i] = round(batdata.kWhin[i],6) 167 | self.summary['current']['kwhout'][i] = round(batdata.kWhout[i],6) 168 | 169 | for i in range(len(batdata.tin)): # get temperatures 170 | self.summary['current']['tmax'][i] = batdata.temp[i] 171 | self.summary['current']['tmin'][i] = self.summary['current']['tmax'][i] 172 | 173 | vprint = vprint +batdata.iall 174 | batdata.soctxt=str(round(batdata.soc,2)).ljust(6,'0') +' ' 175 | batdata.socadjtxt=str(round(batdata.socadj,2)).ljust(6,'0') + ' ' # + '\033[1A' 176 | sys.stdout.write(eval(config['logging']['data'])+'\n') # + '\033[1A' 177 | self.logfile.write(eval(config['logging']['data'])+'\n') 178 | # self.publish(data=eval(config['mqtt']['data'])) 179 | 180 | # log.info(config['logging']['data']) 181 | 182 | def updatesection(self, summary, section, source): 183 | """ Update 'summary' section 'section' with data from 'source' """ 184 | 185 | section = self.summary[section] 186 | source = self.summary[source] 187 | section['deltav'][1] = max(section['deltav'][1], source['deltav'][1]) 188 | section['deltav'][2] = max(section['deltav'][2], source['deltav'][2]) 189 | section['deltav'][0] = min(section['deltav'][0], source['deltav'][0]) 190 | section['ah'][2] = max(section['ah'][2], source['ah'][2]) 191 | section['ah'][0] = min(section['ah'][0], source['ah'][0]) 192 | section['ah'][1] = (section['ah'][1]*section['ah'][3] + source['ah'][1]) 193 | section['ah'][4] = round(section['ah'][4]+source['ah'][4], 2) 194 | section['ah'][5] = round(section['ah'][5]+source['ah'][5], 2) 195 | section['ah'][6] = round(section['ah'][6]+source['ah'][6], 2) 196 | section['power'][0] = round(section['power'][0]+source['power'][0], 6) 197 | section['power'][1] = round(section['power'][1]+source['power'][1], 6) 198 | section['power'][2] = round(section['power'][2]+source['power'][2], 6) 199 | section['power'][3] = round(section['power'][3]+source['power'][3], 6) 200 | section['dod'][2] = max(section['dod'][2], source['dod'][2]) 201 | section['dod'][0] = min(section['dod'][0], source['dod'][0]) 202 | section['dod'][1] = (section['dod'][1]*section['ah'][3] + source['dod'][1]) 203 | section['ah'][3] += 1 204 | section['ah'][1] = round(section['ah'][1]/section['ah'][3], 6) 205 | section['dod'][1] = round(section['dod'][1]/section['ah'][3], 6) 206 | section['dod'][3] = max(section['dod'][3], source['dod'][3]) 207 | # section['amps'][1] = max(section['amps'][1], source['amps'][1]) 208 | # section['amps'][0] = min(section['amps'][0], source['amps'][0]) 209 | # section['amps'][2] = min(section['amps'][2], source['amps'][2]) 210 | for i in range(len(config['CurrentInputs'])): 211 | section['ioutmax'][i] = max(section['ioutmax'][i], source['ioutmax'][i]) 212 | section['iinmax'][i] = min(section['iinmax'][i], source['iinmax'][i]) 213 | section['kwoutmax'][i] = max(section['kwoutmax'][i], source['kwoutmax'][i]) 214 | section['kwinmax'][i] = min(section['kwinmax'][i], source['kwinmax'][i]) 215 | section['kwhin'][i] = round(source['kwhin'][i]+section['kwhin'][i], 5) 216 | section['kwhout'][i] = round(source['kwhout'][i]+section['kwhout'][i], 5) 217 | for i in range(len(self.summary['current']['maxvoltages'])): 218 | section['maxvoltages'][i] = max(section['maxvoltages'][i], source['maxvoltages'][i]) 219 | section['minvoltages'][i] = min(section['minvoltages'][i], source['minvoltages'][i]) 220 | section['maxnocharge'][i] = max(section['maxnocharge'][i], source['maxnocharge'][i]) 221 | section['minnoload'][i] = min(section['minnoload'][i], source['minnoload'][i]) 222 | for i in range(numcells): 223 | section['baltime'][i] = round(section['baltime'][i]+source['baltime'][i],4) 224 | for i in range(len(config['TemperatureInputs'])): 225 | section['tmax'][i] = max(section['tmax'][i], source['tmax'][i]) 226 | section['tmin'][i] = min(section['tmin'][i], source['tmin'][i]) 227 | section['timestamp'] = self.summary['current']['timestamp'] 228 | 229 | def writesummary(self): 230 | """ Write summary file """ 231 | 232 | for section in summaryfile.sections(): 233 | for option in summaryfile.options(section): 234 | summaryfile.set(section, option, str(self.summary[section][option])) 235 | of = open(config['files']['summaryfile'],'w') 236 | summaryfile.write(of) 237 | of.close() 238 | 239 | # def writehour(self, data): 240 | # hoursummaryfile=open('/media/75cc9171-4331-4f88-ac3f-0278d132fae9/hoursummary','a') 241 | # hoursummaryfile.write(data) 242 | # hoursummaryfile.close() 243 | # logsummary.set('alltime', 'maxvoltages') = round(max(literal_eval(logsummary.get('currentday','maxvoltages')),literal_eval(logsummary.get(),2) 244 | # logsummary.set('alltime', 'minvoltages') = round(min(literal_eval(logsummary.get('currentday','minvoltages')),batdata.batvoltsav[8]),2) 245 | # logsummary.set('alltime', 'ah') = round(max(literal_eval(logsummary.get('currentday','ah'))[1], batdata.soc/1000),2) 246 | # logsummary.set('alltime', 'ah') = round(min(literal_eval(logsummary.get('currentday','ah'))[0], batdata.soc/1000),2) 247 | # logsummary.set('alltime', 'current') = round(max(literal_eval(logsummary.get('alltime','current'))[1], batdata.currentav[-3]/1000),2) 248 | # logsummary.set('alltime', 'current') = round(min(literal_eval(logsummary.get('alltime','current'))[0], batdata.currentav[-3]/1000),2) 249 | 250 | 251 | def writeperiod(self, file, data): 252 | """ Append 'data' to 'file' for previous period """ 253 | periodfile=open(config['files'][file],'a') 254 | writestr='' 255 | y = summaryfile.items(data) 256 | for i in y: 257 | writestr = writestr + str(i) +"\n" 258 | writestr = writestr + "\n" 259 | periodfile.write(writestr) 260 | periodfile.close() 261 | 262 | def starthour(self, summary): 263 | """ Start new hour """ 264 | self.writeperiod('hoursummaryfile', 'hour') 265 | self.summary['hour']['ah'][3] = 0 # zero # of samples for av 266 | self.summary['hour'] = deepcopy(self.summary['current']) 267 | 268 | def startday(self, summary): 269 | """ Start new Day """ 270 | 271 | self.writeperiod('daysummaryfile', 'currentday') 272 | self.summary['prevday'] = deepcopy(self.summary['currentday']) 273 | self.summary['currentday']['ah'][3] = 0 # zero number of samples for av 274 | self.summary['current']['dod'][3] += 1 275 | self.summary['currentday'] = deepcopy(self.summary['current']) 276 | 277 | def startmonth(self, summary): 278 | """ Start new month """ 279 | 280 | self.writeperiod('monthsummaryfile', 'monthtodate') 281 | self.summary['monthtodate']['ah'][3] = 0 # zero number of samples for av 282 | self.summary['monthtodate'] = deepcopy(self.summary['current']) 283 | filecopy(config['files']['summaryfile'],config['files']['summaryfile']+ str(self.currenttime)[0:8]) 284 | 285 | def startyear(self, summary): 286 | """ Start new year """ 287 | 288 | self.writeperiod('yearsummaryfile', 'yeartodate') 289 | self.summary['yeartodate']['ah'][3] = 0 # zero number of samples for av 290 | self.summary['yeartodate'] = deepcopy(self.summary['current']) 291 | self.logfile.close() 292 | rename(config['files']['logfile'],config['files']['logfile']+str(int(str(self.currenttime)[0:4])-1)) 293 | self.logfile = open(config['files']['logfile'],'a') 294 | 295 | def close(self): 296 | """ Close logging file ready for exit """ 297 | 298 | self.logfile.close() 299 | -------------------------------------------------------------------------------- /system/batman.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Start Battery Monitor Deamon 3 | 4 | [Service] 5 | Type=simple 6 | User=pi 7 | WorkingDirectory=/home/pi 8 | ExecStart=/usr/bin/startpython.sh 9 | #Restart=always 10 | 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /system/piptest.sh: -------------------------------------------------------------------------------- 1 | python3 ~/Ba*/piptest.py $1 $2 2 | -------------------------------------------------------------------------------- /system/startpython.sh: -------------------------------------------------------------------------------- 1 | TZ='Australia/Perth'; export TZ 2 | cd ~/BatteryMonitor 3 | python3 -u batteries.py $1 1> /dev/null 2>batteries.log 4 | echo "$!" > batpid 5 | # python batteries.py 6 | exit 0 7 | --------------------------------------------------------------------------------