Skip to content

Commit 1df60ab

Browse files
Fix fetch crash
Support day/night tariff
1 parent 064dcd5 commit 1df60ab

3 files changed

Lines changed: 128 additions & 9 deletions

File tree

‎apps/predbat/fetch.py‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -758,13 +758,14 @@ def minute_data(
758758
if minute in mdata:
759759
last_sample_minute = minute
760760
break
761-
last_sample_value = mdata[last_sample_minute]
762-
last_but_one_minute_sample = mdata[last_sample_minute + 1] if (last_sample_minute + 1) in mdata else last_sample_value
763-
step = last_sample_value - last_but_one_minute_sample
764-
if last_sample_minute < 10 * 60 and last_sample_minute > 0:
765-
for minute in range(last_sample_minute):
766-
if minute not in mdata:
767-
mdata[minute] = dp4(newest_state + step * (last_sample_minute - minute))
761+
if last_sample_minute > 0:
762+
last_sample_value = mdata[last_sample_minute]
763+
last_but_one_minute_sample = mdata[last_sample_minute + 1] if (last_sample_minute + 1) in mdata else last_sample_value
764+
step = last_sample_value - last_but_one_minute_sample
765+
if last_sample_minute < 10 * 60 and last_sample_minute > 0:
766+
for minute in range(last_sample_minute):
767+
if minute not in mdata:
768+
mdata[minute] = dp4(newest_state + step * (last_sample_minute - minute))
768769

769770
# Fill from last sample until now
770771
for minute in range(60 * 24 * days):

‎apps/predbat/octopus.py‎

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@
2626
DATE_STR_FORMAT = "%Y-%m-%d"
2727
DATE_TIME_STR_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
2828

29+
# Hard-wired smart meter day/night rate times
30+
OCTOPUS_NIGHT_RATE_START_HOUR = 0
31+
OCTOPUS_NIGHT_RATE_START_MINUTE = 30
32+
OCTOPUS_NIGHT_RATE_END_HOUR = 7
33+
OCTOPUS_NIGHT_RATE_END_MINUTE = 30
34+
35+
OCTOPUS_DAY_RATE_START_HOUR = 7
36+
OCTOPUS_DAY_RATE_START_MINUTE = 30
37+
OCTOPUS_DAY_RATE_END_HOUR = 0
38+
OCTOPUS_DAY_RATE_END_MINUTE = 30
2939

3040
def is_active(now_utc, activeFrom, activeTo):
3141
if not activeFrom:
@@ -673,6 +683,50 @@ async def async_get_saving_sessions(self, account_id):
673683
else:
674684
return response_data.get("savingSessions", {})
675685

686+
async def async_get_day_night_rates(self, url):
687+
"""
688+
Get day and night rates from Octopus
689+
"""
690+
mdata = []
691+
self.log("Info: Octopus tariff has day and night rates, fetching both")
692+
url_day = url.replace("standard-unit-rates", "day-unit-rates")
693+
url_night = url.replace("standard-unit-rates", "night-unit-rates")
694+
result_day = await self.fetch_url_cached(url_day)
695+
result_night = await self.fetch_url_cached(url_night)
696+
# is_night_rate = self.__is_between_times(rate, "00:30:00", "07:30:00", True)
697+
# Find the current day rate by scanning all the values looking at valid from date and picking the latest that is before now
698+
current_day_rate = None
699+
current_night_rate = None
700+
for rate in result_day:
701+
valid_from_stamp = rate.get("valid_from", "")
702+
# Convert from string to datetime
703+
valid_from_stamp = datetime.strptime(valid_from_stamp, DATE_TIME_STR_FORMAT)
704+
if valid_from_stamp <= self.now_utc:
705+
current_day_rate = rate.get('value_inc_vat', None)
706+
for rate in result_night:
707+
valid_from_stamp = rate.get("valid_from", "")
708+
# Convert from string to datetime
709+
valid_from_stamp = datetime.strptime(valid_from_stamp, DATE_TIME_STR_FORMAT)
710+
if valid_from_stamp <= self.now_utc:
711+
current_night_rate = rate.get('value_inc_vat', None)
712+
self.log("Info: Current day rate {} night rate {}".format(current_day_rate, current_night_rate))
713+
if current_day_rate is not None and current_night_rate is not None:
714+
# Now create a combined list of rates, start from 2 days back and go forward 3 days with the day and night rates
715+
night_start_time = self.now_utc.replace(hour=OCTOPUS_NIGHT_RATE_START_HOUR, minute=OCTOPUS_NIGHT_RATE_START_MINUTE, second=0, microsecond=0) - timedelta(days=2)
716+
night_end_time = night_start_time.replace(hour=OCTOPUS_NIGHT_RATE_END_HOUR, minute=OCTOPUS_NIGHT_RATE_END_MINUTE)
717+
day_start_time = night_start_time.replace(hour=OCTOPUS_DAY_RATE_START_HOUR, minute=OCTOPUS_DAY_RATE_START_MINUTE)
718+
day_end_time = night_start_time.replace(hour=OCTOPUS_DAY_RATE_END_HOUR, minute=OCTOPUS_DAY_RATE_END_MINUTE) + timedelta(days=1)
719+
for day in range(8):
720+
# Night rate
721+
mdata.append({"valid_from": night_start_time.strftime(DATE_TIME_STR_FORMAT), "valid_to": night_end_time.strftime(DATE_TIME_STR_FORMAT), "value_inc_vat": current_night_rate})
722+
# Day rate
723+
mdata.append({"valid_from": day_start_time.strftime(DATE_TIME_STR_FORMAT), "valid_to": day_end_time.strftime(DATE_TIME_STR_FORMAT), "value_inc_vat": current_day_rate})
724+
night_start_time += timedelta(days=1)
725+
night_end_time += timedelta(days=1)
726+
day_start_time += timedelta(days=1)
727+
day_end_time += timedelta(days=1)
728+
return mdata
729+
676730
async def async_download_octopus_url(self, url):
677731
"""
678732
Download octopus rates directly from a URL
@@ -683,7 +737,7 @@ async def async_download_octopus_url(self, url):
683737
while url and pages < 3:
684738
self.requests_total += 1
685739
r = requests.get(url)
686-
if r.status_code not in [200, 201]:
740+
if r.status_code not in [200, 201, 400]:
687741
self.failures_total += 1
688742
self.log("Warn: Error downloading Octopus data from URL {}, code {}".format(url, r.status_code))
689743
return {}
@@ -694,9 +748,28 @@ async def async_download_octopus_url(self, url):
694748
self.failures_total += 1
695749
self.log("Warn: Error downloading Octopus data from URL {} (JSONDecodeError)".format(url))
696750
return {}
751+
752+
if r.status_code == 400:
753+
detail = data.get("detail", "")
754+
if 'This tariff has day and night rates' in detail:
755+
self.log("Info: Octopus tariff has day and night rates, fetching both")
756+
mdata = await self.async_get_day_night_rates(url)
757+
if mdata:
758+
return mdata
759+
else:
760+
self.failures_total += 1
761+
self.log("Warn: Error downloading Octopus data from URL {} (No Results)".format(url))
762+
return {}
763+
else:
764+
self.failures_total += 1
765+
self.log("Warn: Error downloading Octopus data from URL {} (400) - {}".format(url, detail))
766+
return {}
767+
697768
if "results" in data:
698769
mdata += data["results"]
699770
else:
771+
detail = data.get("detail", "")
772+
700773
self.failures_total += 1
701774
self.log("Warn: Error downloading Octopus data from URL {} (No Results)".format(url))
702775
return {}
@@ -751,7 +824,7 @@ async def fetch_tariffs(self, tariffs):
751824
tariff_type = "gas"
752825
else:
753826
tariff_type = "electricity"
754-
tariffs[tariff]["data"] = await self.fetch_url_cached(f"https://api.octopus.energy/v1/products/{product_code}/{tariff_type}-tariffs/{tariff_code}/standard-unit-rates/")
827+
tariffs[tariff]["data"] = await self.fetch_url_cached(f"https://api.octopus.energy/v1/products/{product_code}/{tariff_type}-tariffs/{tariff_code}/standard-unit-rates/")
755828
tariffs[tariff]["standing"] = await self.fetch_url_cached(f"https://api.octopus.energy/v1/products/{product_code}/{tariff_type}-tariffs/{tariff_code}/standing-charges/")
756829

757830
entity_id = "sensor.predbat_octopus_" + self.account_id.replace("-", "_") + "_" + tariff

‎apps/predbat/unit_test.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import glob
1616
from datetime import datetime, timedelta
1717
import argparse
18+
import asyncio
1819

1920
try:
2021
import requests
@@ -9133,6 +9134,48 @@ def run_test_units(my_predbat):
91339134
return failed
91349135

91359136

9137+
def test_download_octopus_url_wrapper(my_predbat):
9138+
"""
9139+
Wrapper to run the async test function
9140+
"""
9141+
return asyncio.run(test_download_octopus_url(my_predbat))
9142+
9143+
async def test_download_octopus_url(my_predbat):
9144+
"""
9145+
Test the download_octopus_url function
9146+
"""
9147+
print("**** Running download_octopus_url tests ****")
9148+
failed = False
9149+
9150+
# Test URL for VAR-22-11-01 tariff
9151+
test_url = "https://api.octopus.energy/v1/products/VAR-22-11-01/electricity-tariffs/E-2R-VAR-22-11-01-A/standard-unit-rates/"
9152+
9153+
# Test the download function
9154+
api = OctopusAPI("", "", my_predbat)
9155+
api.now_utc = my_predbat.now_utc
9156+
rates_data = await api.async_download_octopus_url(test_url)
9157+
9158+
# Basic validation checks
9159+
if not rates_data:
9160+
print("ERROR: No rate data downloaded from URL {}".format(test_url))
9161+
failed = True
9162+
else:
9163+
print("Successfully downloaded {} rate points from VAR-22-11-01 tariff".format(len(rates_data)))
9164+
pdata = my_predbat.minute_data(rates_data, my_predbat.forecast_days + 1, my_predbat.midnight_utc, "value_inc_vat", "valid_from", backwards=False, to_key="valid_to")
9165+
if len(pdata) < 24 * 60:
9166+
print("ERROR: Expecting at least {} minutes of rate data got {}".format(24 * 60, len(pdata)))
9167+
failed = True
9168+
else:
9169+
print("Successfully processed {} minutes of rate data".format(len(pdata)))
9170+
night_rate = pdata.get(60)
9171+
day_rate = pdata.get(600)
9172+
if night_rate == day_rate:
9173+
print("ERROR: Expecting different night and day rates got {} and {}".format(night_rate, day_rate))
9174+
failed = True
9175+
9176+
return failed
9177+
9178+
91369179
def main():
91379180
# Parse command line arguments
91389181
parser = argparse.ArgumentParser(description="Predbat unit tests")
@@ -9179,6 +9222,8 @@ def main():
91799222
if args.perf_only:
91809223
return failed
91819224

9225+
if not failed:
9226+
failed |= test_download_octopus_url_wrapper(my_predbat)
91829227
if not failed:
91839228
failed |= test_plugin_startup_order(my_predbat)
91849229

0 commit comments

Comments
 (0)