mirror of
https://github.com/alexhopeoconnor/SunGather.git
synced 2026-10-23 19:12:11 +10:00
Improved error checking, added HTTP export, huge README update
This commit is contained in:
@@ -25,11 +25,11 @@
|
|||||||
## About The Project
|
## About The Project
|
||||||
Access ModBus data from almost any network connected Sungow Inverter.
|
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>
|
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
|
* Console - Log directly to screen
|
||||||
* MQTT - Load into MQTT, and optionally Home Assistance Discovery
|
* MQTT - Load into MQTT, and optionally Home Assistance Discovery
|
||||||
* PVOutput - Load into PVOutput.org
|
* 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>
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
### TO DO
|
### TO DO
|
||||||
* Commandline Arguments
|
|
||||||
* Better Home Assistant Support
|
* Better Home Assistant Support
|
||||||
|
|
||||||
|
|
||||||
@@ -81,16 +80,52 @@ python3 sungather.py
|
|||||||
docker pull bohdans/sungather
|
docker pull bohdans/sungather
|
||||||
docker run -v {path to}/config.yaml:/config/config.yaml -e TZ=Australia/Sydney --name sungather 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>
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
|
||||||
<!-- USAGE EXAMPLES -->
|
<!-- USAGE EXAMPLES -->
|
||||||
## Usage
|
## 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>
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
## Home Assistant setup
|
## Home Assistant setup
|
||||||
@@ -143,7 +178,7 @@ sensor.sensor_inverter_import_from_grid_integral:
|
|||||||
friendly_name: Grid Import
|
friendly_name: Grid Import
|
||||||
```
|
```
|
||||||
Restart Home Assistant for the sensors.yaml and customize.yaml to be loaded.
|
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:
|
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
|
Electricity Grid > Grid Import - sensor.sensor_inverter_import_from_grid_integral
|
||||||
|
|||||||
+59
-25
@@ -10,34 +10,58 @@ from datetime import datetime
|
|||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
import getopt
|
||||||
import yaml
|
import yaml
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format='%(asctime)s %(levelname)-8s %(message)s',
|
format='%(asctime)s %(levelname)-8s %(message)s',
|
||||||
level=20,
|
level=30,
|
||||||
datefmt='%Y-%m-%d %H:%M:%S')
|
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__}')
|
logging.info(f'Starting SunGather {__version__}')
|
||||||
|
|
||||||
#opts = sys.getopt(sys.argv[1:],"hi:o:",["ifile=","ofile="])
|
try:
|
||||||
|
config = yaml.safe_load(open(configfile))
|
||||||
IS_DOCKER = os.environ.get('IS_DOCKER', False)
|
logging.info(f"Loaded config: {configfile}")
|
||||||
|
except Exception as err:
|
||||||
# If we are in docker, get config.py from a easy location to map via volumes
|
logging.error(f"Failed: Loading config: {configfile} \n\t\t\t {err}")
|
||||||
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)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -47,8 +71,14 @@ except Exception as err:
|
|||||||
logging.error(f"Failed: Loading registers: {os.getcwd()}/registers.yaml {err}")
|
logging.error(f"Failed: Loading registers: {os.getcwd()}/registers.yaml {err}")
|
||||||
sys.exit(1)
|
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 = []
|
exports = []
|
||||||
@@ -85,8 +115,9 @@ else:
|
|||||||
client = ModbusTcpClient(**client_payload)
|
client = ModbusTcpClient(**client_payload)
|
||||||
logging.info("Connection: " + str(client))
|
logging.info("Connection: " + str(client))
|
||||||
|
|
||||||
client.connect()
|
if not config['inverter'].get("connection") == "http":
|
||||||
client.close()
|
client.connect()
|
||||||
|
client.close()
|
||||||
|
|
||||||
def load_registers(register_type, start, count=100):
|
def load_registers(register_type, start, count=100):
|
||||||
try:
|
try:
|
||||||
@@ -98,11 +129,11 @@ def load_registers(register_type, start, count=100):
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError(f"Unsupported register type: {type}")
|
raise RuntimeError(f"Unsupported register type: {type}")
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logging.warning(f'No data. Try increasing the timeout or scan interval for {register_type}, {start}:{count}: {err}')
|
logging.warning(f'No data returned for {register_type}, {start}:{count}\n\t\t\t\t{str(err)}')
|
||||||
return True
|
return
|
||||||
|
|
||||||
if rr.isError():
|
if rr.isError():
|
||||||
logging.warning("Modbus connection failed")
|
logging.warning(f"Modbus connection failed: {rr}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not hasattr(rr, 'registers'):
|
if not hasattr(rr, 'registers'):
|
||||||
@@ -309,5 +340,8 @@ while True:
|
|||||||
# logging.info("Exiting due to --one-shot")
|
# logging.info("Exiting due to --one-shot")
|
||||||
# break
|
# break
|
||||||
|
|
||||||
|
if not 'runonce' in locals():
|
||||||
# Sleep until the next scan
|
# Sleep until the next scan
|
||||||
time.sleep(config['inverter'].get('scan_interval', 30))
|
time.sleep(config['inverter'].get('scan_interval', 30))
|
||||||
|
else:
|
||||||
|
sys.exit(0)
|
||||||
Reference in New Issue
Block a user