├── README.md ├── bms_post.py ├── bms_post.service ├── si.py ├── si_control.py └── solarinvert.service /README.md: -------------------------------------------------------------------------------- 1 | #Description 2 | This is a set of python scripts used for controlling a 3 | DIY battery storage for a Photovoltaic based system. 4 | Two python scripts plus a iobroker installation on a 5 | raspberry pi. 6 | 7 | bms_post.py is used to parse the information coming 8 | via bluetooth from a china made Battery Monitoring System 9 | which is taking care of a 16s LIFEPO 100Ah bank. 10 | The type of BMS is ANT (can be found on aliexpress) 11 | The script reads info from the serial port and sends it 12 | via simple_api to iobroker signals 13 | 14 | si_control.py is the main control script: It reads some 15 | information directly from the pI (ADC converter reading 16 | battery pack voltage) as well as grid and photovoltaic 17 | powers (using TCP Modbus from a Fronius Inverter). Some 18 | signals are read from iobroker via simple_api. 19 | 20 | There is a branched influxdb variant, which is simpler 21 | and more robust. It dows not need iobroker to run since 22 | it posts directly from the python script to the influxdb 23 | server 24 | 25 | To be able to automatically connect to the bluetooth of 26 | the BMS this solution uses the following: 27 | 28 | This is part of the si_control.py file 29 | ``` 30 | # Define Serial port (over bluetooth) for BMS 31 | ser_blue = serial.Serial( 32 | port='/dev/rfcomm0', 33 | baudrate = 9600, 34 | parity=serial.PARITY_NONE, 35 | stopbits=serial.STOPBITS_ONE, 36 | bytesize=serial.EIGHTBITS, 37 | timeout = 0) 38 | ``` 39 | 40 | And in /etc/bluetooth/rfcomm.conf put 41 | ``` 42 | rfcomm0 { 43 | # Automatically bind the device at startup 44 | bind yes; 45 | 46 | # Bluetooth address of the device 47 | device AA:BB:CC:A1:23:45; 48 | 49 | # RFCOMM channel for the connection 50 | channel 0; 51 | 52 | # Description of the connection 53 | comment "BMS"; 54 | } 55 | 56 | ``` 57 | -------------------------------------------------------------------------------- /bms_post.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import serial 4 | import struct 5 | from binascii import unhexlify 6 | import requests as req 7 | 8 | #Define RS485 serial port 9 | ser = serial.Serial( 10 | port='/dev/rfcomm0', 11 | baudrate = 9600, 12 | parity=serial.PARITY_NONE, 13 | stopbits=serial.STOPBITS_ONE, 14 | bytesize=serial.EIGHTBITS, 15 | timeout = 0) 16 | url = 'http://localhost:8087/set/vis.0.' 17 | 18 | #104 19 | MOSFET_Discharge_St=["OFF","ON","cell overdischarge","overcurrent","4","pack overdischarge", 20 | "bat overtemp","MOSFET overtemp","Abnormal current","battery is not detected", 21 | "PCB overtemp","charge MOSFET turn on","shortcircuit","Discharge MOSFET abnormality", 22 | "Start exception","Manual off"] 23 | #103 24 | MOSFET_Charge_St=["OFF","ON","overcharge","overcurrent","batt full","pack overvoltage", 25 | "bat overtemp","MOSFET overtemp","abnormal current","bat not detected", 26 | "PCB overtemp","11-undefined","12-undefined","Discharge MOSFET abnormality","14","Manual off"] 27 | #134-135 28 | Bal_St=["OFF","limit trigger exceeds","charging v diff too high","overtemp","ACTIVE", 29 | "5-udef","6-udef","7-udef","8-udef","9-udef","PCB Overtemp"] 30 | while True : 31 | test='DBDB00000000' 32 | try: 33 | ser.write (test.decode('hex')) 34 | except: 35 | ser.close() 36 | time.sleep(1) 37 | if(ser.isOpen() == False): 38 | ser.open() 39 | Antw33 = ser.read(140) 40 | # print Antw33 41 | #SoC 42 | data = (Antw33.encode('hex') [(74*2):(75*2)]) 43 | try: 44 | resp = req.get(url+'SoC'+'?value='+str(int(data,16))) 45 | # print 46 | except: 47 | pass 48 | #Power 49 | data = (Antw33.encode('hex') [(111*2):(114*2+2)]) 50 | try: 51 | if int(data,16)>2147483648: 52 | data=(-(2*2147483648)+int(data,16)) 53 | else: 54 | data=int(data,16) 55 | resp = req.get(url+'BMS_pow'+'?value='+str(data)) 56 | except: 57 | pass 58 | #MOSFET Status 59 | data = (Antw33.encode('hex') [103*2:103*2+2]) 60 | try: 61 | resp = req.get(url+'BMS_MOSFET_Ch_St'+'?value='+MOSFET_Charge_St[int(data,16)]) 62 | except: 63 | pass 64 | data = (Antw33.encode('hex') [104*2:104*2+2]) 65 | try: 66 | resp = req.get(url+'BMS_MOSFET_Disch_St'+'?value='+MOSFET_Discharge_St[int(data,16)]) 67 | except: 68 | pass 69 | 70 | #BALANCING STATUS 71 | data = (Antw33.encode('hex') [105*2:105*2+2]) 72 | try: 73 | resp = req.get(url+'BMS_Bal_St'+'?value='+Bal_St[int(data,16)]) 74 | except: 75 | pass 76 | 77 | data = Antw33.encode('hex') [134*2:135*2+2] 78 | try: 79 | data=struct.unpack('>H',unhexlify(data))[0] 80 | except: 81 | data=0xFFFF 82 | try: 83 | resp = req.get(url+'BMS_Bal0'+'?value='+str(data>>0&1)) 84 | resp = req.get(url+'BMS_Bal1'+'?value='+str(data>>1&1)) 85 | resp = req.get(url+'BMS_Bal2'+'?value='+str(data>>2&1)) 86 | resp = req.get(url+'BMS_Bal3'+'?value='+str(data>>3&1)) 87 | resp = req.get(url+'BMS_Bal4'+'?value='+str(data>>4&1)) 88 | resp = req.get(url+'BMS_Bal5'+'?value='+str(data>>5&1)) 89 | resp = req.get(url+'BMS_Bal6'+'?value='+str(data>>6&1)) 90 | resp = req.get(url+'BMS_Bal7'+'?value='+str(data>>7&1)) 91 | resp = req.get(url+'BMS_Bal8'+'?value='+str(data>>8&1)) 92 | resp = req.get(url+'BMS_Bal9'+'?value='+str(data>>9&1)) 93 | resp = req.get(url+'BMS_Bal10'+'?value='+str(data>>10&1)) 94 | resp = req.get(url+'BMS_Bal11'+'?value='+str(data>>11&1)) 95 | resp = req.get(url+'BMS_Bal12'+'?value='+str(data>>12&1)) 96 | resp = req.get(url+'BMS_Bal13'+'?value='+str(data>>13&1)) 97 | resp = req.get(url+'BMS_Bal14'+'?value='+str(data>>14&1)) 98 | resp = req.get(url+'BMS_Bal15'+'?value='+str(data>>15&1)) 99 | except: 100 | pass 101 | 102 | #BMS_Current 103 | data = (Antw33.encode('hex') [(70*2):(73*2+2)]) 104 | try: 105 | if int(data,16)>2147483648: 106 | data=(-(2*2147483648)+int(data,16))*0.1 107 | else: 108 | data = int(data,16)*0.1 109 | resp = req.get(url+'BMS_Current'+'?value='+str(data)) 110 | except: 111 | pass 112 | #BMS V 113 | data = (Antw33.encode('hex') [8:12]) 114 | try: 115 | resp = req.get(url+'BMS_V'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.1)) 116 | except: 117 | pass 118 | 119 | #Cell_avg 120 | data = (Antw33.encode('hex') [(121*2):(122*2+2)]) 121 | try: 122 | resp = req.get(url+'cell_avg'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 123 | except: 124 | pass 125 | #Cell_min 126 | data = (Antw33.encode('hex') [(119*2):(120*2+2)]) 127 | try: 128 | resp = req.get(url+'cell_min'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 129 | except: 130 | pass 131 | #Cell_max 132 | data = (Antw33.encode('hex') [(116*2):(117*2+2)]) 133 | try: 134 | resp = req.get(url+'cell_max'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 135 | except: 136 | pass 137 | #Cell_1 138 | data = (Antw33.encode('hex') [(6*2):(7*2+2)]) 139 | try: 140 | resp = req.get(url+'cell1'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 141 | except: 142 | pass 143 | #Cell_2 144 | data = (Antw33.encode('hex') [(8*2):(9*2+2)]) 145 | try: 146 | resp = req.get(url+'cell2'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 147 | except: 148 | pass 149 | #Cell_3 150 | data = (Antw33.encode('hex') [(10*2):(11*2+2)]) 151 | try: 152 | resp = req.get(url+'cell3'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 153 | except: 154 | pass 155 | #Cell_4 156 | data = (Antw33.encode('hex') [(12*2):(13*2+2)]) 157 | try: 158 | resp = req.get(url+'cell4'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 159 | except: 160 | pass 161 | #Cell_5 162 | data = (Antw33.encode('hex') [(14*2):(15*2+2)]) 163 | try: 164 | resp = req.get(url+'cell5'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 165 | except: 166 | pass 167 | #Cell_6 168 | data = (Antw33.encode('hex') [(16*2):(17*2+2)]) 169 | try: 170 | resp = req.get(url+'cell6'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 171 | except: 172 | pass 173 | #Cell_7 174 | data = (Antw33.encode('hex') [(18*2):(19*2+2)]) 175 | try: 176 | resp = req.get(url+'cell7'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 177 | except: 178 | pass 179 | #Cell_8 180 | data = (Antw33.encode('hex') [(20*2):(21*2+2)]) 181 | try: 182 | resp = req.get(url+'cell8'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 183 | except: 184 | pass 185 | #Cell_9 186 | data = (Antw33.encode('hex') [(22*2):(23*2+2)]) 187 | try: 188 | resp = req.get(url+'cell9'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 189 | except: 190 | pass 191 | #Cell_10 192 | data = (Antw33.encode('hex') [(24*2):(25*2+2)]) 193 | try: 194 | resp = req.get(url+'cell10'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 195 | except: 196 | pass 197 | #Cell_11 198 | data = (Antw33.encode('hex') [(26*2):(27*2+2)]) 199 | try: 200 | resp = req.get(url+'cell11'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 201 | except: 202 | pass 203 | #Cell_12 204 | data = (Antw33.encode('hex') [(28*2):(29*2+2)]) 205 | try: 206 | resp = req.get(url+'cell12'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 207 | except: 208 | pass 209 | #Cell_13 210 | data = (Antw33.encode('hex') [(30*2):(31*2+2)]) 211 | try: 212 | resp = req.get(url+'cell13'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 213 | except: 214 | pass 215 | #Cell_14 216 | data = (Antw33.encode('hex') [(32*2):(33*2+2)]) 217 | try: 218 | resp = req.get(url+'cell14'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 219 | except: 220 | pass 221 | #Cell_15 222 | data = (Antw33.encode('hex') [(34*2):(35*2+2)]) 223 | try: 224 | resp = req.get(url+'cell15'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 225 | except: 226 | pass 227 | #Cell_16 228 | data = (Antw33.encode('hex') [(36*2):(37*2+2)]) 229 | try: 230 | resp = req.get(url+'cell16'+'?value='+str((struct.unpack('>H',unhexlify(data))[0])*0.001)) 231 | except: 232 | pass 233 | 234 | 235 | 236 | time.sleep(10) 237 | ser.close() 238 | -------------------------------------------------------------------------------- /bms_post.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=BMS posting into iobroker 3 | After=iobroker.service 4 | 5 | [Service] 6 | User=pi 7 | Type=idle 8 | ExecStart=/usr/bin/python /home/pi/bms_post.py 9 | Restart=always 10 | RestartSec=3 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | 15 | -------------------------------------------------------------------------------- /si.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | import time 4 | import crcmod 5 | import serial 6 | 7 | #POSITIVE means discharge battery 8 | def build_data ( pow ) : 9 | crc16_modbus = crcmod.mkCrcFun(0x18005, rev=True, initCrc=0xFFFF, xorOut=0x0000) 10 | if pow >= 0 : 11 | power = str('%04X' % (pow)) 12 | else: 13 | pow = -pow 14 | power = str('8''%03X' % (pow)) 15 | Adr = '0B3F0D' 16 | #Rest = '01f9021c00960000000000' 50.5V 54V 17 | # Rest = '01f9022b00960000000000' 18 | Rest = '01db023000960000000000' 19 | data = (Adr+str(power)+Rest) 20 | crc = (crc16_modbus(data.decode('hex'))) 21 | crch = (crc & 0xff) 22 | crcl=(((crc>>8) & 0xff)) 23 | crchst=('%02X' % crch) 24 | crclst= ('%02X' % crcl) 25 | Send=(data.decode('hex'))+(crchst.decode('hex'))+crclst.decode('hex') 26 | return Send 27 | 28 | #Define RS485 serial port 29 | ser = serial.Serial( 30 | port='/dev/ttyUSB0', 31 | baudrate = 57600, 32 | parity=serial.PARITY_NONE, 33 | stopbits=serial.STOPBITS_ONE, 34 | bytesize=serial.EIGHTBITS, 35 | timeout = 0) 36 | while True: 37 | value = int(sys.argv[1]) 38 | print 'Setting SI to ', value, 'W' 39 | data_stream = build_data(value) 40 | test=data_stream.encode('hex') 41 | ser.write (test.decode('hex')) 42 | time.sleep(5) 43 | #Anfr33 = '0b330101325f' 44 | #ser.write (Anfr33.decode('hex')) 45 | #Antw33 = ser.read(42) 46 | #batp = (Antw33.encode('hex') [20:24]) 47 | #print (batp) 48 | -------------------------------------------------------------------------------- /si_control.py: -------------------------------------------------------------------------------- 1 | import requests as req 2 | import sys 3 | import time 4 | from time import strftime 5 | import crcmod 6 | import serial 7 | from pymodbus.constants import Endian 8 | from pymodbus.payload import BinaryPayloadDecoder 9 | from pymodbus.client.sync import ModbusTcpClient as ModbusClient 10 | from pymodbus.diag_message import * 11 | from pymodbus.file_message import * 12 | from pymodbus.other_message import * 13 | from pymodbus.mei_message import * 14 | import struct 15 | from binascii import unhexlify 16 | 17 | # Limit inverter power 18 | maxpower = 2200 19 | minpower = 2200 20 | # Declare 21 | si_power = 0 22 | si_volt = 0 23 | wh_daily = 0 24 | 25 | 26 | def set_power(grid, PV, charger, si_power, action): 27 | # Send to iobroker values for diagram purposes 28 | url = 'http://localhost:8087/set/vis.0.' 29 | resp = req.get(url + 'Grid' + '?value=' + str(grid)) 30 | resp = req.get(url + 'PV' + '?value=' + str(PV)) 31 | resp = req.get(url + 'control_status' + '?value=' + action) 32 | if charger < 0 and si_power < 0: 33 | req.get(url + 'Batt_Charge' + '?value=' + str(-si_power)) 34 | req.get(url + 'Batt_Discharge' + '?value=' + str(0)) 35 | elif charger > 0: 36 | req.get(url + 'Batt_Charge' + '?value=' + str(0)) 37 | if si_power > 0: 38 | req.get(url + 'Batt_Discharge' + '?value=' + str(si_power)) 39 | else: 40 | req.get(url + 'Batt_Charge' + '?value=' + str(0)) 41 | req.get(url + 'Batt_Discharge' + '?value=' + str(0)) 42 | return () 43 | 44 | 45 | def get_wh_daily(wh_total, today): 46 | if int(strftime("%d", time.localtime())) != today: # New Day 47 | today = int(strftime("%d", time.localtime())) # Change day 48 | wh_total = GET_SI_WH() # Reference total counter 49 | wh_day = GET_SI_WH() - wh_total # Calculate current daily 50 | url = 'http://localhost:8087/set/vis.0.' 51 | resp = req.get(url + 'wh_day' + '?value=' + str(wh_day)) 52 | resp = req.get(url + 'SI_Wh_total' + '?value=' + str(wh_total)) 53 | return (wh_total, wh_day, today) 54 | 55 | 56 | # POSITIVE means discharge battery 57 | def build_data(pow): 58 | crc16_modbus = crcmod.mkCrcFun(0x18005, rev=True, initCrc=0xFFFF, xorOut=0x0000) 59 | if pow >= 0: 60 | power = str('%04X' % (pow)) 61 | else: 62 | pow = -pow 63 | power = str('8''%03X' % (pow)) 64 | Adr = '0B3F0D' 65 | # Rest = '01f9021c00960000000000' #50.5V 54V 66 | # Rest = '01f9023500960000000000' #50.5V 56.5V 67 | Rest = '01e0023500960000000000' # 48 56.5V 68 | # Rest = '01f9023000960000000000' #50.5V 56V 69 | # Rest = '01f9022100960000000000' #50.5V 54.5V 70 | # Rest = '01f9022b00960000000000' #50.5V 55.5V 71 | # Rest = '01f9021900960000000000' #50.5V 53.7V 72 | data = (Adr + str(power) + Rest) 73 | crc = (crc16_modbus(data.decode('hex'))) 74 | crch = (crc & 0xff) 75 | crcl = (((crc >> 8) & 0xff)) 76 | crchst = ('%02X' % crch) 77 | crclst = ('%02X' % crcl) 78 | Send = (data.decode('hex')) + (crchst.decode('hex')) + crclst.decode('hex') 79 | return Send 80 | 81 | 82 | def GET_SI(): 83 | Anfr33 = '0b330101325f' 84 | ser.write(Anfr33.decode('hex')) 85 | time.sleep(0.1) 86 | Antw33 = ser.read(42) 87 | Antw33h = Antw33.encode('hex') 88 | #print('Sent %s read %s' % (Anfr33, Antw33h)) 89 | reply_ID = (Antw33.encode('hex')[2:4]) 90 | if (reply_ID) == '3f': 91 | #Chop off 0b3f0101f25c bytes 92 | Antw33 = Antw33[6:41] 93 | #print('Chopped to %s' % Antw33.encode('hex')) 94 | reply_ID = (Antw33.encode('hex')[2:4]) 95 | 96 | if reply_ID == '33': 97 | batp = (Antw33.encode('hex')[20:24]) 98 | batu = (Antw33.encode('hex')[6:10]) 99 | try: 100 | si_power = struct.unpack('>H', unhexlify(batp))[0] 101 | si_volt = struct.unpack('>H', unhexlify(batu))[0] * 0.108 102 | except: 103 | si_volt = 0 104 | si_power = 0 105 | time_s = strftime("%y/%m/%d %H:%M:%S ", time.localtime()) 106 | print('%s Wrong feedback from Battery Inverter' % (time_s)) 107 | if si_power > 25000: # negative value comes with twos compl 108 | si_power = -(-32768 + si_power) * 0.1 109 | else: 110 | si_power = si_power * 0.1 111 | else: # no update possible 112 | si_power = 12345 113 | si_volt = 12345 114 | # print('SI does not answer to request, setting fdb to 12345') 115 | #print('Power read SI %d ' % si_power) 116 | return (si_power, si_volt) 117 | 118 | 119 | def GET_SI_WH(): 120 | Anfr3E = '0b3e0101a39c' 121 | ser.write(Anfr3E.decode('hex')) 122 | time.sleep(0.1) 123 | Antw3E = ser.read(38) 124 | # Antw3Eh = Antw3E.encode('hex') 125 | # print ('Sent %s read %s'%(Anfr3E,Antw3Eh)) 126 | NRG = (Antw3E.encode('hex')[18:26]) 127 | NRG = int(NRG, 16) / 36000 128 | # print ('%f kWh total' % NRG) 129 | return (NRG) 130 | 131 | 132 | time.sleep(10) 133 | # Define RS485 serial port 134 | ser = serial.Serial( 135 | port='/dev/ttyUSB0', 136 | baudrate=57600, 137 | parity=serial.PARITY_NONE, 138 | stopbits=serial.STOPBITS_ONE, 139 | bytesize=serial.EIGHTBITS, 140 | timeout=0) 141 | 142 | # Connect to inverter 143 | ip = '192.168.0.71' # This is the ip from your fronius inverter 144 | client = ModbusClient(ip, port=502) 145 | client.connect() 146 | 147 | load = 0 # (+) 148 | pv = 0 # (+) 149 | grid = int(0) # Total going to GRID 150 | charger = int(10) 151 | charger_old = 0 152 | batt_full_flag = 0 153 | batt_empty_flag = 0 154 | SoC_target_int = 90 155 | SoC_min_target_int = 15 156 | count = 0 157 | AVM = 0 158 | si_power = 0 159 | si_volt = 0 160 | # try: 161 | # wh_total=GET_SI_WH() 162 | # except: 163 | # print("WH_TOTAL funtion call error") 164 | wh_total = 0; 165 | today = int(strftime("%M", time.localtime())) 166 | while 1: 167 | try: 168 | BMS_SoC = int(req.get('http://localhost:8087/getPlainValue/vis.0.SoC').text) 169 | except: 170 | BMS_SoC = 0 # Default to meas. Voltage based SoC if BMS_SoC not available 171 | # GET GRID VALUE from SMARTMETER 172 | try: 173 | value = client.read_holding_registers(40098 - 1, 2, unit=240) 174 | smACPower = BinaryPayloadDecoder.fromRegisters(value.registers, byteorder=Endian.Big, 175 | wordorder=Endian.Big) 176 | grid = int(smACPower.decode_32bit_float()) 177 | except: 178 | time_s = strftime("%y/%m/%d %H:%M:%S ", time.localtime()) 179 | print('%s WARNING Modbus smartmeter not answering' % (time_s)) 180 | try: 181 | value = client.read_holding_registers(40092 - 1, 2, unit=1) 182 | sf = BinaryPayloadDecoder.fromRegisters(value.registers, byteorder=Endian.Big, 183 | wordorder=Endian.Big) 184 | pv = int(sf.decode_32bit_float()) 185 | except: 186 | time_s = strftime("%y/%m/%d %H:%M:%S ", time.localtime()) 187 | print('%s WARNING Modbus Inverter not answering' % (time_s)) 188 | try: # Read from iobroker the user whised max SoC 189 | SoC_target = int(req.get('http://localhost:8087/getPlainValue/vis.0.SoC_target').text) 190 | if SoC_target > 10 and SoC_target <= 100 and SoC_target != SoC_target_int: 191 | SoC_target_int = SoC_target 192 | batt_full_flag = 0 193 | time_s = strftime("%y/%m/%d %H:%M:%S ", time.localtime()) 194 | print('%s updating SoC Target' % (time_s)) 195 | except: 196 | pass 197 | try: # Read from Iobroker the user wished min SoC 198 | SoC_min_target = int(req.get('http://localhost:8087/getPlainValue/vis.0.SoC_min_target').text) 199 | if SoC_min_target >= 0 and SoC_min_target <= 100 and SoC_min_target != SoC__min_target_int: 200 | SoC_min_target_int = SoC_min_target 201 | except: 202 | pass 203 | 204 | # control loop start here 205 | if grid < -10 and batt_full_flag == 0: 206 | charger -= 2 # slowly increment charger 207 | if grid < -80: 208 | charger += grid # setpoint charger 209 | if grid < -20: 210 | charger -= 10 211 | if charger < 0: 212 | action = "Inc.Charg" 213 | else: 214 | action = "Dec.Supply" 215 | if charger <= -minpower: # Upper limit 216 | charger = -minpower 217 | action = "Charg.Max" 218 | if BMS_SoC >= SoC_target_int: # Batt. full 219 | charger = 0 220 | action = "Max.SoC" 221 | batt_full_flag = 1 222 | time_s = strftime("%y/%m/%d %H:%M:%S ", time.localtime()) 223 | print('%s Batt Full' % (time_s)) 224 | 225 | elif grid > 10: 226 | charger += grid # setpoint charger 227 | action = "Inc.Supply" 228 | if charger > maxpower: # Lower limit 229 | charger = maxpower 230 | action = "Suppl.Max" 231 | if BMS_SoC < (SoC_target - 2) and batt_full_flag == 1: 232 | batt_full_flag = 0 233 | 234 | 235 | elif batt_full_flag == 1 and grid < 0: # and pv > 150: #Batt Full and PV available 236 | if charger > 0: # Grid injection is not coming from PV but from Supply 237 | charger += grid # Supply less 238 | action = "Decr.Supply" 239 | else: 240 | charger = 0 # Supply was off, Turn SI OFF 241 | action = "Batt_Full" 242 | else: 243 | action = "Opt._Ctrl" 244 | 245 | # Avoid toggling after big load turns off and system changes from supply to charge 246 | # even though PV is not enough (or even 0) 247 | if (pv + charger) < 0: # if charger bigger than photovoltaik 248 | time_s = strftime("%y/%m/%d %H:%M:%S", time.localtime()) 249 | # print ('%s Avoid toggle charger %d pv %d' %(time_s,charger,pv)) 250 | charger = -pv 251 | if pv == 0: 252 | charger = - 100 # keep supplying something, avoid turning SI off 253 | 254 | # Battery lower limit 255 | if BMS_SoC <= SoC_min_target: 256 | batt_empty_flag = 1 257 | if BMS_SoC > (SoC_min_target + 2): 258 | batt_empty_flag = 0 259 | if (charger > 0) and (batt_empty_flag == 1): 260 | charger = 0 # Stop discharging the battery 261 | action = "Empty" 262 | 263 | (si_power_new, si_volt_new) = GET_SI() 264 | # print('charger value',charger) 265 | if si_power_new != 12345: # Updated values available 266 | si_power = si_power_new 267 | si_volt = si_volt_new 268 | elif charger == 0: # Looks like no updates du to inverter turned off 269 | print ('Setting SI Power to zero since it does not update feedback') 270 | si_power = 0 # so setting the value to zero for nice plots 271 | 272 | set_power(grid, pv, charger, si_power, action) 273 | if count == 5: 274 | try: 275 | (wh_total, wh_daily, today) = get_wh_daily(wh_total, today) 276 | count = 0 277 | except: 278 | AVM = -9999 279 | else: 280 | count += 1 281 | if charger > 0: 282 | modus = "Supply" 283 | else: 284 | modus = "Charge" 285 | # if charger >-100 and charger_old >-100 : #Do not charge if lower than 100 W (efficiency very bad) 286 | # charger = 0 287 | 288 | # VERBOSE MODE for logging to file 289 | time_s = strftime("%y/%m/%d %H:%M:%S ", time.localtime()) 290 | # print ('%s%s %04d act %04.0f PV %04d GRID %04d PACK %05.2f SoC %2.1f Wh day %d total %d Wh %s' 291 | # %(time_s, modus, charger, si_power, pv, grid, si_volt, BMS_SoC , wh_daily, wh_total, action)) 292 | data_stream = build_data(charger) 293 | test = data_stream.encode('hex') 294 | ser.write(test.decode('hex')) 295 | if (charger_old >= 0 and charger < 0) or (charger_old <= 0 and charger > 0): 296 | time.sleep(15) 297 | print("MOde switch, wait 15s") 298 | else: 299 | time.sleep(2) 300 | charger_old = charger # Remember last cycle mode 301 | client.close() 302 | -------------------------------------------------------------------------------- /solarinvert.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=SolarInvert control script 3 | After=iobroker.service 4 | 5 | [Service] 6 | User=pi 7 | Type=idle 8 | ExecStart=/usr/bin/python /home/pi/si_control.py 9 | # ExecStart=nohup stdbuf -o 0 /usr/bin/python /home/pi/si_control.py > /home/pi/si_control.log 10 | # StandardOutput=syslog 11 | # StandardError=syslog 12 | Restart=always 13 | RestartSec=3 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | 18 | --------------------------------------------------------------------------------