mirror of
https://github.com/alexhopeoconnor/SunGather.git
synced 2026-10-16 07:51:36 +10:00
Improved error checking, added HTTP export, huge README update
This commit is contained in:
@@ -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: <a href="https://github.com/bohdan-s/Sungrow-Inverter/blob/main/Modbus%20Information/Communication%20Protocol%20of%20PV%20Grid-Connected%20String%20Inverters_V1.1.37_EN.pdf">Communication Protocol of PV Grid-Connected String Inverters</a> and <a hreh="https://github.com/bohdan-s/Sungrow-Inverter/blob/main/Modbus%20Information/communication-protocol-of-residential-hybrid-inverterv1.0.20-1.pdf">Communication Protocol of Residential Hybrid Inverters</a>
|
||||
|
||||
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
|
||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||
|
||||
### 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
|
||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||
|
||||
|
||||
<!-- USAGE EXAMPLES -->
|
||||
## 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.
|
||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||
|
||||
|
||||
## 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://<serverip>: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.
|
||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||
|
||||
## 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
|
||||
|
||||
+62
-28
@@ -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))
|
||||
if not 'runonce' in locals():
|
||||
# Sleep until the next scan
|
||||
time.sleep(config['inverter'].get('scan_interval', 30))
|
||||
else:
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user