Skip to content

Instantly share code, notes, and snippets.

@dutchLuck
Last active January 4, 2026 10:25
Show Gist options
  • Select an option

  • Save dutchLuck/64375fbdff945f17347ceef2f6db74f3 to your computer and use it in GitHub Desktop.

Select an option

Save dutchLuck/64375fbdff945f17347ceef2f6db74f3 to your computer and use it in GitHub Desktop.
Python script to graph the real-time power/energy and voltage data from a Fronius inverter
#! /bin/python
#
# F R O N I U S P V _ G R A P H . P Y
#
# Last Modified on Fri Jan 04 21:23:30 2026 as version 0v12
#
# Code to graph the real-time data from a Fronius Primo inverter.
#
# Graphs Output AC Voltage and Output AC Power by default and the
# user can select alternate data to graph via command line options.
# Also can output Status, Energy, Power, Current and Voltage data to a CSV file if requested.
# Also can output raw JSON data from the Fronius to a JSON file if requested.
# Also can save last graph to PNG file if requested (and triggered by Ctrl-C).
#
################################################################################################
#
# This code started out as AI (ChatGPT) generated code.
# Prompt to ChatGPT:
#
# write a program to graph the output voltage and power of a
# Fronius Primo Solar Inverter in real time and update the graph on
# the screen every 10 seconds.
#
# ChatGPT response was; -
# Key Points:
# 1. Mock Data: The get_inverter_data function simulates inverter data. Replace it with your
# actual data fetching logic.
# 2. Real-Time Update: The animation.FuncAnimation updates the graph every 10 seconds.
# 3. Data Handling: The program keeps only the last 30 data points for clarity in visualization.
#
# The Mock data was replaced with some Copilot AI generated code, that actually does fetch
# data from a Fronius inverter using the solar_api specification.
#
################################################################################################
#
# You may need to install the requests and matplotlib libraries if they are not already available.
# Run the following commands to install them; -
# pip install requests
# pip install matplotlib
#
################################################################################################
#
# 0v11 Takes a while to start, but seems to work OK on Win 10 using python 3.13.11 querying a
# Fronius Primo that doesn't look like the latest box, but has relatively up-to-date Firmware.
# There are still some timing issues with an occasional data query timeout, but it recovers OK.
#
# 0v12 Added Power Efficiency graphing option
# 0v11 Added "-j A-z" append JSON data to a json file option
# 0v10 Added timestamp output on transfer timeout warning and added append data to CSV file option
# 0v9 Change plot line colour for error conditions and add info to axis labels
# 0v8 Fixed bugs and added status code, LED colour code & error code output
# 0v7 defaults to AC Volts and AC Power graphs,
# -UX and -LX allow non-default graph selection
# instead of -E or -P previously available.
# 0v6 defaults to display half an hour of data,
# NB: -f is now -g option and -E selects show Energy [Wh]
#
################################################################################################
#
# Further ideas or fixes;-
#
# 1. Identify the Fronius device ID number
# 2. Fix Windows Ctrl-C interrupt handling (Isn't immediate, but works just before next graph update)
# 3. Over-voltage and other beyond limits flagging
# 4. Time used in plots should be from the Fronius timestamps, not local system time
# 5. Add option to count number of graph updates
# 6. Option to not show graph window and just log data to file(s)
#
################################################################################################
#
# Example of command to start operation; -
#
# python froniusPV_graph.py -v -f fronius_data -j fronius_data -U5 -L6 -p181 -w10 fronius.local
#
# This command will graph up to the last 181 points (30 minutes of data at 10 sec intervals)
# of AC Power and AC Voltage and output verbose info, append data to fronius_data.csv
# and fronius_data.json files. The Fronius device is assumed to be at network name "fronius.local"
#
# Example (Text) output from running the command; -
#
# PS C:\> python froniusPV_graph.py -v -f fronius_data -j fronius_data -U5 -L6 -p181 -w10 fronius.local
#
# froniusPV_graph.py 0v11, Jan 2026
# 2026-01-03, 15:45:21, +11:00, Status Code: 7, Error Code 0, LED Colour: 2, Year Energy: 92469.4 Wh, Day Energy: 19635 Wh, AC Frequency: 50 Hz, AC Power: 1780 W, AC Current: 7.62 A, AC Voltage: 233.3 V, DC Current: 7.19 A, DC Voltage: 313.3 V
# 2026-01-03, 15:45:31, +11:00, Status Code: 7, Error Code 0, LED Colour: 2, Year Energy: 92474.4 Wh, Day Energy: 19639 Wh, AC Frequency: 49.98 Hz, AC Power: 1752 W, AC Current: 7.5 A, AC Voltage: 233.3 V, DC Current: 7.09 A, DC Voltage: 313.3 V
# 2026-01-03, 15:45:41, +11:00, Status Code: 7, Error Code 0, LED Colour: 2, Year Energy: 92478.5 Wh, Day Energy: 19644 Wh, AC Frequency: 49.98 Hz, AC Power: 1741 W, AC Current: 7.46 A, AC Voltage: 233.3 V, DC Current: 7.05 A, DC Voltage: 313.4 V
# Program terminated via CTRL-C.
# PS C:\>
#
##################################################################################################
#
# Example of (help) usage (plus initial debug info); -
#
# PS C:\> python .\froniusPV_graph.py -D -h
#
# froniusPV_graph.py 0v12, Jan 2026
#
# Graph data from a networked Fronius Inverter
#
# Debug: ".\froniusPV_graph.py" Python script running on system type "win32"
# Debug: Argument List ['.\\froniusPV_graph.py', '-D', '-h']
# Debug: Python version 3.13.11 (tags/v3.13.11:6278944, Dec 5 2025, 16:26:58) [MSC v.1944 64 bit (AMD64)]
# Debug: 181 Graph Points, 10 sec b/w Points
# Debug: Upper Graph: Output AC Power
# Debug: Lower Graph: Output AC Voltage
# Usage:
# .\froniusPV_graph.py [-cX][-D][-f A-z][-g A-z][-h][-j A-z][-LX][-pX][-UX][-v][-wX] [targetFronius]
# where; -
# -cX do count graph updates (not yet implemented)
# -D or --debug prints out Debug information
# -f A-z save data in CSV format in file named "A-z.csv"
# -g A-z save last graph in PNG format in file name "A-z.png" on CNTR-C exit
# -h or --help outputs this usage message
# -j A-z save JSON records received in file name "A-z.json"
# -LX Select lower graph data code [1 <= X <= 10] (default is 6)
# -pX display X points in graph [10 <= X <= 360]
# -UX Select upper graph data code [1 <= X <= 10] (default is 5)
# -v or --verbose prints verbose output
# -wX wait X sec [4 <= X <= 60] instead of default (10) sec to update graph
# where codes for graph selection are; -
# 1: Energy Output Today
# 2: Frequency
# 3: Output AC Current
# 4: Input DC Current
# 5: Output AC Power
# 6: Output AC Voltage
# 7: Input DC Voltage
# 8: Years Energy Output
# 9: Input DC Power
# 10: Inverter Power Efficiency
# and where; -
# targetFronius is either the name or IP address of the fronius device
# E.g.; -
# python .\froniusPV_graph.py -v fronius.local
# PS C:\>
#
import requests
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import time
import re
from datetime import datetime
import sys # exit()
import getopt # getopt()
# Define codes for graph selection
START_CODE = 1
DAY_ENERGY_CODE = 1
FREQUENCY_AC_CODE = 2
CURRENT_AC_CODE = 3
CURRENT_DC_CODE = 4
POWER_AC_CODE = 5
VOLTAGE_AC_CODE = 6
VOLTAGE_DC_CODE = 7
YEAR_ENERGY_CODE = 8
POWER_DC_CODE = 9
EFFICIENCY_CODE = 10
END_CODE = 10
# Program start-up Information
code_info = "froniusPV_graph.py 0v12, Jan 2026"
inverter_ip = "192.168.1.100" # Default IP address of Fronius Device
base_url = "/solar_api/v1/" # Expected version of API to use
estimatedTimeToGetDataAndGraphIt = 230 # Guess time taken in mSec
# Default options
options = {
"count": int(1),
"debug": False,
"file": "", # Default is to not append to CSV file
"graph": "", # Default is to not save graph on CNTR-C exit
"help": False,
"json": "", # Default is to not to append to JSON file
"lower_graph_code": int(VOLTAGE_AC_CODE),
"points": int(181), # default points limit on graph
"upper_graph_code": int(POWER_AC_CODE),
"verbose": False,
"wait": int(10), # interval between updates defaults to 10 sec
}
def get_title( code ):
if code == DAY_ENERGY_CODE:
return "Energy Output Today"
elif code == FREQUENCY_AC_CODE:
return "Frequency"
elif code == CURRENT_AC_CODE:
return "Output AC Current"
elif code == CURRENT_DC_CODE:
return "Input DC Current"
elif code == POWER_AC_CODE:
return "Output AC Power"
elif code == VOLTAGE_AC_CODE:
return "Output AC Voltage"
elif code == VOLTAGE_DC_CODE:
return "Input DC Voltage"
elif code == YEAR_ENERGY_CODE:
return "Years Energy Output"
elif code == POWER_DC_CODE:
return "Input DC Power"
elif code == EFFICIENCY_CODE:
return "Inverter Power Efficiency"
else:
return "Unknown Plot"
def get_y_label( code ):
if code == DAY_ENERGY_CODE:
return "Energy"
elif code == FREQUENCY_AC_CODE:
return "Frequency"
elif code == CURRENT_AC_CODE:
return "AC Current"
elif code == CURRENT_DC_CODE:
return "DC Current"
elif code == POWER_AC_CODE:
return "AC Power"
elif code == VOLTAGE_AC_CODE:
return "AC Volts"
elif code == VOLTAGE_DC_CODE:
return "DC Volts"
elif code == YEAR_ENERGY_CODE:
return "Energy"
elif code == POWER_DC_CODE:
return "DC Power (calculated)"
elif code == EFFICIENCY_CODE:
return "Efficiency (calculated)"
else:
return "Unknown Y Label"
def useage_codes():
for x in range(START_CODE, END_CODE + 1):
print(f" {x}: {get_title( x )}")
def usage():
print(
"Usage:\n%s [-cX][-D][-f A-z][-g A-z][-h][-j A-z][-LX][-pX][-UX][-v][-wX] [targetFronius]"
% sys.argv[0]
)
print(" where; -\n -cX do count graph updates (not yet implemented)")
print(" -D or --debug prints out Debug information")
print(" -f A-z save data in CSV format in file named \"A-z.csv\"")
print(" -g A-z save last graph in PNG format in file name \"A-z.png\" on CNTR-C exit")
print(" -h or --help outputs this usage message")
print(" -j A-z save JSON records received in file name \"A-z.json\"")
print(f" -LX Select lower graph data code [{START_CODE} <= X <= {END_CODE}] (default is {options["lower_graph_code"]})")
print(f" -pX display X points in graph [10 <= X <= 360]")
print(f" -UX Select upper graph data code [{START_CODE} <= X <= {END_CODE}] (default is {options["upper_graph_code"]})")
print(" -v or --verbose prints verbose output")
print(
f" -wX wait X sec [4 <= X <= 60] instead of default ({options['wait']}) sec to update graph"
)
print( " where codes for graph selection are; -" )
useage_codes()
print(" and where; -\n targetFronius is either the name or IP address of the fronius device")
print(" E.g.; -")
print(f" python {sys.argv[0]} -v fronius.local")
# Get options and arguments from the command line
def processCommandLine():
try:
opts, args = getopt.getopt(
sys.argv[1:],
"c:Df:g:hj:L:p:U:vw:",
[
"", # count
"debug",
"", # CSV file name
"", # graph file name
"help",
"", # json file name
"", # lower code
"", # points
"", # upper code
"verbose",
"", # wait
],
)
except getopt.GetoptError as err:
print(str(err))
usage()
sys.exit(1) # Indicate unsuccessful to shell, if there is one.
for o, a in opts:
if o in "-c":
options["count"] = int(a)
if options["count"] < 1:
options["count"] = 1
elif o in ("-D", "--debug"):
options["debug"] = True
elif o in "-f":
options["file"] = a
elif o in "-g":
options["graph"] = a
elif o in ("-h", "--help"):
options["help"] = True
elif o in "-j":
options["json"] = a
elif o in "-L":
options["lower_graph_code"] = int(a)
# Validate lower graph code
if options["lower_graph_code"] < START_CODE:
options["lower_graph_code"] = START_CODE
elif options["lower_graph_code"] > END_CODE:
options["lower_graph_code"] = END_CODE
elif o in "-p":
options["points"] = int(a) # Get Points limit for graph
if options["points"] < 10:
options["points"] = 10
elif options["points"] > 360: # limit to 60 minutes of data at default 10 sec b/w points
options["points"] = 361
elif o in "-U":
options["upper_graph_code"] = int(a)
# Validate upper graph code
if options["upper_graph_code"] < START_CODE:
options["upper_graph_code"] = START_CODE
elif options["upper_graph_code"] > END_CODE:
options["upper_graph_code"] = END_CODE
elif o in ("-v", "--verbose"):
options["verbose"] = True
elif o in "-w":
options["wait"] = int(a) # Get Seconds between sample points
if options["wait"] < 4: # Don't allow smaller than 4 sec between fronius get calls
options["wait"] = 4 # Fastest sampling rate with min points equals a 40 sec graph
elif options["wait"] > 60: # Don't allow greater than 60 sec between fronius get calls
options["wait"] = 60 # Slowest sampling rate with max points equals a 6 hours graph
if options["debug"]:
options["verbose"] = True # Debug implies verbose output
return args
# Get Fronius inverter data. Data is expected in JSON format.
def get_inverter_data( upper_graph_selector, lower_graph_selector ):
status_code = 254
upper_graph_value = lower_graph_value = -1
upper_graph_unit = lower_graph_unit = "N/A"
device_error_code = LED_colour = device_status_code = -1
power = in_power = day_power = year_power = -1
power_unit = in_power_unit = day_power_unit = year_power_unit = "Wh"
frequency = -1
frequency_unit = "Hz"
current = in_current = -1
current_unit = in_current_unit = "A"
voltage = in_voltage = -1
voltage_unit = in_voltage_unit = "V"
efficiency = -1
efficiency_unit = "%"
status_reason = "No Reason Given"
data_timestamp = datetime.now().strftime("%Y-%m-%dT%H:%M:%S+11:00") # Default to local time if no timestamp from Fronius
if options["debug"]:
print(f"\n{data_timestamp} Debug: get_inverter_data({upper_graph_selector},{lower_graph_selector}) called.")
try:
response = requests.get(
f"http://{inverter_ip}{base_url}GetInverterRealtimeData.cgi?Scope=Device&DeviceId=1&DataCollection=CommonInverterData",
timeout=5,
verify=True,
)
response.raise_for_status()
except requests.exceptions.HTTPError as errh:
print(f"{data_timestamp} HTTP Error")
print(errh.args[0])
raise SystemExit(errh)
except requests.exceptions.Timeout as errrt:
print(f"{data_timestamp} Connection Time-out Error")
raise SystemExit(errrt)
except requests.exceptions.ConnectionError as conerr:
print(f"{data_timestamp} Connection Error")
raise SystemExit(conerr)
except KeyboardInterrupt:
raise
except requests.exceptions.RequestException as e:
raise SystemExit(e)
if options['json'] != "":
with open( f"{options['json']}.json", 'a') as j:
j.write( response.text + "\n" ) # Append Fronius raw JSON data to json file
# See if we got a response with valid JSON
try:
data = response.json()
except requests.exceptions.JSONDecodeError as jsonerr:
print("\nError: JSON Decode Error in response from Network Device.\n")
print(f"{data_timedstamp} Warning: No Data available : {jsonerr}.")
else:
if options["debug"]:
print(data) # output the json response data if in debug mode
# Obtain Data Timestamp
if "Head" in data and "Timestamp" in data["Head"]:
data_timestamp = data["Head"]["Timestamp"]
else:
print("Warning: No Data timestamp available - using local date and time.")
ts_match = re.match(r"(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)([-\+])(\d+):(\d+)", data_timestamp)
yr = ts_match.group(1)
mnth = ts_match.group(2)
dy = ts_match.group(3)
hr = ts_match.group(4)
mn = ts_match.group(5)
sc = ts_match.group(6)
osgn = ts_match.group(7)
ohr = ts_match.group(8)
omn = ts_match.group(9)
if "Head" in data and "Status" in data["Head"]:
status_code = data["Head"]["Status"]["Code"]
if status_code != 0:
if "Head" in data and "Status" in data["Head"]:
status_reason = data["Head"]["Status"]["Reason"]
print(
f'{hr}:{mn}:{sc} Warning: "{status_reason}": Returned Data is not trustworthy (status code: {status_code}).'
)
elif "Body" in data and "Data" in data["Body"]:
# Fronius Status so far today
if "DeviceStatus" in data["Body"]["Data"]:
device_error_code = data["Body"]["Data"]["DeviceStatus"]["ErrorCode"]
LED_colour = data["Body"]["Data"]["DeviceStatus"]["LEDColor"]
device_status_code = data["Body"]["Data"]["DeviceStatus"]["StatusCode"]
else:
print(f"{data_timestamp} Warning: Device Status unavailable.")
device_error_code = -1
LED_colour = -1
device_status_code = -1
# AC Energy output so far today
if "DAY_ENERGY" in data["Body"]["Data"]:
day_power = data["Body"]["Data"]["DAY_ENERGY"]["Value"]
day_power_unit = data["Body"]["Data"]["DAY_ENERGY"]["Unit"]
else:
print(f"{data_timestamp} Warning: No Day Energy data available.")
day_power = -1
day_power_unit = "N/A"
if upper_graph_selector == DAY_ENERGY_CODE:
upper_graph_value = day_power
upper_graph_unit = day_power_unit
if lower_graph_selector == DAY_ENERGY_CODE:
lower_graph_value = day_power
lower_graph_unit = day_power_unit
# AC Frequency output
if "FAC" in data["Body"]["Data"]:
frequency = data["Body"]["Data"]["FAC"]["Value"]
frequency_unit = data["Body"]["Data"]["FAC"]["Unit"]
else:
print(f"{data_timestamp} Warning: No Frequency data available.")
frequency = -1
frequency_unit = "N/A"
if upper_graph_selector == FREQUENCY_AC_CODE:
upper_graph_value = frequency
upper_graph_unit = frequency_unit
if lower_graph_selector == FREQUENCY_AC_CODE:
lower_graph_value = frequency
lower_graph_unit = frequency_unit
# AC Current output
if "IAC" in data["Body"]["Data"]:
current = data["Body"]["Data"]["IAC"]["Value"]
current_unit = data["Body"]["Data"]["IAC"]["Unit"]
else:
print(f"{data_timestamp} Warning: No AC current data available.")
current = -1
current_unit = "N/A"
if upper_graph_selector == CURRENT_AC_CODE:
upper_graph_value = current
upper_graph_unit = current_unit
if lower_graph_selector == CURRENT_AC_CODE:
lower_graph_value = current
lower_graph_unit = current_unit
# DC input Current
if "IDC" in data["Body"]["Data"]:
in_current = data["Body"]["Data"]["IDC"]["Value"]
in_current_unit = data["Body"]["Data"]["IDC"]["Unit"]
else:
print(f"{data_timestamp} Warning: No DC current data available.")
in_current = -1
in_current_unit = "N/A"
if upper_graph_selector == CURRENT_DC_CODE:
upper_graph_value = in_current
upper_graph_unit = in_current_unit
if lower_graph_selector == CURRENT_DC_CODE:
lower_graph_value = in_current
lower_graph_unit = in_current_unit
# AC Power output
if "PAC" in data["Body"]["Data"]:
power = data["Body"]["Data"]["PAC"]["Value"]
power_unit = data["Body"]["Data"]["PAC"]["Unit"]
else:
print(f"{data_timestamp} Warning: No Power data available.")
power = -1
power_unit = "N/A"
if upper_graph_selector == POWER_AC_CODE:
upper_graph_value = power
upper_graph_unit = power_unit
if lower_graph_selector == POWER_AC_CODE:
lower_graph_value = power
lower_graph_unit = power_unit
# AC Voltage output
if "UAC" in data["Body"]["Data"]:
voltage = data["Body"]["Data"]["UAC"]["Value"]
voltage_unit = data["Body"]["Data"]["UAC"]["Unit"]
else:
voltage = -1
voltage_unit = "V"
print(f"{data_timestamp} Warning: No AC Voltage data available.")
if upper_graph_selector == VOLTAGE_AC_CODE:
upper_graph_value = voltage
upper_graph_unit = voltage_unit
if lower_graph_selector == VOLTAGE_AC_CODE:
lower_graph_value = voltage
lower_graph_unit = voltage_unit
# DC input Voltage
if "UDC" in data["Body"]["Data"]:
in_voltage = data["Body"]["Data"]["UDC"]["Value"]
in_voltage_unit = data["Body"]["Data"]["UDC"]["Unit"]
else:
print(f"{data_timestamp} Warning: No DC Voltage data available.")
in_voltage = -1
in_voltage_unit = "N/A"
if upper_graph_selector == VOLTAGE_DC_CODE:
upper_graph_value = in_voltage
upper_graph_unit = in_voltage_unit
if lower_graph_selector == VOLTAGE_DC_CODE:
lower_graph_value = in_voltage
lower_graph_unit = in_voltage_unit
# AC Energy output so far this year
if "YEAR_ENERGY" in data["Body"]["Data"]:
year_power = data["Body"]["Data"]["YEAR_ENERGY"]["Value"]
year_power_unit = data["Body"]["Data"]["YEAR_ENERGY"]["Unit"]
else:
print(f"{data_timestamp} Warning: No Year Energy data available.")
year_power = -1
year_power_unit = "N/A"
if upper_graph_selector == YEAR_ENERGY_CODE:
upper_graph_value = year_power
upper_graph_unit = year_power_unit
if lower_graph_selector == YEAR_ENERGY_CODE:
lower_graph_value = year_power
lower_graph_unit = year_power_unit
# DC input Power calculation (not directly available)
if in_voltage != -1 and in_current != -1:
in_power = round( in_voltage * in_current )
in_power_unit = "W"
else:
in_power = -1
in_power_unit = "N/A"
if upper_graph_selector == POWER_DC_CODE:
upper_graph_value = in_power
upper_graph_unit = in_power_unit
if lower_graph_selector == POWER_DC_CODE:
lower_graph_value = in_power
lower_graph_unit = in_power_unit
# Input to Output Power Efficiency calculation (not directly available)
if in_power != -1 and power != -1:
efficiency = round(( power / in_power ) * 100, 1 )
efficiency_unit = "%"
else:
efficiency = -1
efficiency_unit = "N/A"
if upper_graph_selector == EFFICIENCY_CODE:
upper_graph_value = efficiency
upper_graph_unit = efficiency_unit
if lower_graph_selector == EFFICIENCY_CODE:
lower_graph_value = efficiency
lower_graph_unit = efficiency_unit
if options["verbose"] or options["debug"]:
print(
f"{yr}-{mnth}-{dy}, {hr}:{mn}:{sc}, {osgn}{ohr}:{omn}, Status Code: {device_status_code}, Error Code {device_error_code}, LED Colour: {LED_colour}, Year Energy: {year_power} {year_power_unit}, Day Energy: {day_power} {day_power_unit}, AC Frequency: {frequency} {frequency_unit}, AC Power: {power} {power_unit}, AC Current: {current} {current_unit}, AC Voltage: {voltage} {voltage_unit}, DC Power: {in_power} {in_power_unit}, DC Current: {in_current} {in_current_unit}, DC Voltage: {in_voltage} {in_voltage_unit}, Power Efficiency: {efficiency} {efficiency_unit}"
)
if options['file'] != "" and status_code == 0:
with open(f"{options['file']}.csv", 'a') as f:
f.write( f"{yr}-{mnth}-{dy}, {hr}:{mn}:{sc}, {osgn}{ohr}:{omn}, {device_status_code}, {device_error_code}, {LED_colour}, {year_power}, {year_power_unit}, {day_power}, {day_power_unit}, {frequency}, {frequency_unit}, {power}, {power_unit}, {current}, {current_unit}, {voltage}, {voltage_unit}, {in_current}, {in_current_unit}, {in_voltage}, {in_voltage_unit}\n" )
else:
print(f"{data_timestamp} Warning: No Data available.")
if options["debug"]:
print(f"{data_timestamp} Debug: get_inverter_data() about to return status: {status_code}.")
return status_code, data_timestamp, device_status_code, device_error_code, upper_graph_value, upper_graph_unit, lower_graph_value, lower_graph_unit
# Start up and get command line configs if there are any
args = processCommandLine()
if options["debug"] or options["verbose"]:
print(f"\n{code_info}")
if options["debug"]:
print("\nGraph data from a networked Fronius Inverter")
print(
'\nDebug: "%s" Python script running on system type "%s"'
% (sys.argv[0], sys.platform)
)
print(f"Debug: Argument List {sys.argv}")
print(f"Debug: Python version {sys.version}")
print(f"Debug: {options['points']} Graph Points, {options['wait']} sec b/w Points")
print(f"Debug: Upper Graph: {get_title(options['upper_graph_code'])}")
print(f"Debug: Lower Graph: {get_title(options['lower_graph_code'])}")
# If help is requested then print usage() and exit
if options["help"]:
usage()
sys.exit(1)
# Attempt 1st contact with Fronius device
if len(args) > 0:
inverter_ip = args[
0
] # First non-switch is assumed to be an IP address or Network name
try:
response = requests.get(
f"http://{inverter_ip}/solar_api/GetAPIVersion.cgi",
timeout=4,
verify=True,
)
except requests.exceptions.Timeout as errrt:
print(
f"\nError: Connection Time-out Error (Probably no Network Device at {inverter_ip} ?)\n"
)
raise SystemExit(errrt)
except requests.exceptions.ConnectionError as conerr:
print(
f"\nError: Connection Error (Probably no HTTP enabled Network Device at {inverter_ip} ?)\n"
)
raise SystemExit(conerr)
except requests.exceptions.RequestException as e:
raise SystemExit(e)
# Determine API version if possible
if response.status_code == 404: # Possibly Version 0 API ?
base_url = "/solar_api/"
print(
f"Warning: Fronius device did not responded to API version request; Defaulting to: {base_url}"
)
else:
# We got a response - see if we should log the raw JSON data
if options['json'] != "":
with open( f"{options['json']}.json", 'a') as j:
j.write( response.text + "\n" ) # Append Fronius raw JSON data to json file
# See if we got a response with valid JSON
try:
data = response.json()
except requests.exceptions.JSONDecodeError as jsonerr:
print(
"\nError: JSON Decode Error in response from Network Device (Probably not a Fronius?)\n"
)
raise SystemExit(jsonerr)
if "BaseURL" in data:
base_url = data["BaseURL"]
if options["debug"]:
print(f"Fronius device responded to API version request with: {base_url}")
else:
print(
f"Warning: Fronius device did not responded to API version request; Defaulting to: {base_url}"
)
if options["debug"]:
print(data)
# Set up header comment for CSV file output if requested
if options['file'] != "":
with open(f"{options['file']}.csv", 'a') as f:
f.write( "#yy-mm-dd, hh:mm:ss, ohh:omm, device_status_code, device_error_code, LED_colour, year_power, year_power_unit, day_power, day_power_unit, frequency, frequency_unit, power, power_unit, current, current_unit, voltage, voltage_unit, in_current, in_current_unit, in_voltage, in_voltage_unit\n" )
# Initialize lists to store time, voltage, and power values.
timestamps = []
upper_values = []
lower_values = []
timeRelativeToMostRecentReading = []
# Create a figure and two subplots for voltage and power.
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
fig.suptitle( code_info, fontsize=16)
# Configure the Upper subplot.
ax1.set_title( get_title( options["upper_graph_code"]))
ax1.set_xlabel("Time [Sec] (relative to last reading)")
ax1.set_ylabel( get_y_label( options["upper_graph_code"]))
ax1.grid(True)
# Configure the Lower subplot.
ax2.set_title( get_title( options["lower_graph_code"]))
ax2.set_xlabel("Time [Sec] (relative to last reading)")
ax2.set_ylabel( get_y_label( options["lower_graph_code"]))
ax2.grid(True)
# Update function for real-time graphing.
def update(frame):
# Get current time and inverter data.
result_code, d_timestamp, d_status, d_error, upper_graph_value, upper_graph_unit, lower_graph_value, lower_graph_unit = get_inverter_data( options["upper_graph_code"], options["lower_graph_code"] )
current_time = time.time()
# Append the data to the lists if it is valid data.
if result_code == 0:
timestamps.append(current_time)
upper_values.append(upper_graph_value)
lower_values.append(lower_graph_value)
# Limit the lists to the last -p X points (defaults to 30 min if not specified) points for clarity.
if len(timestamps) > options["points"]:
timestamps.pop(0)
upper_values.pop(0)
lower_values.pop(0)
# Calculate X Axis values relative to most recent reading
lastIndex = len(timestamps) - 1
timeRelativeToMostRecentReading = [t - timestamps[lastIndex] for t in timestamps]
line_colour = "green"
if d_error != 0:
line_colour = "red"
# Clear and replot the Upper subplot.
ax1.clear()
ax1.plot(
timeRelativeToMostRecentReading,
upper_values,
color = line_colour,
)
ax1.set_title( f'{get_title( options["upper_graph_code"])} ({upper_graph_value} [{upper_graph_unit}])')
ax1.set_ylabel(f'{get_y_label( options["upper_graph_code"])} [{upper_graph_unit}]')
ax1.set_xlabel(f"Time [Sec] (relative to last reading: {d_timestamp}) (Status Code: {d_status})")
ax1.grid(True)
# Clear and replot the Lower subplot.
ax2.clear()
ax2.plot(
timeRelativeToMostRecentReading,
lower_values,
color = line_colour,
)
ax2.set_title( f'{get_title( options["lower_graph_code"])} ({lower_graph_value} [{lower_graph_unit}])')
ax2.set_ylabel(f'{get_y_label( options["lower_graph_code"])} [{lower_graph_unit}]')
ax2.set_xlabel(f"Time [Sec] (relative to last reading: {d_timestamp}) (Error Code: {d_error})")
ax2.grid(True)
# Make sure we trap keyboard ^C and exit relatively cleanly.
try:
# Set up animation to update periodically (defaults to 10 seconds).
ani = animation.FuncAnimation(
fig, update, interval=options["wait"] * 1000 - estimatedTimeToGetDataAndGraphIt, save_count=1
)
# Display the graph.
plt.tight_layout()
plt.show()
except KeyboardInterrupt:
print(f"Program terminated via CTRL-C.")
finally:
if options['graph'] != "":
plt.savefig(f"{options['graph']}.png")
print(f"Last graph saved as \"{options['graph']}.png\".")
sys.exit(0) # Indicate successful to shell, if there is one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment