add files
This commit is contained in:
parent
a7959663fb
commit
88970774a4
184
JKBMS.py
Normal file
184
JKBMS.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2025 Extrafu <extrafu@gmail.com>
|
||||||
|
#
|
||||||
|
# This file is part of BerryBMS.
|
||||||
|
#
|
||||||
|
# BerryBMS is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the
|
||||||
|
# Free Software Foundation; either version 3, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
import pymodbus.client as ModbusClient
|
||||||
|
from pymodbus.client.mixin import ModbusClientMixin
|
||||||
|
import pymodbus.exceptions
|
||||||
|
import json
|
||||||
|
|
||||||
|
from ModbusDevice import ModbusDevice
|
||||||
|
from Register import Register
|
||||||
|
|
||||||
|
class JKBMS(ModbusDevice):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
port):
|
||||||
|
super().__init__(id)
|
||||||
|
self.name = name
|
||||||
|
self.port = port
|
||||||
|
self.registers = [
|
||||||
|
Register(self.id,'CellUnderVoltProtectionValue', 0x1004, ModbusClientMixin.DATATYPE.UINT32, .001),
|
||||||
|
Register(self.id,'CellOverVoltProtectionValue', 0x100C, ModbusClientMixin.DATATYPE.UINT32, .001),
|
||||||
|
Register(self.id,'100PercentSoCVoltageValue', 0x1018, ModbusClientMixin.DATATYPE.UINT32, .001),
|
||||||
|
Register(self.id,'ZeroPercentSoCVoltageValue', 0x101C, ModbusClientMixin.DATATYPE.UINT32, .001),
|
||||||
|
Register(self.id,'CellChargeOverCurrentProtection', 0x102C, ModbusClientMixin.DATATYPE.UINT32, .001),
|
||||||
|
Register(self.id,'CellDischargeOverCurrentProtection', 0x1038, ModbusClientMixin.DATATYPE.UINT32, .001),
|
||||||
|
Register(self.id, "CellCount", 0x106C, ModbusClientMixin.DATATYPE.UINT32),
|
||||||
|
Register(self.id, "BatChargeEN", 0x1070, ModbusClientMixin.DATATYPE.UINT32),
|
||||||
|
Register(self.id, "BatDisChargeEN", 0x1074, ModbusClientMixin.DATATYPE.UINT32),
|
||||||
|
Register(self.id, "BatBalanceEN", 0x1078, ModbusClientMixin.DATATYPE.UINT32),
|
||||||
|
Register(self.id, "StartBalanceVol", 0x1078, ModbusClientMixin.DATATYPE.UINT32, .001),
|
||||||
|
Register(self.id, "DeviceOptions", 0x1114, ModbusClientMixin.DATATYPE.UINT16, length=1),
|
||||||
|
Register(self.id, "CellVolAve", 0x1244, ModbusClientMixin.DATATYPE.UINT16, .001),
|
||||||
|
Register(self.id, "BatCurrent", 0x1298, ModbusClientMixin.DATATYPE.INT32, .001),
|
||||||
|
Register(self.id, "Alarms", 0x12A0, ModbusClientMixin.DATATYPE.UINT32, length=4),
|
||||||
|
Register(self.id, "SOCStateOfcharge", 0x12A6, ModbusClientMixin.DATATYPE.UINT16),
|
||||||
|
Register(self.id, "SOCCapRemain", 0x12A8, ModbusClientMixin.DATATYPE.INT32, 0.001),
|
||||||
|
Register(self.id, "SOCFullChargeCap", 0x12AC, ModbusClientMixin.DATATYPE.UINT32, 0.001),
|
||||||
|
Register(self.id, "SOCCycleCount", 0x12B0, ModbusClientMixin.DATATYPE.UINT32),
|
||||||
|
Register(self.id, "SOCCycleCap", 0x12B4, ModbusClientMixin.DATATYPE.UINT32, .001),
|
||||||
|
Register(self.id, "BatVol", 0x12E4, ModbusClientMixin.DATATYPE.UINT16, .01),
|
||||||
|
Register(self.id, "ChargingStatus", 0x12C0, ModbusClientMixin.DATATYPE.UINT16),
|
||||||
|
Register(self.id, "ManufacturerDeviceID", 0x1400, ModbusClientMixin.DATATYPE.STRING, None, 8),
|
||||||
|
Register(self.id, "HardwareVersion", 0x1410, ModbusClientMixin.DATATYPE.STRING, None, 4),
|
||||||
|
Register(self.id, "SoftwareVersion", 0x1418, ModbusClientMixin.DATATYPE.STRING, None, 4),
|
||||||
|
Register(self.id, "BatTempMos", 0x128A, ModbusClientMixin.DATATYPE.UINT16, .1),
|
||||||
|
Register(self.id, "BatTemp1", 0x129C, ModbusClientMixin.DATATYPE.UINT16, .1),
|
||||||
|
Register(self.id, "BatTemp2", 0x129E, ModbusClientMixin.DATATYPE.UINT16, .1),
|
||||||
|
Register(self.id, "BatTemp3", 0x12F8, ModbusClientMixin.DATATYPE.UINT16, .1),
|
||||||
|
Register(self.id, "BatTemp4", 0x12FA, ModbusClientMixin.DATATYPE.UINT16, .1),
|
||||||
|
Register(self.id, "BatTemp5", 0x12FC, ModbusClientMixin.DATATYPE.UINT16, .1)
|
||||||
|
]
|
||||||
|
self.cellVoltages = None
|
||||||
|
self.binary_sensors = ['BatChargeEN', 'BatDisChargeEN', 'BatBalanceEN']
|
||||||
|
self.device_options = ['HeaterEN', 'DisableTempSensor', 'GpsHeartBeat', 'PortSwitch', 'LcdAlwaysOn', 'SpecialCharger', 'SmartSleep', 'DisablePclModule', 'TimedStoredData', 'ChargingFloatEN',"UnknownOption",'DryArmIntermittent','DischargeOCP2','DischargeOCP3']
|
||||||
|
self.device_alarms = ['AlarmWireRes','MosOtp', 'CellQuantity', 'CurSensorErr', 'CellOVP', 'BatOVP', 'ChargeOVP', 'ChargeSCP', 'ChargeOTP', 'ChargeUTP',
|
||||||
|
'CpuAuxCommErr', 'CellUVP', 'BatUVP', 'DischargeOCP', 'DischargeSCP', 'DischargeOTP', 'ChargeMos', 'DischargeMos', 'GpsDisconnected', 'ModifyPasswordTime',
|
||||||
|
'DischargeOnFail', 'BatteryOverTemp','TemperatureSensorAbnormality',"PclModuleAbnormality"]
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
if self.connection == None:
|
||||||
|
self.connection = ModbusClient.ModbusSerialClient(port=self.port, stopbits=1, bytesize=8, parity='N', baudrate=115200, timeout=1)
|
||||||
|
if self.connection.connect():
|
||||||
|
print("Successfully connected to BMS id %d" % self.id)
|
||||||
|
else:
|
||||||
|
print("Cannot connect to BMS id %d" % self.id)
|
||||||
|
self.connection = None
|
||||||
|
|
||||||
|
return self.connection
|
||||||
|
|
||||||
|
def setChargeMode(self, mode):
|
||||||
|
register = self.getRegister("BatChargeEN")
|
||||||
|
register.setValue(self.connection, mode)
|
||||||
|
|
||||||
|
def setDischargeMode(self, mode):
|
||||||
|
register = self.getRegister("BatDisChargeEN")
|
||||||
|
register.setValue(self.connection, mode)
|
||||||
|
|
||||||
|
def getCellVoltages(self, reload=False):
|
||||||
|
if self.cellVoltages == None or reload == True:
|
||||||
|
count = self.getRegister('CellCount').value
|
||||||
|
values = []
|
||||||
|
|
||||||
|
recv = self.connection.read_holding_registers(address=0x1200, count=count, slave=self.id)
|
||||||
|
if not isinstance(recv, pymodbus.pdu.register_message.ReadHoldingRegistersResponse):
|
||||||
|
return None
|
||||||
|
|
||||||
|
values = self.connection.convert_from_registers(recv.registers, data_type=self.connection.DATATYPE.UINT16)
|
||||||
|
|
||||||
|
for i in range(0, count):
|
||||||
|
r = Register(self.id, f'CellVol{i}', 0x1200+i*2, ModbusClient.ModbusSerialClient.DATATYPE.UINT16, 0.001)
|
||||||
|
r.value = round(values[i]*0.001,3)
|
||||||
|
values[i] = r.value
|
||||||
|
self.registers.append(r)
|
||||||
|
|
||||||
|
self.cellVoltages = values
|
||||||
|
|
||||||
|
return self.cellVoltages
|
||||||
|
|
||||||
|
def dump(self, dic = {}):
|
||||||
|
#self.getCellVoltages()
|
||||||
|
d = super().dump()
|
||||||
|
topic_soc = "bms-%d" % self.id
|
||||||
|
d["name"] = self.name
|
||||||
|
dic[topic_soc] = d
|
||||||
|
self.normalize_values(dic[topic_soc])
|
||||||
|
return dic
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
self.getCellVoltages()
|
||||||
|
return super().__str__()
|
||||||
|
|
||||||
|
def formattedOutput(self):
|
||||||
|
bms_model = self.getRegister('ManufacturerDeviceID').value
|
||||||
|
bms_hw_version = self.getRegister('HardwareVersion').value
|
||||||
|
bms_sw_version = self.getRegister('SoftwareVersion').value
|
||||||
|
alarms = self.getRegister('Alarms').value
|
||||||
|
|
||||||
|
# We skip the first byte, as it can be 0, 1 and 2
|
||||||
|
# A 100% SOC will give us a value of 100 with first byte set to 0, 356 when set to 1 and 612 when set to 2
|
||||||
|
soc = self.getRegister('SOCStateOfcharge').value & 0x0FF
|
||||||
|
cycle_count = self.getRegister('SOCCycleCount').value
|
||||||
|
pack_voltage = self.getRegister('BatVol').value
|
||||||
|
cell_count = self.getRegister('CellCount').value
|
||||||
|
cell_avg_voltage = self.getRegister('CellVolAve').value
|
||||||
|
battery_current = self.getRegister('BatCurrent').value
|
||||||
|
discharge_enabled = self.getRegister('BatDisChargeEN').value
|
||||||
|
pack_capacity = self.getRegister('SOCFullChargeCap').value
|
||||||
|
pack_remaining = self.getRegister('SOCCapRemain').value
|
||||||
|
status = "charging" if battery_current > 0 else "discharging"
|
||||||
|
options = self.getRegister("DeviceOptions").value
|
||||||
|
|
||||||
|
s = f"== {self.name} (id {self.id}) - {bms_model} (v{bms_hw_version}) - sw v{bms_sw_version}) ==\n"
|
||||||
|
s += f"Alarms?\t\t\t{alarms}\n"
|
||||||
|
s += f"SOC:\t\t\t{soc} ({cycle_count} cycle(s))\n"
|
||||||
|
s += f"Voltage:\t\t{pack_voltage:.2f}v\n"
|
||||||
|
s += f"Cell Voltages:\t\t{str(self.getCellVoltages())}\n"
|
||||||
|
s += f"Cell Average Voltage:\t{cell_avg_voltage:.3f} ({cell_count} cells)\n"
|
||||||
|
s += f"Battery Current:\t{battery_current:.3f}A ({status} {pack_voltage*battery_current:.2f}W)\n"
|
||||||
|
s += f"Discharge Enabled?\t{discharge_enabled}\n"
|
||||||
|
s += f"Remaining Capacity:\t{pack_remaining:.2f}Ah ({pack_capacity}Ah capacity)"
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
|
# def update_alarms(self, alarms_value):
|
||||||
|
# alarms_bits = list(bin(options_value)[2:])
|
||||||
|
# res = {}
|
||||||
|
# for i in range(len(self.device_options)):
|
||||||
|
# res[self.device_alarms[i]] = "ON" if alarms_value[i] else "OFF"
|
||||||
|
# return res
|
||||||
|
|
||||||
|
# def update_options(self, options_value):
|
||||||
|
# options_bits = list(bin(options_value)[2:])
|
||||||
|
# res = {}
|
||||||
|
# for i in range(len(self.device_options)):
|
||||||
|
# res[self.device_options[i]] = "ON" if options_value[i] == 1 else "OFF"
|
||||||
|
# return res
|
||||||
|
def parse_bits(self, key_names, bytes_object):
|
||||||
|
bits = bin(bytes_object)[2:].zfill(32)
|
||||||
|
bit_flags = list(reversed(bits))
|
||||||
|
res = {}
|
||||||
|
|
||||||
|
for pos in range(len(key_names)):
|
||||||
|
res.update({key_names[pos]: "ON" if bit_flags[pos] == "1" else "OFF"})
|
||||||
|
return res
|
||||||
|
|
||||||
|
def normalize_values(self, dic):
|
||||||
|
for topic in self.binary_sensors:
|
||||||
|
dic[topic] = True if dic[topic] == 1 else False
|
||||||
|
dic['DeviceOptions'] = self.parse_bits(self.device_options,dic['DeviceOptions'])
|
||||||
|
dic["Alarms"] = self.parse_bits(self.device_alarms, dic["Alarms"])
|
||||||
|
# dic.update(self.update_options(dic['DeviceOptions']))
|
||||||
|
# dic.pop("DeviceOptions")
|
||||||
|
|
||||||
|
# dic.update(self.update_alarms(dic['Alarms']))
|
||||||
|
# dic.pop('Alarms')
|
||||||
47
ModbusDevice.py
Normal file
47
ModbusDevice.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2025 Extrafu <extrafu@gmail.com>
|
||||||
|
#
|
||||||
|
# This file is part of BerryBMS.
|
||||||
|
#
|
||||||
|
# BerryBMS is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the
|
||||||
|
# Free Software Foundation; either version 3, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
import json
|
||||||
|
|
||||||
|
class ModbusDevice(object):
|
||||||
|
|
||||||
|
def __init__(self, id):
|
||||||
|
self.id = id
|
||||||
|
self.connection = None
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
if self.connection != None:
|
||||||
|
#print("Closing connnection...")
|
||||||
|
self.connection.close()
|
||||||
|
|
||||||
|
def getAllRegisters(self):
|
||||||
|
return self.registers
|
||||||
|
|
||||||
|
# Return the initialized value of a register
|
||||||
|
def getRegister(self, name):
|
||||||
|
for register in self.registers:
|
||||||
|
if name == register.name:
|
||||||
|
register.getValue(self.connection)
|
||||||
|
return register
|
||||||
|
return None
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
values = {}
|
||||||
|
for register in self.registers:
|
||||||
|
values[register.name] = register.getValue(self.connection)
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
json_formatted = json.dumps(self.dump(), indent=2)
|
||||||
|
return json_formatted
|
||||||
|
|
||||||
|
def formattedOutput(self):
|
||||||
|
return ""
|
||||||
73
Register.py
Normal file
73
Register.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2025 Extrafu <extrafu@gmail.com>
|
||||||
|
#
|
||||||
|
# This file is part of BerryBMS.
|
||||||
|
#
|
||||||
|
# BerryBMS is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the
|
||||||
|
# Free Software Foundation; either version 3, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
import pymodbus.client as ModbusClient
|
||||||
|
from pymodbus.client.mixin import ModbusClientMixin
|
||||||
|
|
||||||
|
import pymodbus.exceptions
|
||||||
|
import pymodbus.payload
|
||||||
|
import time
|
||||||
|
|
||||||
|
class Register(object):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
type,
|
||||||
|
scale=1,
|
||||||
|
length=0
|
||||||
|
):
|
||||||
|
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.address = address
|
||||||
|
self.type = type
|
||||||
|
self.scale = scale
|
||||||
|
self.length = length
|
||||||
|
self.value = None
|
||||||
|
|
||||||
|
def getValue(self, c, reload=False):
|
||||||
|
|
||||||
|
if reload == True:
|
||||||
|
self.value = None
|
||||||
|
|
||||||
|
if self.value != None:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
use_scale = True
|
||||||
|
if self.type == ModbusClientMixin.DATATYPE.STRING:
|
||||||
|
use_scale = False
|
||||||
|
elif (self.type == ModbusClientMixin.DATATYPE.INT16 or self.type == ModbusClientMixin.DATATYPE.UINT16):
|
||||||
|
self.length = 1
|
||||||
|
elif (self.type == ModbusClientMixin.DATATYPE.INT32 or self.type == ModbusClientMixin.DATATYPE.UINT32):
|
||||||
|
self.length = 2
|
||||||
|
# elif self.type == BITS:
|
||||||
|
# use_scale = False
|
||||||
|
|
||||||
|
recv = c.read_holding_registers(address=self.address, count=self.length, slave=self.id)
|
||||||
|
if not isinstance(recv, pymodbus.pdu.register_message.ReadHoldingRegistersResponse):
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.value = c.convert_from_registers(recv.registers, data_type=self.type)
|
||||||
|
if use_scale:
|
||||||
|
self.value = self.value * self.scale
|
||||||
|
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def setValue(self, c, value):
|
||||||
|
|
||||||
|
raw_value = c.convert_to_registers(value, self.type)
|
||||||
|
r = c.write_registers(self.address, raw_value, slave=self.id)
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
return r
|
||||||
0
__init__.py
Normal file
0
__init__.py
Normal file
1
berrybms
1
berrybms
@ -1 +0,0 @@
|
|||||||
Subproject commit ff8cff8b86eafdfbe3493f220d87787d17551a80
|
|
||||||
154
berrybms.py
Normal file
154
berrybms.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
#
|
||||||
|
# Copyright (C) 2025 Extrafu <extrafu@gmail.com>
|
||||||
|
#
|
||||||
|
# This file is part of BerryBMS.
|
||||||
|
#
|
||||||
|
# BerryBMS is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the
|
||||||
|
# Free Software Foundation; either version 3, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import yaml
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import signal
|
||||||
|
|
||||||
|
import paho.mqtt.client as paho
|
||||||
|
|
||||||
|
from ConextAGS import ConextAGS
|
||||||
|
from ConextInsightHome import ConextInsightHome
|
||||||
|
from JKBMS import JKBMS
|
||||||
|
from Register import Register
|
||||||
|
|
||||||
|
# Global variables to enable cleanups in signal handler
|
||||||
|
all_modbus_devices = []
|
||||||
|
paho_client = None
|
||||||
|
|
||||||
|
def cleanup(_signo, _stack_frame):
|
||||||
|
print("Cleaning up before being terminated!")
|
||||||
|
global paho_client
|
||||||
|
if paho_client != None:
|
||||||
|
paho_client.disconnect()
|
||||||
|
|
||||||
|
global all_modbus_devices
|
||||||
|
for device in all_modbus_devices:
|
||||||
|
device.disconnect()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
def main(daemon):
|
||||||
|
# setup the signal handler
|
||||||
|
signal.signal(signal.SIGTERM, cleanup)
|
||||||
|
|
||||||
|
# load yaml config
|
||||||
|
f = open("config.yaml","r")
|
||||||
|
config = yaml.load(f, Loader=yaml.SafeLoader)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Connect to MQTT server. We do it each time since it's not costly
|
||||||
|
# and avoids long keepalive if the updateinterval is set to a high value.
|
||||||
|
global paho_client
|
||||||
|
paho_client = paho.Client()
|
||||||
|
try:
|
||||||
|
paho_client.connect(config['mqtt']['host'], int(config['mqtt']['port']), 60)
|
||||||
|
except:
|
||||||
|
print("Couldn't connect to the MQTT broker, the GUI part if used, won't be updated.")
|
||||||
|
paho_client = None
|
||||||
|
|
||||||
|
global all_modbus_devices
|
||||||
|
all_devices = {}
|
||||||
|
all_bms = config['bms']
|
||||||
|
|
||||||
|
average_voltage = 0
|
||||||
|
average_soc = 0
|
||||||
|
active_bms = 0
|
||||||
|
total_used_capacity = 0
|
||||||
|
|
||||||
|
highest_soc = 0
|
||||||
|
highest_id = 0
|
||||||
|
lowest_soc = 100
|
||||||
|
lowest_id = 0
|
||||||
|
|
||||||
|
if all_bms != None:
|
||||||
|
for key in all_bms.keys():
|
||||||
|
bms = all_bms[key]
|
||||||
|
bms_id = bms['id']
|
||||||
|
bms_port = bms['port']
|
||||||
|
|
||||||
|
jkbms = JKBMS(key, bms_id, bms_port)
|
||||||
|
c = jkbms.connect()
|
||||||
|
|
||||||
|
if c == None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
#print(jkbms)
|
||||||
|
print(jkbms.formattedOutput(), '\n')
|
||||||
|
|
||||||
|
all_modbus_devices.append(jkbms)
|
||||||
|
active_bms += 1
|
||||||
|
|
||||||
|
soc = jkbms.getRegister('SOCStateOfcharge').value & 0x0FF
|
||||||
|
average_soc += soc;
|
||||||
|
|
||||||
|
# We adjust the lowest/highest SOC
|
||||||
|
if soc > highest_soc:
|
||||||
|
highest_soc = soc
|
||||||
|
highest_id = bms_id
|
||||||
|
if lowest_soc > soc:
|
||||||
|
lowest_soc = soc
|
||||||
|
lowest_id = bms_id
|
||||||
|
|
||||||
|
average_voltage += jkbms.getRegister('BatVol').value
|
||||||
|
total_used_capacity += (jkbms.getRegister('SOCFullChargeCap').value - jkbms.getRegister('SOCCapRemain').value)
|
||||||
|
|
||||||
|
# Publish all BMS values in MQTT
|
||||||
|
jkbms.publish(all_devices)
|
||||||
|
|
||||||
|
if config['insighthome'] != None:
|
||||||
|
conext = ConextInsightHome(config['insighthome']['host'], config['insighthome']['port'], config['insighthome'].get('ids', None))
|
||||||
|
all_modbus_devices.append(conext)
|
||||||
|
c = conext.connect()
|
||||||
|
devices = conext.allDevices()
|
||||||
|
for device in devices:
|
||||||
|
device.publish(all_devices)
|
||||||
|
#print("%s: %s\n" % (type(device).__name__, device))
|
||||||
|
print(device.formattedOutput(), '\n')
|
||||||
|
|
||||||
|
# Publish all values in MQTT
|
||||||
|
if paho_client != None:
|
||||||
|
paho_client.publish("berrybms", json.dumps(all_devices))
|
||||||
|
paho_client.disconnect()
|
||||||
|
|
||||||
|
# We disconnect from all Modbus devices
|
||||||
|
for device in all_modbus_devices:
|
||||||
|
device.disconnect()
|
||||||
|
all_modbus_devices = []
|
||||||
|
|
||||||
|
print("== Global BMS Statistics ==")
|
||||||
|
average_voltage = average_voltage/active_bms
|
||||||
|
average_soc = average_soc/active_bms
|
||||||
|
|
||||||
|
print(f"Average Voltage:\t{average_voltage:.2f}v")
|
||||||
|
print(f"Average SOC:\t\t{average_soc:.1f}%")
|
||||||
|
print(f"Total Used Capacity:\t{total_used_capacity:.2f} Ah (~ {(total_used_capacity*average_voltage/1000):.2f} KWh)")
|
||||||
|
print(f"Lowest SOC:\t\t{lowest_id} ({lowest_soc}%) Highest: {highest_id} ({highest_soc}%)")
|
||||||
|
|
||||||
|
if daemon:
|
||||||
|
updateinterval = config.get('updateinterval')
|
||||||
|
if updateinterval == None:
|
||||||
|
updateinterval = 30
|
||||||
|
print(f'Sleeping for {updateinterval} seconds...')
|
||||||
|
time.sleep(updateinterval)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args = sys.argv[1:]
|
||||||
|
daemon = False
|
||||||
|
#print(args)
|
||||||
|
if len(args) == 1 and args[0] == '-d':
|
||||||
|
daemon = True
|
||||||
|
|
||||||
|
main(daemon)
|
||||||
366
berrydash.py
Normal file
366
berrydash.py
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
#
|
||||||
|
# Copyright (C) 2025 Extrafu <extrafu@gmail.com>
|
||||||
|
#
|
||||||
|
# This file is part of BerryBMS.
|
||||||
|
#
|
||||||
|
# BerryBMS is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the
|
||||||
|
# Free Software Foundation; either version 3, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
import dash
|
||||||
|
from dash import Dash, html, dcc, Input, Output
|
||||||
|
import dash_daq as daq
|
||||||
|
import dash_bootstrap_components as dbc
|
||||||
|
from dash_bootstrap_templates import load_figure_template
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
from flask_mqtt import Mqtt
|
||||||
|
import json
|
||||||
|
|
||||||
|
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.DARKLY, dbc.icons.FONT_AWESOME])
|
||||||
|
server = app.server
|
||||||
|
load_figure_template("darkly")
|
||||||
|
|
||||||
|
server.config['MQTT_BROKER_URL'] = 'localhost'
|
||||||
|
server.config['MQTT_BROKER_PORT'] = 1883
|
||||||
|
mqtt = Mqtt()
|
||||||
|
mqtt.init_app(app)
|
||||||
|
|
||||||
|
all_battmon = {}
|
||||||
|
all_bms = {}
|
||||||
|
all_mppt = {}
|
||||||
|
all_xw = {}
|
||||||
|
|
||||||
|
@mqtt.on_connect()
|
||||||
|
def handle_connect(client, userdata, flags, reason_code):
|
||||||
|
print("Connected with result code "+ str(reason_code))
|
||||||
|
mqtt.subscribe("berrybms")
|
||||||
|
|
||||||
|
@mqtt.on_message()
|
||||||
|
def handle_mqtt_message(client, userdata, message):
|
||||||
|
devices = json.loads(message.payload)
|
||||||
|
#print("Got values (%s) from MQTT from topic: %s" % (str(devices), message.topic))
|
||||||
|
for key in devices.keys():
|
||||||
|
if key.startswith("battmon"):
|
||||||
|
global all_battmon
|
||||||
|
all_battmon[key] = devices[key]
|
||||||
|
elif key.startswith("bms"):
|
||||||
|
global all_bms
|
||||||
|
all_bms[key] = devices[key]
|
||||||
|
elif key.startswith("mppt"):
|
||||||
|
global all_mppt
|
||||||
|
all_mppt[key] = devices[key]
|
||||||
|
elif key.startswith("xw"):
|
||||||
|
global all_xw
|
||||||
|
all_xw[key] = devices[key]
|
||||||
|
|
||||||
|
def buildConextGauge():
|
||||||
|
if len(all_xw) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
active_power = 0
|
||||||
|
#battery_power = 0
|
||||||
|
ac_load_active_power = 0
|
||||||
|
mppt_dc_output_power = 0
|
||||||
|
pv_input_active_today = 0
|
||||||
|
grid_ac_input_power = 0
|
||||||
|
grid_active_today = 0
|
||||||
|
generator_ac_power = 0
|
||||||
|
generator_active_today = 0
|
||||||
|
charge_dc_power = 0
|
||||||
|
charge_dc_current = 0
|
||||||
|
|
||||||
|
for key in all_xw.keys():
|
||||||
|
xw = all_xw[key]
|
||||||
|
#battery_power += xw['BatteryPower']
|
||||||
|
ac_load_active_power += xw['LoadACPowerApparent']
|
||||||
|
grid_ac_input_power += xw['GridACInputPower']
|
||||||
|
grid_active_today += xw['GridInputActiveToday']
|
||||||
|
generator_ac_power += xw['GeneratorACPowerApparent']
|
||||||
|
generator_active_today += xw['GeneratorInputActiveToday']
|
||||||
|
charge_dc_power += xw['ChargeDCPower']
|
||||||
|
charge_dc_current += xw['ChargeDCCurrent']
|
||||||
|
|
||||||
|
for key in all_mppt.keys():
|
||||||
|
mppt = all_mppt[key]
|
||||||
|
mppt_dc_output_power += mppt['DCOutputPower']
|
||||||
|
pv_input_active_today += mppt['PVInputActiveToday']
|
||||||
|
|
||||||
|
active_power =(mppt_dc_output_power+grid_ac_input_power+generator_ac_power)-(ac_load_active_power)
|
||||||
|
|
||||||
|
gauge = daq.Gauge(
|
||||||
|
showCurrentValue=True,
|
||||||
|
color={"gradient": True, "ranges": {"red": [-10000, -1000], "yellow": [-1000, 1000], "green": [1000, 10000]}},
|
||||||
|
id="active-power-gauge",
|
||||||
|
max=10000,
|
||||||
|
min=-10000,
|
||||||
|
size=200,
|
||||||
|
units="Watts",
|
||||||
|
style={'display': 'block', 'margin-bottom': -80, 'margin-top': -15},
|
||||||
|
value=active_power,
|
||||||
|
digits=0 # available after 0.5, see https://github.com/plotly/dash-daq/pull/117/files
|
||||||
|
)
|
||||||
|
|
||||||
|
row1 = html.Tr([html.Td("AC Load Active"), html.Td(f'{ac_load_active_power} W')])
|
||||||
|
row2 = html.Tr([html.Td("MPPT DC Ouput"),
|
||||||
|
html.Td(['{} W ({} '.format(mppt_dc_output_power, time.strftime("%H:%M:%S", time.gmtime(pv_input_active_today))),
|
||||||
|
html.I(className='fa-solid fa-hourglass'),
|
||||||
|
')'])
|
||||||
|
])
|
||||||
|
row3 = html.Tr([html.Td("Grid AC Input"),
|
||||||
|
html.Td(['{} W ({} '.format(grid_ac_input_power, time.strftime("%H:%M:%S", time.gmtime(grid_active_today))),
|
||||||
|
html.I(className='fa-solid fa-hourglass'),
|
||||||
|
')'])
|
||||||
|
])
|
||||||
|
row4 = html.Tr([html.Td("Generator AC Input"),
|
||||||
|
html.Td(['{} W ({} '.format(generator_ac_power, time.strftime("%H:%M:%S", time.gmtime(generator_active_today))),
|
||||||
|
html.I(className='fa-solid fa-hourglass'),
|
||||||
|
')'])
|
||||||
|
])
|
||||||
|
row5 = html.Tr([html.Td("XW+ Charge DC"), html.Td(f'{charge_dc_power} W ({charge_dc_current:.2f} A)')])
|
||||||
|
table_body_left = [html.Tbody([row1, row2, row3, row4, row5])]
|
||||||
|
|
||||||
|
card = dbc.Card(
|
||||||
|
[
|
||||||
|
dbc.CardHeader(children=[html.B("Active Power", style={'font-size':'13px'})]),
|
||||||
|
dbc.CardBody([
|
||||||
|
gauge,
|
||||||
|
dbc.Table(table_body_left, dark=True, striped=True, bordered=False, hover=True, style={'font-size': '11px'})
|
||||||
|
], style={
|
||||||
|
'padding': '0'
|
||||||
|
})
|
||||||
|
],
|
||||||
|
#color="primary",
|
||||||
|
inverse=True,
|
||||||
|
style={"width": "16rem"}
|
||||||
|
)
|
||||||
|
|
||||||
|
return card
|
||||||
|
|
||||||
|
def buildConextStats():
|
||||||
|
if len(all_bms) == 0 or len(all_battmon) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
average_bms_soc = 0
|
||||||
|
average_battmon_soc = 0
|
||||||
|
average_bms_voltage = 0
|
||||||
|
average_bms_current = 0
|
||||||
|
average_battmon_voltage = 0
|
||||||
|
average_battmon_current = 0
|
||||||
|
removed_bms_capacity = 0
|
||||||
|
remaining_bms_capacity = 0
|
||||||
|
total_bms_capacity = 0
|
||||||
|
removed_battmon_capacity = 0
|
||||||
|
remaining_battmon_capacity = 0
|
||||||
|
total_battmon_capacity = 0
|
||||||
|
|
||||||
|
for key in all_bms.keys():
|
||||||
|
bms = all_bms[key]
|
||||||
|
average_bms_soc += (bms['SOCStateOfcharge'] & 0x0FF)
|
||||||
|
average_bms_voltage += bms['BatVol']
|
||||||
|
average_bms_current += bms['BatCurrent']
|
||||||
|
remaining_bms_capacity += bms['SOCCapRemain']
|
||||||
|
total_bms_capacity += bms['SOCFullChargeCap']
|
||||||
|
average_bms_soc /= len(all_bms)
|
||||||
|
average_bms_voltage /= len(all_bms)
|
||||||
|
removed_bms_capacity = total_bms_capacity-remaining_bms_capacity
|
||||||
|
|
||||||
|
if len(all_battmon):
|
||||||
|
for key in all_battmon.keys():
|
||||||
|
battmon = all_battmon[key]
|
||||||
|
average_battmon_soc += battmon['BatterySOC']
|
||||||
|
average_battmon_voltage += battmon['BatteryVoltage']
|
||||||
|
average_battmon_current += battmon['BatteryCurrent']
|
||||||
|
remaining_battmon_capacity += battmon['BatteryCapacityRemaining']
|
||||||
|
removed_battmon_capacity += battmon['BatteryCapacityRemoved']
|
||||||
|
total_battmon_capacity += battmon['BatteryCapacity']
|
||||||
|
|
||||||
|
average_battmon_soc /= len(all_battmon)
|
||||||
|
average_battmon_voltage /= len(all_battmon)
|
||||||
|
|
||||||
|
row1 = html.Tr([html.Td("Average SOC (BMS)"), html.Td(f'{average_bms_soc:.2f} %')])
|
||||||
|
row2 = html.Tr([html.Td("Average SOC (BattMon)"), html.Td(f'{average_battmon_soc:.2f} %')])
|
||||||
|
row3 = html.Tr([html.Td("Average voltage/current (BMS)"), html.Td(f'{average_bms_voltage:.2f} v / {average_bms_current:.2f} A')])
|
||||||
|
row4 = html.Tr([html.Td("Average voltage/current (BattMon)"), html.Td(f'{average_battmon_voltage:.2f} v / {average_battmon_current:.2f} A')])
|
||||||
|
row5 = html.Tr([html.Td("Removed capacity (BMS)"), html.Td(f'{removed_bms_capacity:.1f} Ah ({remaining_bms_capacity:.1f} Ah remaining of {total_bms_capacity:.0f} Ah)')])
|
||||||
|
row6 = html.Tr([html.Td("Removed capacity (BattMon)"), html.Td(f'{removed_battmon_capacity:.1f} Ah ({remaining_battmon_capacity:.1f} Ah remaining of {total_battmon_capacity:.0f} Ah)')])
|
||||||
|
table_body_right = [html.Tbody([row1, row2, row3, row4, row5, row6])]
|
||||||
|
|
||||||
|
table = dbc.Table(table_body_right, dark=True, striped=True, bordered=False, hover=True, responsive=True, style={'font-size': '11px'})
|
||||||
|
|
||||||
|
card = dbc.Card(
|
||||||
|
[
|
||||||
|
dbc.CardHeader(children=[html.B("BMS/BattMon Statistics", style={'font-size':'13px'})]),
|
||||||
|
dbc.CardBody([
|
||||||
|
table
|
||||||
|
], style={
|
||||||
|
'padding': '0'
|
||||||
|
})
|
||||||
|
],
|
||||||
|
style={"width": "16rem", "height": "100%"}
|
||||||
|
)
|
||||||
|
|
||||||
|
return card
|
||||||
|
|
||||||
|
def buildBMSGauge(key, bms):
|
||||||
|
cellCount = bms.get("CellCount")
|
||||||
|
highestCellVoltage = 0
|
||||||
|
lowestCellVoltage = sys.maxsize
|
||||||
|
highestCellIndex = 0
|
||||||
|
lowestCellIndex = 0
|
||||||
|
badges = []
|
||||||
|
tooltips = []
|
||||||
|
|
||||||
|
for i in range(0,cellCount):
|
||||||
|
cellVoltage = round(float(bms.get(f'CellVol{i}')),3)
|
||||||
|
if cellVoltage < lowestCellVoltage:
|
||||||
|
lowestCellIndex = i
|
||||||
|
lowestCellVoltage = cellVoltage
|
||||||
|
elif cellVoltage >= highestCellVoltage:
|
||||||
|
highestCellIndex = i
|
||||||
|
highestCellVoltage = cellVoltage
|
||||||
|
b = dbc.Badge('{:.3f}v'.format(cellVoltage), color="primary", id=f'{key}-cellVol{i}')
|
||||||
|
badges.append(b)
|
||||||
|
tooltips.append(dbc.Tooltip(f'Cell {i+1}', target=f'{key}-cellVol{i}'))
|
||||||
|
|
||||||
|
badges[highestCellIndex].color = "green"
|
||||||
|
badges[lowestCellIndex].color = "warning"
|
||||||
|
|
||||||
|
warning_icon_color = 'gray'
|
||||||
|
warning_message = 'No warning'
|
||||||
|
if bms.get("Alarms") > 0:
|
||||||
|
warning_icon_color = 'red'
|
||||||
|
|
||||||
|
card = dbc.Card(
|
||||||
|
[
|
||||||
|
dbc.CardHeader(children=[
|
||||||
|
html.B("{} - {} (v{}) ".format(bms["name"], bms['ManufacturerDeviceID'], bms['SoftwareVersion']), style={'font-size':'13px'}),
|
||||||
|
html.I(className='fa-solid fa-triangle-exclamation', style={"color": warning_icon_color}, id=f'{key}-warning'),
|
||||||
|
dbc.Tooltip(warning_message, target=f'{key}-warning')
|
||||||
|
]),
|
||||||
|
dbc.CardBody([
|
||||||
|
daq.Gauge(
|
||||||
|
showCurrentValue=True,
|
||||||
|
color={"gradient": True, "ranges": {"red": [0, 25], "yellow": [25, 65], "green": [65, 100]}},
|
||||||
|
#label="{} - {} (v{})".format(key, bms['ManufacturerDeviceID'], bms['SoftwareVersion']),
|
||||||
|
labelPosition='top',
|
||||||
|
id=f'soc-gauge-{key}',
|
||||||
|
max=100,
|
||||||
|
size=200,
|
||||||
|
units="{:.2f} Ah of {:.0f} Ah".format(bms['SOCCapRemain'], bms['SOCFullChargeCap']),
|
||||||
|
style={'display': 'block', 'margin-bottom': -80, 'margin-top': -30},
|
||||||
|
value=bms['SOCStateOfcharge'],
|
||||||
|
digits=0 # available after 0.5, see https://github.com/plotly/dash-daq/pull/117/files
|
||||||
|
),
|
||||||
|
html.Span("{} at {:.2f}A/{:.0f}W ({} cycles)".format(("Charging" if bms['BatCurrent'] > 0 else "Discharging"),
|
||||||
|
abs(bms['BatCurrent']),
|
||||||
|
abs(bms['BatCurrent'])*bms['BatVol'],
|
||||||
|
bms['SOCCycleCount']),
|
||||||
|
style={'font-size':'11px'}),
|
||||||
|
html.Br(),
|
||||||
|
html.Span(badges+tooltips)
|
||||||
|
])
|
||||||
|
],
|
||||||
|
#color="primary",
|
||||||
|
inverse=True,
|
||||||
|
style={"width": "16rem", 'padding': '0'}
|
||||||
|
)
|
||||||
|
|
||||||
|
return card
|
||||||
|
|
||||||
|
def buildBMSGauges():
|
||||||
|
cols = []
|
||||||
|
for key in all_bms.keys():
|
||||||
|
bms = all_bms[key]
|
||||||
|
card = buildBMSGauge(key, bms)
|
||||||
|
cols.append(dbc.Col(card))
|
||||||
|
|
||||||
|
return dbc.Row(id='bms-gauges', children=cols, className="g-0")
|
||||||
|
|
||||||
|
app.layout = html.Div([
|
||||||
|
dcc.Interval(
|
||||||
|
id='interval-component',
|
||||||
|
interval=5*1000,
|
||||||
|
n_intervals=0
|
||||||
|
),
|
||||||
|
html.Div(dbc.Row([
|
||||||
|
dbc.Col(id='conext-gauge', children=[buildConextGauge()]),
|
||||||
|
dbc.Col(id='conext-stats', children=[buildConextStats()]),
|
||||||
|
dbc.Col(dbc.Tabs(id='tabs', active_tab='tab-0'))
|
||||||
|
],
|
||||||
|
className="g-0")),
|
||||||
|
html.Div(buildBMSGauges())
|
||||||
|
])
|
||||||
|
|
||||||
|
@app.callback(Output(component_id='conext-gauge', component_property='children'),
|
||||||
|
Input(component_id='interval-component', component_property='n_intervals'))
|
||||||
|
def update_conext_gauge(n):
|
||||||
|
return buildConextGauge()
|
||||||
|
|
||||||
|
@app.callback(Output(component_id='conext-stats', component_property='children'),
|
||||||
|
Input(component_id='interval-component', component_property='n_intervals'))
|
||||||
|
def update_conext_stats(n):
|
||||||
|
return buildConextStats()
|
||||||
|
|
||||||
|
@app.callback(Output(component_id='bms-gauges', component_property='children'),
|
||||||
|
Input(component_id='interval-component', component_property='n_intervals'))
|
||||||
|
def update_bms_gauges(n):
|
||||||
|
return buildBMSGauges()
|
||||||
|
|
||||||
|
@app.callback(Output(component_id='tabs', component_property='children'),
|
||||||
|
Input(component_id='interval-component', component_property='n_intervals'))
|
||||||
|
def update_tabs(n):
|
||||||
|
tabs = []
|
||||||
|
if len(all_mppt) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
x_axis = ['Battery Out', 'Battery In', 'PV In', 'Generator In', 'Grid In', 'Load Out']
|
||||||
|
tab_index = 0
|
||||||
|
for period in ['ThisHour', 'Today', 'ThisWeek', 'ThisMonth']:
|
||||||
|
y_axis = [0, 0, 0, 0, 0, 0]
|
||||||
|
|
||||||
|
# Get data from the MPPT charge controllers
|
||||||
|
for key in all_mppt.keys():
|
||||||
|
mppt = all_mppt[key]
|
||||||
|
y_axis[1] += mppt[f'EnergyToBattery{period}']
|
||||||
|
y_axis[2] += mppt[f'EnergyFromPV{period}']
|
||||||
|
|
||||||
|
# Get data from the XW inverters
|
||||||
|
for key in all_xw.keys():
|
||||||
|
xw = all_xw[key]
|
||||||
|
y_axis[0] += xw[f'EnergyToBattery{period}']
|
||||||
|
y_axis[1] += xw[f'EnergyFromBattery{period}']
|
||||||
|
y_axis[3] += xw[f'GeneratorInputEnergy{period}']
|
||||||
|
y_axis[4] += xw[f'GridInputEnergy{period}']
|
||||||
|
y_axis[5] += xw[f'LoadOutputEnergy{period}']
|
||||||
|
|
||||||
|
for i in range(0,6):
|
||||||
|
y_axis[i] = round(y_axis[i], 2)
|
||||||
|
|
||||||
|
colors = ['crimson','aqua','aquamarine','aquamarine','aquamarine','crimson']
|
||||||
|
fig = go.Figure(data=[go.Bar(
|
||||||
|
x=x_axis,
|
||||||
|
y=y_axis,
|
||||||
|
text=y_axis,
|
||||||
|
textposition='auto',
|
||||||
|
marker_color=colors
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
fig.update_layout(margin=dict(l=5, r=5, t=5, b=5))
|
||||||
|
fig.update_yaxes(title='kWh')
|
||||||
|
|
||||||
|
tab = dbc.Tab(id=f'tab-{tab_index}',
|
||||||
|
label=period.replace('This',''),
|
||||||
|
active_label_style={"color": "#F39C12"},
|
||||||
|
children=[dcc.Graph(id=f'graph-{period}', figure=fig, config={'displayModeBar': False}, style={'width': '50vw', 'height': '50vh'})]
|
||||||
|
)
|
||||||
|
tabs.append(tab)
|
||||||
|
tab_index += 1
|
||||||
|
|
||||||
|
return tabs
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run_server(debug=False, port=8080, host='0.0.0.0', use_reloader=False)
|
||||||
86
homeassistant.py
Normal file
86
homeassistant.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
from json import dumps
|
||||||
|
from os.path import join
|
||||||
|
|
||||||
|
class ha_mqtt_formatter():
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
manufacturer : str,
|
||||||
|
model : str,
|
||||||
|
name : str,
|
||||||
|
identifiers : list,
|
||||||
|
availability_topic : list,
|
||||||
|
device_class : str
|
||||||
|
):
|
||||||
|
self.base = {
|
||||||
|
'device' : {
|
||||||
|
'manufacturer': manufacturer,
|
||||||
|
'model' : model,
|
||||||
|
'name' : name,
|
||||||
|
'identifiers' : identifiers,
|
||||||
|
},
|
||||||
|
'device_class' : device_class,
|
||||||
|
'availability' : [
|
||||||
|
{"topic" : x,
|
||||||
|
"value_template": "{{ value_json.state }}"
|
||||||
|
} for x in availability_topic
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def binary_sensor(self, name, unique_id, payload_on, payload_off, state_topic, entity_category='diagnostic'):
|
||||||
|
update = self.base.copy()
|
||||||
|
update.update(
|
||||||
|
{
|
||||||
|
'name' : name,
|
||||||
|
'unique_id' : unique_id,
|
||||||
|
'payload_on' : payload_on,
|
||||||
|
'payload_off' : payload_off,
|
||||||
|
'state_topic' : state_topic,
|
||||||
|
'entity_category' : entity_category,
|
||||||
|
#"value_template": "{{ value_json.state }}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return update
|
||||||
|
|
||||||
|
def switch(self, unique_id, name, payload_on, payload_off, state_topic, entity_category='config'):
|
||||||
|
update = self.base.copy()
|
||||||
|
update.update(
|
||||||
|
{
|
||||||
|
'name' : name,
|
||||||
|
'unique_id' : unique_id,
|
||||||
|
'payload_on' : payload_on,
|
||||||
|
'payload_off' : payload_off,
|
||||||
|
'state_topic' : state_topic,
|
||||||
|
"command_toppic" : join(self.base["base_topic", 'name', 'set']),
|
||||||
|
'entity_category' : entity_category,
|
||||||
|
# "value_template": "{{ value_json.state }}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return update
|
||||||
|
|
||||||
|
def sensor(self, name, unique_id, state_class, state_topic, unit, entity_category='diagnostic'):
|
||||||
|
update = self.base.copy()
|
||||||
|
update.update(
|
||||||
|
{
|
||||||
|
'name' : name,
|
||||||
|
'unique_id' : unique_id,
|
||||||
|
'unit' : unit,
|
||||||
|
'state_topic' : state_topic,
|
||||||
|
'entity_category' : entity_category,
|
||||||
|
'state_class' : state_class,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return update
|
||||||
|
|
||||||
|
def number(self, unique_id, name, state_topic, unit, entity_category='config'):
|
||||||
|
update = self.base.copy()
|
||||||
|
update.update(
|
||||||
|
{
|
||||||
|
'name' : name,
|
||||||
|
'unique_id' : unique_id,
|
||||||
|
'unit' : unit,
|
||||||
|
'entity_category' : entity_category,
|
||||||
|
'state_topic' : state_topic,
|
||||||
|
# "value_template": f"{{ value_json.{name} }}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return update
|
||||||
50
test.py
Normal file
50
test.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from JKBMS import JKBMS
|
||||||
|
from Register import Register
|
||||||
|
from homeassistant import ha_mqtt_formatter
|
||||||
|
import paho.mqtt.client as paho
|
||||||
|
import json
|
||||||
|
|
||||||
|
HOMEASSISTANT_TOPIC = 'homeassistant'
|
||||||
|
BASE_TOPIC = 'jk-bms'
|
||||||
|
# switches = ['BatChargeEN', 'BatDisChargeEN', 'BatBalanceEN', 'HeaterEN', 'DisableTempSensor', 'GpsHeartBeat' , 'PortSwitch', 'LcdAlwaysOn', 'SpecialCharger', 'SmartSleep', 'DisablePclModule', 'TimedStoredData', 'ChargingFloatEN']
|
||||||
|
# sensors_v = [, 'CellVolAve', 'BatCurrent', 'SOCStateOfcharge', 'SOCCapRemain', , 'SOCCycleCap']
|
||||||
|
# sensors_c = ['CellCount', 'SOCCycleCount', ]
|
||||||
|
# sensors_a =
|
||||||
|
# numbers = ['CellUnderVoltProtectionValue', '100PercentSoCVoltageValue', 'ZeroPercentSoCVoltageValue', 'CellChargeOverCurrentProtection', 'CellDischargeOverCurrentProtection', 'SOCFullChargeCap']
|
||||||
|
|
||||||
|
#setup
|
||||||
|
jkbms = JKBMS(BASE_TOPIC, 0x01, "/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0")
|
||||||
|
# paho_client = paho.Client(client_id='bms-monitor', callback_api_version=paho.CallbackAPIVersion.VERSION2)
|
||||||
|
# paho_client.username_pw_set('growatt','growatt_password')
|
||||||
|
# paho_client.connect('homeassistant.internal', 1883, 60)
|
||||||
|
# mqtt_ha_base = ha_mqtt_formatter("JiKong", 'PB2A16S20A', f'bms-{jkbms.id}', [f'{BASE_TOPIC}_bms-{jkbms.id}'], [f'{BASE_TOPIC}/bms-{jkbms.id}/availability'], 'battery')
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not jkbms.connect():
|
||||||
|
raise Exception.with_traceback()
|
||||||
|
# for cell in range(len(jkbms.getRegister('CellCount').value)):
|
||||||
|
# paho_client.publish(f'{HOMEASSISTANT_TOPIC}/sensor', mqtt_ha_base.sensor(f'{BASE_TOPIC}_bms-{jkbms.id}_CellVol{cell}', f'CellVol{cell}', "measurement", f'{BASE_TOPIC}/bms-{jkbms.id}', 'v',))
|
||||||
|
# for switch in switches:
|
||||||
|
# paho_client.publish(f'{HOMEASSISTANT_TOPIC}/switch', mqtt_ha_base.switch(f'{BASE_TOPIC}_bms-{jkbms.id}_{switch}', switch, True, False, f'{BASE_TOPIC}/bms-{jkbms.id}'))
|
||||||
|
# for sensor in sensors:
|
||||||
|
# paho_client.publish(mqtt_ha_base.sensor(f'{BASE_TOPIC}_bms-{jkbms.id}_{sensor}', sensor, "measurement", f'{BASE_TOPIC}/bms-{jkbms.id}', ))
|
||||||
|
except Exception as ex:
|
||||||
|
print("Couldn't connect to the MQTT broker, the GUI part if used, won't be updated.")
|
||||||
|
paho_client = None
|
||||||
|
|
||||||
|
#main
|
||||||
|
try:
|
||||||
|
|
||||||
|
jkbms.getCellVoltages()
|
||||||
|
updates = jkbms.dump()
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# if paho_client != None:
|
||||||
|
|
||||||
|
|
||||||
|
# paho_client.publish(f"bms-monitor/{list(updates.keys())[0]}", json.dumps(updates[list(updates.keys())[0]]))
|
||||||
|
# paho_client.on_subscribe
|
||||||
|
# paho_client.disconnect()
|
||||||
|
except Exception as ex:
|
||||||
|
print(ex)
|
||||||
Loading…
x
Reference in New Issue
Block a user