Skip to content

feat: add battery control and configuration services - #5

Merged
lowsbarrel merged 2 commits into
mainfrom
feat/battery-control-services
Sep 2, 2026
Merged

feat: add battery control and configuration services#5
lowsbarrel merged 2 commits into
mainfrom
feat/battery-control-services

Conversation

@lowsbarrel

Copy link
Copy Markdown
Owner

Six services, in two groups.

Configurationset_tou_schedule writes the tariff calendar, set_operating_mode changes mode and optionally that mode's reserve. Both go through SwitchModeRequest, so neither can reset a field by omitting it.

Battery controlforce_charge, force_discharge, hold_battery, clear_override. The API has no forced-charge endpoint, so these move the reserve state-of-charge floor and let the device track it, written with onlySave so the operating mode is never disturbed.

Notably this avoids the synthetic-tariff-window approach the other integration uses. One lever, no fake user settings left behind, nothing to disentangle from a real schedule.

The revert is the hard part

An override is a promise to put settings back, held by a process that can be restarted at any moment. So the baseline goes to disk before the first write. On startup an expired override reverts immediately; a live one has its timer re-armed.

Verified on real hardware:

force_charge to 55% for 30 min   reserve 15% -> 55%, override=charge
docker restart epcube-ha         override=charge, timer re-armed
clear_override                   reserve 55% -> 15%   RESTORED

What live testing found

allowChargingXiaGrid is read-only on this firmware. Four payload shapes — onlySave 0 and 1, string and integer, and the correctly-spelled key alongside the API's own misspelling — were all accepted with HTTP 200 and none applied. The reserve written in the same payload took effect immediately, so it is that one field being refused, not the write failing.

Two consequences:

  • The overrides do not touch it, and force_charge loses the allow_grid_charging parameter it could never have honoured.
  • The existing grid-charging switch was a lie — it appeared to toggle and silently did nothing. It now reads the value back and raises with an explanation if the device refused. Where the field is writable it behaves normally.

Documented in docs/api-endpoints.md with the evidence.

Also

A diagnostic sensor reports which override is running and when it ends. Without it an override is invisible beyond a reserve slider moving on its own.

Coverage stays at 100%.

Six services, in two groups.

Configuration: set_tou_schedule writes the tariff calendar, set_operating_mode
changes mode and optionally that mode's reserve. Both go through
SwitchModeRequest, so neither can reset a field by omitting it.

Battery control: force_charge, force_discharge, hold_battery and
clear_override. The API has no forced-charge endpoint, so these move the
reserve state-of-charge floor and let the device track it, written with
onlySave so the operating mode is never disturbed.

The revert is the hard part, and it is what override.py is really about. An
override is a promise to put settings back, held by a process that can be
restarted at any moment, so the baseline is written to disk before the first
write. On startup an expired override reverts immediately and a live one has
its timer re-armed. Verified against real hardware: a force_charge survived a
full Home Assistant restart and still reverted cleanly.

Testing on the live system also found that allowChargingXiaGrid is read-only
on this firmware. Four payload shapes (onlySave 0 and 1, string and integer,
and the correctly-spelled key alongside the API's misspelling) were all
accepted with HTTP 200 and none applied, while the reserve written in the same
payload took effect immediately. So:

- the overrides do not touch that field, and force_charge loses the
  allow_grid_charging parameter it could not have honoured
- the existing grid-charging switch, which until now appeared to work and
  silently did nothing, reads the value back and raises if the device refused

A diagnostic sensor reports which override is running and when it ends;
without it an override is invisible beyond a reserve slider moving on its own.
hassfest rejected two things: a service cannot carry a target device filter
(it needs a device selector field, which the schemas already required), and a
translation placeholder must not sit inside single quotes.

Also switches uv sync and uv run from --frozen to --locked. --frozen only
promises to use the lockfile; --locked additionally fails when the lockfile has
drifted from pyproject.toml, so a dependency added without re-locking is caught
in CI rather than resolving differently elsewhere.
@lowsbarrel
lowsbarrel merged commit f643667 into main Sep 2, 2026
4 checks passed
@lowsbarrel
lowsbarrel deleted the feat/battery-control-services branch September 2, 2026 10:27
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