#!/usr/bin/env python3 # # Communicate with a JBD/SZLLT BMS and return basic information # in order to shutdown equipment when voltage levels drop or remaining # capacity gets low # TODO: scripts to shutdown NAS when voltage goes below 13.xV or # percent_capacity goes below 25% # import argparse import atexit import json import pprint import serial import serial.rs485 import signal import sys import time try: import prometheus_client can_export_prometheus = True except: 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 SERIALPORT = "/dev/ttyUSB0" #SERIALPORT = "/dev/rfcomm1" BAUDRATE = 9600 # usb 1-1.4: new full-speed USB device number 4 using xhci_hcd # usb 1-1.4: New USB device found, idVendor=0403, idProduct=6001, bcdDevice= 6.00 # usb 1-1.4: New USB device strings: Mfr=1, Product=2, SerialNumber=3 # usb 1-1.4: Product: FT232R USB UART # usb 1-1.4: Manufacturer: FTDI # usb 1-1.4: SerialNumber: AQ00QFHZ # usbcore: registered new interface driver usbserial_generic # usbserial: USB Serial support registered for generic # usbcore: registered new interface driver ftdi_sio # usbserial: USB Serial support registered for FTDI USB Serial Device # ftdi_sio 1-1.4:1.0: FTDI USB Serial Device converter detected # usb 1-1.4: Detected FT232RL # usb 1-1.4: FTDI USB Serial Device converter now attached to ttyUSB0 # usb 1-1.4: usbfs: interface 0 claimed by ftdi_sio while 'python3' sets config #1 # TODO: ensure ttyUSB0 points to idVendor=0403, idProduct=6001 # with serial.tools.list_ports.ListPortInfo # python3 -m serial.tools.list_ports USB ser = serial.Serial(SERIALPORT, BAUDRATE) ser.parity = serial.PARITY_NONE # set parity check: no parity ser.bytesize = serial.EIGHTBITS # number of bits per bytes ser.stopbits = serial.STOPBITS_ONE # number of stop bits ser.timeout = 1 # timeout block read ser.writeTimeout = 1 # timeout for write ''' Clean up serial port ''' def cleanup(): global debug global ser if debug > 2: print("Cleaning up...") if ser.is_open: ser.reset_input_buffer() # flush input buffer, discarding all its contents ser.reset_output_buffer() # flush output buffer, aborting current output 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): checksum = '' return checksum def verify_checksum(data, checksum): # (data + length + command code) checksum, then complement, then add 1, high bit first, low bit last # data should have start/rw stripped s = 0 for i in data: s += i s = (s ^ 0xFFFF) + 1 chk = bytes_to_digits(checksum[0], checksum[1]) return s == chk def convert_to_signed(x): # For values below 1024, these seem to be actual results # For values above 1024, these seem to be encoded to account for high and negative floats max_uint = 1024 if x >= max_uint: return (x - 2**9) % 2**10 - 2**9 else: return x def bytes_to_digits(high, low): result = high result <<= 8 result = result | low return result def bytes_to_date(high, low): result= bytes_to_digits(high, low) day = result & 0x1f mon = (result >> 5) & 0x0f year = 2000 + (result >> 9) return "{:04d}-{:02d}-{:02d}".format(year, mon, day) # takes a serial object and a message, returns a response def requestMessage(reqmsg): global ser global debug if debug > 2: print('Starting Up Serial Monitor') if ser.is_open: ser.close() try: ser.open() except Exception as e: print("error open serial port: {0}".format(str(e))) return False if ser.is_open: try: # Resetting once alone doesn't seem to do the trick when we discarded data # on a previous run for i in range(2): ser.reset_input_buffer() # flush input buffer, discarding all its contents ser.reset_output_buffer() # flush output buffer, aborting current output if debug > 0: print("Write data: {0}".format("".join('0x{:02x} '.format(x) for x in reqmsg))) w = ser.write(reqmsg) if debug > 2: print("Bytes written: {0}".format(w)) #time.sleep(1) if w != len(reqmsg): print("ERROR: {0} bytes written, {1} expected.".format(w, len(reqmsg))) return False wait_time = 0 while ser.in_waiting == 0: # Return an empty string if we end up waiting too long if wait_time > 2: cleanup() return '' if debug > 2: print("Waiting for data...") time.sleep(0.5) wait_time += 1 if debug > 1: print("Awaiting reading: {0}".format(ser.in_waiting)) response = ser.read_until(b'\x77') # Return an empty string if the read timed out or returned nothing if len(response) == 0: return '' if debug > 0: print("Read data: {0}".format(response.hex())) cleanup() return response except Exception as e: print("error communicating...: {0}".function(str(e))) else: print("cannot open serial port") def parse_03_response(response): global debug data = dict() # Response is 34 bytes: # 00 begin: \xDD # 01 r/w: \xA5 # 02 status: \x00 = correct; \x80 = incorrect # 03 length (usually 27) # 04 data (size of length) # ... # length+4 checksum # length+5 checksum # length+6 end \x77 if bytes([response[0]]) != b'\xdd': print("ERROR: first byte not found: {0}".format(response[0])) return False if bytes([response[2]]) == b'\x80': print("ERROR: error byte returned from BMS: {0}".format(response[2])) return False data_len = response[3] if debug > 2: print("Data length (trimming 4 bytes): {0}".format(data_len)) # The checksum is two bytes, offset by data_len + 4 # Five bytes at the front of data # begin; rw; status, command; length # The checksum should check command, length, and data: [3] to [3+data_len+1] first = data_len + 4 second = data_len + 5 if second > len(response): print("ERROR: primary response checksum not found") return False; checksum = bytes([response[first], response[second]]) if not verify_checksum(response[3:first], checksum): print("ERROR: failed to validate received checksum") return False if data_len > 0: vtot = bytes_to_digits(response[4], response[5]) * 0.01 data['bms_voltage_total_volts'] = dict() 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']['units'] = "V" if debug > 1: print("Total voltage: {:.2f}V".format(vtot)) current = bytes_to_digits(response[6], response[7]) current = convert_to_signed(current) * 0.01 data["bms_current_amps"] = dict() 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"]['units'] = "A" if debug > 1: print("Current: {:.2f}A".format(current)) res_cap = bytes_to_digits(response[8], response[9]) * 0.01 nom_cap = bytes_to_digits(response[10], response[11]) * 0.01 data['bms_capacity_remaining_ah'] = dict() 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']['units'] = "Ah" data['bms_capacity_nominal_ah'] = dict() 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']['units'] = "Ah" if debug > 1: print("Remaining capacity: {:.2f}Ah".format(res_cap)) print("Nominal capacity: {:.2f}Ah".format(nom_cap)) cycle_times = bytes_to_digits(response[12], response[13]) data['bms_charge_cycles'] = dict() data['bms_charge_cycles']['help'] = "Charge Cycles" data['bms_charge_cycles']['raw_value'] = cycle_times data['bms_charge_cycles']['value'] = "{0}".format(cycle_times) if debug > 1: print("Cycle times: {0}".format(cycle_times)) man_date = bytes_to_date(response[14], response[15]) data['bms_manufacture_date'] = dict() data['bms_manufacture_date']['help'] = "Date of Manufacture" data['bms_manufacture_date']['info'] = "{0}".format(man_date) if debug > 1: print("Manufacturing date: {0}".format(man_date)) cells = response[25] # 4S data['bms_cell_number'] = dict() data['bms_cell_number']['help'] = "Cells" data['bms_cell_number']['raw_value'] = cells data['bms_cell_number']['value'] = "{0}".format(cells) if debug > 1: print("Cells: {0}S".format(cells)) balance_state_high = bytes_to_digits(response[16], response[17]) # 1S to 16S balance_state_low = bytes_to_digits(response[18], response[19]) # 17S to 32S # 1 bit per 4S (2 bytes = 16S); in 4S, we should expect: # 0x0 (no cells balancing) 0 # 0x1 (cell 1 balancing) 1 # 0x2 (cell 2 balancing) 2 # 0x3 (cells 1 + 2 balancing) 3 # 0x4 (cell 3 balancing) 4 # 0x5 (cells 1 + 3 balancing) 5 # 0x6 (cells 2 + 3 balancing) 6 # 0x7 (cells 1 + 2 + 3 balancing) 7 # 0x8 (cell 4 balancing) 8 # 0x9 (cells 1 + 4 balancing) 9 # 0xA (cells 2 + 4 balancing) 10 # 0xB (cells 1 + 2 + 4 balancing) 11 # 0xC (cells 3 + 4 balancing) 12 # 0xD (cells 1 + 3 + 4 balancing) 13 # 0xE (cells 2 + 3 + 4 balancing) 14 # 0xF (cells 1 + 2 + 3 + 4 balancing) 15 #data["Balancing"] = dict() data['bms_cells_balancing'] = dict() data['bms_cells_balancing']['help'] = "Cells balancing" data['bms_cells_balancing']['label'] = 'cell' data['bms_cells_balancing']['raw_values'] = dict() data['bms_cells_balancing']['values'] = dict() for cell in range(cells): # Cells from 1 to 16 are recorded in balance_state_low, # and from 17 to 32 in balance_state_high; hilo_cell records the offset # relative to the state group if cell >= 16: state = balance_state_high hilo_cell = cell - 16 else: state = balance_state_low hilo_cell = cell # Cells are recorded as groups of 4 bits (0x0-0xF) per 4 cells g = int(hilo_cell / 4) b = 2**(hilo_cell - (g * 4 )) data['bms_cells_balancing']['raw_values'][cell+1] = bool((state >> g) & b) data['bms_cells_balancing']['values'][cell+1] = "{0}".format(int(bool((state >> g) & b))) if debug > 1: print("Balancing cell {0}: {1}".format(cell, bool((state >> g & b)))) protection_state = bytes_to_digits(response[20], response[21]) sop = protection_state & 1 sup = protection_state & 2 gop = protection_state & 4 gup = protection_state & 8 cotp = protection_state & 16 cutp = protection_state & 32 dotp = protection_state & 64 dutp = protection_state & 128 cocp = protection_state & 256 docp = protection_state & 512 scp = protection_state & 1024 fdic = protection_state & 2048 slm = protection_state & 4096 data['bms_protection_sop_bool'] = dict() data['bms_protection_sop_bool']['help'] = "Single overvoltage protection" data['bms_protection_sop_bool']['raw_value'] = bool(sop) data['bms_protection_sop_bool']['value'] = "{0}".format(int(bool(sop))) data['bms_protection_sup_bool'] = dict() data['bms_protection_sup_bool']['help'] = "Single undervoltage protection" data['bms_protection_sup_bool']['raw_value'] = bool(sup) data['bms_protection_sup_bool']['value'] = "{0}".format(int(bool(sup))) data['bms_protection_wgop_bool'] = dict() data['bms_protection_wgop_bool']['help'] = "Whole group overvoltage protection" data['bms_protection_wgop_bool']['raw_value'] = bool(gop) data['bms_protection_wgop_bool']['value'] = "{0}".format(int(bool(gop))) data['bms_protection_wgup_bool'] = dict() data['bms_protection_wgup_bool']['help'] = "Whole group undervoltage protection" data['bms_protection_wgup_bool']['raw_value'] = bool(gup) data['bms_protection_wgup_bool']['value'] = "{0}".format(int(bool(gup))) data['bms_protection_cotp_bool'] = dict() data['bms_protection_cotp_bool']['help'] = "Charging over-temperature protection" data['bms_protection_cotp_bool']['raw_value'] = bool(cotp) data['bms_protection_cotp_bool']['value'] = "{0}".format(int(bool(cotp))) data['bms_protection_cutp_bool'] = dict() data['bms_protection_cutp_bool']['help'] = "Charging under-temperature protection" data['bms_protection_cutp_bool']['raw_value'] = bool(cutp) data['bms_protection_cutp_bool']['value'] = "{0}".format(int(bool(cutp))) data['bms_protection_dotp_bool'] = dict() data['bms_protection_dotp_bool']['help'] = "Discharging over-temperature protection" data['bms_protection_dotp_bool']['raw_value'] = bool(dotp) data['bms_protection_dotp_bool']['value'] = "{0}".format(int(bool(dotp))) data['bms_protection_dutp_bool'] = dict() data['bms_protection_dutp_bool']['help'] = "Discharging under-protection" data['bms_protection_dutp_bool']['raw_value'] = bool(dutp) data['bms_protection_dutp_bool']['value'] = "{0}".format(int(bool(dutp))) data['bms_protection_cocp_bool'] = dict() data['bms_protection_cocp_bool']['help'] = "Charging over-current protection" data['bms_protection_cocp_bool']['raw_value'] = bool(cocp) data['bms_protection_cocp_bool']['value'] = "{0}".format(int(bool(cocp))) data['bms_protection_docp_bool'] = dict() data['bms_protection_docp_bool']['help'] = "Discharging over-current protection" data['bms_protection_docp_bool']['raw_value'] = bool(docp) data['bms_protection_docp_bool']['value'] = "{0}".format(int(bool(docp))) data['bms_protection_scp_bool'] = dict() data['bms_protection_scp_bool']['help'] = "Short-circuit protection" data['bms_protection_scp_bool']['raw_value'] = bool(scp) data['bms_protection_scp_bool']['value'] = "{0}".format(int(bool(scp))) data['bms_protection_fdic_bool'] = dict() data['bms_protection_fdic_bool']['help'] = "Front detection IC error" data['bms_protection_fdic_bool']['raw_value'] = bool(fdic) data['bms_protection_fdic_bool']['value'] = "{0}".format(int(bool(fdic))) data['bms_protection_slmos_bool'] = dict() data['bms_protection_slmos_bool']['help'] = "Software lock MOS" data['bms_protection_slmos_bool']['raw_value'] = bool(slm) data['bms_protection_slmos_bool']['value'] = "{0}".format(int(bool(slm))) if debug > 2: print("Protection state: {0}".format(protection_state)) print("Single overvoltage protection: {0}".format(bool(sop))) print("Single undervoltage protection: {0}".format(bool(sup))) print("Whole group overvoltage protection: {0}".format(bool(gop))) print("Whole group undervoltage protection: {0}".format(bool(gup))) print("Charging over-temperature protection: {0}".format(bool(cotp))) print("Charging under-temperature protection: {0}".format(bool(cutp))) print("Discharging over-temperature protection: {0}".format(bool(dotp))) print("Discharging under-protection: {0}".format(bool(dutp))) print("Charging over-current protection: {0}".format(bool(cocp))) print("Discharging over-current protection: {0}".format(bool(docp))) print("Short-circuit protection: {0}".format(bool(scp))) print("Front detection IC error: {0}".format(bool(fdic))) print("Software lock MOS: {0}".format(bool(slm))) software_version = bytes([response[22]]) # percent of capacity remaining, converted to a per mille ratio between 0 and 1 rsoc = response[23] * 0.01 data['bms_capacity_charge_ratio'] = dict() data['bms_capacity_charge_ratio']['help'] = "Percent Charge" data['bms_capacity_charge_ratio']['raw_value'] = rsoc data['bms_capacity_charge_ratio']['value'] = "{0}".format(rsoc) data['bms_capacity_charge_ratio']['units'] = "\u2030" if debug > 1: print("Capacity remaining: {0}%".format(rsoc * 100)) # bit0 = charging; bit1 = discharging; 0 = MOS closing; 1 = MOS opening control_status = response[24] data['bms_charge_is_charging'] = dict() data['bms_charge_is_charging']['help'] = "MOSFET charging" data['bms_charge_is_charging']['raw_value'] = bool(control_status & 1) data['bms_charge_is_charging']['value'] = "{0}".format(int(bool(control_status & 1))) data['bms_charge_is_discharging'] = dict() data['bms_charge_is_discharging']['help'] = "MOSFET discharging" data['bms_charge_is_discharging']['raw_value'] = bool(control_status & 1) data['bms_charge_is_discharging']['value'] = "{0}".format(int(bool(control_status & 1))) if debug > 1: if (control_status & 1): print("MOSFET charging: yes") else: print("MOSFET charging: no") if ((control_status >> 1) & 1): print("MOSFET discharging: yes") else: print("MOSFET discharging: no") ntc_num = response[26] # number of temperature sensors ntc_content = bytearray() # 2 * ntc_num in size temperatures = list() for i in range(ntc_num): 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']['help'] = "Temperature Sensors" data['bms_temperature_sensor_num']['raw_value'] = ntc_num data['bms_temperature_sensor_num']['value'] = "{0}".format(ntc_num) data['bms_temperature_celcius'] = dict() data['bms_temperature_celcius']['help'] = "Temperature" data['bms_temperature_celcius']['units'] = "\u00B0C" data['bms_temperature_celcius']['label'] = 'sensor' data['bms_temperature_celcius']['raw_values'] = dict() data['bms_temperature_celcius']['values'] = dict() 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) if debug > 1: print("Number of temperature sensors: {0}".format(ntc_num)) for i, temp in enumerate(temperatures): print(u"Temperature sensor {:d}: {:.2f}\u00B0C".format(i+1, temp)) return data def parse_04_response(response): data = dict() # Response is 7 + cells * 2 bytes: # 00 begin: \xDD # 01 r/w: \xA5 # 02 status: \x00 = correct; \x80 = incorrect # 03 length (usually 8) # 04 data (size of length) # ... # length+4 checksum # length+5 checksum # length+6 end \x77 if bytes([response[0]]) != b'\xdd': print("ERROR: first byte not found: {0}".format(response[0])) return False if bytes([response[2]]) == b'\x80': print("ERROR: error byte returned from BMS: {0}".format(response[2])) return False data_len = response[3] if debug > 2: print("Data length (trimming 4 bytes): {0}".format(data_len)) # The checksum is two bytes, offset by data_len + 4 # Five bytes at the front of data # begin; rw; status, command; length # The checksum should check command, length, and data: [3] to [3+data_len+1] first = data_len + 4 second = data_len + 5 if second > len(response): print("ERROR: cell voltage checksum not found") return False checksum = bytes([response[first], response[second]]) if not verify_checksum(response[3:first], checksum): print("ERROR: failed to validate received checksum") return False if data_len > 0: data['bms_voltage_cells_volts'] = dict() data['bms_voltage_cells_volts']['help'] = "Cell Voltages" data['bms_voltage_cells_volts']['units'] = "V" data['bms_voltage_cells_volts']['label'] = "cell" data['bms_voltage_cells_volts']['raw_values'] = dict() data['bms_voltage_cells_volts']['values'] = dict() for cell in range(int(data_len / 2)): first = (cell * 2) + 4 second = (cell * 2) + 5 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) if debug > 1: print("Cell {:.0f}: {:.3f}V".format(cell+1, cellv)) return data def collect_data(): global debug # Request is 7 bytes: # \xDD for start # \xA5 for read, \x5A for write # \x03 for regular info; \x04 for individual voltages # \x77 ends data = dict() reqmsg = bytearray([ 0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77 ]) response_03 = requestMessage(reqmsg) if len(response_03) == 0: if debug > 0: print("Error retrieving BMS info. Trying again...") return False response_03 = bytearray(response_03) reqmsg = bytearray([ 0xDD, 0xA5, 0x04, 0x00, 0xFF, 0xFC, 0x77 ]) response_04 = requestMessage(reqmsg) if len(response_04) == 0: if debug > 0: print("Error retrieving BMS info. Trying again...") return False response_04 = bytearray(response_04) parsed_03 = parse_03_response(response_03) if parsed_03 is not False: data.update(parsed_03) else: return False parsed_04 = parse_04_response(response_04) if parsed_04 is not False: data.update(parsed_04) else: return False return data def main(): global debug data = dict() while bool(data) is False: data = collect_data() time.sleep(1) if args.report_json: print(json.dumps(data)) elif args.report_print: pp = pprint.PrettyPrinter(indent=4) pp.pprint(data) def prometheus_export(daemonize=True, filename=None): global debug if not can_export_prometheus: return data = dict() # Initialize data structure, to fill in help values while bool(data) is False: data = collect_data() time.sleep(1) registry = prometheus_client.CollectorRegistry(auto_describe=True) # Set up the metric data structure for Prometheus metric = prometheus_create_metric(registry, data) # Populate the metric data structure this period prometheus_populate_metric(metric, data) if daemonize: prometheus_client.start_http_server(9999, registry=registry) 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) prometheus_populate_metric(metric, data) prometheus_client.generate_latest(registry) else: if filename is None: print("Invalid filename supplied"); return False prometheus_client.write_to_textfile(filename, registry=registry) return True def prometheus_create_metric(registry, data): metric = dict() for name, contains in data.items(): helpmsg = '' if contains.get('help') is not None: helpmsg = contains.get('help') if contains.get('units'): helpmsg += ' (' + contains.get('units') + ')' if contains.get('value') is not None: metric[name] = prometheus_client.Gauge(name, helpmsg, registry=registry) # Has multiple values, each a different label elif contains.get('values') is not None: if contains.get('label') is None: print("ERROR: no label for {0} specified".format(name)) label = contains.get('label') metric[name] = prometheus_client.Gauge(name, helpmsg, [label], registry=registry) elif contains.get('info') is not None: metric[name] = prometheus_client.Info(name, helpmsg, registry=registry) else: pass return metric def prometheus_populate_metric(metric, data): for name, contains in data.items(): if contains.get('value') is not None: value = contains.get('value') metric[name].set(value) # doesn't have a value, but has [1-4]: if contains.get('values') is not None and isinstance(contains.get('values'), dict): for idx, label_value in contains.get('values').items(): metric[name].labels(idx).set(label_value) if contains.get('info'): value = contains.get('info') metric[name].info({name: value}) else: 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__': debug = 0 atexit.register(cleanup) atexit.register(shutdown) try: parser = argparse.ArgumentParser( description='Query JBD BMS and report status', add_help=True, ) parser.add_argument('--json', '-j', dest='report_json', action='store_true', default=False, help='Report data as JSON') parser.add_argument('--prometheus', '-p', dest='report_prometheus', action='store_true', default=False, help='Daemonize and report data to Prometheus') parser.add_argument('--file', '-f', dest='report_textfile', type=str, action='store', default=False, help='Report data to Prometheus using textfile ') 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', default=True, help='Report data as text') parser.add_argument('--verbose', '-v', action='count', default=0, help='Print more verbose information (can be specified multiple times)') args = parser.parse_args() 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: 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: prometheus_export(daemonize=False, filename=args.report_textfile) else: main() except KeyboardInterrupt: cleanup()