Add TeslemetryAPI component — Tesla Powerwall control via Teslemetry/Fleet API#4177
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>
… 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>
… tariff Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address Task 1 review: BOOST_TIER must be a Tesla-accepted label (ON_PEAK), and the agile quantise test now uses >3 distinct post-round prices so it drives the equal-width else banding branch, asserting exact band means. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… synced separately
Add test_teslemetry_boot_resumes_from_persisted_schedule (persisted mid-discharge schedule + empty dedupe cache drives autonomous/battery_ok on the first cycle) and test_teslemetry_boost_price_floor_wins_on_low_rates (the static EXPORT_SELL_RATE floor wins when 2x the highest real band is below it). Drop a tautological "or True" and a truncated "reconcil" substring check in the boot test, delete a dead re-assignment in _side_layout (the preceding loop already covers it), and correct the _rate_base get_arg docstring to describe its actual behaviour (a keyword name mismatch means it always returns None, not the caller's default). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ap sell assert; flat-fallback comment
…mport_export POST Both fields share the grid_import_export endpoint, but set_export_rule and set_grid_charging POSTed it separately, so a cycle that changed both (notably entering an export window: pv_only+grid-on -> battery_ok+grid-off) made two calls to the same endpoint. assert_device_state now writes both fields in a single set_grid_import_export() call, halving that transition's API-call cost against Teslemetry's monthly quota. Deduped on the (export_rule, grid_charging) pair via the same per-field cache keys the single-field setters use, so the manual select/switch overrides stay coherent (drift on either is still corrected next cycle). The individual setters remain for those manual events. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolved conflicts (both sides added new entries; kept both): - apps/predbat/config.py: INVERTER_DEF gained both TESLA and EnphaseCloud; APPS_SCHEMA gained both teslemetry_* and enphase_* keys. - docs/components.md: kept both the Teslemetry and Enphase component sections (TOC + body). Also corrected the now-stale Teslemetry note: four control entities (tariff_mode removed), tariff now publishes the customer's real rates. teslemetry.py/test_teslemetry.py had no conflicts. Teslemetry and Enphase test suites both pass on the merged result. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
site_id is a filter over the sites discovered from the account (empty = use the first account site) - initialize() defaults it to "", APPS_SCHEMA does not set empty:False, and the CLI --site-id defaults to None. components.md wrongly showed it Required=Yes; corrected to No, and added 'optional' notes to the inverter-setup quick-start and the teslemetry.yaml template. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new TeslemetryAPI component to integrate Tesla Powerwall control via Teslemetry/Tesla Fleet API, aligning it with Predbat’s existing “Fox-style” inverter window control plane (virtual schedule entities + inverter-driven programming), plus documentation/templates and a large unit test suite.
Changes:
- Introduces
apps/predbat/teslemetry.pyimplementing polling, schedule persistence, tariff syncing, and a scheduler-emulator control loop. - Registers a new
TESLAinverter type and wires the new component into the component registry + config schema. - Adds a comprehensive
tests/test_teslemetry.pysuite and updates docs/templates for the new beta setup path.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| templates/teslemetry.yaml | New apps.yaml template for the Teslemetry component configuration. |
| docs/inverter-setup.md | Documents the new “Teslemetry component (beta)” setup path for Powerwall. |
| docs/components.md | Adds component documentation for Teslemetry. |
| apps/predbat/unit_test.py | Registers the new teslemetry test group. |
| apps/predbat/tests/test_teslemetry.py | Adds extensive unit tests covering the new component’s behavior. |
| apps/predbat/teslemetry.py | Implements the Teslemetry Powerwall component, control-plane entities, schedule persistence, and emulator. |
| apps/predbat/config.py | Adds TESLA to INVERTER_DEF and new Teslemetry config keys to APPS_SCHEMA. |
| apps/predbat/components.py | Registers the teslemetry component and its args. |
| .gitignore | Ignores docs/superpowers/ artifacts. |
| .cspell/custom-dictionary-workspace.txt | Adds new accepted words (“dedupe”, “preseed”, etc.). |
| async def set_backup_reserve(self, percent, force=False): | ||
| """Set the backup reserve percentage (cloud caps at 80; 80-100 treated as 100 by Predbat), deduped on write-on-change.""" | ||
| percent = int(percent) | ||
| return await self._apply_command("backup_reserve", percent, lambda: self._command("backup", {"backup_reserve_percent": percent}), force=force) |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…driven) Adds an 'oauth' auth_method alongside the default static-token 'api_key' mode, reusing OAuthMixin (as Fox/Kraken/Solis do): token refresh is owned by predbat.com's oauth-refresh edge function, so the component only holds a short-lived access token, refreshes it before each call, swaps the bearer, and retries once on 401. Energy-site commands need no vehicle command signing, so the tariff/control layer is unchanged. - TeslemetryAPI(ComponentBase, OAuthMixin); initialize() calls _init_oauth(...,'tesla') - _bearer_token() picks access_token (oauth) vs static key (api_key) - _request: refresh-before-call, bearer swap, handle_oauth_401 -> single retry - config: teslemetry_auth_method/_token_expires_at/_token_hash (schema + components args) - docs + template note that oauth is predbat.com-provisioned - 5 tests: bearer selection, _init_oauth state, refresh-skip, 401 refresh+retry, api_key no-refresh Inert until predbat.com's oauth-refresh edge function registers a 'tesla' provider. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…0, no cap) Addresses a Copilot PR finding. The old docstring implied the /backup endpoint caps at 80 and would reject higher requests, which — via evaluate_schedule sending reserve=charge target (up to 100) — reads as a persistent failure/retry loop risk. It isn't: the endpoint accepts the full 0-100 range (confirmed by Home Assistant's tesla_fleet integration, which exposes backup_reserve_percent as a 0-100 number over the same api.backup call), and the Powerwall snaps >80% to 100% rather than rejecting. Value is forwarded as-is (per maintainer decision); docstring corrected to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…default (100) The /backup endpoint accepts the full 0-100 reserve range (confirmed via HA's tesla_fleet integration), so the conservative inverter_reserve_max=80 override in automatic_config is no longer warranted. Removing it lets Predbat's reserve planning use its default max of 100 (inverter.py get_arg fallback). Test updated to assert the override is no longer set. Co-Authored-By: Claude Opus 4.8 (1M context) <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