mirror of
https://github.com/alexhopeoconnor/SunGather.git
synced 2026-10-23 11:02:10 +10:00
Update Readme to be clearer about HA
This commit is contained in:
@@ -30,7 +30,7 @@ Access ModBus data from almost any network connected Sungow Inverter.
|
|||||||
|
|
||||||
On first connection the tool will query your inverter, retrieve the model and return the correct registers for your device. No more searching registers or creating model files.
|
On first connection the tool will query your inverter, retrieve the model and return the correct registers for your device. No more searching registers or creating model files.
|
||||||
|
|
||||||
Register information based on official documentation: <a href="https://github.com/bohdan-s/Sungrow-Inverter/blob/main/Modbus%20Information/Communication%20Protocol%20of%20PV%20Grid-Connected%20String%20Inverters_V1.1.37_EN.pdf">Communication Protocol of PV Grid-Connected String Inverters</a> and <a hreh="https://github.com/bohdan-s/Sungrow-Inverter/blob/main/Modbus%20Information/communication-protocol-of-residential-hybrid-inverterv1.0.20-1.pdf">Communication Protocol of Residential Hybrid Inverters</a>
|
Register information based on official documentation: <a href="https://github.com/bohdan-s/Sungrow-Inverter/blob/main/Modbus%20Information/Communication%20Protocol%20of%20PV%20Grid-Connected%20String%20Inverters_V1.1.37_EN.pdf">Communication Protocol of PV Grid-Connected String Inverters</a> and <a href="https://github.com/bohdan-s/Sungrow-Inverter/blob/main/Modbus%20Information/communication-protocol-of-residential-hybrid-inverterv1.0.20-1.pdf">Communication Protocol of Residential Hybrid Inverters</a>
|
||||||
|
|
||||||
Has multiple export locations out of the box:
|
Has multiple export locations out of the box:
|
||||||
* Console - Log directly to screen
|
* Console - Log directly to screen
|
||||||
@@ -219,6 +219,8 @@ sensor.inverter_meter_power
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The Inverter reports Power (W), but HA needs Energy (Wh).
|
||||||
|
You can use a "Riemann sum integral" (called integration) to convert it.
|
||||||
Put the following into your sensors.yaml
|
Put the following into your sensors.yaml
|
||||||
```
|
```
|
||||||
sensor:
|
sensor:
|
||||||
|
|||||||
@@ -143,73 +143,72 @@ class export_pvoutput(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def publish(self, inverter):
|
def publish(self, inverter):
|
||||||
self.collect_data(inverter)
|
if self.collect_data(inverter):
|
||||||
|
# Process data points every status_interval
|
||||||
# Process data points every status_interval
|
if((time.time() - self.last_publish) >= (self.status_interval * 60)):
|
||||||
if((time.time() - self.last_publish) >= (self.status_interval * 60)):
|
any_data = False
|
||||||
any_data = False
|
if inverter.validateLatestScrape('timestamp'):
|
||||||
if inverter.validateLatestScrape('timestamp'):
|
now = datetime.datetime.strptime(inverter.getRegisterValue('timestamp'), "%Y-%m-%d %H:%M:%S")
|
||||||
now = datetime.datetime.strptime(inverter.getRegisterValue('timestamp'), "%Y-%m-%d %H:%M:%S")
|
data_point = str(now.strftime("%Y%m%d")) + "," + str(now.strftime("%H:%M"))
|
||||||
data_point = str(now.strftime("%Y%m%d")) + "," + str(now.strftime("%H:%M"))
|
for x in range(1, 13):
|
||||||
for x in range(1, 13):
|
field = 'v' + str(x)
|
||||||
field = 'v' + str(x)
|
if self.collected_data.get(field):
|
||||||
if self.collected_data.get(field):
|
if x == 6 or x == 7: # Round to 1 decimal place
|
||||||
if x == 6 or x == 7: # Round to 1 decimal place
|
value = round(self.collected_data[field] / self.collected_data['count'], 1)
|
||||||
value = round(self.collected_data[field] / self.collected_data['count'], 1)
|
else: # Getting errors when uploading decimals for power/energy so return INT
|
||||||
else: # Getting errors when uploading decimals for power/energy so return INT
|
value = int((self.collected_data[field] / self.collected_data['count']))
|
||||||
value = int((self.collected_data[field] / self.collected_data['count']))
|
data_point = data_point + "," + str(value)
|
||||||
data_point = data_point + "," + str(value)
|
any_data = True
|
||||||
any_data = True
|
|
||||||
else:
|
|
||||||
data_point = data_point + ","
|
|
||||||
self.collected_data = {}
|
|
||||||
|
|
||||||
if any_data:
|
|
||||||
self.batch_data.append(data_point)
|
|
||||||
else:
|
|
||||||
logging.warning(f"PVOutput: No data collected in last {(self.status_interval * 60)} minutes")
|
|
||||||
|
|
||||||
# Max upload is 30, if over 30 then remove the oldest one
|
|
||||||
if self.batch_data.__len__() > 30:
|
|
||||||
logging.warning(f"PVOutput: Over 30 data points scheduled to upload. max is 30 so removing oldest data point")
|
|
||||||
self.batch_data.pop(0)
|
|
||||||
|
|
||||||
self.batch_count +=1
|
|
||||||
if self.batch_count >= self.pvoutput_config['batch_points']:
|
|
||||||
if not self.batch_data.__len__() > 0:
|
|
||||||
logging.warning(f"PVOutput: No data collected in last {((self.status_interval * 60) * self.batch_count)} minutes, Skipping upload")
|
|
||||||
return False
|
|
||||||
elif self.batch_data.__len__() >= 1:
|
|
||||||
payload_data = None
|
|
||||||
for data in self.batch_data:
|
|
||||||
if payload_data:
|
|
||||||
payload_data = payload_data + ";" + data
|
|
||||||
else:
|
else:
|
||||||
payload_data = data
|
data_point = data_point + ","
|
||||||
|
self.collected_data = {}
|
||||||
|
|
||||||
payload = {}
|
if any_data:
|
||||||
payload['data'] = payload_data
|
self.batch_data.append(data_point)
|
||||||
|
else:
|
||||||
|
logging.warning(f"PVOutput: No data collected in last {(self.status_interval * 60)} minutes")
|
||||||
|
|
||||||
if self.pvoutput_config['cumulative_flag'] > 0:
|
# Max upload is 30, if over 30 then remove the oldest one
|
||||||
payload['c1'] = self.pvoutput_config['cumulative_flag']
|
if self.batch_data.__len__() > 30:
|
||||||
|
logging.warning(f"PVOutput: Over 30 data points scheduled to upload. max is 30 so removing oldest data point")
|
||||||
|
self.batch_data.pop(0)
|
||||||
|
|
||||||
try:
|
self.batch_count +=1
|
||||||
logging.debug("PVOutput: Request; " + self.url_addbatchstatus + ", " + str(self.headers) + " : " + str(payload))
|
if self.batch_count >= self.pvoutput_config['batch_points']:
|
||||||
response = requests.post(url=self.url_addbatchstatus, headers=self.headers, params=payload, timeout=3)
|
if not self.batch_data.__len__() > 0:
|
||||||
self.batch_count = 0
|
logging.warning(f"PVOutput: No data collected in last {((self.status_interval * 60) * self.batch_count)} minutes, Skipping upload")
|
||||||
|
return False
|
||||||
|
elif self.batch_data.__len__() >= 1:
|
||||||
|
payload_data = None
|
||||||
|
for data in self.batch_data:
|
||||||
|
if payload_data:
|
||||||
|
payload_data = payload_data + ";" + data
|
||||||
|
else:
|
||||||
|
payload_data = data
|
||||||
|
|
||||||
if response.status_code != requests.codes.ok:
|
payload = {}
|
||||||
logging.error(f"PVOutput: Upload Failed; {str(response.status_code)} Message; {str(response.text)}")
|
payload['data'] = payload_data
|
||||||
else:
|
|
||||||
self.batch_data = []
|
if self.pvoutput_config['cumulative_flag'] > 0:
|
||||||
self.last_publish = time.time()
|
payload['c1'] = self.pvoutput_config['cumulative_flag']
|
||||||
logging.info("PVOutput: Data uploaded")
|
|
||||||
except Exception as err:
|
try:
|
||||||
logging.error(f"PVOutput: Failed to Upload")
|
logging.debug("PVOutput: Request; " + self.url_addbatchstatus + ", " + str(self.headers) + " : " + str(payload))
|
||||||
logging.debug(f"{err}")
|
response = requests.post(url=self.url_addbatchstatus, headers=self.headers, params=payload, timeout=3)
|
||||||
|
self.batch_count = 0
|
||||||
|
|
||||||
|
if response.status_code != requests.codes.ok:
|
||||||
|
logging.error(f"PVOutput: Upload Failed; {str(response.status_code)} Message; {str(response.text)}")
|
||||||
|
else:
|
||||||
|
self.batch_data = []
|
||||||
|
self.last_publish = time.time()
|
||||||
|
logging.info("PVOutput: Data uploaded")
|
||||||
|
except Exception as err:
|
||||||
|
logging.error(f"PVOutput: Failed to Upload")
|
||||||
|
logging.debug(f"{err}")
|
||||||
|
else:
|
||||||
|
logging.info("PVOutput: Data added to next batch upload")
|
||||||
else:
|
else:
|
||||||
logging.info("PVOutput: Data added to next batch upload")
|
logging.info(f"PVOutput: Data logged, next upload in {int(((self.status_interval) * 60) - (time.time() - self.last_publish))} secs")
|
||||||
else:
|
|
||||||
logging.info(f"PVOutput: Data logged, next upload in {int(((self.status_interval) * 60) - (time.time() - self.last_publish))} secs")
|
|
||||||
|
|
||||||
self.last_run = time.time()
|
self.last_run = time.time()
|
||||||
@@ -432,7 +432,7 @@ registers:
|
|||||||
level: 2
|
level: 2
|
||||||
address: 5077
|
address: 5077
|
||||||
datatype: "U32"
|
datatype: "U32"
|
||||||
unit: "w"
|
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"]
|
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"
|
- name: "reactive_power_regulation_setpoint"
|
||||||
level: 2
|
level: 2
|
||||||
@@ -480,32 +480,32 @@ registers:
|
|||||||
datatype: "S32"
|
datatype: "S32"
|
||||||
indicator: True
|
indicator: True
|
||||||
smart_meter: True
|
smart_meter: True
|
||||||
unit: "w"
|
unit: "W"
|
||||||
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
|
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
|
||||||
- name: "meter_a_phase_power"
|
- name: "meter_a_phase_power"
|
||||||
level: 2
|
level: 2
|
||||||
address: 5085
|
address: 5085
|
||||||
datatype: "S32"
|
datatype: "S32"
|
||||||
unit: "w"
|
unit: "W"
|
||||||
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
|
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
|
||||||
- name: "meter_b_phase_power"
|
- name: "meter_b_phase_power"
|
||||||
level: 2
|
level: 2
|
||||||
address: 5087
|
address: 5087
|
||||||
datatype: "S32"
|
datatype: "S32"
|
||||||
unit: "w"
|
unit: "W"
|
||||||
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
|
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
|
||||||
- name: "meter_c_phase_power"
|
- name: "meter_c_phase_power"
|
||||||
level: 2
|
level: 2
|
||||||
address: 5089
|
address: 5089
|
||||||
datatype: "S32"
|
datatype: "S32"
|
||||||
unit: "w"
|
unit: "W"
|
||||||
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
|
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
|
||||||
- name: "load_power"
|
- name: "load_power"
|
||||||
level: 1
|
level: 1
|
||||||
address: 5091
|
address: 5091
|
||||||
datatype: "S32"
|
datatype: "S32"
|
||||||
smart_meter: True
|
smart_meter: True
|
||||||
unit: "w"
|
unit: "W"
|
||||||
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
|
models: ["SG5KTL-MT","SG6KTL-MT","SG8KTL-M","SG10KTL-M","SG10KTL-MT","SG12KTL-M","SG15KTL-M","SG17KTL-M","SG20KTL-M"]
|
||||||
- name: "daily_export_energy"
|
- name: "daily_export_energy"
|
||||||
level: 1
|
level: 1
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class SungrowInverter():
|
|||||||
|
|
||||||
self.registers = [[]]
|
self.registers = [[]]
|
||||||
self.registers.pop() # Remove null value from list
|
self.registers.pop() # Remove null value from list
|
||||||
self.registers_custom = [{'name': 'export_to_grid', 'unit': 'w'}, {'name': 'import_from_grid', 'unit': 'w'}, {'name': 'run_state'}]
|
self.registers_custom = [{'name': 'export_to_grid', 'unit': 'W'}, {'name': 'import_from_grid', 'unit': 'W'}, {'name': 'run_state'}]
|
||||||
self.register_ranges = [[]]
|
self.register_ranges = [[]]
|
||||||
self.register_ranges.pop() # Remove null value from list
|
self.register_ranges.pop() # Remove null value from list
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user