Skip to content

Add TeslemetryAPI component — Tesla Powerwall control via Teslemetry/Fleet API#4177

Open
mgazza wants to merge 9 commits into
mainfrom
feat/tesla-powerwall-teslemetry
Open

Add TeslemetryAPI component — Tesla Powerwall control via Teslemetry/Fleet API#4177
mgazza wants to merge 9 commits into
mainfrom
feat/tesla-powerwall-teslemetry

Conversation

@mgazza

@mgazza mgazza commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

New TeslemetryAPI component (apps/predbat/teslemetry.py): Tesla Powerwall support over the Teslemetry REST API (which mirrors Tesla Fleet API paths — the base_url is configurable so a direct Fleet API connection works with zero component changes). Ports the proven templates/tesla_powerwall.yaml control semantics (operation mode / backup reserve / grid-charging / export rule / export tariff-trick) from HA service hooks onto component virtual entities, driven by the existing *_service hook loopback.

  • Data path: polls live_status (120 s), site_info (capacity → soc_max; seeds operation-mode/reserve entity states), calendar_history (daily kWh) → publishes predbat_teslemetry_* sensors. 401/403 latches an auth-failed guard (recovery via live_status probe only); 429 exponential backoff.
  • Control path: five virtual entities (operation_mode, backup_reserve, allow_charging_from_grid, allow_export, tariff_mode) with write-only-on-success semantics — a failed REST command leaves entity state untouched so the repeat: true hooks re-assert next cycle.
  • Export tariff-trick: tariff_mode=export_now posts a synthetic ToU tariff (current 30-min window ON_PEAK, sell price max(0.50, 2× live export rate)); correct circle-complement off-peak periods including midnight wrap (property-tested: exact partition of the day for all 48 slots). normal restores a flat tariff from the customer's live rates.
  • Boot reconciliation: reads the device's current tariff (GET /tariff_rate) and, if our PREDBAT-EXPORT-NOW marker is present (pod died mid-export), restores the normal tariff + disables export — gated on the real set_read_only config via get_arg.
  • Registration: components.py entry (event_filter: predbat_teslemetry_) + APPS_SCHEMA keys teslemetry_key/teslemetry_site_id/teslemetry_base_url.

Companion SaaS PR: Predictive-Cloud-Ltd/predbat-saas feat/tesla-powerwall-teslemetry (onboarding + config generation). Part of Predictive-Cloud-Ltd/predbat-saas#1346.

Testing

  • 34 unit tests in tests/test_teslemetry.py (registered in unit_test.py): field mapping/kWh conversions, real _request status branching (401 latch, 429 retry+backoff, recovery), run() bool contract, all five command handlers incl. failure paths, tariff builder (incl. midnight-wrap partition invariant and sell clamp), reconcile (marker found / absent / read-failed / read-only / boot-default gating).
  • ./run_all --quick: 122 groups pass (1 pre-existing unrelated failure multi_car_iog_load_slots_regression, verified pre-existing via stash-baseline).
  • Pre-commit (black/ruff/interrogate 100%/cspell en-gb) clean.

Known follow-ups

  • GET /tariff_rate response shape is inferred from docs (defensive code-walker parse; fails safe) — to be validated on live PW3 hardware during the beta pilot.
  • Auth-failed steady state logs a warning per minute (deliberate: component is genuinely unhealthy); may want throttling before long soak.

🤖 Generated with Claude Code

mgazza and others added 9 commits July 2, 2026 22:55
Implement control path for Tesla Powerwall integration:
- Add site_info_done latch to ensure soc_max publishes eventually
- Register virtual control entities: operation_mode, backup_reserve, allow_charging_from_grid, allow_export, tariff_mode
- Implement command handlers: set_operation_mode, set_backup_reserve, set_grid_charging, set_export_rule
- Add set_tariff stub (completed in Task 6)
- Implement event handler overrides: select_event, number_event, switch_event
- Add 6 control tests + 1 latch test (16 passing tests total)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement Task 6 of Tesla Powerwall integration: tariff builder, export-now trick,
and boot reconciliation.

- Add EXPORT_SELL_RATE constant and current_rates() to read from base.rate_import/export
- Implement build_tariff(mode, now) with export_now (current 30-min window ON_PEAK) and
  normal (flat) modes, returning tariff_content_v2 dict
- Replace set_tariff() stub with real implementation POSTing /time_of_use_settings
- Add reconcile_on_start() to restore safe state if previous run died mid-export
- Wire reconcile_on_start() into run() after site_info latch

All 5 new tests pass (build tariff, set tariff, reconcile); regression on ge_cloud
also passes (66 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address review findings on the export tariff-trick:

- CRITICAL: export_now windows crossing midnight (e.g. 23:30-00:30) produced
  overlapping SUPER_OFF_PEAK periods covering the full day. The complement is now
  a proper circle complement of [start, end): a single wrapped segment when the
  window crosses midnight, two segments otherwise.
- IMPORTANT: ON_PEAK sell is now clamped to max(EXPORT_SELL_RATE, 2x live export
  rate) so the trick cannot silently invert when the live export rate exceeds 0.50.
- MINOR: buy-side ON_PEAK mirrors the high sell rate to discourage grid-charging
  during the export window.
- Tests: midnight-wrap (23:40) and exact-midnight (23:10) cases with a
  partition-of-day assertion (no overlap, no gap), sell-clamp with a live 0.60
  export rate, and a reconcile_on_start partial-failure pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Registers TeslemetryAPI in COMPONENT_LIST (phase 1, event_filter
predbat_teslemetry_) and adds teslemetry_key/teslemetry_site_id/
teslemetry_base_url to APPS_SCHEMA, placed alongside the fox_key
inverter block (config.py has no deye_key entry to anchor on).
…ation

reconcile_on_start previously read the local tariff_mode entity, which
register_control_entities() unconditionally reseeds to "normal" on every
boot before run(first=True) fires - making the export_now recovery path
dead code (a pod restart mid-export could never self-heal). It now reads
the Powerwall's actual tariff via GET tariff_rate and walks the response
for a "code" matching build_tariff("export_now"), respecting read-only
mode. _command also treated any parsed 2xx body as success; it now fails
on an application-level "error" key or response.code >= 400. fetch_site_info
seeds the operation_mode/backup_reserve entity states from device data
instead of leaving them at hardcoded defaults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_is_read_only() read base.set_read_only directly, but the PredBat
constructor defaults that attribute to True (predbat.py:504) and it is
only refreshed from config in the fetch cycle (fetch.py:2401), which
runs AFTER phase-1 components start - so at reconcile_on_start time the
attribute was always True and the recovery write was unreachable again,
with a misleading read-only log. load_user_config(load_config=True)
runs before phase-1 start (predbat.py:1574 vs 1577), so reading via
get_arg("set_read_only", False) returns the real configured value.
Also split get_current_tariff_code's failure logging: "read failed"
(no response) vs "no tariff code" (response without a code key).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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