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 37 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 |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ack live SOC Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n commit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ate each cycle Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r type Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Alphabetical re-sort triggered by the file-contents-sorter pre-commit hook ahead of final verification for the Teslemetry window control plane work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ional auto-config args (review) Addresses three review findings on the Tesla Powerwall Teslemetry component: - reconcile_on_start's boot check could permanently gate the scheduler emulator off if the device's tariff_rate response never yields a usable code; bound it to RECONCILE_MAX_ATTEMPTS retries, after which run() force-latches reconcile_done with a one-time warning. - get_minutes_now() now always reads the live wall clock instead of preferring base.minutes_now, which only refreshes on Predbat's 5-minute cycle and could freeze window enforcement. - automatic_config() only wires battery_rate_max/inverter_limit args when fetch_site_info actually published those sensors, so a site missing nameplate_power/max_site_meter_power_ac no longer gets args pointing at entities that never exist. Also: drop the "(recommended)" Tesla Fleet Integration doc heading that contradicted the new Teslemetry-first recommendation, document that the five diagnostic control entities are transient mirrors of the emulator state, and de-duplicate a case-insensitive "teslemetry" entry in the cspell workspace dictionary that was making the file-contents-sorter pre-commit hook non-deterministic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…plate - Restore templates/tesla_powerwall.yaml to the original manual HA-integration config (service hooks) instead of repurposing it for the component - Add templates/teslemetry.yaml for the beta component path - Mark the Teslemetry component as beta / not-yet-recommended in components.md and inverter-setup.md - Add a Teslemetry component (beta) row to the inverter table - gitignore docs/superpowers/ planning artifacts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fault) Adds a MockBase, test_teslemetry_api() and argparse main() to teslemetry.py, matching the fox.py / gecloud.py CLI test pattern. Run with: python teslemetry.py --key <token> --site-id <id> By default the harness runs read-only: it polls the Powerwall and prints/ publishes all entities and status but sends no control commands (the scheduler emulator and crash-recovery writes are gated on set_read_only). Pass --control to allow commands to be sent. MockBase.get_arg consults args so the read-only default actually gates writes; covered by a unit test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The standalone test harness printed the run result and exited 0 even when run() returned False, so an invalid token (401) or unreachable API looked like a pass. test_teslemetry_api now returns a success bool - False on an auth failure or unsuccessful run, printing a clear FAILED message - and main() exits with that status. Regression test covers both outcomes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
site_id is now optional and acts as a filter over the sites discovered from GET /api/1/products rather than a hardcoded target. When omitted the account's sites are used; when given (string or list) it reduces the discovered set. For now a single site is supported - the first matching candidate is used and any others are logged (full multi-site is a follow-up). run() resolves the site before polling and retries discovery until it succeeds; the CLI --site-id is optional. Tests cover filter/first-match/no-match and run-time discovery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… data Fixes found running against a real Powerwall 3: - soc_max: source from live_status total_pack_energy (PW3 site_info omits nameplate_energy); decouple the site_info latch from soc_max so automatic config is no longer blocked, and gate auto-config on soc_max-present after the live poll (reordered in run()). - inverter_limit: max_site_meter_power_ac is often an 'unlimited' sentinel (e.g. 1e9); clamp absurd/absent values and fall back to nameplate_power. - soc: round to 2 decimal places. - tariff: there is no GET /tariff_rate endpoint (404); read the current tariff from site_info (tariff_content_v2) instead. - Log every raw API response (Info) so live output can be cross-checked against the component's parsing during the beta. Tests updated for the new site_info/tariff sourcing; adds soc-rounding, inverter-limit-sentinel, auto-config-from-live-soc_max and raw-response-log coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stimate Real PW3 (firmware 26.18.1) exposes no battery-capacity field anywhere - neither total_pack_energy/energy_left in live_status nor nameplate_energy in site_info - so soc_max was never published. Resolve capacity in priority order and prefer a real value over an estimate: 1. live_status total_pack_energy 2. live_status energy_left / percentage_charged 3. site_info nameplate_energy 4. site_info battery_count x ~13.5 kWh/unit (estimate, logged; overridable) A real value later upgrades a published estimate. Also read the current tariff from tariff_content (v1) as well as tariff_content_v2 - the observed device carries the real code under v1. On the user's logged data this now yields soc_max=13.5 and fires auto-config. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…kWh) When capacity must be estimated from battery_count, tell Powerwall 1 apart from Powerwall 2/3 by per-unit nameplate power (site nameplate_power / battery_count): a PW1's ~3.3 kW inverter is far below PW2 (~5 kW) / PW3 (~11.5 kW), so <=3500 W/unit -> 6.4 kWh, otherwise 13.5 kWh, scaled by battery_count. The gateway part_name is logged for confirmation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…daily totals The calendar_history?period=day response is large (~160 KB, ~200 five-minute buckets plus SmartBreakerEnergyLogs) yet we only use it to compute four daily kWh totals. energy_history?kind=energy&period=day aggregates to the same totals from far fewer buckets and without the breaker logs - the same endpoint the Home Assistant Teslemetry integration uses. Field names and summing are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ess often energy_history 404s on Teslemetry: the library's energy_history() is just a wrapper around calendar_history?kind=energy&period=day (same HTTP endpoint), so there is no smaller 'totals only' route. Revert to calendar_history (known good) and instead cut the repeated ~150 KB downloads by raising the energy poll interval from 5 to 15 minutes - the values are slow daily counters. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bug log - Revert ENERGY_POLL_SECONDS to 300: Predbat derives 5-minute energy buckets from the daily cumulative totals, so they must be refreshed every 5 minutes. - Summarise the raw-response log: the calendar_history time_series (and SmartBreakerEnergyLogs) and the site_info tariff_content/tariff_content_v2 blocks are replaced with a short placeholder (entry count / tariff code), so the log shows which endpoint returned what without dumping ~150 KB of history or a full ToU rate table (calendar_history 167 KB -> 194 B, site_info 29 KB -> 2.6 KB in practice). Small responses pass through unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-only works The MockBase.get_arg had reverted to returning the default, ignoring self.args. That made _is_read_only() read set_read_only as False in the standalone CLI harness, so the scheduler emulator issued device-write commands (operation, backup, grid_import_export, time_of_use_settings) even in the default READ-ONLY mode. Restore get_arg to consult self.args (keeping the state-printing set_arg); the existing guard test now passes and a read-only run sends zero control POSTs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
This replaces the original service-hook control path for the TeslemetryAPI component (
apps/predbat/teslemetry.py, Tesla Powerwall support over the Teslemetry / Tesla Fleet API) with a fox-style window control plane. Predbat now programs Tesla Powerwall exactly the way it programs a Fox inverter — through aTESLAinverter type and virtual schedule entities driven directly byinverter.py— with no HA service hooks anywhere in the control path.Window control plane
"TESLA"entry inINVERTER_DEF(apps/predbat/config.py). Notablysupport_discharge_freeze: False(the Powerwall has no discharge-freeze/hold state — see below),support_charge_freeze: True,output_charge_control: "none"(no rate control, on/off + target SOC only),time_button_press: True(writes are committed atomically rather than instantly on entity change), andcan_span_midnight: False.schedule_write) that commits a staged edit to the live schedule in one step.inverter.pyreads and writes these exactly as it does for a Fox inverter; the component itself never talks to the HA service-call layer.Scheduler emulator
The Powerwall has no native on-device scheduler to hand a plan to (unlike Fox Cloud, which stores its own windows). To emulate one,
run()evaluates the committed schedule + wall clock + live SOC every cycle (evaluate_schedule) into a single device-state tuple:normal/export_now)never/pv_only/battery_ok)self_consumption/autonomous/backup)That tuple is asserted onto the real device via
assert_device_state, in a fixed order (tariff first, mode last — the order proven safe by the original template). Each of the five writes is deduplicated: a signature of the last successfully confirmed value is cached per control, and a write is skipped unless the desired value has actually changed. This keeps Teslemetry command-credit spend flat under steady state. If a write fails, the cache is left stale (it's only updated on success), so the identical desired state is simply re-sent and retried on the next 5-minute cycle — no separate retry loop needed. All of this is gated onset_read_only, so a read-only Predbat instance never issues a device command.One behavioural change from the original template: the idle state (no active charge or discharge window) now sets export rule to
pv_onlyrather thannever. The old template'sneverwould curtail excess solar generation while idle;pv_onlylets PV export through while still blocking grid export.Persistence
The committed schedule is saved/loaded through the Storage component (
("teslemetry", "schedule")), so it survives an add-on restart mid-plan — the Powerwall can't be asked to hand the schedule back, so Predbat has to remember it.Automatic configuration
Setting
teslemetry_automatic: Truenow auto-wires the inverter args for you. End-user setup is down to threeapps.yamlkeys —teslemetry_key,teslemetry_site_id,teslemetry_automatic(an optional fourth,teslemetry_base_url, only needs setting for a non-default Fleet API endpoint).templates/tesla_powerwall.yamlhas been rewritten from the old manual HA-integration template to this component-based form, anddocs/components.md/docs/inverter-setup.mddocument the new setup and behaviour (including the export-freeze and idle-pv_onlynotes above).Why
support_discharge_freeze: FalseThe Powerwall cannot pin SOC while continuing to export PV — the hardware's
self_consumption/autonomousdischarge modes let backup reserve absorb solar rather than hold state at a fixed level, so there is no device mode that reproduces Predbat's discharge-freeze semantics. Rather than special-casing this in the Tesla component, theINVERTER_DEFflag is setFalseand the existing generic gate inexecute.py(used by any inverter type lacking the feature) auto-disablesset_export_freeze/set_export_freeze_onlywhen the first enabled inverter doesn't support it.Testing
tests/test_teslemetry.py: 78 tests (plus theunit_test.pyregistry entry point) covering the data path, transport robustness, control-entity handlers, tariff/export-trick building, boot reconciliation, write-on-change dedupe, the scheduler emulator, schedule entities + persistence, device-state assertion, andautomatic_configwiring../run_all --quick: all 131 test groups pass, includingteslemetryandmulti_car_iog_load_slots_regression(previously the one known pre-existing flake on this branch — passed clean on this run).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.Compatibility notes
inverter:dict keep their service-hook behaviour — the inline dict overlays the new built-inTESLAtype, and the generic*_serviceargs still exist; unset keys now default to theTESLAentry's values (behaviour-neutral in practice).set_read_onlyis enabled.🤖 Generated with Claude Code