Add influx exporter
... as well as turning debug and ser into global variables (for ease of cleanup). Influx, unlike Prometheus, requires typed values, so a raw_value field has been added to the data structure: value is a string, raw_value keeps its type.
This commit is contained in:
parent
951013f0b8
commit
c174022d98
295
bms.py
295
bms.py
@ -3,11 +3,11 @@
|
|||||||
# Communicate with a JBD/SZLLT BMS and return basic information
|
# Communicate with a JBD/SZLLT BMS and return basic information
|
||||||
# in order to shutdown equipment when voltage levels drop or remaining
|
# in order to shutdown equipment when voltage levels drop or remaining
|
||||||
# capacity gets low
|
# capacity gets low
|
||||||
# TODO: get individual cell voltage
|
|
||||||
# TODO: scripts to shutdown NAS when voltage goes below 13.xV or
|
# TODO: scripts to shutdown NAS when voltage goes below 13.xV or
|
||||||
# percent_capacity goes below 25%
|
# percent_capacity goes below 25%
|
||||||
#
|
#
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import json
|
import json
|
||||||
import pprint
|
import pprint
|
||||||
import serial
|
import serial
|
||||||
@ -20,9 +20,17 @@ try:
|
|||||||
can_export_prometheus = True
|
can_export_prometheus = True
|
||||||
except:
|
except:
|
||||||
can_export_prometheus = False
|
can_export_prometheus = False
|
||||||
|
try:
|
||||||
|
from influxdb_client import InfluxDBClient, Point
|
||||||
|
from influxdb_client.client.write_api import SYNCHRONOUS
|
||||||
|
import datetime
|
||||||
|
can_export_influxdb = True
|
||||||
|
influxclient = None
|
||||||
|
writeapi = None
|
||||||
|
except:
|
||||||
|
can_export_influxdb = False
|
||||||
|
|
||||||
|
DAEMON_UPDATE_PERIOD = 30
|
||||||
PROMETHEUS_UPDATE_PERIOD = 30
|
|
||||||
SERIALPORT = "/dev/ttyUSB0"
|
SERIALPORT = "/dev/ttyUSB0"
|
||||||
#SERIALPORT = "/dev/rfcomm1"
|
#SERIALPORT = "/dev/rfcomm1"
|
||||||
BAUDRATE = 9600
|
BAUDRATE = 9600
|
||||||
@ -53,7 +61,10 @@ ser.stopbits = serial.STOPBITS_ONE # number of stop bits
|
|||||||
ser.timeout = 1 # timeout block read
|
ser.timeout = 1 # timeout block read
|
||||||
ser.writeTimeout = 1 # timeout for write
|
ser.writeTimeout = 1 # timeout for write
|
||||||
|
|
||||||
def cleanup(ser, debug):
|
''' Clean up serial port '''
|
||||||
|
def cleanup():
|
||||||
|
global debug
|
||||||
|
global ser
|
||||||
if debug > 2:
|
if debug > 2:
|
||||||
print("Cleaning up...")
|
print("Cleaning up...")
|
||||||
if ser.is_open:
|
if ser.is_open:
|
||||||
@ -61,6 +72,16 @@ def cleanup(ser, debug):
|
|||||||
ser.reset_output_buffer() # flush output buffer, aborting current output
|
ser.reset_output_buffer() # flush output buffer, aborting current output
|
||||||
ser.close()
|
ser.close()
|
||||||
|
|
||||||
|
''' Close remote connections '''
|
||||||
|
def shutdown():
|
||||||
|
global debug
|
||||||
|
global influxclient
|
||||||
|
if influxclient is not None:
|
||||||
|
if writeapi is not None:
|
||||||
|
writeapi.close()
|
||||||
|
influxclient.close()
|
||||||
|
|
||||||
|
|
||||||
def calculate_checksum(msg):
|
def calculate_checksum(msg):
|
||||||
checksum = ''
|
checksum = ''
|
||||||
return checksum
|
return checksum
|
||||||
@ -98,7 +119,9 @@ def bytes_to_date(high, low):
|
|||||||
return "{:04d}-{:02d}-{:02d}".format(year, mon, day)
|
return "{:04d}-{:02d}-{:02d}".format(year, mon, day)
|
||||||
|
|
||||||
# takes a serial object and a message, returns a response
|
# takes a serial object and a message, returns a response
|
||||||
def requestMessage(ser, reqmsg, debug):
|
def requestMessage(reqmsg):
|
||||||
|
global ser
|
||||||
|
global debug
|
||||||
if debug > 2:
|
if debug > 2:
|
||||||
print('Starting Up Serial Monitor')
|
print('Starting Up Serial Monitor')
|
||||||
if ser.is_open:
|
if ser.is_open:
|
||||||
@ -130,7 +153,7 @@ def requestMessage(ser, reqmsg, debug):
|
|||||||
while ser.in_waiting == 0:
|
while ser.in_waiting == 0:
|
||||||
# Return an empty string if we end up waiting too long
|
# Return an empty string if we end up waiting too long
|
||||||
if wait_time > 2:
|
if wait_time > 2:
|
||||||
cleanup(ser, debug)
|
cleanup()
|
||||||
return ''
|
return ''
|
||||||
if debug > 2:
|
if debug > 2:
|
||||||
print("Waiting for data...")
|
print("Waiting for data...")
|
||||||
@ -144,7 +167,7 @@ def requestMessage(ser, reqmsg, debug):
|
|||||||
return ''
|
return ''
|
||||||
if debug > 0:
|
if debug > 0:
|
||||||
print("Read data: {0}".format(response.hex()))
|
print("Read data: {0}".format(response.hex()))
|
||||||
cleanup(ser, debug)
|
cleanup()
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("error communicating...: {0}".function(str(e)))
|
print("error communicating...: {0}".function(str(e)))
|
||||||
@ -152,6 +175,7 @@ def requestMessage(ser, reqmsg, debug):
|
|||||||
print("cannot open serial port")
|
print("cannot open serial port")
|
||||||
|
|
||||||
def parse_03_response(response):
|
def parse_03_response(response):
|
||||||
|
global debug
|
||||||
data = dict()
|
data = dict()
|
||||||
# Response is 34 bytes:
|
# Response is 34 bytes:
|
||||||
# 00 begin: \xDD
|
# 00 begin: \xDD
|
||||||
@ -193,9 +217,9 @@ def parse_03_response(response):
|
|||||||
vtot = bytes_to_digits(response[4], response[5]) * 0.01
|
vtot = bytes_to_digits(response[4], response[5]) * 0.01
|
||||||
data['bms_voltage_total_volts'] = dict()
|
data['bms_voltage_total_volts'] = dict()
|
||||||
data['bms_voltage_total_volts']['help'] = "Total Voltage"
|
data['bms_voltage_total_volts']['help'] = "Total Voltage"
|
||||||
|
data['bms_voltage_total_volts']['raw_value'] = vtot
|
||||||
data['bms_voltage_total_volts']['value'] = "{:.2f}".format(vtot)
|
data['bms_voltage_total_volts']['value'] = "{:.2f}".format(vtot)
|
||||||
data['bms_voltage_total_volts']['units'] = "V"
|
data['bms_voltage_total_volts']['units'] = "V"
|
||||||
#data["Total Voltage"] = "{:.2f}V".format(vtot)
|
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
print("Total voltage: {:.2f}V".format(vtot))
|
print("Total voltage: {:.2f}V".format(vtot))
|
||||||
|
|
||||||
@ -203,9 +227,9 @@ def parse_03_response(response):
|
|||||||
current = convert_to_signed(current) * 0.01
|
current = convert_to_signed(current) * 0.01
|
||||||
data["bms_current_amps"] = dict()
|
data["bms_current_amps"] = dict()
|
||||||
data["bms_current_amps"]['help'] = "Current"
|
data["bms_current_amps"]['help'] = "Current"
|
||||||
|
data["bms_current_amps"]['raw_value'] = current
|
||||||
data["bms_current_amps"]['value'] = "{:.2f}".format(current)
|
data["bms_current_amps"]['value'] = "{:.2f}".format(current)
|
||||||
data["bms_current_amps"]['units'] = "A"
|
data["bms_current_amps"]['units'] = "A"
|
||||||
#data["Current"] = "{:.2f}A".format(current)
|
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
print("Current: {:.2f}A".format(current))
|
print("Current: {:.2f}A".format(current))
|
||||||
|
|
||||||
@ -213,14 +237,14 @@ def parse_03_response(response):
|
|||||||
nom_cap = bytes_to_digits(response[10], response[11]) * 0.01
|
nom_cap = bytes_to_digits(response[10], response[11]) * 0.01
|
||||||
data['bms_capacity_remaining_ah'] = dict()
|
data['bms_capacity_remaining_ah'] = dict()
|
||||||
data['bms_capacity_remaining_ah']['help'] = "Remaining Capacity"
|
data['bms_capacity_remaining_ah']['help'] = "Remaining Capacity"
|
||||||
|
data['bms_capacity_remaining_ah']['raw_value'] = res_cap
|
||||||
data['bms_capacity_remaining_ah']['value'] = "{:.2f}".format(res_cap)
|
data['bms_capacity_remaining_ah']['value'] = "{:.2f}".format(res_cap)
|
||||||
data['bms_capacity_remaining_ah']['units'] = "Ah"
|
data['bms_capacity_remaining_ah']['units'] = "Ah"
|
||||||
data['bms_capacity_nominal_ah'] = dict()
|
data['bms_capacity_nominal_ah'] = dict()
|
||||||
data['bms_capacity_nominal_ah']['help'] = "Nominal Capacity"
|
data['bms_capacity_nominal_ah']['help'] = "Nominal Capacity"
|
||||||
|
data['bms_capacity_nominal_ah']['raw_value'] = nom_cap
|
||||||
data['bms_capacity_nominal_ah']['value'] = "{:.2f}".format(nom_cap)
|
data['bms_capacity_nominal_ah']['value'] = "{:.2f}".format(nom_cap)
|
||||||
data['bms_capacity_nominal_ah']['units'] = "Ah"
|
data['bms_capacity_nominal_ah']['units'] = "Ah"
|
||||||
#data["Remaining Capacity"] = "{:.2f}Ah".format(res_cap)
|
|
||||||
#data["Nominal Capacity"] = "{:.2f}Ah".format(nom_cap)
|
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
print("Remaining capacity: {:.2f}Ah".format(res_cap))
|
print("Remaining capacity: {:.2f}Ah".format(res_cap))
|
||||||
print("Nominal capacity: {:.2f}Ah".format(nom_cap))
|
print("Nominal capacity: {:.2f}Ah".format(nom_cap))
|
||||||
@ -228,8 +252,8 @@ def parse_03_response(response):
|
|||||||
cycle_times = bytes_to_digits(response[12], response[13])
|
cycle_times = bytes_to_digits(response[12], response[13])
|
||||||
data['bms_charge_cycles'] = dict()
|
data['bms_charge_cycles'] = dict()
|
||||||
data['bms_charge_cycles']['help'] = "Charge Cycles"
|
data['bms_charge_cycles']['help'] = "Charge Cycles"
|
||||||
|
data['bms_charge_cycles']['raw_value'] = cycle_times
|
||||||
data['bms_charge_cycles']['value'] = "{0}".format(cycle_times)
|
data['bms_charge_cycles']['value'] = "{0}".format(cycle_times)
|
||||||
#data["Charge Cycles"] = "{0}".format(cycle_times)
|
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
print("Cycle times: {0}".format(cycle_times))
|
print("Cycle times: {0}".format(cycle_times))
|
||||||
|
|
||||||
@ -237,15 +261,14 @@ def parse_03_response(response):
|
|||||||
data['bms_manufacture_date'] = dict()
|
data['bms_manufacture_date'] = dict()
|
||||||
data['bms_manufacture_date']['help'] = "Date of Manufacture"
|
data['bms_manufacture_date']['help'] = "Date of Manufacture"
|
||||||
data['bms_manufacture_date']['info'] = "{0}".format(man_date)
|
data['bms_manufacture_date']['info'] = "{0}".format(man_date)
|
||||||
#data["Date of Manufacture"] = "{0}".format(man_date)
|
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
print("Manufacturing date: {0}".format(man_date))
|
print("Manufacturing date: {0}".format(man_date))
|
||||||
|
|
||||||
cells = response[25] # 4S
|
cells = response[25] # 4S
|
||||||
data['bms_cell_number'] = dict()
|
data['bms_cell_number'] = dict()
|
||||||
data['bms_cell_number']['help'] = "Cells"
|
data['bms_cell_number']['help'] = "Cells"
|
||||||
|
data['bms_cell_number']['raw_value'] = cells
|
||||||
data['bms_cell_number']['value'] = "{0}".format(cells)
|
data['bms_cell_number']['value'] = "{0}".format(cells)
|
||||||
#data["Cells"] = "{0}".format(cells)
|
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
print("Cells: {0}S".format(cells))
|
print("Cells: {0}S".format(cells))
|
||||||
|
|
||||||
@ -272,6 +295,7 @@ def parse_03_response(response):
|
|||||||
data['bms_cells_balancing'] = dict()
|
data['bms_cells_balancing'] = dict()
|
||||||
data['bms_cells_balancing']['help'] = "Cells balancing"
|
data['bms_cells_balancing']['help'] = "Cells balancing"
|
||||||
data['bms_cells_balancing']['label'] = 'cell'
|
data['bms_cells_balancing']['label'] = 'cell'
|
||||||
|
data['bms_cells_balancing']['raw_values'] = dict()
|
||||||
data['bms_cells_balancing']['values'] = dict()
|
data['bms_cells_balancing']['values'] = dict()
|
||||||
for cell in range(cells):
|
for cell in range(cells):
|
||||||
# Cells from 1 to 16 are recorded in balance_state_low,
|
# Cells from 1 to 16 are recorded in balance_state_low,
|
||||||
@ -284,13 +308,13 @@ def parse_03_response(response):
|
|||||||
state = balance_state_low
|
state = balance_state_low
|
||||||
hilo_cell = cell
|
hilo_cell = cell
|
||||||
# Cells are recorded as groups of 4 bits (0x0-0xF) per 4 cells
|
# Cells are recorded as groups of 4 bits (0x0-0xF) per 4 cells
|
||||||
g = int(hilo_cell / 4)
|
g = int(hilo_cell / 4)
|
||||||
b = 2**(hilo_cell - (g * 4 ))
|
b = 2**(hilo_cell - (g * 4 ))
|
||||||
|
data['bms_cells_balancing']['raw_values'][cell+1] = int(bool((state >> g) & b))
|
||||||
data['bms_cells_balancing']['values'][cell+1] = "{0}".format(int(bool((state >> g) & b)))
|
data['bms_cells_balancing']['values'][cell+1] = "{0}".format(int(bool((state >> g) & b)))
|
||||||
#data["Balancing"][cell+1] = "{0}".format(bool((state >> g) & b))
|
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
print("Balancing cell {0}: {1}".format(cell, bool((state >> g & b))))
|
print("Balancing cell {0}: {1}".format(cell, bool((state >> g & b))))
|
||||||
|
|
||||||
protection_state = bytes_to_digits(response[20], response[21])
|
protection_state = bytes_to_digits(response[20], response[21])
|
||||||
sop = protection_state & 1
|
sop = protection_state & 1
|
||||||
sup = protection_state & 2
|
sup = protection_state & 2
|
||||||
@ -307,56 +331,56 @@ def parse_03_response(response):
|
|||||||
slm = protection_state & 4096
|
slm = protection_state & 4096
|
||||||
data['bms_protection_sop_bool'] = dict()
|
data['bms_protection_sop_bool'] = dict()
|
||||||
data['bms_protection_sop_bool']['help'] = "Single overvoltage protection"
|
data['bms_protection_sop_bool']['help'] = "Single overvoltage protection"
|
||||||
data['bms_protection_sop_bool']['value'] ="{0}".format(int(bool(sop)))
|
data['bms_protection_sop_bool']['raw_value'] = int(bool(sop))
|
||||||
|
data['bms_protection_sop_bool']['value'] = "{0}".format(int(bool(sop)))
|
||||||
data['bms_protection_sup_bool'] = dict()
|
data['bms_protection_sup_bool'] = dict()
|
||||||
data['bms_protection_sup_bool']['help'] = "Single undervoltage protection"
|
data['bms_protection_sup_bool']['help'] = "Single undervoltage protection"
|
||||||
data['bms_protection_sup_bool']['value'] ="{0}".format(int(bool(sup)))
|
data['bms_protection_sup_bool']['raw_value'] = int(bool(sup))
|
||||||
|
data['bms_protection_sup_bool']['value'] = "{0}".format(int(bool(sup)))
|
||||||
data['bms_protection_wgop_bool'] = dict()
|
data['bms_protection_wgop_bool'] = dict()
|
||||||
data['bms_protection_wgop_bool']['help'] = "Whole group overvoltage protection"
|
data['bms_protection_wgop_bool']['help'] = "Whole group overvoltage protection"
|
||||||
data['bms_protection_wgop_bool']['value'] ="{0}".format(int(bool(gop)))
|
data['bms_protection_wgop_bool']['raw_value'] = int(bool(gop))
|
||||||
|
data['bms_protection_wgop_bool']['value'] = "{0}".format(int(bool(gop)))
|
||||||
data['bms_protection_wgup_bool'] = dict()
|
data['bms_protection_wgup_bool'] = dict()
|
||||||
data['bms_protection_wgup_bool']['help'] = "Whole group undervoltage protection"
|
data['bms_protection_wgup_bool']['help'] = "Whole group undervoltage protection"
|
||||||
data['bms_protection_wgup_bool']['value'] ="{0}".format(int(bool(gup)))
|
data['bms_protection_wgup_bool']['raw_value'] = int(bool(gup))
|
||||||
|
data['bms_protection_wgup_bool']['value'] = int(bool(gup))
|
||||||
data['bms_protection_cotp_bool'] = dict()
|
data['bms_protection_cotp_bool'] = dict()
|
||||||
data['bms_protection_cotp_bool']['help'] = "Charging over-temperature protection"
|
data['bms_protection_cotp_bool']['help'] = "Charging over-temperature protection"
|
||||||
data['bms_protection_cotp_bool']['value'] ="{0}".format(int(bool(cotp)))
|
data['bms_protection_cotp_bool']['raw_value'] = \
|
||||||
|
data['bms_protection_cotp_bool']['value'] = "{0}".format(int(bool(cotp)))
|
||||||
data['bms_protection_cutp_bool'] = dict()
|
data['bms_protection_cutp_bool'] = dict()
|
||||||
data['bms_protection_cutp_bool']['help'] = "Charging under-temperature protection"
|
data['bms_protection_cutp_bool']['help'] = "Charging under-temperature protection"
|
||||||
data['bms_protection_cutp_bool']['value'] ="{0}".format(int(bool(cutp)))
|
data['bms_protection_cutp_bool']['raw_value'] = int(bool(cutp))
|
||||||
|
data['bms_protection_cutp_bool']['value'] = "{0}".format(int(bool(cutp)))
|
||||||
data['bms_protection_dotp_bool'] = dict()
|
data['bms_protection_dotp_bool'] = dict()
|
||||||
data['bms_protection_dotp_bool']['help'] = "Discharging over-temperature protection"
|
data['bms_protection_dotp_bool']['help'] = "Discharging over-temperature protection"
|
||||||
data['bms_protection_dotp_bool']['value'] ="{0}".format(int(bool(dotp)))
|
data['bms_protection_dotp_bool']['raw_value'] = int(bool(dotp))
|
||||||
|
data['bms_protection_dotp_bool']['value'] = "{0}".format(int(bool(dotp)))
|
||||||
data['bms_protection_dutp_bool'] = dict()
|
data['bms_protection_dutp_bool'] = dict()
|
||||||
data['bms_protection_dutp_bool']['help'] = "Discharging under-protection"
|
data['bms_protection_dutp_bool']['help'] = "Discharging under-protection"
|
||||||
data['bms_protection_dutp_bool']['value'] ="{0}".format(int(bool(dutp)))
|
data['bms_protection_dutp_bool']['raw_value'] = int(bool(dutp))
|
||||||
|
data['bms_protection_dutp_bool']['value'] = "{0}".format(int(bool(dutp)))
|
||||||
data['bms_protection_cocp_bool'] = dict()
|
data['bms_protection_cocp_bool'] = dict()
|
||||||
data['bms_protection_cocp_bool']['help'] = "Charging over-current protection"
|
data['bms_protection_cocp_bool']['help'] = "Charging over-current protection"
|
||||||
data['bms_protection_cocp_bool']['value'] ="{0}".format(int(bool(cocp)))
|
data['bms_protection_cocp_bool']['raw_value'] = int(bool(cocp))
|
||||||
|
data['bms_protection_cocp_bool']['value'] = "{0}".format(int(bool(cocp)))
|
||||||
data['bms_protection_docp_bool'] = dict()
|
data['bms_protection_docp_bool'] = dict()
|
||||||
data['bms_protection_docp_bool']['help'] = "Discharging over-current protection"
|
data['bms_protection_docp_bool']['help'] = "Discharging over-current protection"
|
||||||
data['bms_protection_docp_bool']['value'] ="{0}".format(int(bool(docp)))
|
data['bms_protection_docp_bool']['raw_value'] = int(bool(docp))
|
||||||
|
data['bms_protection_docp_bool']['value'] = "{0}".format(int(bool(docp)))
|
||||||
data['bms_protection_scp_bool'] = dict()
|
data['bms_protection_scp_bool'] = dict()
|
||||||
data['bms_protection_scp_bool']['help'] = "Short-circuit protection"
|
data['bms_protection_scp_bool']['help'] = "Short-circuit protection"
|
||||||
data['bms_protection_scp_bool']['value'] ="{0}".format(int(bool(scp)))
|
data['bms_protection_scp_bool']['raw_value'] = int(bool(scp))
|
||||||
|
data['bms_protection_scp_bool']['value'] = "{0}".format(int(bool(scp)))
|
||||||
data['bms_protection_fdic_bool'] = dict()
|
data['bms_protection_fdic_bool'] = dict()
|
||||||
data['bms_protection_fdic_bool']['help'] = "Front detection IC error"
|
data['bms_protection_fdic_bool']['help'] = "Front detection IC error"
|
||||||
data['bms_protection_fdic_bool']['value'] ="{0}".format(int(bool(fdic)))
|
data['bms_protection_fdic_bool']['raw_value'] = int(bool(fdic))
|
||||||
|
data['bms_protection_fdic_bool']['value'] = "{0}".format(int(bool(fdic)))
|
||||||
data['bms_protection_slmos_bool'] = dict()
|
data['bms_protection_slmos_bool'] = dict()
|
||||||
data['bms_protection_slmos_bool']['help'] = "Software lock MOS"
|
data['bms_protection_slmos_bool']['help'] = "Software lock MOS"
|
||||||
data['bms_protection_slmos_bool']['value'] ="{0}".format(int(bool(slm)))
|
data['bms_protection_slmos_bool']['raw_value'] = int(bool(slm))
|
||||||
#data["Single overvoltage protection"] = "{0}".format(bool(sop))
|
data['bms_protection_slmos_bool']['value'] = int(bool(slm))
|
||||||
#data["Single undervoltage protection"] = "{0}".format(bool(sup))
|
|
||||||
#data["Whole group overvoltage protection"] = "{0}".format(bool(gop))
|
|
||||||
#data["Whole group undervoltage protection"] = "{0}".format(bool(gup))
|
|
||||||
#data["Charging over-temperature protection"] = "{0}".format(bool(cotp))
|
|
||||||
#data["Charging under-temperature protection"] = "{0}".format(bool(cutp))
|
|
||||||
#data["Discharging over-temperature protection"] = "{0}".format(bool(dotp))
|
|
||||||
#data["Discharging under-protection"] = "{0}".format(bool(dutp))
|
|
||||||
#data["Charging over-current protection"] = "{0}".format(bool(cocp))
|
|
||||||
#data["Discharging over-current protection"] = "{0}".format(bool(docp))
|
|
||||||
#data["Short-circuit protection"] = "{0}".format(bool(scp))
|
|
||||||
#data["Front detection IC error"] = "{0}".format(bool(fdic))
|
|
||||||
#data["Software lock MOS"] = "{0}".format(bool(slm))
|
|
||||||
if debug > 2:
|
if debug > 2:
|
||||||
print("Protection state: {0}".format(protection_state))
|
print("Protection state: {0}".format(protection_state))
|
||||||
print("Single overvoltage protection: {0}".format(bool(sop)))
|
print("Single overvoltage protection: {0}".format(bool(sop)))
|
||||||
@ -379,9 +403,9 @@ def parse_03_response(response):
|
|||||||
rsoc = response[23] * 0.01
|
rsoc = response[23] * 0.01
|
||||||
data['bms_capacity_charge_ratio'] = dict()
|
data['bms_capacity_charge_ratio'] = dict()
|
||||||
data['bms_capacity_charge_ratio']['help'] = "Percent Charge"
|
data['bms_capacity_charge_ratio']['help'] = "Percent Charge"
|
||||||
|
data['bms_capacity_charge_ratio']['raw_value'] = format(rsoc)
|
||||||
data['bms_capacity_charge_ratio']['value'] = "{0}".format(rsoc)
|
data['bms_capacity_charge_ratio']['value'] = "{0}".format(rsoc)
|
||||||
data['bms_capacity_charge_ratio']['units'] = "\u2030"
|
data['bms_capacity_charge_ratio']['units'] = "\u2030"
|
||||||
#data["Percent Charge"] = "{0}".format(rsoc)
|
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
print("Capacity remaining: {0}%".format(rsoc * 100))
|
print("Capacity remaining: {0}%".format(rsoc * 100))
|
||||||
|
|
||||||
@ -389,12 +413,12 @@ def parse_03_response(response):
|
|||||||
control_status = response[24]
|
control_status = response[24]
|
||||||
data['bms_charge_is_charging'] = dict()
|
data['bms_charge_is_charging'] = dict()
|
||||||
data['bms_charge_is_charging']['help'] = "MOSFET charging"
|
data['bms_charge_is_charging']['help'] = "MOSFET charging"
|
||||||
|
data['bms_charge_is_charging']['raw_value'] = int(bool(control_status & 1))
|
||||||
data['bms_charge_is_charging']['value'] = int(bool(control_status & 1))
|
data['bms_charge_is_charging']['value'] = int(bool(control_status & 1))
|
||||||
data['bms_charge_is_discharging'] = dict()
|
data['bms_charge_is_discharging'] = dict()
|
||||||
data['bms_charge_is_discharging']['help'] = "MOSFET discharging"
|
data['bms_charge_is_discharging']['help'] = "MOSFET discharging"
|
||||||
data['bms_charge_is_discharging']['value'] = int(bool(control_status & 1))
|
data['bms_charge_is_discharging']['raw_value'] = int(bool(control_status & 1))
|
||||||
#data["Charging"] = bool(control_status & 1)
|
data['bms_charge_is_discharging']['value'] = "{0}".format(int(bool(control_status & 1)))
|
||||||
#data["Discharging"] = bool((control_status >> 1) & 1)
|
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
if (control_status & 1):
|
if (control_status & 1):
|
||||||
print("MOSFET charging: yes")
|
print("MOSFET charging: yes")
|
||||||
@ -412,17 +436,17 @@ def parse_03_response(response):
|
|||||||
temperatures.append((bytes_to_digits(response[27+(2*i)], response[28+(2*i)]) - 2731) * 0.1)
|
temperatures.append((bytes_to_digits(response[27+(2*i)], response[28+(2*i)]) - 2731) * 0.1)
|
||||||
data['bms_temperature_sensor_num'] = dict()
|
data['bms_temperature_sensor_num'] = dict()
|
||||||
data['bms_temperature_sensor_num']['help'] = "Temperature Sensors"
|
data['bms_temperature_sensor_num']['help'] = "Temperature Sensors"
|
||||||
data['bms_temperature_sensor_num']['value'] = ntc_num
|
data['bms_temperature_sensor_num']['raw_value'] = ntc_num
|
||||||
#data["Temperature Sensors"] = ntc_num
|
data['bms_temperature_sensor_num']['value'] = "{0}".format(ntc_num)
|
||||||
#data["Temperature"] = dict()
|
|
||||||
data['bms_temperature_celcius'] = dict()
|
data['bms_temperature_celcius'] = dict()
|
||||||
data['bms_temperature_celcius']['help'] = "Temperature"
|
data['bms_temperature_celcius']['help'] = "Temperature"
|
||||||
data['bms_temperature_celcius']['units'] = "\u00B0C"
|
data['bms_temperature_celcius']['units'] = "\u00B0C"
|
||||||
data['bms_temperature_celcius']['label'] = 'sensor'
|
data['bms_temperature_celcius']['label'] = 'sensor'
|
||||||
|
data['bms_temperature_celcius']['raw_values'] = dict()
|
||||||
data['bms_temperature_celcius']['values'] = dict()
|
data['bms_temperature_celcius']['values'] = dict()
|
||||||
for i, temp in enumerate(temperatures):
|
for i, temp in enumerate(temperatures):
|
||||||
|
data['bms_temperature_celcius']['raw_values'][i+1] = temp
|
||||||
data['bms_temperature_celcius']['values'][i+1] = "{:.2f}".format(temp)
|
data['bms_temperature_celcius']['values'][i+1] = "{:.2f}".format(temp)
|
||||||
#data["Temperature"][i+1] = "{:.2f}\u00B0C".format(temp)
|
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
print("Number of temperature sensors: {0}".format(ntc_num))
|
print("Number of temperature sensors: {0}".format(ntc_num))
|
||||||
for i, temp in enumerate(temperatures):
|
for i, temp in enumerate(temperatures):
|
||||||
@ -469,23 +493,24 @@ def parse_04_response(response):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if data_len > 0:
|
if data_len > 0:
|
||||||
#data["Cell Voltages"] = dict()
|
|
||||||
data['bms_voltage_cells_volts'] = dict()
|
data['bms_voltage_cells_volts'] = dict()
|
||||||
data['bms_voltage_cells_volts']['help'] = "Cell Voltages"
|
data['bms_voltage_cells_volts']['help'] = "Cell Voltages"
|
||||||
data['bms_voltage_cells_volts']['units'] = "V"
|
data['bms_voltage_cells_volts']['units'] = "V"
|
||||||
data['bms_voltage_cells_volts']['label'] = "cell"
|
data['bms_voltage_cells_volts']['label'] = "cell"
|
||||||
|
data['bms_voltage_cells_volts']['raw_values'] = dict()
|
||||||
data['bms_voltage_cells_volts']['values'] = dict()
|
data['bms_voltage_cells_volts']['values'] = dict()
|
||||||
for cell in range(int(data_len / 2)):
|
for cell in range(int(data_len / 2)):
|
||||||
first = (cell * 2) + 4
|
first = (cell * 2) + 4
|
||||||
second = (cell * 2) + 5
|
second = (cell * 2) + 5
|
||||||
cellv = bytes_to_digits(response[first], response[second]) * 0.001
|
cellv = bytes_to_digits(response[first], response[second]) * 0.001
|
||||||
|
data['bms_voltage_cells_volts']['raw_values'][cell+1] = cellv
|
||||||
data['bms_voltage_cells_volts']['values'][cell+1] = "{:.3f}".format(cellv)
|
data['bms_voltage_cells_volts']['values'][cell+1] = "{:.3f}".format(cellv)
|
||||||
#data["Cell Voltages"][i+1] = "{:.3f}V".format(cellv)
|
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
print("Cell {:.0f}: {:.3f}V".format(cell+1, cellv))
|
print("Cell {:.0f}: {:.3f}V".format(cell+1, cellv))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def collect_data(ser, debug):
|
def collect_data():
|
||||||
|
global debug
|
||||||
# Request is 7 bytes:
|
# Request is 7 bytes:
|
||||||
# \xDD for start
|
# \xDD for start
|
||||||
# \xA5 for read, \x5A for write
|
# \xA5 for read, \x5A for write
|
||||||
@ -493,7 +518,7 @@ def collect_data(ser, debug):
|
|||||||
# \x77 ends
|
# \x77 ends
|
||||||
data = dict()
|
data = dict()
|
||||||
reqmsg = bytearray([ 0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77 ])
|
reqmsg = bytearray([ 0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77 ])
|
||||||
response_03 = requestMessage(ser, reqmsg, debug)
|
response_03 = requestMessage(reqmsg)
|
||||||
|
|
||||||
if len(response_03) == 0:
|
if len(response_03) == 0:
|
||||||
if debug > 0:
|
if debug > 0:
|
||||||
@ -502,7 +527,7 @@ def collect_data(ser, debug):
|
|||||||
response_03 = bytearray(response_03)
|
response_03 = bytearray(response_03)
|
||||||
|
|
||||||
reqmsg = bytearray([ 0xDD, 0xA5, 0x04, 0x00, 0xFF, 0xFC, 0x77 ])
|
reqmsg = bytearray([ 0xDD, 0xA5, 0x04, 0x00, 0xFF, 0xFC, 0x77 ])
|
||||||
response_04 = requestMessage(ser, reqmsg, debug)
|
response_04 = requestMessage(reqmsg)
|
||||||
|
|
||||||
if len(response_04) == 0:
|
if len(response_04) == 0:
|
||||||
if debug > 0:
|
if debug > 0:
|
||||||
@ -525,10 +550,11 @@ def collect_data(ser, debug):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def main(ser, debug):
|
def main():
|
||||||
|
global debug
|
||||||
data = dict()
|
data = dict()
|
||||||
while bool(data) is False:
|
while bool(data) is False:
|
||||||
data = collect_data(ser, debug)
|
data = collect_data()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
if args.report_json:
|
if args.report_json:
|
||||||
@ -539,14 +565,15 @@ def main(ser, debug):
|
|||||||
pp.pprint(data)
|
pp.pprint(data)
|
||||||
|
|
||||||
|
|
||||||
def prometheus_export(ser, debug, daemonize=True, filename=False):
|
def prometheus_export(daemonize=True, filename=False):
|
||||||
|
global debug
|
||||||
if not can_export_prometheus:
|
if not can_export_prometheus:
|
||||||
return
|
return
|
||||||
|
|
||||||
data = dict()
|
data = dict()
|
||||||
# Initialize data structure, to fill in help values
|
# Initialize data structure, to fill in help values
|
||||||
while bool(data) is False:
|
while bool(data) is False:
|
||||||
data = collect_data(ser, debug)
|
data = collect_data()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
registry = prometheus_client.CollectorRegistry(auto_describe=True)
|
registry = prometheus_client.CollectorRegistry(auto_describe=True)
|
||||||
@ -560,21 +587,21 @@ def prometheus_export(ser, debug, daemonize=True, filename=False):
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Delay, collect new data, and start again
|
# Delay, collect new data, and start again
|
||||||
time.sleep(PROMETHEUS_UPDATE_PERIOD)
|
time.sleep(DAEMON_UPDATE_PERIOD)
|
||||||
# Reset data, so it is re-populated correctly
|
# Reset data, so it is re-populated correctly
|
||||||
data = dict()
|
data = dict()
|
||||||
while bool(data) is False:
|
while bool(data) is False:
|
||||||
data = collect_data(ser, debug)
|
data = collect_data()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
prometheus_populate_metric(metric, data)
|
prometheus_populate_metric(metric, data)
|
||||||
prometheus_client.generate_latest(registry)
|
prometheus_client.generate_latest(registry)
|
||||||
else:
|
else:
|
||||||
if not filename:
|
if not filename:
|
||||||
print("Invalid filename supplied");
|
print("Invalid filename supplied");
|
||||||
return False
|
return False
|
||||||
prometheus_client.write_to_textfile(filename, registry=registry)
|
prometheus_client.write_to_textfile(filename, registry=registry)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def prometheus_create_metric(registry, data):
|
def prometheus_create_metric(registry, data):
|
||||||
metric = dict()
|
metric = dict()
|
||||||
for name, contains in data.items():
|
for name, contains in data.items():
|
||||||
@ -613,30 +640,156 @@ def prometheus_populate_metric(metric, data):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def influxdb_export(bucket, url=None, org=None, token=None, daemonize=True):
|
||||||
|
global debug
|
||||||
|
global influxclient
|
||||||
|
|
||||||
|
if not can_export_influxdb:
|
||||||
|
return
|
||||||
|
|
||||||
|
data = dict()
|
||||||
|
# Initialize data structure, to fill in help values
|
||||||
|
while bool(data) is False:
|
||||||
|
data = collect_data()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if url:
|
||||||
|
influxclient = InfluxDBClient(url=url, token=token, org=org)
|
||||||
|
else:
|
||||||
|
influxclient = InfluxDBClient.from_env_properties()
|
||||||
|
influxdb_write_snapshot(bucket, data)
|
||||||
|
|
||||||
|
if (daemonize):
|
||||||
|
while True:
|
||||||
|
# Delay, collect new data, and start again
|
||||||
|
time.sleep(DAEMON_UPDATE_PERIOD)
|
||||||
|
# Reset data, so it is re-populated correctly
|
||||||
|
data = dict()
|
||||||
|
while bool(data) is False:
|
||||||
|
data = collect_data()
|
||||||
|
time.sleep(1)
|
||||||
|
influxdb_write_snapshot(bucket, data)
|
||||||
|
influxclient.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
def influxdb_write_snapshot(bucket,data):
|
||||||
|
global debug
|
||||||
|
global influxclient
|
||||||
|
global writeapi
|
||||||
|
writeapi = influxclient.write_api(write_options=SYNCHRONOUS)
|
||||||
|
# Populate the data structure this period
|
||||||
|
points = influxdb_create_snapshot(data)
|
||||||
|
writeapi.write(bucket=bucket, record=points)
|
||||||
|
writeapi.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
def influxdb_create_snapshot(data):
|
||||||
|
global debug
|
||||||
|
points = []
|
||||||
|
helpmsg = ''
|
||||||
|
units = ''
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||||
|
''' Note that the fieldname is set to "gauge" in order to retain
|
||||||
|
compatibility with data imported from Prometheus (and it's
|
||||||
|
as good a name as any). '''
|
||||||
|
for kind, contains in data.items():
|
||||||
|
if contains.get('help'):
|
||||||
|
helpmsg = contains.get('help')
|
||||||
|
if contains.get('units'):
|
||||||
|
units = contains.get('units')
|
||||||
|
# Simple values
|
||||||
|
if contains.get('raw_value') is not None:
|
||||||
|
value = contains.get('raw_value')
|
||||||
|
if debug > 2:
|
||||||
|
print("value: {} : {}".format(kind, value));
|
||||||
|
point = Point(kind) \
|
||||||
|
.tag("units", units) \
|
||||||
|
.tag("help", helpmsg) \
|
||||||
|
.field("gauge", value) \
|
||||||
|
.time(now)
|
||||||
|
points.append(point)
|
||||||
|
# Doesn't have a value, but multiple values, each with a label:
|
||||||
|
if contains.get('raw_values') is not None and isinstance(contains.get('raw_values'), dict):
|
||||||
|
label = contains.get('label')
|
||||||
|
for idx, label_value in contains.get('raw_values').items():
|
||||||
|
if debug > 2:
|
||||||
|
print("labels: {} [{}] : {}".format(kind, idx, label_value));
|
||||||
|
point = Point(kind) \
|
||||||
|
.tag(label, idx) \
|
||||||
|
.tag("units", units) \
|
||||||
|
.tag("help", helpmsg) \
|
||||||
|
.field("gauge", label_value) \
|
||||||
|
.time(now)
|
||||||
|
points.append(point)
|
||||||
|
# Information (like a manufacturing date or a serial number)
|
||||||
|
if contains.get('info') is not None:
|
||||||
|
value = contains.get('info')
|
||||||
|
if debug > 2:
|
||||||
|
print("info: {} : {}".format(kind, value));
|
||||||
|
point = Point(kind) \
|
||||||
|
.tag("units", units) \
|
||||||
|
.tag("help", helpmsg) \
|
||||||
|
.field("gauge", value) \
|
||||||
|
.time(now)
|
||||||
|
points.append(point)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
debug = 0
|
||||||
|
atexit.register(cleanup)
|
||||||
|
atexit.register(shutdown)
|
||||||
try:
|
try:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Query JBD BMS and report status',
|
description='Query JBD BMS and report status',
|
||||||
add_help=True,
|
add_help=True,
|
||||||
)
|
)
|
||||||
parser.add_argument('--json', '-j', dest='report_json', action='store_true',
|
parser.add_argument('--json', '-j', dest='report_json', action='store_true',
|
||||||
default=False, help='Report data as JSON')
|
default=False, help='Report data as JSON')
|
||||||
parser.add_argument('--prometheus', '-p', dest='report_prometheus', action='store_true',
|
parser.add_argument('--prometheus', '-p', dest='report_prometheus', action='store_true',
|
||||||
default=False, help='Daemonize and report data to Prometheus')
|
default=False, help='Daemonize and report data to Prometheus')
|
||||||
parser.add_argument('--textfile', '-t', dest='report_textfile', type=str, action='store',
|
parser.add_argument('--file', '-f', dest='report_textfile', type=str, action='store',
|
||||||
default=False, help='Report data to Prometheus using textfile <file>')
|
default=False, help='Report data to Prometheus using textfile <file>')
|
||||||
|
parser.add_argument('--influxdb', '-i', dest='report_influxdb', action='store_true',
|
||||||
|
default=False, help='Daemonize and report data to InfluxDB using INFLUXDB_V2_URL, INFLUXDB_V2_ORG and INFLUXDB_V2_TOKEN environment variables')
|
||||||
|
parser.add_argument('--bucket', '-b', dest='influx_bucket', type=str, action='store',
|
||||||
|
default="ups", help='Set the bucket name when sending data to influxdb (defaults to "ups")')
|
||||||
|
parser.add_argument('--url', '-u', dest='influx_url', type=str, action='store',
|
||||||
|
default=False, help='Set the URL when sending data to influxdb (overrides INFLUXDB environment variables)')
|
||||||
|
parser.add_argument('--org', '-o', dest='influx_org', type=str, action='store',
|
||||||
|
default=False, help='Set the influx organization when sending data to influxdb (overrides INFLUXDB environment variables)')
|
||||||
|
parser.add_argument('--token', '-t', dest='influx_token', type=str, action='store',
|
||||||
|
default=False, help='Set the influx token when sending data to influxdb (overrides INFLUXDB environment variables)')
|
||||||
parser.add_argument('--print', dest='report_print', action='store_true',
|
parser.add_argument('--print', dest='report_print', action='store_true',
|
||||||
default=True, help='Report data as text')
|
default=True, help='Report data as text')
|
||||||
parser.add_argument('--verbose', '-v', action='count',
|
parser.add_argument('--verbose', '-v', action='count',
|
||||||
default=0, help='Print more verbose information (can be specified multiple times)')
|
default=0, help='Print more verbose information (can be specified multiple times)')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
debug=args.verbose
|
debug=args.verbose
|
||||||
|
|
||||||
|
if args.report_influxdb:
|
||||||
|
num_args = 0
|
||||||
|
for arg in [ args.influx_url, args.influx_org, args.influx_token ]:
|
||||||
|
if arg is not False:
|
||||||
|
num_args += 1
|
||||||
|
if num_args != 0 and num_args != 3:
|
||||||
|
raise argparse.ArgumentTypeError('Missing value for --url, --org or --token')
|
||||||
|
|
||||||
if args.report_prometheus:
|
if args.report_prometheus:
|
||||||
prometheus_export(ser, debug, daemonize=True)
|
prometheus_export(daemonize=True)
|
||||||
|
if args.report_influxdb:
|
||||||
|
influxdb_export(bucket=args.influx_bucket, \
|
||||||
|
url=args.influx_url, \
|
||||||
|
org=args.influx_org, \
|
||||||
|
token=args.influx_token, \
|
||||||
|
daemonize=True)
|
||||||
elif args.report_textfile:
|
elif args.report_textfile:
|
||||||
prometheus_export(ser, debug, daemonize=False, filename=args.report_textfile)
|
prometheus_export(daemonize=False, filename=args.report_textfile)
|
||||||
else:
|
else:
|
||||||
main(ser, debug)
|
main()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
cleanup(ser, debug)
|
cleanup()
|
||||||
|
Loading…
Reference in New Issue
Block a user