From adf9718f4ed573118d27bcb63d410222aab1ad20 Mon Sep 17 00:00:00 2001 From: bohdan-s Date: Fri, 24 Dec 2021 10:43:05 +1100 Subject: [PATCH] Improved error checking, added HTTP export, huge README update --- README.md | 49 +++++++++++++++++++---- SunGather/sungather.py | 90 +++++++++++++++++++++++++++++------------- 2 files changed, 104 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 2442077..508fd3b 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ ## About The Project Access ModBus data from almost any network connected Sungow Inverter. -On first connection the tool will query your inverter, retreive the model and return the correct registers for your device. No more searching registers or creating model files. +On first connection the tool will query your inverter, retrieve the model and return the correct registers for your device. No more searching registers or creating model files. Register information based on official documentation: Communication Protocol of PV Grid-Connected String Inverters and Communication Protocol of Residential Hybrid Inverters -Has muliple export locations out of the box: +Has multiple export locations out of the box: * Console - Log directly to screen * MQTT - Load into MQTT, and optionally Home Assistance Discovery * PVOutput - Load into PVOutput.org @@ -43,7 +43,6 @@ I have borrowed HEAVILY from the following projects, THANK YOU

(back to top)

### TO DO -* Commandline Arguments * Better Home Assistant Support @@ -81,16 +80,52 @@ python3 sungather.py docker pull bohdans/sungather docker run -v {path to}/config.yaml:/config/config.yaml -e TZ=Australia/Sydney --name sungather bohdans/sungather ``` -Note: replace Australia/Sydney with relevent timezone +Note: replace Australia/Sydney with relevant timezone

(back to top)

## Usage -See config-exmaple.py it contains default options and comments. +See config-example.py it contains default options and comments. + +If you want to use the new Energy section in Home Assistant, follow the Home Assistant setup below. +

(back to top)

+ + +## Exports + +A collection of exports are available: + +* console: Output information to console, useful for troubleshooting +* http: Output to a simple website, default http://localhost:8080 or http://:8080 +* mqtt: Output to a pre-existing MQTT server, needed for Home Assistant integration +* pvoutput: Output to PVOutput.org, requires account and solar is set up on website first. + +## Registers + +This tool should be able to access most registers exposed. +Set the following in config.yaml under inverters section. +* level: 1 - This is the most useful data for day to day +* Level: 2 - This should be everything your inverter supports +* Level: 3 - This will try every register, you will get lots of 0/65535 responses for registers not supported. + +* smart_meter: True - Set to true if you have a smart reader installed, this will return power usage at the meter box, without it you can not calculate house power usage. + +### Useful Registers: +This is just a brief list of registers I have found useful + +**daily_power_yields** - Total Power in kWh generated today +**total_power_yields** - Total Power in kWh generated since inverter install +**total_running_time** - Total Hours inverter has been powered on since install +**internal_temperature** - Internal temperature of the Inverter +**total_active_power** - Current power being generated by the inverter in Watts +**meter_power** - Power usage at the meter box, needs a smart meter installed. +ve means consuming from the grid, -ve means exporting to the grid +**load_power** - Power being consumed in total +**export_to_grid** - How much being currently exported to the grid. This is calculated from meter_power if -ve value, returned as a positive value +**import_from_grid** - How much being currently imported from grid. This is calculated from meter_power if +ve value +**timestamp** - Last time data was collected, based on Inverters clock by default -If you want to use the new Energy section in Home Assistant, follow the Home Assitant setup below.

(back to top)

## Home Assistant setup @@ -143,7 +178,7 @@ sensor.sensor_inverter_import_from_grid_integral: friendly_name: Grid Import ``` Restart Home Assistant for the sensors.yaml and customize.yaml to be loaded. -Make sure SunGather is running, wait 5 minutes for inital data to populate. +Make sure SunGather is running, wait 5 minutes for initial data to populate. Navigate to the Energy platform (Configuration > Energy). Add these sensors to the following areas: Electricity Grid > Grid Import - sensor.sensor_inverter_import_from_grid_integral diff --git a/SunGather/sungather.py b/SunGather/sungather.py index 351439d..524e25b 100644 --- a/SunGather/sungather.py +++ b/SunGather/sungather.py @@ -10,35 +10,59 @@ from datetime import datetime import importlib import logging import sys +import getopt import yaml import time import os logging.basicConfig( format='%(asctime)s %(levelname)-8s %(message)s', - level=20, + level=30, datefmt='%Y-%m-%d %H:%M:%S') + +try: + opts, args = getopt.getopt(sys.argv[1:],"hc:v:", "runonce") +except getopt.GetoptError: + logging.debug(f'No options passed via command line') + +configfile = 'config.yaml' + +for opt, arg in opts: + if opt == '-h': + print(f'\nSunGather {__version__}') + print(f'usage: python3 sungather.py [options]') + print(f'\nCommandling arguemnts override any config file settings') + print(f'Options and arguments:') + print(f'-c config.yaml : Specify config file.') + print(f'-v 30 : Logging Level, 10 = Debug, 20 = Info, 30 = Warnning (default), 40 = Error') + print(f'--runonce : Run once then exit') + print(f'-h : print this help message and exit (also --help)') + print(f'\nExample:') + print(f'python3 sungather.py -c /full/path/config.yaml\n') + sys.exit() + elif opt == '-c': + configfile = arg + elif opt == '-v': + if arg.isnumeric(): + if int(arg) >= 0 and int(arg) <= 50: + loglevel = int(arg) + else: + logging.error(f"Valid verbos options: 10 = Debug, 20 = Info, 30 = Warnning (default), 40 = Error") + sys.exit(2) + else: + logging.error(f"Valid verbos options: 10 = Debug, 20 = Info, 30 = Warnning (default), 40 = Error") + sys.exit(2) + elif opt == '--runonce': + runonce = True + logging.info(f'Starting SunGather {__version__}') -#opts = sys.getopt(sys.argv[1:],"hi:o:",["ifile=","ofile="]) - -IS_DOCKER = os.environ.get('IS_DOCKER', False) - -# If we are in docker, get config.py from a easy location to map via volumes -if IS_DOCKER: - try: - config = yaml.safe_load(open('/config/config.yaml')) - logging.info(f"Loaded config: /config/config.yaml") - except Exception as err: - logging.error(f"Failed: Loading config: /config/config.yaml {err}") - sys.exit(1) -else: - try: - config = yaml.safe_load(open('config.yaml')) - logging.info(f"Loaded config: {os.getcwd()}/config.yaml") - except Exception as err: - logging.error(f"Failed: Loading config: {os.getcwd()}/config.yaml {err}") - sys.exit(1) +try: + config = yaml.safe_load(open(configfile)) + logging.info(f"Loaded config: {configfile}") +except Exception as err: + logging.error(f"Failed: Loading config: {configfile} \n\t\t\t {err}") + sys.exit(1) try: registers = yaml.safe_load(open('registers.yaml')) @@ -47,8 +71,14 @@ except Exception as err: logging.error(f"Failed: Loading registers: {os.getcwd()}/registers.yaml {err}") sys.exit(1) -logging.getLogger().setLevel(config['inverter'].get('logging',30)) +if not config.get('inverter'): + logging.error(f"Failed Loading config, missing Inverter settings") + sys.exit(1) +if 'loglevel' in locals(): + logging.getLogger().setLevel(loglevel) +else: + logging.getLogger().setLevel(config['inverter'].get('logging',30)) exports = [] @@ -85,8 +115,9 @@ else: client = ModbusTcpClient(**client_payload) logging.info("Connection: " + str(client)) -client.connect() -client.close() +if not config['inverter'].get("connection") == "http": + client.connect() + client.close() def load_registers(register_type, start, count=100): try: @@ -98,11 +129,11 @@ def load_registers(register_type, start, count=100): else: raise RuntimeError(f"Unsupported register type: {type}") except Exception as err: - logging.warning(f'No data. Try increasing the timeout or scan interval for {register_type}, {start}:{count}: {err}') - return True + logging.warning(f'No data returned for {register_type}, {start}:{count}\n\t\t\t\t{str(err)}') + return if rr.isError(): - logging.warning("Modbus connection failed") + logging.warning(f"Modbus connection failed: {rr}") return if not hasattr(rr, 'registers'): @@ -309,5 +340,8 @@ while True: # logging.info("Exiting due to --one-shot") # break - # Sleep until the next scan - time.sleep(config['inverter'].get('scan_interval', 30)) \ No newline at end of file + if not 'runonce' in locals(): + # Sleep until the next scan + time.sleep(config['inverter'].get('scan_interval', 30)) + else: + sys.exit(0) \ No newline at end of file