mirror of
https://github.com/alexhopeoconnor/SunGather.git
synced 2026-10-23 02:52:10 +10:00
Merge branch 'bohdan-s:main' into bohdan-s-main
This commit is contained in:
+118
@@ -0,0 +1,118 @@
|
|||||||
|
config.yaml
|
||||||
|
*.log.*
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
config.yaml
|
config.yaml
|
||||||
|
*.log.*
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
+5
-2
@@ -5,13 +5,16 @@ RUN useradd sungather
|
|||||||
WORKDIR /usr/src/sungather
|
WORKDIR /usr/src/sungather
|
||||||
|
|
||||||
COPY requirements.txt ./
|
COPY requirements.txt ./
|
||||||
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
# pycryptodomex 3.14 currently fails to compile for arm64
|
||||||
|
#RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
||||||
|
RUN pip install --no-cache-dir --upgrade pycryptodomex==3.11.0 -r requirements.txt
|
||||||
|
|
||||||
COPY SunGather/ .
|
COPY SunGather/ .
|
||||||
|
|
||||||
|
VOLUME /logs
|
||||||
VOLUME /config
|
VOLUME /config
|
||||||
COPY SunGather/config-example.yaml /config/config.yaml
|
COPY SunGather/config-example.yaml /config/config.yaml
|
||||||
|
|
||||||
USER sungather
|
USER sungather
|
||||||
|
|
||||||
CMD [ "python", "sungather.py", "-c", "/config/config.yaml" ]
|
CMD [ "python", "sungather.py", "-c", "/config/config.yaml", "-l", "/logs/" ]
|
||||||
@@ -9,12 +9,14 @@
|
|||||||
<br />
|
<br />
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<h2 align="center">SunGather</h3>
|
<h2 align="center">SunGather</h2>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Collect data from Sungrow Inverters using ModbusTcpClient, <a href="https://github.com/rpvelloso/Sungrow-Modbus">SungrowModbusTcpClient</a> or <a href="https://github.com/bohdan-s/SungrowModbusWebClient">SungrowModbusWebClient</a> and export to various locations.
|
Collect data from Sungrow Inverters using ModbusTcpClient, <a href="https://github.com/rpvelloso/Sungrow-Modbus">SungrowModbusTcpClient</a> or <a href="https://github.com/bohdan-s/SungrowModbusWebClient">SungrowModbusWebClient</a> and export to various locations.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
<a href="https://sungather.app">Website Sungather.app</a>
|
||||||
|
·
|
||||||
<a href="https://github.com/bohdan-s/SunGather/issues">Report Bug</a>
|
<a href="https://github.com/bohdan-s/SunGather/issues">Report Bug</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/bohdan-s/SunGather/issues">Request Feature</a>
|
<a href="https://github.com/bohdan-s/SunGather/issues">Request Feature</a>
|
||||||
@@ -30,7 +32,7 @@ Access ModBus data from almost any network connected Sungow Inverter.
|
|||||||
|
|
||||||
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.
|
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 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:
|
Has multiple export locations out of the box:
|
||||||
* Console - Log directly to screen
|
* Console - Log directly to screen
|
||||||
@@ -51,64 +53,19 @@ I have learned a lot from the following projects, THANK YOU
|
|||||||
## Raodmap / TO DO
|
## Raodmap / TO DO
|
||||||
* Full Home Assistant integration, as HACS addon
|
* Full Home Assistant integration, as HACS addon
|
||||||
|
|
||||||
## Updates
|
|
||||||
**0.3.2**
|
|
||||||
* Added logging to file
|
|
||||||
* Hopefully better connection recovery
|
|
||||||
* Bug fixes
|
|
||||||
|
|
||||||
**0.3.0**
|
|
||||||
**IMPORTANT: If updating from v0.1.x or v0.2.x please check config against config-example. some options for MQTT and PVOutput have changed**
|
|
||||||
* Heaps bug fixes
|
|
||||||
* Fixed PVOutput not working after midnight
|
|
||||||
* MQTT now auto-reconnects
|
|
||||||
* MQTT/PVOutput now verify values exist before trying to publish
|
|
||||||
* Rasberry Pi Docker support
|
|
||||||
* lots more...
|
|
||||||
|
|
||||||
|
|
||||||
**0.2.2**
|
|
||||||
* Minor bug fixes
|
|
||||||
* Add support for some inverter scrapes to fail
|
|
||||||
* Add support for zero_on_standby, this will 0 registers like temp that 'stick' to the last value when the inverter goes into standy. Handy if you want to graph 0 instead of the last read overnight
|
|
||||||
|
|
||||||
**0.2.1**
|
|
||||||
* Minor bug fixes
|
|
||||||
* Updated config-example.yaml to clearly explain each option and if it is required or optional
|
|
||||||
* Replace is_running with run_state, was using bool which was causing errors in MQTT, run_state uses "ON" and "OFF"
|
|
||||||
|
|
||||||
**0.2.0**
|
|
||||||
**IMPORTANT: If updating from v0.1.x please check config against config-example. some options for MQTT and PVOutput have changed**
|
|
||||||
* Re-write to Inverter scanning code, improved performance and more resilient to failures
|
|
||||||
* Re-write to PVOutput code, now uploads every 5 minutes as per API documentation, averages all data points over the 5 min window to reduce random high/low values
|
|
||||||
* Re-Write to MQTT code, now supports multiple sensor types
|
|
||||||
|
|
||||||
**0.1.3**
|
|
||||||
* Improved error recovery, e.g. Inverter powers down overnight
|
|
||||||
|
|
||||||
**0.1.2**
|
|
||||||
* Added InfluxDB export
|
|
||||||
|
|
||||||
**0.1.1**
|
|
||||||
* Added docker image
|
|
||||||
* Added simple http web server
|
|
||||||
* Improved recovery when errors (See: SungrowModbusWebClient 0.2.6)
|
|
||||||
|
|
||||||
**0.1.0**
|
|
||||||
Initial build
|
|
||||||
|
|
||||||
### Built With
|
### Built With
|
||||||
|
|
||||||
* [Python3](https://www.python.org/)
|
* [Python3](https://www.python.org/)
|
||||||
|
|
||||||
### Requires
|
### Requires
|
||||||
* [paho-mqtt>=1.5.1](https://pypi.org/project/paho-mqtt/)
|
* [paho-mqtt](https://pypi.org/project/paho-mqtt/)
|
||||||
* [pymodbus>=2.4.0](https://pypi.org/project/pymodbus/)
|
* [pymodbus](https://pypi.org/project/pymodbus/)
|
||||||
* [SungrowModbusTcpClient>=0.1.6](https://pypi.org/project/SungrowModbusTcpClient/)
|
* [SungrowModbusTcpClient](https://pypi.org/project/SungrowModbusTcpClient/)
|
||||||
* [SungrowModbusWebClient>=0.2.6](https://pypi.org/project/SungrowModbusWebClient/)
|
* [SungrowModbusWebClient](https://pypi.org/project/SungrowModbusWebClient/)
|
||||||
* [PyYAML>=6.0] (https://pypi.org/project/PyYAML/)
|
* [PyYAML](https://pypi.org/project/PyYAML/)
|
||||||
* [requests>=2.26.0](https://pypi.org/project/requests/)
|
* [requests](https://pypi.org/project/requests/)
|
||||||
* [influxdb-client>=1.24.0](https://pypi.org/project/influxdb-client/)
|
* [influxdb-client](https://pypi.org/project/influxdb-client/)
|
||||||
|
|
||||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
@@ -130,13 +87,21 @@ python3 sungather.py
|
|||||||
```
|
```
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
|
docker run options:
|
||||||
|
-v {path to}/config.yaml:/config/config.yaml <- Set Config File Location
|
||||||
|
-v {logpath}:/logs <- Set Log folder Location if using log to file (also set log_file in config.yaml)
|
||||||
|
-e TZ=Australia/Sydney <- Set Timezone
|
||||||
|
-p 8080:8080 <- Set Web server port (only if using the webserver export)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
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 -d --name sungather \
|
||||||
```
|
--restart always \
|
||||||
or if using the webserver export
|
-v {path to}/config.yaml:/config/config.yaml \
|
||||||
```sh
|
-v {logpath}:/logs \
|
||||||
docker run -v {path to}/config.yaml:/config/config.yaml -e TZ=Australia/Sydney -p 8080:8080 --name sungather bohdans/sungather
|
-e TZ=Australia/Sydney \
|
||||||
|
-p 8080:8080 \
|
||||||
|
bohdans/sungather
|
||||||
```
|
```
|
||||||
Note: replace Australia/Sydney with relevant 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>
|
||||||
@@ -219,6 +184,8 @@ sensor.inverter_meter_power
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
Put the following into your sensors.yaml
|
||||||
```
|
```
|
||||||
sensor:
|
sensor:
|
||||||
@@ -283,6 +250,7 @@ docker push bohdans/sungather -a
|
|||||||
|
|
||||||
|
|
||||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
<!-- LICENSE -->
|
<!-- LICENSE -->
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -290,7 +258,6 @@ Distributed under the GPL3 License. See `LICENSE.txt` for more information.
|
|||||||
|
|
||||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
|
||||||
<!-- CONTACT -->
|
<!-- CONTACT -->
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
@@ -298,7 +265,6 @@ Project Link: [https://github.com/bohdan-s/SungrowModbusWebClient](https://githu
|
|||||||
|
|
||||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
|
||||||
<!-- ACKNOWLEDGMENTS -->
|
<!-- ACKNOWLEDGMENTS -->
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,12 @@ class export_mqtt(object):
|
|||||||
return name.lower().replace(' ','_')
|
return name.lower().replace(' ','_')
|
||||||
|
|
||||||
def publish(self, inverter):
|
def publish(self, inverter):
|
||||||
if not self.mqtt_client.is_connected():
|
try:
|
||||||
logging.warning(f'MQTT: Server Disconnected; {self.mqtt_queue.__len__()} messages queued, will automatically attempt to reconnect')
|
if not self.mqtt_client.is_connected():
|
||||||
|
logging.warning(f'MQTT: Server Disconnected; {self.mqtt_queue.__len__()} messages queued, will automatically attempt to reconnect')
|
||||||
|
except Exception as err:
|
||||||
|
logging.warning(f'MQTT: Server Error; Server not configured')
|
||||||
|
return False
|
||||||
# qos=0 is set, so no acknowledgment is sent, rending this check useless
|
# qos=0 is set, so no acknowledgment is sent, rending this check useless
|
||||||
#elif self.mqtt_queue.__len__() > 10:
|
#elif self.mqtt_queue.__len__() > 10:
|
||||||
# logging.warning(f'MQTT: {self.mqtt_queue.__len__()} messages queued, this may be due to a MQTT server issue')
|
# logging.warning(f'MQTT: {self.mqtt_queue.__len__()} messages queued, this may be due to a MQTT server issue')
|
||||||
|
|||||||
@@ -125,10 +125,17 @@ class export_pvoutput(object):
|
|||||||
# Add new data to old data and increase count of data points
|
# Add new data to old data and increase count of data points
|
||||||
for parameter in self.pvoutput_parameters:
|
for parameter in self.pvoutput_parameters:
|
||||||
value = inverter.getRegisterValue(parameter.get('register'))
|
value = inverter.getRegisterValue(parameter.get('register'))
|
||||||
|
|
||||||
if parameter.get('multiple'):
|
if parameter.get('multiple'):
|
||||||
value = value * parameter.get('multiple')
|
value = value * parameter.get('multiple')
|
||||||
|
|
||||||
if self.collected_data.get(parameter.get('name'),False):
|
# If using Cumulative Energy we just need the last data point, not the average
|
||||||
|
if parameter.get('name') == 'v1' and (self.pvoutput_config['cumulative_flag'] == 1 or self.pvoutput_config['cumulative_flag'] == 2):
|
||||||
|
self.collected_data[parameter.get('name')] = value
|
||||||
|
elif parameter.get('name') == 'v3' and (self.pvoutput_config['cumulative_flag'] == 1 or self.pvoutput_config['cumulative_flag'] == 3):
|
||||||
|
self.collected_data[parameter.get('name')] = value
|
||||||
|
# Add the last data point to the previous data point if exists, otherwise set as the last data point
|
||||||
|
elif self.collected_data.get(parameter.get('name'),False):
|
||||||
self.collected_data[parameter.get('name')] = round(self.collected_data[parameter.get('name')] + value,3)
|
self.collected_data[parameter.get('name')] = round(self.collected_data[parameter.get('name')] + value,3)
|
||||||
else:
|
else:
|
||||||
self.collected_data[parameter.get('name')] = value
|
self.collected_data[parameter.get('name')] = value
|
||||||
@@ -143,73 +150,77 @@ class export_pvoutput(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def publish(self, inverter):
|
def publish(self, inverter):
|
||||||
self.collect_data(inverter)
|
if self.collect_data(inverter):
|
||||||
|
# Process data points every status_interval
|
||||||
# Process data points every status_interval
|
if((time.time() - self.last_publish) >= (self.status_interval * 60)):
|
||||||
if((time.time() - self.last_publish) >= (self.status_interval * 60)):
|
any_data = False
|
||||||
any_data = False
|
if inverter.validateLatestScrape('timestamp'):
|
||||||
if inverter.validateLatestScrape('timestamp'):
|
now = datetime.datetime.strptime(inverter.getRegisterValue('timestamp'), "%Y-%m-%d %H:%M:%S")
|
||||||
now = datetime.datetime.strptime(inverter.getRegisterValue('timestamp'), "%Y-%m-%d %H:%M:%S")
|
data_point = str(now.strftime("%Y%m%d")) + "," + str(now.strftime("%H:%M"))
|
||||||
data_point = str(now.strftime("%Y%m%d")) + "," + str(now.strftime("%H:%M"))
|
for x in range(1, 13):
|
||||||
for x in range(1, 13):
|
field = 'v' + str(x)
|
||||||
field = 'v' + str(x)
|
if self.collected_data.get(field):
|
||||||
if self.collected_data.get(field):
|
if x == 1 and (self.pvoutput_config['cumulative_flag'] == 1 or self.pvoutput_config['cumulative_flag'] == 2):
|
||||||
if x == 6 or x == 7: # Round to 1 decimal place
|
value = int(self.collected_data[field])
|
||||||
value = round(self.collected_data[field] / self.collected_data['count'], 1)
|
elif x == 3 and (self.pvoutput_config['cumulative_flag'] == 1 or self.pvoutput_config['cumulative_flag'] == 3):
|
||||||
else: # Getting errors when uploading decimals for power/energy so return INT
|
value = int(self.collected_data[field])
|
||||||
value = int((self.collected_data[field] / self.collected_data['count']))
|
elif x == 6 or x == 7: # Round to 1 decimal place
|
||||||
data_point = data_point + "," + str(value)
|
value = round(self.collected_data[field] / self.collected_data['count'], 1)
|
||||||
any_data = True
|
else: # Getting errors when uploading decimals for power/energy so return INT
|
||||||
else:
|
value = int((self.collected_data[field] / self.collected_data['count']))
|
||||||
data_point = data_point + ","
|
data_point = data_point + "," + str(value)
|
||||||
self.collected_data = {}
|
any_data = True
|
||||||
|
|
||||||
if any_data:
|
|
||||||
self.batch_data.append(data_point)
|
|
||||||
else:
|
|
||||||
logging.warning(f"PVOutput: No data collected in last {(self.status_interval * 60)} minutes")
|
|
||||||
|
|
||||||
# Max upload is 30, if over 30 then remove the oldest one
|
|
||||||
if self.batch_data.__len__() > 30:
|
|
||||||
logging.warning(f"PVOutput: Over 30 data points scheduled to upload. max is 30 so removing oldest data point")
|
|
||||||
self.batch_data.pop(0)
|
|
||||||
|
|
||||||
self.batch_count +=1
|
|
||||||
if self.batch_count >= self.pvoutput_config['batch_points']:
|
|
||||||
if not self.batch_data.__len__() > 0:
|
|
||||||
logging.warning(f"PVOutput: No data collected in last {((self.status_interval * 60) * self.batch_count)} minutes, Skipping upload")
|
|
||||||
return False
|
|
||||||
elif self.batch_data.__len__() >= 1:
|
|
||||||
payload_data = None
|
|
||||||
for data in self.batch_data:
|
|
||||||
if payload_data:
|
|
||||||
payload_data = payload_data + ";" + data
|
|
||||||
else:
|
else:
|
||||||
payload_data = data
|
data_point = data_point + ","
|
||||||
|
self.collected_data = {}
|
||||||
|
|
||||||
payload = {}
|
if any_data:
|
||||||
payload['data'] = payload_data
|
self.batch_data.append(data_point)
|
||||||
|
else:
|
||||||
|
logging.warning(f"PVOutput: No data collected in last {(self.status_interval * 60)} minutes")
|
||||||
|
|
||||||
if self.pvoutput_config['cumulative_flag'] > 0:
|
# Max upload is 30, if over 30 then remove the oldest one
|
||||||
payload['c1'] = self.pvoutput_config['cumulative_flag']
|
if self.batch_data.__len__() > 30:
|
||||||
|
logging.warning(f"PVOutput: Over 30 data points scheduled to upload. max is 30 so removing oldest data point")
|
||||||
|
self.batch_data.pop(0)
|
||||||
|
|
||||||
try:
|
self.batch_count +=1
|
||||||
logging.debug("PVOutput: Request; " + self.url_addbatchstatus + ", " + str(self.headers) + " : " + str(payload))
|
if self.batch_count >= self.pvoutput_config['batch_points']:
|
||||||
response = requests.post(url=self.url_addbatchstatus, headers=self.headers, params=payload, timeout=3)
|
if not self.batch_data.__len__() > 0:
|
||||||
self.batch_count = 0
|
logging.warning(f"PVOutput: No data collected in last {((self.status_interval * 60) * self.batch_count)} minutes, Skipping upload")
|
||||||
|
return False
|
||||||
|
elif self.batch_data.__len__() >= 1:
|
||||||
|
payload_data = None
|
||||||
|
for data in self.batch_data:
|
||||||
|
if payload_data:
|
||||||
|
payload_data = payload_data + ";" + data
|
||||||
|
else:
|
||||||
|
payload_data = data
|
||||||
|
|
||||||
if response.status_code != requests.codes.ok:
|
payload = {}
|
||||||
logging.error(f"PVOutput: Upload Failed; {str(response.status_code)} Message; {str(response.text)}")
|
payload['data'] = payload_data
|
||||||
else:
|
|
||||||
self.batch_data = []
|
if self.pvoutput_config['cumulative_flag'] > 0:
|
||||||
self.last_publish = time.time()
|
payload['c1'] = self.pvoutput_config['cumulative_flag']
|
||||||
logging.info("PVOutput: Data uploaded")
|
|
||||||
except Exception as err:
|
try:
|
||||||
logging.error(f"PVOutput: Failed to Upload")
|
logging.debug("PVOutput: Request; " + self.url_addbatchstatus + ", " + str(self.headers) + " : " + str(payload))
|
||||||
logging.debug(f"{err}")
|
response = requests.post(url=self.url_addbatchstatus, headers=self.headers, params=payload, timeout=3)
|
||||||
|
self.batch_count = 0
|
||||||
|
|
||||||
|
if response.status_code != requests.codes.ok:
|
||||||
|
logging.error(f"PVOutput: Upload Failed; {str(response.status_code)} Message; {str(response.text)}")
|
||||||
|
logging.error("PVOutput: Request; " + self.url_addbatchstatus + ", " + str(self.headers) + " : " + str(payload))
|
||||||
|
else:
|
||||||
|
self.batch_data = []
|
||||||
|
self.last_publish = time.time()
|
||||||
|
logging.info("PVOutput: Data uploaded")
|
||||||
|
except Exception as err:
|
||||||
|
logging.error(f"PVOutput: Failed to Upload")
|
||||||
|
logging.debug(f"{err}")
|
||||||
|
else:
|
||||||
|
logging.info("PVOutput: Data added to next batch upload")
|
||||||
else:
|
else:
|
||||||
logging.info("PVOutput: Data added to next batch upload")
|
logging.info(f"PVOutput: Data logged, next upload in {int(((self.status_interval) * 60) - (time.time() - self.last_publish))} secs")
|
||||||
else:
|
|
||||||
logging.info(f"PVOutput: Data logged, next upload in {int(((self.status_interval) * 60) - (time.time() - self.last_publish))} secs")
|
|
||||||
|
|
||||||
self.last_run = time.time()
|
self.last_run = time.time()
|
||||||
@@ -43,7 +43,7 @@ class MyServer(BaseHTTPRequestHandler):
|
|||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-type", "text/html")
|
self.send_header("Content-type", "text/html")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(bytes("<html><head><title>SunGather</title><meta http-equiv='refresh' content='15'></head>", "utf-8"))
|
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("<body>", "utf-8"))
|
self.wfile.write(bytes("<body>", "utf-8"))
|
||||||
self.wfile.write(bytes(export_webserver.html_body, "utf-8"))
|
self.wfile.write(bytes(export_webserver.html_body, "utf-8"))
|
||||||
self.wfile.write(bytes("</table>", "utf-8"))
|
self.wfile.write(bytes("</table>", "utf-8"))
|
||||||
|
|||||||
+556
-110
File diff suppressed because it is too large
Load Diff
+25
-22
@@ -8,6 +8,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
|
import logging.handlers
|
||||||
import sys
|
import sys
|
||||||
import getopt
|
import getopt
|
||||||
import yaml
|
import yaml
|
||||||
@@ -35,7 +36,7 @@ class SungrowInverter():
|
|||||||
|
|
||||||
self.registers = [[]]
|
self.registers = [[]]
|
||||||
self.registers.pop() # Remove null value from list
|
self.registers.pop() # Remove null value from list
|
||||||
self.registers_custom = [{'name': 'export_to_grid', 'unit': 'w'}, {'name': 'import_from_grid', 'unit': 'w'}, {'name': 'run_state'}]
|
self.registers_custom = [{'name': 'export_to_grid', 'unit': 'W'}, {'name': 'import_from_grid', 'unit': 'W'}, {'name': 'run_state'}]
|
||||||
self.register_ranges = [[]]
|
self.register_ranges = [[]]
|
||||||
self.register_ranges.pop() # Remove null value from list
|
self.register_ranges.pop() # Remove null value from list
|
||||||
|
|
||||||
@@ -200,12 +201,16 @@ class SungrowInverter():
|
|||||||
if register_type == register['type'] and register['address'] == run:
|
if register_type == register['type'] and register['address'] == run:
|
||||||
register_name = register['name']
|
register_name = register['name']
|
||||||
|
|
||||||
register_value = None
|
register_value = rr.registers[num]
|
||||||
# Return the 32bit value if needed
|
|
||||||
if register.get('datatype') == "U32" or register.get('datatype') == "S32":
|
# Convert unsigned to signed
|
||||||
|
if register.get('datatype') == "S32":
|
||||||
u32_value = rr.registers[num+1]
|
u32_value = rr.registers[num+1]
|
||||||
if u32_value and register_value:
|
if u32_value == 65535: # Flag to say value is negative for 32bit
|
||||||
register_value = (u32_value * 65535) + register_value
|
register_value = (register_value - 65535)
|
||||||
|
elif register.get('datatype') == "S16":
|
||||||
|
if register_value >= 32767: # Anything greater than 32767 is a negative for 16bit
|
||||||
|
register_value = (register_value - 65535)
|
||||||
|
|
||||||
# We convert a system response to a human value
|
# We convert a system response to a human value
|
||||||
if register.get('datarange'):
|
if register.get('datarange'):
|
||||||
@@ -215,16 +220,8 @@ class SungrowInverter():
|
|||||||
if not register_value:
|
if not register_value:
|
||||||
register_value = rr.registers[num]
|
register_value = rr.registers[num]
|
||||||
|
|
||||||
# Adjust the value if needed
|
# If xFF / xFFFF then change to 0, looks better when logging / graphing
|
||||||
if register.get('indicator'):
|
if register_value == 65535:
|
||||||
indicator_value = rr.registers[num+1]
|
|
||||||
if indicator_value == 65535:
|
|
||||||
register_value = -1 * (65535 - register_value)
|
|
||||||
|
|
||||||
# If xFF (U 65535 / S 32767) then change to 0, looks better when logging / graphing
|
|
||||||
if register.get('datatype') == 'S16' and (register_value == 32767 or register_value == 65535):
|
|
||||||
register_value = 0
|
|
||||||
elif (register.get('datatype') == 'U16' or register.get('datatype') == 'S32' or register.get('datatype') == 'U32') and register_value == 65535:
|
|
||||||
register_value = 0
|
register_value = 0
|
||||||
|
|
||||||
if register.get('accuracy'):
|
if register.get('accuracy'):
|
||||||
@@ -333,7 +330,7 @@ class SungrowInverter():
|
|||||||
# to help with graphing.
|
# to help with graphing.
|
||||||
try:
|
try:
|
||||||
if self.latest_scrape.get('start_stop'):
|
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) == 'Run':
|
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"
|
self.latest_scrape["run_state"] = "ON"
|
||||||
else:
|
else:
|
||||||
self.latest_scrape["run_state"] = "OFF"
|
self.latest_scrape["run_state"] = "OFF"
|
||||||
@@ -398,19 +395,22 @@ class SungrowInverter():
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
configfilename = 'config.yaml'
|
configfilename = 'config.yaml'
|
||||||
|
logfolder = ''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
opts, args = getopt.getopt(sys.argv[1:],"hc:v:", "runonce")
|
opts, args = getopt.getopt(sys.argv[1:],"hc:l:v:", "runonce")
|
||||||
except getopt.GetoptError:
|
except getopt.GetoptError:
|
||||||
logging.debug(f'No options passed via command line')
|
logging.debug(f'No options passed via command line')
|
||||||
|
|
||||||
for opt, arg in opts:
|
for opt, arg in opts:
|
||||||
if opt == '-h':
|
if opt == '-h':
|
||||||
print(f'\nSunGather {__version__}')
|
print(f'\nSunGather {__version__}')
|
||||||
|
print(f'\nhttps://sungather.app')
|
||||||
print(f'usage: python3 sungather.py [options]')
|
print(f'usage: python3 sungather.py [options]')
|
||||||
print(f'\nCommandling arguments override any config file settings')
|
print(f'\nCommandling arguments override any config file settings')
|
||||||
print(f'Options and arguments:')
|
print(f'Options and arguments:')
|
||||||
print(f'-c config.yaml : Specify config file.')
|
print(f'-c config.yaml : Specify config file.')
|
||||||
|
print(f'-l /logs/ : Specify folder to store logs.')
|
||||||
print(f'-v 30 : Logging Level, 10 = Debug, 20 = Info, 30 = Warning (default), 40 = Error')
|
print(f'-v 30 : Logging Level, 10 = Debug, 20 = Info, 30 = Warning (default), 40 = Error')
|
||||||
print(f'--runonce : Run once then exit')
|
print(f'--runonce : Run once then exit')
|
||||||
print(f'-h : print this help message and exit (also --help)')
|
print(f'-h : print this help message and exit (also --help)')
|
||||||
@@ -418,7 +418,9 @@ def main():
|
|||||||
print(f'python3 sungather.py -c /full/path/config.yaml\n')
|
print(f'python3 sungather.py -c /full/path/config.yaml\n')
|
||||||
sys.exit()
|
sys.exit()
|
||||||
elif opt == '-c':
|
elif opt == '-c':
|
||||||
configfilename = arg
|
configfilename = arg
|
||||||
|
elif opt == '-l':
|
||||||
|
logfolder = arg
|
||||||
elif opt == '-v':
|
elif opt == '-v':
|
||||||
if arg.isnumeric():
|
if arg.isnumeric():
|
||||||
if int(arg) >= 0 and int(arg) <= 50:
|
if int(arg) >= 0 and int(arg) <= 50:
|
||||||
@@ -430,7 +432,7 @@ def main():
|
|||||||
logging.error(f"Valid verbose options: 10 = Debug, 20 = Info, 30 = Warning (default), 40 = Error")
|
logging.error(f"Valid verbose options: 10 = Debug, 20 = Info, 30 = Warning (default), 40 = Error")
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
elif opt == '--runonce':
|
elif opt == '--runonce':
|
||||||
runonce = True
|
runonce = True
|
||||||
|
|
||||||
logging.info(f'Starting SunGather {__version__}')
|
logging.info(f'Starting SunGather {__version__}')
|
||||||
|
|
||||||
@@ -447,6 +449,7 @@ def main():
|
|||||||
try:
|
try:
|
||||||
registersfile = yaml.safe_load(open('registers.yaml'))
|
registersfile = yaml.safe_load(open('registers.yaml'))
|
||||||
logging.info(f"Loaded registers: {os.getcwd()}/registers.yaml")
|
logging.info(f"Loaded registers: {os.getcwd()}/registers.yaml")
|
||||||
|
logging.info(f"Registers file version: {registersfile.get('version','UNKNOWN')}")
|
||||||
except Exception as err:
|
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(f"Failed: Loading registers: {os.getcwd()}/registers.yaml {err}")
|
sys.exit(f"Failed: Loading registers: {os.getcwd()}/registers.yaml {err}")
|
||||||
@@ -474,8 +477,8 @@ def main():
|
|||||||
|
|
||||||
if not config_inverter['log_file'] == "OFF":
|
if not config_inverter['log_file'] == "OFF":
|
||||||
if config_inverter['log_file'] == "DEBUG" or config_inverter['log_file'] == "INFO" or config_inverter['log_file'] == "WARNING" or config_inverter['log_file'] == "ERROR":
|
if config_inverter['log_file'] == "DEBUG" or config_inverter['log_file'] == "INFO" or config_inverter['log_file'] == "WARNING" or config_inverter['log_file'] == "ERROR":
|
||||||
logfile = datetime.now().strftime("%Y%m%d_%H%M%S_SunGather.log")
|
logfile = logfolder + "SunGather.log"
|
||||||
fh = logging.FileHandler(logfile, mode='w', encoding='utf-8')
|
fh = logging.handlers.RotatingFileHandler(logfile, mode='w', encoding='utf-8', maxBytes=10485760, backupCount=10) # Log 10mb files, 10 x files = 100mb
|
||||||
fh.formatter = logger.handlers[0].formatter
|
fh.formatter = logger.handlers[0].formatter
|
||||||
fh.setLevel(config_inverter['log_file'])
|
fh.setLevel(config_inverter['log_file'])
|
||||||
logger.addHandler(fh)
|
logger.addHandler(fh)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = '0.3.2'
|
__version__ = '0.3.7'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
sungather.app
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
theme: jekyll-theme-cayman
|
||||||
|
plugins:
|
||||||
|
- jekyll-relative-links
|
||||||
|
relative_links:
|
||||||
|
enabled: true
|
||||||
|
collections: true
|
||||||
+287
@@ -0,0 +1,287 @@
|
|||||||
|
<div id="top"></div>
|
||||||
|
|
||||||
|
[![Contributors][contributors-shield]][contributors-url]
|
||||||
|
[![Forks][forks-shield]][forks-url]
|
||||||
|
[![Stargazers][stars-shield]][stars-url]
|
||||||
|
[![Issues][issues-shield]][issues-url]
|
||||||
|
[![GPL3 License][license-shield]][license-url]
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<h2 align="center">SunGather</h2>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Collect data from Sungrow Inverters using ModbusTcpClient, <a href="https://github.com/rpvelloso/Sungrow-Modbus">SungrowModbusTcpClient</a> or <a href="https://github.com/bohdan-s/SungrowModbusWebClient">SungrowModbusWebClient</a> and export to various locations.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/bohdan-s/SunGather/issues">Report Bug</a>
|
||||||
|
·
|
||||||
|
<a href="https://github.com/bohdan-s/SunGather/issues">Request Feature</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ABOUT THE PROJECT -->
|
||||||
|
## About The Project
|
||||||
|
|
||||||
|
<b>Join the Discord Server to discuss, suggestions or for any help: <a href="https://discord.gg/7j2MVsT5wn">SunGather Discord</a></b>
|
||||||
|
|
||||||
|
Access ModBus data from almost any network connected Sungow Inverter.
|
||||||
|
|
||||||
|
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 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:
|
||||||
|
* Console - Log directly to screen
|
||||||
|
* MQTT - Load into MQTT, and optionally Home Assistance including Discovery
|
||||||
|
* PVOutput - Load into PVOutput.org
|
||||||
|
* InfluxDB - Load data directly into InfluxDB
|
||||||
|
* Simple webserver showing collected data
|
||||||
|
* Rasberry Pi Docker support
|
||||||
|
* and more coming....
|
||||||
|
|
||||||
|
I have learned a lot from the following projects, THANK YOU
|
||||||
|
* [solariot](https://github.com/meltaxa/solariot)
|
||||||
|
* [modbus4mqtt](https://github.com/tjhowse/modbus4mqtt)
|
||||||
|
* [ModbusTCP2MQTT](https://github.com/TenySmart/ModbusTCP2MQTT)
|
||||||
|
|
||||||
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
## Raodmap / TO DO
|
||||||
|
* Full Home Assistant integration, as HACS addon
|
||||||
|
|
||||||
|
### Built With
|
||||||
|
|
||||||
|
* [Python3](https://www.python.org/)
|
||||||
|
|
||||||
|
### Requires
|
||||||
|
* [paho-mqtt>=1.5.1](https://pypi.org/project/paho-mqtt/)
|
||||||
|
* [pymodbus>=2.4.0](https://pypi.org/project/pymodbus/)
|
||||||
|
* [SungrowModbusTcpClient>=0.1.6](https://pypi.org/project/SungrowModbusTcpClient/)
|
||||||
|
* [SungrowModbusWebClient>=0.2.6](https://pypi.org/project/SungrowModbusWebClient/)
|
||||||
|
* [PyYAML>=6.0](https://pypi.org/project/PyYAML/)
|
||||||
|
* [requests>=2.26.0](https://pypi.org/project/requests/)
|
||||||
|
* [influxdb-client>=1.24.0](https://pypi.org/project/influxdb-client/)
|
||||||
|
|
||||||
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
<!-- GETTING STARTED -->
|
||||||
|
## Getting Started
|
||||||
|
# Local
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/bohdan-s/SunGather.git
|
||||||
|
cd SunGather
|
||||||
|
pip3 install --upgrade -r requirements.txt
|
||||||
|
```
|
||||||
|
Copy config-example.py to config.py, change values as required (see comments in file)
|
||||||
|
```sh
|
||||||
|
copy config-example.py config.py
|
||||||
|
```
|
||||||
|
Run SunGather:
|
||||||
|
```sh
|
||||||
|
python3 sungather.py
|
||||||
|
```
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker run options:
|
||||||
|
-v {path to}/config.yaml:/config/config.yaml <- Set Config File Location
|
||||||
|
-v {logpath}:/logs <- Set Log folder Location if using log to file (also set log_file in config.yaml)
|
||||||
|
-e TZ=Australia/Sydney <- Set Timezone
|
||||||
|
-p 8080:8080 <- Set Web server port (only if using the webserver export)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker pull bohdans/sungather
|
||||||
|
docker run -d --name sungather \
|
||||||
|
--restart always \
|
||||||
|
-v {path to}/config.yaml:/config/config.yaml \
|
||||||
|
-v {logpath}:/logs \
|
||||||
|
-e TZ=Australia/Sydney \
|
||||||
|
-p 8080:8080 \
|
||||||
|
bohdans/sungather
|
||||||
|
```
|
||||||
|
Note: replace Australia/Sydney with relevant timezone
|
||||||
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- USAGE EXAMPLES -->
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
### Commandline Arguments
|
||||||
|
usage: python3 sungather.py [options]
|
||||||
|
|
||||||
|
Commandline arguments override any config file settings
|
||||||
|
**-c config.yaml** - Specify config file.
|
||||||
|
**-v 30** - Logging Level, 10 = Debug, 20 = Info, 30 = Warning (default), 40 = Error
|
||||||
|
**--runonce** - Run once then exit
|
||||||
|
**-h** - print this help message and exit (also --help)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```sh
|
||||||
|
python3 sungather.py -c /full/path/config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
|
||||||
|
A collection of exports are available:
|
||||||
|
|
||||||
|
* console: Output information to console, useful for troubleshooting
|
||||||
|
* webserver: 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.
|
||||||
|
* InfluxDB: Output directly to InfluxDB, can then be used by Grafana, etc..
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
## Home Assistant setup
|
||||||
|
|
||||||
|
In the SunGather config.yaml you need to set the smart_meter if you have one;
|
||||||
|
|
||||||
|
```
|
||||||
|
smart_meter: True
|
||||||
|
```
|
||||||
|
|
||||||
|
HA sensors created by the MQTT auto-discovery;
|
||||||
|
```
|
||||||
|
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
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
The additional sensors then setup in HA are the following with Wh unit of measurement for the Energy platform.
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
## Tested
|
||||||
|
* SG7.0RT with WiNet-S Dongle
|
||||||
|
* SG10RT with WiNet-S Dongle and Ethernet (Credit: @rark-ha)
|
||||||
|
|
||||||
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
## Supported
|
||||||
|
### PV Grid-Connected String Inverters
|
||||||
|
SG30KTL, SG10KTL, SG12KTL, SG15KTL, SG20KTL, SG30KU, SG36KTL, SG36KU, SG40KTL, SG40KTL-M, SG50KTL-M, SG60KTL-M, SG60KU, SG30KTL-M, SG30KTL-M-V31, SG33KTL-M, SG36KTL-M, SG33K3J, SG49K5J, SG34KJ, LP_P34KSG, SG50KTL-M-20, SG60KTL, SG80KTL, SG80KTL-20, SG60KU-M, SG5KTL-MT, SG6KTL-MT, SG8KTL-M, SG10KTL-M, SG10KTL-MT, SG12KTL-M, SG15KTL-M, SG17KTL-M, SG20KTL-M, SG80KTL-M, SG111HV, SG125HV, SG125HV-20, SG30CX, SG33CX, SG36CX-US, SG40CX, SG50CX, SG60CX-US, SG110CX, SG250HX, SG250HX-US, SG100CX, SG100CX-JP, SG250HX-IN, SG25CX-SA, SG75CX, SG3.0RT, SG4.0RT, SG5.0RT, SG6.0RT, SG7.0RT, SG8.0RT, SG10RT, SG12RT, SG15RT, SG17RT, SG20RT
|
||||||
|
|
||||||
|
### PV Grid-Connected String Inverters Gen 2
|
||||||
|
SG5K-D, SG8K-D
|
||||||
|
|
||||||
|
### Residential Hybrid Inverters
|
||||||
|
SH5K-20, SH3K6, SH4K6, SH5K-V13, SH5K-30, SH3K6-30, SH4K6-30, SH5.0RS, SH3.6RS, SH4.6RS, SH6.0RS, SH10RT, SH8.0RT, SH6.0RT, SH5.0RT
|
||||||
|
|
||||||
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
|
||||||
|
## Building
|
||||||
|
Multi Arch (BuildX) Including Raspberry Pi
|
||||||
|
```sh
|
||||||
|
docker run --privileged --rm tonistiigi/binfmt --install all
|
||||||
|
docker buildx create --platform linux/amd64,linux/arm64/v8 --use
|
||||||
|
docker buildx build --push --platform linux/amd64,linux/arm64/v8 -t bohdans/sungather:latest -t bohdans/sungather:v<version> .
|
||||||
|
```
|
||||||
|
Current Arch (docker build)
|
||||||
|
```sh
|
||||||
|
docker build --no-cache --rm -t bohdans/sungather:latest -t bohdans/sungather:v<version> .
|
||||||
|
docker push bohdans/sungather -a
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
<!-- LICENSE -->
|
||||||
|
## License
|
||||||
|
|
||||||
|
Distributed under the GPL3 License. See `LICENSE.txt` for more information.
|
||||||
|
|
||||||
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
<!-- CONTACT -->
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Project Link: [https://github.com/bohdan-s/SungrowModbusWebClient](https://github.com/bohdan-s/SungrowModbusWebClient)
|
||||||
|
|
||||||
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
<!-- ACKNOWLEDGMENTS -->
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
* [solariot](https://github.com/meltaxa/solariot)
|
||||||
|
* [modbus4mqtt](https://github.com/tjhowse/modbus4mqtt)
|
||||||
|
* [ModbusTCP2MQTT](https://github.com/TenySmart/ModbusTCP2MQTT)
|
||||||
|
|
||||||
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- MARKDOWN LINKS & IMAGES -->
|
||||||
|
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
||||||
|
[contributors-shield]: https://img.shields.io/github/contributors/bohdan-s/SunGather.svg?style=for-the-badge
|
||||||
|
[contributors-url]: https://github.com/bohdan-s/SunGather/graphs/contributors
|
||||||
|
[forks-shield]: https://img.shields.io/github/forks/bohdan-s/SunGather.svg?style=for-the-badge
|
||||||
|
[forks-url]: https://github.com/bohdan-s/SunGather/network/members
|
||||||
|
[stars-shield]: https://img.shields.io/github/stars/bohdan-s/SunGather.svg?style=for-the-badge
|
||||||
|
[stars-url]: https://github.com/bohdan-s/SunGather/stargazers
|
||||||
|
[issues-shield]: https://img.shields.io/github/issues/bohdan-s/SunGather.svg?style=for-the-badge
|
||||||
|
[issues-url]: https://github.com/bohdan-s/SunGather/issues
|
||||||
|
[license-shield]: https://img.shields.io/github/license/bohdan-s/SunGather.svg?style=for-the-badge
|
||||||
|
[license-url]: https://github.com/bohdan-s/SunGather/blob/main/LICENSE.txt
|
||||||
+1
-1
@@ -3,5 +3,5 @@ requests>=2.26.0
|
|||||||
paho-mqtt>=1.5.1
|
paho-mqtt>=1.5.1
|
||||||
pymodbus>=2.5.3
|
pymodbus>=2.5.3
|
||||||
SungrowModbusTcpClient>=0.1.5
|
SungrowModbusTcpClient>=0.1.5
|
||||||
SungrowModbusWebClient>=0.2.7
|
SungrowModbusWebClient>=0.3.2
|
||||||
influxdb-client>=1.24.0
|
influxdb-client>=1.24.0
|
||||||
|
|||||||
Reference in New Issue
Block a user