easee: warn on rogue CommandResponse not triggered by evcc#27916
easee: warn on rogue CommandResponse not triggered by evcc#27916andig merged 16 commits intoevcc-io:masterfrom
Conversation
…ves loop - waitForTickResponse now accepts a <-chan parameter instead of reading c.cmdC - postJSONAndWait creates and registers a per-tick channel via registerPendingTick/unregisterPendingTick - CommandResponse dispatches to per-tick channels in pendingTicks map, warns on rogue responses - Test updated: e.cmdC removed from newEasee helper and TestEasee_waitForTickResponse; TestEasee_postJsonAndWait goroutine now waits for pendingTicks registration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
WIP - will test this on my local build first. |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… Phases1p3p Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
start_charging causes the Easee API to return commandId:25 (LOCATION) with ticks that never exactly match the charger's CommandResponse — causing a false rogue warning and a waitForTickResponse timeout. Add pendingByID as a fallback lookup in CommandResponse, checked after ticks-match fails. postJSONAndWait now registers the same channel in both pendingTicks (by ticks) and pendingByID (by ObservationID from cmd.CommandId), so the LOCATION CommandResponse from start_charging is correctly delivered to the waiting caller instead of being flagged rogue. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If the circuit settings POST fails, the pre-registered orphan counter for CIRCUIT_MAX_CURRENT_P1 was leaked, silently swallowing a future rogue CommandResponse instead of logging it as a warning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
tested locally, tweaked some minor things, looking good on my end. |
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="charger/easee.go" line_range="443-444" />
<code_context>
+ chID, idOk := c.pendingByID[obsID]
+ c.cmdMu.Unlock()
+
+ if tickOk {
+ chTick <- res
+ return
+ }
</code_context>
<issue_to_address>
**issue:** Using a blocking send to the tick channel may deadlock if multiple responses are received for the same tick.
When `tickOk` is true, this does an unguarded `chTick <- res`. Since `chTick` has a buffer of 1 in `postJSONAndWait`, a second `CommandResponse` for the same `Ticks` value would block indefinitely and stall the SignalR handler. Consider a non-blocking send (e.g. `select { case chTick <- res: default: /* log/drop duplicate */ }`) or otherwise handling unexpected duplicates explicitly instead of risking a blocking send.
</issue_to_address>
### Comment 2
<location path="charger/easee_test.go" line_range="228-237" />
<code_context>
if tc.cmdResp != nil {
go func() {
- e.cmdC <- *tc.cmdResp
+ // wait for postJSONAndWait to register the per-tick channel
+ var ch chan easee.SignalRCommandResponse
+ for {
+ e.cmdMu.Lock()
+ ch = e.pendingTicks[tc.cmdResp.Ticks]
+ e.cmdMu.Unlock()
+ if ch != nil {
+ break
+ }
+ time.Sleep(time.Millisecond)
+ }
+ ch <- *tc.cmdResp
</code_context>
<issue_to_address>
**issue (testing):** Add a timeout in the polling loop to avoid a potentially hanging test
The spin loop that waits for `pendingTicks[tc.cmdResp.Ticks]` has no upper bound, so if registration regresses or the tick value changes, this test can busy-wait indefinitely and hang the suite. Please add a timeout (e.g., `time.After` in a `select`) and fail the test if the channel is not found in time, so issues show up as test failures rather than stuck runs.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| if tickOk { | ||
| chTick <- res |
There was a problem hiding this comment.
issue: Using a blocking send to the tick channel may deadlock if multiple responses are received for the same tick.
When tickOk is true, this does an unguarded chTick <- res. Since chTick has a buffer of 1 in postJSONAndWait, a second CommandResponse for the same Ticks value would block indefinitely and stall the SignalR handler. Consider a non-blocking send (e.g. select { case chTick <- res: default: /* log/drop duplicate */ }) or otherwise handling unexpected duplicates explicitly instead of risking a blocking send.
| if tc.cmdResp != nil { | ||
| go func() { | ||
| e.cmdC <- *tc.cmdResp | ||
| // wait for postJSONAndWait to register the per-tick channel | ||
| var ch chan easee.SignalRCommandResponse | ||
| for { | ||
| e.cmdMu.Lock() | ||
| ch = e.pendingTicks[tc.cmdResp.Ticks] | ||
| e.cmdMu.Unlock() | ||
| if ch != nil { | ||
| break |
There was a problem hiding this comment.
issue (testing): Add a timeout in the polling loop to avoid a potentially hanging test
The spin loop that waits for pendingTicks[tc.cmdResp.Ticks] has no upper bound, so if registration regresses or the tick value changes, this test can busy-wait indefinitely and hang the suite. Please add a timeout (e.g., time.After in a select) and fail the test if the channel is not found in time, so issues show up as test failures rather than stuck runs.
Summary
cmdCchannel with apendingTicks map[int64]chan SignalRCommandResponse— each outgoing API command registers its tick to a per-tick buffered channelCommandResponse()is now the single routing point: known tick → delivers to channel; unknown tick → emitsWARNlog identifying the charger serial, tick value, and a note that another system may be controlling the chargerTest Plan
TestEasee_*tests pass (go test ./charger/ -run TestEasee)TestEasee_CommandResponse_rogue— verifies unknown tick logs WARN without panicTestEasee_CommandResponse_legitimate— verifies known tick is delivered to the registered channelgo vet ./charger/is clean🤖 Generated with Claude Code