Repository navigation
fix(fox): don't model Feed-in First on FoxCloud, which never selects it (#5015) - #5023
Closed
chalfontchubby wants to merge 1 commit into
Closed
chalfontchubby wants to merge 1 commit into
chalfontchubby wants to merge 1 commit into
Conversation
…it (#5015) FoxCloud declared support_feedin_first: True on the grounds that it is the same hardware as FoxESS, so the planner modelled its freeze export slots as a real "Feed-in First" mode - load, then export, then battery, with PV past the export limit recaptured into the battery rather than clipped. The Cloud path never actually selects that mode. It drives the inverter through the Fox API scheduler, which only ever emits ForceCharge and ForceDischarge groups (apply_battery_schedule), and adjust_inverter_mode pins the work mode to SelfUse for any has_fox_inverter_mode inverter. So a freeze export slot leaves the inverter in Self Use: PV covers the house load, then charges the battery, and only exports once the battery is full - exactly the Demand behaviour the reporter observes. Same silicon, different control surface: FoxESS (modbus) reaches the mode through a work-mode select, wired up by the discharge_freeze_service stanza in templates/fox.yaml. fox_cloud.yaml has no service stanzas at all. The planner was therefore picking freeze export slots on a premise the execute path could not deliver, inventing energy that never reached the battery. Turning the flag off makes the plan honest; those slots go back to being Demand slots, as they were before the flag was added. support_discharge_freeze stays True - freeze export itself is still attempted, it is only the Feed-in First recapture modelling that was wrong. Adding the mode to the Cloud path is tracked separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collaborator
Author
|
In theory not needed now since #5022 is merged. |
Owner
|
Yes, I fixed fox :) |
Collaborator
Author
|
A better solution. Figured I'd put the quick fix in in case. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
(Claude wrote this PR, posted under my account.)
Fixes #5015.
The bug
FoxCloud declares
support_feedin_first: True, justified inconfig.pyby a comment saying it is the same hardware as FoxESS. That flag tells the planner the inverter's freeze export is a real "Feed-in First" mode — load, then export, then battery — so PV past the export limit is recaptured into the battery rather than clipped (prediction.py:1146).The Cloud path never selects that mode:
apply_battery_scheduleonly ever emitsForceCharge/ForceDischargegroups —Feedinappears infox.pyonly in enum lists, never in a written scheduleadjust_inverter_modehardcodesnew_inverter_mode = "SelfUse"for anyhas_fox_inverter_modeinverter (inverter.py:2443)fox_cloud.yamlhas nodischarge_freeze_service(no*_servicestanzas at all), so the freeze hook finds nothing configured and no-opsSo the inverter sits in Self Use for the whole slot: PV covers house load, then charges the battery, and exports only once the battery is full — exactly the Demand behaviour the reporter observes.
Same silicon, different control surface. FoxESS (modbus) reaches the mode through
select.foxess_work_mode, wired up by thedischarge_freeze_servicestanza intemplates/fox.yaml.Why now
Two commits that are individually fine but mutually inconsistent:
support_feedin_first: Trueon FoxCloud came from 3ed9151, 24 Aug 2026The old hardcode was harmless until the recent planner change started relying on the mode it blocks. That matches the reporter's "until a few weeks ago freeze export slots were never planned".
Worth noting #4864 (GivTCP) is not implicated despite also shipping in v9.0.1 — it touched no Fox files, and the hardcode predates it by a year.
The change
One flag, plus the two tests that asserted the old value.
support_discharge_freezestaysTrue— freeze export is still attempted, it is only the Feed-in First recapture modelling that was wrong.The shared opt-in test's docstring claimed all five other types genuinely select the mode, so I checked before shrinking the set: they do (
solax.py:833callsset_default_work_mode(mode="feedin"), sunsynk/deye have dedicated Selling First freeze handling, solis.py has the feed-in priority bit). FoxCloud was the only false member.Effect for users
Freeze export slots on FoxCloud go back to being Demand slots, as they were before 24 Aug. That is the correct plan for what the inverter actually does, but it will look like a regression if unexplained — the plan stops promising something the hardware was never being told to do.
Adding the mode properly to the Cloud path is tracked in #5022, which records the implementation route I verified (the Cloud component does publish a writable work-mode select whose enum includes
Feedin; the blocker is thatadjust_inverter_modewould revert it).Testing
./run_all --test inverterpasses./run_all --quicksuite green (1015 passed, exit 0)Known follow-up, deliberately not in this PR
support_feedin_firstis a type-level constant asserting something that is really user config —call_service_templateresolvesdischarge_freeze_servicefromself.base.argswith no inverter-type gate anywhere. A modbus user who trimmed that stanza from theirapps.yamlhas this same silent mismatch today.Gating the flag on the service actually being configured would fix that, but it changes behaviour for all five remaining opt-in types on a path nobody has reported a problem with, and it needs a design call on when the gate is evaluated (apps.yaml changes at runtime; the kernel context carries the flag). Kept out of a targeted bug fix — happy to file it separately if you want it tracked.