Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions openeihttp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,66 @@

return rate_structure
return None

@property
def next_energy_rate_structure(self) -> int | None:
"""Return the next rate structure."""
return self.next_rate_schedule(datetime.datetime.today(), "energy")[1]

@property
def next_energy_rate_structure_time(self) -> datetime.datetime | None:
"""Return the time at which the next rate structure will take effect."""
return self.next_rate_schedule(datetime.datetime.today(), "energy")[0]

def next_rate_schedule(self, start: datetime.datetime, rate_type: str) -> tuple[datetime.datetime | None, int | None]:
"""
Return the next datetime at which the rate structure changes and the new rate structure.
This function is optimzied to avoid looping over every hour, day, month combination.
"""
assert self._data is not None
if not f"{rate_type}ratestructure" in self._data:
return None, None

Check warning on line 271 in openeihttp/__init__.py

View check run for this annotation

Codecov / codecov/patch

openeihttp/__init__.py#L271

Added line #L271 was not covered by tests

current_structure = self.rate_structure(start, rate_type)
current_time = start
# Loop through the next 12 months
for month_idx in range(start.month - 1, 12 + start.month - 1):
current_time = current_time.replace(year=start.year + (month_idx // 12), month=(month_idx % 12) + 1, minute=0, second=0, microsecond=0)
day_of_week = current_time.weekday()

schedules = ["weekendschedule", "weekdayschedule"] if day_of_week > 4 else ["weekdayschedule", "weekendschedule"]
# If the hour is greater than 0 (only the first month), a case can occur where the next rate is earlier in the same schedule
# This requires checking the first schedule again if there is no change found in the latter part of the first schedule or the second schedule
if current_time.hour > 0:
schedules.append(schedules[0])

for schedule in schedules:
table = f"{rate_type}{schedule}"
day_of_week = current_time.weekday()

for hour in range(current_time.hour, 24 + current_time.hour):
hour = hour % 24
rate_structure = self._data[table][current_time.month - 1][hour]
if rate_structure != current_structure:
# hour < currnet_time.hour indicates we are in the next day
# Check to make sure the schedule type hasn't changed and we are in the same month
if hour < current_time.hour and day_of_week not in [4, 6]:
if (current_time + datetime.timedelta(days=1)).month == current_time.month:
return current_time.replace(day=current_time.day + 1, hour=hour), rate_structure
elif hour >= current_time.hour:
return current_time.replace(hour=hour), rate_structure

# Move to the day where the next schedule starts
days_to_move = 5 - day_of_week if day_of_week <= 4 else 7 - day_of_week
if (current_time + datetime.timedelta(days=days_to_move)).month > current_time.month:
break

Check warning on line 305 in openeihttp/__init__.py

View check run for this annotation

Codecov / codecov/patch

openeihttp/__init__.py#L305

Added line #L305 was not covered by tests
current_time = current_time.replace(hour=0, day=current_time.day + days_to_move)

current_time = current_time.replace(day=1)

# If we reach here, it means we didn't find a change in the next 12 months
# Assume the rate structure doesn't change
return None, current_structure

Check warning on line 312 in openeihttp/__init__.py

View check run for this annotation

Codecov / codecov/patch

openeihttp/__init__.py#L312

Added line #L312 was not covered by tests

@property
def current_rate(self) -> float | None:
Expand Down
165 changes: 165 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,171 @@ async def test_get_tier_rate_data_low_second_period(
assert structure == 1


@freeze_time("2025-01-01 10:21:34")
async def test_get_next_rate_structure(mock_aioclient):
"""
Test calculating the next rate structure
This test is run on a weekday with the new structure occuring the same day
"""
mock_aioclient.get(
re.compile(TEST_PATTERN),
status=200,
body=load_fixture("plan_data.json"),
)
test_rates = openeihttp.Rates(
api="fakeAPIKey", lat="1", lon="1", plan="574613aa5457a3557e906f5b"
)

await test_rates.clear_cache()
await test_rates.update()

current_structure = test_rates.current_energy_rate_structure
assert current_structure == 3

next_struture = test_rates.next_energy_rate_structure
next_time = test_rates.next_energy_rate_structure_time
assert next_struture == 2
assert next_time == datetime.datetime(2025, 1, 1, 12, 0)


@freeze_time("2025-01-01 23:21:34")
async def test_get_next_rate_structure_next_day(mock_aioclient):
"""
Test calculating the next rate structure
This test is run on a weekday with the new structure occuring the next day (also a weekday)
"""
mock_aioclient.get(
re.compile(TEST_PATTERN),
status=200,
body=load_fixture("plan_data.json"),
)
test_rates = openeihttp.Rates(
api="fakeAPIKey", lat="1", lon="1", plan="574613aa5457a3557e906f5b"
)

await test_rates.clear_cache()
await test_rates.update()

current_structure = test_rates.current_energy_rate_structure
assert current_structure == 3

next_struture = test_rates.next_energy_rate_structure
next_time = test_rates.next_energy_rate_structure_time
assert next_struture == 2
assert next_time == datetime.datetime(2025, 1, 2, 12, 0)


@freeze_time("2025-01-03 23:21:34")
async def test_get_next_rate_structure_weekend_loop(mock_aioclient):
"""
Test calculating the next rate structure
This test is run on a weekday. The following weekend has the same structure for the entire week
So we must loop back to the following weekday to find the next structure
"""
mock_aioclient.get(
re.compile(TEST_PATTERN),
status=200,
body=load_fixture("plan_data.json"),
)
test_rates = openeihttp.Rates(
api="fakeAPIKey", lat="1", lon="1", plan="574613aa5457a3557e906f5b"
)

await test_rates.clear_cache()
await test_rates.update()

current_structure = test_rates.current_energy_rate_structure
assert current_structure == 3

next_struture = test_rates.next_energy_rate_structure
next_time = test_rates.next_energy_rate_structure_time
assert next_struture == 2
assert next_time == datetime.datetime(2025, 1, 6, 12, 0)


@freeze_time("2025-01-03 23:21:34")
async def test_get_next_rate_structure_next_month(mock_aioclient):
"""
Test calculating the next rate structure
plan_tier_data has a different structure than plan_data and the structure doesn't change until May
"""
mock_aioclient.get(
re.compile(TEST_PATTERN),
status=200,
body=load_fixture("plan_tier_data.json"),
)
test_rates = openeihttp.Rates(
api="fakeAPIKey", lat="1", lon="1", plan="574613aa5457a3557e906f5b"
)

await test_rates.clear_cache()
await test_rates.update()

current_structure = test_rates.current_energy_rate_structure
assert current_structure == 1

next_struture = test_rates.next_energy_rate_structure
next_time = test_rates.next_energy_rate_structure_time
assert next_struture == 0
assert next_time == datetime.datetime(2025, 5, 1, 0, 0)


@freeze_time("2025-01-04 23:21:34")
async def test_get_next_rate_structure_next_month_weekend_start(mock_aioclient):
"""
Test calculating the next rate structure
This test is run on a weekend. The following weekday has the same structure for the entire week
The structure doesn't change until May
"""
mock_aioclient.get(
re.compile(TEST_PATTERN),
status=200,
body=load_fixture("plan_tier_data.json"),
)
test_rates = openeihttp.Rates(
api="fakeAPIKey", lat="1", lon="1", plan="574613aa5457a3557e906f5b"
)

await test_rates.clear_cache()
await test_rates.update()

current_structure = test_rates.current_energy_rate_structure
assert current_structure == 1

next_struture = test_rates.next_energy_rate_structure
next_time = test_rates.next_energy_rate_structure_time
assert next_struture == 0
assert next_time == datetime.datetime(2025, 5, 1, 0, 0)


@freeze_time("2024-11-01 23:21:34")
async def test_get_next_rate_structure_next_year(mock_aioclient):
"""
Test calculating the next rate structure
This test is run on a weekday. The following weekend has the same structure for the entire week
The structure doesn't change until May of the following year
"""
mock_aioclient.get(
re.compile(TEST_PATTERN),
status=200,
body=load_fixture("plan_tier_data.json"),
)
test_rates = openeihttp.Rates(
api="fakeAPIKey", lat="1", lon="1", plan="574613aa5457a3557e906f5b"
)

await test_rates.clear_cache()
await test_rates.update()

current_structure = test_rates.current_energy_rate_structure
assert current_structure == 1

next_struture = test_rates.next_energy_rate_structure
next_time = test_rates.next_energy_rate_structure_time
assert next_struture == 0
assert next_time == datetime.datetime(2025, 5, 1, 0, 0)


@freeze_time("2021-08-13 10:21:34")
async def test_get_tier_rate_data_med(test_lookup_tier_med, mock_aioclient):
"""Test rate schedules."""
Expand Down
Loading