Add TeslemetryAPI component — Tesla Powerwall control via Teslemetry/Fleet API#4177
Add TeslemetryAPI component — Tesla Powerwall control via Teslemetry/Fleet API#4177mgazza wants to merge 14 commits into
Conversation
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>
…oc_max retry, calendar_history query, control attrs, number guard Fixes six code-review findings, each with a regression test confirmed red before/green after: - build_tariff now derives `now` from the base's local time (base.now_utc.astimezone(base.local_tz)) instead of raw UTC, so the export_now ON_PEAK window lands on the Powerwall's actual local wall-clock boundary instead of being off by the site's UTC offset. - run() gates reconcile_on_start() on a new `reconcile_done` latch (mirroring site_info_done) instead of the `first` flag, so a boot that 401s (consuming `first`) and later recovers still reconciles a device stuck exporting. reconcile_on_start() now returns whether it actually got a usable tariff read, so the latch only sets once that happened. - fetch_site_info() only returns True once soc_max was actually published; when nameplate_energy is absent/zero it still seeds the other control-entity fields but returns False, so site_info_done stays False and it retries. - fetch_energy_today() now requests calendar_history with kind=energy&period=day, which the Fleet/Teslemetry endpoint requires. - All control-entity writes (select/number/switch events, reconcile's restore, site_info seeding) now route through a new publish_control() helper that re-applies the options/min/max/friendly_name register_control_entities recorded at init, instead of wiping them via a bare set_state_wrapper call. - number_event() guards the int(float(value)) conversion against a null value (e.g. from a number.increment/decrement service call) instead of raising, matching gateway.py's pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed findings from a follow-up review pass in
Tests 34 → 42; |
…slemetry command-credit spend Route the 5 control writes (operation_mode, backup_reserve, grid_charging, export_rule, tariff) through a per-key _last_sent signature cache so a POST only fires when the value actually changed, instead of every 5-minute loopback cycle from the repeat:true service hooks. The cache is only updated on a confirmed-successful send, so a failed command still retries next cycle, and fetch_site_info refreshes operation_mode/backup_reserve to the device's actual state so external drift (e.g. changed in the Tesla app) is detected and re-asserted. reconcile_on_start forces its tariff and export-rule recovery writes so a future cache-preseed can never make recovery look like a no-op. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… document transition-based drift limitation
|
Command-credit optimisation in The control hooks re-assert every ~5-min cycle (
Net: ~£25–30/mo → ~£2–4/mo per site. Failure-retry and drift both covered by tests; suite 42 → 53. Known limitation (documented in-code, accepted for the beta pilot): the device-drift refresh is boot-only for mode/reserve, and |
Summary
New TeslemetryAPI component (
apps/predbat/teslemetry.py): Tesla Powerwall support over the Teslemetry REST API (which mirrors Tesla Fleet API paths — thebase_urlis configurable so a direct Fleet API connection works with zero component changes). Ports the proventemplates/tesla_powerwall.yamlcontrol semantics (operation mode / backup reserve / grid-charging / export rule / export tariff-trick) from HA service hooks onto component virtual entities, driven by the existing*_servicehook loopback.live_status(120 s),site_info(capacity →soc_max; seeds operation-mode/reserve entity states),calendar_history(daily kWh) → publishespredbat_teslemetry_*sensors. 401/403 latches an auth-failed guard (recovery via live_status probe only); 429 exponential backoff.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 therepeat: truehooks re-assert next cycle.tariff_mode=export_nowposts a synthetic ToU tariff (current 30-min window ON_PEAK, sell pricemax(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).normalrestores a flat tariff from the customer's live rates.GET /tariff_rate) and, if ourPREDBAT-EXPORT-NOWmarker is present (pod died mid-export), restores the normal tariff + disables export — gated on the realset_read_onlyconfig viaget_arg.components.pyentry (event_filter: predbat_teslemetry_) +APPS_SCHEMAkeysteslemetry_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
tests/test_teslemetry.py(registered inunit_test.py): field mapping/kWh conversions, real_requeststatus 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 failuremulti_car_iog_load_slots_regression, verified pre-existing via stash-baseline).Known follow-ups
GET /tariff_rateresponse shape is inferred from docs (defensive code-walker parse; fails safe) — to be validated on live PW3 hardware during the beta pilot.🤖 Generated with Claude Code