Skip to content

Fox: fall back to v2/v3 scheduler API for EVO series (v1 returns errno 41200 permanently)#4181

Open
mgazza wants to merge 2 commits into
mainfrom
fix/fox-evo-scheduler-v3-fallback
Open

Fox: fall back to v2/v3 scheduler API for EVO series (v1 returns errno 41200 permanently)#4181
mgazza wants to merge 2 commits into
mainfrom
fix/fox-evo-scheduler-v3-fallback

Conversation

@mgazza

@mgazza mgazza commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

Fox ESS EVO-series inverters (productType 812) cannot be controlled via the v1 scheduler OpenAPI: /op/v1/device/scheduler/get and /op/v1/device/scheduler/get/flag return errno 41200 ("Failed to load data") permanently — not transiently — even though the devices fully support the scheduler.

Verified live (2026-07-04) against real hardware:

Endpoint EVO 10-5-H KH10 / KH8
/op/v1/device/scheduler/get errno 41200, persistent across 24h+ errno 0 ✓
/op/v0/device/scheduler/get/flag errno 0 {enable:true, support:true} errno 0 ✓
/op/v2/device/scheduler/get errno 0, full schedule ✓ errno 0 ✓
/op/v3/device/scheduler/get errno 0, full schedule ✓ errno 0 ✓

Fox's own device-detail reports function.scheduler: true for the EVO. The foxesscloud reference library migrated get_schedule()/set_schedule() to the v3 interface immediately after adding EVO recognition (2.9.1 → 2.9.2).

Because 41200 is (correctly) treated as retryable, each poll against an EVO burns FOX_RETRIES attempts with up-to-30s sleeps before failing — so beyond never fetching a schedule, it also wastes minutes per cycle and inflates the account's API-call count.

Changes

  • get_scheduler(): on v1 failure, fall back to /op/v2/device/scheduler/get and flatten each group's extraParam back to the flat v1 shape, so downstream consumers (compute_schedule, schedules_are_equal, HA publishing) are unchanged. v2 has no properties block, so the existing fdPwr (8000, capacity-capped) and fdSoc (10) defaults apply.
  • set_scheduler(): on v1 failure, fall back to /op/v3/device/scheduler/enable, converting groups via new groups_to_v3() — SOC/power fields nested inside extraParam, only enabled groups carried (matching the request shape the foxesscloud library sends).
  • Devices that needed the fallback are marked in scheduler_api_v2 and skip v1 thereafter, avoiding the retry/sleep cost on every poll.
  • No behaviour change for KH-series or any device where v1 works — v1 remains the first choice and the fallback only engages when it fails.
  • set_scheduler_enabled() (v1 set/flag) is left as-is: there is no v2/v3 flag endpoint, and the foxesscloud library also still uses the v1 flag write, which suggests v1 writes work on EVO (only reads are broken). If that turns out false we'll need a follow-up.

Tests

Three new tests in tests/test_fox_api.py (written first, watched fail, then pass):

  • test_api_get_scheduler_v2_fallback — v1 fails → v2 used, extraParam flattened, defaults applied
  • test_api_get_scheduler_v2_sticky — second poll and subsequent writes skip v1 entirely
  • test_api_set_scheduler_v3_fallback — v1 enable fails → v3 request carries extraParam-nested, enabled-only groups

--test fox_api and --test fox_oauth suites pass; pre-commit (ruff, black, cspell, sorters) clean.

Impact

Two PredBat.com customers with EVO 10-5-H units are currently blocked at onboarding by this (their validation probe has been fixed separately); this PR is required for actual charge/discharge control of EVO-series devices.

🤖 Generated with Claude Code

mgazza and others added 2 commits July 4, 2026 09:58
Fox's v1 scheduler OpenAPI endpoints return errno 41200 permanently for
EVO-series inverters (productType 812) even though the devices fully
support the scheduler — verified live against an EVO 10-5-H (v1 fails,
v0/v2/v3 succeed) and two KH-series controls. Each failed v1 call costs
FOX_RETRIES attempts with long sleeps, so EVO devices also mark
themselves via scheduler_api_v2 and skip v1 thereafter.

- get_scheduler: fall back to /op/v2/device/scheduler/get and flatten
  each group's extraParam back to the flat v1 shape (v2 has no
  properties block, so the existing fdPwr/fdSoc defaults apply)
- set_scheduler: fall back to /op/v3/device/scheduler/enable, nesting
  SOC/power fields inside extraParam and carrying only enabled groups
  (matching the request shape the foxesscloud reference library sends)
- KH-series and other devices where v1 works see no behaviour change

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to the v2/v3 scheduler fallback addressing an xhigh code review.

The previous approach routed a device to the v2/v3 scheduler API whenever a
v1 call returned None, and made that sticky. But errno 41200 is ALSO Fox's
shared transient rate-limit/comms code, so a single network blip on a healthy
KH-series device permanently rerouted it to the property-less v2/v3 path — a
real regression. It also never marked the device from the write path (so EVO
writes re-probed the failing v1 endpoint, stalling the loop for up to an hour),
and the flag was lost across restarts.

Replace failure-based detection with deterministic productType detection:

- FOX_V2_SCHEDULER_PRODUCT_TYPES = {"812"} (verified EVO 10-5-H code); a new
  uses_v2_scheduler() helper reads device_detail productType
- get_scheduler / set_scheduler route by productType, so KH stays 100% on v1
  (zero regression) and EVO uses v2/v3 directly (no per-cycle v1 stall)
- drop the runtime scheduler_api_v2 sticky flag entirely (productType is
  re-derived each fetch, so it survives restarts and never mis-fires)
- harden get_scheduler_v2: null-safe groups, default per-group and top-level
  enable so an active EVO schedule is not mis-read as disabled/empty

Tests: EVO uses v2/v3 directly; KH stays v1 on failure (regression guards);
null/absent-enable v2 shapes; EVO v2 also-fails returns None without crashing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mgazza mgazza requested a review from springfall2008 July 4, 2026 10:26
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