Skip to content

Teslemetry: forced export never works — _tesla_dow writes the boost band one day late (Tesla uses Monday=0) #4610

Description

@springfall2008

Summary

TeslemetryAPI._tesla_dow() converts Python's weekday to a Sunday=0 day-of-week convention, but Tesla's tariff_content_v2 fromDayOfWeek/toDayOfWeek fields use Monday=0 — the same convention as datetime.weekday().

Every ON_PEAK boost band Predbat writes therefore lands one day late. During an actual export window the Powerwall sees only the ordinary off-peak tiers, so it has no reason to export and simply covers house load. Forced export has never worked on the TESLA inverter type.

Confirmed live: correcting only the day index made a Powerwall go from grid=0 to grid=-5156 (full 5 kW battery export) within 50 seconds, with no other change.

The defect

@staticmethod
def _tesla_dow(python_weekday):
    """Map a Python weekday (0=Mon..6=Sun) to Tesla's fromDayOfWeek (0=Sun..6=Sat)."""
    return (python_weekday + 1) % 7

The premise in that docstring is wrong. Tesla's day index is Monday=0. Reference: the Teslemetry-maintained python-tesla-fleet-api ships a pure offline resolver for this exact object, and its _minute_of_week is explicit:

def _minute_of_week(moment: datetime) -> int:
    """Monday 00:00 = 0 .. Sunday 23:59 = 10079, matching ``datetime.weekday()``."""
    return moment.weekday() * _MINUTES_PER_DAY + moment.hour * 60 + moment.minute

_tesla_dow feeds both _apply_boost (which day the boost is carved onto) and _side_layout (which day today's tier shape is placed on), so the whole per-day layout is shifted, not just the boost.

Evidence

Resolver, run against a tariff read back off a live device

The boost was pushed for Wednesday 21:30-23:30, and was written as fromDayOfWeek: 3:

Device ON_PEAK sell period: [{"fromDayOfWeek": 3, "toDayOfWeek": 3,
                              "fromHour": 21, "fromMinute": 30, "toHour": 23, "toMinute": 30}]

  Wed (intended)  22:00  buy=0.29 (OFF_PEAK)  sell=0.12 (SUPER_OFF_PEAK)   <- no boost
  Thu (day + 1)   22:00  buy=0.58 (ON_PEAK)   sell=0.58 (ON_PEAK)          <- boost lands here

Live before/after on a Powerwall 2 (firmware 26.18.3)

Same 0.58 boost price Predbat already computes, same autonomous, same battery_ok, same 4% reserve. The only difference is the day index — dow=3 (corrected, Mon=0) instead of dow=4 (current, Sun=0):

before     soc=95.0%  batt= 2100  load= 2652  grid=     0   <- load-following, as always
t+050s     soc=95.0%  batt= 5010  load=  430  grid= -5156   <- full 5 kW export to grid
t+100s     soc=95.0%  batt= 5000  load= 3309  grid= -2288
t+175s     soc=95.0%  batt= 5000  load= 3309  grid= -2288

Why this went unnoticed

Every prior symptom is explained by the shift, and each one independently looked like something else:

  • The battery tracks house load to the watt with grid=0 during "Exporting" — which is the correct response to the plain OFF_PEAK / SUPER_OFF_PEAK tariff it actually saw.
  • An overnight export window let the house import at 6.9p while the battery sat at 97% SoC. Also correct under a plain SUPER_OFF_PEAK rate.
  • Raising the boost to £500/kWh on both buy and sell changed nothing, because the £500 was written to the following day.
  • Comparing the generated tariff against Tesla's published example (PGE-EV2-A) does not reveal it — that tariff spans fromDayOfWeek: 0 -> toDayOfWeek: 6, so the convention is invisible. Same for the library's own real-world fixture.

Why the tests don't catch it

The assertions derive the expected day by calling the function under test:

today_dow = api._tesla_dow(api.base.now.weekday())
assert set(p["fromDayOfWeek"] for p in sell_periods["ON_PEAK"]["periods"]) == {today_dow}

That passes under any mapping. Same pattern in the other build_tariff tests via sell_price_at() / tier_at() helpers, which filter periods using api._tesla_dow(...).

Fix

Make _tesla_dow the identity function — Python's weekday() already matches Tesla's convention. Because both call sites (_apply_boost, _side_layout) take the same value, it stays internally consistent.

In apps/predbat/teslemetry.py:

     @staticmethod
     def _tesla_dow(python_weekday):
-        """Map a Python weekday (0=Mon..6=Sun) to Tesla's fromDayOfWeek (0=Sun..6=Sat).
+        """Return Tesla's fromDayOfWeek for a Python weekday.
 
-        Isolated so any future convention change is a one-line fix.
+        Tesla's tariff_content_v2 fromDayOfWeek/toDayOfWeek use Monday=0..Sunday=6,
+        the same convention as datetime.weekday(), so this is the identity mapping.
+        Kept as a named helper so the convention is stated in exactly one place.
         """
-        return (python_weekday + 1) % 7
+        return python_weekday

Verified live: with this mapping and no other change, a Powerwall went from grid=0 to grid=-5156 inside 50 seconds.

Test changes needed alongside it:

  • Pin a known date and assert the absolute expected index (e.g. Wednesday 2026-08-19 -> fromDayOfWeek == 2), never a value derived from _tesla_dow.
  • Worth adding a resolver-style test that evaluates the built tariff at a moment inside the window and asserts the ON_PEAK price is returned — that is the property that actually matters and it would have caught this.

Scope

Affects every site on the TESLA inverter type, not one installation. Any Powerwall using Predbat forced export has been writing its boost band to the wrong day, so the export leg has been silently inert while the plan reported Exporting. Charging is unaffected (it does not depend on the boost band).

Supersedes #4600 — the optimization_strategy theory there was a red herring. That field genuinely cannot be set alongside tariff_content_v2 (the API accepts the call and silently discards it), but it was never the reason export failed.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions