Skip to content

fix(S2): the remaining findings (2 of 2) - #279

Open
Flix6x wants to merge 98 commits into
mainfrom
dev/s2-frbc-complete
Open

Flix6x wants to merge 98 commits into
mainfrom
dev/s2-frbc-complete

Conversation

@Flix6x

@Flix6x Flix6x commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

The remaining findings. This is the second of two PRs that finish the S2 FRBC Customer Energy Manager. PR 1 (#278) is merged (squashed); this branch has main merged in, so the diff shows only this PR's own changes. dev/s2-frbc-p1 is restored, so #278's commit links still resolve. Do not delete dev/s2-frbc-p1 in the same step, or this PR closes.

What this is

19 commits, 19 files, each from an internal PR into dev/s2-frbc-complete (#271 to #277). Every one of them had:

  • an adversarial review;
  • a mutation run;
  • full coverage of its added lines;
  • green CI.

What it changes:

Please merge with a merge commit, as for PR 1.

Evidence

Both gates were re-run on the final code (ea0b1d03; #277 after it only changes when a warning fires):

  • Demo gate: clean. The resource manager received 45 instructions and 40 revocations, with no errors on either side.
  • Simulation gate (the same private two-home co-simulation, one day):
    • Baseline: identical to the earlier gate. All 8 steps compliant, and breach and peak unchanged to the third decimal.
    • Steered: breach energy and peak consumption identical to the earlier gate. No tracebacks.

Retracted: the cost trade-off below was noise, not an effect of #271. I re-ran the steered one-day co-simulation with a deterministic solver setting (1% MIP gap, a 2000-node limit, no time limit), on three versions: #278's head, #271 alone on top of it, and this PR's head. A repeat of the first checks for noise.

Run Degree-hours outside the comfort range, per home Mean indoor temperature (°C) Self-consumption Energy cost (EUR)
#278's head 0 / 0 20.48 / 21.36 73% / 70% 11.01 / 11.29
#278's head, repeated 0 / 0 20.37 / 21.46 64% / 100% 9.76 / 11.52
#271 alone 0 / 0 20.46 / 21.43 71% / 75% 10.39 / 11.82
This PR's head 0 / 0 20.30 / 21.33 100% / 36% 10.35 / 9.32
  • No run leaves the comfort range.
  • The repeat differs from its original as much as the versions differ from each other. So, even with a deterministic solver, the co-simulation's timing (which storage status triggers a plan, how the steering rounds interleave) moves cost and self-consumption by tens of percent per home.
  • Breach and peak are the same in every run.
  • I see no effect of fix(FRBC): the declared fill-level range reaches the server, as a band, with or without sensors #271 on comfort, cost or feed-in that one-day runs can resolve.

One change for you to judge. In the steered run, PV feed-in disappeared: self-consumption rose to 100%, and energy cost rose by roughly 20% to 30% per home.

Added after review of #278

Merged into this branch, in answer to the review comments on #278:

Finding → commit → file map

PR 2 is ad9a12ba..ea0b1d03, "the remaining findings" (14 non-merge commits).

Finding Package Commit(s) Files
B0 declared fill-level range no longer reaches the server P2.1 309e283 fix(FRBC): the declared fill-level range reaches the server, as a band, with or without sensors; 9a1ddf6 fix(FRBC): what the review of the declared band found; fe65dcd test(FRBC): an empty profile covers no step s2/control_types/FRBC/frbc_simple.py, CL-S2
B11 negative soc-min refused, widening works one way only P2.1 309e283; 9a1ddf6 (read-back from zero) s2/control_types/FRBC/frbc_simple.py, CL-S2
B12 negative usage forecast becomes an unrelaxable stock gain P2.1 309e283 (recorded as no usage, steps counted in a warning); 9a1ddf6 (read-back matches) s2/control_types/FRBC/frbc_simple.py, CL-S2
B4 flex model offers abnormal-condition-only modes P2.2 c34155a fix(FRBC): the flex model offers only what the actuator may use, and bands only where they say something; f8c46b1 fix(FRBC): a mode for abnormal conditions does not set the charging efficiency either s2/control_types/FRBC/frbc_simple.py, s2/control_types/FRBC/utils.py, s2/control_types/FRBC/init.py, CL-S2
B14 operation-modes sent where bands say nothing extra (LP became MILP) P2.2 c34155a s2/control_types/FRBC/frbc_simple.py, s2/control_types/FRBC/utils.py, CL-S2
B6 retry misses JobTimeoutError and ConnectionError P2.3 f32b44d fix(FRBC): a failure is answered or said out loud; 1eb62eb fix(FRBC): what the review of "a failure is answered or said out loud" found s2/control_types/FRBC/frbc_simple.py, s2/control_types/FRBC/init.py, s2/utils.py, CL-S2, CL
B7 state-of-charge post can cancel the planning round P2.3 f32b44d s2/control_types/FRBC/frbc_simple.py
B8 swallowed ingestion timeout remembered as recorded efficiency P2.3 7c45746 feat(client): post_sensor_data(confirm_ingestion=True) raises where ingestion is unconfirmed; f32b44d client.py, s2/control_types/FRBC/init.py, s2/control_types/FRBC/frbc_simple.py
B9 retry inert against a server older than 0.33.0 P2.3 f32b44d; 1eb62eb (4xx not retried, version lookup caught) s2/control_types/FRBC/frbc_simple.py
D several actuators answered INVALID_CONTENT; timers and transitions warned; provides_* flags honoured P2.3 f32b44d; 1eb62eb (usage sent despite a false flag is used; timer warned once) s2/control_types/FRBC/frbc_simple.py, s2/control_types/FRBC/init.py, s2/utils.py
C5 full history keeps the oldest entries (popitem) P2.4 2557896 fix(S2): housekeeping, and tests that pin what they claim to; ab2b0dc fix(S2): a message set again is the latest; what the review of housekeeping found s2/utils.py, CL-S2
C6 actuator status IndexError before any description P2.4 2557896 s2/control_types/FRBC/frbc_simple.py
D band posts unprotected (two posts) P2.4 e2d5e8d fix(FRBC): guard the display-only band posts; say a negative top needs no head room of its own s2/control_types/FRBC/frbc_simple.py, CL-S2
D hard_bound_factor assumes a non-negative fill level P2.4 e2d5e8d (left multiplicative; mutation showed no difference, documented) s2/control_types/FRBC/frbc_simple.py
D fill-rate post unguarded (companion to D9) P2.4 ea0b1d0 fix(FRBC): comments that later changes made false, and the unguarded fill-rate post s2/control_types/FRBC/frbc_simple.py
F four hollow tests, plus the untested 0.33 job id, upload wait, default of five attempts, fill_level_scale in bounds P2.4 2557896 (test-only); ab2b0dc tests only (client ingestion, retry, bounds)
E1 two Added lists in one Unreleased section P1.6 (landed in this range) fa18fdd docs(changelog): say once what the S2 integration ships since v0.9.0 CL-S2
E3 stale fallback-scheduler claim P1.6 (landed in this range) fa18fdd (changelog fallback entry); ea0b1d0 (warning text in code; the exact comment at the old line :755 not separately confirmed) CL-S2, s2/control_types/FRBC/frbc_simple.py

Rows: 18.

PR 2 commits that address no numbered finding

Review fixes and sweep:

  • f15b9e2 fix(S2): what the sweep of the whole branch found (comment line breaks; ensure_site falls back on a non-asset answer; s2/site.py, FRBC/init.py, frbc_simple, utils)

Everything else in the range is attributed above.
fe65dcd (test-only) is attributed to B0 as a test of the band built from a target profile.

Numbered findings in neither range, from the PR 2 viewpoint

  • B1 is not in PR 2: the plan put it in P2.2, but 5ff631d (PR 1) fixed it; c34155a says so.
  • D5: in PR 1 (fix(example): a heat pump with a buffer that is planned for, instructed, and simulated #259).
  • D UniqueViolation retry: already a marker on main, untouched by either range.
  • D transitions, timers, several actuators, provides_* flags as supported features: out of scope per plan section 9; only made loud.
  • Plan section 9 out of scope: TUNES own fill-rate and unit problems (a few TUNES fixes landed in PR 1 anyway), per-commodity power ranges, the parked belief-key collision.
  • F "two tests in test_frbc_registry reimplement the flow" and the mocked 202 payload shape: no plan package, no commit; unplaced.
  • The plan's gates (G1 to G9) and the section 5 FlexMeasures-side asks (B1, B11, B12, B14): not client code, so no commit.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW

Flix6x and others added 30 commits November 5, 2026 13:10
FillRateBasedControlTUNES.send_leakage_behaviour read its fill-level scale from
self._fill_level_scalefill_level_scale, an attribute name pasted in twice, which
has been there since #158. The AttributeError that raised was caught by the
handler's broad except as if the post had failed, so every FRBC.LeakageBehaviour
was answered with PERMANENT_ERROR and none was ever recorded.

mypy could not see it: the type-check task only looks at files that contain
`from typing import`, and the FRBC base class is not one of them, so the
attribute lookup went to Any. The next commit widens that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
The type-check task fed mypy only the files that contain `from typing import`,
which were seven of eighteen, and left out frbc_simple.py, both control-type
__init__.py files and the example scripts. With --follow-imports skip, anything
those files defined then looked like Any to the files that were checked, which
is how the previous commit's misspelt attribute went unnoticed since #158.

It now checks all of src, and with --check-untyped-defs, so the bodies of
functions without annotations are checked too. What that surfaced, besides the
attribute fixed in the previous commit:

- get_access_token stored a token whose type could be None and then used it as a
  header value; it now returns the token, so the caller holds a str.
- get_access_token and get_versions indexed a response typed as a dict or a list;
  each now says so plainly when the server answers with something other than an
  object, instead of failing on the lookup.
- The S2 translations annotated their resolution as a string while frbc_simple
  passes a timedelta, which pandas accepts as readily; the annotation now says so.
- FillRateBasedControlTUNES overrode trigger_schedule without the arguments the
  base class declares. It calls its own override without them and ignores them,
  so it now accepts and ignores them.
- The example server indexed an account that can be None and spread a dict of
  mixed values into add_asset; it now says so when the demo user has no
  organisation, and types the dict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
None of the S2 integration had a coverage number, for two reasons that each hid
the other. The S2 tests run in a CI job of their own, since they need the s2
extra, and that job never asked for coverage. And .coveragerc omitted
src/flexmeasures_client/s2/* outright, since #183, so asking for it would have
reported nothing about S2 while looking as though it did.

The omission is gone, and the S2 job now reports and uploads its own partial
report, which Coveralls merges with the other job's in the finalize step, as it
already does across the test matrix. The poe task that runs the S2 tests locally
collects it as well. The job that skips tests/s2 will now list the S2 files as
unexecuted in its own report; the merged figure is the one that counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
A CEM's handlers run for days with nobody watching standard output, so anything
they print is lost to a deployment, which keeps the log instead. FRBCSimple gave
up on a schedule that way when no storage status had arrived, which is the very
failure, a storage left without an instruction, that the handler otherwise goes
to some length to say out loud. It now logs a warning.

A pygrep hook keeps it that way for s2/control_types and s2/cem.py. The example
scripts print on purpose, as do the client's interactive delete confirmations,
which answer an input() prompt on the console, so the hook leaves those alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
- The print ban now matches a print anywhere on a line and with a space before
  its parenthesis, plus pprint and writes to stdout or stderr, and covers all of
  s2/ but the example scripts. It only caught an indented `print(` at the start
  of a line before, in two directories, and missed s2/__init__.py, utils.py and
  wrapper.py, which run inside the CEM as much as the handlers do.
- The example scripts get an __init__.py. Without one, coverage never listed
  websockets_server.py at all, since no test imports it, so the S2 report left
  it out instead of showing it at 0%. Running them by path is unaffected.
- The TUNES leakage test now pins what is posted, its value and its unit,
  rather than only that something reached the sensor.
- A client without a token now has a test that it authorises with the one it
  fetches.
- FillRateBasedControlTUNES.trigger_schedule is back to taking no arguments,
  with the override mismatch marked for mypy instead. Accepting the base
  class's arguments and ignoring them would have turned a caller's mistake,
  such as expecting its superseded-request protection, into a silent one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…s them in

S2 lets the elements of an operation mode, and of a leakage behaviour, cover the
fill-level range in pieces, and s2python checks that they are contiguous on a
sorted copy, leaving the list in whatever order it was sent. Four places took an
element by its position instead, and answered three different ways:

- the measured power of an actuator status took elements[0], so a heat pump that
  derates as its buffer warms was recorded at its cold power when warm: 2000 W
  where it draws at most 1200 W;
- the charging efficiency took elements[-1] as though it were the top of the
  range, so a mode listed high element first planned against the wrong ratio;
- the storage efficiency took the leakage rate of elements[-1], on the same
  assumption, though the leakiest element need not be the top one;
- the schedule translation selected by fill level, but settled a shared boundary
  by list order.

element_for_fill_level now answers wherever a fill level is known: the element
whose range holds it, the upper of two at a shared boundary, and past either end
of the range the element that applies at that end, so the answer does not jump
there. The measured power and the schedule translation use it. Before any storage
status there is no fill level, so the measured power takes the bottom of the range
and says in the log that it guessed.

Where FlexMeasures takes one number for a whole schedule, it does not know which
fill levels the schedule will pass through, so the most cautious element answers,
which is the rule #253 already applies across modes: the lowest conversion
efficiency, and the leakiest element. That is also independent of order.

The efficiency now also skips an element that draws power while the storage does
not fill, such as a standby or circulation mode. Before, such a mode gave a ratio
of zero or below, which then won the minimum across modes, went onto the
charging-efficiency sensor, and told the scheduler that no power could fill the
storage at all; with the fill-level band priced rather than hard, never running
the heat pump was then a feasible plan.

Two guards were dropped rather than tested, because S2 makes them unreachable:
every operation mode and leakage behaviour has at least one element, and every
element at least one power range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…owest

An adversarial review showed that the lowest efficiency over a mode's elements
is not the cautious choice the previous commit called it. In FlexMeasures' linear
program a charging efficiency scales what the actuator adds to the stock, so
understating it protects the lower band only: the plan overshoots the upper
band, buys more than it uses, and against a usage forecast the plan cannot
relax, it can turn a problem the actuator can meet into one the scheduler
finds infeasible. A heat pump at a COP of 4 cold and 2 warm, planned at 2,
empties its planned buffer in a few hours while the real one keeps up.

So within a mode, the element that applies at the fill level the schedule
starts from answers. That is also the part of a schedule that runs before the
next storage status asks for a new one, and it is what the actuator really does
there. The storage efficiency likewise takes the leakage element at the latest
fill level, and the leakiest only where no fill level is known yet.
Across modes, the lowest still goes out, as #253 decided; the same argument
applies to it, and that is left as a question rather than changed here.

Also from the review:
- A fill level that is not a number, which s2python lets through, counts as no
  fill level, and the selector refuses one, where it used to pick the bottom
  element silently.
- The selector's docstring now says what is true: s2python checks contiguity for
  an operation mode only, never for a leakage behaviour; a single-point range,
  which the standard rules out, applies only at the top; and of two elements
  with the same range, the first listed answers.
- Tests now cover the latest of several storage statuses, a fill-level scale
  other than 1 where scaling would pick another element, no warning for a mode
  with one element, and a schedule starting in the upper element. Each was a
  production mutation that passed the suite before.

Formatted with black as well, which the previous commit was not: the
pre-commit hooks were never installed in this repository, so they did not run
on commit, and CI's check caught it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
S2 makes an operation mode's diagnostic label optional, and
FillRateBasedControlTUNES tells its modes apart by looking for "THP" or "NES"
inside it. On a mode without one that raised a TypeError, so a single
unlabelled mode stopped both planning and the recording of every mode's
conversion efficiency. A missing label now simply matches neither.

Found by type checking against s2python's own types, which also asked for two
variables that held a range and then a number, or a message and then a series,
to be given a name for each. New tests cover the planning path, the efficiency
path, and posting a usage forecast, none of which any test reached before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…inted

request() ran check_content_type after its retry loop. It could never raise
there: request_once parses every answer with aiohttp's json(), which refuses any
content type but JSON, and the loop already reports that refusal as a
ConnectionError, like every other failure to reach the API.

Had it fired, it would have done harm. It built aiohttp's ContentTypeError from
a message and a dict, where aiohttp expects the request's info and history, so
anything that formatted the error, a log line included, raised an AttributeError
of its own; and it read the body with response.text() without awaiting it.
Its only test passed a mock whose text() was not a coroutine, and never
formatted the error.

The check is gone. The test that replaces it states where a non-JSON answer,
such as a proxy's error page during a restart, is actually handled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ncluded

mypy ran with --follow-imports skip, so s2python, pydantic, pandas and aiohttp
were Any to it, and a misspelt attribute on an S2 message passed. It now follows
imports, with pydantic's plugin, since S2 messages are pydantic models, and the
pre-commit hook installs the s2 extra so those types are there to check. The
plugin is also the guard against sliding back: without the extra it fails to
load, rather than leaving every S2 type silently Any. A misspelt message_id on a
leakage behaviour is now an error naming it.

What it found, besides the two fixes in the previous commits:

- get_validation_error_summary iterated a pydantic ValidationError, which is not
  iterable, so it raised a TypeError on any error it was given. Nothing called
  it, so it is gone rather than repaired.
- Revocations were recorded by the RevokeObject's raw id, while every other id
  a handler keeps is text, so a lookup by the text of the id would have missed.
  They are recorded as text now. Nothing calls is_revoked yet, which means a
  revocation is acknowledged and then never consulted; that is recorded as a
  finding for later rather than changed here.
- The rest is annotation: a message that arrives as a model, a dict or text is
  normalised to a dict once and checked as one; get_message returns the dict it
  always returned rather than claiming a str; the message type of a registered
  schema is read through model_fields rather than the deprecated __fields__; an
  operation mode's id is text where a label is wanted; and the actuator's power
  ranges are pairs rather than tuples of any length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
… worked

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ested

An adversarial review ran every claim on this branch, and three were false:

- The removed content-type check could fire. aiohttp's json() accepts JSON
  variants such as application/problem+json and application/vnd.api+json, and
  the check refused those, with an error that could not be printed. Removing it
  is right, but it changes behaviour: such an answer is now read like any other.
  The comment at the removed call, the test's docstring and the changelog entry
  now say so, instead of calling it dead code.
- "A non-JSON answer is reported as a ConnectionError, as before" was only true
  of a wrong content type. A body that is not JSON under a JSON content type is
  refused by json() itself, as a JSONDecodeError, as it was before.
- One unlabelled TUNES mode stopped planning, and the recording of the
  efficiencies of the modes listed after it, not of every mode.

Two more overstatements live only in pushed commit messages and the PR body:
following imports leaves pandas Any, since it ships no type information and no
stubs are installed, and the efficiency path was already reached by a test.

Tests now cover what five mutations showed was unpinned: a revocation taken by
the CEM itself, with no control type active; a model of a type a handler does
not handle; a usage forecast starting off the quarter hour; and one averaged
within a quarter hour rather than taking its highest value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ions

The schedule translation keeps a running fill level, so that each step is
answered with the operation mode for the fill level the storage is then at. It
advanced that level by a fill rate per second times a step in hours, so it moved
3600 times too little, and every step of a twelve-hour schedule was answered at
the fill level the schedule started from (finding C1). Fixing that alone would
have been worse: the level would then rise at the real rate and never fall, and
pin every step at the top. So the integrator gets one written contract, the one
S2 states for all three rates: a change in fill level per second, in the
resource manager's own units, with usage and leakage lowering it where positive.
Its step is a duration rather than a number of some unit.

- Leakage now counts (C2). It read leakage_rate_per_second, which S2 does not
  have, and an actuator's leakage_behaviour, which cannot exist, both through
  getattr with a default, so the branch never ran. The handler now passes the
  latest FRBC.LeakageBehaviour, and the element that applies at the fill level.
- Usage now counts. The handler passes the latest FRBC.UsageForecast, read per
  step by usage_rate_at; a frame may still carry a usage_forecast column instead,
  per second, which is what TUNES does, now dividing by the quarter hour it
  posts in.
- The step is the schedule's own (B5): its duration over its number of values,
  which is the resolution of the sensor FlexMeasures answered at. It was taken
  to be a quarter hour whatever it was, so an hourly sensor's twelve values were
  laid on forty-eight quarter hours, pandas refused, and no instruction went out.
- An instruction goes out wherever the mode or its factor changes, not only
  where the power does: at a constant power the fill level can pass from one
  element into another, which needs another factor.
- The factor is the one that draws the power asked for (C4), taken from the power
  range, rather than worked back out of the fill rate, which failed wherever the
  fill rate was one value and the power a range: asked for 1200 W from a mode
  running 800 to 1200 W, that asked for 800 W. Where the power is one value and
  the fill rate a range, every factor draws that power, so the one that fills
  most is asked for. op_mode_compute_factor is gone.
- The storage efficiency compounds a leakage rate per second (B10), where it
  compounded it per hour, declaring a buffer that loses about 7% over twelve
  hours as holding its heat to within 0.002%.
- The handler's power and fill-rate measurements cover its own resolution
  (D9), rather than a quarter hour whatever it planned on.
- translations.py is linted again: it opened with `# flake8: noqa`, which hid
  nothing, as it turns out.

TUNES shares the translation, so its instructions change with it, for the
better: it carried the same per-hour integration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…showed it did not

An adversarial review built its own integrator from S2's definitions, a second at
a time, and found the previous commit agreed with it only while nothing changed
within a step. Where something did, it did not:

- A step that crossed from one element into another was integrated as if it had
  not: from 46, filling at 0.02 per second below 50 and 0.01 above, a quarter hour
  at factor 1 reaches 57, where holding the first element said 64, and at an hour's
  step the client thought a buffer full that was at 84. The same held for leakage.
- Usage was sampled at the instant a step began, where FlexMeasures planned with
  the mean over the step, so a five-minute draw in each quarter hour counted fully
  or not at all, and a forecast starting off the step counted as nothing.

follow_operation_mode now follows a step a minute at a time, as a resource manager
follows an instruction: the mode and the factor are what the instruction fixes,
while the element of the mode and of the leakage behaviour, and the usage a
forecast expects, are each taken where and when they apply. The fill level is held
between its bounds throughout, rather than only at the end of a step.

Also from the review:
- An instruction no longer goes out for a solver's own rounding: near factor 0,
  math.isclose without an absolute tolerance is exact equality, so 800 W give or
  take a ten-millionth from a mode running 800 to 1200 W sent 30 instructions in
  twelve hours instead of one.
- A gap in a usage column, as FlexMeasures returns for a moment it holds nothing
  for, is no usage known, rather than a NaN that pinned the running fill level at
  the top of its bounds for the rest of the schedule.
- FillRateBasedControlTUNES reads its usage forecast back divided by the
  fill-level scale it posted it with, which left it ten times too small at TUNES'
  default scale, and plans with its latest leakage behaviour. The previous commit
  message called TUNES' change "for the better"; without these it was not.
- One power and a fill-rate range listed downwards asks for the top of the range,
  where operation_mode_conversion_efficiency declares the efficiency FlexMeasures
  plans with, so the instruction and the plan agree.
- FRBCSimple refuses a schedule_duration that is not a whole number of resolution
  steps: FlexMeasures answers with whole steps only, so the step, read as the
  duration over the number of values, would come out wrong.
- The handler's storage efficiency is what remains after one step of its own
  resolution, and FlexMeasures reads it per step of the leakage sensor's
  resolution, so the two have to agree. That is now said where the sensor is
  taken and where the value is posted.

Tests cover each of these, and each of the four mutations the review found
surviving, among them an on/off heat pump told to switch off: both modes ask for
factor 0, and only the mode tells them apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ed, and simulated

The shipped example sent no instruction at all, for two reasons that only show
with both halves running (findings A1, A2). Its resource manager sent its storage
status before its system description, which planned nothing, since planning hangs
off a storage status and needs a description to turn a schedule into
instructions; and its description was dated 2026-01-15, while the server seeds
prices from the day it starts, so a schedule's window had no prices.

It also could not have shown steering if it had: one operation mode, with a power
range of -0.5 to 0.5 W against a fill rate of -0.5 to 0.5 per second, which is
1.8 MW, over a declared range of 0.4.

The resource manager is now a heat pump keeping a hot-water buffer warm. It is
idle, or heats at anything from 400 to 1500 W, at a COP of 4 while the buffer is
below 6 kWh and 3 above it. It declares its leakage, warmer losing more; a day of
hot-water use with showers in the morning and washing up in the evening; and a
comfort profile that pre-heats before the showers. It sends its description first,
dated now, and then simulates its buffer from S2's own definitions, written
separately from the CEM's code: it follows each instruction from the moment it is
due, and reports its fill level every so often, which asks for a new schedule.
That simulation is a second opinion on the CEM's arithmetic, and a test holds the
two to agreement across an element boundary.

The server gives the handler a sensor for every declaration (fill rate, band,
usage, leakage, charging efficiency), so each can be seen next to the schedule
planned against it, and plans from now rather than a day after the description.
Its connection handling is split out of the aiohttp handler, so a test can serve
the shipped resource manager with the shipped CEM, over an in-memory websocket,
with only FlexMeasures stood in for.

Two library changes the example needed:
- A system description that arrives after the latest storage status, or that
  changes what the device can do, is planned for against that status (finding
  A3), rather than waiting for a status that a resource manager whose fill level
  holds steady may never send. It plans without re-posting the old status as a
  new measurement.
- A schedule never starts before now. Planning happens on every storage status
  while a description can be days old, so a schedule anchored to the description
  alone slid into the past, and its instructions all arrived as one overdue burst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…xMeasures

FRBCSimple passed its energy_unit straight through as the flex model's soc-unit,
while FlexMeasures validates soc-unit with OneOf(["kWh", "MWh"]). So four of the
six units fill_rate_in_watts documents, J, kJ, MJ and Wh, made every trigger a
422, and the storage got no schedule at all (finding B2).

The plan for this was to refuse those units when the handler is made. The carve
source answers it better, by converting joules at the edge, and that is what
happens now, for every supported unit: the state of charge and its two hard
bounds go to FlexMeasures in kWh, converted from the recorded unit with the table
fill_rate_in_watts already keeps, while kWh and MWh pass through as they are.
The band, the usage and the leakage are read from sensors, which FlexMeasures
converts itself, and the bounds the schedule is read back against stay in the
recorded unit, since the operation modes are stated in it.

An energy unit outside that table is now refused when the handler is made, where
it used to surface only at planning time, as a logged failure to record a
charging efficiency. The handler for that failure is gone, since nothing can now
reach it.

Not changed, and now pinned by a test: the measured state-of-charge sensor is
never named in the flex model (finding B13 stands). Named there, FlexMeasures
would save the state of charge it plans onto that sensor, beside the
measurements. The carve source does name it, and the carve will have to leave
that line behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…sures is sent

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…nges

A second round of review showed that following a step a minute at a time traded
one class of error for four others, and cost ten times the time:

- Usage was sampled at the start of each minute. Half-minute draws starting on
  the half minute were never seen at all, and the same draws starting on the
  minute were counted twice.
- A level settling on an element boundary, where filling stops above it, flipped
  across that boundary every step: 127 instructions in 144 steps, against one.
- An element narrower than a minute's travel was skipped: a level that stops at
  50 was carried to 80.
- A 48-hour schedule against the largest forecast S2 allows took 1.7 s, on the
  event loop, scanning the forecast once a minute.

Every rate is constant between the moments the usage changes and the moments
the level reaches an element boundary, of the mode or of the leakage behaviour,
or one of its bounds. So follow_operation_mode now steps from one such moment
to the next and is exact. A level that reaches a boundary whose far side would
push it back stays at that boundary, as a real storage does, and so does a level
at one of its bounds, which rises again from there once filling wins. The usage
forecast is read through a UsageProfile, built once per schedule and looked up
by bisection.

Also from the second round:
- FillRateBasedControlTUNES posted its usage forecast as an amount per quarter
  hour labelled MW. FlexMeasures reads soc-usage as a power and multiplies it by
  the step's share of an hour, so it planned a quarter of the real draw. It now
  posts per hour, and reads back accordingly.
- FRBCSimple refuses a resolution or schedule duration that is not positive,
  where a zero resolution raised a ZeroDivisionError.
- The factor tolerance's justification was wrong: FlexMeasures rounds a
  schedule to six decimals of MW, so what it absorbs is the noise of converting
  to watts, not a solver's rounding. The tolerance stays; the comment and the
  changelog now say what it is for.
- One of the first round's mutations had stopped applying after black split
  its line, so the gate's count overstated what was checked. The harness now
  rejects a mutant that cannot run, and every mutation is anchored to the
  formatted code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…2-example-heat-pump

Signed-off-by: F.N. Claessen <claessen@seita.nl>
…, and what review found

An adversarial review of the example, and of the two library changes it needed,
found the example resource manager following every plan it had ever been sent.
Each instruction ran in a task of its own, nothing cancelled the tasks of a
replaced batch, and the server sent no revocations. In the live demo against a
sandboxed FlexMeasures, thirty instructions for the same quarter hour fired at
once when it came, one from each replan.

The server now sets send_revocations, so a newer plan revokes what is left of the
one before, and the resource manager cancels a revoked instruction it has not yet
followed, and acknowledges the revocation.

Also from the review:
- The leakage, the usage forecast and the comfort profile were sent once, at
  connect, and the storage efficiency was recorded once, for twelve hours from
  the message, so planning went without leakage after twelve hours and without
  usage or a band after a day. The resource manager now sends all three again
  every S2_EXAMPLE_FORECAST_INTERVAL seconds, from then on, and the server seeds
  its demo prices again every twelve hours.
- In the library, a storage efficiency is now recorded each time a schedule is
  asked for, over the window it covers and as a share of the fill level it starts
  from, and named in the flex model only where it was recorded. On arrival, it
  covered the wrong hours, at a fill level nobody had reported yet, and as a share
  of the top of the range it understated the loss at lower fill levels.
- align_to_resolution floored the local clock, which in the repeated hour of an
  autumn DST change took the first pass of that hour, so a schedule could start
  67 minutes in the past. It now floors the instant, in UTC.
- A state of charge is posted as a reading at an instant, where it was posted over
  the minute after it, which FlexMeasures stored as a forecast.
- The example declares what it does: transitions between its two modes, which S2
  lists as the ones possible; no PowerForecast, since it sends none; and a
  consumer of electricity rather than a storage of it.
- Claims corrected: a description that differs from the one before it, its
  validity included, asks for a schedule, not only one that "changes what the
  device can do"; and a schedule starts in the current step, which has begun, not
  "never before now".
- The test that held the example's buffer to the CEM's integrator compared two
  copies of one discretisation. The CEM is now exact and the buffer steps a
  minute at a time, so the test bounds their difference by a minute of the rate
  difference at the crossing, which is what two correct discretisations owe each
  other.

Tests cover everything the review's thirteen mutations found unpinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
- The example now tells the CEM where each instruction stands: accepted on
  arrival, succeeded once carried out, revoked once revoked. Without that every
  instruction stayed NEW, so each replan revoked the whole previous batch, the
  instructions already carried out included.
- The mode a transition reports leaving is the one the last carried-out
  instruction put the heat pump in. It was the mode the previous instruction to
  arrive asked for, which after a revocation the heat pump was never in.
- A storage efficiency takes the loss as a share of the start fill level or of
  the middle of the storage's range, whichever is larger. A share of a near-empty
  start left almost nothing per step, and overstated the loss many times over
  once the plan filled the storage.
- A state of charge is posted as an instant only on a sensor that records
  instants: FlexMeasures refuses a single value of no duration on a sensor with
  a resolution, so there it is posted over a minute, as before.
- align_to_resolution floors on the moment's own clock, which is the grid
  FlexMeasures floors to, with the pass of an autumn DST hour taken from the
  moment. Flooring in UTC put hourly steps half an hour off in zones that are,
  and daily steps on the wrong midnight.
- The example server's price reseeding survives a failed ingestion job, a wrong
  content type and a wrong API version, not only a lost connection.
- Statuses about different steps that interleave within a second get belief
  times a second apart per step, where only the latest status was compared. The
  demo gate's first run had thirty statuses for one quarter hour arrive at once,
  and one was refused as a replacement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
- In the repeated hour of an autumn DST change, a boundary is the later of the
  two candidates that is not after the moment. Taking the pass from the moment
  put 03:10 at two-hourly steps on 02:00 of the first pass, three hours back.
  The candidates are compared as pandas timestamps, which compare instants.
- The changelog says what the floor does: on the clock of the moment, in the
  offset it carries, which is how FlexMeasures floors a posted series. "The
  site's own clock" was not true for series a resource manager anchors in UTC.
- A state-of-charge sensor stating a resolution that is not a duration is
  posted over a minute and logged, where it aborted the status before planning.
  The docstring says the sensor is asked until it answers, which it is.
- An instruction that has become due is no longer pending, so a revocation
  arriving while it is carried out is not answered as REVOKED for an
  instruction the heat pump is following.
- The comment on the storage efficiency's reference says that below the middle
  of the range the loss is understated, and by how much at most.
- Tests pin the forgetting of belief times more than a day old.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…er way

The demo gate's second run, against a sandboxed FlexMeasures, heated the buffer
in the quarter hour the plan chose, but every replan inside that quarter
replanned the same quarter, at 0.72 kW, then 0.67, 0.63 and on down to 0.51 kW.

A schedule asked for mid-step started at the boundary that step began at, while
it was planned from the fill level reported now, which FlexMeasures takes to be
the level at the schedule's start. So each replan counted on the whole step
where only part of it was left, asked for less power over what remained, and
the step delivered less than any plan meant it to.

A schedule now starts at the next boundary, or at now where now is one. The
step under way keeps the instruction it is already following, which a newer
plan does not revoke, since it has been carried out.

The end-to-end test stands the CEM's clock on a boundary, so that its schedules
start there and the first instruction is due within the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ix/frbc-energy-unit

Signed-off-by: F.N. Claessen <claessen@seita.nl>
Black had split the expression around the trailing comment after the merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Flix6x and others added 7 commits November 6, 2026 01:09
…s no head room of its own

The fill-level band a schedule is planned against goes out inline since the
declared range reaches the server, so the minima and maxima posts are for
display; a failure in one is now logged, and the other still recorded, rather
than raised in a task nobody awaits.

hard_bound_factor stays multiplicative: a top below zero would be pulled further
down, but the bound widens to take in the start, which is never below zero, so
the mutation that tried the change showed it to make no difference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…eeping found

- A key set again in a SizeLimitOrderedDict stayed where it was, so a
  resource manager reusing a message id left [-1] reading a stale message;
  it now moves to the end.
- The band-post test checks that the second post, with the maxima, went out.
- The changelog gives the actuator status and the band post a bullet each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
- Five comments broke a line mid-phrase, against the rule that a line ends at
  punctuation; each pair is now one line. A scan of every comment the branch
  adds finds no others.
- ensure_site fell back to the listing where a known asset could not be
  fetched, but not where the server answered with something other than an
  asset (ContentTypeError), which escaped and failed the setup.

Left as they are, and why: FRBC TUNES keeps its broad except clauses and its
15-minute tick, which predate this branch and are its own story;
usage_rate_at is used by the end-to-end example harness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Rewrite the Unreleased section of CHANGELOG-S2.rst so that each feature has one entry,
described against v0.9.0 rather than against states that never shipped, and checked against the code at this commit.

Folded, keeping every PR link: the newest-request entries (#237, #261, #269);
the two subclass signature changes (#237, #240);
the power bands and their v1.0.0 gate (#248, #272);
the declared fill-level band, usage, leakage and hard bounds (#244, #245, #259, #271, #273);
retrying a schedule (#254, #273);
the state-of-charge unit (#244);
the actuator's power, its sensor, sign, unit, step and belief time (#236, #252, #256, #258, #259);
the example's sensors, instruction handling and prices (#241, #252, #259, #262);
target-profile recording (#244, #274);
revocations and the instruction-status history (#237);
the planning triggers, re-sent descriptions and the guarded state-of-charge post (#240, #259, #273);
the schedule start and DST boundary (#240, #259);
the resolution and its validation (#240, #258);
and leakage_behaviour_to_storage_efficiency (#256, #258, #259).
The stray second Added list is merged into the first,
and get_validation_error_summary moves to Removed.
Fixes to behaviour that never shipped no longer have entries of their own.

Corrected: the declared range now goes out inline whatever the sensors,
so soc-minima and soc-maxima are no longer said to be named by sensor;
soc-usage is named only where the resource manager provides a usage forecast;
the retry entry no longer claims a missing state-of-charge refusal applies to FRBCSimple, which names no state-of-charge sensor;
the fallback entry no longer claims a lost state of charge for later windows;
a multi-actuator description is compared with v0.9.0, which planned for the first actuator;
the schedule start and the example's prices describe what the code does now;
and the hard bounds say plainly that v0.9.0 held a storage hard to its declared range.

Added: fill_level_scale and power_unit;
rm_discharge_sensor_id no longer written to;
the breaking signature changes in FRBC.utils;
the new keyword arguments of fm_schedule_to_instructions and leakage_behaviour_to_storage_efficiency;
is_revoked finding a revoked object;
schedule_attempts below one refused;
and the new public FRBC methods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…s2-changelog-what-ships

Signed-off-by: F.N. Claessen <claessen@seita.nl>
…fill-rate post

- The fallback-scheduler warning said no state of charge was recorded for a
  later window to be planned from. This handler sends its state of charge
  inline, so a later window does not depend on one; the warning now says only
  that the schedule is a coarse charging policy, not a plan.
- The power_unit comment said nothing converts a power value on the way out,
  with a TODO to do so; it is converted, so the comment says that.
- The fill-rate post in send_actuator_status runs in a task nobody awaits, and
  is now guarded and logged like the power post beside it.

Found by the changelog rewrite, which read the code at the head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
The demo gate on the final head showed the warning that timers and
transitions are not honoured firing on the repository's own example, an
on/off heat pump that declares both of its switches and no timers. Such
transitions rule nothing out, so there is nothing this CEM fails to honour.
The warning now goes out only where the actuator declares timers, or where
some switch between two modes usable under normal conditions has no
transition usable under normal conditions. A blocking timer needs a declared
timer, which s2python checks, so timers cover blocked transitions too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
@Flix6x Flix6x added this to the 0.10.0 milestone Nov 6, 2026
@Flix6x Flix6x added the S2 label Nov 6, 2026
@coveralls

coveralls commented Nov 6, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 42681938280

Coverage increased (+1.0%) to 95.943%

Details

  • Coverage increased (+1.0%) from the base build.
  • Patch coverage: 146 of 146 lines across 6 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 3180
Covered Lines: 3051
Line Coverage: 95.94%
Coverage Strength: 4.57 hits per line

💛 - Coveralls

@Flix6x Flix6x left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the steered run, PV feed-in disappeared: self-consumption rose to 100%, and energy cost rose by roughly 20% to 30% per home.

Let's check this, together with the comfort metric. Our study does have a comfort metric: degree hours outside of the comfort range.

Rewrite the Unreleased entries of CHANGELOG-S2.rst, and the three this
work added to CHANGELOG.rst, for legibility: lead with what changes for
the reader, in short sentences with plain word order, and split the
longest entries into a lead sentence and a list. No claim is added or
dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Flix6x and others added 3 commits November 8, 2026 15:27
A check of every entry against the text it replaced found the rewrite
had given the charging-efficiency risk a direction (wrong for a heat
pump, whose COP exceeds one), made "a lower bound" of the hard upper
bound, and dropped or shifted several smaller facts: planning from an
unrecorded state of charge, the storage efficiency named only once
confirmed, the DST boundary rule, abnormal modes being left out of both
capacities and bands, power being converted into power_unit, polling,
and the handler always tracking the batch in force. Restore each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
… leave untested

Raise combined line coverage from 95.17% to 95.93%, above main's 95.68%
before the S2 package counted: an invalid message answered as
INVALID_DATA, messages given as JSON, failure callbacks, activating the
control type in force or one the resource manager lacks, re-registering a
control type, an empty schedule, and the example server's websocket
producer and consumer (send, reset, bad JSON, close, connection error).
Each test fails when the code it covers is broken (6 of 6 mutants killed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
It measured wall-clock time against 0.5 s, and failed in 2 of 12 runs on
a busy machine; the S2 job now also runs under coverage. In CPU time it
takes 0.11 s, and 0.22 s under coverage, still well apart from the 1.7 s
it guards against.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Flix6x and others added 3 commits November 8, 2026 15:48
Rewrite the docstrings and comments that the S2 work added or changed,
so that each one leads with what the code does or why,
in short sentences with plain word order.
Every fact, condition, unit and caveat is kept.
Only text changed: each touched file parses to the same AST once docstrings are set aside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
A check of every rewritten docstring and comment against the code found
one false claim (that a failure was always retried before retries were
bounded, when there were no retries at all), a capacity comment that read
as a community-level capacity rather than each member site's, and smaller
shifts: a condition made of a fact, a lost "because", a lost example, a
tense slip, a softened "cannot", and a line over the length limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Base automatically changed from dev/s2-frbc-p1 to main November 8, 2026 16:40
main's squash commit 1e21422 has the same tree as #278's head ad9a12b,
which this branch already contains, so this branch's tree is kept as is.

Signed-off-by: F.N. Claessen <claessen@seita.nl>
Flix6x and others added 5 commits November 9, 2026 16:48
…escription

After a reconnection, a resource manager can send its actuator status
before its system description. Only a description says what power an
operation mode stands for, so such a status was logged and skipped, and
the power sensor showed nothing until the next status. It is now kept,
the latest per actuator, and recorded once a description arrives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
… all operation modes

As asked in the review of #283.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…nces

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ensor

Signed-off-by: F.N. Claessen <claessen@seita.nl>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants