Skip to content

feat(manual): extend manual rate override horizon to 7 days - #4930

Merged
springfall2008 merged 1 commit into
mainfrom
feat/manual-rate-override-7day-horizon
Sep 5, 2026
Merged

feat(manual): extend manual rate override horizon to 7 days#4930
springfall2008 merged 1 commit into
mainfrom
feat/manual-rate-override-7day-horizon

Conversation

@mgazza

@mgazza mgazza commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

A manual rate override (select.predbat_manual_import_rates and friends) placed more than 48 hours ahead is silently discarded.

manual_rates() filters on (minutes - minutes_now_real) < manual_rate_max where manual_rate_max = 48 * 60. A slot beyond that is dropped, the select value is rebuilt without it — off if nothing is left — and pushed back via expose_config(..., force=True). No warning is logged and nothing surfaces to the user, so the entry simply vanishes shortly after being saved.

The case that motivated this: an energy supplier announces a free-electricity hour several days in advance. Users are told about it, enter the 0p override straight away, and it disappears. Repeatedly, because there is no feedback telling them why. It only starts working once the target slot drifts inside 48 hours, which looks like an intermittent bug rather than a horizon.

Why rate overrides are different from time overrides

The 48 hour bound is right for a manual charge/export/freeze/demand slot: Predbat can only act on a slot the plan actually reaches, so there is no point holding one further out.

A rate override is not an instruction to act — it is future tariff data that only has to be remembered until the plan reaches it. Nothing about it needs the plan to be that long.

Change

  • Split the two horizons into named constants in const.py: MANUAL_TIME_MAX_MINUTES (48 hours, unchanged) and MANUAL_RATE_MAX_MINUTES (7 days).
  • 7 days is the ceiling the Day HH:MM selection format can express anyway — get_override_time_from_string() resolves a weekday to the next occurrence within 7 days — so this is the natural limit rather than an arbitrary one.
  • The web rate-override endpoint's own >= 48 * 60 validation now reads the same constants (and the plan-override endpoint reads the time constant), so its error message can no longer drift away from what the engine accepts. The message is derived from the constant instead of being hardcoded.

Safety of distant overrides

An override outside the plan window is inert until the plan reaches it:

  • apply_manual_rates() writes into a plain dict, so an out-of-window minute key is simply never read.
  • rate_minmax() iterates range(minutes_now, forecast_minutes + minutes_now) — a distant 0p slot cannot drag rate_min/rate_average down.
  • publish_rates() iterates up to minutes_now + forecast_minutes + 24 * 60, so a distant slot is not published until it comes into range.

Tests

Added to test_manual_times (inline in run_test_manual_times, so it runs under unit_test.py, not only under pytest):

  • T14 — a rate override 4 days ahead survives the round trip through manual_select()manual_rates().
  • T15 — a manual demand slot the same distance out is still dropped, pinning the deliberate asymmetry.

T14 fails against the old 48 hour value (verified by reverting the constant), so it is not vacuous.

unit_test.py -k manual_times / manual_select / manual_api / manual_soc / manual_overrides all pass.

A manual import/export/load/soc rate override placed more than 48 hours
ahead was silently discarded: manual_rates() dropped the slot, rewrote the
select back to "off" and pushed that to HA, with no error anywhere. From a
user's point of view the entry simply vanished after being saved.

That 48 hour bound makes sense for a manual charge/export/freeze/demand
slot, which Predbat can only act on if the plan reaches it. A rate override
is different: it is future tariff data that only has to be REMEMBERED until
the plan reaches it. The case that motivated this is an energy supplier
announcing a free-electricity hour several days in advance - users are told
about it, enter it straight away, and it disappears.

Split the two horizons into named constants and give rate overrides 7 days,
which is the ceiling the "Day HH:MM" selection format can express anyway.
Manual time slots keep their 48 hours. The web rate-override endpoint's
matching validation follows the same constants, so its error message can no
longer drift away from what the engine actually accepts.

Out-of-plan-window overrides are inert until the plan reaches them:
rate_minmax() and publish_rates() are both bounded by forecast_minutes, so a
distant slot cannot skew rate min/max/average or the published rates.

Tests: test_manual_times T14 (a rate override 4 days out survives) and T15
(a manual demand slot the same distance out is still dropped). T14 fails
against the old 48 hour value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The updated html_rate_override() path still needs input validation and a safer midnight origin to avoid returning 500s on bad time strings and to prevent clear-operations from misaddressing stored rates under the known midnight_utc mutation scenario.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Extends Predbat’s manual rate override retention horizon so users can enter future tariff events (e.g., free-electricity hours announced days ahead) without the override being silently dropped, while keeping manual time/action overrides bounded to the plan horizon.

Changes:

  • Introduces separate horizon constants for manual time overrides (48h) vs manual rate overrides (7d) in const.py.
  • Updates manual override decoding/validation to use the new constants (UI parsing in userinterface.py, web endpoints in web.py).
  • Adds regression tests covering a retained 4-day-ahead rate override and a still-dropped 4-day-ahead manual time slot.
File summaries
File Description
apps/predbat/web.py Uses the new horizon constants in rate/plan override endpoints and derives the validation message from the constants.
apps/predbat/userinterface.py Switches manual rate/time override horizons from hardcoded values to named constants.
apps/predbat/tests/test_manual_times.py Adds tests verifying 7-day rate retention vs 48-hour time-slot dropping.
apps/predbat/const.py Adds MANUAL_TIME_MAX_MINUTES (48h) and MANUAL_RATE_MAX_MINUTES (7d) constants with rationale.
Review details

Suppressed comments (1)

apps/predbat/web.py:4647

  • html_rate_override() computes minutes_from_midnight using self.midnight_utc (base.midnight_utc). manual_rates()/manual_times() deliberately avoid base.midnight_utc because calculate_yesterday() can rewrite it temporarily (#4900); using it here can make Clear Import/Export/SOC fail to find the actual stored rate and generate a non-matching clear option. Use the same origin as manual_rates by calling base.manual_time_origin() (or deriving midnight from now_utc) before calculating minutes_from_midnight.
                return web.json_response({"success": False, "message": f"Override time must be within {max_hours} hours from now."}, status=400)

            # Calculate minutes from midnight for looking up existing rates
            minutes_from_midnight = int((override_time - self.midnight_utc).total_seconds() / 60)
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/predbat/web.py
@springfall2008
springfall2008 merged commit f4d9d27 into main Sep 5, 2026
3 checks passed
@springfall2008
springfall2008 deleted the feat/manual-rate-override-7day-horizon branch September 5, 2026 12:17
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.

3 participants