Merge branch 'main' of https://github.com/bohdan-s/SunGather into bohdan-s-main

This commit is contained in:
benni336
2022-01-25 18:28:14 +01:00
11 changed files with 893 additions and 628 deletions
+19 -4
View File
@@ -35,9 +35,10 @@ Has multiple export locations out of the box:
* 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 borrowed HEAVILY from the following projects, THANK YOU
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)
@@ -45,12 +46,18 @@ I have borrowed HEAVILY from the following projects, THANK YOU
<p align="right">(<a href="#top">back to top</a>)</p>
## Raodmap / TO DO
* Rasberry Pi Docker support
* Full Home Assistant integration, as HACS addon
## Updates
**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
@@ -254,11 +261,19 @@ SH5K-20, SH3K6, SH4K6, SH5K-V13, SH5K-30, SH3K6-30, SH4K6-30, SH5.0RS, SH3.6RS,
## 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
+4 -12
View File
@@ -1,15 +1,15 @@
inverter:
host: 192.168.1.100 # [Required] IP Address of the Inverter or Dongle
# port: 502 # [Optional] Default for modbus is 502, for http is 8082
# timeout: 10 # [Optional] Default is 10, how long to wait for a connection
# retries: 3 # [Optional] Default is 3, how many times to retry if connection fails
# slave: 0x01 # [Optional] Default is 0x01
# timeout: 10 # [Optional] Default is 10
# scan_interval: 30 # [Optional] Default is 30
connection: sungrow # [Required] options: modbus, sungrow, http
# model: "SG7.0RT" # [Optional] This is autodetected on startup, only needed if detection issues or for testing
# See model list here: https://github.com/bohdan-s/SunGather#supported
# smart_meter: True # [Optional] Default is False, Set to true if inverter supports reading grind / house consumption
# use_local_time: False # [Optional] Default False, Uses Inventer time, if try uses PC time when updating timestamps (e.g. PVOutput)
# manual_load: False # [Optional] Manually calculate load total_active_power + meter_power if the inverter does not supply it
# use_local_time: False # [Optional] Default False, Uses Inventer time, if true it uses PC time when updating timestamps (e.g. PVOutput)
# logging: 30 # [Optional] 10 = Debug, 20 = Info, 30 = Warning (default), 40 = Error
# level: 1 # [Optional] Set the amount of information to gather
# 0 = Model and Solar Generation,
@@ -68,43 +68,36 @@ exports:
- name: "Daily Generation"
sensor_type: sensor
register: daily_power_yields
unit: kWh
dev_class: energy
state_class: total_increasing
- name: "Active Power"
sensor_type: sensor
register: total_active_power
unit: W
dev_class: power
state_class: measurement
- name: "Load Power"
sensor_type: sensor
register: load_power
unit: W
dev_class: power
state_class: measurement
- name: "Meter Power"
sensor_type: sensor
register: meter_power
unit: W
dev_class: power
state_class: measurement
- name: "Export to Grid"
sensor_type: sensor
register: export_to_grid
unit: W
dev_class: power
state_class: measurement
- name: "Import from Grid"
sensor_type: sensor
register: import_from_grid
unit: W
dev_class: power
state_class: measurement
- name: "Temperature"
sensor_type: sensor
register: internal_temperature
unit: °C
dev_class: temperature
state_class: measurement
- name: "Power State"
@@ -121,6 +114,7 @@ exports:
sid: "xxxxx" # [Optional] System ID, Settings > Registered Systems > System ID
# join_team: False # [Optional] Default True, This will join the SunGather team in PVOutput, Setting to False will leave the team if previously joined
rate_limit: 60 # [Optional] Default 60, 60 for regular accounts, 300 for donation accounts
cumulative_flag: 2 # If using v2 & v4 set to 1, of using only v1 set to 2 (if daily totals)
batch_points: 1 # [Optional] Default 1, how many data points to batch upload,
# Time between uploads will be status_interval * batch_points. e.g. status_invterval of 5min, and batch_points of 12 will upload to PVOutput Hourly (5 * 12 = 60 mins)
parameters: # [Required] v1 & v3 or v2 & v4 minimum. See: https://pvoutput.org/help/api_specification.html#power-and-energy-calculation
@@ -137,8 +131,6 @@ exports:
# register: internal_temperature #
- name: v6 # Voltage
register: phase_a_voltage # Read voltage from first Phase
- name: c1 # Cumulative Flag,
value: 2 # If using v2 & v4 set to 1, of using only v1 set to 2 (if daily totals)
# - name: "v7" # Extended Value v7 - Donation Only
# register:
# - name: "v8" # Extended Value v8 - Donation Only
+22 -15
View File
@@ -1,22 +1,29 @@
import logging
class export_console(object):
def __init__(self):
return
pass
# Configure Console
def configure(self, config, config_inverter):
logging.info("Console: Configured")
print("{:<20} {:<25}".format('Config','Value'))
for setting in config_inverter:
print("{:<40} {:<25}".format(setting,str(config_inverter.get(setting))))
def configure(self, config, inverter):
print("+----------------------------------------------+")
print("{:<46} {:<1}".format("| " + 'Inverter Configuration Settings',"|"))
print("+----------------------------------------------+")
print("{:<20} {:<25} {:<1}".format("| " + 'Config',"| " + 'Value', "|"))
print("+--------------------+-------------------------+")
for setting, value in inverter.client_config.items():
print("{:<20} {:<25} {:<1}".format("| " + str(setting), "| " + str(value), "|"))
for setting, value in inverter.inverter_config.items():
print("{:<20} {:<25} {:<1}".format("| " + str(setting), "| " + str(value), "|"))
print("+----------------------------------------------+")
return True
def publish(self, inverter):
print("+----------------------------------------------------------------------+")
print("| {:<7} | {:<35} | {:<20} |".format('Address', 'Register','Value'))
print("+---------+-------------------------------------+----------------------+")
for register, value in inverter.latest_scrape.items():
print("| {:<7} | {:<35} | {:<20} |".format(str(inverter.getRegisterAddress(register)), str(register), str(value) + " " + str(inverter.getRegisterUnit(register))))
print("+----------------------------------------------------------------------+")
print(f"Logged {len(inverter.latest_scrape)} registers to Console")
print("{:<40} {:<25}".format('Register','Value'))
for register in inverter:
print("{:<40} {:<25}".format(register,str(inverter.get(register))))
print(f"Logged {len(inverter)} registers to Console")
return
return True
+44 -22
View File
@@ -4,51 +4,60 @@ from influxdb_client.client.write_api import SYNCHRONOUS
class export_influxdb(object):
def __init__(self):
self._isConfigured = False
self.client = None
self.write_api = None
self.bucket = None
self.measurements = []
# Configure InfluxDB
def configure(self, config, config_inverter):
if not config.get('token') and config.get('org') and config.get('bucket') and config.get('measurements'):
def configure(self, config, inverter):
self.influxdb_config = {
'url': config.get('url', "http://localhost:8086"),
'token': config.get('token', None),
'username': config.get('username', None),
'password': config.get('password', None),
'org': config.get('org',None),
'bucket': config.get('bucket',None)
}
self.influxdb_measurements = [{}]
self.influxdb_measurements.pop() # Remove null value from list
if not self.influxdb_config['org'] or not self.influxdb_config['bucket'] or not (self.influxdb_config['token'] or (self.influxdb_config['username'] and self.influxdb_config['password'])):
logging.warning(f"InfluxDB: Please check configuration")
return False
try:
if config.get('token', False):
if self.influxdb_config['token']:
self.client = influxdb_client.InfluxDBClient(
url=config.get('url', 'http://localhost:8086'),
token=config.get('token'),
org=config.get('org')
url=self.influxdb_config['url'],
token=self.influxdb_config['token'],
org=self.influxdb_config['org']
)
elif config.get('username',False) and config.get('password',False):
self.client = influxdb_client.InfluxDBClient(
url=config.get('url', 'http://localhost:8086'),
token=f"{config.get('username')}:{config.get('password')}",
org=config.get('org')
url=self.influxdb_config['url'],
token=f"{self.influxdb_config['username']}:{self.influxdb_config['password']}",
org=self.influxdb_config['org']
)
except Exception as err:
logging.error(f"InfluxDB: Error: {err}")
return False
self.bucket=config.get('bucket')
for measurement in config.get('measurements'):
self.measurements.append(measurement)
if not inverter.validateRegister(measurement['register']):
logging.error(f"InfluxDB: Configured to use {measurement['register']} but not configured to scrape this register")
return False
self.influxdb_measurements.append(measurement)
self.write_api = self.client.write_api(write_options=SYNCHRONOUS)
logging.info(f"InfluxDB: Configured: {self.client.url}")
self._isConfigured = True
return self._isConfigured
return True
def publish(self, inverter):
if not self._isConfigured:
logging.info("InfluxDB: Skipped, Initial Configuration Failed")
return False
#HEAD
'''if not self._isConfigured:
# logging.info("InfluxDB: Skipped, Initial Configuration Failed")
# return False
# Setting a Standard Measurement Name and Inverter as a tag for later filtering
# could be extended by using values from config.yaml or SerialNo or ...
@@ -61,8 +70,21 @@ class export_influxdb(object):
# remove last ","
sequence=sequence[:-1]
logging.debug(f'InfluxDB: Sequence; {sequence}')
'''
# ---------
sequence = []
for measurement in self.influxdb_measurements:
if not inverter.validateLatestScrape(measurement['register']):
logging.error(f"InfluxDB: Skipped collecting data, {measurement['register']} missing from last scrape")
return False
sequence.append(f"{measurement['point']},inverter={inverter.getInverterModel(True)} {measurement['register']}={inverter.getRegisterValue(measurement['register'])}")
logging.debug(f'InfluxDB: Sequence; {sequence}')
# f11ee5fe75be6aa1bc8046901443122e4116b702
try:
self.write_api.write(self.bucket, self.client.org, sequence)
self.write_api.write(self.influxdb_config['bucket'], self.client.org, sequence)
except Exception as err:
logging.error("InfluxDB: " + str(err))
+65 -56
View File
@@ -6,91 +6,100 @@ class export_mqtt(object):
def __init__(self):
self.mqtt_client = None
self.sensor_topic = None
self.homeassistant = False
self.ha_discovery = True
self.ha_sensors = []
self.model = None
self.model_clean = None
self.inverter_ip = None
self.mqtt_queue = []
self.ha_discovery_published = False
# Configure MQTT
def configure(self, config, config_inverter):
def configure(self, config, inverter):
self.mqtt_config = {
'host': config.get('host', None),
'port': config.get('port', 1883),
'topic': config.get('topic', "inverter/{model}/registers").replace('{model}', inverter.getInverterModel(True)),
'username': config.get('username', None),
'password': config.get('password',None),
'homeassistant': config.get('homeassistant',False)
}
self.ha_sensors = [{}]
self.ha_sensors.pop() # Remove null value from list
if not self.mqtt_config['host']:
logging.info(f"MQTT: Host config is required")
return False
self.mqtt_client = mqtt.Client(client_id="SunGather")
self.mqtt_client.on_connect = self.on_connect
self.mqtt_client.on_disconnect = self.on_disconnect
self.mqtt_client.on_publish = self.on_publish
if config.get('username') and config.get('password'):
self.mqtt_client.username_pw_set(config.get('username'), config.get('password'))
if self.mqtt_config['username'] and self.mqtt_config['password']:
self.mqtt_client.username_pw_set(self.mqtt_config['username'], self.mqtt_config['password'])
if config.get('port') == 8883:
if self.mqtt_config['port'] == 8883:
self.mqtt_client.tls_set()
self.mqtt_client.connect_async(config.get('host'), port=config.get('port', 1883), keepalive=60)
self.mqtt_client.connect_async(self.mqtt_config['host'], port=self.mqtt_config['port'], keepalive=60)
self.mqtt_client.loop_start()
self.inverter_ip = config_inverter.get('host')
self.sensor_topic = config.get('topic', 'inverter/{model}/registers')
self.homeassistant = config.get('homeassistant', False)
if self.homeassistant:
if self.mqtt_config['homeassistant']:
for ha_sensor in config.get('ha_sensors'):
self.ha_sensors.append(ha_sensor)
if not inverter.validateRegister(ha_sensor['register']):
logging.error(f"MQTT: Configured to use {ha_sensor['register']} but not configured to scrape this register")
return False
else:
self.ha_sensors.append(ha_sensor)
return True
def on_connect(self, client, userdata, flags, rc):
logging.info(f"MQTT: Connected {client._host}:{client._port}")
logging.info(f"MQTT: Connected to {client._host}:{client._port}")
def on_disconnect(self, client, userdata, rc):
logging.info(f"MQTT: Server Disconnected code:{rc}")
logging.info(f"MQTT: Server Disconnected code: {rc}")
def on_publish(self, client, userdata, mid):
self.mqtt_queue.remove(mid)
try:
self.mqtt_queue.remove(mid)
except Exception as err:
pass
logging.info(f"MQTT: Message {mid} Published")
def cleanName(self, name):
return name.lower().replace(' ','_')
def publish(self, inverter):
if not self.mqtt_client.is_connected():
logging.warning(f'MQTT: Server Disconnected; {self.mqtt_queue.__len__()} messages queued')
logging.warning(f'MQTT: Server Disconnected; {self.mqtt_queue.__len__()} messages queued, will automatically attempt to reconnect')
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')
if not self.model:
self.model = inverter.get('device_type_code', 'unknown')
self.model_clean = self.model.replace('.','').replace('-','')
self.sensor_topic = self.sensor_topic.replace('{model}', self.model_clean)
logging.debug(f"MQTT: Publishing: {self.mqtt_config['topic']} : {json.dumps(inverter.latest_scrape)}")
self.mqtt_queue.append(self.mqtt_client.publish(self.mqtt_config['topic'], json.dumps(inverter.latest_scrape).replace('"', '\"'), qos=0).mid)
logging.debug(f'MQTT: Sensor Topic = {self.sensor_topic}')
logging.debug(f"MQTT: Publishing: {self.sensor_topic} : {json.dumps(inverter)}")
self.mqtt_queue.append(self.mqtt_client.publish(self.sensor_topic, json.dumps(inverter).replace('"', '\"'), qos=1).mid)
if self.mqtt_config['homeassistant'] and not self.ha_discovery_published:
for ha_sensor in self.ha_sensors:
config_msg = {}
if ha_sensor.get('name'):
ha_topic = 'homeassistant/' + ha_sensor.get('sensor_type', 'sensor') + '/inverter/' + self.cleanName(ha_sensor.get('name')) + '/config'
config_msg['name'] = "Inverter " + ha_sensor.get('name')
config_msg['unique_id'] = "inverter_" + self.cleanName(ha_sensor.get('name'))
config_msg['state_topic'] = self.mqtt_config['topic']
config_msg['value_template'] = "{{ value_json." + ha_sensor.get('register') + " }}"
config_msg['unit_of_measurement'] = inverter.getRegisterUnit(ha_sensor.get('register'))
if ha_sensor.get('dev_class'):
config_msg['device_class'] = ha_sensor.get('dev_class')
if ha_sensor.get('state_class'):
config_msg['state_class'] = ha_sensor.get('state_class')
if ha_sensor.get('payload_on'):
config_msg['payload_on'] = ha_sensor.get('payload_on')
if ha_sensor.get('payload_off'):
config_msg['payload_off'] = ha_sensor.get('payload_off')
config_msg['ic'] = "mdi:solar-power"
config_msg['device'] = { "name":"Solar Inverter", "mf":"Sungrow", "mdl":inverter.getInverterModel(), "connections":[["address", inverter.getHost() ]]}
if self.homeassistant:
if self.ha_discovery:
for ha_sensor in self.ha_sensors:
config_msg = {}
if ha_sensor.get('name'):
ha_topic = 'homeassistant/' + ha_sensor.get('sensor_type', 'sensor') + '/inverter/' + ha_sensor.get('name').lower().replace(' ','_') + '/config'
config_msg['name'] = "Inverter " + ha_sensor.get('name')
config_msg['unique_id'] = "inverter_" + ha_sensor.get('name').lower().replace(' ','_')
config_msg['state_topic'] = self.sensor_topic
config_msg['value_template'] = "{{ value_json." + ha_sensor.get('register') + " }}"
if ha_sensor.get('unit'):
config_msg['unit_of_measurement'] = ha_sensor.get('unit')
if ha_sensor.get('dev_class'):
config_msg['device_class'] = ha_sensor.get('dev_class')
if ha_sensor.get('state_class'):
config_msg['state_class'] = ha_sensor.get('state_class')
if ha_sensor.get('payload_on'):
config_msg['payload_on'] = ha_sensor.get('payload_on')
if ha_sensor.get('payload_off'):
config_msg['payload_off'] = ha_sensor.get('payload_off')
config_msg['ic'] = "mdi:solar-power"
config_msg['device'] = { "name":"Solar Inverter", "mf":"Sungrow", "mdl":self.model, "connections":[["address", self.inverter_ip ]]}
logging.debug(f'MQTT: Topic; {ha_topic}, Message: {config_msg}')
self.mqtt_queue.append(self.mqtt_client.publish(ha_topic, json.dumps(config_msg), retain=True, qos=1).mid)
self.ha_discovery = False
logging.info("MQTT: Published Home Assistant Discovery messages")
logging.debug(f'MQTT: Topic; {ha_topic}, Message: {config_msg}')
self.mqtt_queue.append(self.mqtt_client.publish(ha_topic, json.dumps(config_msg), retain=True, qos=1).mid)
self.ha_discovery_published = True
logging.info("MQTT: Published Home Assistant Discovery messages")
return True
+117 -105
View File
@@ -3,45 +3,75 @@ import requests
import datetime
import time
# Configure PVOutput
"""
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
"""
class export_pvoutput(object):
def __init__(self):
self._isConfigured = False
self.api_key = None
self.system_id = None
self.url_base = "https://pvoutput.org/service/r2/"
self.url_addbatchstatus = self.url_base + "addbatchstatus.jsp"
self.url_jointeam = self.url_base + "jointeam.jsp"
self.url_leaveteam = self.url_base + "leaveteam.jsp"
self.url_getsystem = self.url_base + "getsystem.jsp"
self.rate_limit = None
self.parameters = []
self.last_run = None
self.tid = '1618'
self.status_interval = 5
@property
def headers(self):
return {
"X-Pvoutput-Apikey": self.api_key,
"X-Pvoutput-SystemId": self.system_id,
"X-Pvoutput-Apikey": self.pvoutput_config['api'],
"X-Pvoutput-SystemId": self.pvoutput_config['sid'],
"Content-Type": "application/x-www-form-urlencoded",
"cache-control": "no-cache",
}
def configure(self, config, config_inverter):
self.api_key = config.get('api')
self.system_id = config.get('sid')
self.rate_limit = config.get('rate_limit', 60)
self.status_interval = config.get('status_interval',5)
self.batch_points = config.get('batch_points',1)
def configure(self, config, inverter):
self.pvoutput_config = {
'api': config.get('api', None),
'sid': config.get('sid', None),
'join_team': config.get('join_team', True),
'rate_limit': config.get('rate_limit', 60),
'cumulative_flag': config.get('cumulative_flag',0),
'batch_points': config.get('batch_points',1)
}
self.pvoutput_parameters = [{}]
self.pvoutput_parameters.pop() # Remove null value from list
self.collected_data = {}
self.payload_data = None
self.batch_data = []
self.batch_count = 0
self.last_run = 0
self.last_publish = 0
for parameter in config.get('parameters'):
self.parameters.append(parameter)
if not inverter.validateRegister(parameter['register']):
logging.error(f"PVOutput: Configured to use {parameter['register']} but not configured to scrape this register")
return False
self.pvoutput_parameters.append(parameter)
try:
logging.debug(f"PVOutput: Get System ; {self.url_getsystem}, {str(self.headers)}, 'teams': '1'")
response = requests.post(url=self.url_getsystem,headers=self.headers, params={'teams': '1'}, timeout=3)
@@ -58,6 +88,7 @@ class export_pvoutput(object):
for team in teams.split(','):
if team == self.tid:
team_member = True
break
else:
logging.error(f"PVOutput: System Status Failed; {str(response.status_code)} Message; {str(response.content)}")
@@ -67,76 +98,40 @@ class export_pvoutput(object):
return False
try:
join_team = config.get('join_team',True)
if not team_member and join_team:
if not team_member and self.pvoutput_config['join_team']:
logging.debug(f"PVOutput: Join Team; {self.url_jointeam}, {str(self.headers)}, 'tid': '{self.tid}'")
response = requests.post(url=self.url_jointeam,headers=self.headers, params={'tid': self.tid}, timeout=3)
logging.debug(f"PVOutput: Response; {str(response.status_code)} Message; {str(response.content)}")
elif team_member and not join_team:
elif team_member and not self.pvoutput_config['join_team']:
logging.debug(f"PVOutput: Leave Team; {self.url_leaveteam}, {str(self.headers)}, 'tid': '{self.tid}'")
response = requests.post(url=self.url_leaveteam,headers=self.headers, params={'tid': self.tid}, timeout=3)
logging.debug(f"PVOutput: Response; {str(response.status_code)} Message; {str(response.content)}")
except Exception as err:
logging.debug(f"{err}")
pass
logging.info(f"PVOutput: Configured export to {invertername} every {self.status_interval} minutes")
return True
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
"""
def collect_data(self, inverter):
# Check all required registers have been returned by the inverter
if not inverter.get('timestamp', False):
logging.warning('PVOutput: Skipping, Timestamp missing from last scrape')
return False
for parameter in self.parameters:
if parameter.get('name')[0] == 'v':
if not inverter.get(parameter.get('register', False)) and not inverter.get(parameter.get('register', False)) == 0:
logging.warning(f"PVOutput: Skipping, {parameter.get('name')} configured to use {parameter.get('register')} but inverter is not returning this register")
return False
now = datetime.datetime.strptime(inverter.get('timestamp'), "%Y-%m-%d %H:%M:%S")
if not inverter.validateLatestScrape('timestamp'):
logging.error(f"PVOutput: Skipped collecting data, Timestamp missing from last scrape")
return False
for parameter in self.pvoutput_parameters:
if not inverter.validateLatestScrape(parameter['register']):
logging.error(f"PVOutput: Skipped collecting data, {parameter['register']} missing from last scrape")
return False
# Add new data to old data and increase count of data points
for parameter in self.parameters:
if parameter.get('name')[0] == 'v':
if self.collected_data.get(parameter.get('name'),False):
if parameter.get('multiple'):
self.collected_data[parameter.get('name')] = round(self.collected_data[parameter.get('name')] + (inverter.get(parameter.get('register')) * parameter.get('multiple')),3)
else:
self.collected_data[parameter.get('name')] = round(self.collected_data[parameter.get('name')] + inverter.get(parameter.get('register')),3)
else:
if parameter.get('multiple'):
self.collected_data[parameter.get('name')] = round(inverter.get(parameter.get('register')) * parameter.get('multiple'),3)
else:
self.collected_data[parameter.get('name')] = inverter.get(parameter.get('register'))
elif parameter.get('name') == 'c1':
cumulative_energy = parameter.get('value')
for parameter in self.pvoutput_parameters:
value = inverter.getRegisterValue(parameter.get('register'))
if parameter.get('multiple'):
value = value * parameter.get('multiple')
if self.collected_data.get(parameter.get('name'),False):
self.collected_data[parameter.get('name')] = round(self.collected_data[parameter.get('name')] + value,3)
else:
self.collected_data[parameter.get('name')] = value
if self.collected_data.get('count',False):
self.collected_data['count'] +=1
@@ -145,41 +140,58 @@ class export_pvoutput(object):
logging.debug(f'PVOutput: Data Logged: {self.collected_data}')
return True
def publish(self, inverter):
self.collect_data(inverter)
# Process data points every status_interval
if((time.time() - self.last_publish) >= (self.status_interval * 60)):
for v in self.collected_data:
if v[0] == 'v' and not self.collected_data[v] == 0:
if v[1] == '6' or v[1] == '7': # Round to 1 decimal place
self.collected_data[v] = round(self.collected_data[v] / self.collected_data['count'], 1)
else: # Getting errors when uploading decimals for power/energy so return INT
self.collected_data[v] = int((self.collected_data[v] / self.collected_data['count']))
any_data = False
if inverter.validateLatestScrape('timestamp'):
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"))
for x in range(1, 13):
field = 'v' + str(x)
if self.collected_data.get(field):
if x == 6 or x == 7: # Round to 1 decimal place
value = round(self.collected_data[field] / self.collected_data['count'], 1)
else: # Getting errors when uploading decimals for power/energy so return INT
value = int((self.collected_data[field] / self.collected_data['count']))
data_point = data_point + "," + str(value)
any_data = True
else:
data_point = data_point + ","
self.collected_data = {}
data_point = str(now.strftime("%Y%m%d")) + "," + str(now.strftime("%H:%M"))
if any_data:
self.batch_data.append(data_point)
else:
logging.warning(f"PVOutput: No data collected in last {(self.status_interval * 60)} minutes")
for x in range(1, 13):
v = 'v' + str(x)
if self.collected_data.get(v):
data_point = data_point + "," + str(self.collected_data[v])
else:
data_point = data_point + ","
self.collected_data = {}
if self.payload_data:
self.payload_data = self.payload_data + ";"
else:
self.payload_data = ""
self.payload_data = self.payload_data + data_point
# 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.batch_points:
payload = {}
payload['data'] = self.payload_data
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:
payload_data = data
if 'cumulative_energy' in locals():
payload['c1'] = cumulative_energy
payload = {}
payload['data'] = payload_data
if self.pvoutput_config['cumulative_flag'] > 0:
payload['c1'] = self.pvoutput_config['cumulative_flag']
try:
logging.debug("PVOutput: Request; " + self.url_addbatchstatus + ", " + str(self.headers) + " : " + str(payload))
@@ -187,9 +199,9 @@ class export_pvoutput(object):
self.batch_count = 0
if response.status_code != requests.codes.ok:
logging.error(f"PVOutput: Upload Failed; {str(response.status_code)} Message; {str(response.content)}")
logging.error(f"PVOutput: Upload Failed; {str(response.status_code)} Message; {str(response.text)}")
else:
self.payload_data = None
self.batch_data = []
self.last_publish = time.time()
logging.info("PVOutput: Data uploaded")
except Exception as err:
@@ -198,6 +210,6 @@ class export_pvoutput(object):
else:
logging.info("PVOutput: Data added to next batch upload")
else:
logging.info(f"PVOutput: Data logged, next upload in {int((self.status_interval * 60) - (time.time() - self.last_publish))} secs")
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()
+13 -19
View File
@@ -8,39 +8,33 @@ import logging
class export_webserver(object):
html_body = "Pending Data Retrieval"
def __init__(self):
self._isConfigured = False
return
False
# Configure Webserver
def configure(self, config, config_inverter):
def configure(self, config, inverter):
try:
self.webServer = HTTPServer(('', config.get('port',8080)), MyServer)
self.t = Thread(target=self.webServer.serve_forever)
self.t.daemon = True # Make it a deamon, so if main loop ends the webserver dies
self.t.start()
self.config_inverter = config_inverter
logging.info(f"Webserver: Configured")
self._isConfigured = True
except Exception as err:
logging.error(f"Webserver: Error: {err}")
return self._isConfigured
return False
return True
def publish(self, inverter):
if not self._isConfigured:
logging.info("Webserver: Skipped, Initial Configuration Failed")
return False
body = "<h3>Sungather v" + __version__ + "</h3></p><table><th>Address</th><tr><th>Register</th><th>Value</th></tr>"
for register, value in inverter.latest_scrape.items():
body = body + f"<tr><td>{str(inverter.getRegisterAddress(register))}</td><td>{str(register)}</td><td>{str(value)} {str(inverter.getRegisterUnit(register))}</td></tr>"
export_webserver.html_body = body + f"</table><p>Total {len(inverter.latest_scrape)} registers"
body = "<h3>Sungather v" + __version__ + "</h3></p><table><tr><th>Register</th><th>Value</th>"
for register in inverter:
body = body + f"<tr><td>{register}</td><td>{inverter.get(register)}</td></tr>"
export_webserver.html_body = body + f"</table><p>Total {len(inverter)} registers"
body = "</p></p><table><tr><th>Configuration</th><th>Value</th>"
for config in self.config_inverter:
body = body + f"<tr><td>{config}</td><td>{self.config_inverter.get(config)}</td></tr>"
body = "</p></p><table><tr><th>Configuration</th><th>Value</th></tr>"
for setting, value in inverter.client_config.items():
body = body + f"<tr><td>{str(setting)}</td><td>{str(value)}</td></tr>"
for setting, value in inverter.inverter_config.items():
body = body + f"<tr><td>{str(setting)}</td><td>{str(value)}</td></tr>"
export_webserver.html_body = export_webserver.html_body + body + f"</table></p>"
logging.info("Webserver: Content Updated")
return True
+204 -93
View File
@@ -5,7 +5,7 @@ registers:
address: 4990
datatype: "UTF-8"
- name: "device_type_code"
level: 0
level: 3
address: 5000
datatype: "U16"
datarange:
@@ -178,7 +178,8 @@ registers:
level: 2
address: 5001
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "kW"
- name: "output_type"
level: 2
address: 5002
@@ -194,96 +195,114 @@ registers:
level: 0
address: 5003
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "kWh"
- name: "total_power_yields"
level: 1
address: 5004
datatype: "U32"
unit: "KWh"
- name: "total_running_time"
level: 1
address: 5006
datatype: "U32"
models: ["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"]
unit: "h"
- name: "internal_temperature"
level: 1
address: 5008
datatype: "S16"
multiple: 0.1
accuracy: 0.1
unit: "°C"
- name: "total_apparent_power"
level: 1
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"]
unit: "VA"
- name: "mppt_1_voltage"
level: 2
address: 5011
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
- name: "mppt_1_current"
level: 2
address: 5012
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "A"
- name: "mppt_2_voltage"
level: 2
address: 5013
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
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","SG5K-D","SG8K-D"]
- name: "mppt_2_current"
level: 2
address: 5014
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "A"
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","SG5K-D","SG8K-D"]
- name: "mppt_3_voltage"
level: 2
address: 5015
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
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
accuracy: 0.1
unit: "A"
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","SG8K-D"]
- name: "total_dc_power"
level: 2
address: 5017
datatype: "S32" # Documentation says Unsigned, but seems to be returning Signed 32bit
unit: "W"
- name: "phase_a_voltage"
level: 1
address: 5019
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
- name: "phase_b_voltage"
level: 2
address: 5020
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
- name: "phase_c_voltage"
level: 2
address: 5021
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
- name: "phase_a_current"
level: 2
address: 5022
datatype: "S16" # Documentation says Unsigned, but seems to be returning Signed 16bit
multiple: 0.1
accuracy: 0.1
unit: "A"
models: ["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"]
- name: "phase_b_current"
level: 2
address: 5023
datatype: "S16" # Documentation says Unsigned, but seems to be returning Signed 16bit
multiple: 0.1
accuracy: 0.1
unit: "A"
models: ["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"]
- name: "phase_c_current"
level: 2
address: 5024
datatype: "S16" # Documentation says Unsigned, but seems to be returning Signed 16bit
multiple: 0.1
accuracy: 0.1
unit: "A"
models: ["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"]
# - name: "Reserved"
# address: 5025
@@ -298,21 +317,24 @@ registers:
level: 0
address: 5031
datatype: "U32"
unit: "W"
models: ["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","SG5K-D","SG8K-D"]
- name: "total_reactive_power"
level: 2
address: 5033
datatype: "S32"
unit: "Var"
- name: "power_factor" # >0 means leading, <0 means lagging
level: 2
address: 5035
datatype: "S16"
multiple: 0.001
accuracy: 0.001
- name: "grid_frequency"
level: 2
address: 5036
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "Hz"
# - name: "Reserved"
# address: 5037
# datatype: "U16"
@@ -389,7 +411,8 @@ registers:
level: 2
address: 5049
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "kVar"
models: ["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","SG5K-D","SG8K-D"]
# - name: "Reserved"
# address: 5050 - 5070
@@ -398,6 +421,7 @@ registers:
level: 2
address: 5071
datatype: "U16"
unit: "k-ohm"
models: ["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"]
# - name: "Reserved"
# address: 5072
@@ -408,11 +432,13 @@ registers:
level: 2
address: 5077
datatype: "U32"
unit: "w"
models: ["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"]
- name: "reactive_power_regulation_setpoint"
level: 2
address: 5079
datatype: "S32"
unit: "Var"
models: ["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"]
- name: "work_state_2" # See Appendix 2
level: 2
@@ -454,119 +480,151 @@ registers:
datatype: "S32"
indicator: True
smart_meter: True
unit: "w"
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
- name: "meter_a_phase_power"
level: 2
address: 5085
datatype: "S32"
unit: "w"
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"
unit: "w"
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"
unit: "w"
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
unit: "w"
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"
accuracy: 0.1
unit: "kWh"
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M","SG5K-D","SG8K-D"]
- name: "total_export_energy"
level: 1
address: 5095
datatype: "U32"
accuracy: 0.1
unit: "kWh"
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M","SG5K-D","SG8K-D"]
- name: "daily_import_energy"
level: 1
address: 5097
datatype: "U32"
accuracy: 0.1
unit: "kWh"
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M","SG5K-D","SG8K-D"]
- name: "total_import_energy"
level: 1
address: 5099
datatype: "U32"
accuracy: 0.1
unit: "kWh"
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M","SG5K-D","SG8K-D"]
- name: "daily_direct_energy_consumption"
level: 1
address: 5101
datatype: "U32"
accuracy: 0.1
unit: "kWh"
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M","SG5K-D","SG8K-D"]
- name: "total_direct_energy_consumption"
level: 1
address: 5103
datatype: "U32"
accuracy: 0.1
unit: "kWh"
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M","SG5K-D","SG8K-D"]
# - name: "Reserved"
# address: 5105 - 5112
- name: "daily_running_time"
level: 1
address: 5113
datatype: "U16"
unit: "min"
- name: "mppt_4_voltage"
level: 2
address: 5115
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
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
accuracy: 0.1
unit: "A"
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
accuracy: 0.1
unit: "V"
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
accuracy: 0.1
unit: "A"
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
accuracy: 0.1
unit: "V"
models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
- name: "mppt_6_current"
level: 2
address: 5120
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "A"
models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
- name: "mppt_7_voltage"
level: 2
address: 5121
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
- name: "mppt_7_current"
level: 2
address: 5122
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "A"
models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
- name: "mppt_8_voltage"
level: 2
address: 5123
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
- name: "mppt_8_current"
level: 2
address: 5124
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "A"
models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
# - name: "Reserved"
# address: 5125
@@ -576,55 +634,64 @@ registers:
level: 1
address: 5128
datatype: "U32"
multiple: 0.1
accuracy: 0.1
unit: "kWh"
models: ["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"]
- name: "mppt_9_voltage"
level: 2
address: 5130
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
- name: "mppt_9_current"
level: 2
address: 5131
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "A"
models: ["SG110CX","SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN","SG75CX"]
- name: "mppt_10_voltage"
level: 2
address: 5132
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
models: ["SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN"]
- name: "mppt_10_current"
level: 2
address: 5133
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "A"
models: ["SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN"]
- name: "mppt_11_voltage"
level: 2
address: 5134
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
models: ["SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN"]
- name: "mppt_11_current"
level: 2
address: 5135
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "A"
models: ["SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN"]
- name: "mppt_12_voltage"
level: 2
address: 5136
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
models: ["SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN"]
- name: "mppt_12_current"
level: 2
address: 5137
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "A"
models: ["SG250HX","SG250HX-US","SG100CX","SG100CX-JP","SG250HX-IN"]
# - name: "Reserved"
# address: 5138 - 5139
@@ -640,25 +707,29 @@ registers:
level: 1
address: 5144
datatype: "U32"
multiple: 0.1
accuracy: 0.1
unit: "kWh"
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","SG5K-D","SG8K-D"]
- name: "negative_voltage_to_the_ground"
level: 2
address: 5146
datatype: "S16"
multiple: 0.1
accuracy: 0.1
unit: "V"
models: ["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"]
- name: "bus_voltage"
level: 2
address: 5147
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "V"
models: ["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"]
- name: "grid_frequency"
level: 2
address: 5148
datatype: "U16"
multiple: 0.01
accuracy: 0.01
unit: "Hz"
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","SG5K-D","SG8K-D"]
# - name: "Reserved"
# address: 5149
@@ -791,145 +862,170 @@ registers:
level: 2
address: 7013
datatype: "U16"
multiple: 0.01
accuracy: 0.01
unit: "A"
models: ["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"]
- name: "string_2_current"
level: 2
address: 7014
datatype: "U16"
multiple: 0.01
accuracy: 0.01
unit: "A"
models: ["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"]
- name: "string_3_current"
level: 2
address: 7015
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["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", "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", "SG7.0RT", "SG8.0RT", "SG10RT", "SG12RT", ]
- name: "string_4_current"
level: 2
address: 7016
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["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", "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", ]
- name: "string_5_current"
level: 2
address: 7017
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["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", "SG50KTL-M-20", "SG60KTL", "SG80KTL", "SG80KTL-20", "SG60KU-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", ]
- name: "string_6_current"
level: 2
address: 7018
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["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", "SG50KTL-M-20", "SG60KTL", "SG80KTL", "SG80KTL-20", "SG60KU-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", ]
- name: "string_7_current"
level: 2
address: 7019
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG30KTL", "SG30KU", "SG36KTL", "SG36KU", "SG40KTL", "SG40KTL-M", "SG50KTL-M", "SG60KTL-M", "SG60KU", "SG30KTL-M", "SG30KTL-M-V31", "SG33KTL-M", "SG36KTL-M", "SG33K3J", "SG49K5J", "SG34KJ", "SG50KTL-M-20", "SG60KTL", "SG80KTL", "SG80KTL-20", "SG60KU-M", "SG80KTL-M", "SG30CX", "SG33CX", "SG36CX-US", "SG40CX", "SG50CX", "SG60CX-US", "SG110CX", "SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN", "SG75CX", ]
- name: "string_8_current"
level: 2
address: 7020
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG30KTL", "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", "SG60KTL", "SG80KTL", "SG80KTL-20", "SG60KU-M", "SG80KTL-M", "SG30CX", "SG33CX", "SG36CX-US", "SG40CX", "SG50CX", "SG60CX-US", "SG110CX", "SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN", "SG75CX", ]
- name: "string_9_current"
level: 2
address: 7021
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG30KU", "SG36KTL", "SG36KU", "SG40KTL-M", "SG50KTL-M", "SG60KTL-M", "SG33K3J", "SG49K5J", "SG50KTL-M-20", "SG60KTL", "SG80KTL", "SG80KTL-20", "SG60KU-M", "SG80KTL-M", "SG30CX", "SG33CX", "SG36CX-US", "SG50CX", "SG60CX-US", "SG110CX", "SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN", "SG75CX", ]
- name: "string_10_current"
level: 2
address: 7022
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG30KU", "SG36KTL", "SG36KU", "SG50KTL-M", "SG60KTL-M", "SG49K5J", "SG50KTL-M-20", "SG60KTL", "SG80KTL", "SG80KTL-20", "SG60KU-M", "SG80KTL-M", "SG30CX", "SG33CX", "SG36CX-US", "SG50CX", "SG60CX-US", "SG110CX", "SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN", "SG75CX", ]
- name: "string_11_current"
level: 2
address: 7023
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG50KTL-M", "SG60KTL-M", "SG49K5J", "SG50KTL-M-20", "SG60KTL", "SG80KTL", "SG80KTL-20", "SG60KU-M", "SG80KTL-M", "SG30CX", "SG33CX", "SG36CX-US", "SG110CX", "SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN", "SG75CX", ]
- name: "string_12_current"
level: 2
address: 7024
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG50KTL-M", "SG60KTL-M", "SG49K5J", "SG50KTL-M-20", "SG60KTL", "SG80KTL", "SG80KTL-20", "SG60KU-M", "SG80KTL-M", "SG30CX", "SG33CX", "SG36CX-US", "SG110CX", "SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN", "SG75CX", ]
- name: "string_13_current"
level: 2
address: 7025
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG60KTL-M", "SG60KTL", "SG80KTL", "SG80KTL-20", "SG60KU-M", "SG80KTL-M", "SG30CX", "SG33CX", "SG36CX-US", "SG110CX", "SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN", "SG75CX", ]
- name: "string_14_current"
level: 2
address: 7026
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG60KTL-M", "SG60KTL", "SG80KTL", "SG80KTL-20", "SG60KU-M", "SG80KTL-M", "SG30CX", "SG33CX", "SG36CX-US", "SG110CX", "SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN", "SG75CX", "SG3.0RT", "SG4.0RT", "SG5.0RT", "SG6.0RT", ]
- name: "string_15_current"
level: 2
address: 7027
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG60KTL-M", "SG80KTL", "SG80KTL-20", "SG60KU-M", "SG80KTL-M", "SG110CX", "SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN", "SG75CX"]
- name: "string_16_current"
level: 2
address: 7028
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG60KTL-M", "SG80KTL", "SG80KTL-20", "SG60KU-M", "SG80KTL-M", "SG110CX", "SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN", "SG75CX"]
- name: "string_17_current"
level: 2
address: 7029
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG80KTL", "SG80KTL-20", "SG110CX", "SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN", "SG75CX"]
- name: "string_18_current"
level: 2
address: 7030
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG80KTL", "SG80KTL-20", "SG110CX", "SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN", "SG75CX"]
- name: "string_19_current"
level: 2
address: 7031
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN"]
- name: "string_20_current"
level: 2
address: 7032
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN"]
- name: "string_21_current"
level: 2
address: 7033
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN"]
- name: "string_22_current"
level: 2
address: 7034
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN"]
- name: "string_23_current"
level: 2
address: 7035
datatype: "U16"
multiple: 0.01
models: ["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"]
accuracy: 0.01
unit: "A"
models: ["SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN"]
- name: "string_24_current"
level: 2
address: 7036
datatype: "U16"
multiple: 0.01
accuracy: 0.01
unit: "A"
models: ["SG250HX", "SG250HX-US", "SG100CX", "SG100CX-JP", "SG250HX-IN"]
- name: "system_state"
level: 2
address: 13000
@@ -1002,30 +1098,32 @@ registers:
level: 0
address: 5000
datatype: "U16"
unit: "YYYY"
- name: "month"
level: 0
address: 5001
datatype: "U16"
unit: "MM"
- name: "day"
level: 0
address: 5002
datatype: "U16"
unit: "DD"
- name: "hour"
level: 0
address: 5003
datatype: "U16"
- name: "hour"
level: 0
address: 5003
datatype: "U16"
unit: "HH"
- name: "minute"
level: 0
address: 5004
datatype: "U16"
unit: "MM"
- name: "second"
level: 0
address: 5005
datatype: "U16"
unit: "SS"
- name: "start_stop"
level: 1
address: 5006
@@ -1048,6 +1146,8 @@ registers:
level: 2
address: 5008
datatype: "U16"
accuracy: 0.1
unit: "%"
# - name: "Reserved"
# address: 5009
# datatype: "U16"
@@ -1071,11 +1171,13 @@ registers:
level: 2
address: 5012
datatype: "U16"
unit: "A"
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"
unit: "A"
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
- name: "current_transformer"
level: 2
@@ -1091,17 +1193,21 @@ registers:
level: 2
address: 5015
datatype: "U16"
accuracy: 0.1
unit: "%"
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"
accuracy: 0.01
unit: "KW"
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
accuracy: 0.001
- name: "scheduling_achieve_active_overload"
level: 2
address: 5020
@@ -1144,17 +1250,22 @@ registers:
level: 2
address: 5037
datatype: "S16"
accuracy: 0.1
unit: "%"
# - name: "Reserved"
# address: 5038
- name: "power_limitation_adjustment"
level: 2
address: 5039
datatype: "U16"
multiple: 0.1
accuracy: 0.1
unit: "kW"
- name: "reactive_power_adjustment"
level: 2
address: 5040
datatype: "S16"
accuracy: 0.1
unit: "kVar"
- name: "pid_recovery"
level: 2
address: 5041
+403 -300
View File
@@ -3,7 +3,6 @@
from SungrowModbusTcpClient import SungrowModbusTcpClient
from SungrowModbusWebClient import SungrowModbusWebClient
from pymodbus.client.sync import ModbusTcpClient
from threading import Thread
from version import __version__
from datetime import datetime
@@ -15,208 +14,378 @@ import yaml
import time
import os
config = None
registers = None
register_ranges = None
inverter = None
client = None
logging.basicConfig(
format='%(asctime)s %(levelname)-8s %(message)s',
level=20,
datefmt='%Y-%m-%d %H:%M:%S')
def connect_inverter():
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')
new_client = SungrowModbusWebClient.SungrowModbusWebClient(**client_payload)
elif config['inverter'].get("connection") == "sungrow":
new_client = SungrowModbusTcpClient.SungrowModbusTcpClient(**client_payload)
elif config['inverter'].get("connection") == "modbus":
new_client = ModbusTcpClient(**client_payload)
else:
logging.warning(f'inverter > connection not specified, defaulting to ModbusTcpClient')
new_client = ModbusTcpClient(**client_payload)
logging.info("Connection: " + str(new_client))
class SungrowInverter():
def __init__(self, config_inverter):
self.client_config = {
"host": config_inverter.get('host'),
"port": config_inverter.get('port'),
"timeout": config_inverter.get('timeout'),
"retries": config_inverter.get('retries'),
"RetryOnEmpty": False,
}
self.inverter_config = {
"slave": config_inverter.get('slave'),
"model": config_inverter.get('model'),
"level": config_inverter.get('level'),
"use_local_time": config_inverter.get('use_local_time'),
"smart_meter": config_inverter.get('smart_meter'),
"connection": config_inverter.get('connection')
}
self.client = None
self.registers = [[]]
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.register_ranges = [[]]
self.register_ranges.pop() # Remove null value from list
if not config['inverter'].get("connection") == "http":
new_client.connect()
new_client.close()
self.latest_scrape = {}
return new_client
def scrape_inverter():
global inverter
scrape_start = datetime.now()
client.connect()
load_registers_count = 0
load_registers_failed = 0
for range in register_ranges:
load_registers_count +=1
logging.debug(f'Scraping: {range.get("type")}, {range.get("start")}:{range.get("range")}')
if not load_registers(range.get('type'), int(range.get('start')), int(range.get('range'))):
load_registers_failed +=1
if load_registers_failed == load_registers_count:
return False
if load_registers_failed > 0:
logging.info(f'Scraping: {load_registers_failed}/{load_registers_count} registers failed to scrape')
client.close()
# 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: # If inverter is returning no data for load_power, we can calculate it manually
if config['inverter'].get('manual_load', False):
inverter["load_power"] = int(inverter.get('total_active_power')) + int(inverter.get('meter_power'))
except Exception:
pass
# See if the inverter is running, This is added to inverters so can be read via MQTT etc...
# It is also used below, as some registers hold the last value on 'stop' so we need to set to 0
# to help with graphing.
try:
if inverter.get('start_stop'):
if inverter.get('start_stop', False) == 'Start' and inverter.get('work_state_1', False) == 'Run':
inverter["run_state"] = "ON"
def connect(self):
if self.client:
if(self.client.connect()):
return True
else:
inverter["run_state"] = "OFF"
else:
inverter["run_state"] = "OFF"
except Exception:
pass
self.disconnect()
if config['inverter'].get('use_local_time',False):
inverter["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
logging.debug(f'Using Local Computer Time: {inverter.get("timestamp")}')
del inverter["year"]
del inverter["month"]
del inverter["day"]
del inverter["hour"]
del inverter["minute"]
del inverter["second"]
else:
if self.inverter_config['connection'] == "http":
self.client_config['port'] = '8082'
self.client = SungrowModbusWebClient.SungrowModbusWebClient(**self.client_config)
elif self.inverter_config['connection'] == "sungrow":
self.client = SungrowModbusTcpClient.SungrowModbusTcpClient(**self.client_config)
elif self.inverter_config['connection'] == "modbus":
self.client = ModbusTcpClient(**self.client_config)
else:
logging.warning(f"Inverter: Unknown connection type {self.inverter_config['connection']}, Valid options are http, sungrow or modbus")
return False
logging.info("Connection: " + str(self.client))
retval = self.client.connect()
# Wait 3 seconds, fixes timing issues
time.sleep(3)
if not self.inverter_config['connection'] == "http":
self.client.close()
return retval
def close(self):
logging.debug("Closed session: " + str(self.client))
self.client.close()
def disconnect(self):
logging.info("Disconnected: " + str(self.client))
self.client.close()
self.client = None
def configure_registers(self,registersfile):
# Check model so we can load only valid registers
if self.inverter_config.get('model'):
logging.info(f"Bypassing Model Detection, Using config: {self.inverter_config.get('model')}")
else:
# Load just the register to detect model, then we can load the rest of registers based on returned model
for register in registersfile['registers'][0]['read']:
if register.get('name') == "device_type_code":
register['type'] = "read"
self.registers.append(register)
if self.load_registers(register['type'], register['address'] -1, 1): # Needs to be address -1
if isinstance(self.latest_scrape.get('device_type_code'),int):
logging.warning(f"Unknown Type Code Detected: {self.latest_scrape.get('device_type_code')}")
else:
self.inverter_config['model'] = self.latest_scrape.get('device_type_code')
logging.info(f"Detected Model: {self.inverter_config.get('model')}")
else:
logging.info(f'Model detection failed, please set model in config.py')
self.registers.pop()
break
# Load register list based on name and value after checking model
for register in registersfile['registers'][0]['read']:
if register.get('level',3) <= self.inverter_config.get('level') or self.inverter_config.get('level') == 3:
register['type'] = "read"
register.pop('level')
if register.get('smart_meter') and self.inverter_config.get('smart_meter'):
register.pop('models')
self.registers.append(register)
elif register.get('models') and not self.inverter_config.get('level') == 3:
for supported_model in register.get('models'):
if supported_model == self.inverter_config.get('model'):
register.pop('models')
self.registers.append(register)
else:
self.registers.append(register)
for register in registersfile['registers'][1]['hold']:
if register.get('level',3) <= self.inverter_config.get('level') or self.inverter_config.get('level') == 3:
register['type'] = "hold"
register.pop('level')
if register.get('smart_meter') and self.inverter_config.get('smart_meter'):
register.pop('models')
self.registers.append(register)
elif register.get('models') and not self.inverter_config.get('level',1) == 3:
for supported_model in register.get('models'):
if supported_model == self.inverter_config.get('model'):
register.pop('models')
self.registers.append(register)
else:
self.registers.append(register)
# Load register list based om name and value after checking model
for register_range in registersfile['scan'][0]['read']:
register_range_used = False
register_range['type'] = "read"
for register in self.registers:
if register.get("type") == register_range.get("type"):
if register.get('address') >= register_range.get("start") and register.get('address') <= (register_range.get("start") + register_range.get("range")):
register_range_used = True
continue
if register_range_used:
self.register_ranges.append(register_range)
for register_range in registersfile['scan'][1]['hold']:
register_range_used = False
register_range['type'] = "hold"
for register in self.registers:
if register.get("type") == register_range.get("type"):
if register.get('address') >= register_range.get("start") and register.get('address') <= (register_range.get("start") + register_range.get("range")):
register_range_used = True
continue
if register_range_used:
self.register_ranges.append(register_range)
def load_registers(self, register_type, start, count=100):
try:
inverter["timestamp"] = "%s-%s-%s %s:%02d:%02d" % (
inverter["year"],
inverter["month"],
inverter["day"],
inverter["hour"],
inverter["minute"],
inverter["second"],
)
logging.debug(f'Using Inverter Time: {inverter.get("timestamp")}')
del inverter["year"]
del inverter["month"]
del inverter["day"]
del inverter["hour"]
del inverter["minute"]
del inverter["second"]
logging.debug(f'load_registers: {register_type}, {start}:{count}')
if register_type == "read":
rr = self.client.read_input_registers(start,count=count, unit=self.inverter_config['slave'])
elif register_type == "hold":
rr = self.client.read_holding_registers(start,count=count, unit=self.inverter_config['slave'])
else:
raise RuntimeError(f"Unsupported register type: {type}")
except Exception as err:
logging.warning(f'No data returned for {register_type}, {start}:{count}\n\t\t\t\t{str(err)}')
return False
if rr.isError():
logging.warning(f"Modbus connection failed: {rr}")
return False
if not hasattr(rr, 'registers'):
logging.warning("No registers returned")
return False
if len(rr.registers) != count:
logging.warning(f"Mismatched number of registers read {len(rr.registers)} != {count}")
return False
for num in range(0, count):
run = int(start) + num + 1
for register in self.registers:
if register_type == register['type'] and register['address'] == run:
register_name = register['name']
register_value = None
# Return the 32bit value if needed
if register.get('datatype') == "U32" or register.get('datatype') == "S32":
u32_value = rr.registers[num+1]
if u32_value and register_value:
register_value = (u32_value * 65535) + register_value
# We convert a system response to a human value
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 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
if register.get('accuracy'):
register_value = round(register_value * register.get('accuracy'),2)
# Set the final register value with adjustments above included
self.latest_scrape[register_name] = register_value
break
return True
def validateRegister(self, check_register):
for register in self.registers:
if check_register == register['name']:
return True
for register in self.registers_custom:
if check_register == register['name']:
return True
return False
def getRegisterAddress(self, check_register):
for register in self.registers:
if check_register == register['name']:
return register['address']
return '----'
def getRegisterUnit(self, check_register):
for register in self.registers:
if check_register == register['name']:
return register.get('unit','')
for register in self.registers_custom:
if check_register == register['name']:
return register.get('unit','')
return ''
def validateLatestScrape(self, check_register):
for register, value in self.latest_scrape.items():
if check_register == register:
return True
return False
def getRegisterValue(self, check_register):
for register, value in self.latest_scrape.items():
if check_register == register:
return value
return False
def getHost(self):
return self.client_config['host']
def getInverterModel(self, clean=False):
if clean:
return self.inverter_config['model'].replace('.','').replace('-','')
else:
return self.inverter_config['model']
def scrape(self):
scrape_start = datetime.now()
self.connect()
# Clear previous inverter values, keep the model and run state
if self.latest_scrape.get("run_state"): run_state = self.latest_scrape.get("run_state")
else: run_state = "ON"
self.latest_scrape = {}
self.latest_scrape['device_type_code'] = self.inverter_config['model']
self.latest_scrape["run_state"] = run_state
load_registers_count = 0
load_registers_failed = 0
for range in self.register_ranges:
load_registers_count +=1
logging.debug(f'Scraping: {range.get("type")}, {range.get("start")}:{range.get("range")}')
if not self.load_registers(range.get('type'), int(range.get('start')), int(range.get('range'))):
load_registers_failed +=1
if load_registers_failed == load_registers_count:
# If every scrape fails, disconnect the client
self.disconnect()
return False
if load_registers_failed > 0:
logging.info(f'Scraping: {load_registers_failed}/{load_registers_count} registers failed to scrape')
self.close()
# Create a registers for Power imported and exported to/from Grid
if self.inverter_config['level'] >= 1:
try:
self.latest_scrape["export_to_grid"] = 0
self.latest_scrape["import_from_grid"] = 0
power = self.latest_scrape.get('meter_power', self.latest_scrape.get('export_power', 0))
if power < 0:
self.latest_scrape["export_to_grid"] = abs(power)
elif power >= 0:
self.latest_scrape["import_from_grid"] = power
except Exception:
pass
try: # If inverter is returning no data for load_power, we can calculate it manually
if not self.latest_scrape["load_power"]:
self.latest_scrape["load_power"] = int(self.latest_scrape.get('total_active_power')) + int(self.latest_scrape.get('meter_power'))
except Exception:
pass
# See if the inverter is running, This is added to inverters so can be read via MQTT etc...
# It is also used below, as some registers hold the last value on 'stop' so we need to set to 0
# to help with graphing.
try:
if self.latest_scrape.get('start_stop'):
if self.latest_scrape.get('start_stop', False) == 'Start' and self.latest_scrape.get('work_state_1', False) == 'Run':
self.latest_scrape["run_state"] = "ON"
else:
self.latest_scrape["run_state"] = "OFF"
else:
self.latest_scrape["run_state"] = "OFF"
except Exception:
pass
scrape_end = datetime.now()
logging.info(f'Inverter: Successfully scraped in {(scrape_end - scrape_start).seconds}.{(scrape_end - scrape_start).microseconds} secs')
return True
def load_registers(register_type, start, count=100):
global inverter
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))
if self.inverter_config.get('use_local_time',False):
self.latest_scrape["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
logging.debug(f'Using Local Computer Time: {self.latest_scrape.get("timestamp")}')
del self.latest_scrape["year"]
del self.latest_scrape["month"]
del self.latest_scrape["day"]
del self.latest_scrape["hour"]
del self.latest_scrape["minute"]
del self.latest_scrape["second"]
else:
raise RuntimeError(f"Unsupported register type: {type}")
except Exception as err:
logging.warning(f'No data returned for {register_type}, {start}:{count}\n\t\t\t\t{str(err)}')
return False
try:
self.latest_scrape["timestamp"] = "%s-%s-%s %s:%02d:%02d" % (
self.latest_scrape["year"],
self.latest_scrape["month"],
self.latest_scrape["day"],
self.latest_scrape["hour"],
self.latest_scrape["minute"],
self.latest_scrape["second"],
)
logging.debug(f'Using Inverter Time: {self.latest_scrape.get("timestamp")}')
del self.latest_scrape["year"]
del self.latest_scrape["month"]
del self.latest_scrape["day"]
del self.latest_scrape["hour"]
del self.latest_scrape["minute"]
del self.latest_scrape["second"]
except Exception:
pass
if rr.isError():
logging.warning(f"Modbus connection failed: {rr}")
return False
# If alarm state exists then convert to timestamp, otherwise remove it
try:
if self.latest_scrape["pid_alarm_code"]:
self.latest_scrape["alarm_timestamp"] = "%s-%s-%s %s:%02d:%02d" % (
self.latest_scrape["alarm_time_year"],
self.latest_scrape["alarm_time_month"],
self.latest_scrape["alarm_time_day"],
self.latest_scrape["alarm_time_hour"],
self.latest_scrape["alarm_time_minute"],
self.latest_scrape["alarm_time_second"],
)
del self.latest_scrape["alarm_time_year"]
del self.latest_scrape["alarm_time_month"]
del self.latest_scrape["alarm_time_day"]
del self.latest_scrape["alarm_time_hour"]
del self.latest_scrape["alarm_time_minute"]
del self.latest_scrape["alarm_time_second"]
except Exception:
pass
if not hasattr(rr, 'registers'):
logging.warning("No registers returned")
return False
scrape_end = datetime.now()
logging.info(f'Inverter: Successfully scraped in {(scrape_end - scrape_start).seconds}.{(scrape_end - scrape_start).microseconds} secs')
if len(rr.registers) != count:
logging.warning(f"Mismatched number of registers read {len(rr.registers)} != {count}")
return False
for num in range(0, count):
run = int(start) + num + 1
for register in registers:
if register_type == register.get('type') and register['address'] == run:
register_name = register['name']
register_value = None
# Return the 32bit value if needed
if register.get('datatype') == "U32" or register.get('datatype') == "S32":
u32_value = rr.registers[num+1]
if u32_value and register_value:
register_value = (u32_value * 65535) + register_value
# We convert a system response to a human value
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 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
if register.get('multiple'):
register_value = round(register_value * register.get('multiple'),2)
# Set the final register value with adjustments above included
inverter[register_name] = register_value
break
return True
return True
def main():
global config
global registers
global register_ranges
global inverter
global client
configfile = 'config.yaml'
configfilename = 'config.yaml'
try:
opts, args = getopt.getopt(sys.argv[1:],"hc:v:", "runonce")
@@ -237,7 +406,7 @@ def main():
print(f'python3 sungather.py -c /full/path/config.yaml\n')
sys.exit()
elif opt == '-c':
configfile = arg
configfilename = arg
elif opt == '-v':
if arg.isnumeric():
if int(arg) >= 0 and int(arg) <= 50:
@@ -254,152 +423,86 @@ def main():
logging.info(f'Starting SunGather {__version__}')
try:
config = yaml.safe_load(open(configfile))
logging.info(f"Loaded config: {configfile}")
configfile = yaml.safe_load(open(configfilename))
logging.info(f"Loaded config: {configfilename}")
except Exception as err:
logging.error(f"Failed: Loading config: {configfile} \n\t\t\t {err}")
logging.error(f"Failed: Loading config: {configfilename} \n\t\t\t {err}")
sys.exit(1)
if not config.get('inverter'):
logging.error(f"Failed Loading config, missing Inverter settings")
sys.exit(1)
if not configfile.get('inverter'):
logging.error(f"Failed Loading config, missing Inverter settings")
sys.exit(f"Failed Loading config, missing Inverter settings")
try:
registers_raw = yaml.safe_load(open('registers.yaml'))
registersfile = yaml.safe_load(open('registers.yaml'))
logging.info(f"Loaded registers: {os.getcwd()}/registers.yaml")
except Exception as err:
logging.error(f"Failed: Loading registers: {os.getcwd()}/registers.yaml {err}")
sys.exit(1)
sys.exit(f"Failed: Loading registers: {os.getcwd()}/registers.yaml {err}")
config_inverter = {
"host": configfile['inverter'].get('host',None),
"port": configfile['inverter'].get('port',502),
"timeout": configfile['inverter'].get('timeout',10),
"retries": configfile['inverter'].get('retries',3),
"slave": configfile['inverter'].get('slave',0x01),
"scan_interval": configfile['inverter'].get('scan_interval',15),
"connection": configfile['inverter'].get('connection',"modbus"),
"model": configfile['inverter'].get('model',None),
"smart_meter": configfile['inverter'].get('smart_meter',False),
"use_local_time": configfile['inverter'].get('use_local_time',False),
"manual_load": configfile['inverter'].get('manual_load',False),
"logging": configfile['inverter'].get('logging',30),
"level": configfile['inverter'].get('level',1)
}
if 'loglevel' in locals():
logging.getLogger().setLevel(loglevel)
else:
logging.getLogger().setLevel(config['inverter'].get('logging',30))
logging.getLogger().setLevel(config_inverter['logging'])
logging.debug(f'Inverter Config Loaded: {config_inverter}')
if config_inverter.get('host'):
inverter = SungrowInverter(config_inverter)
else:
logging.error(f"Error: host option in config is required")
sys.exit("Error: host option in config is required")
if not inverter.connect():
logging.error(f"Error: Connection to inverter failed: {config_inverter.get('host')}:{config_inverter.get('port')}")
sys.exit(f"Error: Connection to inverter failed: {config_inverter.get('host')}:{config_inverter.get('port')}")
inverter.configure_registers(registersfile)
# Now we know the inverter is working, lets load the exports
exports = []
if config.get('exports'):
for export in config.get('exports'):
if configfile.get('exports'):
for export in configfile.get('exports'):
try:
if export.get('enabled', False):
export_load = importlib.import_module("exports." + export.get('name'))
logging.info(f"Loading Export: exports\{export.get('name')}")
exports.append(getattr(export_load, "export_" + export.get('name'))())
exports[-1].configure(export, config['inverter'])
retval = exports[-1].configure(export, inverter)
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 = connect_inverter()
inverter = {}
registers = [[]]
registers.pop()
register_ranges = [[]]
register_ranges.pop()
scan_interval = config_inverter.get('scan_interval')
# Inverter Model Scanning
model = 'unknown'
if config['inverter'].get('model'):
model = config['inverter'].get('model')
logging.info(f'Bypassing Model Detection, Using config: {model}')
else:
# Load just the register to detect model, then we can load the rest of registers based on returned model
for register in registers_raw['registers'][0]['read']:
if register.get('name') == "device_type_code":
register['type'] = "read"
registers.append(register)
break
if load_registers("read", 4999, 1):
registers.pop() # Remove the register, as we only needed it once
if isinstance(inverter.get('device_type_code'),int):
logging.warning(f"Unknown Type Code Detected: {inverter.get('device_type_code')}")
else:
model = inverter.get('device_type_code')
logging.info(f"Detected Model: {model}")
else:
logging.info(f'Model detection failed, please set model in config.py')
# Load register list based om name and value after checking model
for register in registers_raw['registers'][0]['read']:
if register.get('level',3) <= config['inverter'].get('level',1) or config['inverter'].get('level',1) == 3:
register['type'] = "read"
register.pop('level')
if register.get('smart_meter') and config['inverter'].get('smart_meter'):
register.pop('models')
registers.append(register)
elif register.get('models') and not config['inverter'].get('level',1) == 3:
for supported_model in register.get('models'):
if supported_model == inverter.get('device_type_code'):
register.pop('models')
registers.append(register)
else:
registers.append(register)
for register in registers_raw['registers'][1]['hold']:
if register.get('level',3) <= config['inverter'].get('level',1) or config['inverter'].get('level',1) == 3:
register['type'] = "hold"
register.pop('level')
if register.get('smart_meter') and config['inverter'].get('smart_meter'):
register.pop('models')
registers.append(register)
elif register.get('models') and not config['inverter'].get('level',1) == 3:
for supported_model in register.get('models'):
if supported_model == inverter.get('device_type_code'):
register.pop('models')
registers.append(register)
else:
registers.append(register)
# Load register list based om name and value after checking model
for register_range in registers_raw['scan'][0]['read']:
register_range_used = False
register_range['type'] = "read"
for register in registers:
if register.get("type") == register_range.get("type"):
if register.get('address') >= register_range.get("start") and register.get('address') <= (register_range.get("start") + register_range.get("range")):
register_range_used = True
continue
if register_range_used:
register_ranges.append(register_range)
for register_range in registers_raw['scan'][1]['hold']:
register_range_used = False
register_range['type'] = "hold"
for register in registers:
if register.get("type") == register_range.get("type"):
if register.get('address') >= register_range.get("start") and register.get('address') <= (register_range.get("start") + register_range.get("range")):
register_range_used = True
continue
if register_range_used:
register_ranges.append(register_range)
scan_interval = config['inverter'].get('scan_interval', 30)
# Core monitoring loop
# Core polling loop
while True:
loop_start = datetime.now()
# Clear previous inverter values, keep the model and run state
if inverter.get("run_state"): run_state = inverter["run_state"]
else: run_state = "ON"
inverter = {}
inverter['device_type_code'] = model
inverter["run_state"] = run_state
if not client:
client = connect_inverter()
time.sleep(3) # Wait 3 secs for connection, resvoles issues with modbus not reconnecting on failure
inverter.connect()
# Scrape the inverter
success = scrape_inverter()
success = inverter.scrape()
if(success):
for export in exports:
export.publish(inverter)
else:
client.close()
client = None
inverter.disconnect()
logging.warning(f"Data collection failed, skipped exporting data. Retying in {scan_interval} secs")
loop_end = datetime.now()
@@ -420,4 +523,4 @@ def main():
if __name__== "__main__":
main()
sys.exit()
sys.exit()
+1 -1
View File
@@ -1 +1 @@
__version__ = '0.2.10'
__version__ = '0.3.1'
+1 -1
View File
@@ -3,5 +3,5 @@ requests>=2.26.0
paho-mqtt>=1.5.1
pymodbus>=2.4.0
SungrowModbusTcpClient>=0.1.5
SungrowModbusWebClient>=0.2.6
SungrowModbusWebClient>=0.2.7
influxdb-client>=1.24.0