Skip to content

Fix the half-known flag crash, and make diagnostics answer the question - #276

Merged
Poshy163 merged 4 commits into
CharlesGillanders:mainfrom
Poshy163:fix/half-restored-enable-intent
Aug 25, 2026
Merged

Poshy163 merged 4 commits into
CharlesGillanders:mainfrom
Poshy163:fix/half-restored-enable-intent

Conversation

@Poshy163

@Poshy163 Poshy163 commented Aug 24, 2026 •

Copy link
Copy Markdown
Collaborator

Three things out of the testing on #267.

1. Crash moving a schedule switch

Reported by @dragon2611 on main:

File "coordinator.py", line 1297, in stage_schedule_change
    grid_flag = int(draft["charge"].get("gridCharge", grid_flag))
TypeError: int() argument must be ... not 'NoneType'

Since #275 the entity view reports None for an enable flag nobody has answered for, and .get(key, default) returns that None rather than the default — the key is present, its value is unknown.

Reaching it needs exactly half an answer: one switch restored a usable state and the other did not. _committed_enable_flags() then returns None (it requires both), while Time Based Control Active still reads true from the side that is known — enough to enter the last-timer guard, which casts both.

The guard now runs only when both committed flags are actually known. Its or (1, 1) fallback was the same class of guess #275 removed everywhere else: assuming the other timer was on in order to decide whether this one was the last. A staged half of None is skipped rather than cast. A write still needs both flags, so a half-known pair is refused before it reaches the API.

2. Dead mappings left behind

The periodic patcher still carried "cycle": "gridChargeCycle" / "cycle_field": "gridCharge" entries. Nothing has read them since those fields stopped being part of the resource, and leaving a mapping there that names the write-only pair is an invitation to send them from a read again.

3. Diagnostics that answer the question

Every question on #267 needed something the download did not carry, so it took three rounds of hand-signed API requests to establish what the integration already had in memory.

Download diagnostics now includes, per inverter:

  • governing_store and periodic_read — which surface the system is on
  • capabilities — the exact flags behind every unavailable control, so a locked UI explains itself
  • enable_intent / enable_last_sent — the write-only pair, which nothing else can report
  • periodic_snapshot and legacy_snapshot — what each store actually held, including an empty period list or a stale window the app no longer shows
  • draft — what is staged, which fields are dirty, whether an Apply is in flight
  • duration cooldowns and consecutive poll errors

Plus a coordinator.api section with the current call spacing and whether the fast lane is still on — worth checking now that Predbat's native integration can share the same API account.

That one file would have answered the whole report on its own: the empty dischargeTimeList, the 0/0 pair with nothing recorded, and the capability flags explaining the locked controls. Serials stay anonymised as inverter_N and credentials stay redacted.

Two README gaps from the same testing are closed too: AlphaESS will store an empty period list even though it refuses to accept one, so a system with no discharge periods cannot be written to until it has one; and installs predating the serial-prefixed rename keep IDs carrying the device name, which is why the entity names in the automation section did not match for him.

Tests

573, 100% coverage, ruff clean. Regressions for the reported crash sequence and for the half-known pair still refusing the write, plus four covering the new diagnostics sections across periodic, backup, drafted and no-store systems.

dragon2611 hit a TypeError moving a switch on CharlesGillanders#267:

    grid_flag = int(draft["charge"].get("gridCharge", grid_flag))
    TypeError: int() argument must be ... not 'NoneType'

The entity view now reports None for an enable flag nobody has answered for,
and .get returns that None rather than the default, because the key is present.
Reaching it needs exactly half an answer: one switch restored a usable state and
the other did not, so _committed_enable_flags returns None while the entity view
still reports time-based control active from the side that is known, which is
enough to enter the last-timer guard.

The guard now runs only when both committed flags are actually known. Falling
back to (1, 1) was the same class of guess this branch removed everywhere else -
it claimed the other timer was on in order to decide whether this one was the
last. A staged half of None is likewise skipped rather than cast.

Nothing escapes as a result: a write still needs both flags, so a half-known
pair is refused before it reaches the API, with the message that names the two
switches.
@Poshy163
Poshy163 marked this pull request as ready for review August 24, 2026 11:33
@Poshy163

Copy link
Copy Markdown
Collaborator Author

@CharlesGillanders bump, need this for b4

The periodic patcher still carried "cycle"/"cycle_field" entries pointing at
gridChargeCycle and ctrDisCycle. Nothing reads them since those fields stopped
being part of the resource, and leaving a mapping there that names the write-only
pair is an invitation to send them from a read again.

Two gaps in the README from the same testing: AlphaESS will store an empty
period list even though it refuses to accept one, so a system with no discharge
periods cannot be written to at all until it has one; and installs predating the
serial-prefixed rename keep IDs carrying the device name, which is why the entity
names in the automation section did not match.
Every question asked on CharlesGillanders#267 needed something the download did not carry, so it
took three rounds of signed API requests written by hand to establish what the
integration already had in memory.

The download now includes, per inverter: which store governs and why, the
capability flags behind each unavailable control, the write-only enable pair
that nothing else can report, the periodic and legacy snapshots as they were
read, any open draft with its dirty fields and in-flight state, the duration
cooldowns, and the consecutive poll error count. Alongside it, how the client is
pacing calls and whether the fast lane is still on, which is what to look at when
something else shares the API account.

That one file would have answered the whole report: an empty dischargeTimeList,
a 0/0 enable pair with nothing recorded, and the capability flags explaining the
locked UI. Serials stay anonymised and credentials stay redacted.
@Poshy163 Poshy163 changed the title Don't guard on a flag pair we only half know Fix the half-known flag crash, and make diagnostics answer the question Aug 24, 2026
Working out what this API does has twice meant asking a user to write and sign
their own requests, because nothing in the integration reported what it sent or
what came back. Every call already goes through one gate, so that is where the
trace belongs.

With debug on, each request is written out as it would be typed, with its full
response, how long it took and how long it waited for the call spacing.
Rejections carry the return code, transport failures the exception, and a retry
after a 6053 says so. Nothing is rendered when debug is off, and the clock is not
even read then, so the cost is a level check per call. Long responses are capped
rather than dropped - one day of power data is thousands of samples.

Three additions around it: the write log now carries the enable pair it sent,
which is the field that decides the inverter's working mode and the one that was
missing when it mattered; staging logs each field and the resulting draft; and a
lock or unlock of the schedule surface is announced once, with the read state,
the timed-control answer and the recorded flags that decided it. A user reporting
that the controls vanished should be answerable from the log alone.

README says how to turn it on, and warns that the log carries the serial.
@CharlesGillanders

Copy link
Copy Markdown
Owner

@Poshy163 What permission changes need to be made so that you can run your own reviews? I'm a roadblock too often at the moment.

@Poshy163

Copy link
Copy Markdown
Collaborator Author

What permission changes need to be made so that you can run your own reviews

Not quite sure, as i think we looked into this before and we needed this repo to be apart of an org

the only way would be to (from what i understand, and i had to ask AI for this, coz i wasnt too sure how it applied to individuals

image

this by the looks gives my the bypass for the review, but the CI still needs to pass

@Poshy163
Poshy163 merged commit 9a0188b into CharlesGillanders:main Aug 25, 2026
3 checks passed
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.

2 participants