Skip to content

feat(tariff): add Tariff V2 rate resolver#102

Closed
Bre77 wants to merge 3 commits into
mainfrom
fm/tariff-helper-impl-py
Closed

feat(tariff): add Tariff V2 rate resolver#102
Bre77 wants to merge 3 commits into
mainfrom
fm/tariff-helper-impl-py

Conversation

@Bre77

@Bre77 Bre77 commented Jul 23, 2026

Copy link
Copy Markdown
Member

Intent

Implement the getTariffPeriods pure resolver for python-tesla-fleet-api per the approved design report (data/tariff-period-helper-design/report.md). New module tesla_fleet_api/tariff.py, re-exported from the top-level package like util.py: get_tariff_periods(tariff, now, *, horizon_hours=None) -> TariffResolution | None over the raw tariff_content_v2 object (caller supplies tz-aware now in the site zone), plus unwrap_tariff_v2(response) which raises InvalidResponse on a malformed/null body. Frozen dataclasses TariffRate/TariffPeriod/TariffResolution return current buy AND sell rate, current_start, next_change, currency, and an optional upcoming list. Ported HA's matching sub-logic (season year-cross, day-of-week wrap, midnight cross, season/period ALL fallback) but not its file - deliberately represented instants as minute-of-week arithmetic rather than datetime.replace(hour=...), since the HA reference crashes on toHour: 24 (a real value in live tariffs) via that construction; this resolver normalizes toHour==24/toMinute==60 to a next-day rollover instead. Price lookups use key-presence checks, never truthiness, since a 0.0 sell price is a legitimate value, not missing. A naive (tz-unaware) now raises ValueError since the tariff object itself carries no timezone. Committed a live-shaped Moorinya tariff fixture (tests/fixtures/tariff_moorinya_v2.json, 48 half-hour buy/sell periods, includes the toHour:24 real anomaly and an empty Winter season object) plus tests/test_tariff.py covering season year-cross, Friday->Monday day-of-week wrap, a midnight-crossing period, the toHour:24 regression against the HA crash, a 0.0 sell price, missing-rate->None, absent sell_tariff, no-season->None coverage, and naive-now->ValueError. This is a read-only tariff resolver: pure, offline, no write, no VPP access. A sibling task mirrors the same API shape in node-tesla-fleet-api, so the Python shape here is the reference the node port should match. Added an AGENTS.md entry documenting the new module.

What Changed

  • Add a pure Tariff V2 resolver that returns frozen buy/sell rate, period, currency, boundary, and optional upcoming-period models.
  • Support tariff envelope unwrapping, timezone-aware resolution, season/day/week wrapping, midnight rollovers, season boundaries, fallback rates, and zero-valued prices.
  • Export and document the tariff helpers, with fixture-backed coverage for live tariff shapes and edge cases.

Risk Assessment

✅ Low: The follow-up correctly bounds current and upcoming periods at both buy and sell season transitions, with focused regression coverage and no new material risks found.

Testing

The focused tariff tests and full regression suite passed, while a captured public-API transcript demonstrated correct buy/sell resolution, preservation of a 0.0 sell price, next-day rollover for toHour: 24, and upcoming periods across midnight. This is a non-UI library change, so command/API output is the appropriate reviewer-visible evidence.

Evidence: Public API tariff resolution transcript
local time:     2026-07-20T10:00:00+10:00
buy:            0.31 AUD (PERIOD_10_00)
sell:           0.0 AUD (PERIOD_10_00)
current start:  2026-07-20T10:00:00+10:00
next change:    2026-07-20T10:30:00+10:00

local time:     2026-07-20T17:45:00+10:00
buy:            0.42 AUD (PERIOD_17_30)
sell:           0.45 AUD (PERIOD_17_30)
current start:  2026-07-20T17:30:00+10:00
next change:    2026-07-20T18:00:00+10:00

local time:     2026-07-20T23:45:00+10:00
buy:            0.31 AUD (PERIOD_23_30)
sell:           0.0 AUD (PERIOD_23_30)
current start:  2026-07-20T23:30:00+10:00
next change:    2026-07-21T00:00:00+10:00

2-hour upcoming schedule from 23:00:
  2026-07-20T23:00:00+10:00 -> 2026-07-20T23:30:00+10:00 | buy=0.31 sell=0.0
  2026-07-20T23:30:00+10:00 -> 2026-07-21T00:00:00+10:00 | buy=0.31 sell=0.0
  2026-07-21T00:00:00+10:00 -> 2026-07-21T00:30:00+10:00 | buy=0.24 sell=0.0
  2026-07-21T00:30:00+10:00 -> 2026-07-21T01:00:00+10:00 | buy=0.24 sell=0.0

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • 🚨 tesla_fleet_api/tariff.py:204 - next_change and current_start are derived only from weekly period starts, so season boundaries are ignored. If a tariff changes season mid-period (for example at midnight on April 1 while the active TOU period spans several days), the returned rate interval crosses the season boundary and upcoming continues using the old season until the next weekly start. Include the selected buy/sell season start/end boundaries when bracketing the resolution interval.
  • 🚨 tesla_fleet_api/tariff.py:93 - The required criterion says unwrap_tariff_v2 “raises InvalidResponse on a malformed/null body,” and its docstring likewise promises rejection of malformed shapes, but the fallback returns any unrecognized dictionary—including {} or {"unexpected": true}—as a valid bare tariff. Validate that a bare object has the required Tariff V2 structure or clarify which minimum shape is intentionally accepted.

🔧 Fix: Respect tariff season boundaries in resolved periods
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Inspected the target diff and reviewed tesla_fleet_api/tariff.py and tests/test_tariff.py against the authoritative acceptance criteria.
  • uv run pytest tests/test_tariff.py
  • Ran a consumer-style Python script using top-level tesla_fleet_api imports and the live-shaped Moorinya fixture, capturing daytime, peak, zero sell price, toHour: 24, and midnight-crossing horizon results.
  • uv run pytest tests
  • git status --short confirmed testing introduced no worktree changes.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

firstmate crewmate added 3 commits July 23, 2026 21:01
Add get_tariff_periods, a pure offline resolver over the raw
tariff_content_v2 object, returning the current buy/sell rate plus the
next boundary. Ports the Home Assistant teslemetry integration's
season/day-of-week/midnight-cross matching logic without its file -
the reference crashes on real data's toHour: 24 end-of-day periods
because it constructs datetime.replace(hour=24); this resolver
represents instants as minute-of-week instead, which sidesteps that
crash entirely, and treats a 0.0 sell price as a real value rather
than a missing one.

Includes a live-shaped 48-half-hour-period fixture plus tests for
season year-crossing, day-of-week wrap, midnight-crossing periods,
the toHour: 24 regression, absent sell_tariff, missing rates, and
naive-now rejection.
@Bre77

Bre77 commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Closing: this PR's fixture/docs referenced a real site name that must not be public. Re-opening after sanitizing the fixture.

@Bre77 Bre77 closed this Jul 23, 2026
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.

1 participant