import smbus import time import argparse import sys import logging import subprocess import shlex try: from gpiozero import PWMLED GPIOZERO = True except ImportError as e: print('Can not import gpiozero module.') GPIOZERO = False # Config Register (R/W) _REG_CONFIG = 0x00 # SHUNT VOLTAGE REGISTER (R) _REG_SHUNTVOLTAGE = 0x01 # BUS VOLTAGE REGISTER (R) _REG_BUSVOLTAGE = 0x02 # POWER REGISTER (R) _REG_POWER = 0x03 # CURRENT REGISTER (R) _REG_CURRENT = 0x04 # CALIBRATION REGISTER (R/W) _REG_CALIBRATION = 0x05 class BusVoltageRange: """Constants for ``bus_voltage_range``""" RANGE_16V = 0x00 # set bus voltage range to 16V RANGE_32V = 0x01 # set bus voltage range to 32V (default) class Gain: """Constants for ``gain``""" DIV_1_40MV = 0x00 # shunt prog. gain set to 1, 40 mV range DIV_2_80MV = 0x01 # shunt prog. gain set to /2, 80 mV range DIV_4_160MV = 0x02 # shunt prog. gain set to /4, 160 mV range DIV_8_320MV = 0x03 # shunt prog. gain set to /8, 320 mV range class ADCResolution: """Constants for ``bus_adc_resolution`` or ``shunt_adc_resolution``""" ADCRES_9BIT_1S = 0x00 # 9bit, 1 sample, 84us ADCRES_10BIT_1S = 0x01 # 10bit, 1 sample, 148us ADCRES_11BIT_1S = 0x02 # 11 bit, 1 sample, 276us ADCRES_12BIT_1S = 0x03 # 12 bit, 1 sample, 532us ADCRES_12BIT_2S = 0x09 # 12 bit, 2 samples, 1.06ms ADCRES_12BIT_4S = 0x0A # 12 bit, 4 samples, 2.13ms ADCRES_12BIT_8S = 0x0B # 12bit, 8 samples, 4.26ms ADCRES_12BIT_16S = 0x0C # 12bit, 16 samples, 8.51ms ADCRES_12BIT_32S = 0x0D # 12bit, 32 samples, 17.02ms ADCRES_12BIT_64S = 0x0E # 12bit, 64 samples, 34.05ms ADCRES_12BIT_128S = 0x0F # 12bit, 128 samples, 68.10ms class Mode: """Constants for ``mode``""" POWERDOW = 0x00 # power down SVOLT_TRIGGERED = 0x01 # shunt voltage triggered BVOLT_TRIGGERED = 0x02 # bus voltage triggered SANDBVOLT_TRIGGERED = 0x03 # shunt and bus voltage triggered ADCOFF = 0x04 # ADC off SVOLT_CONTINUOUS = 0x05 # shunt voltage continuous BVOLT_CONTINUOUS = 0x06 # bus voltage continuous SANDBVOLT_CONTINUOUS = 0x07 # shunt and bus voltage continuous class INA219: def __init__(self, i2c_bus=1, addr=0x40): self.bus = smbus.SMBus(i2c_bus); self.addr = addr # Set chip to known config values to start self._cal_value = 0 self._current_lsb = 0 self._power_lsb = 0 self.set_calibration_32V_2A() def read(self, address): data = self.bus.read_i2c_block_data(self.addr, address, 2) return ((data[0] * 256) + data[1]) def write(self, address, data): temp = [0, 0] temp[1] = data & 0xFF temp[0] = (data & 0xFF00) >> 8 self.bus.write_i2c_block_data(self.addr, address, temp) def set_calibration_32V_2A(self): """Configures to INA219 to be able to measure up to 32V and 2A of current. Counter overflow occurs at 3.2A. ..note :: These calculations assume a 0.1 shunt ohm resistor is present """ # By default we use a pretty huge range for the input voltage, # which probably isn't the most appropriate choice for system # that don't use a lot of power. But all of the calculations # are shown below if you want to change the settings. You will # also need to change any relevant register settings, such as # setting the VBUS_MAX to 16V instead of 32V, etc. # VBUS_MAX = 32V (Assumes 32V, can also be set to 16V) # VSHUNT_MAX = 0.32 (Assumes Gain 8, 320mV, can also be 0.16, 0.08, 0.04) # RSHUNT = 0.1 (Resistor value in ohms) # 1. Determine max possible current # MaxPossible_I = VSHUNT_MAX / RSHUNT # MaxPossible_I = 3.2A # 2. Determine max expected current # MaxExpected_I = 2.0A # 3. Calculate possible range of LSBs (Min = 15-bit, Max = 12-bit) # MinimumLSB = MaxExpected_I/32767 # MinimumLSB = 0.000061 (61uA per bit) # MaximumLSB = MaxExpected_I/4096 # MaximumLSB = 0,000488 (488uA per bit) # 4. Choose an LSB between the min and max values # (Preferrably a roundish number close to MinLSB) # CurrentLSB = 0.0001 (100uA per bit) self._current_lsb = .1 # Current LSB = 100uA per bit # 5. Compute the calibration register # Cal = trunc (0.04096 / (Current_LSB * RSHUNT)) # Cal = 4096 (0x1000) self._cal_value = 4096 # 6. Calculate the power LSB # PowerLSB = 20 * CurrentLSB # PowerLSB = 0.002 (2mW per bit) self._power_lsb = .002 # Power LSB = 2mW per bit # 7. Compute the maximum current and shunt voltage values before overflow # # Max_Current = Current_LSB * 32767 # Max_Current = 3.2767A before overflow # # If Max_Current > Max_Possible_I then # Max_Current_Before_Overflow = MaxPossible_I # Else # Max_Current_Before_Overflow = Max_Current # End If # # Max_ShuntVoltage = Max_Current_Before_Overflow * RSHUNT # Max_ShuntVoltage = 0.32V # # If Max_ShuntVoltage >= VSHUNT_MAX # Max_ShuntVoltage_Before_Overflow = VSHUNT_MAX # Else # Max_ShuntVoltage_Before_Overflow = Max_ShuntVoltage # End If # 8. Compute the Maximum Power # MaximumPower = Max_Current_Before_Overflow * VBUS_MAX # MaximumPower = 3.2 * 32V # MaximumPower = 102.4W # Set Calibration register to 'Cal' calculated above self.write(_REG_CALIBRATION,self._cal_value) # Set Config register to take into account the settings above self.bus_voltage_range = BusVoltageRange.RANGE_32V self.gain = Gain.DIV_8_320MV self.bus_adc_resolution = ADCResolution.ADCRES_12BIT_32S self.shunt_adc_resolution = ADCResolution.ADCRES_12BIT_32S self.mode = Mode.SANDBVOLT_CONTINUOUS self.config = self.bus_voltage_range << 13 | \ self.gain << 11 | \ self.bus_adc_resolution << 7 | \ self.shunt_adc_resolution << 3 | \ self.mode self.write(_REG_CONFIG, self.config) def getShuntVoltage_mV(self): self.write(_REG_CALIBRATION, self._cal_value) value = self.read(_REG_SHUNTVOLTAGE) if value > 32767: value -= 65535 return value * 0.01 def getBusVoltage_V(self): self.write(_REG_CALIBRATION, self._cal_value) self.read(_REG_BUSVOLTAGE) return (self.read(_REG_BUSVOLTAGE) >> 3) * 0.004 def getCurrent_mA(self): value = self.read(_REG_CURRENT) if value > 32767: value -= 65535 return value * self._current_lsb def getPower_W(self): self.write(_REG_CALIBRATION, self._cal_value) value = self.read(_REG_POWER) if value > 32767: value -= 65535 return value * self._power_lsb def main(): """ general main function """ parser = argparse.ArgumentParser(description='UPS HAT Monitoring Daemon') parser.add_argument('-d', '--debug', action='store_true', help='log all messages to console') parser.add_argument('-s', '--statistics', action='store_true', help='show more statistics') parser.add_argument('-i', '--interval', default=5, help='check interval') parser.add_argument('-m', '--minimum', help='the minimum battery level in percent for the poweroff hook', default=20) parser.add_argument('-p', '--poweroff', help='command for the poweroff hook') parser.add_argument('-l', '--led', help='GPIO Pin Number for use with a LED') args = parser.parse_args() if args.debug: print(args) ## # set log level if args.debug or args.statistics: logging.basicConfig(level=logging.DEBUG) # run the loop mainloop(args) sys.exit(1) def mainloop(args): # Create an INA219 instance. ina219 = INA219(addr=0x42) myled = PWMLED(args.led) while True: # voltage on V- (load side) bus_voltage = ina219.getBusVoltage_V() # voltage between V+ and V- across the shunt # shunt_voltage = ina219.getShuntVoltage_mV() / 1000 # current in mA current = ina219.getCurrent_mA() # power in W power = ina219.getPower_W() p = (bus_voltage - 6)/2.4*100 if(p > 100): p = 100 if(p < 0): p = 0 # INA219 measure bus voltage on the load side. So PSU # voltage = bus_voltage + shunt_voltage # print("PSU Voltage: {:6.3f} V".format(bus_voltage + shunt_voltage)) # print("Shunt Voltage: {:9.6f} V".format(shunt_voltage)) if args.statistics: log_stats(bus_voltage, current, power, p) # check the state and trigger hooks check_state(current, p, args.minimum, args.poweroff, myled) # switch_led(myled) time.sleep(int(args.interval)) def log_stats(bus_voltage, current, power, percent): """ print or log statistics """ logging.debug("Load Voltage: {:6.3f} V".format(bus_voltage)) logging.debug("Current: {:9.6f} A".format(current/1000)) logging.debug("Power: {:6.3f} W".format(power)) logging.debug("Percent: {:3.1f}%".format(percent)) def check_state(current, percent, minimum, poweroff_cmd, myled): """ if current is negative and percent lower than args.minimum run the poweroff hook command if any is set :param current: in ampere :param percent: battery level """ # run only current is negative if current < 0: switch_led(myled, 'unload') if percent < float(minimum): logging.warning(" Minimum battery level ({:3.1f}%) is reached" .format(percent)) logging.warning(" try to run poweroff hook cmd {}" .format(poweroff_cmd)) run_hook(poweroff_cmd) else: logging.info("battery level ({:3.1f}%) is okay: do nothing!". format(percent)) else: switch_led(myled) def switch_led(myled, mode='load'): if GPIOZERO: if mode == 'load': for i in range(10, 110, 10): # logging.warning(i) myled.value = float(i/100) time.sleep(0.1) else: myled.off() def run_hook(cmd): """ run the configed hook cmd if exists :param cmd: path to cmd """ args = shlex.split(cmd) try: subprocess.run(args) except subprocess.CalledProcessError as e: print('Error: {}'.format(e)) sys.exit(1) if __name__ == '__main__': main()