From f89f9322d857822f9480e98ca7d94a843b3c3b83 Mon Sep 17 00:00:00 2001 From: Reiko Kaps Date: Wed, 2 Jun 2021 17:33:33 +0200 Subject: [PATCH] Basis-Fassung --- README.md | 0 upshatd.py | 341 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 README.md create mode 100644 upshatd.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/upshatd.py b/upshatd.py new file mode 100644 index 0000000..3ffd445 --- /dev/null +++ b/upshatd.py @@ -0,0 +1,341 @@ +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()