diff --git a/byd_bat/__init__.py b/byd_bat/__init__.py index 5419a7224..ea5e2ff22 100644 --- a/byd_bat/__init__.py +++ b/byd_bat/__init__.py @@ -40,6 +40,19 @@ # V0.0.5 231030 - Diagnose ergaenzt: Bat-Voltag, V-Out, Current # - Liste der Wechselrichter aktualisiert # - Alle Plot-Dateien beim Plugin-Start loeschen +# - Anpassungen fuer mathplotlib 3.8.0 mit requirements.txt +# - webif aktualisiert (Uebersetzungen, Parameter) +# +# V0.0.6 231126 - Diagnose ergaenzt Temperatur max/min +# - Auslesen und anzeigen der Balancing-Flags im Plot +# - Plot diverse Fehler korrigiert +# - Plot-Dateien loeschen ueberarbeitet +# - Batterietypen HVS, HVM und LVS im Plot getestet +# +# V0.0.7 231209 - item_structs.byd_struct.enable_connection neu +# true -> Kommunikation mit BYD aktiv, false -> keine Kommunikation +# - Temperatur Fehler beim Auslesen korrigiert +# - Neuer Parameter 'diag_cycle' fuer Abfrage der Diagnosedaten # # ----------------------------------------------------------------------- # @@ -68,13 +81,16 @@ import numpy as np import os +#import random # only for internal test + byd_ip_default = "192.168.16.254" -scheduler_name = 'mmbyd' +scheduler_name = 'byd_bat' BUFFER_SIZE = 4096 byd_sample_basics = 60 # Abfrage fuer Basisdaten [s] +byd_sample_diag = 300 # Abfrage fuer Diagnosedaten [s] byd_timeout_1s = 1.0 byd_timeout_2s = 2.0 @@ -84,7 +100,9 @@ byd_cells_max = 160 byd_temps_max = 64 -byd_no_of_col = 8 +byd_no_of_col_7 = 7 +byd_no_of_col_8 = 8 +byd_no_of_col_12 = 12 byd_webif_img = "/webif/static/img/" byd_path_empty = "x" @@ -92,24 +110,28 @@ byd_fname_temp = "bydtt" byd_fname_ext = ".png" -MESSAGE_0 = "010300000066c5e0" -MESSAGE_1 = "01030500001984cc" -MESSAGE_2 = "010300100003040e" - -MESSAGE_3_1 = "0110055000020400018100f853" # Start Messung Turm 1 -MESSAGE_3_2 = "01100550000204000281000853" # Start Messung Turm 2 -MESSAGE_3_3 = "01100550000204000381005993" # Start Messung Turm 3 -MESSAGE_4 = "010305510001d517" -MESSAGE_5 = "01030558004104e5" -MESSAGE_6 = "01030558004104e5" -MESSAGE_7 = "01030558004104e5" -MESSAGE_8 = "01030558004104e5" - -MESSAGE_9 = "01100100000306444542554700176f" # switch to second turn for the last few cells (not tested, perhaps only for tower 1 ?) -MESSAGE_10 = "0110055000020400018100f853" # start measuring remaining cells (like 3a) (not tested, perhaps only for tower 1 ?) -MESSAGE_11 = "010305510001d517" # (like 4) (not tested) -MESSAGE_12 = "01030558004104e5" # (like 5) (not tested) -MESSAGE_13 = "01030558004104e5" # (like 6) (not tested) +MESSAGE_0 = "010300000066c5e0" +MESSAGE_1 = "01030500001984cc" +MESSAGE_2 = "010300100003040e" + +MESSAGE_3_1 = "0110055000020400018100f853" # Start Messung Turm 1 +MESSAGE_3_2 = "01100550000204000281000853" # Start Messung Turm 2 +MESSAGE_3_3 = "01100550000204000381005993" # Start Messung Turm 3 +MESSAGE_4 = "010305510001d517" +MESSAGE_5 = "01030558004104e5" +MESSAGE_6 = "01030558004104e5" +MESSAGE_7 = "01030558004104e5" +MESSAGE_8 = "01030558004104e5" + # to read the 5th module, the box must first be reconfigured (not tested) +MESSAGE_9 = "01100100000306444542554700176f" # switch to second turn for the last few cells +MESSAGE_10_1 = "0110055000020400018100f853" # start measuring remaining cells in tower 1 (like 3) +MESSAGE_10_2 = "01100550000204000281000853" # start measuring remaining cells in tower 2 (like 3) +MESSAGE_10_3 = "01100550000204000381005993" # start measuring remaining cells in tower 3 (like 3) +MESSAGE_11 = "010305510001d517" # (like 4) +MESSAGE_12 = "01030558004104e5" # (like 5) +MESSAGE_13 = "01030558004104e5" # (like 6) +MESSAGE_14 = "01030558004104e5" # (like 7) +MESSAGE_15 = "01030558004104e5" # (like 8) byd_errors = [ "High Temperature Charging (Cells)", @@ -190,7 +212,7 @@ class properties and methods (class variables and class functions) are already available! """ - PLUGIN_VERSION = '0.0.5' + PLUGIN_VERSION = '0.0.7' def __init__(self,sh): """ @@ -225,15 +247,26 @@ def __init__(self,sh): self.log_info("no path defined") self.bpath = byd_path_empty - self.log_debug("BYD ip = " + self.ip) - self.log_debug("BYD path = " + self.bpath) + if self.get_parameter_value('diag_cycle') != '': + self.diag_cycle = self.get_parameter_value('diag_cycle') + if self.diag_cycle is None: + self.diag_cycle = byd_sample_diag + else: + self.log_info("no diag_cycle defined => use default '" + str(byd_sample_diag) + "s'") + self.diag_cycle = byd_sample_diag + if self.diag_cycle < byd_sample_basics: + self.diag_cycle = byd_sample_basics + + self.log_debug("BYD ip = " + self.ip) + self.log_debug("BYD path = " + self.bpath) + self.log_debug("BYD diagnostic cycle = " + str(self.diag_cycle) + "s") # cycle time in seconds, only needed, if hardware/interface needs to be # polled for value changes by adding a scheduler entry in the run method of this plugin # (maybe you want to make it a plugin parameter?) self._cycle = byd_sample_basics - self.last_diag_hour = 99 # erzwingt beim ersten Aufruf das Abfragen der Detaildaten + self.last_diag_secs = 9999 # erzwingt beim ersten Aufruf das Abfragen der Detaildaten self.byd_root_found = False @@ -245,11 +278,14 @@ def __init__(self,sh): self.byd_diag_volt_max_c = [] self.byd_diag_volt_min = [] self.byd_diag_volt_min_c = [] + self.byd_diag_temp_max = [] self.byd_diag_temp_max_c = [] + self.byd_diag_temp_min = [] self.byd_diag_temp_min_c = [] self.byd_volt_cell = [] + self.byd_balance_cell = [] self.byd_temp_cell = [] - for x in range(0,byd_towers_max + 1): + for x in range(0,byd_towers_max + 1): # 0..3 self.byd_diag_soc.append(0) self.byd_diag_bat_voltag.append(0) self.byd_diag_v_out.append(0) @@ -258,14 +294,20 @@ def __init__(self,sh): self.byd_diag_volt_max_c.append(0) self.byd_diag_volt_min.append(0) self.byd_diag_volt_min_c.append(0) + self.byd_diag_temp_max.append(0) self.byd_diag_temp_max_c.append(0) + self.byd_diag_temp_min.append(0) self.byd_diag_temp_min_c.append(0) a = [] - for xx in range(0,byd_cells_max + 1): + for xx in range(0,byd_cells_max + 1): # 0..160 a.append(0) self.byd_volt_cell.append(a) a = [] - for xx in range(0,byd_temps_max + 1): + for xx in range(0,byd_cells_max + 1): # 0..160 + a.append(0) + self.byd_balance_cell.append(a) + a = [] + for xx in range(0,byd_temps_max + 1): # 0..64 a.append(0) self.byd_temp_cell.append(a) @@ -274,7 +316,7 @@ def __init__(self,sh): self.plt_file_del() - # self.simulate_data() # for internal tests only +# self.simulate_data() # for internal tests only # Initialization code goes here @@ -344,12 +386,17 @@ def update_item(self, item, caller=None, source=None, dest=None): def poll_device(self): # Wird alle 'self._cycle' aufgerufen - self.log_debug("BYD Start *********************") - if self.byd_root_found is False: self.log_debug("BYD not root found - please define root item with structure 'byd_struct'") return + if self.byd_root.enable_connection() is False: + self.byd_root.info.connection(False) + self.log_info("communication disabled !") + return + + self.log_debug("BYD Start *********************") + # Verbindung herstellen client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) try: @@ -360,7 +407,7 @@ def poll_device(self): client.close() return - # 1.Befehl senden + # 0.Befehl senden client.send(bytes.fromhex(MESSAGE_0)) client.settimeout(byd_timeout_1s) @@ -373,7 +420,7 @@ def poll_device(self): return self.decode_0(data) - # 2.Befehl senden + # 1.Befehl senden client.send(bytes.fromhex(MESSAGE_1)) client.settimeout(byd_timeout_1s) @@ -386,7 +433,7 @@ def poll_device(self): return self.decode_1(data) - # 3.Befehl senden + # 2.Befehl senden client.send(bytes.fromhex(MESSAGE_2)) client.settimeout(byd_timeout_1s) @@ -398,25 +445,31 @@ def poll_device(self): client.close() return self.decode_2(data) + if self.byd_cells_n == 0: + # Batterietyp wird nicht unterstuetzt ! + self.log_info("battery type " + self.byd_batt_str + " not supported !") + self.byd_root.info.connection(False) + client.close() + return # Speichere die Basisdaten self.basisdata_save(self.byd_root) # Pruefe, ob die Diagnosedaten abgefragt werden sollen - tn = self.now() - if tn.hour == self.last_diag_hour: + self.last_diag_secs = self.last_diag_secs + self._cycle + if self.last_diag_secs < self.diag_cycle: self.byd_root.info.connection(True) self.log_debug("BYD Basic Done ****************") client.close() return - self.last_diag_hour = tn.hour + self.last_diag_secs = 0 # Durchlaufe alle Tuerme for x in range(1,self.byd_bms_qty + 1): self.log_debug("Turm " + str(x)) - # 4.Befehl senden + # 3.Befehl senden if x == 1: client.send(bytes.fromhex(MESSAGE_3_1)) elif x == 2: @@ -435,7 +488,7 @@ def poll_device(self): self.decode_nop(data,x) time.sleep(2) - # 5.Befehl senden + # 4.Befehl senden client.send(bytes.fromhex(MESSAGE_4)) client.settimeout(byd_timeout_8s) @@ -448,7 +501,7 @@ def poll_device(self): return self.decode_nop(data,x) - # 6.Befehl senden + # 5.Befehl senden client.send(bytes.fromhex(MESSAGE_5)) client.settimeout(byd_timeout_1s) @@ -461,7 +514,7 @@ def poll_device(self): return self.decode_5(data,x) - # 7.Befehl senden + # 6.Befehl senden client.send(bytes.fromhex(MESSAGE_6)) client.settimeout(byd_timeout_1s) @@ -474,7 +527,7 @@ def poll_device(self): return self.decode_6(data,x) - # 8.Befehl senden + # 7.Befehl senden client.send(bytes.fromhex(MESSAGE_7)) client.settimeout(byd_timeout_1s) @@ -487,7 +540,7 @@ def poll_device(self): return self.decode_7(data,x) - # 9.Befehl senden + # 8.Befehl senden client.send(bytes.fromhex(MESSAGE_8)) client.settimeout(byd_timeout_1s) @@ -500,6 +553,105 @@ def poll_device(self): return self.decode_8(data,x) + if self.byd_cells_n > 128: + # Switch to second turn for the last module - 9.Befehl senden + client.send(bytes.fromhex(MESSAGE_9)) + client.settimeout(byd_timeout_1s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 9 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_nop(data,x) + time.sleep(2) + + # 10.Befehl senden (wie Befehl 3) + if x == 1: + client.send(bytes.fromhex(MESSAGE_10_1)) + elif x == 2: + client.send(bytes.fromhex(MESSAGE_10_2)) + elif x == 3: + client.send(bytes.fromhex(MESSAGE_10_3)) + client.settimeout(byd_timeout_2s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 10 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_nop(data,x) + time.sleep(2) + + # 11.Befehl senden (wie Befehl 4) + client.send(bytes.fromhex(MESSAGE_11)) + client.settimeout(byd_timeout_8s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 11 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_nop(data,x) + + # 12.Befehl senden (wie Befehl 5) + client.send(bytes.fromhex(MESSAGE_12)) + client.settimeout(byd_timeout_1s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 12 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_12(data,x) + + # 13.Befehl senden (wie Befehl 6) + client.send(bytes.fromhex(MESSAGE_13)) + client.settimeout(byd_timeout_1s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 13 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_13(data,x) + + # 14.Befehl senden (wie Befehl 7) + client.send(bytes.fromhex(MESSAGE_14)) + client.settimeout(byd_timeout_1s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 14 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_14(data,x) + + # 15.Befehl senden (wie Befehl 8) + client.send(bytes.fromhex(MESSAGE_15)) + client.settimeout(byd_timeout_1s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 15 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_15(data,x) + self.diagdata_save(self.byd_root) self.byd_root.info.connection(True) @@ -515,7 +667,7 @@ def decode_0(self,data): # Serienummer self.byd_serial = "" - for x in range(3,22): + for x in range(3,22): # 3..21 self.byd_serial = self.byd_serial + chr(data[x]) # Firmware-Versionen @@ -611,7 +763,7 @@ def decode_2(self,data): self.byd_batt_type = data[5] if self.byd_batt_type == 0: - # HVL -> unknown specification, so 0 cells and 0 temps + # HVL -> Lithium Iron Phosphate (LFP), 3-8 Module (12kWh-32kWh), unknown specification, so 0 cells and 0 temps self.byd_batt_str = "HVL" self.byd_capacity_module = 4.0 self.byd_volt_n = 0 @@ -680,19 +832,35 @@ def decode_5(self,data,x): self.log_debug("decode_5 (" + str(x) + ") : " + data.hex()) - self.byd_diag_soc[x] = self.buf2int16SI(data,53) * 1.0 / 10.0 # Byte 53+54 + self.byd_diag_volt_max[x] = self.buf2int16SI(data,5) / 1000.0 # Byte 5+6 (Index 1) + self.byd_diag_volt_min[x] = self.buf2int16SI(data,7) / 1000.0 # Byte 7+8 (Index 2) + self.byd_diag_volt_max_c[x] = data[9] # Byte 9 (Index 3) + self.byd_diag_volt_min_c[x] = data[10] # Byte 10 + self.byd_diag_temp_max[x] = self.buf2int16SI(data,11) # Byte 11+12 (Index 4) + self.byd_diag_temp_min[x] = self.buf2int16SI(data,13) # Byte 13+14 (Index 5) + self.byd_diag_temp_max_c[x] = data[15] # Byte 15 (Index 6) + self.byd_diag_temp_min_c[x] = data[16] # Byte 16 + + # Balancing-Flags. Es folgen 8x 16-bit-Worte = 16 Byte => 0..127 Bits + i = 0 + for xx in range(17,33): # 17..32 (16 Byte) + a = data[xx] + self.log_debug("Balancing i=" + str(i) + " d=" + str(a)) + for yy in range(0,8): # 0..7 + if (int(a) & 1) == 1: + self.byd_balance_cell[x][i] = 1 + else: + self.byd_balance_cell[x][i] = 0 + a = a / 2 + i = i + 1 + self.byd_diag_bat_voltag[x] = self.buf2int16SI(data,45) * 1.0 / 10.0 # Byte 45+46 self.byd_diag_v_out[x] = self.buf2int16SI(data,51) * 1.0 / 10.0 # Byte 51+52 + self.byd_diag_soc[x] = self.buf2int16SI(data,53) * 1.0 / 10.0 # Byte 53+54 self.byd_diag_current[x] = self.buf2int16SI(data,57) * 1.0 / 10.0 # Byte 57+58 - self.byd_diag_volt_max[x] = self.buf2int16SI(data,5) / 1000.0 - self.byd_diag_volt_max_c[x] = data[9] - self.byd_diag_volt_min[x] = self.buf2int16SI(data,7) / 1000.0 - self.byd_diag_volt_min_c[x] = data[10] - self.byd_diag_temp_max_c[x] = data[15] - self.byd_diag_temp_min_c[x] = data[16] - - # starting with byte 101, ending with 131, Cell voltage 1-16 - for xx in range(0,16): + + # starting with byte 101, ending with 131, Cell voltage 0-15 + for xx in range(0,16): # 0..15 self.byd_volt_cell[x][xx] = self.buf2int16SI(data,101 + (xx * 2)) / 1000.0 self.log_debug("SOC : " + str(self.byd_diag_soc[x])) @@ -701,8 +869,8 @@ def decode_5(self,data,x): self.log_debug("Current : " + str(self.byd_diag_current[x])) self.log_debug("Volt max : " + str(self.byd_diag_volt_max[x]) + " c=" + str(self.byd_diag_volt_max_c[x])) self.log_debug("Volt min : " + str(self.byd_diag_volt_min[x]) + " c=" + str(self.byd_diag_volt_min_c[x])) - self.log_debug("Temp max : " + " c=" + str(self.byd_diag_temp_max_c[x])) - self.log_debug("Temp min : " + " c=" + str(self.byd_diag_temp_min_c[x])) + self.log_debug("Temp max : " + str(self.byd_diag_temp_max[x]) + " c=" + str(self.byd_diag_temp_max_c[x])) + self.log_debug("Temp min : " + str(self.byd_diag_temp_min[x]) + " c=" + str(self.byd_diag_temp_min_c[x])) # for xx in range(0,16): # self.log_debug("Turm " + str(x) + " Volt " + str(xx) + " : " + str(self.byd_volt_cell[x][xx])) @@ -713,7 +881,7 @@ def decode_6(self,data,x): self.log_debug("decode_6 (" + str(x) + ") : " + data.hex()) - for xx in range(0,64): + for xx in range(0,64): # 0..63, Cell voltage 16-79 self.byd_volt_cell[x][16 + xx] = self.buf2int16SI(data,5 + (xx * 2)) / 1000.0 # for xx in range(0,64): @@ -727,11 +895,11 @@ def decode_7(self,data,x): self.log_debug("decode_7 (" + str(x) + ") : " + data.hex()) # starting with byte 5, ending 101, voltage for cell 81 to 128 - for xx in range(0,48): + for xx in range(0,48): # 0..47, Cell voltage 80-127 self.byd_volt_cell[x][80 + xx] = self.buf2int16SI(data,5 + (xx * 2)) / 1000.0 # starting with byte 103, ending 132, temp for cell 1 to 30 - for xx in range(0,30): + for xx in range(0,30): # 0..29 self.byd_temp_cell[x][xx] = data[103 + xx] # for xx in range(0,48): @@ -746,7 +914,7 @@ def decode_8(self,data,x): self.log_debug("decode_8 (" + str(x) + ") : " + data.hex()) - for xx in range(0,34): + for xx in range(0,34): # 0..33 self.byd_temp_cell[x][30 + xx] = data[5 + xx] # for xx in range(0,34): @@ -754,6 +922,59 @@ def decode_8(self,data,x): return + def decode_12(self,data,x): + # Decodieren der Nachricht auf Befehl 'MESSAGE_12' fuer den Turm 'x'. + + self.log_debug("decode_12 (" + str(x) + ") : " + data.hex()) + + # Balancing-Flags. Es folgen 8x 16-bit-Worte = 16 Byte => 0..127 Bits + i = 127 + for xx in range(17,33): # 17..32 + a = data[xx] + self.log_debug("Balancing i=" + str(i) + " d=" + str(a)) + for yy in range(0,8): # 0..7 + if i <= byd_cells_max: + if (int(a) & 1) == 1: + self.byd_balance_cell[x][i] = 1 + else: + self.byd_balance_cell[x][i] = 0 + a = a / 2 + i = i + 1 + + # starting with byte 101, ending with 116, Cell voltage 129-144 + for xx in range(0,16): # 0..15, Cell voltage 128-143 + self.byd_volt_cell[x][128 + xx] = self.buf2int16SI(data,101 + (xx * 2)) / 1000.0 + + return + + def decode_13(self,data,x): + # Decodieren der Nachricht auf Befehl 'MESSAGE_13'. + + self.log_debug("decode_13 (" + str(x) + ") : " + data.hex()) + + # The first round measured up to 128 cells, request[12] then get another 16 + # With 5 HVS Modules (max for HVS), only 16 cells are remaining + + # starting with byte 5, ending with 21, Cell voltage 145-161 + for xx in range(0,16): # 0..15, Cell voltage 144-160 + self.byd_volt_cell[x][144 + xx] = self.buf2int16SI(data,5 + (xx * 2)) / 1000.0 + + return + + def decode_14(self,data,x): + # Decodieren der Nachricht auf Befehl 'MESSAGE_14'. + + self.log_debug("decode_14 (" + str(x) + ") : " + data.hex()) + + return + + def decode_15(self,data,x): + # Decodieren der Nachricht auf Befehl 'MESSAGE_15'. + + self.log_debug("decode_15 (" + str(x) + ") : " + data.hex()) + + return + def decode_nop(self,data,x): # self.log_debug("decode_nop (" + str(x) + ") : " + data.hex()) return @@ -822,8 +1043,10 @@ def diagdata_save_one(self,device,x): device.volt_max.cell(self.byd_diag_volt_max_c[x]) device.volt_min.volt(self.byd_diag_volt_min[x]) device.volt_min.cell(self.byd_diag_volt_min_c[x]) - device.temp_max_cell(self.byd_diag_temp_max_c[x]) - device.temp_min_cell(self.byd_diag_temp_min_c[x]) + device.temp_max.temp(self.byd_diag_temp_max[x]) + device.temp_max.cell(self.byd_diag_temp_max_c[x]) + device.temp_min.temp(self.byd_diag_temp_min[x]) + device.temp_min.cell(self.byd_diag_temp_min_c[x]) self.diag_plot(x) @@ -836,27 +1059,32 @@ def diagdata_save_one(self,device,x): return def diag_plot(self,x): + # Erstellt die beiden Plots fuer Turm 'x'. # Heatmap der Spannungen + if self.byd_volt_n == byd_no_of_col_7: + no_of_col = byd_no_of_col_7 + else: + no_of_col = byd_no_of_col_8 i = 0 j = 1 - rows = self.byd_cells_n // byd_no_of_col + rows = self.byd_cells_n // no_of_col # Anzahl Zeilen bestimmen d = [] rt = [] - for r in range(0,rows): + for r in range(0,rows): # 0..rows-1 c = [] - for cc in range(0,byd_no_of_col): + for cc in range(0,no_of_col): # 0..no_of_col-1 c.append(self.byd_volt_cell[x][i]) i = i + 1 d.append(c) rt.append("M" + str(j)) - if ((r + 1) % (self.byd_volt_n // self.byd_modules)) == 0: + if ((r + 1) % (self.byd_volt_n // no_of_col)) == 0: j = j + 1 dd = np.array(d) fig,ax = plt.subplots(figsize=(10,4)) # Erzeugt ein Bitmap von 1000x500 Pixel - im = ax.imshow(dd) + im = ax.imshow(dd) # Befehl fuer Heatmap cbar = ax.figure.colorbar(im,ax=ax,shrink=0.5) cbar.ax.yaxis.set_tick_params(color='white') cbar.outline.set_edgecolor('white') @@ -867,22 +1095,28 @@ def diag_plot(self,x): ax.set_yticks(np.arange(len(rt)),labels=rt) ax.spines[:].set_visible(False) - ax.set_xticks(np.arange(dd.shape[1] + 1) - .5,minor=True) - ax.set_yticks(np.arange(dd.shape[0] + 1) - .5,minor=True) + ax.set_xticks(np.arange(dd.shape[1] + 1) - 0.5,minor=True) + ax.set_yticks(np.arange(dd.shape[0] + 1) - 0.5,minor=True) ax.tick_params(which='minor',bottom=False,left=False) ax.tick_params(axis='y',colors='white',labelsize=10) textcolors = ("white","black") - threshold = im.norm(dd.max()) / 2. + threshold = im.norm(dd.max()) / 2.0 kw = dict(horizontalalignment="center",verticalalignment="center",size=9) valfmt = matplotlib.ticker.StrMethodFormatter("{x:.3f}") + valfmtb = matplotlib.ticker.StrMethodFormatter("{x:.3f} B") # Loop over data dimensions and create text annotations. - for i in range(0,rows): - for j in range(0,byd_no_of_col): + k = 0 + for i in range(0,rows): # 0..rows-1 + for j in range(0,no_of_col): # 0..no_of_col-1 kw.update(color=textcolors[int(im.norm(dd[i,j]) > threshold)]) - text = ax.text(j,i,valfmt(dd[i,j], None),**kw) - + if self.byd_balance_cell[x][k] == 0: + text = ax.text(j,i,valfmt(dd[i,j],None),**kw) + else: + text = ax.text(j,i,valfmtb(dd[i,j],None),**kw) + k = k + 1 + ax.set_title("Turm " + str(x) + " - Spannungen [V]" + " (" + self.now_str() + ")",size=10,color='white') fig.tight_layout() @@ -893,23 +1127,31 @@ def diag_plot(self,x): format='png',transparent=True) self.log_debug("save " + self.get_plugin_dir() + byd_webif_img + byd_fname_temp + str(x) + byd_fname_ext) plt.close('all') + + if self.byd_temps_n == 0: + return # Heatmap der Temperaturen + if self.byd_temp_n == byd_no_of_col_8: + no_of_col = byd_no_of_col_8 + else: + no_of_col = byd_no_of_col_12 i = 0 j = 1 - rows = self.byd_temps_n // byd_no_of_col + rows = self.byd_temps_n // no_of_col d = [] rt = [] for r in range(0,rows): c = [] - for cc in range(0,byd_no_of_col): + for cc in range(0,no_of_col): c.append(self.byd_temp_cell[x][i]) i = i + 1 d.append(c) rt.append("M" + str(j)) - if ((r + 1) % (self.byd_temp_n // self.byd_modules)) == 0: + if ((r + 1) % (self.byd_temp_n // no_of_col)) == 0: j = j + 1 dd = np.array(d) +# self.log_info("dd.min=" + str(dd.min()) + " dd.max=" + str(dd.max())) cmap = matplotlib.colors.LinearSegmentedColormap.from_list('',['#f5f242','#ffaf38','#fc270f']) norm = matplotlib.colors.TwoSlopeNorm(vcenter=dd.min() + (dd.max() - dd.min()) / 2, vmin=dd.min(),vmax=dd.max()) @@ -939,7 +1181,7 @@ def diag_plot(self,x): # Loop over data dimensions and create text annotations. for i in range(0,rows): - for j in range(0,byd_no_of_col): + for j in range(0,no_of_col): kw.update(color=textcolors[int(im.norm(dd[i,j]) > threshold)]) text = ax.text(j,i,valfmt(dd[i,j], None),**kw) @@ -956,52 +1198,33 @@ def diag_plot(self,x): return + def plt_file_del_single(self,fn): + if os.path.exists(fn) == True: + os.remove(fn) + return + def plt_file_del(self): # Loescht alle Plot-Dateien # Spannungs-Plots - fn = self.get_plugin_dir() + byd_webif_img + byd_fname_volt + str(1) + byd_fname_ext - if os.path.exists(fn) == True: - os.remove(fn) - fn = self.get_plugin_dir() + byd_webif_img + byd_fname_volt + str(2) + byd_fname_ext - if os.path.exists(fn) == True: - os.remove(fn) - fn = self.get_plugin_dir() + byd_webif_img + byd_fname_volt + str(3) + byd_fname_ext - if os.path.exists(fn) == True: - os.remove(fn) + self.plt_file_del_single(self.get_plugin_dir() + byd_webif_img + byd_fname_volt + str(1) + byd_fname_ext) + self.plt_file_del_single(self.get_plugin_dir() + byd_webif_img + byd_fname_volt + str(2) + byd_fname_ext) + self.plt_file_del_single(self.get_plugin_dir() + byd_webif_img + byd_fname_volt + str(3) + byd_fname_ext) if len(self.bpath) != byd_path_empty: - fn = self.bpath + byd_fname_volt + str(1) + byd_fname_ext - if os.path.exists(fn) == True: - os.remove(fn) - fn = self.bpath + byd_fname_volt + str(2) + byd_fname_ext - if os.path.exists(fn) == True: - os.remove(fn) - fn = self.bpath + byd_fname_volt + str(3) + byd_fname_ext - if os.path.exists(fn) == True: - os.remove(fn) + self.plt_file_del_single(self.bpath + byd_fname_volt + str(1) + byd_fname_ext) + self.plt_file_del_single(self.bpath + byd_fname_volt + str(2) + byd_fname_ext) + self.plt_file_del_single(self.bpath + byd_fname_volt + str(3) + byd_fname_ext) # Temperatur-Plots - fn = self.get_plugin_dir() + byd_webif_img + byd_fname_temp + str(1) + byd_fname_ext - if os.path.exists(fn) == True: - os.remove(fn) - fn = self.get_plugin_dir() + byd_webif_img + byd_fname_temp + str(2) + byd_fname_ext - if os.path.exists(fn) == True: - os.remove(fn) - fn = self.get_plugin_dir() + byd_webif_img + byd_fname_temp + str(3) + byd_fname_ext - if os.path.exists(fn) == True: - os.remove(fn) + self.plt_file_del_single(self.get_plugin_dir() + byd_webif_img + byd_fname_temp + str(1) + byd_fname_ext) + self.plt_file_del_single(self.get_plugin_dir() + byd_webif_img + byd_fname_temp + str(2) + byd_fname_ext) + self.plt_file_del_single(self.get_plugin_dir() + byd_webif_img + byd_fname_temp + str(3) + byd_fname_ext) if len(self.bpath) != byd_path_empty: - fn = self.bpath + byd_fname_temp + str(1) + byd_fname_ext - if os.path.exists(fn) == True: - os.remove(fn) - fn = self.bpath + byd_fname_temp + str(2) + byd_fname_ext - if os.path.exists(fn) == True: - os.remove(fn) - fn = self.bpath + byd_fname_temp + str(3) + byd_fname_ext - if os.path.exists(fn) == True: - os.remove(fn) + self.plt_file_del_single(self.bpath + byd_fname_temp + str(1) + byd_fname_ext) + self.plt_file_del_single(self.bpath + byd_fname_temp + str(2) + byd_fname_ext) + self.plt_file_del_single(self.bpath + byd_fname_temp + str(3) + byd_fname_ext) return @@ -1070,24 +1293,42 @@ def init_webinterface(self): def simulate_data(self): # For internal tests only - self.byd_modules = 7 - self.byd_batt_str = "HVM" - self.byd_capacity_module = 2.76 - self.byd_volt_n = 16 - self.byd_temp_n = 8 + +# simul = 1 # HVM + simul = 2 # HVS +# simul = 3 # LVS + + twr = 3 + + if simul == 1: + self.byd_batt_str = "HVM" + self.byd_modules = 7 + self.byd_capacity_module = 2.76 + self.byd_volt_n = 16 + self.byd_temp_n = 8 + elif simul == 2: + self.byd_batt_str = "HVS" + self.byd_modules = 5 + self.byd_capacity_module = 2.56 + self.byd_volt_n = 32 + self.byd_temp_n = 12 + elif simul == 3: + self.byd_batt_str = "LVS" + self.byd_modules = 3 + self.byd_capacity_module = 4.0 + self.byd_volt_n = 7 + self.byd_temp_n = 0 + self.byd_cells_n = self.byd_modules * self.byd_volt_n self.byd_temps_n = self.byd_modules * self.byd_temp_n for xx in range(0,self.byd_cells_n): - if (xx % 2) == 0: - self.byd_volt_cell[1][xx] = 2.2 - else: - self.byd_volt_cell[1][xx] = 2.4 + self.byd_volt_cell[twr][xx] = round(random.uniform(2.1,2.9),2) + self.byd_balance_cell[twr][xx] = random.randint(0,1) +# self.log_info("xx=" + str(xx) + " v=" + str(self.byd_volt_cell[1][xx])) for xx in range(0,self.byd_temps_n): - if (xx % 2) == 0: - self.byd_temp_cell[1][xx] = 23 - else: - self.byd_temp_cell[1][xx] = 26 + self.byd_temp_cell[twr][xx] = round(random.uniform(20.0,28.0),2) +# self.log_info("xx=" + str(xx) + " v=" + str(self.byd_temp_cell[1][xx])) - self.diag_plot(1) + self.diag_plot(twr) \ No newline at end of file diff --git a/byd_bat/locale.yaml b/byd_bat/locale.yaml index 0d6f17c2b..58de3c1a0 100644 --- a/byd_bat/locale.yaml +++ b/byd_bat/locale.yaml @@ -39,8 +39,8 @@ plugin_translations: 'Strom': {'de': '=', 'en': 'Current'} 'Spannung max (Zelle)': {'de': '=', 'en': 'Voltage max (cell)'} 'Spannung min (Zelle)': {'de': '=', 'en': 'Voltage min (cell)'} - 'Temperatur Zelle max': {'de': '=', 'en': 'Temperature cell max'} - 'Temperatur Zelle min': {'de': '=', 'en': 'Temperature cell min'} + 'Temperatur max (Zelle)': {'de': '=', 'en': 'Temperature max (cell)'} + 'Temperatur min (Zelle)': {'de': '=', 'en': 'Temperature min (cell)'} # Alternative format for translations of longer texts: 'Hier kommt der Inhalt des Webinterfaces hin.': diff --git a/byd_bat/plugin.yaml b/byd_bat/plugin.yaml index 8186f4e6d..66349d240 100644 --- a/byd_bat/plugin.yaml +++ b/byd_bat/plugin.yaml @@ -12,7 +12,7 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1886748-support-thread-f%C3%BCr-das-byd-batterie-plugin - version: 0.0.5 # Plugin version (must match the version specified in __init__.py) + version: 0.0.7 # Plugin version (must match the version specified in __init__.py) sh_minversion: 1.9 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) py_minversion: 3.9 # minimum Python version to use for this plugin @@ -59,6 +59,12 @@ item_structs: initial_value: false enforce_updates: true visu_acl: ro + + enable_connection: + type: bool + initial_value: true + enforce_updates: true + visu_acl: rw state: @@ -252,16 +258,30 @@ item_structs: visu_acl: ro database: init - temp_max_cell: # cell number of max temperature - type: num - visu_acl: ro - database: init + temp_max: + + temp: # max temperature [°C] + type: num + visu_acl: ro + database: init + + cell: # cell number of max temperature + type: num + visu_acl: ro + database: init - temp_min_cell: # cell number of min temperature - type: num - visu_acl: ro - database: init + temp_min: + temp: # min temperature [V] + type: num + visu_acl: ro + database: init + + cell: # cell number of min temperature + type: num + visu_acl: ro + database: init + tower2: soc: # [%] SOC @@ -308,15 +328,29 @@ item_structs: visu_acl: ro database: init - temp_max_cell: # cell number of max temperature - type: num - visu_acl: ro - database: init + temp_max: + + temp: # max temperature [°C] + type: num + visu_acl: ro + database: init + + cell: # cell number of max temperature + type: num + visu_acl: ro + database: init - temp_min_cell: # cell number of min temperature - type: num - visu_acl: ro - database: init + temp_min: + + temp: # min temperature [V] + type: num + visu_acl: ro + database: init + + cell: # cell number of min temperature + type: num + visu_acl: ro + database: init tower3: @@ -364,15 +398,29 @@ item_structs: visu_acl: ro database: init - temp_max_cell: # cell number of max temperature - type: num - visu_acl: ro - database: init + temp_max: + + temp: # max temperature [°C] + type: num + visu_acl: ro + database: init + + cell: # cell number of max temperature + type: num + visu_acl: ro + database: init - temp_min_cell: # cell number of min temperature - type: num - visu_acl: ro - database: init + temp_min: + + temp: # min temperature [V] + type: num + visu_acl: ro + database: init + + cell: # cell number of min temperature + type: num + visu_acl: ro + database: init logic_parameters: NONE diff --git a/byd_bat/user_doc.rst b/byd_bat/user_doc.rst index 706d6b300..c1aad8f5f 100644 --- a/byd_bat/user_doc.rst +++ b/byd_bat/user_doc.rst @@ -16,7 +16,7 @@ Anzeigen von Parametern eines BYD Energiespeichers. Die Parameter entsprechen de Es werden 1-3 Türme unterstützt. -Die Grunddaten werden alle 60 Sekunden aktualisiert. Die Diagnosedaten werden beim Start des Plugin und dann immer zur vollen Stunde abgerufen. +Die Grunddaten werden alle 60 Sekunden aktualisiert. Die Diagnosedaten werden beim Start des Plugin und gemäss dem Parameter 'diag_cycle' abgerufen. Die Spannungen und Temperaturen in den Modulen werden mit Hilfe von Heatmaps dargestellt. Diese werden im Web Interface angezeigt. Zusätzlich können diese Bilder auch in ein weiteres Verzeichnis kopiert werden (z.Bsp. für smartvisu). diff --git a/byd_bat/webif/__init__.py b/byd_bat/webif/__init__.py index 60b1fd383..90c65a066 100644 --- a/byd_bat/webif/__init__.py +++ b/byd_bat/webif/__init__.py @@ -129,8 +129,8 @@ def get_data_html(self, dataSet=None): data['t1_current'] = f'{self.plugin.byd_diag_current[1]:.1f}' + " A" data['t1_volt_max'] = f'{self.plugin.byd_diag_volt_max[1]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_max_c[1]) + ")" data['t1_volt_min'] = f'{self.plugin.byd_diag_volt_min[1]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_min_c[1]) + ")" - data['t1_temp_max_cell'] = str(self.plugin.byd_diag_temp_max_c[1]) - data['t1_temp_min_cell'] = str(self.plugin.byd_diag_temp_min_c[1]) + data['t1_temp_max'] = f'{self.plugin.byd_diag_temp_max[1]:.1f}' + " V (" + str(self.plugin.byd_diag_temp_max_c[1]) + ")" + data['t1_temp_min'] = f'{self.plugin.byd_diag_temp_min[1]:.1f}' + " V (" + str(self.plugin.byd_diag_temp_min_c[1]) + ")" if self.plugin.byd_bms_qty > 1: data['t2_soc'] = f'{self.plugin.byd_diag_soc[2]:.1f}' + " %" data['t2_bat_voltag'] = f'{self.plugin.byd_diag_bat_voltag[2]:.1f}' + " V" @@ -138,8 +138,8 @@ def get_data_html(self, dataSet=None): data['t2_current'] = f'{self.plugin.byd_diag_current[2]:.1f}' + " A" data['t2_volt_max'] = f'{self.plugin.byd_diag_volt_max[2]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_max_c[2]) + ")" data['t2_volt_min'] = f'{self.plugin.byd_diag_volt_min[2]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_min_c[2]) + ")" - data['t2_temp_max_cell'] = str(self.plugin.byd_diag_temp_max_c[2]) - data['t2_temp_min_cell'] = str(self.plugin.byd_diag_temp_min_c[2]) + data['t2_temp_max'] = f'{self.plugin.byd_diag_temp_max[2]:.1f}' + " V (" + str(self.plugin.byd_diag_temp_max_c[2]) + ")" + data['t2_temp_min'] = f'{self.plugin.byd_diag_temp_min[2]:.1f}' + " V (" + str(self.plugin.byd_diag_temp_min_c[2]) + ")" else: data['t2_soc'] = "-" data['t2_bat_voltag'] = "-" @@ -147,8 +147,8 @@ def get_data_html(self, dataSet=None): data['t2_current'] = "-" data['t2_volt_max'] = "-" data['t2_volt_min'] = "-" - data['t2_temp_max_cell'] = "-" - data['t2_temp_min_cell'] = "-" + data['t2_temp_max'] = "-" + data['t2_temp_min'] = "-" if self.plugin.byd_bms_qty > 2: data['t3_soc'] = f'{self.plugin.byd_diag_soc[3]:.1f}' + " %" data['t3_bat_voltag'] = f'{self.plugin.byd_diag_bat_voltag[3]:.1f}' + " V" @@ -156,8 +156,8 @@ def get_data_html(self, dataSet=None): data['t3_current'] = f'{self.plugin.byd_diag_current[3]:.1f}' + " A" data['t3_volt_max'] = f'{self.plugin.byd_diag_volt_max[3]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_max_c[3]) + ")" data['t3_volt_min'] = f'{self.plugin.byd_diag_volt_min[3]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_min_c[3]) + ")" - data['t3_temp_max_cell'] = str(self.plugin.byd_diag_temp_max_c[3]) - data['t3_temp_min_cell'] = str(self.plugin.byd_diag_temp_min_c[3]) + data['t3_temp_max'] = f'{self.plugin.byd_diag_temp_max[3]:.1f}' + " V (" + str(self.plugin.byd_diag_temp_max_c[3]) + ")" + data['t3_temp_min'] = f'{self.plugin.byd_diag_temp_min[3]:.1f}' + " V (" + str(self.plugin.byd_diag_temp_min_c[3]) + ")" else: data['t3_soc'] = "-" data['t3_bat_voltag'] = "-" @@ -165,8 +165,8 @@ def get_data_html(self, dataSet=None): data['t3_current'] = "-" data['t3_volt_max'] = "-" data['t3_volt_min'] = "-" - data['t3_temp_max_cell'] = "-" - data['t3_temp_min_cell'] = "-" + data['t3_temp_max'] = "-" + data['t3_temp_min'] = "-" # return it as json the the web page try: diff --git a/byd_bat/webif/templates/index.html b/byd_bat/webif/templates/index.html index d441d5edf..6c9f57ea6 100644 --- a/byd_bat/webif/templates/index.html +++ b/byd_bat/webif/templates/index.html @@ -105,24 +105,24 @@ shngInsertText('t1_current',objResponse['t1_current']); shngInsertText('t1_volt_max',objResponse['t1_volt_max']); shngInsertText('t1_volt_min',objResponse['t1_volt_min']); - shngInsertText('t1_temp_max_cell',objResponse['t1_temp_max_cell']); - shngInsertText('t1_temp_min_cell',objResponse['t1_temp_min_cell']); + shngInsertText('t1_temp_max',objResponse['t1_temp_max']); + shngInsertText('t1_temp_min',objResponse['t1_temp_min']); shngInsertText('t2_soc',objResponse['t2_soc']); shngInsertText('t2_bat_voltag',objResponse['t2_bat_voltag']); shngInsertText('t2_v_out',objResponse['t2_v_out']); shngInsertText('t2_current',objResponse['t2_current']); shngInsertText('t2_volt_max',objResponse['t2_volt_max']); shngInsertText('t2_volt_min',objResponse['t2_volt_min']); - shngInsertText('t2_temp_max_cell',objResponse['t2_temp_max_cell']); - shngInsertText('t2_temp_min_cell',objResponse['t2_temp_min_cell']); + shngInsertText('t2_temp_max',objResponse['t2_temp_max']); + shngInsertText('t2_temp_min',objResponse['t2_temp_min']); shngInsertText('t3_soc',objResponse['t3_soc']); shngInsertText('t3_bat_voltag',objResponse['t3_bat_voltag']); shngInsertText('t3_v_out',objResponse['t3_v_out']); shngInsertText('t3_current',objResponse['t3_current']); shngInsertText('t3_volt_max',objResponse['t3_volt_max']); shngInsertText('t3_volt_min',objResponse['t3_volt_min']); - shngInsertText('t3_temp_max_cell',objResponse['t3_temp_max_cell']); - shngInsertText('t3_temp_min_cell',objResponse['t3_temp_min_cell']); + shngInsertText('t3_temp_max',objResponse['t3_temp_max']); + shngInsertText('t3_temp_min',objResponse['t3_temp_min']); var source = 'static/img/bydvt1.png', timestamp = (new Date()).getTime(), @@ -446,16 +446,16 @@