# V1.0 Maintain Ah on restart # @reboot sleep 20 && sudo python /var/www/html/BatteryMon_3.py & # 24.6.23 Voltage measurement for Controller 2 # IMPORT DEFINITIONS #************************************************************************************** import platform # for platform.system() check import sys import os # for CSV file existence check import time sys.path.append(r"C:\Users\kauko\env\Lib\site-packages") import serial import datetime # for time management from datetime import timedelta # for time management import matplotlib # Styles #import numpy as np # !!!not sure if needed matplotlib.use('Agg') #matplotlib.use('TkAgg') # Use an interactive backend import pylab # for graph saving import matplotlib.pyplot as plt # graphs library import matplotlib.dates as mdates # formatting the graph plt.style.use('ggplot') # graph style from drawnow import * # for graph update import atexit from csv import writer # for CSV file control import csv # FILE DEFINITIONS #************************************************************************************** Controlfile = '/var/www/html/Data/Control.csv' # CSV file for control data StatusFile = '/var/www/html/Data/Status.csv' # CSV file for status data DataFile = '/var/www/html/Data/Tiedosto.csv' # CSV file for log data GraphFile = '/var/www/html/Data/GraphFile.csv' # CSV file for graph data Graph = '/var/www/html/Data/Graph6' # CSV file for graph data # FILE DEFINITIONS #************************************************************************************** Current = 0.0 # Current value Voltage = 12 # Voltage value Energy2 = 0.0 # Energy value Energy = 0.0 # Energy value EnergyIn = 0.0 EnergyOut = 0.0 ChargeTime = timedelta() cumulativeChargeTime = timedelta() #ChargeTime = timedelta(hours=0, minutes=0, seconds=0) # CONTROL FILE DEFINITIONS #************************************************************************************** AhStart = 0.0 VStart = 0.0 AhStop = 0.0 VStop = 0.0 InvTrigger = 0.0 AhValue = 300 allowedMin = 10 IOffset = 0 ICal = 0 State = "Idle" PrevState = "Idle" chargeStartTime = time.time() cumulativeChargeTimeStr = "00:00:00" x2 = datetime.datetime.now() # Time stamp for graph, refreshed by message from Arduino shortTimerOn = 0 sec5Timer = 60 graphTimer = 600 # HW MEASUREMENT RELATED DEFINITIONS #************************************************************************************** # Voltage measurement calibration factor VoltageFactor = 0.0189 #Proto1 12V corresponds 2,99V in ADC and as 635 binary with Vref 4.5V #VoltageFactor = 0.0195 #Proto2 # DC virran mittaus 50 (proto1) or 20 (proto2) is the multiplier in Arduino IdcFactor = 0.715/20 # = 0,03575 Controller V2 #IdcFactor = 0.749/50 # = 0,01498 Controller V1 # ARDUINO INTERFACE #************************************************************************************** # Detect the operating system and define the serial USB port accordingly def get_serial_port(): if platform.system() == "Windows": return "COM4" # Example for Windows elif platform.system() == "Linux": return "/dev/ttyACM0" # Example for Raspberry Pi else: raise EnvironmentError("Unsupported platform") port = get_serial_port() # Define and open serial port, baud must be same as in Arduino #port = "/dev/ttyUSB0" #vanhalla Arduino Unolla #port = "COM4" # Arduino Nano with PC #port = "/dev/ttyUSB0" # Arduino Nano #port = "/dev/ttyACM0" # Arduino Every baud = 9600 # GRAPHS DEFINITIONS #************************************************************************************** def OwnGraph1(): plt.figure(1) fig = plt.figure(1) fig.set_figwidth(5) #Picture sizing fig.set_figheight(8) #SoC plt.subplot(311) plt.ylim(0, AhValue) plt.title("Varaustila %1.1f Ah" %Energy2, fontweight='bold', fontsize=12) plt.plot(x, values3, 'g') plt.grid(linewidth=1) plt.axhspan(60, 300, facecolor="green", alpha=0.1) plt.axhspan(30, 60, facecolor="yellow", alpha=0.1) plt.axhspan(0, 30, facecolor="red", alpha=0.1) #plt.suptitle("xxxxx", fontsize=12) #Current plt.subplot(312) plt.title("Virta %1.1f A, (+lataus, -purku)" %Current, fontweight='bold', fontsize=12) if ((max(values2) < 10) & (min(values2) > -10)): plt.ylim(-10, 10) plt.plot(x, values2, 'r') #Voltage plt.subplot(313) plt.title("Jannite %1.1f V" %Voltage, fontweight='bold', fontsize=12) plt.ylim(10, 16) plt.plot(x, values, 'b') plt.grid(linewidth=1) plt.axhspan(14.8, 16, facecolor="red", alpha=0.1) plt.axhspan(11.5, 14.8, facecolor="green", alpha=0.1) plt.axhspan(10, 11.5, facecolor="red", alpha=0.1) fig.tight_layout() plt.gcf().autofmt_xdate() #plt.show() # FUNCTIONS #************************************************************************************** def readArduino(Energy): global EnergyIn, EnergyOut, Energy2, Time0, Voltage, Current, x2 data = serialArduino.readline().strip() # Strip new-line chars parts = data.split(b" ") if parts[0] == b"Graph1": try: Voltage1 = int(parts[1]) Current1 = int(parts[2]) except (IndexError, ValueError): print("Invalid data received from Arduino") return None Voltage = round(Voltage1 * VoltageFactor, 2) Current = round(IOffset + ICal * Current1 * IdcFactor, 3) # Remove 1-bit noise if Current1 * Current1 == 1: Current = 0 # calculate time from last Energy update Time = (datetime.datetime.now() - Time0).total_seconds() # check that time is not too long in case Raspi has just updated it's time if Time > 600: Time = 120 Energy2 = round(Energy + Current * Time / 3600, 2) # Ensure that energy is not negative or above battery capacity Energy2 = max(0.1, min(Energy2, AhValue - 2)) Time0 = datetime.datetime.now() EnergyChange = round(Current * Time / 3600, 3) if EnergyChange > 0: EnergyIn = round(EnergyIn + EnergyChange, 3) else: EnergyOut = round(EnergyOut - EnergyChange, 3) time_str = str(datetime.datetime.now().replace(microsecond=0)) x2 = datetime.datetime.strptime(time_str, '%Y-%m-%d %H:%M:%S') updateStatusfile(Voltage, Current, Energy2, EnergyIn, EnergyOut, x2) updateGraph2(Voltage, Current, Energy2, x2) updateDataFile(Voltage, Current, Energy2, x2) return Energy2 def updateGraph2(Voltage, Current, Energy, x2): global values, values2, values3 if 0 <= Voltage <= 10240000: values.append(Voltage) values.pop(0) values2.append(Current) values2.pop(0) values3.append(Energy) values3.pop(0) x.append(x2) x.pop(0) drawnow(OwnGraph1) #pylab.savefig("Graafi6.png", bbox_inches="tight") plt.savefig(Graph, bbox_inches="tight") return Voltage, Current, Energy else: print("Invalid! Voltage out of range") return None def sendToArduino( Label, Data ): #Add message header/terminator as defined in Arduino SW Task = "<" + Label + " " + Data + ">" print ("Sent command:",Task) serialArduino.write(Task.encode()) return; def initControlData (): global ControlData0 with open(Controlfile, 'r') as f: reader = csv.reader(f) ControlData0 = list(reader) lines = len(ControlData0) #print ("Control data lines from file ",lines) for ind in range(0,lines): #print (ind,1) ControlData0[ind][1] = "xx" #Read data from CSV file cell, compare to previous and send delta to Arduino def readControlfile(): global ControlData0, Energy2, allowedMin, AhValue, IOffset, ICal, VStart, AhStart, VStop, AhStop, InvTrigger with open(Controlfile, 'r') as f: reader = csv.reader(f) ControlData = list(reader) if ControlData0 != ControlData: #compare firs full arrays, if delta compare line by line for ind, control in enumerate(ControlData): #Then compare line by luine if ControlData0[ind] != control: Label, Data = control[0], control[1] if Label == "Energy" and float(Data) >= 2: #Update energy only if it is bigger that 2 Energy2 = float(Data) if Label == "AhValue": AhValue = float(Data) # CURRENT MEASUREMENT CALIBRATION if Label == "IOffset": IOffset = float(Data) if Label == "ICal": ICal = float(Data) # CHARGER AUTOSTART PARAMETERS if Label == "VStart": # voltage to start charger VStart = float(Data) if Label == "AhStart": # Ah to start charger AhStart = float(Data) if Label == "VStop": # Voltage to stop charger VStop = float(Data) if Label == "AhStop": # Ah to stop charger AhStop = float(Data) if Label == "InvTrigger": InvTrigger = float(Data) elif Label == "Run" and Data == "Stop": updateGraphFile() print("Stopped by Run stop command") sys.exit() elif Label == "VMin" and float(Data) != allowedMin: allowedMin = float(Data) #print("Print to Arduino due Control data change") sendToArduino(Label, Data) ControlData0 = ControlData return "NA" return "NA" #STORES THE DATA ABOUT ENERGY, CURRENT AND VOLTAGE #************************************************************************************** def createDataFile(): if os.path.isfile(DataFile): print("Data file exists") else: print("Create new Data file") with open(DataFile, "w", newline="") as f: writer = csv.writer(f) writer.writerow(['Date', 'Ah', 'I', 'V']) writer.writerow([0, 0, 0]) def updateDataFile (v,c,ah,x2): with open(DataFile, "a", newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow([x2,ah,c,v]) #STORES THE DATA SHOWN IN "TILASTOT" VIEW #************************************************************************************** def format_timedelta(td): total_seconds = int(td.total_seconds()) hours, remainder = divmod(total_seconds, 3600) minutes, seconds = divmod(remainder, 60) return f"{hours:02}:{minutes:02}:{seconds:02}" def updateStatusfile(v, c, e, eIn, eOut, timestamp): global cumulativeChargeTime with open(StatusFile, "w", newline='', encoding='utf-8') as f: writer = csv.writer(f) #print("XXXXXX Update Status") writer.writerows([ ["Voltage/V", v], ["Current/A", c], ["Energy/Ah", e], ["EnergyIn", eIn], ["EnergyOut", eOut], ["Timestamp", timestamp], ["ChargeTime", format_timedelta(ChargeTime)], ["ChargeTimeTot", format_timedelta(cumulativeChargeTime)] ]) # At SW start, this reads the last status data from the file def readStatusfile(): global EnergyIn, EnergyOut, Energy2, ChargeTime, ChargeTimeTot data = {} with open(StatusFile, 'r') as f: reader = csv.reader(f) for row in reader: if len(row) == 2: label, value = row data[label] = value try: Energy2 = float(data.get("Energy/Ah", 0.0)) EnergyIn = float(data.get("EnergyIn", 0.0)) EnergyOut = float(data.get("EnergyOut", 0.0)) ChargeTime = parse_timedelta(data.get("ChargeTime", "00:00:00")) ChargeTimeTot = parse_timedelta(data.get("ChargeTimeTot", "00:00:00")) except ValueError as e: print("Error parsing values:", e) def parse_timedelta(time_str): try: time_parts = list(map(int, time_str.split(':'))) if len(time_parts) == 3: return timedelta(hours=time_parts[0], minutes=time_parts[1], seconds=time_parts[2]) elif len(time_parts) == 4: return timedelta(hours=time_parts[0], minutes=time_parts[1], seconds=time_parts[2], milliseconds=time_parts[3]) except ValueError: print("Invalid time format:", time_str) return timedelta() def updateControlData0(Labelx, Datax): global ControlData0 for control in ControlData0: if control[0] == Labelx: control[1] = Datax print("New control data:", Labelx, Datax) break return def get_control_value(label): for control in ControlData0: if control[0] == label: return float(control[1]) return None #This function writes ControlData0 back into file.This can be used to change controls from Python! def updateControlfile(): global ControlData0 with open(Controlfile, "w", newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerows(ControlData0) print("Controlfile updated") def updateGraphFile (): global values, values2, values3, x with open(GraphFile, "w", newline='', encoding='utf-8') as f: writer = csv.writer(f) for ind in range(0,XLength): writer.writerow([x[ind],values[ind],values2[ind],values3[ind]]) def readGraphFile (): # File opened with w for adding data global values, values2, values3, x with open(GraphFile, "r", newline='', encoding='utf-8') as f: reader = csv.reader(f) GraphData = list(reader) # Check that oldest time stamp is less than 24 hours and data is then relevant Time1 = datetime.datetime.strptime(GraphData[1][0], '%Y-%m-%d %H:%M:%S') Timeperiod = ((datetime.datetime.now() - Time1)/3600).seconds if Timeperiod > 24: return for ind in range(0,XLength): time = GraphData[ind][0] x[ind] = datetime.datetime.strptime(time, '%Y-%m-%d %H:%M:%S') values[ind] = float(GraphData[ind][1]) values2[ind] = float(GraphData[ind][2]) values3[ind] = float(GraphData[ind][3]) print ("Max value", min(values3)) # Checks current and voltage and changes state accordingly #************************************************************************************** def AutoStart(Energy): global Energy2, Voltage, Current, State #print(f"Condition {State} {Energy2} {AhStart} {Voltage} {VStart}") if State == "Idle" and ((Energy2 < AhStart or Voltage <= VStart)): print(f"Start condition {Energy2} {AhStart} {Voltage} {VStart}") startCharge() elif State == "Charge" and ((Energy2 > AhStop or Voltage > VStop)): print(f"Stop condition {Energy2} {AhStop} {Voltage} {VStop}") stopCharge() return 1 def startCharge (): global State, shortTimerOn, chargeStartTime State = "Charge" chargeStartTime = time.time() readControlfile() updateControlData0("Load",1) updateControlData0("Charge",1) updateControlData0("GraphT",5) updateControlfile() def stopCharge (): global State State = "Idle" readControlfile() updateControlData0("Charge",0) updateControlData0("GraphT",5) updateControlfile() return # MAIN #************************************************************************************** if __name__ == '__main__': serialArduino = serial.Serial(port, baud, timeout=1) time.sleep(1) serialArduino.flushInput() serialArduino.flushOutput() # CONTROL DATA TABLE INITIALIZATION #************************************************************************************** XLength = 600 values = [0] * XLength values2 = [0] * XLength values3 = [0] * XLength #x = [datetime.datetime.now() + datetime.timedelta(seconds=i - XLength) for i in range(XLength)] x = [datetime.datetime.now().replace(microsecond=0) + datetime.timedelta(seconds=i - XLength) for i in range(XLength)] #plt.ion() # Tell matplotlib you want interactive mode to plot live data initControlData() # TIME VARIABLE DEFINITIONS #************************************************************************************** ChargeTime = datetime.timedelta(hours=0, minutes=0, seconds=0) Time0 = datetime.datetime.now() startTime = time.time() # Timer for 5 sec interval setting, until switch back to 120 sec shortTimerOn = 0 # Flag for 5 sec interval setting graphTime = time.time() # Timer for storing graph data # INITIALIZE TABLES #************************************************************************************** createDataFile() readStatusfile() readGraphFile() readControlfile() updateControlData0("GraphT", 5) updateControlfile() print("TGraph A =", ControlData0[0][1]) while True: readControlfile() readArduino(Energy2) if get_control_value("AutoStart") == 1:# Autostart activated AutoStart(200) if get_control_value("Charge") == 1: State = "Charge" if get_control_value("Load") == 0: updateControlData0("Load", 1) else: State = "Idle" #time.sleep(1) # Counter to remove 5 sec interval setting after sec5Timer if shortTimerOn == 0 and float(ControlData0[0][1]) == 5: shortTimerOn = 1 startTime = time.time() elif shortTimerOn == 1 and (time.time() - startTime) > sec5Timer: shortTimerOn = 0 updateControlData0("GraphT", 120) updateControlfile() sendToArduino("GraphT", "120") # Counter to store graph data after graphTimer if (time.time() - graphTime) > graphTimer: updateGraphFile() print("Graph data stored") graphTime = time.time() # Update cumulative charge time if state is Charge if State == "Charge": elapsed_time = time.time() - chargeStartTime ChargeTime += timedelta(seconds=elapsed_time) chargeStartTime = time.time() # Reset the start time for the next interval else: chargeStartTime = time.time() # Reset the start time for the next interval # Print the cumulative charge time as a timedelta object # print(ChargeTime)