From 295944de6f2bd615e940f08531f3d87116dbda31 Mon Sep 17 00:00:00 2001 From: bohdan-s Date: Tue, 25 Jan 2022 11:41:02 +1100 Subject: [PATCH 1/7] v0.3.0 large re-write of inverter scraping code --- README.md | 9 +- SunGather/config-example.yaml | 13 +- SunGather/exports/console.py | 37 +- SunGather/exports/influxdb.py | 61 +-- SunGather/exports/mqtt.py | 116 +++--- SunGather/exports/pvoutput.py | 222 ++++++----- SunGather/exports/webserver.py | 32 +- SunGather/registers.yaml | 297 ++++++++++----- SunGather/sungather.py | 673 ++++++++++++++++++--------------- SunGather/version.py | 2 +- requirements.txt | 2 +- 11 files changed, 842 insertions(+), 622 deletions(-) diff --git a/README.md b/README.md index 878c5aa..6b65307 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,15 @@ I have borrowed HEAVILY from the following projects, THANK YOU * 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 +* lots more... + **0.2.2** * Minor bug fixes diff --git a/SunGather/config-example.yaml b/SunGather/config-example.yaml index 7db3739..d844d22 100644 --- a/SunGather/config-example.yaml +++ b/SunGather/config-example.yaml @@ -8,8 +8,7 @@ inverter: # 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 +67,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 +113,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 +130,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 diff --git a/SunGather/exports/console.py b/SunGather/exports/console.py index f396dc5..62362fe 100644 --- a/SunGather/exports/console.py +++ b/SunGather/exports/console.py @@ -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 \ No newline at end of file + return True \ No newline at end of file diff --git a/SunGather/exports/influxdb.py b/SunGather/exports/influxdb.py index 0bc4276..528b67d 100644 --- a/SunGather/exports/influxdb.py +++ b/SunGather/exports/influxdb.py @@ -4,58 +4,67 @@ 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 - sequence = [] - for measurement in self.measurements: - sequence.append(f"{measurement.get('point')},inverter={inverter.get('device_type_code', 'unknown').replace('.','').replace('-','')} {measurement.get('register')}={inverter.get(measurement.get('register'))}") - logging.debug(f'InfluxDB: 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}') + 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)) diff --git a/SunGather/exports/mqtt.py b/SunGather/exports/mqtt.py index d1decba..783cbd2 100644 --- a/SunGather/exports/mqtt.py +++ b/SunGather/exports/mqtt.py @@ -6,91 +6,97 @@ 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) 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 \ No newline at end of file diff --git a/SunGather/exports/pvoutput.py b/SunGather/exports/pvoutput.py index 53dd461..3bf7cd3 100644 --- a/SunGather/exports/pvoutput.py +++ b/SunGather/exports/pvoutput.py @@ -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() \ No newline at end of file diff --git a/SunGather/exports/webserver.py b/SunGather/exports/webserver.py index da3e7ef..f2e7085 100644 --- a/SunGather/exports/webserver.py +++ b/SunGather/exports/webserver.py @@ -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 = "

Sungather v" + __version__ + "

" + for register, value in inverter.latest_scrape.items(): + body = body + f"" + export_webserver.html_body = body + f"
Address
RegisterValue
{str(inverter.getRegisterAddress(register))}{str(register)}{str(value)} {str(inverter.getRegisterUnit(register))}

Total {len(inverter.latest_scrape)} registers" - body = "

Sungather v" + __version__ + "

" - for register in inverter: - body = body + f"" - export_webserver.html_body = body + f"
RegisterValue
{register}{inverter.get(register)}

Total {len(inverter)} registers" - - body = "

" - for config in self.config_inverter: - body = body + f"" + body = "

ConfigurationValue
{config}{self.config_inverter.get(config)}
" + for setting, value in inverter.client_config.items(): + body = body + f"" + for setting, value in inverter.inverter_config.items(): + body = body + f"" export_webserver.html_body = export_webserver.html_body + body + f"
ConfigurationValue
{str(setting)}{str(value)}
{str(setting)}{str(value)}

" - logging.info("Webserver: Content Updated") return True diff --git a/SunGather/registers.yaml b/SunGather/registers.yaml index f31af50..c8bebab 100644 --- a/SunGather/registers.yaml +++ b/SunGather/registers.yaml @@ -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 diff --git a/SunGather/sungather.py b/SunGather/sungather.py index c1cb628..7a02fa9 100644 --- a/SunGather/sungather.py +++ b/SunGather/sungather.py @@ -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,359 @@ 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') + } + 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 connect(self, connection="modbus"): + if connection == "http": + self.client_config['port'] = '8082' + self.client = SungrowModbusWebClient.SungrowModbusWebClient(**self.client_config) + elif connection == "sungrow": + self.client = SungrowModbusTcpClient.SungrowModbusTcpClient(**self.client_config) + elif connection == "modbus": + self.client = ModbusTcpClient(**self.client_config) + else: + logging.warning(f'Inverter: Unknown connection type {connection}, Valid options are http, sungrow or modbus') + return False + logging.info("Connection: " + str(self.client)) -def scrape_inverter(): - global inverter + retval = self.client.connect() + if not connection == "http": + self.client.close() - scrape_start = datetime.now() - client.connect() + return retval - load_registers_count = 0 - load_registers_failed = 0 + 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 - 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') + # 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) - client.close() + 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) - # Create a registers for Power imported and exported to/from Grid - if config['inverter'].get('level',1) >= 1: + # 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["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" + 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: - inverter["run_state"] = "OFF" - else: - inverter["run_state"] = "OFF" - except Exception: - pass + 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 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 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.client.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: + return False + if load_registers_failed > 0: + logging.info(f'Scraping: {load_registers_failed}/{load_registers_count} registers failed to scrape') + + self.client.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: - 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"] + 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 +387,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 +404,85 @@ 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) - - if 'loglevel' in locals(): - logging.getLogger().setLevel(loglevel) - else: - logging.getLogger().setLevel(config['inverter'].get('logging',30)) + 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), + "slave": configfile['inverter'].get('slave',0x01), + "timeout": configfile['inverter'].get('timeout',10), + "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) + } + + logging.getLogger().setLevel(configfile['inverter'].get('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(config_inverter.get('connection')): + 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 + if not inverter.client: + inverter.connect() + time.sleep(3) # Wait 3 secs for connection, resolves issues with modbus not reconnecting on failure # Scrape the inverter - - success = scrape_inverter() + success = inverter.scrape() if(success): for export in exports: export.publish(inverter) else: - client.close() - client = None + inverter.client.close() + inverter.client = None logging.warning(f"Data collection failed, skipped exporting data. Retying in {scan_interval} secs") loop_end = datetime.now() diff --git a/SunGather/version.py b/SunGather/version.py index f0ca935..290d7c6 100644 --- a/SunGather/version.py +++ b/SunGather/version.py @@ -1 +1 @@ -__version__ = '0.2.10' \ No newline at end of file +__version__ = '0.3.0' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f046c05..f0ce8fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 From 9e7a926fca51a8fc1cc2b2db8209d15105eb233b Mon Sep 17 00:00:00 2001 From: bohdan-s Date: Tue, 25 Jan 2022 12:40:58 +1100 Subject: [PATCH 2/7] Quick fix to MQTT publish messages --- README.md | 14 +++++++++++--- SunGather/exports/mqtt.py | 5 +++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6b65307..554477d 100644 --- a/README.md +++ b/README.md @@ -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,10 +46,8 @@ I have borrowed HEAVILY from the following projects, THANK YOU

(back to top)

## 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** @@ -56,6 +55,7 @@ I have borrowed HEAVILY from the following projects, THANK YOU * 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... @@ -261,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 . +``` +Current Arch (docker build) ```sh docker build --no-cache --rm -t bohdans/sungather:latest -t bohdans/sungather:v . docker push bohdans/sungather -a ``` +

(back to top)

## License diff --git a/SunGather/exports/mqtt.py b/SunGather/exports/mqtt.py index 783cbd2..18b106a 100644 --- a/SunGather/exports/mqtt.py +++ b/SunGather/exports/mqtt.py @@ -58,8 +58,9 @@ class export_mqtt(object): logging.info(f"MQTT: Server Disconnected code: {rc}") def on_publish(self, client, userdata, mid): - self.mqtt_queue.remove(mid) - logging.info(f"MQTT: Message {mid} Published") + if mid: + self.mqtt_queue.remove(mid) + logging.info(f"MQTT: Message {mid} Published") def cleanName(self, name): return name.lower().replace(' ','_') From 1b54ae3a708e10478fea6f1270153df6c093bfa5 Mon Sep 17 00:00:00 2001 From: bohdan-s Date: Tue, 25 Jan 2022 12:48:43 +1100 Subject: [PATCH 3/7] fix MQTT publish error --- SunGather/exports/mqtt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SunGather/exports/mqtt.py b/SunGather/exports/mqtt.py index 18b106a..d756c3d 100644 --- a/SunGather/exports/mqtt.py +++ b/SunGather/exports/mqtt.py @@ -58,9 +58,11 @@ class export_mqtt(object): logging.info(f"MQTT: Server Disconnected code: {rc}") def on_publish(self, client, userdata, mid): - if mid: + try: self.mqtt_queue.remove(mid) - logging.info(f"MQTT: Message {mid} Published") + except Exception as err: + pass + logging.info(f"MQTT: Message {mid} Published") def cleanName(self, name): return name.lower().replace(' ','_') From 6d75c06e0ccf5cf42bd7ef0527247d501aa63bca Mon Sep 17 00:00:00 2001 From: bohdan-s Date: Tue, 25 Jan 2022 20:39:59 +1100 Subject: [PATCH 4/7] v0.3.1: Fix logging setting, force disconnect on failure --- SunGather/config-example.yaml | 3 +- SunGather/sungather.py | 55 +++++++++++++++++++++++------------ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/SunGather/config-example.yaml b/SunGather/config-example.yaml index d844d22..1a6df9c 100644 --- a/SunGather/config-example.yaml +++ b/SunGather/config-example.yaml @@ -1,8 +1,9 @@ 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 diff --git a/SunGather/sungather.py b/SunGather/sungather.py index 7a02fa9..39ddfe9 100644 --- a/SunGather/sungather.py +++ b/SunGather/sungather.py @@ -45,25 +45,41 @@ class SungrowInverter(): self.latest_scrape = {} - def connect(self, connection="modbus"): - if connection == "http": + def connect(self): + if self.client: + if(self.client.connect()): + return True + else: + self.disconnect() + + if self.config_inverter['connection'] == "http": self.client_config['port'] = '8082' self.client = SungrowModbusWebClient.SungrowModbusWebClient(**self.client_config) - elif connection == "sungrow": + elif self.config_inverter['connection'] == "sungrow": self.client = SungrowModbusTcpClient.SungrowModbusTcpClient(**self.client_config) - elif connection == "modbus": + elif self.config_inverter['connection'] == "modbus": self.client = ModbusTcpClient(**self.client_config) else: - logging.warning(f'Inverter: Unknown connection type {connection}, Valid options are http, sungrow or modbus') + logging.warning(f"Inverter: Unknown connection type {self.config_inverter['connection']}, Valid options are http, sungrow or modbus") return False logging.info("Connection: " + str(self.client)) retval = self.client.connect() - if not connection == "http": + # Wait 3 seconds, fixes timing issues + time.sleep(3) + if not self.config_inverter['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'): @@ -254,7 +270,7 @@ class SungrowInverter(): def scrape(self): scrape_start = datetime.now() - self.client.connect() + 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") @@ -272,11 +288,13 @@ class SungrowInverter(): 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.client.close() + self.close() # Create a registers for Power imported and exported to/from Grid if self.inverter_config['level'] >= 1: @@ -423,8 +441,9 @@ def main(): config_inverter = { "host": configfile['inverter'].get('host',None), "port": configfile['inverter'].get('port',502), - "slave": configfile['inverter'].get('slave',0x01), "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), @@ -435,8 +454,11 @@ def main(): "level": configfile['inverter'].get('level',1) } - logging.getLogger().setLevel(configfile['inverter'].get('logging')) - logging.debug(f'Inverter Config Loaded: {config_inverter}') + if 'loglevel' in locals(): + logging.getLogger().setLevel(loglevel) + else: + logging.getLogger().setLevel(config_inverter['logging']) + logging.debug(f'Inverter Config Loaded: {config_inverter}') if config_inverter.get('host'): inverter = SungrowInverter(config_inverter) @@ -444,7 +466,7 @@ def main(): logging.error(f"Error: host option in config is required") sys.exit("Error: host option in config is required") - if not inverter.connect(config_inverter.get('connection')): + 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')}") @@ -470,9 +492,7 @@ def main(): while True: loop_start = datetime.now() - if not inverter.client: - inverter.connect() - time.sleep(3) # Wait 3 secs for connection, resolves issues with modbus not reconnecting on failure + inverter.connect() # Scrape the inverter success = inverter.scrape() @@ -481,8 +501,7 @@ def main(): for export in exports: export.publish(inverter) else: - inverter.client.close() - inverter.client = None + inverter.disconnect() logging.warning(f"Data collection failed, skipped exporting data. Retying in {scan_interval} secs") loop_end = datetime.now() From 3acf9fc9681ed56caac12c6ba914c0173fe5d400 Mon Sep 17 00:00:00 2001 From: bohdan-s Date: Tue, 25 Jan 2022 20:44:34 +1100 Subject: [PATCH 5/7] fix type --- SunGather/sungather.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SunGather/sungather.py b/SunGather/sungather.py index 39ddfe9..5eec2fa 100644 --- a/SunGather/sungather.py +++ b/SunGather/sungather.py @@ -52,22 +52,22 @@ class SungrowInverter(): else: self.disconnect() - if self.config_inverter['connection'] == "http": + if self.inverter_config['connection'] == "http": self.client_config['port'] = '8082' self.client = SungrowModbusWebClient.SungrowModbusWebClient(**self.client_config) - elif self.config_inverter['connection'] == "sungrow": + elif self.config_inverter_configinverter['connection'] == "sungrow": self.client = SungrowModbusTcpClient.SungrowModbusTcpClient(**self.client_config) - elif self.config_inverter['connection'] == "modbus": + elif self.inverter_config['connection'] == "modbus": self.client = ModbusTcpClient(**self.client_config) else: - logging.warning(f"Inverter: Unknown connection type {self.config_inverter['connection']}, Valid options are http, sungrow or modbus") + 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.config_inverter['connection'] == "http": + if not self.inverter_config['connection'] == "http": self.client.close() return retval From f168f7ce9e3200cab5c1892e2a2bf97823538b8f Mon Sep 17 00:00:00 2001 From: bohdan-s Date: Tue, 25 Jan 2022 20:46:21 +1100 Subject: [PATCH 6/7] fix another typo --- SunGather/sungather.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SunGather/sungather.py b/SunGather/sungather.py index 5eec2fa..c9e9ae8 100644 --- a/SunGather/sungather.py +++ b/SunGather/sungather.py @@ -33,7 +33,8 @@ class SungrowInverter(): "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') + "smart_meter": config_inverter.get('smart_meter'), + "connection": config_inverter.get('connection') } self.client = None From f11ee5fe75be6aa1bc8046901443122e4116b702 Mon Sep 17 00:00:00 2001 From: bohdan-s Date: Tue, 25 Jan 2022 20:48:46 +1100 Subject: [PATCH 7/7] bump version --- SunGather/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SunGather/version.py b/SunGather/version.py index 290d7c6..9c5adf7 100644 --- a/SunGather/version.py +++ b/SunGather/version.py @@ -1 +1 @@ -__version__ = '0.3.0' \ No newline at end of file +__version__ = '0.3.1' \ No newline at end of file