Solar is back online
A while ago, some 4 years to be more accurate, my server which magically recorded details from my Aurora Solar inverter died, and along with it my enthusiasm to restore said function. However, recent events have restored the need to know what exactly is happening on my roof so I rekindled my programming mojo to see what could be done. Long story short: my solar is being monitored again courtesy of PVOutput.
I had a few goals setting out on this little project:
- Write it in Python (break my nasty "bash" habit)
- Use modern web API tools (JSON etc)
- Make it resilient

Ah, the nostalgia! (Image courtesy of asus.com)
At my disposal I have:
- Aurora 3600 Inverter, already wired with an RS425 to USB converter
- An ancient Asus eeePC (remember those?) running Ubuntu 18.04
- A Netatmo weather station (for environmental stuff like outside temperature)
- A reasonable grasp of programming
So far, so good. As I started researching the finer details of what I was hoping to accomplish I realised there were a few things I needed to refresh my understanding of, or fresh concepts I've previously avoided. Thankfully, both Netatmo and and PVOutput have well documented APIs and sample code to make getting started simple. In the case of my weather data, it is all exported in a neat JSON blob after setting myself up with Netatmo Connect. I already had an account and API access token from PVOutput, and their API Reference is very thorough, although lacking specific examples for Python it wasn't hard to adapt what is there.
JSON; so easy to understand conceptually, implementing can be tricky to get your head around when dealing with more complex data structures. The output from Netatmo is fantastically detailed and well structured, but the syntax for extracting just the outside temperature took me longer to nut out than I'd expected. However, this was more my lack of recent coding practise in Python! My Python-fu is weak.
Then there was the whole dilemma of using an external binary to poll the inverter, get the result and parse it in Python. New territory, again but ultimately rather simple. There's a little program called aurora written by a guy named Curt Blank which talks to Aurora solar inverters and is free! One day I may port it to a Python library (but not today). It's very well documented, fast and works really well; props to Curt!
NOTE - if you're not using a Netatmo weather station, you can simply rip all that code out and replace it with your own weather feed too. If you you don't care about the outside temperature (which is used to calculate efficiency degredation at high ambient temps on the solar panels) just dont feed "'v5' : myTemp," to PVOutput which is right at the bottom of the code around line 160.
So with all the various pieces in place, the basic logic of solar live feed is:
Now here's the redacted code with LOTS of comments to explain what is going on. I'm positive there are better ways to do most (all?) of this, and any constuctive feedback is most welcome! Obvious things you need to take care of (if you want to use this) are prefixed "FIX ME:", although some file locations in the setup may need tweaking for your environment. So, without further adieu I give you my solar inverter feeder in all it's clunky glory!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
#!/usr/bin/env python3 #This code uses a few libraries - could probably reduce this to subsets within these...but moving on... import requests import json import os import re import datetime # Define some variables first...we'll update these as we go, so we need them in global scope. myaccesstoken="" myTemp="" pvenergy="" pvpower="" pvvolts="" # These variables are needed to drive the show... pvoSysID="FIXME: Your PVOutput System ID" pvoAPIKey="FIXME: Your PVOutput API Key" pvoSvcURL="https://pvoutput.org/service/r2/addstatus.jsp" aurorainverter="/dev/ttyUSB0" address=2 # Default Aurora RS425 address tries=5 # Number of times to attempt communication with the inverter poller="/usr/local/bin/aurora" # Where your compiled "aurora" binary lives pollopts=" -a " + str(address) + " -Y " + str(tries) + " -c -d 0 -e " + aurorainverter outdoor_module="FIXME: Your Netatmo Outdoor module NAME" pvOutputLog="/usr/local/logs/pvout.csv" # CSV copy of data we want to send - can be fed into PVOutput.org if necessary. appLog="/usr/local/logs/pvout-uploader.log" # A log of what this program does # Open the log file... logf=open(appLog, "a+") # Function to log messgaes to log file and screen... def log_it(message, fh): # Add the date/time to the front of the message # For some reason %z/%Z timezone not working for me - hard coded time zone message=datetime.datetime.now().strftime("%a %d %b %T AEST %Y ") + message # Now display and log it print(message) fh.write(message + "\n") # Now we start the process to poll the solar inverter log_it("Polling the solar inverter", logf) # First grab a time stamp to tie to the inverter's data # This avoids any potential delay in the program skewing the # sample collection details - unlikely, but you never know. currentDT=datetime.datetime.now() myDate=currentDT.strftime("%Y%m%d") myTime=currentDT.strftime("%H:%M") # The actual system call to the "aurora" binary... output=os.popen(poller + pollopts + " 2>&1").readlines() # Tidy up the output so we have usable data # If the inverter responded, all the data was output in space-delimited columns output[0]=output[0].lstrip() # Strip leading whitespace output[0]=re.sub(' +',' ',output[0]) # Replace repeated spaces with a single space output[0]=re.sub('\n','',output[0]) # Remove new line characters solarparams=output[0].split(" ") # Now split all the values retrieved into a list :) # Check if the inverter is online or not (last element in list will be "OK" if it all worked). if solarparams[-1] == "OK": # Inverter online - do all the time-consuming stuff and grok the output, then send to PVOutput # Get a token to authenticate to Netatmo # See https://dev.netatmo.com/myaccount/createanapp for client ID/Secret etc. payload = { 'grant_type' : 'password', 'username' : "FIXME: Your Netatmo Login", 'password' : "FIXME: Your Netatmo Password", 'client_id' : "FIXME: Your Netatmo client ID", 'client_secret': "FIXME: Your Netatmo client secret", 'scope' : 'read_station'} try: response = requests.post("https://api.netatmo.com/oauth2/token", data=payload) response.raise_for_status() access_token=response.json()["access_token"] myaccesstoken=access_token refresh_token=response.json()["refresh_token"] scope=response.json()["scope"] except requests.exceptions.HTTPError as error: print(error.response.status_code, error.response.text) # If we get this far, we successfully authenticated and have an access token in 'myaccesstoken' (duh) log_it("Fetching current weather data", logf) params = { 'access_token': myaccesstoken, 'device_id' : 'FIXME: Your Netatmo Weather Station MAC address' } # Poll my Netatmo weather station for the current weather information try: response = requests.post("https://api.netatmo.com/api/getstationsdata", params=params) response.raise_for_status() data = response.json()["body"] # Iterate over the modules, until we find the outside station named in 'outdoor_module' for item in data['devices'][0]['modules']: if item['module_name'] == outdoor_module: # Found the outdoor module - grab the temp and store as a string. myTemp=str(item['dashboard_data'].get('Temperature')) log_it("Outside Temp: " + myTemp + "ºC", logf) except requests.exceptions.HTTPError as error: print(error.response.status_code, error.response.text) # We should now have a value for the outside temperature in 'myTemp' stored as a string. # Process the values we're interested in, using the supported precision (see https://pvoutput.org/help.html) etc. # Store as strings to save conversion later - we don't need to manipulate the values further. # Because we used "-d 0 -e" to pull data via the "aurora" binary, the resultant output columns are: # NOTE: All values are floats with varying precision # solarparams[0] = Input 1 Voltage (V) # solarparams[1] = Input 1 Current (A) # solarparams[2] = Input 1 Power (W) # solarparams[3] = Input 2 Voltage (V) # solarparams[4] = Input 2 Current (A) # solarparams[5] = Input 2 Power (W) # solarparams[6] = Grid Voltage Reading (V) # solarparams[7] = Grid Current Reading (A) # solarparams[8] = Grid Power Reading (W) # solarparams[9] = Frequency Reading (Hz) # solarparams[10] = DC/AC Conversion Efficiency # solarparams[11] = Inverter Temperature (ºC) # solarparams[12] = Booster Temperature (ºC) # solarparams[13] = Daily Energy (kWh cumulative) # solarparams[14] = Weekly Energy (kWh cumulative) # solarparams[15] = Monthly Energy (kWh cumulative) # solarparams[16] = Yearly Energy (kWh cumulative) # solarparams[17] = Total Energy (kWh cumulative) # solarparams[18] = Partial Energy (kWh cumulative) # solarparams[19] = Status (should always be "OK") pvenergy=str(int(float(solarparams[13])*1000)) # Inverter output kWh, PVOutput needs Wh pvpower=str(int(round(float(solarparams[8]),0))) # Convert a float to rounded integer for PVOutput pvvolts=str(round(float(solarparams[6]),1)) # Reduce precision to 1 decimal place for PVOutput # Dump some debug info. print("Data to send to PVOutput") print(" Date: ",myDate) print(" Time: ",myTime) print(" Temp: ",myTemp) print(" Energy: ",pvenergy) print(" Power: ",pvpower) print(" Volts: ",pvvolts) # Open the CSV data file for capturing exactly what we want upload outf=open(pvOutputLog, "a+") outf.write(myDate + "," + myTime + "," + pvenergy + "," + pvpower + "," + myTemp + "," + pvvolts + "\n") # Create POST payload (the data) payload={ 'd' : myDate, 't' : myTime, 'v1' : pvenergy, 'v2' : pvpower, 'v5' : myTemp, 'v6' : pvvolts } # Add the headers so PVOutput.org know what system this is for headers={ 'X-Pvoutput-Apikey' : pvoAPIKey, 'X-Pvoutput-SystemId' : pvoSysID } # Now send the data to PVOutput.org (ie, svcURL web API) postit=requests.post(pvoSvcURL, data=payload, headers=headers) log_it("Attempting upload to PVOutput: " + str(postit.status_code) + " " + postit.reason, logf) else: log_it("No response from inverter - probably sleeping", logf) |
Comments
Comments powered by Disqus