Skip to content

Conversation

@springfall2008
Copy link
Owner

No description provided.

Copilot AI review requested due to automatic review settings December 22, 2025 18:41
@springfall2008 springfall2008 changed the title Change savings calc to update at 1am Fox Cloud retry backoff, savings calc update change to 1am Dec 22, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request changes the savings calculation update timing from midnight to 1 AM and adds rate limiting protection to the Fox API integration to prevent excessive API usage.

Key changes:

  • Savings totals now update at 1 AM instead of midnight to allow cloud data to stabilize
  • Fox API rate limiting tracks request rates and disables retries when exceeding 60 requests/hour
  • Daily counters reset at midnight to track per-day API usage

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 11 comments.

File Description
apps/predbat/predbat.py Modified savings calculation to update at 1 AM (after 60 minutes past midnight) instead of at midnight, allowing cloud data to stabilize before daily totals are calculated
apps/predbat/fox.py Added rate limiting logic including request tracking, hourly rate calculation with 30-minute floor, midnight reset of counters, and retry prevention when exceeding 60 requests/hour
apps/predbat/tests/test_fox_api.py Added comprehensive test coverage for rate limiting scenarios, midnight resets, available variables API, and updated mock objects to support rate limiting attributes

if fox.last_midnight_utc is not None and fox.last_midnight_utc != current_midnight:
fox.requests_today = 0
fox.rate_limit_errors_today = 0
fox.start_time_today = datetime.now(timezone.utc)
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test on line 4877 uses datetime.now(timezone.utc) directly instead of using the mocked datetime. Since the patch on line 4869 patches fox.datetime which doesn't correctly intercept the imported datetime function, this call will use the real datetime.now() instead of the mocked time. This will cause the test assertion on line 4882 to potentially fail because start_time_today will be set to the actual current time rather than the mocked day2_time.

Suggested change
fox.start_time_today = datetime.now(timezone.utc)
fox.start_time_today = mock_datetime.now(timezone.utc)

Copilot uses AI. Check for mistakes.
# Increment total at midnight for next day, don't do if in read-only mode
if savings_total_last_updated and savings_total_last_updated != todays_date and scheduled and recompute and not self.set_read_only:
# Increment total at 1am once we have today's data stable (cloud data can lag)
if self.minutes_now > 60 and savings_total_last_updated and savings_total_last_updated != todays_date and scheduled and recompute and not self.set_read_only:
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition self.minutes_now > 60 only checks if it's past 1:00 AM. However, this check will always be true during the rest of the day (1:05 AM, 2:00 PM, etc.), which means the savings update logic can potentially trigger multiple times per day instead of once. Consider using a more precise check such as self.minutes_now >= 60 and self.minutes_now < 65 or add an additional flag to ensure the update only happens once per day at 1 AM.

Suggested change
if self.minutes_now > 60 and savings_total_last_updated and savings_total_last_updated != todays_date and scheduled and recompute and not self.set_read_only:
if self.minutes_now >= 60 and self.minutes_now < 65 and savings_total_last_updated and savings_total_last_updated != todays_date and scheduled and recompute and not self.set_read_only:

Copilot uses AI. Check for mistakes.
Comment on lines +3286 to +3288
with patch("fox.datetime") as mock_datetime:
# Mock datetime.now() to return our test time
mock_datetime.now.return_value = current_time
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses patch("fox.datetime") to mock datetime, but the actual code in fox.py imports datetime at the top with from datetime import datetime, timedelta, timezone. This means datetime.now() in the code is a direct reference to the function, not an attribute of a module, so patch("fox.datetime") won't correctly intercept the call. The patch should be patch("fox.datetime.now") or you need to patch at the point where it's used as datetime.now.

Suggested change
with patch("fox.datetime") as mock_datetime:
# Mock datetime.now() to return our test time
mock_datetime.now.return_value = current_time
with patch("fox.datetime.now") as mock_datetime_now:
# Mock datetime.now() to return our test time
mock_datetime_now.return_value = current_time

Copilot uses AI. Check for mistakes.
Comment on lines +4746 to +4747
with patch("fox.datetime") as mock_datetime:
mock_datetime.now.return_value = current_time
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses patch("fox.datetime") to mock datetime, but the actual code in fox.py imports datetime at the top with from datetime import datetime, timedelta, timezone. This means datetime.now() in the code is a direct reference to the function, not an attribute of a module, so patch("fox.datetime") won't correctly intercept the call. The patch should be patch("fox.datetime.now") or you need to patch at the point where it's used as datetime.now.

Copilot uses AI. Check for mistakes.
Comment on lines +4782 to +4786
with patch("fox.datetime") as mock_datetime:
# Set start time (30 minutes minimum)
fox.start_time_today = start_time
current_time = start_time + timedelta(minutes=30)
mock_datetime.now.return_value = current_time
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses patch("fox.datetime") to mock datetime, but the actual code in fox.py imports datetime at the top with from datetime import datetime, timedelta, timezone. This means datetime.now() in the code is a direct reference to the function, not an attribute of a module, so patch("fox.datetime") won't correctly intercept the call. The patch should be patch("fox.datetime.now") or you need to patch at the point where it's used as datetime.now.

Suggested change
with patch("fox.datetime") as mock_datetime:
# Set start time (30 minutes minimum)
fox.start_time_today = start_time
current_time = start_time + timedelta(minutes=30)
mock_datetime.now.return_value = current_time
with patch("fox.datetime.now") as mock_datetime_now:
# Set start time (30 minutes minimum)
fox.start_time_today = start_time
current_time = start_time + timedelta(minutes=30)
mock_datetime_now.return_value = current_time

Copilot uses AI. Check for mistakes.
Comment on lines +4898 to +4903
with patch("fox.datetime") as mock_datetime:
fox.start_time_today = start_time

# Scenario 1: 20 requests in first 5 minutes (would be 240/hour without floor!)
current_time = start_time + timedelta(minutes=5)
mock_datetime.now.return_value = current_time
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses patch("fox.datetime") to mock datetime, but the actual code in fox.py imports datetime at the top with from datetime import datetime, timedelta, timezone. This means datetime.now() in the code is a direct reference to the function, not an attribute of a module, so patch("fox.datetime") won't correctly intercept the call. The patch should be patch("fox.datetime.now") or you need to patch at the point where it's used as datetime.now.

Copilot uses AI. Check for mistakes.
Comment on lines +4940 to +4945
with patch("fox.datetime") as mock_datetime:
fox.start_time_today = start_time

# Hour 1: 20 requests
current_time = start_time + timedelta(hours=1)
mock_datetime.now.return_value = current_time
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses patch("fox.datetime") to mock datetime, but the actual code in fox.py imports datetime at the top with from datetime import datetime, timedelta, timezone. This means datetime.now() in the code is a direct reference to the function, not an attribute of a module, so patch("fox.datetime") won't correctly intercept the call. The patch should be patch("fox.datetime.now") or you need to patch at the point where it's used as datetime.now.

Copilot uses AI. Check for mistakes.
if self.minutes_now > 60 and savings_total_last_updated and savings_total_last_updated != todays_date and scheduled and recompute and not self.set_read_only:
savings_total_predbat += self.savings_today_predbat
savings_total_pvbat += self.savings_today_pvbat
savings_total_soc = self.savings_today_predbat_soc
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assignment to savings_total_soc should use the compound assignment operator += to accumulate the total, not simple assignment =. This is inconsistent with the other savings totals being accumulated on lines 827-828, 830, and 832.

Suggested change
savings_total_soc = self.savings_today_predbat_soc
savings_total_soc += self.savings_today_predbat_soc

Copilot uses AI. Check for mistakes.
Comment on lines +4817 to +4821
with patch("fox.datetime") as mock_datetime:
# Start with high rate (35 requests in 30 minutes = 70/hour)
fox.start_time_today = start_time
current_time = start_time + timedelta(minutes=30)
mock_datetime.now.return_value = current_time
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses patch("fox.datetime") to mock datetime, but the actual code in fox.py imports datetime at the top with from datetime import datetime, timedelta, timezone. This means datetime.now() in the code is a direct reference to the function, not an attribute of a module, so patch("fox.datetime") won't correctly intercept the call. The patch should be patch("fox.datetime.now") or you need to patch at the point where it's used as datetime.now.

Copilot uses AI. Check for mistakes.
Comment on lines +4824 to +4825
elapsed_seconds = max((current_time - start_time).total_seconds(), 1800)
hourly_rate = (fox.requests_today * 3600) / elapsed_seconds
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to 'hourly_rate' is unnecessary as it is redefined before this value is used.

Suggested change
elapsed_seconds = max((current_time - start_time).total_seconds(), 1800)
hourly_rate = (fox.requests_today * 3600) / elapsed_seconds

Copilot uses AI. Check for mistakes.
@springfall2008 springfall2008 merged commit 40fab04 into main Dec 22, 2025
1 check passed
@springfall2008 springfall2008 deleted the fixes23 branch December 22, 2025 19:52
iangregory pushed a commit to iangregory/batpred that referenced this pull request Dec 24, 2025
…l2008#3108)

* Fox API retry backoff

* Change savings calculation to update at 1am

* Fix issue with fox test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants