Skip to content

Commit

Permalink
v0.2.0, Improved register detection, improved performance, MQTT and P…
Browse files Browse the repository at this point in the history
…VOutput rewrites
  • Loading branch information
bohdan-s committed Jan 13, 2022
1 parent 378237d commit bb8b224
Show file tree
Hide file tree
Showing 9 changed files with 358 additions and 298 deletions.
146 changes: 82 additions & 64 deletions SunGather/config-example.yaml
Original file line number Diff line number Diff line change
@@ -1,41 +1,43 @@
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
# slave: 0x01 # [Optional] Default is 0x01
# timeout: 10 # [Optional] Default is 10
# scan_interval: 30 # [Optional] Default is 30
connection: sungrow # [Required] options: modbus, sungrow, http
# model: "SG7.0RT" # [Optional] This is autodetected on startup, only needed if detection issues or for testing
# See model list here: https://github.com/bohdan-s/SunGather#supported
# smart_meter: True # [Optional] Default is False, Set to true if inverter supports reading grind / house consumption
# use_local_time: False # [Optional] Default False, Uses Inventer time, if try uses PC time when updating timestamps (e.g. PVOutput)
# hybrid: False # [Optional] Default false, if you have a Hybrid (battery) set to True
# manual_load: False # [Optional] Manually calculate load total_active_power + meter_power if the inverter does not supply it
# 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,
# 1 (default) = Useful data, all required for exports,
# 2 everything your Inverter supports,
# 3 Everything from every register
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
# slave: 0x01 # [Optional] Default is 0x01
# timeout: 10 # [Optional] Default is 10
# scan_interval: 30 # [Optional] Default is 30
connection: sungrow # [Required] options: modbus, sungrow, http
# model: "SG7.0RT" # [Optional] This is autodetected on startup, only needed if detection issues or for testing
# See model list here: https://github.com/bohdan-s/SunGather#supported
# smart_meter: True # [Optional] Default is False, Set to true if inverter supports reading grind / house consumption
# use_local_time: False # [Optional] Default False, Uses Inventer time, if try uses PC time when updating timestamps (e.g. PVOutput)
# manual_load: False # [Optional] Manually calculate load total_active_power + meter_power if the inverter does not supply it
# 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,
# 1 (default) = Useful data, all required for exports,
# 2 everything your Inverter supports,
# 3 Everything from every register

# If you do not want to use a export, you can either remove the whole configuration block
# or set enabled: False
exports:
# Print Registers to console, good for debugging / troubleshooting
- name: console
enabled: False
- name: console
enabled: False # [Optional] Default is False

# Runs a simple Webserver showing Config and last read registers
- name: http
enabled: True # Access at http://localhost:8080 or http://[serverip]:8080
# port: 8080 # Default port is 8080
# Access at http://localhost:8080 or http://[serverip]:8080
- name: webserver
enabled: True # [Optional] Default is False
# port: 8080 # [Optional] Default is 8080

# Output data to InfluxDB
- name: influxdb
enabled: False
token: "xxx" # [Required] API Token
url: "http://localhost:8086" # [Optional] Default URL is http://localhost:8086
org: "Default" # [Required] InfluxDB Organization
bucket: "SunGather" # [Required] InfluxDB Bucket
measurements:
enabled: False # [Optional] Default is False
token: "xxx" # [Required] API Token
url: "http://localhost:8086" # [Optional] Default URL is http://localhost:8086
org: "Default" # [Required] InfluxDB Organization
bucket: "SunGather" # [Required] InfluxDB Bucket
measurements: # [Required] Registers to publish to bucket
- point: "power"
register: daily_power_yields
- point: "power"
Expand All @@ -51,88 +53,104 @@ exports:
- point: "temp"
register: internal_temperature


# Publish Registers to MQTT / Home Assistant
- name: mqtt
host: 192.168.1.200
# port: 1883
topic: "tele/inverter_{model}/SENSOR" # Variable {model} will be replaced with model number
# username:
# password:
ha_discovery: True # Home Assistant Discovery, False by default
ha_topics: # Name in HA, from Register
enabled: False # [Optional] Default is False
host: 192.168.1.200 # [Required] IP or Hostname of MQTT Server
# port: 1883 # [Optional] Default 1883
# topic: "tele/inverter_{model}/SENSOR" # [Optional] Default: "tele/inverter_{model}/SENSOR", Variable {model} will be replaced with model number of inverter
# username: # [Optional] Username is MQTT server requires it
# password: # [Optional] Password is MQTT server requires it
ha_discovery: True # [Optional] Default False, Home Assistant Discovery
ha_topics: # [Optional] / [Required] ha_discovery: True, Topics to enable discovery for HA
- 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"
sensor_type: binary_sensor
register: start_stop
dev_class: running
payload_on: "Start"
payload_off: "Stop"

- name: pvoutput # Publish Registers to PVOutput
api: "xxxxx"
sid: "xxxxx"
# 60 for regular accounts, 300 for donation accounts
rate_limit: 60
enabled: True
parameters:
- name: v1 # Energy Generation
register: daily_power_yields # Solar Generated Today (Energy)
# Publish Registers to PVOutput
- name: pvoutput
enabled: False # [Optional] Default is False
api: "xxxxx" # [Required] API Key, Settings > API Key
sid: "xxxxx" # [Optional] System ID, Settings > Registered Systems > System ID
rate_limit: 60 # [Optional] Default 60, 60 for regular accounts, 300 for donation accounts
status_interval: 5 # [Optional] Default 5, In minutes, options are 5 ,10,15 minute intervals: https://pvoutput.org/help/live_data.html
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
- name: v1 # Energy Generation
register: daily_power_yields # Solar Generated Today (Energy)
multiple: 1000
- name: v2 # Power Generation
register: total_active_power # Current Generation (Power)
# - name: v3 # Energy Consumption
- name: v2 # Power Generation
register: total_active_power # Current Generation (Power)
# - name: v3 # Energy Consumption
# register:
- name: v4 # Power Consumption
register: load_power # Current Home usage (Power)
- name: v5 # Temperature
register: internal_temperature # Inverter internal temperature
# - name: v6 # Voltage
- name: v4 # Power Consumption
register: load_power # Current Home usage (Power)
- name: v5 # Temperature
register: internal_temperature # Inverter internal temperature
# - name: v6 # Voltage
# register: total_active_power
- name: c1 # Cumulative Flag,
value: 1 # If using v2/v4 set to 1
# - name: "n" # Net Flag
- name: c1 # Cumulative Flag,
value: 1 # If using v2/v4 set to 1
# - name: "n" # Net Flag
# value: 1
# - name: "v7" # Extended Value v7 - Donation Only
# - name: "v7" # Extended Value v7 - Donation Only
# register:
# - name: "v8" # Extended Value v8 - Donation Only
# - name: "v8" # Extended Value v8 - Donation Only
# register:
# - name: "v9" # Extended Value v9 - Donation Only
# - name: "v9" # Extended Value v9 - Donation Only
# register:
# - name: "v10" # Extended Value v10 - Donation Only
# - name: "v10" # Extended Value v10 - Donation Only
# register:
# - name: "v11" # Extended Value v11 - Donation Only
# - name: "v11" # Extended Value v11 - Donation Only
# register:
# - name: "v12" # Extended Value v12 - Donation Only
# - name: "v12" # Extended Value v12 - Donation Only
# register:
# - name: "m1" # Text Message 1 - Donation Only
# - name: "m1" # Text Message 1 - Donation Only
# register:

7 changes: 5 additions & 2 deletions SunGather/exports/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ class export_console(object):
def __init__(self):
return

# Configure MQTT
# Configure Console
def configure(self, config, config_inverter):
logging.info("Configured Console Logging")
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 publish(self, inverter):

Expand Down
11 changes: 5 additions & 6 deletions SunGather/exports/influxdb.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from typing import Sequence
import influxdb_client
import logging
from influxdb_client.client.write_api import SYNCHRONOUS
Expand All @@ -12,7 +11,7 @@ def __init__(self):

# 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'):
logging.warning(f"InfluxDB: Please check configuration")
return False
Expand All @@ -27,16 +26,16 @@ def configure(self, config, config_inverter):
self.measurements.append(measurement)

self.write_api = self.client.write_api(write_options=SYNCHRONOUS)
logging.info(f"Configured InfluxDB Client: {self.client.url}")
logging.info(f"InfluxDB: Configured: {self.client.url}")

def publish(self, inverter):
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.info(f'InfluxDB Write: {sequence}')
logging.debug(f'InfluxDB: Sequence; {sequence}')
try:
self.write_api.write(self.bucket, self.client.org, sequence)
except Exception as err:
logging.error(err)
logging.error("InfluxDB: " + str(err))

logging.info("Published to InfluxDB")
logging.info("InfluxDB: Published")
78 changes: 52 additions & 26 deletions SunGather/exports/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ class export_mqtt(object):
def __init__(self):
self.mqtt_client = None
self.sensor_topic = None
self.ha_discovery = False
self.ha_topics = []
self.update_model = True
self.homeassistant = False
self.ha_discovery = True
self.ha_sensors = []
self.model = None
self.model_clean = None
self.inverter_ip = None

# Configure MQTT
def configure(self, config, config_inverter):
Expand All @@ -22,43 +25,66 @@ def configure(self, config, config_inverter):

self.mqtt_client.connect(config.get('host'), port=config.get('port', 1883))

self.sensor_topic = config.get('topic', 'tele/inverter_{model}/SENSOR')
self.ha_discovery = config.get('ha_discovery', False)
self.inverter_ip = config_inverter.get('host')

if self.ha_discovery:
for ha_topic in config.get('ha_topics'):
self.ha_topics.append(ha_topic)
self.sensor_topic = config.get('topic', 'inverter/{model}/registers')
self.homeassistant = config.get('homeassistant', False)

logging.info(f"Configured MQTT Client: {config.get('host')}:{config.get('port', 1883)}")
if self.homeassistant:
for ha_sensor in config.get('ha_sensors'):
self.ha_sensors.append(ha_sensor)

logging.info(f"MQTT: Configured {config.get('host')}:{config.get('port', 1883)}")

def publish(self, inverter):
global mqtt_client

if (self.update_model):
self.sensor_topic = self.sensor_topic.replace('{model}', inverter.get('device_type_code', 'unknown').replace('.','').replace('-',''))
self.update_model = False
# After a while you'll need to reconnect, so just reconnect before each publish
self.mqtt_client.reconnect()

if self.ha_discovery:
self.mqtt_client.reconnect()
logging.info("Publishing Home Assistant Discovery messages")
discovery_topic = 'homeassistant/sensor/inverter/{}/config'
discovery_payload = '{{"name":"Inverter {}", "uniq_id":"{}", "stat_t":"{}", "json_attr_t":"{}", "unit_of_meas":"{}", "dev_cla":"{}", "state_class":"{}", "val_tpl":"{{{{ value_json.{} }}}}", "ic":"mdi:solar-power", "device":{{ "name":"Solar Inverter", "mf":"Sungrow", "mdl":"{}", "connections":[["address", "' + self.mqtt_client._host + '" ]]}}}}'
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)

for ha_topic in self.ha_topics:
msg = discovery_payload.format( ha_topic.get('name'), "inverter_" + ha_topic.get('name').lower().replace(' ','_'), self.sensor_topic, self.sensor_topic, ha_topic.get('unit'), ha_topic.get('dev_class'), ha_topic.get('state_class'), ha_topic.get('register'), inverter.get('device_type_code', 'unknown').replace('.',''))
result = self.mqtt_client.publish(discovery_topic.format(ha_topic.get('name').lower().replace(' ','_')), msg, retain=True)
result.wait_for_publish()
self.ha_discovery = False
logging.debug(f'MQTT: Sensor Topic = {self.sensor_topic}')

# After a while you'll need to reconnect, so just reconnect before each publish
self.mqtt_client.reconnect()
logging.debug(f"MQTT: Publishing: {self.sensor_topic} : {json.dumps(inverter)}")
result = self.mqtt_client.publish(self.sensor_topic, json.dumps(inverter).replace('"', '\"'))
result.wait_for_publish()

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 ]]}

result = self.mqtt_client.publish(ha_topic, json.dumps(config_msg), retain=True)
result.wait_for_publish()
logging.info("MQTT: Published Home Assistant Discovery messages")
self.ha_discovery = False

if result.rc != mqtt.MQTT_ERR_SUCCESS:
# See https://github.com/eclipse/paho.mqtt.python/blob/master/src/paho/mqtt/client.py#L149 for error code mapping
logging.error(f"Failed to publish to MQTT with error code: {result.rc}")
logging.error(f"MQTT: Failed to publish with error code: {result.rc}")
else:
logging.info("Published to MQTT")
logging.info("MQTT: Published")

return result
Loading

0 comments on commit bb8b224

Please sign in to comment.