SRNE: guarded TOU charge/discharge schedule-window writes (#67) - #24
Conversation
…EMS#67) Extend the proven OpenEMS#53 guarded-write path to the SRNE charge/discharge SCHEDULE registers so time-of-use arbitrage (charge off-peak, discharge peak) can be programmed safely: - charge window 1 (0xE026/E027) + discharge window 1 (0xE02D/E02E), read back and exposed as new channels; written via the same SafeWriteHandler (default-off controlEnabled gate, verified-machine-state gate, one-shot, readback-verified) - reconcileTime() validates the value as a time (hour*256+min, hour 0..23, minute 0..59) before queueing, so a mistyped window can't reach the inverter - Config + MyConfig fields; readme updated Tests: static time validation/formatting, schedule-write-queues, invalid-time rejected; full module :check green on JDK 21. No live write - offline code only.
tushabe
left a comment
There was a problem hiding this comment.
I do not recommend onsite commissioning from this revision yet. CI is green and the scalar time validator is useful, but the schedule operation is not complete enough for a persistent production write.
Blocking findings:
-
The discharge schedule cannot be enabled. The live map records
0xE033=0as the time-slot battery-discharge enable flag, but this PR neither reads nor writes0xE033(and likewise omits charge enable0xE02C). Writing0xE02D/E02Ealone therefore does not establish that TOU discharge can run. -
A window is split into independent one-register writes. Start and stop are reconciled by separate handlers and separate FC16 tasks. There is no pair-level validation, ordering guarantee, atomic block write, or rollback if one succeeds and the other fails. That can leave a persistent half-updated window. Validate the complete window first and update it as one coherent operation, or implement an explicit safe sequence with verified rollback.
-
The tests stop at QUEUED. They do not prove the exact FC16 frame(s), successful execution plus fresh readback, mismatch/timeout handling for a window, or partial-failure recovery. Add coverage for the complete start/stop operation and for control-disabled/unverified-state zero-write behavior specific to schedules.
-
RTC and interval semantics remain unresolved. OpenEMS#67 requires verifying the inverter RTC before relying on wall-clock execution. Also document and test how an intended 18:00–midnight window is represented: the validator rejects 24:00, while 18:00→00:00 is a cross-midnight pair whose validity is not currently assessed.
Please keep every new target at -1 and do not publish/commission this schedule-write image until these are resolved. A read-only onsite capture of 0xE026–0xE036, including enable flags and RTC, is still safe and useful.
…#24 review) Address the four review findings on the TOU schedule-write path. - Add schedule enable flags E02C (charge) / E033 (discharge) as gated settings; the windows did nothing without them. - Introduce ScheduleWindow: the contiguous start/stop pair is written as one atomic FC16 block and read-back verified, and the enable is written last, only after both time registers verify. enable=0 (disarm) is written first, independent of the window, so a failed or invalid window can never leave a schedule armed (disabled is the safe resting state). - Reject cross-midnight windows: the SRNE encodes hour*256+min with no 24:00 and no defined wrap, so end-of-day must be 23:59 (5947); a window requires start < stop and both parts set together. Expose the RTC (E034-036) read-only so commissioning can verify the inverter clock first. - Range-validate the enable flag to 0/1 only (plus a metatype Option) and add deep unit tests driving the full write/execute/readback/verify lifecycle, partial-failure-never-enables, execute-error, timeout and disarm-first. Every schedule target still defaults to -1 (leave unchanged); not commissioned. Build green: ./gradlew :io.openems.edge.ess.srne:check on JDK 21.
|
Revised to address all four review findings. Every schedule target still defaults to Design change: the schedule window is now a coherent unit ( 1. Enable flags — added charge-schedule-enable 2. Coherent / atomic window — the contiguous Direction-of-safety ordering for the enable:
3. Deeper tests — the full lifecycle is now unit-tested in 4. RTC + cross-midnight — cross-midnight windows ( I also ran an independent adversarial review pass over the revision, which caught and I fixed: (a) the enable register was not range-validated (a typo Per your guidance I have not commissioned this — all targets remain |
tushabe
left a comment
There was a problem hiding this comment.
Re-review at dcfb8a7a69cff7eeb84f42fb84abf0b0072085bf: the original four findings are substantially addressed, and both CI and a local ./gradlew :io.openems.edge.ess.srne:check pass.
One production-safety blocker remains:
Changing an already-enabled window does not disarm first. In reconcileFromIdle(), disarm-first only runs when the requested target is cfgEnable == 0. If the device currently reads actualEnable == 1 and the operator supplies a different complete window with cfgEnable == 1 (or leaves enable unmanaged at -1), enterWindowPhase() queues the FC16 start/stop write while the old schedule remains armed. The test windowMatchesButEnableAlreadySet currently codifies this path.
Please require a changed window to transition through disable → verified disabled → atomic window write → verified window → re-enable when requested. If enable is -1 and the existing schedule is armed, either reject the window change or safely disarm and restore the captured enable state; do not mutate a live enabled window.
Add tests for both:
- enabled device + changed window +
cfgEnable=1: disable first, then re-arm last; - enabled device + changed window +
cfgEnable=-1: defined safe behavior with no live-window mutation.
Until that is fixed, the read-only onsite capture remains safe, but I still do not recommend schedule-write commissioning from this commit.
…eview) Address the remaining blocker: changing an already-enabled window queued the FC16 start/stop write while the old schedule was still armed. Rebuild ScheduleWindow reconcile around one invariant: the enable register must read 0 on the device whenever the window registers are written. It is now a convergence loop - any window change on an armed device is sequenced disarm -> verify-disabled -> atomic window write -> verify-window -> re-arm. The desired end enable state is captured once: enable=0/1 from config, and enable=-1 (unmanaged) captures the current device state, so a window change on an armed device is disarmed, rewritten and restored to armed rather than left disabled or mutated in place. Tests: replace the test that codified the unsafe path with two that prove the new sequencing - enabled device + changed window with enable=1 (disarm first, re-arm last) and with enable=-1 (disarm, rewrite, restore armed). Build green, all tests pass on JDK 21.
|
Fixed the remaining blocker at Never mutate a live enabled window. The reconcile is rebuilt around one invariant: the enable register must read 0 on the device whenever the window registers are written. It's now a convergence loop, and any change to a currently-armed window is sequenced exactly as you asked: disarm → verify-disabled → atomic window write → verify-window → re-arm. The window start/stop FC16 is only ever queued when the device reads For Tests: replaced
|
…sequence; note transient states in readme
tushabe
left a comment
There was a problem hiding this comment.
Re-reviewed through 80137a794774d1195668f990bd4dbea5652c890a.
The remaining blocker is resolved:
- an armed schedule is disarmed and verified before any window FC16;
- the start/stop pair is written atomically and read back;
- re-arm happens last;
enable=-1captures and restores the original armed state;- an enable flap before the window step re-disarms and never mutates the live window.
The new tests cover both requested enabled-device paths plus the flap case. I also ran ./gradlew :io.openems.edge.ess.srne:check locally: BUILD SUCCESSFUL.
Approved. Merge after the latest build-java check passes. For onsite commissioning, still begin with the planned read-only windows/enables/RTC capture, pin the resulting Edge image by digest, preserve the original block, and use a supervised reversible window test before enabling a real TOU period.
Extends the OpenEMS#53 guarded settings-write path to the SRNE charge/discharge schedule registers, so time-of-use arbitrage (charge off-peak, discharge at peak) can be programmed through the same safe path. Revised to address the review; see the resolution comment below.
What
0xE026/E027) + discharge (0xE02D/E02E), encodedhour*256+min, plus the schedule enable flags (0xE02Ccharge,0xE033discharge).ScheduleWindow): the contiguous start/stop pair is written as one atomic FC16 block and read-back verified; the enable is written last, only after the window verifies.enable=0(disarm) is written first, independent of the window, so a failed/invalid window can never leave a schedule armed (disabled is the safe resting state).start < stop(cross-midnight is rejected - the SRNE has no 24:00 or defined wrap, so end-of-day is23:59); enable is0/1only.0xE034-036) exposed read-only so commissioning can verify the inverter clock before enabling any schedule.controlEnabled, verified-machine-state gate, one-shot, no auto-retry. Every target defaults to-1(leave unchanged).Tests (module
:checkgreen, JDK 21):ScheduleWindowTestdrives the full write -> execute -> readback -> verify -> enable -> done lifecycle plus partial-failure-never-enables, execute-error, timeout, disarm-first, cross-midnight/zero-length/invalid/incomplete/enable-without-window/enable-out-of-range rejects. Component-level gate tests (control-disabled, unverified-state, queue) still pass.Safety: offline code only, no live write; not commissioned. Plan + tariff economics in Bookstack "SRNE TOU arbitrage: plan (OpenEMS#67)"; commissioning is a supervised daytime step starting with a read-only on-site capture of the windows/enables/RTC. Companion to logger OpenEMS#67.