How I made the UPS v2 work for me

Well friends, perhaps I’m just dumb, but it took me a good long time to get the UPS to work for me. I’ll give you the short version and the long version

Acknowledgement: A big thanks to @shawnj007 for helping me get an understanding of what is going on. He went out into the cold cold night to pull his rPi out of his car setup to get his code back out.

Equipment: I’m using a RaspberryPi 4 and a Sixfab UPS v2. I’m not using solar at this time, but will begin testing that soon.

Short Version:
Create a systemd service to run a python script that controls the Sixfab API on startup, and that script must reset to factory settings and set the RTC settings. Within that same script, you can then create the scheduled events or power outage event from the API. Make sure no other scripts access the SixFab API at the same time.

Long Version:
I am setting up an off-grid, solar-powered sensor array and I need my RaspberryPi to log data from all of the sensors and send me reports via email automatically. I will not have physical access to the computer. This is where the Sixfab UPS comes into play - it will accept solar power to charge the computer and keep the power regulated.

Problems I ran into, at least in my experience:

  • API must be reset to default factory settings after restarting from a power outage event or it will glitch
  • API cannot accept two different python scripts talking to it at the same time or it will glitch and you will have to remove the battery after shutting down to clear the UPS’ memory
  • Since I need reports using data from the API, I had to combine my API power setting script, my computer startup email script and my sensor datalogging script into a single script

First, we need to make a systemd service:
01

In my case I named the service “custompi.service” and it will run the python file “startup_mailer.py”

Notice the ExecStartPre= line. This service will only run if an internet connection is established.

Make sure after you save the service file after creating/editing that you give the proper chmod permissions to the service and python script.

Also make sure you do a daemon-reload, then enable the service then start the service in terminal.

Now to the fun part, the python script:

Read the comments after the #s to understand each section of code

This script:

  • Sends an email report to me when the computer starts up and at noon every day
  • Creates a power outage event on the UPS if the battery level is <= 30 and is not charging
  • Logs battery level, input_power level, UPS working mode, soil sensor humidity, soil sensor temperature
#Import the SixFab Power Api
from power_api import SixfabPower, Definition, Event
#Import BeautifulSoup to read Sensor Data from an ESP8266 microcontroller (has nothing to do with SixFab)
from bs4 import BeautifulSoup
import requests
#Import datetime and time for sleep and timestamps and RTC functions
import datetime
import time
#Import libraries for sending an email (has nothing to do with SixFab)
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders

#Key to Making Script Work: Reset the UPS
#Do Not Run SixFabPower API in any other script at the same time, especially if you have loops like I do
api = SixfabPower()
api.restore_factory_defaults()
time.sleep(2)
#Set Power Outage Event Parameters to 3 minutes of wake time, 8 hours of sleep time
api.set_power_outage_params(480,3)
time.sleep(2)

#Set RTC every time you reset SixFab
epoch = time.time() # to get timestamp in seconds in GMT0

localtime = time.asctime( time.localtime(time.time()) )
print("Local current time :", localtime)
print(time.timezone)
print("Epoch Time: " + str(int(epoch)))
print("Epoch Local: " + str(int(epoch) - time.timezone))
print("Result: " + str(api.set_rtc_time(int(epoch) - time.timezone)))


#Find values for Startup Email
battery = api.get_battery_level()
mode = api.get_working_mode()
input_power = api.get_input_power()
battstr = str(battery)
modestr = str(mode)
inputstr = str(input_power)
time_s = str(datetime.datetime.now())
time_d = time_s[0:10]
time_t = time_s[11:19]

#Check Sensors
try: 
    requests.get('http://192.168.0.14/humid', timeout=10)
    s1status = "Online"
    
except Exception:
    s1status = "Offline"
    
print("Battery Level: "+str(battery)+"\n\nSensor1 Status: "+s1status)

#Prepare Dashboard (This is to allow time for an automatic PDF Dashboard update that is not described in this script)
time.sleep(90)


#Send Startup Email
email_user = "xxxx@gmail.com"
email_password = "xxxxxxxx"
email_send = "xxxx@gmail.com"

subject = "Central computer on"

msg = MIMEMultipart()
msg["From"] = "Farm rPi"
msg["To"] = email_send
msg["Subject"] = subject

body = "Battery Level: "+str(battery)+"\n\nSensor1 Status: "+s1status
msg.attach(MIMEText(body,"plain"))

file = '/home/pi/Desktop/Dashboard.pdf'
attachment2 = MIMEBase('application', 'octet-stream')
attachment2.set_payload(open(file,'rb').read())
encoders.encode_base64(attachment2)
attachment2.add_header('Content-Disposition', 'attachment; filename="Dashboard.pdf"')
msg.attach(attachment2)

file = '/home/pi/Code/DataLog.txt'
attachment = MIMEBase('application', 'octet-stream')
attachment.set_payload(open(file,'rb').read())
encoders.encode_base64(attachment)
attachment.add_header('Content-Disposition', 'attachment; filename="DataLog.txt"')
msg.attach(attachment)

#Anything that involves an internet connection should have try/except error handling. Internet connections can break!
try: 
    text = msg.as_string()
    server = smtplib.SMTP("smtp.gmail.com",587)
    server.starttls()
    server.login(email_user,email_password)
    server.sendmail(email_user,email_send,text)
    server.quit()
    print("Email Sent")
    
except Exception:
    print("Email Not Sent")



#Write Timestamp to Agriculture Sensor DataLog
if battery >= 1:
    print("Will Write Startup Timestamp")
    csvline = ("\nSTARTUP;"+time_d+";"+time_t+";"+";;"+battstr+";"+modestr+";"+inputstr)
    print(csvline)
    f = open ("/home/pi/Code/DataLog.txt","a")
    f.write (csvline)
    f.close()
else:
    print("Will Not Write Timestamp")
    
    
    
    
#Begin main loop to write sensor data every 60 seconds
i = 1
while i == 1:
    print("Start Loop")
    #Query API for current levels
    battery = api.get_battery_level()
    mode = api.get_working_mode()
    input_power = api.get_input_power()
    battstr = str(battery)
    modestr = str(mode)
    inputstr = str(input_power)
    print("Battery Level: " +battstr)
    print("Working Mode: " +modestr)
    print("Input Power: " +inputstr)
    
        
#Create power off event to power off the device in 15 seconds if battery level is 30 or less and the battery is not charging
    if battery <= 30 and mode == 3:
        api.set_power_outage_event_status(1)
        print("Beginning Shutdown Event in 2 Minutes if Not Powered")
        #Write a timestamp to the Agriculture Sensor Datalog
        csvline = ("\nSHUTDOWN;"+time_d+";"+time_t+";"+";;"+battstr+";"+modestr+";"+inputstr)
        print(csvline)
        f = open ("/home/pi/Code/DataLog.txt","a")
        f.write (csvline)
        f.close()
        time.sleep(30)
        #Exit loop so you don't accidentally write to a file during a shutdown
        exit()
    else:
        print("No Shutdown Event")

#If power level is acceptable, proceed to with 60 second loop to write to Agriculture Sensor DataLog
#Try/Except is due to requests needing an internet connection, which is not always available
    try:
        #Try to get soil sensor readings from ESP8266
        pageh = requests.get('http://192.168.0.14/humid', timeout=10)
        souph = BeautifulSoup(pageh.text,'html.parser')
        paget = requests.get('http://192.168.0.14/temp', timeout=10)
        soupt = BeautifulSoup(paget.text,'html.parser')
        h = str(souph)
        t = str(soupt)
        time_s = str(datetime.datetime.now())
        time_d = time_s[0:10]
        time_t = time_s[11:19]
        #Format then write soil sensor readings to agriculture sensor datalog
        csvline = ("\nS1;"+time_d+";"+time_t+";"+h+";"+t+";"+str(battery)+";"+str(mode)+";"+str(input_power))
        print(csvline)
        f = open ("/home/pi/Code/DataLog.txt","a")
        f.write (csvline)
        f.close()
    except Exception:
        #If unable to connect to the sensor, write blanks/nulls in sensor readings to log
        print("Unable to Connect to Sensor")
        csvline = ("\nS1;"+time_d+";"+time_t+";;;"+str(battery)+";"+str(mode)+";"+str(input_power))
        print(csvline) 
        f = open ("/home/pi/Code/DataLog.txt","a")
        f.write (csvline)
        f.close()
    #Noon Email Report will only send within 1 minute of noon everyday
    time_s = str(datetime.datetime.now())
    time_hr = int(time_s[11:13])
    time_min = int(time_s[14:16])
    time_sec = int(time_s[17:19])
    time_1 = datetime.timedelta(hours= time_hr , minutes= time_min, seconds= time_sec)
    time_2 = datetime.timedelta(hours= 12, minutes=00, seconds=00)
    time_delta_string = (time_2 - time_1)
    email_timer = time_delta_string.seconds
    print("Email Seconds Timer: "+str(email_timer))
    if (email_timer <= 85):
        #Send Email
        email_user = "xxxx@gmail.com"
        email_password = "xxxxxxxx"
        email_send = "xxxx@gmail.com"
        subject = "Noon Report"
        msg = MIMEMultipart()
        msg["From"] = "Farm rPi"
        msg["To"] = email_send
        msg["Subject"] = subject
        body = "Battery Level: "+str(battery)+"\n\nSensor1 Status: "+s1status
        msg.attach(MIMEText(body,"plain"))
        file = '/home/pi/Desktop/Dashboard.pdf'
        attachment2 = MIMEBase('application', 'octet-stream')
        attachment2.set_payload(open(file,'rb').read())
        encoders.encode_base64(attachment2)
        attachment2.add_header('Content-Disposition', 'attachment; filename="Dashboard.pdf"')
        msg.attach(attachment2)
        file = '/home/pi/Code/DataLog.txt'
        attachment = MIMEBase('application', 'octet-stream')
        attachment.set_payload(open(file,'rb').read())
        encoders.encode_base64(attachment)
        attachment.add_header('Content-Disposition', 'attachment; filename="DataLog.txt"')
        msg.attach(attachment)
        #Always use Try/Except with anything demanding an internet connection
        try: 
            text = msg.as_string()
            server = smtplib.SMTP("smtp.gmail.com",587)
            server.starttls()
            server.login(email_user,email_password)
            server.sendmail(email_user,email_send,text)
            server.quit()
            print("Noon Email Sent")
        except Exception:
            print("Noon Email Not Sent")
    else:
        print("Noon Email Not Sent")
    #Loop should only run every 60 seconds
    time.sleep(60)
    print("--")
    print("--")

Hope this helps. I will put out an update that creates its own battery% level from voltage readings in the future rather than using battery level from the API that I think is less accurate since it is based on amps.

@ensar @saeed

Is there a way to customize the wake power tolerance for the power_outage_event in the API? If not, what is the wake power tolerance? All I saw in the docs was 5% - but 5% of what? Is the wake tolerance based on voltage? watts? amps?

This is all very important for connecting to solar which has wide ranges of output.

Hi, @alhamdliyesua ,

Can you edit the source code in your post by wrapping it in single lines with three backticks by themselves? That will preserve the formatting and indentation and make the python legible and pasteable.

Suggestion: we should have a GitHub repository where people can post scripts and code snippets and then just post links to the scripts here.

Example of using backticks:

import subprocess
import re


def check_connection(host):
    cmd = ["ping", "-i", "0.4", "-c", "5", host]

    if verbose:
        print(f"[ecm] Checking ECM connection to {host}")

    output = subprocess.run(cmd, capture_output=True, text=True).stdout

    m = re.search(r' ([0-9]+) received, ', output)

    if m is not None:
        if verbose:
            print(f"[ecm] {host} returned {int(m[1])} packets")

        return int(m[1]) != 0

    return False

1 Like

Thanks for the tip. Done!