Skip to content

Commit 2da3c16

Browse files
fix(teslemetry): correct _tesla_dow - Tesla's day-of-week is Monday=0, not Sunday=0
_tesla_dow() mapped Python's weekday() (Monday=0) to a wrongly-assumed Tesla convention of Sunday=0, via (python_weekday + 1) % 7. Tesla's tariff_content_v2 fromDayOfWeek/toDayOfWeek actually use the same Monday=0 convention as datetime.weekday(), so every ON_PEAK boost band (and the day's real-tier layout) landed one day late. During the actual export window the Powerwall saw only the ordinary off-peak tariff, so it never had a price reason to export - it just covered house load, which is why every prior symptom (load-following with grid=0 during "Exporting", raising the boost price changing nothing) looked like something else. optimization_strategy (#4600) was a red herring; this is the actual cause. Fixed by making _tesla_dow the identity function. Test changes pin absolute expected day indices instead of deriving them from _tesla_dow itself (the previous self-referential pattern passed under any mapping), plus a new resolver-style test that independently resolves the built tariff's price at a moment inside the boost window using Tesla's real day convention. Confirmed live: correcting only the day index took a Powerwall from grid=0 to grid=-5156 (full 5kW export) within 50 seconds, no other change. Fixes #4610 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent d29c13d commit 2da3c16

2 files changed

Lines changed: 57 additions & 12 deletions

File tree

apps/predbat/teslemetry.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -919,11 +919,14 @@ def band_of(value):
919919

920920
@staticmethod
921921
def _tesla_dow(python_weekday):
922-
"""Map a Python weekday (0=Mon..6=Sun) to Tesla's fromDayOfWeek (0=Sun..6=Sat).
922+
"""Return Tesla's fromDayOfWeek for a Python weekday.
923923
924-
Isolated so any future convention change is a one-line fix.
924+
Tesla's tariff_content_v2 fromDayOfWeek/toDayOfWeek use Monday=0..Sunday=6, the same
925+
convention as datetime.weekday(), so this is the identity mapping (GH#4610 - the previous
926+
Sunday=0 mapping shifted every boost band one day late). Kept as a named helper so the
927+
convention is stated in exactly one place.
925928
"""
926-
return (python_weekday + 1) % 7
929+
return python_weekday
927930

928931
@staticmethod
929932
def _coalesce_day(slot_tiers):

apps/predbat/tests/test_teslemetry.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,10 @@ def test_teslemetry_build_tariff_boost_is_strict_max_on_today_dow():
572572
api.base = _rate_base(import_p=28.0, export_p=15.0)
573573
tariff = api.build_tariff((1020, 1080), now_min=600) # 17:00-18:00 window, now 10:00 -> today
574574
sell_periods = tariff["sell_tariff"]["seasons"]["AllYear"]["tou_periods"]
575-
today_dow = api._tesla_dow(api.base.now.weekday())
575+
# Absolute, independent expectation (GH#4610): Tesla's fromDayOfWeek uses Monday=0, the same
576+
# convention as plain datetime.weekday() - deliberately NOT routed through _tesla_dow, the
577+
# function under test, so a wrong mapping there cannot make this assertion trivially pass.
578+
today_dow = api.base.now.weekday()
576579
assert set(p["fromDayOfWeek"] for p in sell_periods["ON_PEAK"]["periods"]) == {today_dow}
577580
boost = tariff["sell_tariff"]["energy_charges"]["AllYear"]["rates"]["ON_PEAK"]
578581
real = [v for t, v in tariff["sell_tariff"]["energy_charges"]["AllYear"]["rates"].items() if t != "ON_PEAK"]
@@ -664,7 +667,8 @@ def agile(offset):
664667
tariff = api.build_tariff((17 * 60, 18 * 60 + 30), now_min=12 * 60)
665668
sell = tariff["sell_tariff"]["energy_charges"]["AllYear"]["rates"]
666669
periods = tariff["sell_tariff"]["seasons"]["AllYear"]["tou_periods"]
667-
today = api._tesla_dow(api._local_today_weekday())
670+
# Absolute, independent expectation (GH#4610) - see test_teslemetry_build_tariff_boost_is_strict_max_on_today_dow.
671+
today = api._local_today_weekday()
668672

669673
def sell_price_at(minute):
670674
"""Return the sell tier price applying on today's day at the given minute-of-day."""
@@ -704,7 +708,8 @@ def test_teslemetry_quantise_in_range_excluded_price_no_keyerror():
704708
tariff = api.build_tariff((17 * 60, 17 * 60 + 30), now_min=12 * 60) # must not raise
705709
sell = tariff["sell_tariff"]["energy_charges"]["AllYear"]["rates"]
706710
periods = tariff["sell_tariff"]["seasons"]["AllYear"]["tou_periods"]
707-
today = api._tesla_dow(api._local_today_weekday())
711+
# Absolute, independent expectation (GH#4610) - see test_teslemetry_build_tariff_boost_is_strict_max_on_today_dow.
712+
today = api._local_today_weekday()
708713

709714
def tier_at(minute):
710715
"""Return the sell tier applying on today at the given minute-of-day."""
@@ -1891,11 +1896,47 @@ def test_teslemetry_quantise_agile_three_bands_clamped_rounded():
18911896
assert len(today) == 48 and len(tomorrow) == 48
18921897

18931898

1894-
def test_teslemetry_tesla_dow_sunday_zero():
1895-
"""Python weekday (Mon=0..Sun=6) maps to Tesla fromDayOfWeek (Sun=0..Sat=6)."""
1896-
assert TeslemetryAPI._tesla_dow(6) == 0 # Sunday
1897-
assert TeslemetryAPI._tesla_dow(0) == 1 # Monday
1898-
assert TeslemetryAPI._tesla_dow(5) == 6 # Saturday
1899+
def test_teslemetry_tesla_dow_matches_python_weekday():
1900+
"""Tesla's tariff_content_v2 fromDayOfWeek/toDayOfWeek use Monday=0..Sunday=6, the same convention
1901+
as datetime.weekday() (GH#4610) - so _tesla_dow must be the identity function. The previous
1902+
(Sunday=0) mapping shifted every boost band one day late: during the actual export window the
1903+
Powerwall saw only the ordinary off-peak tariff and had no reason to export."""
1904+
for python_weekday in range(7):
1905+
assert TeslemetryAPI._tesla_dow(python_weekday) == python_weekday
1906+
1907+
1908+
def test_teslemetry_build_tariff_boost_resolves_at_the_real_tesla_day_index():
1909+
"""Resolver-style regression for GH#4610: independently resolve the built tariff's sell price at a
1910+
moment inside the boost window using Tesla's real day convention (Monday=0, i.e. plain
1911+
datetime.weekday(), never routed through _tesla_dow) and assert the boosted ON_PEAK price applies.
1912+
This is the property that actually matters - a real Powerwall evaluating fromDayOfWeek against its
1913+
own Monday=0 clock must land on the boosted band, not the ordinary off-peak one next to it. Before
1914+
the fix this failed: the boost was carved onto (weekday+1)%7, one day away from where a real
1915+
Powerwall would look for it, so the device saw only off-peak rates during the actual window."""
1916+
api = MockTeslemetryAPI()
1917+
api.base = _rate_base(import_p=28.0, export_p=15.0) # now = 2026-07-20 12:00, a Monday
1918+
window = (17 * 60, 18 * 60) # 17:00-18:00, still ahead of now (12:00) -> lands on today
1919+
tariff = api.build_tariff(window, now_min=12 * 60)
1920+
sell = tariff["sell_tariff"]["energy_charges"]["AllYear"]["rates"]
1921+
periods = tariff["sell_tariff"]["seasons"]["AllYear"]["tou_periods"]
1922+
1923+
real_dow = api.base.now.weekday() # Tesla's actual day index for "today" - independent of _tesla_dow
1924+
minute = 17 * 60 + 30 # inside the window
1925+
1926+
def resolve_tier(dow, minute):
1927+
"""Mimic how a real Powerwall would resolve which tier applies at (dow, minute)."""
1928+
for tier, block in periods.items():
1929+
for period in block["periods"]:
1930+
if period["fromDayOfWeek"] <= dow <= period["toDayOfWeek"]:
1931+
start = period["fromHour"] * 60 + period["fromMinute"]
1932+
end = (period["toHour"] * 60 + period["toMinute"]) or 1440
1933+
if start <= minute < end:
1934+
return tier
1935+
return None
1936+
1937+
tier = resolve_tier(real_dow, minute)
1938+
assert tier == "ON_PEAK", "a real Powerwall resolving fromDayOfWeek={} at minute={} would see tier={}, not ON_PEAK".format(real_dow, minute, tier)
1939+
assert sell[tier] == sell["ON_PEAK"]
18991940

19001941

19011942
def test_teslemetry_boost_price_floor_wins_on_low_rates():
@@ -2173,7 +2214,8 @@ def test_teslemetry(my_predbat=None):
21732214
test_teslemetry_quantise_flat_single_tier()
21742215
test_teslemetry_quantise_two_distinct_exact()
21752216
test_teslemetry_quantise_agile_three_bands_clamped_rounded()
2176-
test_teslemetry_tesla_dow_sunday_zero()
2217+
test_teslemetry_tesla_dow_matches_python_weekday()
2218+
test_teslemetry_build_tariff_boost_resolves_at_the_real_tesla_day_index()
21772219
test_teslemetry_boost_price_floor_wins_on_low_rates()
21782220
test_teslemetry_side_layout_partitions_every_day()
21792221
test_teslemetry_render_side_matched_sets_and_day_end()

0 commit comments

Comments
 (0)