mirror of
https://github.com/alexhopeoconnor/SunGather.git
synced 2026-10-04 02:48:12 +10:00
v0.4.0: HomeAssistant Support!
This commit is contained in:
@@ -37,6 +37,7 @@ On first connection the tool will query your inverter, retrieve the model and re
|
||||
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 href="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 multiple export locations out of the box:
|
||||
* Full Home Assistant Support, as HassIO add-on
|
||||
* Console - Log directly to screen
|
||||
* MQTT - Load into MQTT, and optionally Home Assistance including Discovery
|
||||
* PVOutput - Load into PVOutput.org
|
||||
@@ -53,9 +54,6 @@ I have learned a lot from the following projects, THANK YOU
|
||||
|
||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||
|
||||
## Roadmap / TO DO
|
||||
* Full Home Assistant integration, as HACS addon
|
||||
|
||||
|
||||
## Built With
|
||||
|
||||
@@ -174,6 +172,10 @@ For SG* Models this is calculated from meter_power if -ve value, returned as a p
|
||||
|
||||
### Home Assistant setup
|
||||
|
||||
#### The following are manual steps if you are running SunGather separate,
|
||||
#### For the easy way see: <a href="https://github.com/bohdan-s/hassio-repository">Hassio Repository</a></b>
|
||||
|
||||
|
||||
In the SunGather config.yaml you need to set the smart_meter if you have one
|
||||
_For Hybrid Models smart_meter doesn't need to get enabled_
|
||||
|
||||
@@ -182,46 +184,20 @@ _For Hybrid Models smart_meter doesn't need to get enabled_
|
||||
smart_meter: True
|
||||
```
|
||||
|
||||
HA sensors created by the MQTT auto-discovery:
|
||||
Enable MQTT, configure the server and HA AutoDiscovery
|
||||
```
|
||||
sensor.inverter_active_power
|
||||
sensor.inverter_daily_generation
|
||||
sensor.inverter_export_to_grid
|
||||
sensor.inverter_import_from_grid
|
||||
sensor.inverter_load_power
|
||||
sensor.inverter_meter_power
|
||||
|
||||
- name: mqtt
|
||||
enabled: True
|
||||
host: 192.168.1.200
|
||||
homeassistant: True
|
||||
```
|
||||
|
||||
The Inverter reports Power (W), but HA needs Energy (Wh).
|
||||
You can use a "Riemann sum integral" (called integration) to convert it.
|
||||
Put the following into your sensors.yaml
|
||||
```
|
||||
sensor:
|
||||
- platform: integration
|
||||
source: sensor.inverter_active_power
|
||||
name: Solar Production (Sungather)
|
||||
- platform: integration
|
||||
source: sensor.inverter_export_to_grid
|
||||
name: Return to Grid (Sungather)
|
||||
- platform: integration
|
||||
source: sensor.inverter_import_from_grid
|
||||
name: Grid Consumption (Sungather)
|
||||
```
|
||||
Electricity Grid > Grid Consumption -> Daily Import from Grid
|
||||
Electricity Grid > Return to Grid -> Daily Export to Grid
|
||||
Solar Panels > Solar Production -> Daily Generation
|
||||
|
||||
The additional sensors then setup in HA are the following with Wh unit of measurement for the Energy platform.
|
||||
<img src="img/HomeAssistant-SunGrow_Device.png"/>
|
||||
|
||||
Solar Production (Sungather)
|
||||
Return to Grid (Sungather)
|
||||
Grid Consumption (Sungather)
|
||||
|
||||
Restart Home Assistant for the sensors.yaml to be loaded.
|
||||
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 Consumption -> Grid Consumption (Sungather)
|
||||
Electricity Grid > Return to Grid -> Return to Grid (Sungather)
|
||||
Solar Panels > Solar Production -> Solar Production (Sungather)
|
||||
|
||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ inverter:
|
||||
# retries: 3 # [Optional] Default is 3, how many times to retry if connection fails
|
||||
# slave: 0x01 # [Optional] Default is 0x01
|
||||
# scan_interval: 30 # [Optional] Default is 30
|
||||
connection: modbus # [Required] options: modbus, sungrow, http
|
||||
connection: modbus # [Required] options: modbus, sungrow, http
|
||||
# model: "SG7.0RT" # [Optional] This is autodetected on startup, only needed if detection issues or for testing
|
||||
# See model list here: https://github.com/bohdan-s/SunGather#supported
|
||||
# serial: xxxxxxxxxx # [Optional] This is autodetected on startup, only needed if detection issues or for testing, used as a unique ID
|
||||
@@ -24,7 +24,7 @@ inverter:
|
||||
exports:
|
||||
# Print Registers to console, good for debugging / troubleshooting
|
||||
- name: console
|
||||
enabled: False # [Optional] Default is False
|
||||
enabled: True # [Optional] Default is False
|
||||
|
||||
# Runs a simple Webserver showing Config and last read registers
|
||||
# Access at http://localhost:8080 or http://[serverip]:8080
|
||||
@@ -38,7 +38,6 @@ exports:
|
||||
enabled: False # [Optional] Default is False
|
||||
# port: 8082 # [Optional] Default is 8080
|
||||
|
||||
|
||||
# Output data to InfluxDB
|
||||
- name: influxdb
|
||||
enabled: False # [Optional] Default is False
|
||||
@@ -69,53 +68,60 @@ exports:
|
||||
enabled: False # [Optional] Default is False
|
||||
host: 192.168.1.200 # [Required] IP or Hostname of MQTT Server
|
||||
# port: 1883 # [Optional] Default 1883
|
||||
# topic: "inverter/{model}/registers" # [Optional] Default: "inverter/{model}/registers", Variable {model} will be replaced with model number of inverter
|
||||
# topic: "SunGather/{serial_number}" # [Optional] Default: "SunGather/{serial_number}"
|
||||
# username: # [Optional] Username is MQTT server requires it
|
||||
# password: # [Optional] Password is MQTT server requires it
|
||||
# client_id: # [Optional] Client id for mqtt connection. Defaults to "SunGather", need to be set to different values if you want to run multiple instances of sungather.
|
||||
homeassistant: False # [Optional] Default False, Publish discovery and sensors for HomeAssistant
|
||||
ha_sensors: # [Optional] / [Required] homeassistant: True, Topics to enable discovery for HA
|
||||
# client_id: # [Optional] Client id for mqtt connection. Defaults to Serial Number.
|
||||
homeassistant: True
|
||||
ha_sensors:
|
||||
- name: "Daily Generation"
|
||||
sensor_type: sensor
|
||||
register: daily_power_yields
|
||||
dev_class: energy
|
||||
device_class: energy
|
||||
state_class: total_increasing
|
||||
- name: "Active Power"
|
||||
icon: "mdi:solar-power"
|
||||
- name: "Daily Export to Grid"
|
||||
sensor_type: sensor
|
||||
register: total_active_power
|
||||
dev_class: power
|
||||
state_class: measurement
|
||||
- name: "Load Power"
|
||||
register: daily_export_to_grid
|
||||
device_class: energy
|
||||
state_class: total_increasing
|
||||
icon: "mdi:transmission-tower-export"
|
||||
value_template: "{{ value_json.daily_export_to_grid | round(2) }}"
|
||||
last_reset_value_template: "{{ value_json.last_reset }}"
|
||||
- name: "Daily Import from Grid"
|
||||
sensor_type: sensor
|
||||
register: load_power
|
||||
dev_class: power
|
||||
state_class: measurement
|
||||
- name: "Meter Power"
|
||||
sensor_type: sensor
|
||||
register: meter_power
|
||||
dev_class: power
|
||||
state_class: measurement
|
||||
- name: "Export to Grid"
|
||||
sensor_type: sensor
|
||||
register: export_to_grid
|
||||
dev_class: power
|
||||
state_class: measurement
|
||||
- name: "Import from Grid"
|
||||
sensor_type: sensor
|
||||
register: import_from_grid
|
||||
dev_class: power
|
||||
state_class: measurement
|
||||
register: daily_import_from_grid
|
||||
device_class: energy
|
||||
state_class: total_increasing
|
||||
icon: "mdi:transmission-tower-import"
|
||||
value_template: "{{ value_json.daily_import_from_grid | round(2) }}"
|
||||
last_reset_value_template: "{{ value_json.last_reset }}"
|
||||
- name: "Temperature"
|
||||
sensor_type: sensor
|
||||
register: internal_temperature
|
||||
dev_class: temperature
|
||||
device_class: temperature
|
||||
state_class: measurement
|
||||
- name: "Power State"
|
||||
sensor_type: binary_sensor
|
||||
register: run_state
|
||||
dev_class: running
|
||||
device_class: running
|
||||
payload_on: "ON"
|
||||
payload_off: "OFF"
|
||||
- name: "Active Generation"
|
||||
sensor_type: sensor
|
||||
register: total_active_power
|
||||
dev_class: power
|
||||
state_class: measurement
|
||||
- name: "Active Export to Grid"
|
||||
sensor_type: sensor
|
||||
register: export_to_grid
|
||||
dev_class: power
|
||||
state_class: measurement
|
||||
- name: "Active Import from Grid"
|
||||
sensor_type: sensor
|
||||
register: import_from_grid
|
||||
dev_class: power
|
||||
state_class: measurement
|
||||
|
||||
# Publish Registers to PVOutput
|
||||
- name: pvoutput
|
||||
|
||||
+39
-25
File diff suppressed because one or more lines are too long
@@ -25,7 +25,10 @@ class export_webserver(object):
|
||||
return True
|
||||
|
||||
def publish(self, inverter):
|
||||
body = "<h3>Sungather v" + __version__ + "</h3></p><table><th>Address</th><tr><th>Register</th><th>Value</th></tr>"
|
||||
body = "<h3>SunGather v" + __version__ + "</h3></p>"
|
||||
body += "<h4>Need Help? <href a='https://github.com/bohdan-s/SunGather'>https://github.com/bohdan-s/SunGather</a></h4></p>"
|
||||
body += "<h4>NEW HomeAssistant Add-on: <href a='https://github.com/bohdan-s/hassio-repository'>https://github.com/bohdan-s/SunGather</a></h4></p>"
|
||||
body += "<table><th>Address</th><tr><th>Register</th><th>Value</th></tr>"
|
||||
metrics = ""
|
||||
for register, value in inverter.latest_scrape.items():
|
||||
body += f"<tr><td>{str(inverter.getRegisterAddress(register))}</td><td>{str(register)}</td><td>{str(value)} {str(inverter.getRegisterUnit(register))}</td></tr>"
|
||||
@@ -53,7 +56,10 @@ class MyServer(BaseHTTPRequestHandler):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(bytes("<html><head><title>SunGather</title><meta charset='UTF-8'><meta http-equiv='refresh' content='15'></head>", "utf-8"))
|
||||
self.wfile.write(bytes("<html><head><title>SunGather</title>", "utf-8"))
|
||||
self.wfile.write(bytes("<meta charset='UTF-8'><meta http-equiv='refresh' content='15'>", "utf-8"))
|
||||
self.wfile.write(bytes('<style media = "all"> body { background-color: black; color: white; } @media screen and (prefers-color-scheme: light) { body { background-color: white; color: black; } } </style>', "utf-8"))
|
||||
self.wfile.write(bytes("</head>", "utf-8"))
|
||||
self.wfile.write(bytes("<body>", "utf-8"))
|
||||
self.wfile.write(bytes(export_webserver.html_body, "utf-8"))
|
||||
self.wfile.write(bytes("</table>", "utf-8"))
|
||||
|
||||
+108
-55
@@ -25,19 +25,29 @@ class SungrowInverter():
|
||||
"RetryOnEmpty": False,
|
||||
}
|
||||
self.inverter_config = {
|
||||
"slave": config_inverter.get('slave'),
|
||||
"model": config_inverter.get('model'),
|
||||
"serial": config_inverter.get('serial'),
|
||||
"serial_number": config_inverter.get('serial_number'),
|
||||
"level": config_inverter.get('level'),
|
||||
"scan_interval": config_inverter.get('scan_interval'),
|
||||
"use_local_time": config_inverter.get('use_local_time'),
|
||||
"smart_meter": config_inverter.get('smart_meter'),
|
||||
"connection": config_inverter.get('connection')
|
||||
"connection": config_inverter.get('connection'),
|
||||
"slave": config_inverter.get('slave'),
|
||||
"start_time": ""
|
||||
}
|
||||
self.client = None
|
||||
|
||||
self.registers = [[]]
|
||||
self.registers.pop() # Remove null value from list
|
||||
self.registers_custom = [{'name': 'export_to_grid', 'unit': 'W', 'address': 'vr001'}, {'name': 'import_from_grid', 'unit': 'W', 'address': 'vr002'}, {'name': 'run_state', 'address': 'vr003'}, {'name': 'timestamp', 'address': 'vr004'}]
|
||||
self.registers_custom = [ {'name': 'run_state', 'address': 'vr001'},
|
||||
{'name': 'timestamp', 'address': 'vr002'},
|
||||
{'name': 'last_reset', 'address': 'vr003'},
|
||||
{'name': 'export_to_grid', 'unit': 'W', 'address': 'vr004'},
|
||||
{'name': 'import_from_grid', 'unit': 'W', 'address': 'vr005'},
|
||||
{'name': 'daily_export_to_grid', 'unit': 'kWh', 'address': 'vr006'},
|
||||
{'name': 'daily_import_from_grid', 'unit': 'kWh', 'address': 'vr007'}
|
||||
]
|
||||
|
||||
self.register_ranges = [[]]
|
||||
self.register_ranges.pop() # Remove null value from list
|
||||
|
||||
@@ -330,12 +340,19 @@ class SungrowInverter():
|
||||
def scrape(self):
|
||||
scrape_start = datetime.now()
|
||||
|
||||
# Clear previous inverter values, keep the model and run state
|
||||
if self.latest_scrape.get("run_state"): run_state = self.latest_scrape.get("run_state")
|
||||
else: run_state = "ON"
|
||||
# Clear previous inverter values, persist some values
|
||||
persist_registers = {
|
||||
"run_state": self.latest_scrape.get("run_state","ON"),
|
||||
"last_reset": self.latest_scrape.get("last_reset",""),
|
||||
"daily_export_to_grid": self.latest_scrape.get("daily_export_to_grid",0),
|
||||
"daily_import_from_grid": self.latest_scrape.get("daily_import_from_grid",0),
|
||||
}
|
||||
|
||||
self.latest_scrape = {}
|
||||
self.latest_scrape['device_type_code'] = self.inverter_config['model']
|
||||
self.latest_scrape["run_state"] = run_state
|
||||
|
||||
for register, value in persist_registers.items():
|
||||
self.latest_scrape[register] = value
|
||||
|
||||
load_registers_count = 0
|
||||
load_registers_failed = 0
|
||||
@@ -347,7 +364,7 @@ class SungrowInverter():
|
||||
load_registers_failed +=1
|
||||
if load_registers_failed == load_registers_count:
|
||||
# If every scrape fails, disconnect the client
|
||||
logging.warning
|
||||
#logging.warning
|
||||
self.disconnect()
|
||||
return False
|
||||
if load_registers_failed > 0:
|
||||
@@ -356,52 +373,7 @@ class SungrowInverter():
|
||||
# Leave connection open, see if helps resolve the connection issues
|
||||
#self.close()
|
||||
|
||||
# Create a registers for Power imported and exported to/from Grid
|
||||
if self.inverter_config['level'] >= 1:
|
||||
self.latest_scrape["export_to_grid"] = 0
|
||||
self.latest_scrape["import_from_grid"] = 0
|
||||
|
||||
if self.validateRegister('meter_power'):
|
||||
try:
|
||||
power = self.latest_scrape.get('meter_power', self.latest_scrape.get('export_power', 0))
|
||||
if power < 0:
|
||||
self.latest_scrape["export_to_grid"] = abs(power)
|
||||
elif power >= 0:
|
||||
self.latest_scrape["import_from_grid"] = power
|
||||
except Exception:
|
||||
pass
|
||||
# in this case we connected to a hybrid inverter and need to use export_power_hybrid
|
||||
# export_power_hybrid is negative in case of importing from the grid
|
||||
elif self.validateRegister('export_power_hybrid'):
|
||||
try:
|
||||
power = self.latest_scrape.get('export_power_hybrid', 0)
|
||||
if power < 0:
|
||||
self.latest_scrape["import_from_grid"] = abs(power)
|
||||
elif power >= 0:
|
||||
self.latest_scrape["export_to_grid"] = power
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try: # If inverter is returning no data for load_power, we can calculate it manually
|
||||
if not self.latest_scrape["load_power"]:
|
||||
self.latest_scrape["load_power"] = int(self.latest_scrape.get('total_active_power')) + int(self.latest_scrape.get('meter_power'))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# See if the inverter is running, This is added to inverters so can be read via MQTT etc...
|
||||
# It is also used below, as some registers hold the last value on 'stop' so we need to set to 0
|
||||
# to help with graphing.
|
||||
try:
|
||||
if self.latest_scrape.get('start_stop'):
|
||||
if self.latest_scrape.get('start_stop', False) == 'Start' and self.latest_scrape.get('work_state_1', False).contains('Run'):
|
||||
self.latest_scrape["run_state"] = "ON"
|
||||
else:
|
||||
self.latest_scrape["run_state"] = "OFF"
|
||||
else:
|
||||
self.latest_scrape["run_state"] = "OFF"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
## vr002
|
||||
if self.inverter_config.get('use_local_time',False):
|
||||
self.latest_scrape["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
logging.debug(f'Using Local Computer Time: {self.latest_scrape.get("timestamp")}')
|
||||
@@ -451,6 +423,85 @@ class SungrowInverter():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
### Custom Registers
|
||||
######################
|
||||
|
||||
## vr001 - run_state
|
||||
# See if the inverter is running, This is added to inverters so can be read via MQTT etc...
|
||||
# It is also used below, as some registers hold the last value on 'stop' so we need to set to 0
|
||||
# to help with graphing.
|
||||
try:
|
||||
if self.latest_scrape.get('start_stop'):
|
||||
logging.info(f"DEBUG: start_stop:{self.latest_scrape.get('start_stop', 'null')} work_state_1:{self.latest_scrape.get('work_state_1', 'null')}")
|
||||
if self.latest_scrape.get('start_stop', False) == 'Start' and self.latest_scrape.get('work_state_1', False).contains('Run'):
|
||||
self.latest_scrape["run_state"] = "ON"
|
||||
else:
|
||||
self.latest_scrape["run_state"] = "OFF"
|
||||
else:
|
||||
logging.info(f"DEBUG: Couldn't read start_stop so run_state is OFF")
|
||||
self.latest_scrape["run_state"] = "OFF"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
## vr003 - last_reset
|
||||
date_format = "%Y-%m-%d %H:%M:%S"
|
||||
if not self.latest_scrape.get('last_reset', False):
|
||||
logging.info('Setting Initial Daily registers; daily_export_to_grid, daily_import_from_grid, last_reset')
|
||||
self.latest_scrape["daily_export_to_grid"] = 0
|
||||
self.latest_scrape["daily_import_from_grid"] = 0
|
||||
self.latest_scrape['last_reset'] = self.latest_scrape["timestamp"]
|
||||
elif datetime.strptime(self.latest_scrape['last_reset'], date_format).date() < datetime.strptime(self.latest_scrape['timestamp'], date_format).date():
|
||||
logging.info('last_reset: ' + self.latest_scrape['last_reset'] + ', timestamp: ' + self.latest_scrape['timestamp'])
|
||||
logging.info('Resetting Daily registers; daily_export_to_grid, daily_import_from_grid, last_reset')
|
||||
self.latest_scrape["daily_export_to_grid"] = 0
|
||||
self.latest_scrape["daily_import_from_grid"] = 0
|
||||
self.latest_scrape['last_reset'] = self.latest_scrape["timestamp"]
|
||||
|
||||
## vr004 - import_from_grid, vr005 - export_to_grid
|
||||
# Create a registers for Power imported and exported to/from Grid
|
||||
if self.inverter_config['level'] >= 1:
|
||||
self.latest_scrape["export_to_grid"] = 0
|
||||
self.latest_scrape["import_from_grid"] = 0
|
||||
|
||||
if self.validateRegister('meter_power'):
|
||||
try:
|
||||
power = self.latest_scrape.get('meter_power', self.latest_scrape.get('export_power', 0))
|
||||
if power < 0:
|
||||
self.latest_scrape["export_to_grid"] = abs(power)
|
||||
elif power >= 0:
|
||||
self.latest_scrape["import_from_grid"] = power
|
||||
except Exception:
|
||||
pass
|
||||
# in this case we connected to a hybrid inverter and need to use export_power_hybrid
|
||||
# export_power_hybrid is negative in case of importing from the grid
|
||||
elif self.validateRegister('export_power_hybrid'):
|
||||
try:
|
||||
power = self.latest_scrape.get('export_power_hybrid', 0)
|
||||
if power < 0:
|
||||
self.latest_scrape["import_from_grid"] = abs(power)
|
||||
elif power >= 0:
|
||||
self.latest_scrape["export_to_grid"] = power
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try: # If inverter is returning no data for load_power, we can calculate it manually
|
||||
if not self.latest_scrape["load_power"]:
|
||||
self.latest_scrape["load_power"] = int(self.latest_scrape.get('total_active_power')) + int(self.latest_scrape.get('meter_power'))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
## vr004, vr005
|
||||
if not self.latest_scrape.get('daily_export_to_grid', False):
|
||||
self.latest_scrape["daily_export_to_grid"] = 0
|
||||
|
||||
self.latest_scrape["daily_export_to_grid"] += ((self.latest_scrape["export_to_grid"] / 1000) * (self.inverter_config['scan_interval'] / 60 / 60) )
|
||||
|
||||
## vr005
|
||||
if not self.latest_scrape.get('daily_import_from_grid', False):
|
||||
self.latest_scrape["daily_import_from_grid"] = 0
|
||||
|
||||
self.latest_scrape["daily_import_from_grid"] += ((self.latest_scrape["import_from_grid"] / 1000) * (self.inverter_config['scan_interval'] / 60 / 60) )
|
||||
|
||||
scrape_end = datetime.now()
|
||||
logging.info(f'Inverter: Successfully scraped in {(scrape_end - scrape_start).seconds}.{(scrape_end - scrape_start).microseconds} secs')
|
||||
|
||||
@@ -502,6 +553,8 @@ def main():
|
||||
runonce = True
|
||||
|
||||
logging.info(f'Starting SunGather {__version__}')
|
||||
logging.info(f'Need Help? https://github.com/bohdan-s/SunGather')
|
||||
logging.info(f'NEW HomeAssistant Add-on: https://github.com/bohdan-s/hassio-repository')
|
||||
|
||||
try:
|
||||
configfile = yaml.safe_load(open(configfilename, encoding="utf-8"))
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = '0.3.9'
|
||||
__version__ = '0.4.0'
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 354 KiB |
Reference in New Issue
Block a user