Last active
January 4, 2026 10:25
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #! /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