Move scheduling onto the periodic API, with staged edits and Apply - #274
Merged
CharlesGillanders merged 5 commits intoAug 21, 2026
Merged
Conversation
Two things from testing on CharlesGillanders#269. dragon2611 saw the discharge entry appear in the AlphaESS portal and do nothing, because it had no rate to run at. setTimeChargeBySn accepts a period with no chargePower and then ignores it. We only sent one when a previous periodic read had given us a value to carry over, which for a system with no existing periodic schedule is never. Fall back to the inverter's own rated power from getEssList. benbrown249 hit "charge period 13:30-02:45 overlaps discharge" on a system whose app schedule was empty. That window comes from the legacy store, which on a migrated system is a separate store the app doesn't show, and because it wraps midnight it spans thirteen hours and collides with anything. So every write was skipped. The overlap guard was written from the documentation, before the library could report return codes. It can now, so drop the guess and send the schedule: if the API objects with 6008 we say so, and name the periods we sent. That also removes a guard that could never have been right, since we don't know whether the API applies the overlap rule when a cycle is disabled.
Swept the rest of the endpoints for the two shapes behind the CharlesGillanders#269 reports: an optional field whose absence silently does nothing, and us deciding a call will fail instead of making it. chargePower was the only optional field with that behaviour, and it is now sent, so that one is closed. The EV control pre-check is the same mistake as the overlap guard, and worse. It refused to send start/stop unless the cached charger status matched our own idea of which states allow which command. That status is only as fresh as the last poll, which is configurable up to an hour, and if the status endpoint had failed there was no status at all, so every command was blocked indefinitely. Absorbing API errors on reads made that more likely. The command now always goes out and we report what the charger says. can_control_ev stays for the Can Start/Stop Charging sensors, where a hint is all it ever was. The two services also passed times through untouched. The API wants zero padded HH:mm on the quarter hour, so "9:00" comes back 6001 and an off-grid minute is accepted and then unusable. They are normalised now, the same way the time entities already did it.
Poshy163
force-pushed
the
fix/periodic-charge-discharge
branch
from
August 21, 2026 13:49
ba6acd1 to
d6d9c1a
Compare
Poshy163
force-pushed
the
fix/periodic-charge-discharge
branch
from
August 21, 2026 13:52
d6d9c1a to
ee0db64
Compare
CharlesGillanders#267 asks for the six-period endpoints to become the scheduling API and for the old two-slot endpoints to be retired. This does the first half and stops short of the second: getTimeChargeBySn/setTimeChargeBySn is now the store the integration runs on, and the legacy endpoints stay only as a backup for accounts AlphaESS has not granted the timed charge/discharge permission to. A system is in exactly one mode. A definitive 6017 on the periodic read moves that inverter to the backup, and from then on polling, entity state, writes and the duration buttons all follow the store that mode names. CharlesGillanders#269 showed what mixing them costs: values the app never shows, phantom overlaps, and writes the server accepts without acting on. The two stores are never read together, merged, or mirrored. A system whose periodic read works but whose write is refused fails closed instead of falling back, because the periodic store is still the one governing that inverter. Entity edits are drafts now. Times, cutoff SOCs, per-period power and the enable switches stage locally and go out together when Apply Charge/Discharge Schedule is pressed, so a plan change is one write rather than one per field and a half-finished edit is never sent. Discard puts the remote values back. Both buttons exist only while a draft does. The duration buttons and the two services stay immediate and deliberately do not consume a draft. Every write reads the store fresh inside the transaction and patches only the fields it was asked for, so periods, weekdays and powers the entity UI cannot show survive it. A draft remembers the schedule it was based on and is refused as a conflict if the remote schedule moved underneath it. Nothing is invented to fill a gap. A period with no valid power or cutoff is reported by name instead of being filled in from the inverter's nominal rating, which is not a battery rate: the fallback added in 9a8d61d is reversed here. Empty period lists are refused before they reach the API, which answers 6001 for them anyway. Self Consumption and SCP are indistinguishable over the OpenAPI, since both clear the enable flags and keep the windows. Field testing showed that writing an enable flag does not leave those modes, so while one runs every time-based control locks, the new Time Based Control Active sensor says why, and Home Assistant refuses to author the both-flags-off state itself, having no way to tell it apart from self-consumption afterwards. Polling unlocks everything once the app returns to a timed mode. Periodic Schedule Read reports which store governs the system, and stays unknown rather than guessing while no read has succeeded.
The README described a single charge/discharge surface that no longer exists. It now says which store governs a system and why, what a draft is, what Apply and Discard do, and which limits are the API's rather than ours: both period lists are required in a replacement, a periodic schedule cannot be cleared through the API, per-period power exists only in the periodic store, and 6008 is only ever reported as a generic failure. There is also a section on driving schedules from an automation, prompted by dragon2611 asking in CharlesGillanders#267 for a way to set the parameters and then apply them. That already exists — the entities stage, button.press applies — so it is written down, with the entity IDs and the two rules that matter: pick one surface per inverter, and expect the immediate services to rewrite both slots of their side. Predbat gets its own worked example, since it already knows how to drive an inverter that stages settings and then presses a button: a custom inverter definition with time_button_press, the entity mapping, and the five things that setup depends on, including that Predbat cannot see whether an Apply was accepted. A staged time also stops disappearing when it briefly matches the other half of its slot. Equal start and end is how AlphaESS stores an unused slot, so that pair reads as unset, but an automation writing one half and reading it back before writing the other would see its own edit blanked.
Poshy163
force-pushed
the
fix/periodic-charge-discharge
branch
from
August 21, 2026 13:55
ee0db64 to
3e0fd17
Compare
The 100% gate had never run over this work, because none of it was committed. It was at 96.87%, and everything missing was an error path: the legacy backup write in all of its outcomes (a side that cannot be read, one missing fields, one that already matches, a store that moved under a draft, a half-landed two-sided write, and the same again when a lost response means the outcome is unknown rather than failed), the Reset button's rate limit and its three failure endings, and the guards that only fire in narrow states — staging a power in backup mode, refusing to disable the last enabled timer from a service, a quick button with no cutoff to copy, an alt-mode poll caching the backup snapshot, and cancellation passing through a write untouched. Those are exactly the paths worth pinning: each one exists because a field report showed what happens without it.
Poshy163
marked this pull request as ready for review
August 21, 2026 14:10
Collaborator
Author
|
@CharlesGillanders would be good if you can approve this, going to push it as a beta 2 with reworks most of how the integration works for the new server interface/back-end |
CharlesGillanders
approved these changes
Aug 21, 2026
This was referenced Aug 23, 2026
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.
Takes on the first half of #267:
getTimeChargeBySn/setTimeChargeBySnbecomes the store the integration schedules on. It stops short of the second half — the legacy two-slot endpoints are not deleted, they are demoted to a backup for accounts AlphaESS has not granted the timed charge/discharge permission to. Until that permission is handed out on request, it is the only control path those accounts have.This supersedes the description this PR carried before, which described the first two commits only and one behaviour that has since been reversed (see Nothing is invented below).
One store per system
A definitive
6017on the periodic read moves that inverter to the legacy backup. From then on polling, entity state, writes and the duration buttons all follow the store that mode names. The two stores are never read together, merged, or mirrored — #269 showed what mixing them costs: values the app never shows, phantom overlaps, and writes the server accepts without acting on.A system whose periodic read works but whose write is refused fails closed rather than falling back. The app-facing periodic store still governs that inverter, and editing a store it ignores would only mislead.
Two backup-mode caveats, both from live probing: the inverter may not act on the legacy discharge store even though the write is accepted, and per-period power exists only in the periodic store, so the Power entities are unavailable there.
Edits are staged, Apply sends them
Times, cutoff SOCs, per-period power and the enable switches now stage locally. Apply Charge/Discharge Schedule sends the whole draft as one write; Discard Schedule Changes puts the remote values back. Both buttons exist only while a draft does.
That makes a plan change one API write instead of one per field, and means a half-finished edit is never sent. Polling keeps a draft visible instead of resetting an end time while the start is still being edited. A failed or conflicted Apply keeps the draft so it can be retried or discarded.
The duration buttons and the two services stay immediate, and deliberately do not consume a draft.
Safe writes
Every write reads its store fresh inside the transaction, patches only the fields it was asked for, and serialises per inverter. Periods, weekday selections and powers the two-slot entity UI cannot show all survive a write. A draft records the schedule it was based on and is refused as a conflict if the remote schedule moved underneath it.
In periodic mode there is no partial-write case — one store, one POST. Backup mode writes two stores, so a two-sided change there can half-land, and that is reported as a partial write naming what already applied. A timeout after a POST is reported as an explicitly unknown outcome, not as a failure.
Nothing is invented
A period with no valid power or cutoff is reported by name rather than filled in. In particular the
poinvfallback added in 9a8d61d is reversed: the inverter's nominal rating is not a verified battery charge rate, and sending it as one is exactly the class of write that looks accepted and does something unintended. Empty period lists are refused before they reach the API, which answers6001for them anyway. Invalid remote data is named and blocks the write instead of being clamped.The overlap guard stays gone.
6008is reported with the API's own explanation and the periods that were sent.Self-consumption locks the controls
Self Consumption and SCP are indistinguishable over the OpenAPI: both clear the enable flags and keep the windows stored. Field testing showed that writing an enable flag does not leave those modes, so while one is running every time-based control — times, cutoffs, powers, switches, duration buttons, Reset, Apply/Discard and both services — is unavailable or refused with a message naming the remedy, and the new Time Based Control Active binary sensor says why.
For the same reason Home Assistant will not author the both-flags-off state itself: afterwards it could not tell that state apart from self-consumption. Disabling the last timer belongs in the app. Polling unlocks everything automatically once a timed mode is active again, and a draft stranded by an app-side mode change is kept until then.
Automations
dragon2611 asked in #267 for a way to set the parameters and then apply them. That is what the entities plus
button.pressalready do, so the README now documents it: the entity IDs, the choice between the immediate services and the staged surface, and the rule that an inverter should use one or the other — a service write replaces the remote schedule, so a draft staged before it will fail its next Apply as a conflict.Predbat gets a worked example, because it already drives inverters that stage settings and then press a button: a custom inverter definition with
time_button_press, the entity mapping, and the things that setup depends on — including that Predbat cannot see whether an Apply was accepted, since Home Assistant timestamps a button press before the write is attempted.Also
Periodic Schedule Readdiagnostic sensor reports which store governs the system, and staysunknownrather than guessing while no read has succeeded.6001for9:00).