diff --git a/SunGather/exports/hassio.py b/SunGather/exports/hassio.py new file mode 100644 index 0000000..a184956 --- /dev/null +++ b/SunGather/exports/hassio.py @@ -0,0 +1,200 @@ +import logging +import requests +import datetime +import time + +class export_hassio(object): + def __init__(self): + self.api_base = "http://supervisor/core/api" + 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.tid = '1618' + self.status_interval = 5 + + @property + def headers(self): + return { + "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, 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.batch_data = [] + self.batch_count = 0 + self.last_run = 0 + self.last_publish = 0 + + for parameter in config.get('parameters'): + 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) + logging.debug(f"PVOutput: Response; {str(response.status_code)} Message; {str(response.content)}") + + if response.status_code == 200: + system = response.text.split(';')[0] + teams = response.text.split(';')[2] + + invertername = system.split(',')[0] + self.status_interval = int(system.split(',')[15]) + + team_member = False + 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)}") + + except Exception as err: + logging.error(f"PVOutput: Failed to configure") + logging.debug(f"{err}") + return False + + try: + 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 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: + pass + + logging.info(f"PVOutput: Configured export to {invertername} every {self.status_interval} minutes") + return True + + def collect_data(self, inverter): + # Check all required registers have been returned by the inverter + 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.pvoutput_parameters: + value = inverter.getRegisterValue(parameter.get('register')) + + if parameter.get('multiple'): + value = value * parameter.get('multiple') + + # If using Cumulative Energy we just need the last data point, not the average + if parameter.get('name') == 'v1' and (self.pvoutput_config['cumulative_flag'] == 1 or self.pvoutput_config['cumulative_flag'] == 2): + self.collected_data[parameter.get('name')] = value + elif parameter.get('name') == 'v3' and (self.pvoutput_config['cumulative_flag'] == 1 or self.pvoutput_config['cumulative_flag'] == 3): + self.collected_data[parameter.get('name')] = value + # Add the last data point to the previous data point if exists, otherwise set as the last data point + elif 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 + else: + self.collected_data['count'] = 1 + + logging.debug(f'PVOutput: Data Logged: {self.collected_data}') + + return True + + def publish(self, inverter): + if self.collect_data(inverter): + # Process data points every status_interval + if((time.time() - self.last_publish) >= (self.status_interval * 60)): + 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 == 1 and (self.pvoutput_config['cumulative_flag'] == 1 or self.pvoutput_config['cumulative_flag'] == 2): + value = int(self.collected_data[field]) + elif x == 3 and (self.pvoutput_config['cumulative_flag'] == 1 or self.pvoutput_config['cumulative_flag'] == 3): + value = int(self.collected_data[field]) + elif 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 = {} + + 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: + payload_data = data + + 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)) + 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)}") + logging.error("PVOutput: Request; " + self.url_addbatchstatus + ", " + str(self.headers) + " : " + str(payload)) + 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: + 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/registers-sungrow.yaml b/SunGather/registers-sungrow.yaml index fba1b07..e04bc65 100644 --- a/SunGather/registers-sungrow.yaml +++ b/SunGather/registers-sungrow.yaml @@ -23,7 +23,7 @@ registers: datatype: "U16" models: ["SH5K-20","SH3K6","SH4K6","SH5K-V13","SH5K-30","SH3K6-30","SH4K6-30","SH5.0RS","SH3.6RS","SH4.6RS","SH6.0RS","SH10RT","SH10RT-V112","SH8.0RT","SH6.0RT","SH5.0RT"] - name: "serial_number" - level: 3 + level: 1 address: 4990 datatype: "UTF-8" - name: "device_type_code" diff --git a/SunGather/sungather.py b/SunGather/sungather.py index 454eea9..5f5ec15 100644 --- a/SunGather/sungather.py +++ b/SunGather/sungather.py @@ -230,6 +230,12 @@ class SungrowInverter(): register_value = (register_value + u32_value * 0x10000 - 0xffffffff -1) else: register_value = register_value + u32_value * 0x10000 + elif register.get('datatype') == "UTF-8": # This seems to be Serial only, 10 bytes + utf_value = register_value.to_bytes(2, 'big') + for x in range(1,5): + utf_value += rr.registers[num+x].to_bytes(2, 'big') + register_value = utf_value.decode() + # We convert a system response to a human value if register.get('datarange'):