├── CatchExceptions4.py ├── EnableResetFactoryDefaults.py ├── CatchExceptions2.py ├── CatchExceptions.py ├── reset2FactoryDefaults.py ├── SetUpgradeMode.py ├── SetSamplingInterval.py ├── setSamplingInterval.py ├── setProtectionVoltage.py ├── CatchExceptions3.py ├── SetParameters.py ├── crontab.txt ├── OTA_firmware_upgrade.py ├── PowerDown.py ├── upsPlus_iot.py ├── PowerCycle.py ├── SMBUS-problem.txt ├── UPS_report_for_UPSPlus_mqtt_NoINA.py ├── UPS_report.py ├── UPS_report_for_UPSPlus_mqtt.py ├── README.md ├── upsPlus.py └── fanShutDownUps.py /CatchExceptions4.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # ar - 18-05-2021 5 | 6 | # import os 7 | import time 8 | # import smbus2 9 | from smbus2 import SMBus 10 | 11 | # Define I2C bus 12 | DEVICE_BUS = 1 13 | 14 | # Define device I2C slave address. 15 | DEVICE_ADDR = 0x17 16 | 17 | def settle(): 18 | time.sleep(0) 19 | 20 | # Write byte to specified I2C register address 21 | def putByte(RA, wbyte): 22 | while True: 23 | try: 24 | with SMBus(DEVICE_BUS) as pbus: 25 | pbus.write_byte_data(DEVICE_ADDR, RA, wbyte) 26 | with SMBus(DEVICE_BUS) as gbus: 27 | rbyte = gbus.read_byte_data(DEVICE_ADDR, RA) 28 | if (wbyte) <= rbyte <= (wbyte): 29 | print("OK ", wbyte, rbyte) 30 | break 31 | else: 32 | # if rbyte < max((wbyte - 2),0): 33 | raise ValueError 34 | except ValueError: 35 | print("Write:", wbyte, "!= Read:", rbyte, " Trying again") 36 | 37 | 38 | while True: 39 | putByte(0x18, 180) 40 | # settle() 41 | putByte(0x18, 0) 42 | time.sleep(1) 43 | 44 | # EOF -------------------------------------------------------------------------------- /EnableResetFactoryDefaults.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # adapted from scripts provided at GitHub: Geeekpi/upsplus by nickfox-taterli 5 | # Enable UPS function "0x1B Reset to Factory Defaults 0/1 Bool" 6 | # ar - 21-05-2021, 07-08-2021, 19-08-2021 7 | 8 | from smbus2 import SMBus 9 | 10 | DEVICE_BUS = 1 11 | DEVICE_ADDR = 0x17 12 | 13 | RESET_FLAG=1 14 | 15 | # Write byte to specified I2C register address 'until it sticks'. 16 | def putByte(RA, wbyte): 17 | while True: 18 | try: 19 | with SMBus(DEVICE_BUS) as pbus: 20 | pbus.write_byte_data(DEVICE_ADDR, RA, wbyte) 21 | with SMBus(DEVICE_BUS) as gbus: 22 | rbyte = gbus.read_byte_data(DEVICE_ADDR, RA) 23 | if (wbyte) <= rbyte <= (wbyte): 24 | print("OK ", wbyte, rbyte) 25 | break 26 | else: 27 | raise ValueError 28 | except ValueError: 29 | print("Write:", wbyte, "!= Read:", rbyte, " Trying again") 30 | pass 31 | 32 | putByte(0x1B, RESET_FLAG & 0xFF) 33 | 34 | print("Register at 0x1B was set to: %d" % RESET_FLAG) 35 | 36 | #EOF 37 | -------------------------------------------------------------------------------- /CatchExceptions2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # ar - 11-05-2021 5 | 6 | # import os 7 | import time 8 | # import smbus2 9 | from smbus2 import SMBus 10 | 11 | # Define I2C bus 12 | DEVICE_BUS = 1 13 | 14 | # Define device I2C slave address. 15 | DEVICE_ADDR = 0x17 16 | 17 | # Raspberry Pi Communicates with MCU via I2C protocol. 18 | # bus = smbus2.SMBus(DEVICE_BUS) 19 | 20 | # Essential UPS I2C register default values 21 | # (name format: Operation Mode Register Address) 22 | # for shut down & power off state 23 | OMR0x18 = 0 # seconds, power off delay 24 | OMR0x19 = 0 # boolean, automatic restart or not 25 | OMR0x1A = 240 # seconds, power up delay 26 | 27 | # Write byte to specified I2C register address 28 | def putByte(RA, byte): 29 | with SMBus(DEVICE_BUS) as bus: 30 | bus.write_byte_data(DEVICE_ADDR, RA, byte) 31 | 32 | def getByte(RA): 33 | with SMBus(DEVICE_BUS) as bus: 34 | byte = bus.read_byte_data(DEVICE_ADDR, RA) 35 | return [byte] 36 | 37 | i = 0 38 | while True: 39 | try: 40 | putByte(0x19, 1) 41 | byte = getByte(0x19) 42 | # print(i, ' - ', byte) 43 | putByte(0x19, 0) 44 | byte = getByte(0x19) 45 | # print(i, ' - ', byte) 46 | i += 1 47 | except TimeoutError as e: 48 | print(i, ' - ', byte, ' - ', e) 49 | time.sleep(0.1) 50 | 51 | # EOF -------------------------------------------------------------------------------- /CatchExceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # ar - 11-05-2021 5 | 6 | # import os 7 | import time 8 | import smbus2 9 | 10 | # Define I2C bus 11 | DEVICE_BUS = 1 12 | 13 | # Define device I2C slave address. 14 | DEVICE_ADDR = 0x17 15 | 16 | # Raspberry Pi Communicates with MCU via I2C protocol. 17 | bus = smbus2.SMBus(DEVICE_BUS) 18 | 19 | # Essential UPS I2C register default values 20 | # (name format: Operation Mode Register Address) 21 | # for shut down & power off state 22 | OMR0x18 = 0 # seconds, power off delay 23 | OMR0x19 = 0 # boolean, automatic restart or not 24 | OMR0x1A = 240 # seconds, power up delay 25 | 26 | # Write byte to specified I2C register address 27 | def putByte(RA, byte): 28 | bus.write_byte_data(DEVICE_ADDR, RA, byte) 29 | 30 | def getByte(RA): 31 | byte = bus.read_byte_data(DEVICE_ADDR, RA) 32 | return [byte] 33 | 34 | i = 0 35 | while True: 36 | try: 37 | putByte(0x19, 1) 38 | time.sleep(0.1) 39 | byte = getByte(0x19) 40 | time.sleep(0.1) 41 | i += 1 42 | print(i, ' - ', byte) 43 | putByte(0x19, 0) 44 | time.sleep(0.1) 45 | byte = getByte(0x19) 46 | time.sleep(0.1) 47 | i += 1 48 | print(i, ' - ', byte) 49 | except Exception as e: 50 | print(i, ' - ', byte, ' - ', e) 51 | break 52 | 53 | # EOF -------------------------------------------------------------------------------- /reset2FactoryDefaults.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # adapted from scripts provided at GitHub: Geeekpi/upsplus by nickfox-taterli 5 | # Execute UPS function "0x1B Reset to Factory Defaults 0/1 Bool" 6 | # ar - 08-06-2022, 11-06-2022 7 | 8 | import sys 9 | import smbus2 10 | import syslog 11 | import click 12 | 13 | # Define I2C bus 14 | DEVICE_BUS = 1 15 | 16 | # Define device i2c slave address. 17 | DEVICE_ADDR = 0x17 18 | 19 | # Raspberry Pi communicates with MCU via i2c protocol. 20 | bus = smbus2.SMBus(DEVICE_BUS) 21 | 22 | print("*"*62) 23 | print(("*** {:^54s} ***").format("-- Perform a reset to factory defaults --")) 24 | print(("*** {:^54s} ***").format("Writing 1 to register 0x1B makes the UPS")) 25 | print(("*** {:^54s} ***").format("reset various settings to default values.")) 26 | print(("*** {:^54s} ***").format(">>> 0x1B will automatically be set back to 0 <<<")) 27 | print("*"*62) 28 | print() 29 | 30 | if click.confirm('Do you want to continue?', default=True): 31 | click.echo('Resetting UPS to factory defaults ...') 32 | else: 33 | print('Script aborted') 34 | exit() 35 | 36 | #exit() 37 | 38 | try: 39 | bus.write_byte_data(DEVICE_ADDR, 0x1B, 0x01) 40 | msg = "UPS Plus reset to factory defaults!" 41 | print(msg) 42 | except Exception as e: 43 | errmsg = "Error resetting UPS Plus to factory defaults: " + str(e) 44 | print(errmsg) 45 | syslog.syslog(syslog.LOG_ERR, errmsg) 46 | 47 | # EOF -------------------------------------------------------------------------------- /SetUpgradeMode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # adapted from scripts provided at GitHub: Geeekpi/upsplus by nickfox-taterli 5 | # ar - 08-05-2021 6 | 7 | # How to enter OTA mode: 8 | # 9 | # After setting (=running this script): 10 | # python3 $HOME/UPS+/SetUpgradeMode.py 11 | # shut down the Pi, 12 | # unplug the external power supply, 13 | # remove the batteries, 14 | # reinsert the batteries, 15 | 16 | # MAKING DOUBLE SURE THE BATTERIES GO IN THE RIGHT WAY 17 | # MAKING DOUBLE SURE THE BATTERIES GO IN THE RIGHT WAY 18 | 19 | # Optionally reconnect the external power supply. 20 | 21 | # Run the upgrade program: 22 | # python3 $HOME/UPS+/OTA_firmware_upgrade.py 23 | 24 | # This downloads & upgrades the firmware. 25 | # When the upgrade program is finished, 26 | # it will shutdown your Raspberry Pi automatically, 27 | # and you need to disconnect the charger and remove all batteries from UPS 28 | # and then insert the batteries again, 29 | 30 | # MAKING DOUBLE SURE THE BATTERIES GO IN THE RIGHT WAY 31 | # MAKING DOUBLE SURE THE BATTERIES GO IN THE RIGHT WAY 32 | 33 | # Now press the function button to turn on the UPS. 34 | 35 | # -------------------------------------------------------------------------- 36 | import smbus2 37 | 38 | DEVICE_BUS = 1 39 | DEVICE_ADDR = 0x17 40 | 41 | bus = smbus2.SMBus(DEVICE_BUS) 42 | 43 | # Set OTA mode 44 | #bus.write_byte_data(DEVICE_ADDR, 50, 127) 45 | while True: 46 | try: 47 | bus.write_byte_data(DEVICE_ADDR, 0x32, 0x7F) 48 | break 49 | except TimeoutError: 50 | continue 51 | 52 | #EOF -------------------------------------------------------------------------------- /SetSamplingInterval.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # adapted from scripts provided at GitHub: Geeekpi/upsplus by nickfox-taterli 5 | # modified from script by @frtz13: ar - 08-06-2021 6 | 7 | import sys 8 | import smbus2 9 | 10 | # Define I2C bus 11 | DEVICE_BUS = 1 12 | 13 | # Define device i2c slave address. 14 | DEVICE_ADDR = 0x17 15 | 16 | print("-"*60) 17 | print("Modify battery sampling interval for UPS Plus") 18 | print("-"*60) 19 | 20 | SI_Min = 1 21 | SI_Max = 1440 22 | 23 | # Raspberry Pi communicates with MCU via i2c protocol. 24 | bus = smbus2.SMBus(DEVICE_BUS) 25 | currentSamplingInterval = bus.read_byte_data(DEVICE_ADDR, 0x16) << 0o10 | bus.read_byte_data(DEVICE_ADDR, 0x15) 26 | print("Current value: %d min" % currentSamplingInterval) 27 | 28 | if len(sys.argv) > 1: 29 | try: 30 | givenSI = int(sys.argv[1]) 31 | if givenSI >= SI_Min and givenSI <= SI_Max : 32 | bus.write_byte_data(DEVICE_ADDR, 0x15, givenSI & 0xFF) 33 | bus.write_byte_data(DEVICE_ADDR, 0x16, (givenSI >> 0o10) & 0xFF) 34 | print("Successfully set the battery sampling interval to: %d min" % givenSI) 35 | else: 36 | errMsg = "Valid sampling interval values range from {:.0f} to {:.0f} min".format(SI_Min, SI_Max) 37 | print(errMsg) 38 | except Exception as exc: 39 | print("Incorrect parameter: {}. ({})".format(sys.argv[1], str(exc))) 40 | else: 41 | print("Usage: {} ".format(sys.argv[0])) 42 | print(" between {:.0f} and {:.0f}".format(SI_Min,SI_Max)) -------------------------------------------------------------------------------- /setSamplingInterval.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # adapted from scripts provided at GitHub: Geeekpi/upsplus by nickfox-taterli 5 | # modified from script by @frtz13: ar - 08-06-2021, 11-06-2022 6 | 7 | import sys 8 | import smbus2 9 | 10 | # Define I2C bus 11 | DEVICE_BUS = 1 12 | 13 | # Define device i2c slave address. 14 | DEVICE_ADDR = 0x17 15 | 16 | print("-"*60) 17 | print("Modify battery sampling interval for UPS Plus") 18 | print("-"*60) 19 | 20 | SI_Min = 1 21 | SI_Max = 1440 22 | 23 | # Raspberry Pi communicates with MCU via i2c protocol. 24 | bus = smbus2.SMBus(DEVICE_BUS) 25 | currentSamplingInterval = bus.read_byte_data(DEVICE_ADDR, 0x16) << 0o10 | bus.read_byte_data(DEVICE_ADDR, 0x15) 26 | print("Current value: %d min" % currentSamplingInterval) 27 | 28 | if len(sys.argv) > 1: 29 | try: 30 | givenSI = int(sys.argv[1]) 31 | if givenSI >= SI_Min and givenSI <= SI_Max : 32 | bus.write_byte_data(DEVICE_ADDR, 0x15, givenSI & 0xFF) 33 | bus.write_byte_data(DEVICE_ADDR, 0x16, (givenSI >> 0o10) & 0xFF) 34 | print("Successfully changed the battery sampling interval to: %d min" % givenSI) 35 | else: 36 | errMsg = "Valid sampling interval values range from {:.0f} to {:.0f} min".format(SI_Min, SI_Max) 37 | print(errMsg) 38 | except Exception as exc: 39 | print("Incorrect parameter: {}. ({})".format(sys.argv[1], str(exc))) 40 | else: 41 | print("Usage: {} ".format(sys.argv[0])) 42 | print(" between {:.0f} and {:.0f}".format(SI_Min,SI_Max)) -------------------------------------------------------------------------------- /setProtectionVoltage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # adapted from scripts provided at GitHub: Geeekpi/upsplus by nickfox-taterli 5 | # modified from script by @frtz13: ar - 08-06-2021, 04-08-2021, 19-08-2021, 06-06-2022, 11-06-2022 6 | 7 | # If the protection voltage is set too low, the UPS will not start the Pi after the batteries have been run down. 8 | # Also the UPS will make a futile attempt to start the Pi after it has been made to shut down by the control script. 9 | 10 | import sys 11 | import smbus2 12 | 13 | # Define I2C bus 14 | DEVICE_BUS = 1 15 | 16 | # Define device i2c slave address. 17 | DEVICE_ADDR = 0x17 18 | 19 | print("-"*60) 20 | print("Modify battery protection voltage for UPS Plus") 21 | print("-"*60) 22 | 23 | PV_Mini_mV = 3000 24 | PV_Maxi_mV = 4000 25 | 26 | # Raspberry Pi communicates with MCU via i2c protocol. 27 | bus = smbus2.SMBus(DEVICE_BUS) 28 | currentProtectionVoltage_mV = bus.read_byte_data(DEVICE_ADDR, 0x12) << 0o10 | bus.read_byte_data(DEVICE_ADDR, 0x11) 29 | print("Current value: %d mV" % currentProtectionVoltage_mV) 30 | 31 | if len(sys.argv) > 1: 32 | try: 33 | givenPV_mV = int(sys.argv[1]) 34 | if givenPV_mV >= PV_Mini_mV and givenPV_mV <= PV_Maxi_mV : 35 | bus.write_byte_data(DEVICE_ADDR, 0x11, givenPV_mV & 0xFF) 36 | bus.write_byte_data(DEVICE_ADDR, 0x12, (givenPV_mV >> 0o10) & 0xFF) 37 | print("Successfully changed the protection voltage to: %d mV" % givenPV_mV) 38 | else: 39 | errMsg = "Valid protection voltage values range from {:.0f} to {:.0f} mV".format(PV_Mini_mV, PV_Maxi_mV) 40 | print(errMsg) 41 | except Exception as exc: 42 | print("Incorrect parameter: {}. ({})".format(sys.argv[1], str(exc))) 43 | else: 44 | print("Usage: {} ".format(sys.argv[0])) 45 | print(" between {:.0f} and {:.0f}".format(PV_Mini_mV,PV_Maxi_mV)) -------------------------------------------------------------------------------- /CatchExceptions3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # ar - 18-05-2021 5 | 6 | # import os 7 | import time 8 | # import smbus2 9 | from smbus2 import SMBus 10 | 11 | # Define I2C bus 12 | DEVICE_BUS = 1 13 | 14 | # Define device I2C slave address. 15 | DEVICE_ADDR = 0x17 16 | 17 | RA = 0x18 18 | 19 | # Raspberry Pi Communicates with MCU via I2C protocol. 20 | # bus = smbus2.SMBus(DEVICE_BUS) 21 | 22 | # Write byte to specified I2C register address 23 | # def putByte(RA, byte): 24 | # with SMBus(DEVICE_BUS) as pbus: 25 | # pbus.write_byte_data(DEVICE_ADDR, RA, byte) 26 | # 27 | # def getByte(RA): 28 | # with SMBus(DEVICE_BUS) as gbus: 29 | # byte = gbus.read_byte_data(DEVICE_ADDR, RA) 30 | # return [byte] 31 | 32 | def settle(): 33 | time.sleep(0.5) 34 | 35 | i = 0 36 | while True: 37 | try: 38 | settle() 39 | with SMBus(DEVICE_BUS) as pbus: 40 | pbus.write_byte_data(DEVICE_ADDR, RA, 180) 41 | settle() 42 | with SMBus(DEVICE_BUS) as gbus: 43 | byte = gbus.read_byte_data(DEVICE_ADDR, RA) 44 | settle() 45 | if byte in (180,179,178,177): 46 | pass 47 | print('OK ', i, ' - ', byte) 48 | else: 49 | print('read != write ', i, ' - ', byte, "NOT in (180,179,178,177)") 50 | with SMBus(DEVICE_BUS) as pbus: 51 | pbus.write_byte_data(DEVICE_ADDR, RA, 0) 52 | settle() 53 | with SMBus(DEVICE_BUS) as gbus: 54 | byte = gbus.read_byte_data(DEVICE_ADDR, RA) 55 | settle() 56 | if byte == 0: 57 | pass 58 | print('OK ', i, ' - ', byte) 59 | else: 60 | print('read != write ', i, ' - ', byte, "!= 0") 61 | time.sleep(1) 62 | i += 1 63 | except TimeoutError as e: 64 | print(i, ' - ', byte, ' - ', e) 65 | settle() 66 | except KeyboardInterrupt: 67 | break 68 | except: 69 | pass 70 | 71 | 72 | # EOF -------------------------------------------------------------------------------- /SetParameters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # adapted from scripts provided at GitHub: Geeekpi/upsplus by nickfox-taterli 5 | # ar - 21-05-2021 6 | 7 | from smbus2 import SMBus 8 | 9 | DEVICE_BUS = 1 10 | DEVICE_ADDR = 0x17 11 | 12 | # Set threshold for UPS automatic power-off to prevent 13 | # destroying the batteries by excessive discharge (unit: mV). 14 | # DISCHARGE_LIMIT (a.k.a. protection voltage) will be stored in memory at 0x11-0x12 15 | #DISCHARGE_LIMIT = 2500 # for Sanyo NCR18650GA 3450 mAh Li-Ion batteries 16 | #DISCHARGE_LIMIT = 3700 # default from GeeekPi 17 | DISCHARGE_LIMIT = 3200 18 | 19 | # Set the sampling interval, unit: min (usually 2). 20 | # During a few seconds all blue charging level LEDs are off 21 | # and only the batteries deliver power to the Pi 22 | # as sampling of battery characteristics takes place. 23 | SAMPLING_INTERVAL = 2 24 | 25 | 26 | 27 | # Write byte to specified I2C register address 'until it sticks'. 28 | def putByte(RA, wbyte): 29 | while True: 30 | try: 31 | with SMBus(DEVICE_BUS) as pbus: 32 | pbus.write_byte_data(DEVICE_ADDR, RA, wbyte) 33 | with SMBus(DEVICE_BUS) as gbus: 34 | rbyte = gbus.read_byte_data(DEVICE_ADDR, RA) 35 | if (wbyte) <= rbyte <= (wbyte): 36 | # print("OK ", wbyte, rbyte) 37 | break 38 | else: 39 | raise ValueError 40 | except ValueError: 41 | # print("Write:", wbyte, "!= Read:", rbyte, " Trying again") 42 | pass 43 | 44 | # Store DISCHARGE_LIMIT (a.k.a. protection voltage) in memory at 0x11-0x12 45 | putByte(0x11, DISCHARGE_LIMIT & 0xFF) 46 | putByte(0x12, (DISCHARGE_LIMIT >> 0o10) & 0xFF) 47 | 48 | # Store SAMPLING_INTERVAL for battery characteristics sampling in memory at 0x15-0x16 49 | putByte(0x15, SAMPLING_INTERVAL & 0xFF) 50 | putByte(0x16, (SAMPLING_INTERVAL >> 0o10) & 0xFF) 51 | 52 | print("Discharge limit was set to: %d mV" % DISCHARGE_LIMIT) 53 | print("Sampling interval was set to: %d min" % SAMPLING_INTERVAL) 54 | 55 | #EOF 56 | -------------------------------------------------------------------------------- /crontab.txt: -------------------------------------------------------------------------------- 1 | # Edit this file to introduce tasks to be run by cron. 2 | # 3 | # Each task to run has to be defined through a single line 4 | # indicating with different fields when the task will be run 5 | # and what command to run for the task 6 | # 7 | # To define the time you can provide concrete values for 8 | # minute (m), hour (h), day of month (dom), month (mon), 9 | # and day of week (dow) or use '*' in these fields (for 'any'). 10 | # 11 | # Notice that tasks will be started based on the cron's system 12 | # daemon's notion of time and timezones. 13 | # 14 | # Output of the crontab jobs (including errors) is sent through 15 | # email to the user the crontab file belongs to (unless redirected). 16 | # 17 | # For example, you can run a backup of all your user accounts 18 | # at 5 a.m every week with: 19 | # 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/ 20 | # 21 | # For more information see the manual pages of crontab(5) and cron(8) 22 | 23 | # m h dom mon dow command 24 | 25 | # Run the regular UPS control script upsPlus.py every minute. 26 | # Establish a lock to prevent PowerCycle.py to cut the UPS control script short. 27 | * * * * * flock $HOME/UPS+/UPS_job.lock python3 $HOME/UPS+/upsPlus.py >>$HOME/UPS+/UPS_event.log 28 | 29 | # Running PowerCycle.py ever so often as a stress test ... 30 | # 31 | # Do not reboot or run PowerCycle.py when backintime is scheduled to run! 32 | # Wait for cron scheduled upsPlus.py to lock the lock file first and run. 33 | # Make reboot or PowerCycle.py wait for upsPlus.py to finish and release the lock file. 34 | 20 * * * * [ "$(date '+\%H:\%M')" != "04:00" ] && sleep 5 && flock -w 15 $HOME/UPS+/UPS_job.lock python3 $HOME/UPS+/PowerCycle.py 35 | #0,30 * * * * [ "$(date '+\%H:\%M')" != "04:00" ] && sleep 5 && flock -w 15 $HOME/UPS+/UPS_job.lock python3 $HOME/UPS+/PowerCycle.py 36 | #15,45 * * * * [ "$(date '+\%H:\%M')" != "04:00" ] && sleep 5 && flock -w 15 $HOME/UPS+/UPS_job.lock sudo shutdown --reboot now 37 | #45 3 * * * [ "$(date '+\%H:\%M')" != "04:00" ] && sleep 5 && flock -w 15 $HOME/UPS+/UPS_job.lock sudo shutdown --reboot now 38 | 39 | #Back In Time system entry, this will be edited by the gui: 40 | 0 4 * * * /usr/bin/nice -n 19 /usr/bin/ionice -c2 -n7 /usr/bin/backintime backup-job >/dev/null 41 | -------------------------------------------------------------------------------- /OTA_firmware_upgrade.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Slightly edited from GeeekPi original script 5 | # ar - 16-05-2021 6 | 7 | import os 8 | import time 9 | import json 10 | import smbus2 11 | import requests 12 | 13 | # How to enter into OTA mode: 14 | # 15 | # Method 1) Setting register in terminal: i2cset -y 1 0x17 50 127 b 16 | # (can be done in terminal by running: python3 SetUpgradeMode.py) 17 | # 18 | # Method 2) Remove all power connections and batteries, and then hold the power button, insert the batteries. 19 | 20 | # Define device bus and address, and firmware url. 21 | DEVICE_BUS = 1 22 | DEVICE_ADDR = 0x18 # OTA Firmware Upgrade Mode 23 | UPDATE_URL = "https://api.thekoziolfoundation.com/update" 24 | 25 | # Instance of bus. 26 | bus = smbus2.SMBus(DEVICE_BUS) 27 | aReceiveBuf = [] 28 | 29 | for i in range(0xF0, 0xFC): 30 | aReceiveBuf.append(bus.read_byte_data(DEVICE_ADDR, i)) 31 | 32 | UID0 = "%08X" % (aReceiveBuf[0x03] << 0o30 | aReceiveBuf[0x02] << 0o20 | aReceiveBuf[0x01] << 0o10 | aReceiveBuf[0x00]) 33 | UID1 = "%08X" % (aReceiveBuf[0x07] << 0o30 | aReceiveBuf[0x06] << 0o20 | aReceiveBuf[0x05] << 0o10 | aReceiveBuf[0x04]) 34 | UID2 = "%08X" % (aReceiveBuf[0x0B] << 0o30 | aReceiveBuf[0x0A] << 0o20 | aReceiveBuf[0x09] << 0o10 | aReceiveBuf[0x08]) 35 | 36 | r = requests.post(UPDATE_URL, data={"UID0":UID0, "UID1":UID1, "UID2":UID2}) 37 | r = json.loads(r.text) 38 | 39 | if r['code'] != 0: 40 | print('Could not get the firmware due to:' + r['reason']) 41 | exit(r['code']) 42 | else: 43 | print('Passed authentication, now downloading the latest firmware ...') 44 | req = requests.get(r['url']) 45 | with open("/tmp/firmware.bin", "wb") as f: 46 | f.write(req.content) 47 | print("Firmware downloaded successfully.") 48 | 49 | print("Please keep the UPS+ powered, while the firmware is being upgraded.") 50 | print("Disconnecting power during the upgrade will cause unrecoverable failure of the UPS!") 51 | with open("/tmp/firmware.bin", "rb") as f: 52 | while True: 53 | data = f.read(0x10) 54 | for i in range(len(list(data))): 55 | bus.write_byte_data(0x18, i + 1, data[i]) 56 | bus.write_byte_data(0x18, 0x32, 0xFA) 57 | time.sleep(0.1) 58 | print('.', end='',flush=True) 59 | 60 | if len(list(data)) == 0: 61 | bus.write_byte_data(0X18, 0x32, 0x00) 62 | print('.', flush=True) 63 | print('Firmware upgrade completed.') 64 | print('Please disconnect all power/batteries and reinsert to start using the new firmware.') 65 | os.system("sudo halt") 66 | while True: 67 | time.sleep(10) 68 | #EOF -------------------------------------------------------------------------------- /PowerDown.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # adapted from scripts provided at GitHub: Geeekpi/upsplus by nickfox-taterli 5 | # ar - 08-05-2021, (PowerDown.py) 30-05-2022, 03-06-2022, 07-06-2022, 11-06-2022, 6 | # 17-06-2022 7 | 8 | # ''' Halt the Pi, then cut power ''' 9 | 10 | import os 11 | import time 12 | import smbus2 13 | import click 14 | 15 | # Define I2C bus 16 | DEVICE_BUS = 1 17 | 18 | # Define device I2C slave address. 19 | DEVICE_ADDR = 0x17 20 | 21 | # UPS I2C registers used in this script (name format: Operation Mode Register Address) 22 | # Set only one of 0x18 or 0x19 unequal to 0, not both! 23 | # The values given here determine how the UPS+ will proceed after this script ends. 24 | OMR0x18=60 # seconds, power off delay, power stays off 25 | OMR0x19=0 # boolean, automatic restart or not upon return of external power 26 | OMR0x1A=0 # seconds, power off delay with subsequent restoring power about 10 minutes later. 27 | 28 | # Raspberry Pi communicates with MCU via I2C protocol. 29 | bus = smbus2.SMBus(DEVICE_BUS) 30 | 31 | print("*"*62) 32 | print(("*** {:^54s} ***").format("-- Perform a controlled shutdown --")) 33 | print(("*** {:^54s} ***").format("Script shuts down OS in an orderly manner and makes")) 34 | print(("*** {:^54s} ***").format("the UPS+ cut power to the Pi after "+str(OMR0x18)+" seconds.")) 35 | print("*"*62) 36 | print(("*** {:^54s} ***").format("Physical access to the UPS required to restart the Pi!")) 37 | print("*"*62) 38 | print() 39 | 40 | if click.confirm('Do you want to continue?', default=False): 41 | click.echo('Shut down OS & remove power ...') 42 | else: 43 | print('Script aborted') 44 | exit() 45 | 46 | #exit() 47 | 48 | # Set UPS power down timer (unit: seconds) to allow the Pi ample time to sync & shutdown. 49 | # For power down without restart only 'power down' countdown register (0x18) must be set. 50 | while True: 51 | try: 52 | bus.write_byte_data(DEVICE_ADDR, 0x18, OMR0x18) 53 | time.sleep(0.1) 54 | break 55 | except TimeoutError: 56 | continue 57 | 58 | 59 | # Automatic restart on return of external power? 60 | # Enable: write 1 to register 0x19 61 | # Disable: write 0 to register 0x19 62 | while True: 63 | try: 64 | bus.write_byte_data(DEVICE_ADDR, 0x19, OMR0x19) 65 | time.sleep(0.1) 66 | break 67 | except TimeoutError: 68 | continue 69 | 70 | # For power cycling only the 'restart countdown' register (0x1A) must be set! 71 | # The UPS+ will disconnect power from the Pi at the end of the countdown, 72 | # thus allowing the script to make the OS shut down before the power is cut. 73 | # About 10 minutes later the UPS+ restores power to the Pi, which will then restart. 74 | while True: 75 | try: 76 | bus.write_byte_data(DEVICE_ADDR, 0x1A, OMR0x1A) 77 | time.sleep(0.1) 78 | break 79 | except TimeoutError: 80 | continue 81 | 82 | # Make Pi perform sync and then halt. 83 | os.system("sudo sync && sudo halt &") 84 | 85 | # Script continues executing, indefinitely as it were. 86 | # until it is eventually killed by the Pi shutting down. 87 | while True: 88 | time.sleep(10) 89 | 90 | # Control is now left to the UPS' F/W and MCU ... 91 | # EOF 92 | -------------------------------------------------------------------------------- /upsPlus_iot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # modified by ar - 14-05-2021 5 | 6 | # ''' Update the status of batteries to IoT platform ''' 7 | 8 | import time 9 | import smbus2 10 | import requests 11 | from ina219 import INA219, DeviceRangeError 12 | import random 13 | from datetime import datetime, timezone 14 | 15 | DEVICE_BUS = 1 16 | DEVICE_ADDR = 0x17 17 | SAMPLE_TIME = 2 18 | FEED_URL = "https://api.thekoziolfoundation.com/feed" 19 | #time.sleep(random.randint(0, 59)) 20 | 21 | DATA = dict() 22 | 23 | ina = INA219(0.00725, address=0x40) 24 | ina.configure() 25 | DATA['PiVccVolt'] = ina.voltage() 26 | DATA['PiIddAmps'] = ina.current() 27 | 28 | ina = INA219(0.005, address=0x45) 29 | ina.configure() 30 | DATA['BatVccVolt'] = ina.voltage() 31 | try: 32 | DATA['BatIddAmps'] = ina.current() 33 | except DeviceRangeError: 34 | DATA['BatIddAmps'] = 16000 35 | 36 | bus = smbus2.SMBus(DEVICE_BUS) 37 | 38 | aReceiveBuf = [] 39 | aReceiveBuf.append(0x00) 40 | 41 | i=0x01 42 | while i<0x100: 43 | try: 44 | aReceiveBuf.append(bus.read_byte_data(DEVICE_ADDR, i)) 45 | i=i+1 46 | time.sleep(0.1) 47 | except TimeoutError: 48 | continue 49 | 50 | #for i in range(0x01, 0xFF): 51 | # aReceiveBuf.append(bus.read_byte_data(DEVICE_ADDR, i)) 52 | 53 | DATA['McuVccVolt'] = aReceiveBuf[0x02] << 0o10 | aReceiveBuf[0x01] 54 | DATA['BatPinCVolt'] = aReceiveBuf[0x06] << 0o10 | aReceiveBuf[0x05] 55 | DATA['ChargeTypeCVolt'] = aReceiveBuf[0x08] << 0o10 | aReceiveBuf[0x07] 56 | DATA['ChargeMicroVolt'] = aReceiveBuf[0x0A] << 0o10 | aReceiveBuf[0x09] 57 | 58 | DATA['BatTemperature'] = aReceiveBuf[0x0C] << 0o10 | aReceiveBuf[0x0B] 59 | DATA['BatFullVolt'] = aReceiveBuf[0x0E] << 0o10 | aReceiveBuf[0x0D] 60 | DATA['BatEmptyVolt'] = aReceiveBuf[0x10] << 0o10 | aReceiveBuf[0x0F] 61 | DATA['BatProtectVolt'] = aReceiveBuf[0x12] << 0o10 | aReceiveBuf[0x11] 62 | DATA['SampleTime'] = aReceiveBuf[0x16] << 0o10 | aReceiveBuf[0x15] 63 | DATA['AutoPowerOn'] = aReceiveBuf[0x19] 64 | 65 | DATA['OnlineTime'] = aReceiveBuf[0x1F] << 0o30 | aReceiveBuf[0x1E] << 0o20 | aReceiveBuf[0x1D] << 0o10 | aReceiveBuf[0x1C] 66 | DATA['FullTime'] = aReceiveBuf[0x23] << 0o30 | aReceiveBuf[0x22] << 0o20 | aReceiveBuf[0x21] << 0o10 | aReceiveBuf[0x20] 67 | DATA['OneshotTime'] = aReceiveBuf[0x27] << 0o30 | aReceiveBuf[0x26] << 0o20 | aReceiveBuf[0x25] << 0o10 | aReceiveBuf[0x24] 68 | DATA['Version'] = aReceiveBuf[0x29] << 0o10 | aReceiveBuf[0x28] 69 | 70 | DATA['UID0'] = "%08X" % (aReceiveBuf[0xF3] << 0o30 | aReceiveBuf[0xF2] << 0o20 | aReceiveBuf[0xF1] << 0o10 | aReceiveBuf[0xF0]) 71 | DATA['UID1'] = "%08X" % (aReceiveBuf[0xF7] << 0o30 | aReceiveBuf[0xF6] << 0o20 | aReceiveBuf[0xF5] << 0o10 | aReceiveBuf[0xF4]) 72 | DATA['UID2'] = "%08X" % (aReceiveBuf[0xFB] << 0o30 | aReceiveBuf[0xFA] << 0o20 | aReceiveBuf[0xF9] << 0o10 | aReceiveBuf[0xF8]) 73 | 74 | #time.sleep(random.randint(0, 59)) 75 | 76 | # Record starting time of upload 77 | StartTime = datetime.now(timezone.utc).astimezone() 78 | TimeStampA = '{:%d-%m-%Y %H:%M:%S}'.format(StartTime) 79 | print(("START - UPS data upload to Koziol Foundation: {: 1) and not (sys.argv[1]=="yes"): 48 | print("Incorrect parameter") 49 | print("Syntax for non-interactive use is: PowerCycle.py yes") 50 | exit() 51 | 52 | print('Initiating power cycle ...') 53 | 54 | #exit() 55 | 56 | # Set UPS power down timer (unit: seconds) to allow the Pi ample time to sync & shutdown. 57 | # For power down without restart only 'power down' countdown register (0x18) must be set. 58 | while True: 59 | try: 60 | bus.write_byte_data(DEVICE_ADDR, 0x18, OMR0x18) 61 | time.sleep(0.1) 62 | break 63 | except TimeoutError: 64 | continue 65 | 66 | 67 | # Automatic restart on return of external power? 68 | # Enable: write 1 to register 0x19 69 | # Disable: write 0 to register 0x19 70 | while True: 71 | try: 72 | bus.write_byte_data(DEVICE_ADDR, 0x19, OMR0x19) 73 | time.sleep(0.1) 74 | break 75 | except TimeoutError: 76 | continue 77 | 78 | # For power cycling only the 'restart countdown' register (0x1A) must be set! 79 | # The UPS+ will disconnect power from the Pi at the end of the countdown, 80 | # thus allowing the script to make the OS shut down before the power is cut. 81 | # About 8 minutes later the UPS+ restores power to the Pi, which will then restart. 82 | while True: 83 | try: 84 | bus.write_byte_data(DEVICE_ADDR, 0x1A, OMR0x1A) 85 | time.sleep(0.1) 86 | break 87 | except TimeoutError: 88 | continue 89 | 90 | # Make Pi perform sync and then halt. 91 | os.system("sudo sync && sudo halt &") 92 | 93 | # Script continues executing, indefinitely as it were (& keeping the lock) 94 | # until it is eventually killed by the Pi shutting down. 95 | while True: 96 | time.sleep(10) 97 | 98 | # Control is now left to the UPS' F/W and MCU ... 99 | # EOF 100 | -------------------------------------------------------------------------------- /SMBUS-problem.txt: -------------------------------------------------------------------------------- 1 | pi@RPI-HUB:~ $ $HOME/UPS+/UPS_report 2 | Raspberry Pi supply voltage: 4.920 V 3 | Raspberry Pi current consumption: 2.090 A 4 | Raspberry Pi power consumption: 10.600 W 5 | 6 | Battery voltage (from INA219): 4.220 V 7 | Battery current (charging): 0.160 A 8 | Power supplied to the batteries: 0.790 W 9 | 10 | Traceback (most recent call last): 11 | File "/home/pi/UPS+/UPS_report.py", line 119, in 12 | aReceiveBuf.append(bus.read_byte_data(DEVICE_ADDR, i)) 13 | File "/home/pi/.local/lib/python3.7/site-packages/smbus2/smbus2.py", line 433, in read_byte_data 14 | ioctl(self.fd, I2C_SMBUS, msg) 15 | TimeoutError: [Errno 110] Connection timed out 16 | pi@RPI-HUB:~ $ 17 | 18 | 19 | 20 | i: 10 - byte: 0B 21 | i: 11 - byte: F0 22 | i: 12 - byte: 0A 23 | i: 13 - byte: 63 24 | i: 14 - byte: 00 25 | i: 15 - byte: 02 26 | i: 16 - byte: 00 27 | i: 17 - byte: 01 28 | i: 18 - byte: 00 29 | i: 19 - byte: 00 30 | i: 1A - byte: 00 31 | i: 1B - byte: 00 32 | i: 1C - byte: 33 33 | i: 1D - byte: A4 34 | i: 1E - byte: 06 35 | i: 1F - byte: 00 36 | i: 20 - byte: E7 37 | j= 62 38 | 39 | i: 01 - byte: CD 40 | i: 02 - byte: 0C 41 | i: 03 - byte: 34 42 | i: 04 - byte: 13 43 | i: 05 - byte: 49 44 | i: 06 - byte: 10 45 | i: 07 - byte: CC 46 | i: 08 - byte: 23 47 | i: 09 - byte: 00 48 | i: 0A - byte: 00 49 | i: 0B - byte: 2C 50 | i: 0C - byte: 00 51 | i: 0D - byte: DB 52 | i: 0E - byte: 10 53 | i: 0F - byte: F0 54 | TimeoutError 16 55 | Traceback (most recent call last): 56 | File "/home/pi/UPS+/UPS_reportX.py", line 51, in 57 | print("i: %02X" % i, " - byte: %02X" % bReceiveBuf[i]) 58 | IndexError: list index out of range 59 | pi@RPI-HUB:~ $ 60 | 61 | 62 | pi@RPI-HUB:~ $ python3 $HOME/UPS+/UPS_report.py 63 | *** Data from INA219 at 0x40: 64 | Raspberry Pi supply voltage: 4.920 V 65 | Raspberry Pi current consumption: 2.340 A 66 | Raspberry Pi power consumption: 11.000 W 67 | 68 | *** Data from INA219 at 0x45: 69 | Battery voltage: 4.220 V 70 | Battery current (charging): 0.232 A 71 | Power supplied to the batteries: 1.010 W 72 | 73 | Traceback (most recent call last): 74 | File "/home/pi/UPS+/UPS_report.py", line 81, in 75 | aReceiveBuf.append(bus.read_byte_data(DEVICE_ADDR, i)) 76 | File "/home/pi/.local/lib/python3.7/site-packages/smbus2/smbus2.py", line 433, in read_byte_data 77 | ioctl(self.fd, I2C_SMBUS, msg) 78 | TimeoutError: [Errno 110] Connection timed out 79 | 80 | During handling of the above exception, another exception occurred: 81 | 82 | Traceback (most recent call last): 83 | File "/home/pi/UPS+/UPS_report.py", line 84, in 84 | print(i, ' - ', aReceiveBuf[i], ' - ', e) 85 | IndexError: list index out of range 86 | pi@RPI-HUB:~ $ 87 | 88 | *** Data from INA219 at 0x45: 89 | Battery voltage: 4.160 V 90 | Battery current (discharging): 2.800 A 91 | Traceback (most recent call last): 92 | File "/home/pi/UPS+/UPS_report.py", line 47, in 93 | print("Battery power consumption: %8.3f W" % round_sig(ina.power()/1000,n=3)) 94 | File "/home/pi/.local/lib/python3.7/site-packages/ina219.py", line 204, in power 95 | self._handle_current_overflow() 96 | File "/home/pi/.local/lib/python3.7/site-packages/ina219.py", line 245, in _handle_current_overflow 97 | while self._has_current_overflow(): 98 | File "/home/pi/.local/lib/python3.7/site-packages/ina219.py", line 359, in _has_current_overflow 99 | ovf = self._read_voltage_register() & self.__OVF 100 | File "/home/pi/.local/lib/python3.7/site-packages/ina219.py", line 367, in _read_voltage_register 101 | return self.__read_register(self.__REG_BUSVOLTAGE) 102 | File "/home/pi/.local/lib/python3.7/site-packages/ina219.py", line 394, in __read_register 103 | register_value = self._i2c.readU16BE(register) 104 | File "/home/pi/.local/lib/python3.7/site-packages/Adafruit_GPIO/I2C.py", line 190, in readU16BE 105 | return self.readU16(register, little_endian=False) 106 | File "/home/pi/.local/lib/python3.7/site-packages/Adafruit_GPIO/I2C.py", line 164, in readU16 107 | result = self._bus.read_word_data(self._address,register) & 0xFFFF 108 | File "/home/pi/.local/lib/python3.7/site-packages/Adafruit_PureIO/smbus.py", line 224, in read_word_data 109 | ioctl(self._device.fileno(), I2C_RDWR, request) 110 | TimeoutError: [Errno 110] Connection timed out 111 | pi@RPI-HUB:~ $ 112 | 113 | pi@RPI-HUB:~ $ python3 $HOME/UPS+/UPS_report.py 114 | *** Data from INA219 at 0x40: 115 | Raspberry Pi supply voltage: 4.920 V 116 | Raspberry Pi current consumption: 2.290 A 117 | Raspberry Pi power consumption: 10.700 W 118 | 119 | *** Data from INA219 at 0x45: 120 | Battery voltage: 4.130 V 121 | Battery current (discharging): 2.900 A 122 | Battery power consumption: 12.100 W 123 | 124 | i= 225 125 | Traceback (most recent call last): 126 | File "/home/pi/UPS+/UPS_report.py", line 80, in 127 | aReceiveBuf.append(bus.read_byte_data(DEVICE_ADDR, i)) 128 | File "/home/pi/.local/lib/python3.7/site-packages/smbus2/smbus2.py", line 433, in read_byte_data 129 | ioctl(self.fd, I2C_SMBUS, msg) 130 | TimeoutError: [Errno 110] Connection timed out 131 | 132 | During handling of the above exception, another exception occurred: 133 | 134 | Traceback (most recent call last): 135 | File "/home/pi/UPS+/UPS_report.py", line 84, in 136 | print('byte read=', aReceiveBuf[i], ' error:', e) 137 | IndexError: list index out of range 138 | pi@RPI-HUB:~ $ 139 | -------------------------------------------------------------------------------- /UPS_report_for_UPSPlus_mqtt_NoINA.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # adapted from scripts provided at GitHub: Geeekpi/upsplus by nickfox-taterli 5 | # ar - 25-05-2021, 19-07-2021, 13-08-2021, 16-08-2021, 20-08-2021, 23-08-2021, 6 | # 27-08-2021, 05-04-2022, 09-05-2022, 23-05-2022, 06-06-2022 7 | 8 | import locale 9 | # Set to Dutch locale to get comma decimal separator 10 | #locale.setlocale(locale.LC_NUMERIC, 'nl_NL.UTF-8') 11 | # Set to system default locale (enable number formatting using decimal separator for the locale) 12 | locale.setlocale(locale.LC_ALL, '') 13 | 14 | from smbus2 import SMBus 15 | from datetime import datetime, timezone 16 | from math import log10, floor 17 | #from ina219 import INA219, DeviceRangeError 18 | 19 | PROTECTION_VOLTAGE_MARGIN_mV=float(200) # mV 20 | PROTECTION_VOLTAGE_MARGIN_mV=PROTECTION_VOLTAGE_MARGIN_mV/1000 # convert to V 21 | 22 | # Record starting time & format in two styles 23 | StartTime = datetime.now(timezone.utc).astimezone() 24 | TimeStampA = '{:%d-%m-%Y %H:%M:%S}'.format(StartTime) 25 | TimeStampB = '{:%Y-%m-%d_%H:%M:%S}'.format(StartTime) 26 | 27 | def round_sig(x, n=3): 28 | if not x: return 0 29 | power = -floor(log10(abs(x))) + (n - 1) 30 | factor = (10 ** power) 31 | return round(x * factor) / factor 32 | 33 | DEVICE_BUS = 1 34 | DEVICE_ADDR = 0x17 35 | 36 | print(("------------------ {:^21s} ---------------------------------").format(TimeStampA)) 37 | print() 38 | 39 | aReceiveBuf = [] 40 | aReceiveBuf.append(0x00) 41 | 42 | i = 0x01 43 | while i < 0x100: 44 | try: 45 | with SMBus(DEVICE_BUS) as bus: 46 | aReceiveBuf.append(bus.read_byte_data(DEVICE_ADDR, i)) 47 | i += 1 48 | except Exception as e: 49 | raise Exception("[UPS_report] Error reading UPS registers: " + str(e)) 50 | 51 | print( "*** Report is based on data collected") 52 | print(("*** by the UPS f/w and read from memory at 0x{:02X}").format(DEVICE_ADDR)) 53 | #print( "--- except value(s) marked with *") 54 | print() 55 | print(locale.format_string("UPS board MCU voltage: %6.3f V (0x01-0x02)", round_sig((aReceiveBuf[0x02] << 0o10 | aReceiveBuf[0x01])/1000,n=4))) 56 | print(locale.format_string("Voltage supplied to the Pi at the POGO pins: %6.3f V (0x03-0x04)", round_sig((aReceiveBuf[0x04] << 0o10 | aReceiveBuf[0x03])/1000,n=4))) 57 | 58 | print(locale.format_string("USB type C port input voltage: %6.3f V (0x07-0x08)", round_sig((aReceiveBuf[0x08] << 0o10 | aReceiveBuf[0x07])/1000,n=4))) 59 | print(locale.format_string("Micro USB port input voltage: %6.3f V (0x09-0x0A)", round_sig((aReceiveBuf[0x0A] << 0o10 | aReceiveBuf[0x09])/1000,n=4))) 60 | 61 | print() 62 | # Learned from the battery internal resistance change, the longer the use, the more stable the data: 63 | print(locale.format_string("Battery temperature (estimate): %6.d°C (0x0B-0x0C)" , round_sig(aReceiveBuf[0x0C] << 0o10 | aReceiveBuf[0x0B]))) 64 | 65 | #print() 66 | print("Automatic detection of battery type: " + ("yes" if not aReceiveBuf[0x2A] else " no") + " (0x2A)") 67 | 68 | # Fully charged voltage is learned through charging and discharging: 69 | print(locale.format_string("Batteries fully charged at (UPS/learned value): %6.3f V (0x0D-0x0E)", round_sig((aReceiveBuf[0x0E] << 0o10 | aReceiveBuf[0x0D])/1000,n=4))) 70 | 71 | # This value is inaccurate during charging: 72 | print(locale.format_string("Voltage at battery terminals: %6.3f V (0x05-0x06)", round_sig((aReceiveBuf[0x06] << 0o10 | aReceiveBuf[0x05])/1000,n=4))) 73 | 74 | # The deep discharge limit value is stored in memory at 0x11-0x12 based on the user's own preference: 75 | # DISCHARGE_LIMIT (a.k.a. protection voltage): 76 | DISCHARGE_LIMIT=(aReceiveBuf[0x12] << 0o10 | aReceiveBuf[0x11])/1000 77 | print(locale.format_string("Discharge limit to be used by the control script: %6.3f V (0x11-0x12)", round_sig(DISCHARGE_LIMIT,n=3))) 78 | 79 | # Fully discharged voltage is learned through charging and discharging. 80 | # A.k.a. empty voltage, at which the UPS f/w will cut power delivery to the Pi, if it comes to that: 81 | print(locale.format_string("Batteries fully discharged at (UPS/learned value): %6.3f V (0x0F-0x10)", round_sig((aReceiveBuf[0x10] << 0o10 | aReceiveBuf[0x0F])/1000,n=3))) 82 | 83 | # At least one complete charge and discharge cycle needs to pass before this value is meaningful: 84 | print(locale.format_string("Remaining battery capacity (estimate): %8.d %% (0x13-0x14)", (aReceiveBuf[0x14] << 0o10 | aReceiveBuf[0x13]))) 85 | 86 | # For a few seconds all blue charging level LEDs are off 87 | # and only the batteries deliver power to the Pi 88 | # as sampling of battery characteristics takes place. 89 | # The interval between sampling events is normally 2 minutes. 90 | print(locale.format_string("Battery sampling ('blue LEDs off') interval: %8.d min (0x15-0x16)", (aReceiveBuf[0x16] << 0o10 | aReceiveBuf[0x15]))) 91 | 92 | print() 93 | print("Current power state: " + ("normal" if aReceiveBuf[0x17] else " other") + " (0x17)") 94 | 95 | print() 96 | if (aReceiveBuf[0x08] << 0o10 | aReceiveBuf[0x07]) > 4000: 97 | print('External power is connected to the USB type C input.\n') 98 | print("Should the external power be interrupted long enough to cause the battery") 99 | print(locale.format_string("voltage to drop below %.3g V (+ %.3g V as a safety margin), an appropriate", 100 | (DISCHARGE_LIMIT, PROTECTION_VOLTAGE_MARGIN_mV))) 101 | print("control script should halt the Pi, after which the UPS may eventually") 102 | print("power the Pi down (& possibly restart it upon return of external power)") 103 | print("depending on the content of 0x18 & 0x1A.\n") 104 | elif (aReceiveBuf[0x0A] << 0o10 | aReceiveBuf[0x09]) > 4000: 105 | print('External power is connected to the micro USB input.\n') 106 | print("Should the external power be interrupted long enough to cause the battery") 107 | print(locale.format_string("voltage to drop below %.3g V (+ %.3g V as a safety margin), an appropriate", 108 | (DISCHARGE_LIMIT, PROTECTION_VOLTAGE_MARGIN_mV))) 109 | print("control script should halt the Pi, after which the UPS may eventually") 110 | print("power the Pi down (& possibly restart it upon return of external power)") 111 | print("depending on the content of 0x18 & 0x1A.\n") 112 | else: 113 | # Not charging. 114 | print("*** EXTERNAL POWER LOST! RUNNING ON BATTERY POWER!") 115 | print() 116 | print("Should the external power be interrupted long enough to cause the battery") 117 | print(locale.format_string("voltage to drop below %.3g V (+ %.3g V as a safety margin), an appropriate", 118 | (DISCHARGE_LIMIT, PROTECTION_VOLTAGE_MARGIN_mV))) 119 | print("control script should halt the Pi, after which the UPS may eventually") 120 | print("power the Pi down & possibly restart it upon return of external power.\n") 121 | print("UPS power off/on timer registers 0x18 and 0x1A and register 0x19") 122 | print("should be set to values appropriate for a power failure event") 123 | print("by the control script immediately before halting the Raspberry Pi.") 124 | print() 125 | 126 | 127 | print(("{:<60s}").format("Current values of the UPS power control registers:\n" 128 | + "0x18=" + str(aReceiveBuf[0x18]) 129 | + " / 0x19=" + str(aReceiveBuf[0x19]) 130 | + " / 0x1A=" + str(aReceiveBuf[0x1A]) 131 | + "\nExplanation:")) 132 | if aReceiveBuf[0x18] == 0: 133 | print('0x18 - Power off timer (no restart) - not set.') 134 | else: 135 | print("0x18 - Power off timer (no restart) - set to: %3.d sec" % (aReceiveBuf[0x18])) 136 | 137 | print(("0x19 - Automatic restart upon return of external power: ") + ("yes" if aReceiveBuf[0x19] else "no")) 138 | 139 | if aReceiveBuf[0x1A] == 0: 140 | print('0x1A - Power off timer (with restart) - not set.') 141 | else: 142 | print("0x1A - Power off timer (with restart) - set to: %3.d sec" % (aReceiveBuf[0x1A])) 143 | print() 144 | 145 | print("Accumulated running time: %8.d min (0x1C-0x1F)" % round((aReceiveBuf[0x1F] << 0o30 | aReceiveBuf[0x1E] << 0o20 | aReceiveBuf[0x1D] << 0o10 | aReceiveBuf[0x1C])/60)) 146 | print("Accumulated charging time: %8.d min (0x20-0x23)" % round((aReceiveBuf[0x23] << 0o30 | aReceiveBuf[0x22] << 0o20 | aReceiveBuf[0x21] << 0o10 | aReceiveBuf[0x20])/60)) 147 | print("Current up time: %8.d min (0x24-0x27)" % round((aReceiveBuf[0x27] << 0o30 | aReceiveBuf[0x26] << 0o20 | aReceiveBuf[0x25] << 0o10 | aReceiveBuf[0x24])/60)) 148 | 149 | print(("{:2d}").format("F/W version: ",(aReceiveBuf[0x29] << 0o10 | aReceiveBuf[0x28]))) 150 | 151 | # Serial Number 152 | UID0 = "%08X" % (aReceiveBuf[0xF3] << 0o30 | aReceiveBuf[0xF2] << 0o20 | aReceiveBuf[0xF1] << 0o10 | aReceiveBuf[0xF0]) 153 | UID1 = "%08X" % (aReceiveBuf[0xF7] << 0o30 | aReceiveBuf[0xF6] << 0o20 | aReceiveBuf[0xF5] << 0o10 | aReceiveBuf[0xF4]) 154 | UID2 = "%08X" % (aReceiveBuf[0xFB] << 0o30 | aReceiveBuf[0xFA] << 0o20 | aReceiveBuf[0xF9] << 0o10 | aReceiveBuf[0xF8]) 155 | 156 | print("Serial Number: " + UID0 + "-" + UID1 + "-" + UID2 ) 157 | print() 158 | 159 | #EOF 160 | 161 | -------------------------------------------------------------------------------- /UPS_report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # adapted from scripts provided at GitHub: Geeekpi/upsplus by nickfox-taterli 5 | # ar - 17-05-2021 6 | 7 | import os 8 | import time 9 | from smbus2 import SMBus 10 | from datetime import datetime, timezone 11 | from math import log10, floor 12 | from ina219 import INA219, DeviceRangeError 13 | 14 | # Record starting time & format in two styles 15 | StartTime = datetime.now(timezone.utc).astimezone() 16 | TimeStampA = '{:%d-%m-%Y %H:%M:%S}'.format(StartTime) 17 | TimeStampB = '{:%Y-%m-%d_%H:%M:%S}'.format(StartTime) 18 | 19 | def round_sig(x, n=3): 20 | if not x: return 0 21 | power = -floor(log10(abs(x))) + (n - 1) 22 | factor = (10 ** power) 23 | return round(x * factor) / factor 24 | 25 | DEVICE_BUS = 1 26 | DEVICE_ADDR = 0x17 27 | 28 | ina = INA219(0.00725,address=0x40) 29 | ina.configure() 30 | print("*** Data from INA219 at 0x40:") 31 | print("Raspberry Pi supply voltage: %8.3f V" % round_sig(ina.voltage(),n=3)) 32 | print("Raspberry Pi current consumption: %8.3f A" % round_sig(ina.current()/1000,n=3)) 33 | print("Raspberry Pi power consumption: %8.3f W" % round_sig(ina.power()/1000,n=3)) 34 | 35 | print() 36 | ina = INA219(0.005,address=0x45) 37 | ina.configure() 38 | print("*** Data from INA219 at 0x45:") 39 | print( "Battery voltage: %8.3f V" % round_sig(ina.voltage(),n=3)) 40 | try: 41 | if ina.current() > 0: 42 | print("Battery current (charging): %8.3f A" % round_sig(abs(ina.current()/1000),n=3)) 43 | print("Power supplied to the batteries: %8.3f W" % round_sig(ina.power()/1000,n=3)) 44 | else: 45 | print("Battery current (discharging): %8.3f A" % round_sig(abs(ina.current()/1000),n=2)) 46 | print("Battery power consumption: %8.3f W" % round_sig(ina.power()/1000,n=3)) 47 | except DeviceRangeError: 48 | print('Out of Range Warning: BATTERY VOLTAGE EXCEEDING SAFE LIMITS!') 49 | # Keep sampling in case of another type of error 50 | except: 51 | pass 52 | finally: 53 | print() 54 | 55 | # Path for parameter files 56 | PATH = str(os.getenv('HOME'))+'/UPS+/' 57 | 58 | f = open(PATH+'PowerOffLimit.txt','rt') 59 | line = f.readline() 60 | POWEROFF_LIMIT = line.strip() 61 | #line = f.readline() 62 | #POWERLOSS_TIMER = line.strip() 63 | f.close() 64 | 65 | f = open(PATH+'GraceTime.txt','r') 66 | GRACE_TIME = int(f.read()) 67 | f.close() 68 | 69 | # Force restart (simulate power plug, write the corresponding number of seconds, 70 | # shut down 5 seconds before the end of the countdown, and then turn on at 0 seconds.) 71 | # bus.write_byte_data(DEVICE_ADDR, 0x1A, 0x1E) 72 | 73 | # Restore factory settings 74 | # (clear memory, clear learning parameters, can not clear the cumulative running time, used for after-sales purposes.) 75 | # bus.write_byte_data(DEVICE_ADDR, 0x1B, 0x01) 76 | 77 | aReceiveBuf = [] 78 | aReceiveBuf.append(0x00) 79 | 80 | i = 0x01 81 | while i < 0x100: 82 | try: 83 | with SMBus(DEVICE_BUS) as bus: 84 | aReceiveBuf.append(bus.read_byte_data(DEVICE_ADDR, i)) 85 | i += 1 86 | except TimeoutError as e: 87 | # print('i=', i, ' - error:', e) 88 | time.sleep(0.1) 89 | 90 | print( "*** Remainder of report is based on data collected") 91 | print(("*** by the UPS f/w and read from memory at 0x{:02X}").format(DEVICE_ADDR)) 92 | print( "--- except value(s) marked with *") 93 | print() 94 | print("UPS board MCU voltage: %6.3f V" % round_sig((aReceiveBuf[0x02] << 0o10 | aReceiveBuf[0x01])/1000,n=3)) 95 | print("Voltage at Pi GPIO header pins: %6.3f V" % round_sig((aReceiveBuf[0x04] << 0o10 | aReceiveBuf[0x03])/1000,n=3)) 96 | 97 | print("USB type C port input voltage: %6.3f V" % round_sig((aReceiveBuf[0x08] << 0o10 | aReceiveBuf[0x07])/1000,n=3)) 98 | print("Micro USB port input voltage: %6.3f V" % round_sig((aReceiveBuf[0x0A] << 0o10 | aReceiveBuf[0x09])/1000,n=3)) 99 | 100 | print() 101 | # Learned from the battery internal resistance change, the longer the use, the more stable the data: 102 | print("Battery temperature (estimate): %6.d°C" % round_sig(aReceiveBuf[0x0C] << 0o10 | aReceiveBuf[0x0B])) 103 | 104 | #print() 105 | print("Automatic detection of battery type (0=yes, 1=no) %6.d" % (aReceiveBuf[0x2A])) 106 | 107 | # Fully charged voltage is learned through charging and discharging: 108 | print("Batteries fully charged at (learned value): %6.3f V" % round_sig((aReceiveBuf[0x0E] << 0o10 | aReceiveBuf[0x0D])/1000,n=3)) 109 | 110 | # This value is inaccurate during charging: 111 | print("Current voltage at battery terminals: %6.3f V" % round_sig((aReceiveBuf[0x06] << 0o10 | aReceiveBuf[0x05])/1000,n=3)) 112 | 113 | # Voltage below which UPS shuts down the Pi & powers off to conserve battery capacity 114 | print("* My power-off voltage limit is set at: %6.3f V" % round_sig(float(POWEROFF_LIMIT)/1000,n=3)) 115 | 116 | # Fully discharged voltage is learned through charging and discharging (a.k.a. empty voltage): 117 | print("Batteries fully discharged at (partially learned): %6.3f V" % round_sig((aReceiveBuf[0x10] << 0o10 | aReceiveBuf[0x0F])/1000,n=3)) 118 | 119 | # The deep discharge limit value is stored in memory at 0x11-0x12 by upsPlus.py: 120 | # DISCHARGE_LIMIT (a.k.a. protection voltage) 121 | DISCHARGE_LIMIT=(aReceiveBuf[0x12] << 0o10 | aReceiveBuf[0x11])/1000 122 | print("Battery deep discharge limit is set at: %6.3f V" % round_sig(DISCHARGE_LIMIT,n=3)) 123 | 124 | # At least one complete charge and discharge cycle needs to pass before this value is meaningful: 125 | print("Remaining battery capacity: %8.d %%" % (aReceiveBuf[0x14] << 0o10 | aReceiveBuf[0x13])) 126 | 127 | # For a few seconds all blue charging level LEDs are off 128 | # and only the batteries deliver power to the Pi 129 | # as sampling of battery characteristics takes place. 130 | # The interval between sampling events is normally 2 minutes. 131 | print("Battery sampling ('blue LEDs off') interval: %8.d min" % (aReceiveBuf[0x16] << 0o10 | aReceiveBuf[0x15])) 132 | 133 | print() 134 | if aReceiveBuf[0x17] == 1: 135 | print("Current power state: normal") 136 | else: 137 | print("Current power state: other") 138 | 139 | print() 140 | print(("{:<60s}").format("UPS power off/on timer registers 0x18 and 0x1A")) 141 | print(("{:<60s}").format("and register 0x19 will be set to values selected")) 142 | print(("{:<60s}").format("for a power failure event by the upsPlus.py script")) 143 | print(("{:<60s}").format("immediately before halting the Raspberry Pi.")) 144 | 145 | print() 146 | if (aReceiveBuf[0x08] << 0o10 | aReceiveBuf[0x07]) > 4000: 147 | print('External power is connected to the USB type C input.\n') 148 | print(("{:<60s}").format("Should the external power be interrupted long enough")) 149 | print(("{:<60s}").format("to cause the battery voltage to drop below "+str(max(DISCHARGE_LIMIT,round_sig(float(POWEROFF_LIMIT)/1000,n=3)))+" V,")) 150 | print(("{:<60s}").format("or remain interrupted for more than "+str(GRACE_TIME)+" min,")) 151 | print(("{:<60s}").format("the Pi will be halted & the UPS will power it down.\n")) 152 | elif (aReceiveBuf[0x0A] << 0o10 | aReceiveBuf[0x09]) > 4000: 153 | print('External power is connected to the micro USB input.\n') 154 | print(("{:<60s}").format("Should the external power be interrupted long enough")) 155 | print(("{:<60s}").format("to cause the battery voltage to drop below "+str(max(DISCHARGE_LIMIT,round_sig(float(POWEROFF_LIMIT)/1000,n=3)))+" V,")) 156 | print(("{:<60s}").format("or remain interrupted for more than "+str(GRACE_TIME)+" min,")) 157 | print(("{:<60s}").format("the Pi will be halted & the UPS will power it down.\n")) 158 | else: 159 | # Not charging. 160 | print("*** EXTERNAL POWER LOST! RUNNING ON BATTERY POWER!") 161 | if (GRACE_TIME==0): 162 | print("*** A shutdown command will be sent to the OS/Pi!") 163 | else: 164 | print("*** Grace time till shutdown: %d min" % GRACE_TIME) 165 | print() 166 | 167 | print(("{:<60s}").format("UPS power control registers: " 168 | + "0x18=" + str(aReceiveBuf[0x18]) 169 | + " / 0x19=" + str(aReceiveBuf[0x19]) 170 | + " / 0x1A=" + str(aReceiveBuf[0x1A]))) 171 | if aReceiveBuf[0x18] == 0: 172 | print('0x18 - Power off (no restart) - timer not set.') 173 | else: 174 | print("0x18 - Power off (no restart) - timer set to: %3.d sec" % (aReceiveBuf[0x18])) 175 | 176 | if aReceiveBuf[0x19] == 0x01: 177 | print(("{:<60s}").format("0x19 - Automatic restart upon return of external power")) 178 | else: 179 | print(("{:<60s}").format("0x19 - No automatic restart upon return of external power")) 180 | 181 | if aReceiveBuf[0x1A] == 0: 182 | print('0x1A - Power off (with restart) - timer not set.') 183 | else: 184 | print("0x1A - Power off (with restart) - timer set to: %3.d sec" % (aReceiveBuf[0x1A])) 185 | print() 186 | 187 | print("Accumulated running time: %8.d min" % round((aReceiveBuf[0x1F] << 0o30 | aReceiveBuf[0x1E] << 0o20 | aReceiveBuf[0x1D] << 0o10 | aReceiveBuf[0x1C])/60)) 188 | print("Accumulated charging time: %8.d min" % round((aReceiveBuf[0x23] << 0o30 | aReceiveBuf[0x22] << 0o20 | aReceiveBuf[0x21] << 0o10 | aReceiveBuf[0x20])/60)) 189 | print("Current up time: %8.d min" % round((aReceiveBuf[0x27] << 0o30 | aReceiveBuf[0x26] << 0o20 | aReceiveBuf[0x25] << 0o10 | aReceiveBuf[0x24])/60)) 190 | 191 | print(("{:2d}").format("F/W version:",(aReceiveBuf[0x29] << 0o10 | aReceiveBuf[0x28]))) 192 | 193 | # Serial Number 194 | UID0 = "%08X" % (aReceiveBuf[0xF3] << 0o30 | aReceiveBuf[0xF2] << 0o20 | aReceiveBuf[0xF1] << 0o10 | aReceiveBuf[0xF0]) 195 | UID1 = "%08X" % (aReceiveBuf[0xF7] << 0o30 | aReceiveBuf[0xF6] << 0o20 | aReceiveBuf[0xF5] << 0o10 | aReceiveBuf[0xF4]) 196 | UID2 = "%08X" % (aReceiveBuf[0xFB] << 0o30 | aReceiveBuf[0xFA] << 0o20 | aReceiveBuf[0xF9] << 0o10 | aReceiveBuf[0xF8]) 197 | 198 | print("Serial Number: " + UID0 + "-" + UID1 + "-" + UID2 ) 199 | print() 200 | 201 | #EOF 202 | -------------------------------------------------------------------------------- /UPS_report_for_UPSPlus_mqtt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # adapted from scripts provided at GitHub: Geeekpi/upsplus by nickfox-taterli 5 | # ar - 25-05-2021, 19-07-2021, 13-08-2021, 16-08-2021, 20-08-2021, 23-08-2021, 6 | # 27-08-2021 7 | 8 | import locale 9 | # Set to Dutch locale to get comma decimal separator 10 | #locale.setlocale(locale.LC_NUMERIC, 'nl_NL.UTF-8') 11 | # Set to system default locale (enable number formatting using decimal separator for the locale) 12 | locale.setlocale(locale.LC_ALL, '') 13 | 14 | from smbus2 import SMBus 15 | from datetime import datetime, timezone 16 | from math import log10, floor 17 | from ina219 import INA219, DeviceRangeError 18 | 19 | PROTECTION_VOLTAGE_MARGIN_mV=float(200) # mV 20 | PROTECTION_VOLTAGE_MARGIN_mV=PROTECTION_VOLTAGE_MARGIN_mV/1000 # convert to V 21 | 22 | # Record starting time & format in two styles 23 | StartTime = datetime.now(timezone.utc).astimezone() 24 | TimeStampA = '{:%d-%m-%Y %H:%M:%S}'.format(StartTime) 25 | TimeStampB = '{:%Y-%m-%d_%H:%M:%S}'.format(StartTime) 26 | 27 | def round_sig(x, n=3): 28 | if not x: return 0 29 | power = -floor(log10(abs(x))) + (n - 1) 30 | factor = (10 ** power) 31 | return round(x * factor) / factor 32 | 33 | DEVICE_BUS = 1 34 | DEVICE_ADDR = 0x17 35 | 36 | # Instance INA219 and getting information from it. 37 | #ina = INA219(0.00725, busnum=DEVICE_BUS, address=0x40) # Value given by GeeekPi 38 | #ina = INA219(0.011, busnum=DEVICE_BUS, address=0x40) # Experimental value 39 | ina = INA219(0.0145, busnum=DEVICE_BUS, address=0x40) 40 | ina.configure() 41 | #ina.configure(ina.RANGE_16V, ina.GAIN_2_80MV, ina.ADC_128SAMP, ina.ADC_128SAMP) 42 | 43 | print(("------------------ {:^21s} ---------------------------------").format(TimeStampA)) 44 | print() 45 | print("*** Data from INA219 at 0x40:") 46 | #print("Raspberry Pi supply voltage: %8.3f V" % round_sig(ina.voltage(),n=3)) 47 | print(locale.format_string("Raspberry Pi supply voltage: %8.3f V",round_sig(ina.voltage(),n=4))) 48 | print(locale.format_string("Raspberry Pi current consumption: %8.3f A",round_sig(ina.current()/1000,n=3))) 49 | print(locale.format_string("Raspberry Pi power consumption: %8.3f W", round_sig(ina.power()/1000,n=3))) 50 | print() 51 | 52 | 53 | #ina = INA219(0.005, busnum=DEVICE_BUS, address=0x45) # Value given by GeeekPi 54 | ina = INA219(0.011, busnum=DEVICE_BUS, address=0x45) # Experimental value 55 | ina.configure() 56 | #ina.configure(ina.RANGE_16V, ina.GAIN_2_80MV, ina.ADC_128SAMP, ina.ADC_128SAMP) 57 | print("*** Data from INA219 at 0x45:") 58 | print(locale.format_string( "Battery voltage: %8.3f V", round_sig(ina.voltage(),n=4))) 59 | try: 60 | if ina.current() > 0: 61 | print(locale.format_string("Battery current (charging): %8.3f A", round_sig(abs(ina.current()/1000),n=3))) 62 | print(locale.format_string("Power supplied to the batteries: %8.3f W", round_sig(ina.power()/1000,n=3))) 63 | else: 64 | print(locale.format_string("Battery current (discharging): %8.3f A", round_sig(abs(ina.current()/1000),n=2))) 65 | print(locale.format_string("Battery power consumption: %8.3f W", round_sig(ina.power()/1000,n=3))) 66 | except DeviceRangeError: 67 | print('INA219 at 0x45: Out of Range Warning!') 68 | print('BATTERY CURRENT POSSIBLY EXCEEDING SAFE LIMITS!') 69 | # Keep sampling in case of another type of error 70 | except: 71 | pass 72 | finally: 73 | print() 74 | 75 | aReceiveBuf = [] 76 | aReceiveBuf.append(0x00) 77 | 78 | i = 0x01 79 | while i < 0x100: 80 | try: 81 | with SMBus(DEVICE_BUS) as bus: 82 | aReceiveBuf.append(bus.read_byte_data(DEVICE_ADDR, i)) 83 | i += 1 84 | except Exception as e: 85 | raise Exception("[UPS_report] Error reading UPS registers: " + str(e)) 86 | 87 | print( "*** Remainder of report is based on data collected") 88 | print(("*** by the UPS f/w and read from memory at 0x{:02X}").format(DEVICE_ADDR)) 89 | #print( "--- except value(s) marked with *") 90 | print() 91 | print(locale.format_string("UPS board MCU voltage: %6.3f V (0x01-0x02)", round_sig((aReceiveBuf[0x02] << 0o10 | aReceiveBuf[0x01])/1000,n=4))) 92 | print(locale.format_string("Voltage at Pi GPIO header pins: %6.3f V (0x03-0x04)", round_sig((aReceiveBuf[0x04] << 0o10 | aReceiveBuf[0x03])/1000,n=4))) 93 | 94 | print(locale.format_string("USB type C port input voltage: %6.3f V (0x07-0x08)", round_sig((aReceiveBuf[0x08] << 0o10 | aReceiveBuf[0x07])/1000,n=4))) 95 | print(locale.format_string("Micro USB port input voltage: %6.3f V (0x09-0x0A)", round_sig((aReceiveBuf[0x0A] << 0o10 | aReceiveBuf[0x09])/1000,n=4))) 96 | 97 | print() 98 | # Learned from the battery internal resistance change, the longer the use, the more stable the data: 99 | print(locale.format_string("Battery temperature (estimate): %6.d°C (0x0B-0x0C)" , round_sig(aReceiveBuf[0x0C] << 0o10 | aReceiveBuf[0x0B]))) 100 | 101 | #print() 102 | print("Automatic detection of battery type: " + ("yes" if not aReceiveBuf[0x2A] else " no") + " (0x2A)") 103 | 104 | # Fully charged voltage is learned through charging and discharging: 105 | print(locale.format_string("Batteries fully charged at (UPS/learned value): %6.3f V (0x0D-0x0E)", round_sig((aReceiveBuf[0x0E] << 0o10 | aReceiveBuf[0x0D])/1000,n=4))) 106 | 107 | # This value is inaccurate during charging: 108 | print(locale.format_string("Current voltage at battery terminals: %6.3f V (0x05-0x06)", round_sig((aReceiveBuf[0x06] << 0o10 | aReceiveBuf[0x05])/1000,n=4))) 109 | 110 | # The deep discharge limit value is stored in memory at 0x11-0x12 based on the user's own preference: 111 | # DISCHARGE_LIMIT (a.k.a. protection voltage): 112 | DISCHARGE_LIMIT=(aReceiveBuf[0x12] << 0o10 | aReceiveBuf[0x11])/1000 113 | print(locale.format_string("Discharge limit for use by the control script: %6.3f V (0x11-0x12)", round_sig(DISCHARGE_LIMIT,n=3))) 114 | 115 | # Fully discharged voltage is learned through charging and discharging. 116 | # A.k.a. empty voltage, at which the UPS f/w will cut power delivery to the Pi, if it comes to that: 117 | print(locale.format_string("Batteries fully discharged at (UPS/learned value): %6.3f V (0x0F-0x10)", round_sig((aReceiveBuf[0x10] << 0o10 | aReceiveBuf[0x0F])/1000,n=3))) 118 | 119 | # At least one complete charge and discharge cycle needs to pass before this value is meaningful: 120 | print(locale.format_string("Remaining battery capacity (estimate): %8.d %% (0x13-0x14)", (aReceiveBuf[0x14] << 0o10 | aReceiveBuf[0x13]))) 121 | 122 | # For a few seconds all blue charging level LEDs are off 123 | # and only the batteries deliver power to the Pi 124 | # as sampling of battery characteristics takes place. 125 | # The interval between sampling events is normally 2 minutes. 126 | print(locale.format_string("Battery sampling ('blue LEDs off') interval: %8.d min (0x15-0x16)", (aReceiveBuf[0x16] << 0o10 | aReceiveBuf[0x15]))) 127 | 128 | print() 129 | if aReceiveBuf[0x17] == 1: 130 | print("Current power state: normal (0x17)") 131 | else: 132 | print("Current power state: other (0x17)") 133 | 134 | print() 135 | if (aReceiveBuf[0x08] << 0o10 | aReceiveBuf[0x07]) > 4000: 136 | print('External power is connected to the USB type C input.\n') 137 | print("Should the external power be interrupted long enough to cause the battery") 138 | print(locale.format_string("voltage to drop below %.3g V (+ %.3g V as a safety margin), an appropriate", 139 | (DISCHARGE_LIMIT, PROTECTION_VOLTAGE_MARGIN_mV))) 140 | print("control script should halt the Pi, after which the UPS may eventually") 141 | print("power the Pi down (& possibly restart it upon return of external power)") 142 | print("depending on the content of 0x18 & 0x1A.\n") 143 | elif (aReceiveBuf[0x0A] << 0o10 | aReceiveBuf[0x09]) > 4000: 144 | print('External power is connected to the micro USB input.\n') 145 | print("Should the external power be interrupted long enough to cause the battery") 146 | print(locale.format_string("voltage to drop below %.3g V (+ %.3g V as a safety margin), an appropriate", 147 | (DISCHARGE_LIMIT, PROTECTION_VOLTAGE_MARGIN_mV))) 148 | print("control script should halt the Pi, after which the UPS may eventually") 149 | print("power the Pi down (& possibly restart it upon return of external power)") 150 | print("depending on the content of 0x18 & 0x1A.\n") 151 | else: 152 | # Not charging. 153 | print("*** EXTERNAL POWER LOST! RUNNING ON BATTERY POWER!") 154 | print() 155 | print("Should the external power be interrupted long enough to cause the battery") 156 | print(locale.format_string("voltage to drop below %.3g V (+ %.3g V as a safety margin), an appropriate", 157 | (DISCHARGE_LIMIT, PROTECTION_VOLTAGE_MARGIN_mV))) 158 | print("control script should halt the Pi, after which the UPS may eventually") 159 | print("power the Pi down & possibly restart it upon return of external power.\n") 160 | print("UPS power off/on timer registers 0x18 and 0x1A and register 0x19") 161 | print("should be set to values appropriate for a power failure event") 162 | print("by the control script immediately before halting the Raspberry Pi.") 163 | print() 164 | 165 | 166 | print(("{:<60s}").format("Current values of the UPS power control registers:\n" 167 | + "0x18=" + str(aReceiveBuf[0x18]) 168 | + " / 0x19=" + str(aReceiveBuf[0x19]) 169 | + " / 0x1A=" + str(aReceiveBuf[0x1A]) 170 | + "\nExplanation:")) 171 | if aReceiveBuf[0x18] == 0: 172 | print('0x18 - Power off timer (no restart) - not set.') 173 | else: 174 | print("0x18 - Power off timer (no restart) - set to: %3.d sec" % (aReceiveBuf[0x18])) 175 | 176 | print(("0x19 - Automatic restart upon return of external power: ") + ("yes" if aReceiveBuf[0x19] else "no")) 177 | 178 | if aReceiveBuf[0x1A] == 0: 179 | print('0x1A - Power off timer (with restart) - not set.') 180 | else: 181 | print("0x1A - Power off timer (with restart) - set to: %3.d sec" % (aReceiveBuf[0x1A])) 182 | print() 183 | 184 | print("Accumulated running time: %8.d min (0x1C-0x1F)" % round((aReceiveBuf[0x1F] << 0o30 | aReceiveBuf[0x1E] << 0o20 | aReceiveBuf[0x1D] << 0o10 | aReceiveBuf[0x1C])/60)) 185 | print("Accumulated charging time: %8.d min (0x20-0x23)" % round((aReceiveBuf[0x23] << 0o30 | aReceiveBuf[0x22] << 0o20 | aReceiveBuf[0x21] << 0o10 | aReceiveBuf[0x20])/60)) 186 | print("Current up time: %8.d min (0x24-0x27)" % round((aReceiveBuf[0x27] << 0o30 | aReceiveBuf[0x26] << 0o20 | aReceiveBuf[0x25] << 0o10 | aReceiveBuf[0x24])/60)) 187 | 188 | print(("{:2d}").format("F/W version:",(aReceiveBuf[0x29] << 0o10 | aReceiveBuf[0x28]))) 189 | 190 | # Serial Number 191 | UID0 = "%08X" % (aReceiveBuf[0xF3] << 0o30 | aReceiveBuf[0xF2] << 0o20 | aReceiveBuf[0xF1] << 0o10 | aReceiveBuf[0xF0]) 192 | UID1 = "%08X" % (aReceiveBuf[0xF7] << 0o30 | aReceiveBuf[0xF6] << 0o20 | aReceiveBuf[0xF5] << 0o10 | aReceiveBuf[0xF4]) 193 | UID2 = "%08X" % (aReceiveBuf[0xFB] << 0o30 | aReceiveBuf[0xFA] << 0o20 | aReceiveBuf[0xF9] << 0o10 | aReceiveBuf[0xF8]) 194 | 195 | print("Serial Number: " + UID0 + "-" + UID1 + "-" + UID2 ) 196 | print() 197 | 198 | #EOF 199 | 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Scripts for a 52Pi UPS plus v.5 2 | 3 | 4 | I am a total apprentice at GitHub. I just uploaded these scripts I developed, to the repository so that others can see what I cobbled together. I am probably using GitHub the wrong way to boot ... 5 | 6 | Anyway ... 7 | For best operation all around the UPS registers at address 0x17 and offsets 0x18-0x1A need to be set with thought. 8 | 9 | My PowerCycle.py script can be scheduled to do a full power cycle of the Pi combined with a proper shutdown. It is debatable if you need a script to do a power cycle, as a frozen Pi will obviously not be able to start it. If you like to to reboot the Pi at regular intervals just to make sure, you can simply schedule an ordinary reboot command, of course. 10 | There's a watchdog function which is enabled in upsPlus.py for cases where the Pi freezes. See below. 11 | Under f/w v.5 the UPS used to restore power to the Pi already 5 seconds after cutting the power as a result of running PowerCycle.py. It was a surprise to see that delay lengthened to ca. 10 minutes after the upgrade to f/w v.7. 12 | 13 | My version of upsPLus.py has rounding to significant bits of the reported electrical units, because I don't believe in the accuracy of mV and mA readings as originally presented ;-) I also prefer to see Volts and Amps in a report, although I may read them automatically as mA and mV when it is more suitable. 14 | Out of the full feature script I made a script that reports just about everything there is to now about the surrent state of the UPS. It applies the same rounding to its output. 15 | 16 | I changed bit-wise shifting operations (<< and >>) to octal notation just because I find it more clear. 17 | 18 | I added TimeoutError exception handling to all reads and writes on the I2C bus, except for some reads concerning the INA devices where the values are only used for reporting. In the upsPlus.py code which decides whether to start shutdown, however, there is also exception/error handling for the INA-voltage. 19 | 20 | Sometimes UPS_report.py (and also upsPlus.py) show the batteries to be discharging even though the charger is connected and powered. This happens when the UPS f/w is doing its data collection about the batteries. While this is going on the UPS shuts off all blue LEDs for a short while every two minutes (=sampling interval, by default every 2 minutes). Apparently the batteries are disconnected from the charger during the sampling procedure and the Pi powered from the batteries. 21 | 22 | My crontab is intended for stress testing of the power cycle behaviour of the UPS, and the I2C bus. 23 | The 'sleep x &&' in the line for PowerCycle.py is to make sure it runs after upsPlus.py. 24 | The flock locks are there so make sure they never run and address the same UPS control registers concurrently. 25 | A condition involving the current time is intended to keep upsPlus.py (and PowerCycle.py) from running when BackinTime is scheduled to run. 26 | 27 | I don't run the original *_iot.py script on a cron schedule. It has a delay built in of up to 59 seconds. If you prevent it from running concurrently with upsPlus.py and v.v., because of the I2C bus, it maydelay upsPlus.py. I find it gets in the way. I can run it occasionally. 28 | 29 | The power-up and power-down timer values when not =0 must allow enough time for the Pi to shut down in an orderly manner. I have occasionally observed long shutdown times on a Pi. 30 | As the UPS has no idea of what the Pi is doing, whether it is finished shutting down or not, is quite unforgiving. It will just cut power to the Pi when either countdown timer reaches 0. 31 | 32 | I intend to run motioneye and VNC (server and client) on this Pi4. 33 | 34 | ------------------------------------------------------------ 35 | 36 | Now running on f/w v.7: 37 | 38 | Implemented a watchdog function with two different modes in upsPlus.py per suggestion from Nick Taterli. 39 | 40 | Setting either timer 0x18 or 0x1A to >=120 - I chose 180 - will make the UPS f/w count down to cutting the power to the Pi, unless upsPlus.py runs every minute. 41 | As long as upsPlus.py executes every 60 seconds (cron), the timer 0x18 or 0x1A will be reset to the starting value of >=120 and the Pi will keep running, because the timer never reaches 0. 42 | However, if the Pi freezes, upsPlus.py will stop updating the timer and the reboot timer in the UPS will eventually reach 0, which is when the UPS abruptly cuts the power. 43 | Register 0x19 needs to be =0 for this to work. Check the code and the comments in upsPlus.py. 44 | There are two modes: 45 | If 0x1A is set to >= 120, the UPS will reconnect the power after about 10 minutes and the Pi will restart! 46 | If on the other hand you choose to set 0x18 to >=120, there is no auto-restart. Start by pressing the UPS button. 47 | It's not useful to set both countdown timers. 48 | 49 | Voilá, a watchdog function! With or without auto-restart. 50 | 51 | Of course cutting the power like this can damage your file system, but then, what else is there to do if the OS freezes? 52 | 53 | You can test the watchdog by temporarily commenting out either 'putByte(0x1A, OMR0x1AD)' or 'putByte(0x18, OMR0x18D)' (currently around line 75) of upsPlus.py. You can then observe the countdown by running UPS_report.py repeatedly from a terminal until 0x18 or 0x1A reaches 0 (perhaps actually more like 5 seconds) and the power is abruptly cut. Make a clone of your OS beforehand, if you are worried that cutting the power abruptly may damage your OS. (I would advise using rpi-clone. It will not take much time to update your clone, as it uses rsync and will copy only the changes.) 54 | 55 | Running PowerCycle.py will also cause power-off and power-on after 10 minutes, but it will first do a shutdown of the OS before cutting the power. Make the power-off timer value large enough to allow your Pi to shutdown in an orderly manner, as the UPS has no clue about the state of the Pi or its OS. 56 | Thirty seconds may seem long enough, but in some circumstances a Pi might take longer. I prefer at least 60 seconds or more. 57 | The delay until restart used to be 5 seconds on f/w v.5, but it is now a surprising 10 minutes or so. The firmware developers at Geekpi may provide options to change this and make the button responsive during the 10 minute startup delay. 58 | 59 | During the ca. 10 minutes startup delay, my (Sanyo NCR18650GA 3450 mAh Li-Ion) batteries get charged to the full and also the last of the 4 blue LEDs stops blinking. 60 | 61 | --------------------------------------- 62 | There was an error in the exception handling which caused UPS_report to occasionally fail with the following: 63 | 64 | pi@RPI-HUB:~ $ python3 $HOME/UPS+/UPS_report.py 65 | *** Data from INA219 at 0x40: 66 | Raspberry Pi supply voltage: 4.920 V 67 | Raspberry Pi current consumption: 2.290 A 68 | Raspberry Pi power consumption: 10.700 W 69 | 70 | *** Data from INA219 at 0x45: 71 | Battery voltage: 4.130 V 72 | Battery current (discharging): 2.900 A 73 | Battery power consumption: 12.100 W 74 | 75 | i= 225 76 | Traceback (most recent call last): 77 | File "/home/pi/UPS+/UPS_report.py", line 80, in 78 | aReceiveBuf.append(bus.read_byte_data(DEVICE_ADDR, i)) 79 | File "/home/pi/.local/lib/python3.7/site-packages/smbus2/smbus2.py", line 433, in read_byte_data 80 | ioctl(self.fd, I2C_SMBUS, msg) 81 | TimeoutError: [Errno 110] Connection timed out 82 | 83 | During handling of the above exception, another exception occurred: 84 | 85 | Traceback (most recent call last): 86 | File "/home/pi/UPS+/UPS_report.py", line 84, in 87 | print('byte read=', aReceiveBuf[i], ' error:', e) 88 | IndexError: list index out of range 89 | pi@RPI-HUB:~ $ 90 | 91 | As the same code is used in upsPLus.py, I suspect this bug to also have caused an occasional failure to restart the Pi after a scheduled PowerCycle.py. 92 | The 'list index (to aReceiveBuf[i]) out of range' is obviously because byte 225 which was the subject of the attempted read, hadn't been added to the list aReceiveBuf, i.e. the list element with i=225 hadn't been created yet. Anyway, the remedy is, don't try to print what doesn't yet exist. 93 | 94 | TimeoutError exceptions are rare, but they do happen from time to time. My tests with the scripts called CatchExceptions proved that (to me). My attempts at exception handling are those of an amateur copycat ... 95 | 96 | Testing continues ... 97 | 98 | ----------------------------------- 99 | 100 | Through an oversight exception handling was missing from the putByte function in upsPlus.py, which must have been the reason that occasionally I observed (UPS_report) the countdown timer 0x18 counting down from 180 right past 120. Of course, after another execution of upsPlus.py this situation would be corrected, but it was odd. The recently observed TimeoutError in UPS_report.py already made it very clear that SMBus exception handling is essential in all cases. It should be mandatory, really. 101 | I added exception handling for TimeoutError to putByte() in upsPlus and the same putByte() definition is now also used in PowerCycle.py. 102 | 103 | Despite looking at the UPS_event.log a million times, upsPlus.py still had one wrong unit in its printed output (>>UPS_event.log ), viz. A for power. This was corrected. 104 | 105 | The restart delay after a 'watchdog with restart' event or a 'power cycle' is actually less than 10 minutes, more like 8-9 minutes. 106 | 107 | --------------- 108 | 109 | After more testing using a new test script CatchExceptions.py, I found that rather often a write to a register apparently doesn't succeed and it gets to keep its previous value. This, of course, is a recipe for trouble. 110 | If you run the said script on its own, keeping a terminal ready with upsPlus.py on the command line to return the register used in the test to normalcy, you can see for yourself, if your UPS exhibits write failures. 111 | I wrote failure, as they're not an 'exception' in the python sense. It's just that when you attempt to change the content of a register, it doesn't take. 112 | My prescription is to write a byte, read it back, compare the results and write again, if they're different until the write sticks, so to say. 113 | The bad write and failing comparison 'raise' a Value Error exception in the script. 114 | I have amended both my upsPlus.py and my PowerCycle.py scripts to hammer the byte until it sticks: 115 | 116 | # Write byte to specified I2C register address 'until it sticks'. 117 | def putByte(RA, wbyte): 118 | while True: 119 | try: 120 | with SMBus(DEVICE_BUS) as pbus: 121 | pbus.write_byte_data(DEVICE_ADDR, RA, wbyte) 122 | with SMBus(DEVICE_BUS) as gbus: 123 | rbyte = gbus.read_byte_data(DEVICE_ADDR, RA) 124 | if (wbyte) <= rbyte <= (wbyte): 125 | print("OK ", wbyte, rbyte) 126 | break 127 | else: 128 | raise ValueError 129 | except ValueError: 130 | print("Write:", wbyte, "!= Read:", rbyte, " Trying again") 131 | pass 132 | 133 | It is also noteworthy that writing to a timer register (0x18, 0x1A), when its old value is not 0 seems problematic. Such a register is constantly being decremented by the f/w, which may be why changing it to anything else than 0, proved not very reliable. So I added 'reset to 0' commands to my scripts whenever the ultimate intent is to write a value other than 0 and the current content could be anything. Probably sounds complicated, but I can now have the UPS run overnight and have it do a PowerCycle twice an hour and still find the Pi running in the morning (unless it has just started the powercycle). Restart initiated by PowerCycle.py takes ca. 10 minutes which cannot be controlled by the user. 134 | There's no telling how this all may change whenever there's a new f/w, of course. The jump from v.5 to v.7 brought a lengthening of the restart (elicited by timer 0x1A) from 5 seconds to 10 minutes, just like that. 135 | 136 | -------------------------------------------------------------------------------- /upsPlus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # adapted from scripts provided at GitHub: Geeekpi/upsplus by nickfox-taterli 5 | # ar - 19-05-2021 6 | 7 | # ''' UPS Plus v.5 control script ''' 8 | 9 | import os 10 | import time 11 | from smbus2 import SMBus 12 | from math import log10, floor 13 | from ina219 import INA219, DeviceRangeError 14 | from datetime import datetime, timezone 15 | 16 | # Essential UPS I2C power control registers are 0x18, 0x19 and 0x1A 17 | # (name format: Operation Mode, offset, purpose) 18 | # *** Default values *** 19 | # for normal operation, incl. optional watchdog timer as suggested by GeeekPi, 20 | # with or without automatic restart after 9-10 minutes. 21 | OMR0x18D = 0 # seconds, power-off no restart timer 22 | #OMR0x18D = 180 # seconds,power-off no restart timer (for watchdog set >=120) 23 | OMR0x19D = 0 # boolean, automatic restart (1) or not (0) after ext. power failure 24 | #OMR0x1AD = 0 # seconds, power-off with restart timer 25 | OMR0x1AD = 180 # seconds, power-off with restart timer (for watchdog set >=120, 10 min auto-restart) 26 | 27 | # *** Power failure shutdown event values *** 28 | OMR0x18S = 60 # seconds, power-off timer without restart 29 | OMR0x19S = 1 # boolean, automatic restart (1) or not (0) after ext. power failure 30 | OMR0x1AS = 0 # seconds, power-off with restart timer 31 | 32 | # Set I2C bus 33 | DEVICE_BUS = 1 34 | 35 | # Set device I2C slave address 36 | DEVICE_ADDR = 0x17 37 | 38 | # Set threshold for UPS automatic power-off to prevent 39 | # destroying the batteries by excessive discharge (unit: mV). 40 | # DISCHARGE_LIMIT (a.k.a. protection voltage) will be stored in memory at 0x11-0x12 41 | DISCHARGE_LIMIT = 2500 # for Sanyo NCR18650GA 3450 mAh Li-Ion batteries 42 | 43 | # Set threshold for UPS power-off conserving battery power & 44 | # providing ability to overcome possibly repeated blackouts (unit: mV). 45 | POWEROFF_LIMIT = 3500 46 | # POWEROFF_LIMIT = DISCHARGE_LIMIT 47 | 48 | # Keep Pi running for maximum 'units' of time after blackout 49 | # (unit = upsPlus.py cron job interval, normally 1 min) 50 | # May be cut short by batteries becoming discharged too much. 51 | GRACE_TIME = 1440 52 | GRACE_TIME = 5 53 | # Minimum practical value is 2 ... 54 | GRACE_TIME = max(GRACE_TIME, 2) 55 | 56 | # Path for parameter files 57 | PATH = str(os.getenv('HOME'))+'/UPS+/' 58 | 59 | # Record starting time 60 | StartTime = datetime.now(timezone.utc).astimezone() 61 | TimeStampA = '{:%d-%m-%Y %H:%M:%S}'.format(StartTime) 62 | TimeStampB = '{:%Y-%m-%d_%H:%M:%S}'.format(StartTime) 63 | 64 | # Rounding to n significant digits 65 | def round_sig(x, n=3): 66 | if not x: 67 | return 0 68 | power = -floor(log10(abs(x))) + (n - 1) 69 | factor = (10 ** power) 70 | return round(x * factor) / factor 71 | 72 | # Write byte to specified I2C register address 'until it sticks'. 73 | def putByte(RA, wbyte): 74 | while True: 75 | try: 76 | with SMBus(DEVICE_BUS) as pbus: 77 | pbus.write_byte_data(DEVICE_ADDR, RA, wbyte) 78 | with SMBus(DEVICE_BUS) as gbus: 79 | rbyte = gbus.read_byte_data(DEVICE_ADDR, RA) 80 | if (wbyte) <= rbyte <= (wbyte): 81 | print("OK ", wbyte, rbyte) 82 | break 83 | else: 84 | raise ValueError 85 | except ValueError: 86 | print("Write:", wbyte, "!= Read:", rbyte, " Trying again") 87 | pass 88 | 89 | # Unset = stop power timers 90 | putByte(0x18, 0) 91 | putByte(0x1A, 0) 92 | 93 | # Initialize UPS power control registers 94 | putByte(0x18, OMR0x18D) 95 | putByte(0x19, OMR0x19D) 96 | putByte(0x1A, OMR0x1AD) 97 | 98 | # Save POWEROFF_LIMIT to text file for sharing with other scripts 99 | f = open(PATH+'PowerOffLimit.txt', 'w') 100 | f.write("%s" % POWEROFF_LIMIT) 101 | f.write("\n") 102 | f.close() 103 | 104 | # Store DISCHARGE_LIMIT (a.k.a. protection voltage) in memory at 0x11-0x12 105 | putByte(0x11, DISCHARGE_LIMIT & 0xFF) 106 | putByte(0x12, (DISCHARGE_LIMIT >> 0o10) & 0xFF) 107 | 108 | # Create instance of INA219 and extract information 109 | ina = INA219(0.00725, busnum=DEVICE_BUS, address=0x40) 110 | ina.configure() 111 | print("="*60) 112 | print(("------ {:^46s} ------").format(TimeStampA)) 113 | print(("------ {:^46s} ------").format("Power supplied to Raspberry Pi")) 114 | print("-"*60) 115 | print(("{:<50s}{:>8.3f}{:>2s}").format("Raspberry Pi supply voltage:", 116 | round_sig(ina.voltage(), n=3), " V")) 117 | print(("{:<50s}{:>8.3f}{:>2s}").format("Raspberry Pi current consumption:", 118 | round_sig(ina.current()/1000, n=3), 119 | " A")) 120 | print(("{:<50s}{:>8.3f}{:>2s}").format("Raspberry Pi power consumption:", 121 | round_sig(ina.power()/1000, n=3), " W")) 122 | print("-"*60) 123 | 124 | # Battery information 125 | ina = INA219(0.005, busnum=DEVICE_BUS, address=0x45) 126 | ina.configure() 127 | print(("------ {:^46s} ------").format("UPS Plus batteries")) 128 | print("-"*60) 129 | print(("{:<50s}{:>8.3f}{:>2s}").format("Battery voltage (from INA219):", 130 | round_sig(ina.voltage(), n=3), " V")) 131 | try: 132 | if ina.current() > 0: 133 | print(("{:<50s}{:>8.3f}{:>2s}"). 134 | format("Battery current (charging):", 135 | abs(round_sig(ina.current()/1000, n=2)), " A")) 136 | print(("{:<50s}{:>8.3f}{:>2s}"). 137 | format("Power supplied to the batteries:", 138 | round_sig(ina.power()/1000, n=3), " W")) 139 | else: 140 | print(("{:<50s}{:>8.3f}{:>2s}"). 141 | format("Battery current (discharging):", 142 | abs(round_sig(ina.current()/1000, n=2)), " A")) 143 | print(("{:<50s}{:>8.3f}{:>2s}"). 144 | format("Power supplied by the batteries:", 145 | round_sig(ina.power()/1000, n=3), " W")) 146 | except DeviceRangeError: 147 | print("-"*60) 148 | print('INA219: Out of Range Warning!') 149 | print('BATTERY CURRENT POSSIBLY EXCEEDING SAFE LIMITS!') 150 | # Keep sampling in case of another type of error 151 | except: 152 | pass 153 | finally: 154 | print("-"*60) 155 | 156 | aReceiveBuf = [] 157 | aReceiveBuf.append(0x00) 158 | 159 | i = 0x01 160 | while i < 0x100: 161 | try: 162 | with SMBus(DEVICE_BUS) as bus: 163 | aReceiveBuf.append(bus.read_byte_data(DEVICE_ADDR, i)) 164 | i += 1 165 | except TimeoutError as e: 166 | # print('i=', i, ' - error:', e) 167 | time.sleep(0.1) 168 | 169 | print() 170 | UID0 = "%08X" % (aReceiveBuf[0xF3] << 0o30 | aReceiveBuf[0xF2] << 0o20 | 171 | aReceiveBuf[0xF1] << 0o10 | aReceiveBuf[0xF0]) 172 | UID1 = "%08X" % (aReceiveBuf[0xF7] << 0o30 | aReceiveBuf[0xF6] << 0o20 | 173 | aReceiveBuf[0xF5] << 0o10 | aReceiveBuf[0xF4]) 174 | UID2 = "%08X" % (aReceiveBuf[0xFB] << 0o30 | aReceiveBuf[0xFA] << 0o20 | 175 | aReceiveBuf[0xF9] << 0o10 | aReceiveBuf[0xF8]) 176 | print(("{:^60s}").format('UID: ' + UID0 + '-' + UID1 + '-' + UID2)) 177 | print('*'*60) 178 | 179 | print(("{:^60s}").format("UPS power control registers: " 180 | + "0x18=" + str(aReceiveBuf[0x18]) 181 | + " / 0x19=" + str(aReceiveBuf[0x19]) 182 | + " / 0x1A=" + str(aReceiveBuf[0x1A]))) 183 | 184 | print() 185 | # Update initial GRACE_TIME value to file whenever external power is present 186 | if ((aReceiveBuf[0x08] << 0o10 | aReceiveBuf[0x07]) > 4000) | \ 187 | ((aReceiveBuf[0x0A] << 0o10 | aReceiveBuf[0x09]) > 4000): 188 | f = open(PATH+'GraceTime.txt', 'w') 189 | f.write("%s" % GRACE_TIME) 190 | f.close() 191 | 192 | if (aReceiveBuf[0x08] << 0o10 | aReceiveBuf[0x07]) > 4000: 193 | print(("{:^60s}").format('Charging via USB type C connector\n')) 194 | print(("{:^60s}"). 195 | format("If a power failure lasts longer than ca. " + 196 | str(GRACE_TIME)+" min,")) 197 | print(("{:^60s}"). 198 | format("the UPS will halt the OS and then power the Pi off.")) 199 | print('*'*60, "\n") 200 | elif (aReceiveBuf[0x0A] << 0o10 | aReceiveBuf[0x09]) > 4000: 201 | print(("{:^60s}").format('Charging via micro USB connector\n')) 202 | print(("{:^60s}"). 203 | format("If a power failure lasts longer than ca. " + 204 | str(GRACE_TIME)+" min,")) 205 | print(("{:^60s}"). 206 | format("the UPS will halt the OS and then power the Pi off.")) 207 | else: 208 | # Read GRACE_TIME from file, decrease by 1 and write back to file 209 | f = open(PATH+'GraceTime.txt', 'r') 210 | GRACE_TIME = int(f.read())-1 211 | f.close() 212 | 213 | print(("*** {:^52s} ***"). 214 | format("EXTERNAL POWER LOST! RUNNING ON BATTERY POWER!")) 215 | print(("*** {:^52s} ***").format(" ")) 216 | print(("*** {:^52s} ***"). 217 | format("UPS set to power-off at: " + 218 | str(round_sig(float(POWEROFF_LIMIT)/1000, n=3)) + " V")) 219 | print(("*** {:^52s} ***"). 220 | format("Battery deep discharge limit set at: " + 221 | str(round_sig(float(DISCHARGE_LIMIT)/1000, n=3)) + " V")) 222 | print(("*** {:^52s} ***"). 223 | format("Grace time left till shutdown: " + str(GRACE_TIME) + " min")) 224 | print('*'*60, "\n") 225 | 226 | # After loss of external power buffered data saving & 227 | # subsequent shutdown of the Pi followed by UPS' power down 228 | # will be initiated on one or more of the following conditions: 229 | # 1. Expiry of the grace period, 230 | # 2. The battery voltage dropped below 200 mV above discharge protection 231 | # limit, or 232 | # 3. The battery voltage dropped below the UPS power-off voltage limit. 233 | # The script will set the UPS' power down timer initiating 234 | # a UPS' power down, which allows the Pi time to save buffered data 235 | # and halt. 236 | while True: 237 | try: 238 | INA_VOLTAGE = ina.voltage() 239 | # Catch erroneous battery voltage value 240 | if INA_VOLTAGE == 0: 241 | raise ValueError 242 | break 243 | # Keep sampling in case of an erroneous value or exception 244 | except ValueError: 245 | time.sleep(0.1) 246 | except: 247 | pass 248 | 249 | if ( 250 | (GRACE_TIME <= 0) or 251 | ((INA_VOLTAGE * 1000) <= max((DISCHARGE_LIMIT + 200), 252 | POWEROFF_LIMIT)) 253 | ): 254 | print('#'*60) 255 | print('External power to the UPS has been lost,') 256 | print('the power lost grace period expired, or the battery voltage') 257 | print('either dropped below the UPS\' power-off voltage limit,') 258 | print('or is about to drop below the deep discharge limit ...') 259 | print('Shutting down the OS & powering the Pi off ...') 260 | 261 | # Set UPS power down without restart timer (unit: seconds) 262 | # allowing the Pi time to sync & shutdown. 263 | putByte(0x18, OMR0x18S) 264 | 265 | # Enable/disable automatic restart on return of external power 266 | putByte(0x19, OMR0x19S) 267 | 268 | # Set UPS power down with restart timer (unit: seconds) 269 | # allowing the Pi time to sync & shutdown. 270 | putByte(0x1A, OMR0x1AS) 271 | 272 | # UPS will cut power to the Pi after the UPS' power down 273 | # timer period has expired allowing the Pi to sync and then halt 274 | # in an orderly manner. 275 | os.system("sudo shutdown now") 276 | # Script continues executing, indefinitely as it were, 277 | # until it is killed by the Pi shutting down. 278 | while True: 279 | time.sleep(10) 280 | # Control now passes to the UPS' F/W and MCU ... 281 | # Otherwise update GRACE_TIME on file and end script execution. 282 | else: 283 | f = open(PATH+'GraceTime.txt', 'w') 284 | f.write("%s" % (GRACE_TIME)) 285 | f.close() 286 | 287 | # EOF 288 | -------------------------------------------------------------------------------- /fanShutDownUps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (C) 2021 frtz13.github.com 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | # fan control function: thanks to Andreas Spiess https://www.sensorsiot.org/variable-speed-cooling-fan-for-raspberry-pi-using-pwm-video138/ 20 | # mqtt publishing : many thanks to http://www.steves-internet-guide.com/into-mqtt-python-client/ 21 | # UPSPlus: https://github.com/geeekpi/upsplus 22 | 23 | # With modifications to some parameters by ar - 31-05-2021, 22-07-2021 (fan control enabled and modified; merged with latest version from GitHub) 24 | # 27-07-2021, 29-07-2021 (force fixed fan speed of 100% for the Ice Tower fan) 25 | # 31-07-2021 (add busnum=DEVICE_BUS), 06-08-2021, 26 | # 12-08-2021 (set target temp to 25 degrees -> fan runs at 100% duty cycle) 27 | # 14-08-2021 (merged with Frtz13's latest version), 15-08-2021, 19-08-2021, 28 | # 06-09-2021 (merged with Frtz13's latest version) 29 | # 07-09-2021 (merged with Frtz13's latest version) 30 | # 14-09-2021 31 | # 07-04-2022 (merged with Frtz13's latest version) 32 | # 09-05-2022, 10-05-2022, 14-05-2022 (ar: modified INA219 parameters) 33 | # 23-05-2022, 06-06-2022 (INA219 & PI controller parameters) 34 | # 35 | # Merged with update(s) by frtz13 (on laptop using Kompare) 36 | 37 | import locale 38 | # Set to Dutch locale to get comma decimal separator 39 | #locale.setlocale(locale.LC_NUMERIC, 'nl_NL.UTF-8') 40 | # Set to system default locale (enable number formatting using decimal separator for the locale) 41 | locale.setlocale(locale.LC_ALL, '') 42 | 43 | import os 44 | import time 45 | from time import sleep 46 | # import logging 47 | # import signal 48 | import sys 49 | import json 50 | import configparser 51 | # import random 52 | from collections import deque 53 | import syslog 54 | import requests 55 | import itertools as it 56 | 57 | import RPi.GPIO as GPIO 58 | import smbus2 59 | from ina219 import INA219,DeviceRangeError 60 | import paho.mqtt.client as mqtt 61 | 62 | SCRIPT_VERSION = "20220110" 63 | 64 | CONFIG_FILE = "fanShutDownUps.ini" 65 | CONFIGSECTION_FAN = "fan" 66 | CONFIGSECTION_MQTT = "mqtt" 67 | CONFIGSECTION_UPS = "ups" 68 | 69 | CMD_NO_TIMER_BIAS = "--notimerbias" 70 | CMD_SHUTDOWN_IMMEDIATELY_WHEN_ON_BATTERY = "--shutdowntest" 71 | 72 | DEVICE_BUS = 1 # Define I2C bus 73 | DEVICE_ADDR = 0x17 # Define device i2c slave address. 74 | 75 | MQTT_TOPIC_FAN = "/fanspeed" 76 | MQTT_TOPIC_UPS = "/ups" 77 | MQTT_TOPIC_LWT = "/LWT" 78 | MQTT_PAYLOAD_ONLINE = "online" 79 | MQTT_PAYLOAD_OFFLINE = "offline" 80 | 81 | 82 | def read_config_desired_cpu_temp(): 83 | global DESIRED_CPU_TEMP 84 | confparser = configparser.RawConfigParser() 85 | confparser.read(os.path.join(sys.path[0], CONFIG_FILE)) 86 | DESIRED_CPU_TEMP = int(confparser.get(CONFIGSECTION_FAN, "DESIRED_CPU_TEMP_degC")) 87 | 88 | 89 | def read_config(): 90 | global GPIO_FAN # The GPIO pin ID to control the fan 91 | global FAN_LOOP_TIME 92 | global FANSPEED_FILENAME 93 | global SEND_STATUS_TO_UPSPLUS_IOT_PLATFORM 94 | global UPSPLUS_IOT_PLATFORM_URL 95 | global BATT_LOOP_TIME 96 | global TIMER_BIAS_AT_STARTUP 97 | global SHUTDOWN_IMMEDIATELY_WHEN_ON_BATTERY # for testing shutdown only 98 | global SHUTDOWN_TIMEOUT 99 | global PROTECTION_VOLTAGE_MARGIN_mV 100 | global MQTT_BROKER 101 | global MQTT_PORT 102 | global MQTT_USERNAME 103 | global MQTT_PASSWORD 104 | global MQTT_TOPIC 105 | 106 | try: 107 | confparser = configparser.RawConfigParser() 108 | confparser.read(os.path.join(sys.path[0], CONFIG_FILE)) 109 | 110 | GPIO_FAN = int(confparser.get(CONFIGSECTION_FAN, "GPIO_FAN")) 111 | FAN_LOOP_TIME = int(confparser.get(CONFIGSECTION_FAN, "FAN_LOOP_TIME_s")) 112 | read_config_desired_cpu_temp() 113 | 114 | SEND_STATUS_TO_UPSPLUS_IOT_PLATFORM = 1 == int(confparser.get(CONFIGSECTION_UPS, "SEND_STATUS_TO_UPSPLUS_IOT_PLATFORM")) 115 | try: 116 | UPSPLUS_IOT_PLATFORM_URL = confparser.get(CONFIGSECTION_UPS, "UPSPLUS_IOT_PLATFORM_URL") 117 | except: 118 | UPSPLUS_IOT_PLATFORM_URL = "https://api.52pi.com/feed" 119 | 120 | BATT_LOOP_TIME = int(confparser.get(CONFIGSECTION_UPS, "BATTERY_CHECK_LOOP_TIME_s")) 121 | if CMD_NO_TIMER_BIAS in sys.argv: 122 | TIMER_BIAS_AT_STARTUP = 0 123 | print("Timer bias at start-up set to 0") 124 | else: 125 | try: 126 | TIMER_BIAS_AT_STARTUP = -int(confparser.get(CONFIGSECTION_UPS, "TIMER_BIAS_AT_STARTUP")) 127 | except: 128 | TIMER_BIAS_AT_STARTUP = -300 + BATT_LOOP_TIME 129 | 130 | if CMD_SHUTDOWN_IMMEDIATELY_WHEN_ON_BATTERY in sys.argv: 131 | SHUTDOWN_IMMEDIATELY_WHEN_ON_BATTERY = True 132 | else: 133 | try: 134 | SHUTDOWN_IMMEDIATELY_WHEN_ON_BATTERY = (1 == int(confparser.get(CONFIGSECTION_UPS, "SHUTDOWN_IMMEDIATELY_WHEN_ON_BATTERY"))) 135 | except: 136 | SHUTDOWN_IMMEDIATELY_WHEN_ON_BATTERY = False 137 | if SHUTDOWN_IMMEDIATELY_WHEN_ON_BATTERY: 138 | print("Will immediately shut down when UPS is on battery.") 139 | SHUTDOWN_TIMEOUT = int(confparser.get(CONFIGSECTION_UPS, "SHUTDOWN_TIMEOUT_s")) 140 | 141 | parm = "PROTECTION_VOLTAGE_MARGIN_mV" 142 | PROTECTION_VOLTAGE_MARGIN_mV = int(confparser.get(CONFIGSECTION_UPS, parm)) 143 | protVmargin_mini_mV = 100 144 | protVmargin_maxi_mV = 500 145 | if PROTECTION_VOLTAGE_MARGIN_mV < protVmargin_mini_mV: 146 | print("{} set to {:.0f} mV".format(parm, protVmargin_mini_mV)) 147 | PROTECTION_VOLTAGE_MARGIN_mV = protVmargin_mini_mV 148 | if PROTECTION_VOLTAGE_MARGIN_mV > protVmargin_maxi_mV: 149 | print("{} set to {:.0f} mV".format(parm, protVmargin_maxi_mV)) 150 | PROTECTION_VOLTAGE_MARGIN_mV = protVmargin_maxi_mV 151 | 152 | MQTT_BROKER = confparser.get(CONFIGSECTION_MQTT, "BROKER") 153 | MQTT_PORT = int(confparser.get(CONFIGSECTION_MQTT, "TCP_PORT")) 154 | MQTT_USERNAME = confparser.get(CONFIGSECTION_MQTT, "USERNAME") 155 | MQTT_PASSWORD = confparser.get(CONFIGSECTION_MQTT, "PASSWORD") 156 | MQTT_TOPIC = confparser.get(CONFIGSECTION_MQTT, "TOPIC") 157 | return True 158 | except Exception as e: 159 | errmsg = "Error when reading configuration parameters: " + str(e) 160 | print(errmsg) 161 | syslog.syslog(syslog.LOG_ERR, errmsg) 162 | return False 163 | 164 | 165 | def on_MQTTconnect(client, userdata, flags, rc): 166 | client.connection_rc = rc 167 | if rc == 0: 168 | client.connected_flag = True 169 | # print("connected OK") 170 | try: 171 | client.publish(MQTT_TOPIC + MQTT_TOPIC_LWT, MQTT_PAYLOAD_ONLINE, 0, retain=True) 172 | except: 173 | pass 174 | else: 175 | errMsg = { 176 | 1: "Connection refused – incorrect protocol version", 177 | 2: "Connection refused – invalid client identifier", 178 | 3: "Connection refused – server unavailable", 179 | 4: "Connection refused – bad username or password", 180 | 5: "Connection not authorized" 181 | } 182 | errMsgFull = "Connection to MQTT broker failed. " + errMsg.get(rc, f"Unknown error: {str(rc)}.") 183 | print(errMsgFull) 184 | syslog.syslog(syslog.LOG_ERR, errMsgFull) 185 | 186 | def on_MQTTdisconnect(client, userdata, rc): 187 | # print("disconnecting reason " + str(rc)) 188 | client.connected_flag = False 189 | 190 | def MQTT_connect(client): 191 | client.on_connect = on_MQTTconnect 192 | client.on_disconnect = on_MQTTdisconnect 193 | client.will_set(MQTT_TOPIC + MQTT_TOPIC_LWT, MQTT_PAYLOAD_OFFLINE, 0, retain=True) 194 | if len(MQTT_USERNAME) > 0: 195 | client.username_pw_set(username=MQTT_USERNAME, password=MQTT_PASSWORD) 196 | print("Connecting to broker ", MQTT_BROKER) 197 | try: 198 | client.connect(MQTT_BROKER, MQTT_PORT) #connect to broker 199 | client.loop_start() 200 | except Exception as e: 201 | print("connection failed: " + str(e)) 202 | return False 203 | timeout = time.time() + 5 204 | while client.connection_rc == -1: #wait in loop 205 | if time.time() > timeout: 206 | break 207 | time.sleep(1) 208 | if client.connected_flag: 209 | return True 210 | else: 211 | return False 212 | 213 | def MQTT_terminate(client): 214 | try: 215 | if client.connected_flag: 216 | res = MQTT_client.publish(MQTT_TOPIC + MQTT_TOPIC_LWT, MQTT_PAYLOAD_OFFLINE, 0, retain=True) 217 | # if res[0] == 0: 218 | # print("mqtt go offline ok") 219 | MQTT_client.disconnect() 220 | sleep(1) 221 | client.loop_stop() 222 | except Exception as e: 223 | print("MQTT client terminated with exception: " + str(e)) 224 | pass 225 | 226 | def i2c_bus_read_byte_wait(devaddr, reg, delay_s): 227 | sleep(delay_s) 228 | return i2c_bus.read_byte_data(devaddr, reg) 229 | 230 | class UPSPlus: 231 | def __init__(self, upsCurrent): 232 | self._send_status_data_reply = "" 233 | # get battery status 234 | try: 235 | self._battery_voltage_V = inaBattery.voltage() 236 | try: 237 | self._battery_current_mA = -inaBattery.current() # positive: discharge current 238 | except DeviceRangeError: 239 | self._battery_current_mA = 16000 240 | except Exception as exc: 241 | raise Exception("[UPSPlus.init] Error reading inaBatt registers: " + str(exc)) 242 | self._battery_current_avg_mA = upsCurrent.battery_current_avg_mA 243 | self._battery_power_avg_mW = upsCurrent.battery_power_avg_mW 244 | # get output status 245 | try: 246 | self._RPi_voltage_V = inaRPi.voltage() 247 | try: 248 | self._RPi_current_mA = inaRPi.current() 249 | except DeviceRangeError: 250 | self._RPi_current_mA = 16000 251 | except Exception as exc: 252 | raise Exception("[UPSPlus.init] Error reading inaRPi registers: " + str(exc)) 253 | self._RPi_current_avg_mA = upsCurrent.out_current_avg_mA 254 | self._RPi_power_avg_mW = upsCurrent.out_power_avg_mW 255 | self._RPi_current_peak_mA = upsCurrent.out_current_peak_mA 256 | self._RPi_voltage_mini_V = upsCurrent.out_voltage_mini_V 257 | 258 | # we only read the full set of registers if we report back to the IOT Platform 259 | # otherwise just read the register values we actually use 260 | if SEND_STATUS_TO_UPSPLUS_IOT_PLATFORM: 261 | it_registers = it.chain(range(0x01,0x2A), range(0xF0,0xFC)) 262 | else: 263 | it_registers = it.chain(range(0x07,0x0C), range(0x11, 0x15)) 264 | try: 265 | # pick one of the following lines (with or without delay) 266 | # self._reg_buff = {i:i2c_bus.read_byte_data(DEVICE_ADDR, i) for i in it_registers} 267 | self._reg_buff = {i:i2c_bus_read_byte_wait(DEVICE_ADDR, i, 0.02) for i in it_registers} 268 | except Exception as exc: 269 | raise Exception("[UPSPlus.init] Error reading UPS registers: " + str(exc)) 270 | 271 | self._USB_C_mV = self._reg_buff[0x08] << 8 | self._reg_buff[0x07] 272 | self._USB_micro_mV = self._reg_buff[0x0A] << 8 | self._reg_buff[0x09] 273 | # self.battery_temperature_degC = self.reg_buff[12] << 8 | self.reg_buff[11] 274 | # we very rarely get 0xFF at reg_buff[0x0C]. this value should be 0 anyway for realistic temperatures 275 | BATT_TEMP_CEILING = 70 # sometimes unrealistic values spoil my graphs 276 | self._battery_temperature_degC = min(self._reg_buff[0x0B], BATT_TEMP_CEILING) 277 | # self._battery_remaining_capacity_percent = self._reg_buff[0x14] << 8 | self._reg_buff[0x13] 278 | # remaining capacity always <= 100% 279 | BATT_CAPACITY_CEILING = 101 280 | self._battery_remaining_capacity_percent = min(self._reg_buff[0x13], BATT_CAPACITY_CEILING) 281 | 282 | @property 283 | def protection_voltage_mV(self): 284 | prot_voltage_mV = self._reg_buff[0x12] << 8 | self._reg_buff[0x11] 285 | # protection voltage should be between 3000 and (4000 - margin) mV. 286 | # the UPS firmware is supposed to shut down RPi power when battery voltage goes lower than the Battery Protection Voltage. 287 | # so we have to make sure to gracefully shut down the RPi before we reach this level. 288 | # the script will shut down the RPi at Battery Protection Voltage + _Margin (config. parameter). 289 | # Battery Protection Voltage checks: 290 | # lower bound: make sure that the script will shut down the RPi when battery voltage comes close to the 3000 mV limit. 291 | # Below such battery voltage the charger circuit (IP5328) will go into low current charge mode, 292 | # and charging current won't be sufficient to power the RPi. 293 | # upper bound: make sure the script will not shut down the RPi with a (nearly) full battery 294 | if prot_voltage_mV >= 3000 and prot_voltage_mV <= (4000 - PROTECTION_VOLTAGE_MARGIN_mV) : 295 | pass 296 | else: 297 | PROTECT_VOLT_DEFAULT_mV = 3500 298 | errMsg = f"Protection voltage retrieved from UPS seems to have an incorrect value ({prot_voltage_mV:.0f} mV). "\ 299 | f"Assumed to be {PROTECT_VOLT_DEFAULT_mV:.0f} mV" 300 | print(errMsg) 301 | syslog.syslog(syslog.LOG_WARNING, errMsg) 302 | prot_voltage_mV = PROTECT_VOLT_DEFAULT_mV 303 | return prot_voltage_mV 304 | 305 | @property 306 | def on_battery(self): 307 | # with previous firmwares (earlier than v. 10) it seemed more reliable to check discharging current. 308 | # USB-C and micro-USB voltages are sometimes not reported properly 309 | return self._battery_current_avg_mA > 500 # positive current means battery is discharging 310 | # return (self.UsbC_mV < 4000) and (self.UsbMicro_mV < 4000) 311 | 312 | @property 313 | def battery_is_charging(self): 314 | return self._battery_current_avg_mA < 0 315 | 316 | @property 317 | def battery_voltage_V(self): 318 | return self._battery_voltage_V 319 | 320 | def MQTT_publish(self, mqttclient): 321 | dictPayload = { 322 | 'UsbC_V': self._USB_C_mV / 1000, 323 | 'UsbMicro_V': self._USB_micro_mV / 1000, 324 | 'OnBattery': self.on_battery, 325 | 'BatteryVoltage_V': self._battery_voltage_V, 326 | 'BatteryCurrent_mA': int(self._battery_current_mA), 327 | 'BatteryCurrent_avg_mA': int(self._battery_current_avg_mA), 328 | 'BatteryPower_avg_mW': int(self._battery_power_avg_mW), 329 | 'BatteryCharging': self.battery_is_charging, 330 | 'BatteryRemainingCapacity_percent': self._battery_remaining_capacity_percent, 331 | 'BatteryTemperature_degC': self._battery_temperature_degC, 332 | 'OutputVoltage_V': self._RPi_voltage_V, 333 | 'OutputVoltage_mini_V': self._RPi_voltage_mini_V, 334 | 'OutputCurrent_mA': int(self._RPi_current_mA), 335 | 'OutputCurrent_avg_mA': int(self._RPi_current_avg_mA), 336 | 'OutputPower_avg_mW': int(self._RPi_power_avg_mW), 337 | 'OutputCurrent_peak_mA': int(self._RPi_current_peak_mA), 338 | } 339 | if SEND_STATUS_TO_UPSPLUS_IOT_PLATFORM: 340 | dictPayload['UPSPlus_IOT_Platform_Reply'] = self._send_status_data_reply 341 | payload = json.dumps(dictPayload) 342 | try: 343 | res = mqttclient.publish(MQTT_TOPIC + MQTT_TOPIC_UPS, payload, 0, False) 344 | except: 345 | pass 346 | # if res[0] == 0: 347 | # print("publishing successful: " + payload) 348 | # else: 349 | # print("publishing failed. result: " + str(res.result)) 350 | 351 | def send_UPS_status_data(self): 352 | global send_status_data_warncount 353 | # time.sleep(random.randint(0, 3)) 354 | tel_data = { 355 | 'PiVccVolt': self._RPi_voltage_V, 356 | 'PiIddAmps': self._RPi_current_mA, 357 | 'BatVccVolt': self._battery_voltage_V, 358 | 'BatIddAmps': -self._battery_current_mA, 359 | 'McuVccVolt': self._reg_buff[2] << 8 | self._reg_buff[1], 360 | 'BatPinCVolt': self._reg_buff[6] << 8 | self._reg_buff[5], 361 | 'ChargeTypeCVolt': self._reg_buff[8] << 8 | self._reg_buff[7], 362 | 'ChargeMicroVolt': self._reg_buff[10] << 8 | self._reg_buff[9], 363 | 'BatTemperature': self._reg_buff[12] << 8 | self._reg_buff[11], 364 | 'BatFullVolt': self._reg_buff[14] << 8 | self._reg_buff[13], 365 | 'BatEmptyVolt': self._reg_buff[16] << 8 | self._reg_buff[15], 366 | 'BatProtectVolt': self._reg_buff[18] << 8 | self._reg_buff[17], 367 | 'SampleTime': self._reg_buff[22] << 8 | self._reg_buff[21], 368 | 'AutoPowerOn': self._reg_buff[25], 369 | 'OnlineTime': (self._reg_buff[31] << 24 | self._reg_buff[30] << 16 370 | | self._reg_buff[29] << 8 | self._reg_buff[28]), 371 | 'FullTime': (self._reg_buff[35] << 24 | self._reg_buff[34] << 16 372 | | self._reg_buff[33] << 8 | self._reg_buff[32]), 373 | 'OneshotTime': (self._reg_buff[39] << 24 | self._reg_buff[38] << 16 374 | | self._reg_buff[37] << 8 | self._reg_buff[36]), 375 | 'Version': self._reg_buff[41] << 8 | self._reg_buff[40], 376 | 'UID0': "%08X" % (self._reg_buff[243] << 24 | self._reg_buff[242] << 16 377 | | self._reg_buff[241] << 8 | self._reg_buff[240]), 378 | 'UID1': "%08X" % (self._reg_buff[247] << 24 | self._reg_buff[246] << 16 379 | | self._reg_buff[245] << 8 | self._reg_buff[244]), 380 | 'UID2': "%08X" % (self._reg_buff[251] << 24 | self._reg_buff[250] << 16 381 | | self._reg_buff[249] << 8 | self._reg_buff[248]), 382 | } 383 | print(tel_data) 384 | try: 385 | r = requests.post(UPSPLUS_IOT_PLATFORM_URL, data=tel_data) 386 | # print(r.text) 387 | # json data will be formatted with double quotes, so replace them 388 | self._send_status_data_reply = r.text.replace('"', "'") 389 | send_status_data_warncount = 0 390 | except Exception as e: 391 | # print("sending UPS status data failed: " + str(e)) 392 | warncount = 10 393 | self._send_status_data_reply = "sending UPS status data failed: " + str(e) 394 | if send_status_data_warncount == warncount: 395 | errmsg = f"sending UPS status data failed {warncount} times: " + str(e) 396 | syslog.syslog(syslog.LOG_ERR, errmsg) 397 | print(errmsg) 398 | if send_status_data_warncount <= warncount: 399 | send_status_data_warncount += 1 400 | 401 | 402 | class UPSVoltageCurrent: 403 | 404 | def __init__(self): 405 | self._batt_current = deque([]) 406 | self._batt_power = deque([]) 407 | self._out_current = deque([]) 408 | self._out_power = deque([]) 409 | self._min_RPi_voltage = 6 410 | self._max_out_current = 0 411 | self._can_warn = 0 412 | self._arrLength = 2 * BATT_LOOP_TIME 413 | 414 | def add_value(self): 415 | had_warning = False 416 | had_exception = False 417 | # get measurements and handle i2c bus exceptions 418 | try: 419 | battcurr = -inaBattery.current() 420 | battvolt = inaBattery.voltage() 421 | except Exception as exc: 422 | had_exception = True 423 | if self._can_warn == 0: 424 | syslog.syslog(syslog.LOG_ERR, "[C_UpsCurrent.add_value] Error reading inaBatt registers: " + str(exc)) 425 | had_warning = True 426 | try: 427 | outcurr = inaRPi.current() 428 | outvolt = inaRPi.voltage() 429 | except Exception as exc: 430 | had_exception = True 431 | if self._can_warn == 0: 432 | syslog.syslog(syslog.LOG_ERR, "[C_UpsCurrent.add_value] Error reading inaRPi registers: " + str(exc)) 433 | had_warning = True 434 | # in case we get repeated errors on the i2c bus, we make sure we do not get a warning about this every second 435 | if had_warning: 436 | self._can_warn = 60 # send a warning once a minute at most 437 | else: 438 | if self._can_warn > 0: 439 | self._can_warn -= 1 440 | if had_exception: 441 | return 442 | # put measures into arrays 443 | if len(self._batt_current) >= self._arrLength: 444 | self._batt_current.popleft() 445 | self._batt_current.append(battcurr) 446 | if len(self._batt_power) >= self._arrLength: 447 | self._batt_power.popleft() 448 | self._batt_power.append(battcurr * battvolt) # we do not use the ina.power() function as it always returns positive 449 | if len(self._out_current) >= self._arrLength: 450 | self._out_current.popleft() 451 | self._out_current.append(outcurr) 452 | if self._max_out_current < outcurr: 453 | self._max_out_current = outcurr 454 | if len(self._out_power) >= self._arrLength: 455 | self._out_power.popleft() 456 | self._out_power.append(outcurr * outvolt) 457 | if outvolt < self._min_RPi_voltage: 458 | self._min_RPi_voltage = outvolt 459 | 460 | @property 461 | def battery_current_avg_mA(self): 462 | if len(self._batt_current) > 0: 463 | return sum(self._batt_current) / len(self._batt_current) 464 | else: 465 | return 0 466 | 467 | @property 468 | def battery_power_avg_mW(self): 469 | if len(self._batt_power) > 0: 470 | return sum(self._batt_power) / len(self._batt_power) 471 | else: 472 | return 0 473 | 474 | @property 475 | def out_current_avg_mA(self): 476 | if len(self._out_current) > 0: 477 | return sum(self._out_current) / len(self._out_current) 478 | else: 479 | return 0 480 | 481 | @property 482 | def out_power_avg_mW(self): 483 | if len(self._out_power) > 0: 484 | return sum(self._out_power) / len(self._out_power) 485 | else: 486 | return 0 487 | 488 | @property 489 | # Getting the value will reset max value !!! 490 | def out_current_peak_mA(self): 491 | tmpMax = self._max_out_current 492 | self._max_out_current = 0 493 | return tmpMax 494 | 495 | @property 496 | # Getting the value will reset max value !!! 497 | def out_voltage_mini_V(self): 498 | tmpMin = self._min_RPi_voltage 499 | self._min_RPi_voltage = 6 500 | return tmpMin 501 | 502 | 503 | class Fan: 504 | def __init__(self, mqttclient): 505 | self._currentfanspeed = 999 506 | self._speed = 100 507 | self._fansum = 0 508 | # self._pTemp = 15 509 | # self._iTemp = 0.4 510 | self._pTemp = 40 511 | self._iTemp = 0.2 * FAN_LOOP_TIME 512 | # self._iTemp = 0 513 | GPIO.setmode(GPIO.BCM) 514 | GPIO.setwarnings(False) 515 | GPIO.setup(GPIO_FAN, GPIO.OUT) 516 | self._myPWM = GPIO.PWM(GPIO_FAN, 50) 517 | self._myPWM.start(50) 518 | self._mqttclient = mqttclient 519 | self.switch_off() 520 | 521 | def set_speed(self): 522 | def get_CPU_temperature(): 523 | res = os.popen('vcgencmd measure_temp').readline() 524 | temp =(res.replace("temp=","").replace("'C\n","")) 525 | # print("temp is {0}".format(temp)) # Uncomment here for testing 526 | return temp 527 | try: 528 | actualTemp = float(get_CPU_temperature()) 529 | except Exception as exc: 530 | print("[get_cpu_temperature] exception: " + str(exc)) 531 | return 532 | diff = actualTemp - DESIRED_CPU_TEMP 533 | self._fansum = self._fansum + diff 534 | pDiff = diff * self._pTemp 535 | iDiff = self._fansum * self._iTemp 536 | self._speed = pDiff + iDiff 537 | if self._speed > 100: 538 | self._speed = 100 539 | # if self._speed < 15: 540 | # if self._speed < 30: # actual minimum value 541 | if self._speed < 40: 542 | self._speed = 0 543 | if self._fansum > 100: 544 | self._fansum = 100 545 | # if self._fansum < -100: 546 | # self._fansum = -100 547 | if self._fansum < 0: 548 | self._fansum = 0 549 | ## print("actualTemp %4.2f TempDiff %4.2f pDiff %4.2f iDiff %4.2f fanSpeed %5d" % (actualTemp,diff,pDiff,iDiff,self._speed)) 550 | print(locale.format_string("CPU temperature %3.1f | Proportional %#6.1f | Integral %#6.1f | Duty Cycle %3d", \ 551 | (actualTemp,pDiff,iDiff,self._speed))) 552 | 553 | 554 | self._myPWM.ChangeDutyCycle(self._speed) 555 | self._publish_speed() 556 | 557 | def switch_off(self): 558 | self._myPWM.ChangeDutyCycle(0) # switch fan off 559 | self._speed = 0 560 | self._publish_speed() 561 | return 562 | 563 | def _publish_speed(self): 564 | if self._mqttclient.connected_flag and (self._speed != self._currentfanspeed): 565 | try: 566 | res = self._mqttclient.publish(MQTT_TOPIC + MQTT_TOPIC_FAN, str(int(self._speed)), 0, True) 567 | if res[0] == 0: 568 | self._currentfanspeed = self._speed 569 | except Exception as e: 570 | pass 571 | return 572 | 573 | def cleanup(self): 574 | self.switch_off() 575 | GPIO.cleanup() # resets all GPIO ports used by this program 576 | 577 | 578 | def get_UPS_status_and_check_battery_voltage(mqttclient): 579 | global UPS_was_on_battery 580 | try: 581 | upsplus = UPSPlus(UPS_voltage_current) 582 | except Exception as exc: 583 | errMsg = "[get_UPS_status_and_check_battery_voltage] Error getting data from UPS: " + str(exc) 584 | print(errMsg) 585 | syslog.syslog(syslog.LOG_ERR, errMsg) 586 | return 587 | if SEND_STATUS_TO_UPSPLUS_IOT_PLATFORM: 588 | upsplus.send_UPS_status_data() 589 | try: 590 | if mqttclient.connected_flag: 591 | upsplus.MQTT_publish(mqttclient) 592 | except Exception as e: 593 | print("mqttpublish exception: " + str(e)) 594 | pass 595 | if upsplus.on_battery: 596 | UPS_was_on_battery = True 597 | shutdown_at_battvoltage_V = (upsplus.protection_voltage_mV + PROTECTION_VOLTAGE_MARGIN_mV) / 1000 598 | syslog.syslog(syslog.LOG_WARNING, 599 | f"UPS on battery. Battery voltage: {upsplus.battery_voltage_V:.3f} V. " 600 | f"Shutdown at {shutdown_at_battvoltage_V:.3f} V") 601 | if upsplus._battery_voltage_V > 1: # protect against bad battery voltage reading 602 | if upsplus._battery_voltage_V < shutdown_at_battvoltage_V : 603 | syslog.syslog(syslog.LOG_WARNING, 604 | f"UPS battery voltage below threshold of {shutdown_at_battvoltage_V:.3f} V. Shutting down.") 605 | shut_down_RPi(mqttclient) 606 | else: 607 | if SHUTDOWN_IMMEDIATELY_WHEN_ON_BATTERY : 608 | syslog.syslog(syslog.LOG_INFO, 'Immediate shutdown.') 609 | shut_down_RPi(mqttclient) 610 | else: 611 | if UPS_was_on_battery: 612 | syslog.syslog(syslog.LOG_INFO, 'UPS back on AC supply.') 613 | UPS_was_on_battery = False 614 | return 615 | 616 | 617 | def shut_down_RPi(mqttclient): 618 | if control_fan: 619 | fan.switch_off() 620 | MQTT_terminate(mqttclient) 621 | # initialize shutdown sequence. 622 | try: 623 | # enable switch on when back on AC 624 | i2c_bus.write_byte_data(DEVICE_ADDR, 0x19, 1) 625 | except Exception as exc: 626 | syslog.syslog(syslog.LOG_ERR, "[shut_down_RPi] Error writing UPS register (back to AC power up): " + str(exc)) 627 | try: 628 | i2c_bus.write_byte_data(DEVICE_ADDR, 0x18, SHUTDOWN_TIMEOUT) 629 | except Exception as exc: 630 | syslog.syslog(syslog.LOG_ERR, "[shut_down_RPi] Error writing UPS register (shutdown timeout): " + str(exc)) 631 | time.sleep(1) 632 | os.system("sudo shutdown -h now") 633 | # os.system("sudo sync && sudo halt") 634 | while True: 635 | time.sleep(100) 636 | 637 | 638 | def UPS_is_present(): 639 | """ 640 | check if i2c bus is usable. check if we can read UPS registers. 641 | initialize i2c bus and both INA sensors 642 | returns False if we get an exception in any of these operations 643 | """ 644 | global i2c_bus 645 | global inaRPi 646 | global inaBattery 647 | 648 | # init i2c protocol 649 | try: 650 | i2c_bus = smbus2.SMBus(DEVICE_BUS) 651 | except Exception as e: 652 | errMsg = 'i2c bus for communication with UPS could not be initialized. Error message: ' + str(e) 653 | syslog.syslog(syslog.LOG_WARNING, errMsg) 654 | print(errMsg) 655 | return False 656 | # check if we can read UPS registers on the i2c bus 657 | try: 658 | void = i2c_bus.read_byte_data(DEVICE_ADDR, 0x12) 659 | except OSError as e: 660 | errMsg = 'No reply from UPS on i2c bus. Error message: ' + str(e) 661 | syslog.syslog(syslog.LOG_WARNING, errMsg) 662 | print(errMsg) 663 | return False 664 | # Raspberry Pi output current and voltage 665 | try: 666 | # inaRPi = INA219(0.00725, busnum=DEVICE_BUS, address=0x40) 667 | inaRPi = INA219(0.0145, 4, busnum=DEVICE_BUS, address=0x40) 668 | inaRPi.configure(inaRPi.RANGE_16V) 669 | except Exception as exc: 670 | errMsg = 'Cannot initialize communication with INA219 (output). Error message: ' + str(exc) 671 | syslog.syslog(syslog.LOG_WARNING, errMsg) 672 | print(errMsg) 673 | return False 674 | # Battery current and voltage 675 | try: 676 | # inaBattery = INA219(0.005, busnum=DEVICE_BUS, address=0x45) 677 | inaBattery = INA219(0.011, 4, busnum=DEVICE_BUS, address=0x45) 678 | inaBattery.configure(inaRPi.RANGE_16V) 679 | except Exception as exc: 680 | errMsg = 'Cannot initialize communication with INA219 (battery). Error message: ' + str(exc) 681 | syslog.syslog(syslog.LOG_WARNING, errMsg) 682 | print(errMsg) 683 | return False 684 | return True 685 | 686 | 687 | send_status_data_warncount = 0 688 | try: 689 | print(f"UPS-Plus to MQTT version {SCRIPT_VERSION}") 690 | print("Copyright (C) 2021 https://github.com/frtz13") 691 | print("This program comes with ABSOLUTELY NO WARRANTY") 692 | print("This is free software, and you are welcome to redistribute it") 693 | print("under conditions of the GPL (see http://www.gnu.org/licenses for details).") 694 | print() 695 | 696 | if not read_config(): 697 | print("Please check configuration file and parameters") 698 | syslog.syslog(syslog.LOG_WARNING, "Program stopped. Please check configuration file and parameters.") 699 | exit() 700 | 701 | print("Type ctrl-C to exit") 702 | syslog.syslog(syslog.LOG_INFO, f"Version {SCRIPT_VERSION} running...") 703 | 704 | # init MQTT connection 705 | connect_to_MQTT = MQTT_BROKER != "" 706 | mqtt.Client.connected_flag = False # create flags in class 707 | mqtt.Client.connection_rc = -1 708 | MQTT_client = mqtt.Client("fanShutDownUps") 709 | MQTT_connected = False 710 | 711 | control_fan = (GPIO_FAN >= 0) 712 | if control_fan: 713 | try: 714 | fan = Fan(MQTT_client) 715 | except Exception as exc: 716 | errMsg = 'Fan class initialization failed. Error message: ' + str(exc) 717 | syslog.syslog(syslog.LOG_ERR, errMsg) 718 | print(errMsg) 719 | control_fan = False 720 | 721 | UPS_present = UPS_is_present() 722 | if UPS_present: 723 | UPS_voltage_current = UPSVoltageCurrent() 724 | 725 | UPS_was_on_battery = False 726 | 727 | # set negative value (-300 + BATT_LOOP_TIME) to force long waiting at startup. 728 | # so the RPi will be running for some minimum time if power fails again. 729 | # also, script won't shut down the RPi before elapse of this time, so we have a chance to kill the script should anything malfunction. 730 | # also allows to wait for the MQTT broker to start if it is running on the RPi, too. 731 | batterycheck_timer = TIMER_BIAS_AT_STARTUP 732 | 733 | fan_timer = 0 734 | while True: 735 | if UPS_present: 736 | UPS_voltage_current.add_value() 737 | if control_fan: 738 | if fan_timer >= FAN_LOOP_TIME: 739 | fan_timer = 0 740 | fan.set_speed() 741 | else: 742 | fan_timer += 1 743 | if batterycheck_timer >= BATT_LOOP_TIME: 744 | batterycheck_timer = 0 745 | if control_fan: 746 | read_config_desired_cpu_temp() 747 | if not MQTT_connected and connect_to_MQTT: 748 | MQTT_connected = MQTT_connect(MQTT_client) 749 | if UPS_present: 750 | get_UPS_status_and_check_battery_voltage(MQTT_client) 751 | else: 752 | batterycheck_timer += 1 753 | sleep(1) 754 | 755 | except KeyboardInterrupt: # trap a CTRL+C keyboard interrupt 756 | if control_fan: 757 | fan.cleanup() 758 | MQTT_terminate(MQTT_client) 759 | print() 760 | syslog.syslog(syslog.LOG_INFO, "Stopped") 761 | --------------------------------------------------------------------------------