diff --git a/.gitignore b/.gitignore
index 510c73d..658570a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+config.yaml
+
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
diff --git a/SunGather/README.md b/SunGather/README.md
new file mode 100644
index 0000000..3cfd126
--- /dev/null
+++ b/SunGather/README.md
@@ -0,0 +1,124 @@
+
+
+[![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]
+
+
+
+
+
+## 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.
+
+Register information based on official documentation: Communication Protocol of PV Grid-Connected String Inverters
+
+Has muliple export locations out of the box:
+* Console - Log directly to screen
+* MQQT - Load into MQTT, and optionally Home Assistance Discovery
+* PVOutput - Load into PVOutput.org
+* and more coming....
+
+I have borrowed HEAVILY from the following projects, THANK YOU
+* [solariot](https://github.com/meltaxa/solariot)
+* [modbus4mqtt](https://github.com/tjhowse/modbus4mqtt)
+* [ModbusTCP2MQTT](https://github.com/TenySmart/ModbusTCP2MQTT)
+
+(back to top)
+
+### 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.4](https://pypi.org/project/SungrowModbusWebClient/)
+
+(back to top)
+
+
+## Getting Started
+```sh
+git clone https://github.com/bohdan-s/SunGather.git
+```
+Copy config-example.py to config.py, change values as required (see comments in file)
+```sh
+python3 sungather.py
+```
+
+(back to top)
+
+
+
+
+## Usage
+
+See config-exmaple.py it contains default options and comments.
+
+
+(back to top)
+
+
+## Tested
+* SG7.0RT with WiNet-S Dongle
+
+(back to top)
+
+
+
+## License
+
+Distributed under the GPL3 License. See `LICENSE.txt` for more information.
+
+(back to top)
+
+
+
+## Contact
+
+Project Link: [https://github.com/bohdan-s/SungrowModbusWebClient](https://github.com/bohdan-s/SungrowModbusWebClient)
+
+(back to top)
+
+
+
+## Acknowledgments
+
+* [solariot](https://github.com/meltaxa/solariot)
+* [modbus4mqtt](https://github.com/tjhowse/modbus4mqtt)
+* [ModbusTCP2MQTT](https://github.com/TenySmart/ModbusTCP2MQTT)
+
+(back to top)
+
+
+
+
+
+[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
\ No newline at end of file
diff --git a/SunGather/__init__.py b/SunGather/__init__.py
new file mode 100644
index 0000000..7152555
--- /dev/null
+++ b/SunGather/__init__.py
@@ -0,0 +1 @@
+from .version import __version__
\ No newline at end of file
diff --git a/SunGather/config-example.yaml b/SunGather/config-example.yaml
new file mode 100644
index 0000000..166ec7f
--- /dev/null
+++ b/SunGather/config-example.yaml
@@ -0,0 +1,102 @@
+inverter:
+ host: 192.168.1.100
+ # port: 502 # Default for modbus is 502, for http is 8082
+ # slave: 0x01 # Default is 0x01
+ # timeout: 10 # Default is 10
+ # scan_interval: 30 # Default is 30
+ connection: http # options: modbus, sungrow, http
+ # model: "SG7.0RT" # This is autodected on startup, only needed if detection issues or for testing
+ smart_meter: True # Default is False, Set to true if inverter supports reading grind / house consumption
+ # logging: 30 # 10 = Debug, 20 = Info, 30 = Warnning (default), 40 = Error
+ # level: 1 # 0 = Model and Solar Generation,
+ # 1 (default) = Useful data, all required for exports,
+ # 2 everything your Inverter supports,
+ # 3 Everything from every register
+
+exports:
+ - name: console # Print Registers to console, good for debugging / troubleshooting
+ enabled: False
+
+ - name: mqtt # Publish Registers to MQTT / Home Assistant
+ host: 192.168.1.200
+ # port: 1883
+ topic: "tele/inverter_{model}/SENSOR" # Variable {model} will be replaced with model number
+ # username:
+ # password:
+ ha_discovery: True # Home Assistant Discovery, False by default
+ ha_topics: # Name in HA, from Register
+ - name: "Daily Generation"
+ register: daily_power_yields
+ unit: kWh
+ dev_class: energy
+ state_class: total_increasing
+ - name: "Active Power"
+ register: total_active_power
+ unit: W
+ dev_class: power
+ state_class: measurement
+ - name: "Load Power"
+ register: load_power
+ unit: W
+ dev_class: power
+ state_class: measurement
+ - name: "Meter Power"
+ register: meter_power
+ unit: W
+ dev_class: power
+ state_class: measurement
+ - name: "Export to Grid"
+ register: export_to_grid
+ unit: W
+ dev_class: power
+ state_class: measurement
+ - name: "Import from Grid"
+ register: import_from_grid
+ unit: W
+ dev_class: power
+ state_class: measurement
+ - name: "Temperature"
+ register: internal_temperature
+ unit: °C
+ dev_class: temperature
+ state_class: measurement
+
+ - name: pvoutput # Publish Registers to PVOutput
+ api: "xxxxx"
+ sid: "xxxxx"
+ # 60 for regular accounts, 300 for donation accounts
+ rate_limit: 60
+ enabled: True
+ parameters:
+ - name: v1 # Energy Generation
+ register: daily_power_yields # Solar Generated Today (Energy)
+ multiple: 1000
+ - name: v2 # Power Generation
+ register: total_active_power # Current Generation (Power)
+# - name: v3 # Energy Consumption
+# register:
+ - name: v4 # Power Consumption
+ register: load_power # Current Home usage (Power)
+ - name: v5 # Temperature
+ register: internal_temperature # Inverter internal temperature
+# - name: v6 # Voltage
+# register: total_active_power
+ - name: c1 # Cumulative Flag,
+ value: 1 # If using v2/v4 set to 1
+# - name: "n" # Net Flag
+# value: 1
+# - name: "v7" # Extended Value v7 - Donation Only
+# register:
+# - name: "v8" # Extended Value v8 - Donation Only
+# register:
+# - name: "v9" # Extended Value v9 - Donation Only
+# register:
+# - name: "v10" # Extended Value v10 - Donation Only
+# register:
+# - name: "v11" # Extended Value v11 - Donation Only
+# register:
+# - name: "v12" # Extended Value v12 - Donation Only
+# register:
+# - name: "m1" # Text Message 1 - Donation Only
+# register:
+
\ No newline at end of file
diff --git a/SunGather/exports/__init__.py b/SunGather/exports/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/SunGather/exports/console.py b/SunGather/exports/console.py
new file mode 100644
index 0000000..30ba6d8
--- /dev/null
+++ b/SunGather/exports/console.py
@@ -0,0 +1,19 @@
+import logging
+
+class export_console(object):
+ def __init__(self):
+ return
+
+ # Configure MQTT
+ def configure(self, config):
+ logging.info("Configured Console Logging")
+
+ def publish(self, inverter):
+
+ print("{:<25} {:<25}".format('Register','Value'))
+ for register in inverter:
+ print("{:<25} {:<25}".format(register,str(inverter.get(register))))
+
+ print(f"Logged {len(inverter)} registers to Console")
+
+ return
\ No newline at end of file
diff --git a/SunGather/exports/mqtt.py b/SunGather/exports/mqtt.py
new file mode 100644
index 0000000..b12d8d5
--- /dev/null
+++ b/SunGather/exports/mqtt.py
@@ -0,0 +1,64 @@
+import logging
+import json
+import paho.mqtt.client as mqtt
+
+class export_mqtt(object):
+ def __init__(self):
+ self.mqtt_client = None
+ self.sensor_topic = None
+ self.ha_discovery = False
+ self.ha_topics = []
+ self.update_model = True
+
+ # Configure MQTT
+ def configure(self, config):
+ self.mqtt_client = mqtt.Client("pv_data")
+
+ if config.get('username') and config.get('password'):
+ self.mqtt_client.username_pw_set(config.get('username'), config.get('password'))
+
+ if config.get('port') == 8883:
+ self.mqtt_client.tls_set()
+
+ self.mqtt_client.connect(config.get('host'), port=config.get('port', 1883))
+
+ self.sensor_topic = config.get('topic', 'tele/inverter_{model}/SENSOR')
+ self.ha_discovery = config.get('ha_discovery', False)
+
+ if self.ha_discovery:
+ for ha_topic in config.get('ha_topics'):
+ self.ha_topics.append(ha_topic)
+
+ logging.info("Configured MQTT Client")
+
+ def publish(self, inverter):
+ global mqtt_client
+
+ if (self.update_model):
+ self.sensor_topic = self.sensor_topic.replace('{model}', inverter.get('device_type_code', 'unknown').replace('.',''))
+ self.update_model = False
+
+ if self.ha_discovery:
+ self.mqtt_client.reconnect()
+ logging.info("Publishing Home Assistant Discovery messages")
+ discovery_topic = 'homeassistant/sensor/inverter/{}/config'
+ discovery_payload = '{{"name":"Inverter {}", "uniq_id":"{}", "stat_t":"{}", "json_attr_t":"{}", "unit_of_meas":"{}", "dev_cla":"{}", "state_class":"{}", "val_tpl":"{{{{ value_json.{} }}}}", "ic":"mdi:solar-power", "device":{{ "name":"Solar Inverter", "mf":"Sungrow", "mdl":"{}", "connections":[["address", "' + self.mqtt_client._host + '" ]]}}}}'
+
+ for ha_topic in self.ha_topics:
+ msg = discovery_payload.format( ha_topic.get('name'), "inverter_" + ha_topic.get('name').lower().replace(' ','_'), self.sensor_topic, self.sensor_topic, ha_topic.get('unit'), ha_topic.get('dev_class'), ha_topic.get('state_class'), ha_topic.get('register'), inverter.get('device_type_code', 'unknown').replace('.',''))
+ result = self.mqtt_client.publish(discovery_topic.format(ha_topic.get('name').lower().replace(' ','_')), msg)
+ result.wait_for_publish()
+ self.ha_discovery = False
+
+ # After a while you'll need to reconnect, so just reconnect before each publish
+ self.mqtt_client.reconnect()
+ result = self.mqtt_client.publish(self.sensor_topic, json.dumps(inverter).replace('"', '\"'))
+ result.wait_for_publish()
+
+ if result.rc != mqtt.MQTT_ERR_SUCCESS:
+ # See https://github.com/eclipse/paho.mqtt.python/blob/master/src/paho/mqtt/client.py#L149 for error code mapping
+ logging.error(f"Failed to publish to MQTT with error code: {result.rc}")
+ else:
+ logging.info("Published to MQTT")
+
+ return result
\ No newline at end of file
diff --git a/SunGather/exports/pvoutput.py b/SunGather/exports/pvoutput.py
new file mode 100644
index 0000000..8110974
--- /dev/null
+++ b/SunGather/exports/pvoutput.py
@@ -0,0 +1,104 @@
+import logging
+import datetime
+import requests
+
+# Configure PVOutput
+class export_pvoutput(object):
+ def __init__(self):
+ self.api_key = None
+ self.system_id = None
+ self.status_url = "https://pvoutput.org/service/r2/addstatus.jsp"
+ self.metric_mappings = None
+ self.rate_limit = None
+ self.parameters = []
+ self.latest_run = None
+
+ @property
+ def headers(self):
+ return {
+ "X-Pvoutput-Apikey": self.api_key,
+ "X-Pvoutput-SystemId": self.system_id,
+ "Content-Type": "application/x-www-form-urlencoded",
+ "cache-control": "no-cache",
+ }
+
+ def configure(self, config):
+ self.api_key = config.get('api')
+ self.system_id = config.get('sid')
+ self.rate_limit = config.get('rate_limit', 60)
+
+ for parameter in config.get('parameters'):
+ self.parameters.append(parameter)
+
+ logging.info("Configured PVOutput Client")
+
+
+
+ def publish(self, inverter):
+ """
+ See: https://pvoutput.org/help/api_specification.html#add-status-service
+ Parameter Field Required Format Unit Example Donation
+ d Output Date Yes yyyymmdd date 20210228
+ t Time Yes hh:mm time 14:00
+ v1 Energy Generation No number wh 10000
+ v2 Power Generation No number watts 2000
+ v3 Energy Consumption No number wh 10000
+ v4 Power Consumption No number watts 2000
+ # At least one of the values v1, v2, v3 or v4 must be present.
+ v5 Temperature No decimal celsius 23.4
+ v6 Voltage No decimal volts 239.2
+ c1 Cumulative Flag No number 1
+ # 1 - Both v1 and v3 values are lifetime energy values. Consumption and generation energy is reset to 0 at the start of the day.
+ # 2 - Only v1 generation is a lifetime energy value.
+ # 3 - Only v3 consumption is a lifetime energy value.
+ n Net Flag No number 1
+ # n parameter when set to 1 will indicate that the power values passed are net export/import rather than gross generation/consumption. This option is used for devices that are unable to report gross consumption data. The provided import/export data is merged with existing generation data to derive consumption.
+ v7 Extended Value v7 No number User Defined Yes
+ v8 Extended Value v8 No number User Defined Yes
+ v9 Extended Value v9 No number User Defined Yes
+ v10 Extended Value v10 No number User Defined Yes
+ v11 Extended Value v11 No number User Defined Yes
+ v12 Extended Value v12 No number User Defined Yes
+ m1 Text Message 1 No text 30 chars max Yes
+ """
+ at_least_one_of = set(["v1", "v2", "v3", "v4"])
+
+ now = datetime.datetime.now()
+
+ if self.latest_run:
+ # Spread out our publishes over the hour based on the rate limit
+ time_diff = (now - self.latest_run).total_seconds()
+ if time_diff < (3600 / self.rate_limit):
+ return "skipped"
+
+ payload = {
+ "d": now.strftime("%Y%m%d"),
+ "t": now.strftime("%H:%M"),
+ }
+
+ value_present = False
+ for parameter in self.parameters:
+ if parameter.get('name') == 'v1' or parameter.get('name') == 'v2' or parameter.get('name') == 'v3' or parameter.get('name') == 'v4':
+ value_present = True
+ if parameter.get('register'):
+ if parameter.get('multiple'):
+ payload.update({parameter.get('name'): str(inverter.get(parameter.get('register')) * parameter.get('multiple'))})
+ else:
+ payload.update({parameter.get('name'): str(inverter.get(parameter.get('register')))})
+ elif parameter.get('value'):
+ payload.update({parameter.get('name'): parameter.get('value')})
+
+ if not value_present:
+ logging.error("PVOutput mapping failed, please review metric names and update")
+ return False
+
+ logging.debug("PVOutput Request: " + self.status_url + ", " + str(self.headers) + " : " + str(payload))
+ response = requests.post(url=self.status_url, headers=self.headers, params=payload)
+
+ if response.status_code != requests.codes.ok:
+ raise RuntimeError(response.text)
+
+ logging.info("Published to PVOutput")
+ self.latest_run = now
+
+
diff --git a/SunGather/registers.yaml b/SunGather/registers.yaml
new file mode 100644
index 0000000..2c77284
--- /dev/null
+++ b/SunGather/registers.yaml
@@ -0,0 +1,1011 @@
+registers:
+ - read:
+ - name: "serial_number"
+ address: 4990
+ datatype: "UTF-8"
+ - name: "device_type_code"
+ level: 0
+ address: 5000
+ datatype: "U16"
+ datarange:
+ - response: 0x27
+ value: "SG30KTL"
+ - response: 0x26
+ value: "SG10KTL"
+ - response: 0x29
+ value: "SG12KTL"
+ - response: 0x28
+ value: "SG15KTL"
+ - response: 0x2A
+ value: "SG20KTL"
+ - response: 0x2C
+ value: "SG30KU"
+ - response: 0x2D
+ value: "SG36KTL"
+ - response: 0x2E
+ value: "SG36KU"
+ - response: 0x2F
+ value: "SG40KTL"
+ - response: 0x0135
+ value: "SG40KTL-M"
+ - response: 0x011B
+ value: "SG50KTL-M"
+ - response: 0x0131
+ value: "SG60KTL-M"
+ - response: 0x0136
+ value: "SG60KU"
+ - response: 0x0141
+ value: "SG30KTL-M"
+ - response: 0x70
+ value: "SG30KTL-M-V31"
+ - response: 0x0134
+ value: "SG33KTL-M"
+ - response: 0x74
+ value: "SG36KTL-M"
+ - response: 0x013D
+ value: "SG33K3J"
+ - response: 0x0137
+ value: "SG49K5J"
+ - response: 0x72
+ value: "SG34KJ"
+ - response: 0x73
+ value: "LP_P34KSG"
+ - response: 0x011B
+ value: "SG50KTL-M-20"
+ - response: 0x010F
+ value: "SG60KTL"
+ - response: 0x0138
+ value: "SG80KTL"
+ - response: 0x0138
+ value: "SG80KTL-20"
+ - response: 0x0132
+ value: "SG60KU-M"
+ - response: 0x0147
+ value: "SG5KTL-MT"
+ - response: 0x0148
+ value: "SG6KTL-MT"
+ - response: 0x013F
+ value: "SG8KTL-M"
+ - response: 0x013E
+ value: "SG10KTL-M"
+ - response: 0x2C0F
+ value: "SG10KTL-MT"
+ - response: 0x013C
+ value: "SG12KTL-M"
+ - response: 0x0142
+ value: "SG15KTL-M"
+ - response: 0x0149
+ value: "SG17KTL-M"
+ - response: 0x0143
+ value: "SG20KTL-M"
+ - response: 0x0139
+ value: "SG80KTL-M"
+ - response: 0x014C
+ value: "SG111HV"
+ - response: 0x013B
+ value: "SG125HV"
+ - response: 0x2C03
+ value: "SG125HV-20"
+ - response: 0x2C10
+ value: "SG30CX"
+ - response: 0x2C00
+ value: "SG33CX"
+ - response: 0x2C0A
+ value: "SG36CX-US"
+ - response: 0x2C01
+ value: "SG40CX"
+ - response: 0x2C02
+ value: "SG50CX"
+ - response: 0x2C0B
+ value: "SG60CX-US"
+ - response: 0x2C06
+ value: "SG110CX"
+ - response: 0x2C0C
+ value: "SG250HX"
+ - response: 0x2C11
+ value: "SG250HX-US"
+ - response: 0x2C12
+ value: "SG100CX"
+ - response: 0x2C12
+ value: "SG100CX-JP"
+ - response: 0x2C13
+ value: "SG250HX-IN"
+ - response: 0x2C15
+ value: "SG25CX-SA"
+ - response: 0x2C22
+ value: "SG75CX"
+ - response: 0x243D
+ value: "SG3.0RT"
+ - response: 0x243E
+ value: "SG4.0RT"
+ - response: 0x2430
+ value: "SG5.0RT"
+ - response: 0x2431
+ value: "SG6.0RT"
+ - response: 0x243C
+ value: "SG7.0RT"
+ - response: 0x2432
+ value: "SG8.0RT"
+ - response: 0x2433
+ value: "SG10RT"
+ - response: 0x2434
+ value: "SG12RT"
+ - response: 0x2435
+ value: "SG15RT"
+ - response: 0x2436
+ value: "SG17RT"
+ - response: 0x2437
+ value: "SG20RT"
+ - name: "nominal_active_power"
+ level: 2
+ address: 5001
+ datatype: "U16"
+ multiple: 0.1
+ - name: "output_type"
+ level: 2
+ address: 5002
+ datatype: "U16"
+ datarange:
+ - response: 0
+ value: "2P"
+ - response: 1
+ value: "3P4L"
+ - response: 1
+ value: "3PSL"
+ - name: "daily_power_yields"
+ level: 0
+ address: 5003
+ datatype: "U16"
+ multiple: 0.1
+ - name: "total_power_yields"
+ level: 1
+ address: 5004
+ datatype: "U32"
+ - name: "total_running_time"
+ level: 1
+ address: 5006
+ datatype: "U32"
+ - name: "internal_temperature"
+ level: 1
+ address: 5008
+ datatype: "S16"
+ multiple: 0.1
+ - name: "total_apparent_power"
+ level: 2
+ address: 5009
+ datatype: "U32"
+ models: ["SG6KTL-MT", "SG8KTL-M", "SG10KTL-M", "SG10KTL-MT", "SG12KTL-M", "SG15KTL-M", "SG17KTL-M", "SG20KTL-M", "SG3.0RT", "SG4.0RT", "SG5.0RT", "SG6.0RT", "SG7.0RT", "SG8.0RT", "SG10RT", "SG12RT", "SG15RT", "SG17RT", "SG20RT", "SG33K3J", "SG36KTL-M", "SG40KTL-M", "SG50KTL", "SG50KTL-M", "SG60KTL", "SG60KTL-M", "SG60KU-M", "SG80KTL", "SG80KTL-M", "SG111HV", "SG125HV", "SG125HV-20", "SG33CX", "SG40CX", "SG50CX", "SG110CX", "SG250HX", "SG30CX", "SG36CX-US", "SG60CX-US", "SG250HX-US", "SG250HX-IN", "SG25CX-SA", "SG100CX", "SG75CX", "SG225HX"]
+ - name: "mppt_1_voltage"
+ level: 2
+ address: 5011
+ datatype: "U16"
+ multiple: 0.1
+ - name: "mppt_1_current"
+ level: 2
+ address: 5012
+ datatype: "U16"
+ multiple: 0.1
+ - name: "mppt_2_voltage"
+ level: 2
+ address: 5013
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG30KTL","SG10KTL","SG12KTL","SG15KTL","SG20KTL","SG30KU","SG36KTL","SG36KU","SG40KTL","SG40KTL-M","SG50KTL-M","SG60KTL-M","SG30KTL-M","SG30KTL-M-V31","SG33KTL-M","SG36KTL-M","SG33K3J","SG49K5J","SG34KJ","SG50KTL-M-20","SG60KU-M","SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M","SG80KTL-M","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"]
+ - name: "mppt_2_current"
+ level: 2
+ address: 5014
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG30KTL","SG10KTL","SG12KTL","SG15KTL","SG20KTL","SG30KU","SG36KTL","SG36KU","SG40KTL","SG40KTL-M","SG50KTL-M","SG60KTL-M","SG30KTL-M","SG30KTL-M-V31","SG33KTL-M","SG36KTL-M","SG33K3J","SG49K5J","SG34KJ","SG50KTL-M-20","SG60KU-M","SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M","SG80KTL-M","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"]
+ - name: "mppt_3_voltage"
+ level: 2
+ address: 5015
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG40KTL-M","SG50KTL-M","SG60KTL-M","SG30KTL-M","SG30KTL-M-V31","SG33KTL-M","SG36KTL-M","SG33K3J","SG49K5J","SG50KTL-M-20","SG60KU-M","SG80KTL-M","SG30CX","SG33CX","SG36CX-US","SG40CX","SG50CX","SG60CX-US","SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG25CX-SA","SG75CX"]
+ - name: "mppt_3_current"
+ level: 2
+ address: 5016
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG40KTL-M","SG50KTL-M","SG60KTL-M","SG30KTL-M","SG30KTL-M-V31","SG33KTL-M","SG36KTL-M","SG33K3J","SG49K5J","SG50KTL-M-20","SG60KU-M","SG80KTL-M","SG30CX","SG33CX","SG36CX-US","SG40CX","SG50CX","SG60CX-US","SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG25CX-SA","SG75CX"]
+ - name: "total_dc_power"
+ level: 2
+ address: 5017
+ datatype: "S32" # Documentation says Unsinged, but seems to be returning Signed 32bit
+ - name: "phase_a_voltage"
+ level: 2
+ address: 5019
+ datatype: "U16"
+ multiple: 0.1
+ - name: "phase_b_voltage"
+ level: 2
+ address: 5020
+ datatype: "U16"
+ multiple: 0.1
+ - name: "phase_c_voltage"
+ level: 2
+ address: 5021
+ datatype: "U16"
+ multiple: 0.1
+ - name: "phase_a_current"
+ level: 2
+ address: 5022
+ datatype: "S16" # Documentation says Unsinged, but seems to be returning Signed 16bit
+ multiple: 0.1
+ - name: "phase_b_current"
+ level: 2
+ address: 5023
+ datatype: "S16" # Documentation says Unsinged, but seems to be returning Signed 16bit
+ multiple: 0.1
+ - name: "phase_c_current"
+ level: 2
+ address: 5024
+ datatype: "S16" # Documentation says Unsinged, but seems to be returning Signed 16bit
+ multiple: 0.1
+# - name: "Reserved"
+# address: 5025
+# datatype: "U32"
+# - name: "Reserved"
+# address: 5027
+# datatype: "U32"
+# - name: "Reserved"
+# address: 5029
+# datatype: "U32"
+ - name: "total_active_power"
+ level: 0
+ address: 5031
+ datatype: "U32"
+ - name: "total_reactive_power"
+ level: 2
+ address: 5033
+ datatype: "S32"
+ - name: "power_factor" # >0 means leading, <0 means lagging
+ level: 2
+ address: 5035
+ datatype: "S16"
+ multiple: 0.001
+ - name: "grid_frequency"
+ level: 2
+ address: 5036
+ datatype: "U16"
+ multiple: 0.1
+# - name: "Reserved"
+# address: 5037
+# datatype: "U16"
+ - name: "work_state_1" # See Appendix 1
+ level: 2
+ address: 5038
+ datatype: "U16"
+ datarange:
+ - response: 0x0
+ value: "Run"
+ - response: 0x8000
+ value: "Stop"
+ - response: 0x1300
+ value: "Key Stop"
+ - response: 0x1500
+ value: "Emergancy Stop"
+ - response: 0x1400
+ value: "Standby"
+ - response: 0x1200
+ value: "Initial Stanby"
+ - response: 0x1600
+ value: "Starting"
+ - response: 0x9100
+ value: "Alarm Run"
+ - response: 0x8100
+ value: "Derating Run"
+ - response: 0x8200
+ value: "Dispatch Run"
+ - response: 0x5500
+ value: "Fault"
+ - response: 0x2500
+ value: "Communication Fault"
+ - name: "alarm_time_year"
+ level: 3
+ address: 5039
+ datatype: "U16"
+ - name: "alarm_time_month"
+ level: 3
+ address: 5040
+ datatype: "U16"
+ - name: "alarm_time_day"
+ level: 3
+ address: 5041
+ datatype: "U16"
+ - name: "alarm_time_hour"
+ level: 3
+ address: 5042
+ datatype: "U16"
+ - name: "alarm_time_minute"
+ level: 3
+ address: 5043
+ datatype: "U16"
+ - name: "alarm_time_second"
+ level: 3
+ address: 5045
+ datatype: "U16"
+ - name: "alarm_code_1" # See Appendix 3
+ level: 3
+ address: 5045
+ datatype: "U16"
+ - name: "Reserved"
+ address: 5046 - 5048
+ datatype: "U16"
+ - name: "nominal_reactive_power"
+ level: 2
+ address: 5049
+ datatype: "U16"
+ multiple: 0.1
+# - name: "Reserved"
+# address: 5050 - 5070
+# datatype: "U32"
+ - name: "array_insulation_resistance"
+ level: 2
+ address: 5071
+ datatype: "U16"
+# - name: "Reserved"
+# address: 5072
+# datatype: "U16"
+# - name: "Reserved"
+# address: 5073 - 5076
+ - name: "active_power_regulation_setpoint"
+ level: 2
+ address: 5077
+ datatype: "U32"
+ - name: "reactive_power_regulation_setpoint"
+ level: 2
+ address: 5079
+ datatype: "S32"
+ - name: "work_state_2" # See Appendix 2
+ level: 2
+ address: 5081
+ datatype: "U32"
+ datarange:
+ - response: 0
+ value: "Run"
+ - response: 1
+ value: "Stop"
+ - response: 3
+ value: "Key Stop"
+ - response: 5
+ value: "Emergancy Stop"
+ - response: 4
+ value: "Standby"
+ - response: 2
+ value: "Initial Stanby"
+ - response: 6
+ value: "Starting"
+ - response: 10
+ value: "Alarm Run"
+ - response: 11
+ value: "Derating Run"
+ - response: 12
+ value: "Dispatch Run"
+ - response: 9
+ value: "Fault"
+ - response: 13
+ value: "Communication Fault"
+ - response: 17
+ value: "Total Run Bit"
+ - response: 18
+ value: "Total Fault Bit"
+ - name: "meter_power"
+ level: 1
+ address: 5083
+ datatype: "S32"
+ indicator: True
+ smart_meter: True
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"] # This returns data on my SG7.0RT, maybe due to additional DTSU666?
+ - name: "meter_a_phase_power"
+ level: 2
+ address: 5085
+ datatype: "S32"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "meter_b_phase_power"
+ level: 2
+ address: 5087
+ datatype: "S32"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "meter_c_phase_power"
+ level: 2
+ address: 5089
+ datatype: "S32"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "load_power"
+ level: 1
+ address: 5091
+ datatype: "S32"
+ smart_meter: True
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "daily_export_energy"
+ level: 1
+ address: 5093
+ datatype: "U32"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "total_export_energy"
+ level: 1
+ address: 5095
+ datatype: "U32"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "daily_import_energy"
+ level: 1
+ address: 5097
+ datatype: "U32"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "total_import_energy"
+ level: 1
+ address: 5099
+ datatype: "U32"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "daily_direct_energy_consumption"
+ level: 1
+ address: 5101
+ datatype: "U32"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "total_direct_energy_consumption"
+ level: 1
+ address: 5103
+ datatype: "U32"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+# - name: "Reserved"
+# address: 5105 - 5112
+ - name: "mppt_4_voltage"
+ level: 2
+ address: 5115
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG50KTL-M","SG60KTL-M","SG49K5J","SG50KTL-M-20","SG60KU-M","SG80KTL-M","SG40CX","SG50CX","SG60CX-US","SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
+ - name: "mppt_4_current"
+ level: 2
+ address: 5116
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG50KTL-M","SG60KTL-M","SG49K5J","SG50KTL-M-20","SG60KU-M","SG80KTL-M","SG40CX","SG50CX","SG60CX-US","SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
+ - name: "mppt_5_voltage"
+ level: 2
+ address: 5117
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG50CX","SG60CX-US","SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
+ - name: "mppt_5_current"
+ level: 2
+ address: 5118
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG50CX","SG60CX-US","SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
+ - name: "mppt_6_voltage"
+ level: 2
+ address: 5119
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
+ - name: "mppt_6_current"
+ level: 2
+ address: 5120
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
+ - name: "mppt_7_voltage"
+ level: 2
+ address: 5121
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
+ - name: "mppt_7_current"
+ level: 2
+ address: 5122
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
+ - name: "mppt_8_voltage"
+ level: 2
+ address: 5123
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
+ - name: "mppt_8_current"
+ level: 2
+ address: 5124
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
+# - name: "Reserved"
+# address: 5125
+# - name: "Reserved"
+# address: 5126 - 5127
+ - name: "monthly_power_yields"
+ level: 1
+ address: 5128
+ datatype: "U32"
+ multiple: 0.1
+ - name: "mppt_9_voltage"
+ level: 2
+ address: 5130
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
+ - name: "mppt_9_current"
+ level: 2
+ address: 5131
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
+ - name: "mppt_10_voltage"
+ level: 2
+ address: 5132
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN"]
+ - name: "mppt_10_current"
+ level: 2
+ address: 5133
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN"]
+ - name: "mppt_11_voltage"
+ level: 2
+ address: 5134
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN"]
+ - name: "mppt_11_current"
+ level: 2
+ address: 5135
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN"]
+ - name: "mppt_12_voltage"
+ level: 2
+ address: 5136
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN"]
+ - name: "mppt_12_current"
+ level: 2
+ address: 5137
+ datatype: "U16"
+ multiple: 0.1
+ models: ["SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN"]
+# - name: "Reserved"
+# address: 5138 - 5139
+# - name: "Reserved"
+# address: 5140
+# - name: "Reserved"
+# address: 5141
+# - name: "Reserved"
+# address: 5142
+# - name: "Reserved"
+# address: 5143
+ - name: "total_power_yields"
+ level: 1
+ address: 5144
+ datatype: "U32"
+ multiple: 0.1
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M","SG3.0RT","SG4.0RT","SG5.0RT","SG6.0RT","SG7.0RT","SG8.0RT","SG10RT","SG12RT","SG15RT","SG17RT","SG20RT","SG80KTL-M","SG111HV","SG125HV","SG125HV-20","SG33CX","SG40CX","SG50CX","SG110CX","SG250HX","SG30CX","SG36CX-US","SG60CX-US","SG250HX-US","SG250HX-IN","SG25CX-SA","SG100CX","SG75CX","SG225HX"]
+ - name: "negative_voltage_to_the_ground"
+ level: 2
+ address: 5146
+ datatype: "S16"
+ multiple: 0.1
+ - name: "bus_voltage"
+ level: 2
+ address: 5147
+ datatype: "U16"
+ multiple: 0.1
+ - name: "grid_frequency"
+ level: 2
+ address: 5148
+ datatype: "U16"
+ multiple: 0.01
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M","SG3.0RT","SG4.0RT","SG5.0RT","SG6.0RT","SG7.0RT","SG8.0RT","SG10RT","SG12RT","SG15RT","SG17RT","SG20RT","SG80KTL-M","SG111HV","SG125HV","SG125HV-20","SG33CX","SG40CX","SG50CX","SG110CX","SG250HX","SG30CX","SG36CX-US","SG60CX-US","SG250HX-US","SG250HX-IN","SG25CX-SA","SG100CX","SG75CX","SG225HX"]
+# - name: "Reserved"
+# address: 5149
+# datatype: "U16"
+# multiple: 0.01
+ - name: "pid_work_state"
+ level: 2
+ address: 5150
+ datatype: "U16"
+ datarange:
+ - response: 2
+ value: "PID Recover Operation"
+ - response: 4
+ value: "Anti-PID Operation"
+ - response: 8
+ value: "PID Abnormity"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M","SG3.0RT","SG4.0RT","SG5.0RT","SG6.0RT","SG7.0RT","SG8.0RT","SG10RT","SG12RT","SG15RT","SG17RT","SG20RT","SG80KTL-M","SG111HV","SG125HV","SG125HV-20","SG33CX","SG40CX","SG50CX","SG110CX","SG250HX","SG30CX","SG36CX-US","SG60CX-US","SG250HX-US","SG250HX-IN","SG25CX-SA","SG100CX","SG75CX","SG225HX"]
+ - name: "pid_alarm_code"
+ level: 2
+ address: 5151
+ datatype: "U16"
+ datarange:
+ - response: 432
+ value: "PID resistance abnormal"
+ - response: 433
+ value: "PID function abnormal"
+ - response: 434
+ value: "PID overvoltage/overcurrent protection"
+# - name: "Reserved"
+# address: 5152
+# datatype: "U16"
+ # Folling 2 registers are not in the documentation, but other projects are using them
+ - name: "export_power"
+ level: 3
+ address: 5216
+ datatype: "U16"
+ indicator: True
+ - name: "power_meter"
+ level: 3
+ address: 5218
+ datatype: "U16"
+ multiple: 0.1
+# - name: "Reserved"
+# address: 5153-7012
+ - name: "string_1_current"
+ level: 2
+ address: 7013
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_2_current"
+ level: 2
+ address: 7014
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_3_current"
+ level: 2
+ address: 7015
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_4_current"
+ level: 2
+ address: 7016
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_5_current"
+ level: 2
+ address: 7017
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_6_current"
+ level: 2
+ address: 7018
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_7_current"
+ level: 2
+ address: 7019
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_8_current"
+ level: 2
+ address: 7020
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_9_current"
+ level: 2
+ address: 7021
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_10_current"
+ level: 2
+ address: 7022
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_11_current"
+ level: 2
+ address: 7023
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_12_current"
+ level: 2
+ address: 7024
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_13_current"
+ level: 2
+ address: 7025
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_14_current"
+ level: 2
+ address: 7026
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_15_current"
+ level: 2
+ address: 7027
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_16_current"
+ level: 2
+ address: 7028
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_17_current"
+ level: 2
+ address: 7029
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_18_current"
+ level: 2
+ address: 7030
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_19_current"
+ level: 2
+ address: 7031
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_20_current"
+ level: 2
+ address: 7032
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_21_current"
+ level: 2
+ address: 7033
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_22_current"
+ level: 2
+ address: 7034
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_23_current"
+ level: 2
+ address: 7035
+ datatype: "U16"
+ multiple: 0.01
+ - name: "string_24_current"
+ level: 2
+ address: 7036
+ datatype: "U16"
+ multiple: 0.01
+ - hold:
+ - name: "year"
+ level: 0
+ address: 5000
+ datatype: "U16"
+ - name: "month"
+ level: 0
+ address: 5001
+ datatype: "U16"
+ - name: "day"
+ level: 0
+ address: 5002
+ datatype: "U16"
+ - name: "hour"
+ level: 0
+ address: 5003
+ datatype: "U16"
+ - name: "hour"
+ level: 0
+ address: 5003
+ datatype: "U16"
+ - name: "minute"
+ level: 0
+ address: 5004
+ datatype: "U16"
+ - name: "second"
+ level: 0
+ address: 5005
+ datatype: "U16"
+ - name: "start_stop"
+ level: 1
+ address: 5006
+ datatype: "U16"
+ datarange:
+ - response: 0xCF
+ value: "Start"
+ - response: 0xCE
+ value: "Stop"
+ - name: "power_limitation_switch"
+ level: 2
+ address: 5007
+ datatype: "U16"
+ datarange:
+ - response: 0xAA
+ value: "Enable"
+ - response: 0x55
+ value: "Disable"
+ - name: "power_limitation_setting"
+ level: 2
+ address: 5008
+ datatype: "U16"
+# - name: "Reserved"
+# address: 5009
+# datatype: "U16"
+# models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "export_power_limitation"
+ level: 2
+ address: 5010
+ datatype: "U16"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ datarange:
+ - response: 0xAA
+ value: "Enable"
+ - response: 0x55
+ value: "Disable"
+ - name: "export_power_limitation_value"
+ level: 2
+ address: 5011
+ datatype: "U16"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "current_transformer_output_current"
+ level: 2
+ address: 5012
+ datatype: "U16"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "current_transformer_range"
+ level: 2
+ address: 5013
+ datatype: "U16"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "current_transformer"
+ level: 2
+ address: 5014
+ datatype: "U16"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ datarange:
+ - response: 0
+ value: "Internal"
+ - response: 1
+ value: "External"
+ - name: "export_power_limitation_percentage"
+ level: 2
+ address: 5015
+ datatype: "U16"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "installed_pv_power"
+ level: 2
+ address: 5016
+ datatype: "U16"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
+ - name: "power_factor_setting"
+ level: 2
+ address: 5019
+ datatype: "U16"
+ multiple: 0.001
+ - name: "scheduling_achieve_active_overload"
+ level: 2
+ address: 5020
+ datatype: "U16"
+ models: ["SG33CX","SG40CX","SG50CX","SG75CX","SG110CX","SG136TX","SG250HX","SG30CX","SG36CX-US","SG60CX-US","SG250HX-US","SG250HX-IN","SG225HX","SG250HX","SG25CX-SA","SG100CX","SG3.0RT","SG4.0RT","SG5.0RT","SG6.0RT","SG7.0RT","SG8.0RT","SG10RT","SG12RT","SG15RT","SG17RT","SG20RT"]
+ datarange:
+ - response: 0xAA
+ value: "Enable"
+ - response: 0x55
+ value: "Disable"
+# - name: "Reserved"
+# address: 5021-5033
+# datatype: "U16"
+ - name: "night_svg_switch"
+ level: 2
+ address: 5035
+ datatype: "U16"
+ models: ["SG3.0RT","SG4.0RT","SG5.0RT","SG6.0RT","SG7.0RT","SG8.0RT","SG10RT","SG12RT","SG15RT","SG17RT","SG20RT","SG80KTL-M","SG125HV-20","SG33CX","SG40CX","SG50CX","SG110CX","SG136TX","SG250HX","SG30CX","SG36CX-US","SG60CX-US","SG250HX-US","SG250HX-IN","SG225HX","SG250HX","SG25CX-SA","SG100CX","SG75CX"]
+ datarange:
+ - response: 0xAA
+ value: "Enable"
+ - response: 0x55
+ value: "Disable"
+ - name: "reactive_power_adjustment_mode"
+ level: 2
+ address: 5036
+ datatype: "U16"
+ datarange:
+ - response: 0x55
+ value: "Off"
+ - response: 0xA1
+ value: "Power factor setting"
+ - response: 0xA2
+ value: "Reactive power percentage setting"
+ - response: 0xA3
+ value: "Enable Q(P)"
+ - response: 0xA4
+ value: "Enable Q(U)"
+ - name: "reactive_power_percentage_setting"
+ level: 2
+ address: 5037
+ datatype: "S16"
+ multiple: 0.1
+# - name: "Reserved"
+# address: 5038
+ - name: "power_limitation_adjustment"
+ level: 2
+ address: 5039
+ datatype: "U16"
+ multiple: 0.1
+ - name: "reactive_power_adjustment"
+ level: 2
+ address: 5040
+ datatype: "S16"
+ multiple: 0.1
+ - name: "pid_recovery"
+ level: 2
+ address: 5041
+ datatype: "U16"
+ models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M","SG3.0RT","SG4.0RT","SG5.0RT","SG6.0RT","SG7.0RT","SG8.0RT","SG10RT","SG12RT","SG15RT","SG17RT","SG20RT","SG80KTL-M","SG125HV","SG125HV-20","SG80KTL","SG33CX","SG40CX","SG50CX","SG100CX、SG75CX","SG110CX","SG136TX","SG250HX","SG30CX","SG36CX-US","SG60CX-US","SG250HX-US","SG250HX-IN","SG25CX-SA","SG225HX"]
+ datarange:
+ - response: 0xAA
+ value: "Enable"
+ - response: 0x55
+ value: "Disable"
+ - name: "anti_pid"
+ level: 2
+ address: 5042
+ datatype: "U16"
+ models: ["SG125HV","SG125HV-20","SG250HX","SG250HX-US","SG250HX-IN","SG136TX","SG100CX-JP","SG225HX"]
+ datarange:
+ - response: 0xAA
+ value: "Enable"
+ - response: 0x55
+ value: "Disable"
+ - name: "fullday_pid_suppression"
+ level: 2
+ address: 5043
+ datatype: "U16"
+ models: ["SG250HX","SG250HX-US","SG250HX-IN","SG225HX"]
+ datarange:
+ - response: 0xAA
+ value: "Enable"
+ - response: 0x55
+ value: "Disable"
+# - name: "Reserved"
+# address: 5043-5047
+# - name: "Q(P) Curve"
+# address: 5048-5154
+# - name: "Reserved"
+# address: 5155-5199
+scan: # these have to be 1 less than the first register
+ - read:
+ - start: "4999"
+ range: "100"
+ level: 1
+ - start: "5099"
+ range: "100"
+ level: 1
+ - start: "5199"
+ range: "100"
+ level: 3
+ - start: "7012"
+ range: "25"
+ level: 3
+ - hold:
+# - start: "4999"
+# range: "7"
+ - start: "4999"
+ range: "10"
+ level: 0
+ - start: "5009"
+ range: "10"
+ level: 2
+ - start: "5034"
+ range: "10"
+ level: 2
+
+# full Models Supported:
+#["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"]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SunGather/sungather.py b/SunGather/sungather.py
new file mode 100644
index 0000000..9c52578
--- /dev/null
+++ b/SunGather/sungather.py
@@ -0,0 +1,261 @@
+#!/usr/bin/python3
+
+from SungrowModbusTcpClient import SungrowModbusTcpClient
+from SungrowModbusWebClient import SungrowModbusWebClient
+from pymodbus.client.sync import ModbusTcpClient
+from threading import Thread
+from version import __version__
+
+import importlib
+import logging
+import sys
+import yaml
+import time
+
+logging.basicConfig(
+ format='%(asctime)s %(levelname)-8s %(message)s',
+ level=20,
+ datefmt='%Y-%m-%d %H:%M:%S')
+logging.info(f'Starting SunGather {__version__}')
+
+try:
+ registers = yaml.safe_load(open('registers.yaml'))
+ logging.info(f"Loaded registers: registers.yaml")
+except Exception as err:
+ logging.error(f"Failed: Loading registers: registers.yaml {err}")
+ sys.exit(1)
+
+try:
+ config = yaml.safe_load(open('config.yaml'))
+ logging.info(f"Loaded config: config.yaml")
+except Exception as err:
+ logging.error(f"Failed: Loading config: config.yaml {err}")
+ sys.exit(1)
+
+logging.getLogger().setLevel(config['inverter'].get('logging',30))
+
+exports = []
+if config.get('exports'):
+ for export in config.get('exports'):
+ try:
+ if export.get('enabled', True):
+ export_load = importlib.import_module("exports." + export.get('name'))
+ logging.info(f"Loaded Export: exports\{export.get('name')}")
+ exports.append(getattr(export_load, "export_" + export.get('name'))())
+ exports[-1].configure(export)
+ logging.info(f"Configured export: {export.get('name')}")
+ except Exception as err:
+ logging.error(f"Failed loading export: {err}" +
+ f"\n\t\t\t Please make sure {export.get('name')}.py exists in the exports folder")
+
+
+
+client_payload = {
+ "host": config['inverter'].get('host', '127.0.0.1'),
+ "port": config['inverter'].get('port', 502),
+ "timeout": config['inverter'].get('timeout', 10),
+ "retries": config['inverter'].get('retries', 3),
+ "RetryOnEmpty": config['inverter'].get('RetryOnEmpty', False),
+}
+
+if config['inverter'].get("connection") == "http":
+ client_payload['port'] = config['inverter'].get('port', '8082')
+ client = SungrowModbusWebClient.SungrowModbusWebClient(**client_payload)
+elif config['inverter'].get("connection") == "sungrow":
+ client = SungrowModbusTcpClient.SungrowModbusTcpClient(**client_payload)
+elif config['inverter'].get("connection") == "modbus":
+ client = ModbusTcpClient(**client_payload)
+else:
+ logging.warning(f'inverter > connection not specified, defaulting to ModbusTcpClient')
+ client = ModbusTcpClient(**client_payload)
+logging.info("Connection: " + str(client))
+
+client.connect()
+client.close()
+
+def load_registers(register_type, start, count=100):
+ try:
+ logging.debug(f'load_registers: {register_type}, {start}:{count}')
+ if register_type == "read":
+ rr = client.read_input_registers(start,count=count, unit=config['inverter'].get('slave', 0x01))
+ elif register_type == "hold":
+ rr = client.read_holding_registers(start,count=count, unit=config['inverter'].get('slave', 0x01))
+ else:
+ raise RuntimeError(f"Unsupported register type: {type}")
+ except Exception as err:
+ logging.warning(f'No data. Try increasing the timeout or scan interval. {err}')
+ return True
+
+ if rr.isError():
+ logging.warning("Modbus connection failed")
+ return False
+
+ if not hasattr(rr, 'registers'):
+ logging.warning("No registers returned")
+ return
+
+ if len(rr.registers) != count:
+ logging.warning(f"Mismatched number of registers read {len(rr.registers)} != {count}")
+ return
+
+ for num in range(0, count):
+ run = int(start) + num + 1
+
+ for register in registers['registers'][0]['read']:
+ if register_type == "read" and register['address'] == run:
+ register_name = register['name']
+
+ # We convert a system response to a human value
+ register_value = None
+ if register.get('datarange'):
+ for value in register.get('datarange'):
+ if value['response'] == rr.registers[num]:
+ register_value = value['value']
+ if not register_value:
+ register_value = rr.registers[num]
+
+ # Adjust the value if needed
+ if register.get('indicator'):
+ 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:
+ register_value = 0
+ elif (register.get('datatype') == 'U16' or register.get('datatype') == 'S32') and register_value == 65535:
+ register_value = 0
+
+ if register.get('multiple'):
+ register_value = round(register_value * register.get('multiple'),2)
+
+ # Set the final register name and value after checking model, any adjustments above included
+ if register.get('level',3) <= config['inverter'].get('level',1) or config['inverter'].get('level',1) == 3:
+ if register.get('smart_meter') and config['inverter'].get('smart_meter'):
+ inverter[register_name] = register_value
+ elif register.get('models') and not config['inverter'].get('level',1) == 3:
+ for model in register.get('models'):
+ if model == config['inverter'].get('model'):
+ inverter[register_name] = register_value
+ else:
+ inverter[register_name] = register_value
+
+ for register in registers['registers'][1]['hold']:
+ if register_type == "hold" and register['address'] == run:
+ register_name = register['name']
+ # We convert a system response to a human value
+ register_value = None
+ if register.get('datarange'):
+ for value in register.get('datarange'):
+ if value['response'] == rr.registers[num]:
+ register_value = value['value']
+ if not register_value:
+ register_value = rr.registers[num]
+
+ # Adjust the value if needed
+ if register.get('indicator'):
+ 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:
+ register_value = 0
+ elif (register.get('datatype') == 'U16' or register.get('datatype') == 'S32') and register_value == 65535:
+ register_value = 0
+
+ if register.get('multiple'):
+ register_value = round(register_value * register.get('multiple'),2)
+
+ # Set the final register name and value after checking model, any adjustments above included
+ if register.get('level',3) <= config['inverter'].get('level',1) or config['inverter'].get('level',1) == 3:
+ if register.get('smart_meter') and config['inverter'].get('smart_meter'):
+ inverter[register_name] = register_value
+ elif register.get('models') and not config['inverter'].get('level',1) == 3:
+ for model in register.get('models'):
+ if model == config['inverter'].get('model'):
+ inverter[register_name] = register_value
+ else:
+ inverter[register_name] = register_value
+
+ return True
+
+# Inverter Scanning
+inverter = {}
+model = None
+
+# Core monitoring loop
+def scrape_inverter():
+ """ Connect to the inverter and scrape the metrics """
+ client.connect()
+
+ for scan in registers['scan']:
+ if scan.get('read'):
+ for subscan in registers['scan'][0]['read']:
+ if subscan.get('level',3) <= config['inverter'].get('level',1) or config['inverter'].get('level',1) == 3:
+ logging.debug(f'Scanning: read, {subscan.get("start")}:{subscan.get("range")}')
+ if not load_registers("read", int(subscan.get('start')), int(subscan.get('range'))):
+ return False
+ if scan.get('hold'):
+ for subscan in registers['scan'][1]['hold']:
+ if subscan.get('level',3) <= config['inverter'].get('level',1) or config['inverter'].get('level',1) == 3:
+ logging.debug(f'Scanning: hold, {subscan.get("start")}:{subscan.get("range")}')
+ if not load_registers("hold", int(subscan.get('start')), int(subscan.get('range'))):
+ return False
+
+ # Create a registers for Power imported and exported to/from Grid
+ if config['inverter'].get('level',1) >= 1:
+ try:
+ inverter["export_to_grid"] = 0
+ inverter["import_from_grid"] = 0
+ power = inverter.get('meter_power', inverter.get('export_power', 0))
+ if power < 0:
+ inverter["export_to_grid"] = abs(power)
+ elif power >= 0:
+ inverter["import_from_grid"] = power
+ except Exception:
+ pass
+
+ try:
+ inverter["timestamp"] = "%s-%s-%s %s:%02d:%02d" % (
+ inverter["year"],
+ inverter["month"],
+ inverter["day"],
+ inverter["hour"],
+ inverter["minute"],
+ inverter["second"],
+ )
+ del inverter["year"]
+ del inverter["month"]
+ del inverter["day"]
+ del inverter["hour"]
+ del inverter["minute"]
+ del inverter["second"]
+ except Exception:
+ pass
+
+ client.close()
+ return True
+
+if load_registers("read", 4999, 1):
+ model = inverter.get('device_type_code')
+ logging.info(f'Detected Model: {model}')
+if not config['inverter'].get('model'):
+ config['inverter']['model'] = model
+elif not model == config['inverter'].get('model'):
+ logging.warn(f'Model specified in config {config["inverter"].get("model")} does not match model reported by inverter {model}')
+
+while True:
+ # Scrape the inverter
+ success = scrape_inverter()
+
+ for export in exports:
+ t = Thread(target=export.publish, args=(inverter,))
+ t.start()
+
+# if args.one_shot:
+# logging.info("Exiting due to --one-shot")
+# break
+
+ # Sleep until the next scan
+ time.sleep(config['inverter'].get('scan_interval', 30))
\ No newline at end of file
diff --git a/SunGather/version.py b/SunGather/version.py
new file mode 100644
index 0000000..541f859
--- /dev/null
+++ b/SunGather/version.py
@@ -0,0 +1 @@
+__version__ = '0.1.0'
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..f8ba1d9
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+paho-mqtt>=1.5.1
+pymodbus>=2.4.0
+SungrowModbusTcpClient>=0.1.5
+SungrowModbusWebClient>=0.2.4
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..d247145
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,28 @@
+import setuptools
+
+exec(open('SunGather/version.py').read())
+
+with open("README.md", "r") as fh:
+ long_description = fh.read()
+
+setuptools.setup(
+ name="SunGather",
+ version=__version__,
+ author="Bohdan Flower",
+ author_email="github@bohdan.net",
+ description="Collect data from Sungrow Inverters and feed to various locations (MQTT, PVOutput, Home Assistant)",
+ long_description=long_description,
+ long_description_content_type="text/markdown",
+ url="https://github.com/bohdan-s/SunGather",
+ packages=setuptools.find_packages(),
+ install_requires=[
+ 'pymodbus>=2.3.0',
+ 'websocket-client>=1.2.1',
+ ],
+ classifiers=[
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
+ "Operating System :: OS Independent",
+ ],
+ python_requires='>=3.5.0',
+)