From cf6d3200be61353e97cab0ad7581c77d94850a16 Mon Sep 17 00:00:00 2001 From: Julian Edwards Date: Mon, 23 Dec 2024 10:46:07 +1000 Subject: [PATCH] Make pvoutput reports align with status interval wall clock time The previous code had a problem where it would do an intitial report on start up and then add status_interval minutes to the next report time *after* it had finished processing. This causes 2 problems: 1. Time drift - the report time interval is longer than the actual status_interval so over the course of the day ends up being later and later which can lead to a gap in the pvoutput data. 2. The report time is unpredictable, which will cause clashes with other systems that report consumption data each time the report time drifts enough to clash with the other system's upload with the end result that one cancels out the other. To fix #2, the second system just needs to report the same report timestamp 30 seconds later than the first. The fix here aligns the report time to a rounded fraction of the wall clock time so that it matches the wall clock time on pvoutput's list of reports on the UI. Typically if you report every 5 minutes, it will now send a report to pvoutput at :00, :05, :10, etc. instead of the previous non-rounded times that drifted forward. --- SunGather/exports/pvoutput.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/SunGather/exports/pvoutput.py b/SunGather/exports/pvoutput.py index 2bd53b2..64cd7c2 100644 --- a/SunGather/exports/pvoutput.py +++ b/SunGather/exports/pvoutput.py @@ -64,7 +64,6 @@ def configure(self, config, inverter): 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']): @@ -110,6 +109,7 @@ def configure(self, config, inverter): pass logging.info(f"PVOutput: Configured export to {invertername} every {self.status_interval} minutes") + self.next_publish = datetime.datetime.now() return True def collect_data(self, inverter): @@ -152,7 +152,7 @@ def collect_data(self, inverter): 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)): + if self.next_publish <= datetime.datetime.now(): any_data = False if inverter.validateLatestScrape('timestamp'): now = datetime.datetime.strptime(inverter.getRegisterValue('timestamp'), "%Y-%m-%d %H:%M:%S") @@ -213,7 +213,7 @@ def publish(self, inverter): logging.error("PVOutput: Request; " + self.url_addbatchstatus + ", " + str(self.headers) + " : " + str(payload)) else: self.batch_data = [] - self.last_publish = time.time() + self.next_publish = self.get_next_target_time(self.status_interval) logging.info("PVOutput: Data uploaded") except Exception as err: logging.error(f"PVOutput: Failed to Upload") @@ -221,6 +221,25 @@ def publish(self, inverter): 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 + next_upload_delta = self.next_publish - datetime.datetime.now() + logging.info("PVOutput: Data logged, next upload in %s secs", int(next_upload_delta.total_seconds())) + + self.last_run = time.time() + + def get_next_target_time(self, interval, delay=0): + """Calculate the next time that we should send an update to pvoutput. + + :param interval: (int) The time, in minutes, between updates. Must match + the setting on pvoutput for the system to which you're posting data. + :param delay: (int) The desired post-target delay, in seconds (for avoiding + updates at the same time as other systems that report data). + + :return: A datetime. + """ + now = datetime.datetime.now() + wait_minutes = interval - (now.minute % interval) - 1 + wait_seconds = 60 - now.second + target_delta = datetime.timedelta(minutes=wait_minutes, seconds=wait_seconds) + target_time = now + target_delta + target_time += datetime.timedelta(seconds=delay) + return target_time