-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Toyota: trigger soc refresh when charging#27697
Toyota: trigger soc refresh when charging#27697thomasbecker wants to merge 10 commits intoevcc-io:masterfrom
Conversation
Summary
- Periodically POST to Toyota's
/v1/global/remote/electric/realtime-statusendpoint during charging to trigger the car's TCU to push fresh battery data to the cloud - Implement
api.Resurrector(WakeUp()) for on-demand refresh - Reduce poll interval from 15min to 5min so evcc picks up fresh data promptly after a TCU push
Without this change, Toyota's GET /electric/status returns cached data that can go stale for hours during charging. The car does not proactively push SoC updates while charging -- an explicit POST to the realtime-status endpoint is required. This is a known issue in the Toyota ecosystem (pytoyoda/ha_toyota#150, DurgNomis-drol/ha_toyota#140).
How it works
- On each status fetch, if
chargingStatus == "charging"and >15min since last refresh, fire the POST (side-effect inside the cached getter) - Always return the current GET response (stale data is better than no data)
- The next poll (5min) picks up the fresher cloud data after the TCU push propagates
Rate limiting
- POST refresh: minimum 15 minutes between calls (only while charging)
- GET poll: every 5 minutes (lightweight, reads cached cloud data)
WakeUp()bypasses the 15-min guard (evcc's wakeup logic has its own rate limiting)- 12V battery drain is not a concern while charging (DC-DC converter is active)
Test data from a live charging session (bZ4X)
14:29 38% not charging
14:34 37% charging detected - 1st refresh POST
14:40 38% cloud updated (~5min after POST)
14:45 38% cached, waiting for next refresh
14:51 38% 2nd refresh POST (15min after 1st)
14:56 39% cloud updated (~5min after POST)
15:01 40% another update came through
Before this change, SoC was stuck at the same value for the entire charging session (hours).
Test plan
-
CGO_ENABLED=0 go test -tags=release ./vehicle/toyota/...-- 4 new unit tests -
CGO_ENABLED=0 go vet -tags=release ./vehicle/toyota/... ./vehicle/ - Live tested on bZ4X during two charging sessions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey - I've left some high level feedback:
- If
Providermethods can be called from multiple goroutines (e.g. polling plus manual wakeups), consider protectinglastRefresh(and any coupled state) with a mutex or atomic to avoid data races around the refresh rate limiting. WakeUpcurrently triggersrefreshbut does not resetstatusCache, so callers may still see stale SoC until the cache expires; consider callingstatusCache.Reset()(or otherwise invalidating the cache) insideWakeUpor explicitly documenting that behavior.
Prompt for AI Agents
## Overall Comments
- If `Provider` methods can be called from multiple goroutines (e.g. polling plus manual wakeups), consider protecting `lastRefresh` (and any coupled state) with a mutex or atomic to avoid data races around the refresh rate limiting.
- `WakeUp` currently triggers `refresh` but does not reset `statusCache`, so callers may still see stale SoC until the cache expires; consider calling `statusCache.Reset()` (or otherwise invalidating the cache) inside `WakeUp` or explicitly documenting that behavior.
Help me be more useful! Please click or on each comment and I'll use the feedback to improve your reviews.
70183bd to
430a851
Compare
d6f5d80 to
5b04332
Compare
charging to trigger the car's TCU to push fresh battery data to
the cloud. Poll every 5min to pick up updated data promptly.
Also implement api.Resurrector (WakeUp) for on-demand refresh.
5b04332 to
0e9a0f5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey - I've found 1 issue, and left some high level feedback:
- The comment in
NewProvidersays "Poll at most every 5 min" but the cache duration is set tomin(cache, pollInterval), which allows polling more frequently than 5 minutes ifcacheis smaller; consider usingmax(cache, pollInterval)(or equivalent) to enforce the intended upper bound. - The mutation of
impl.lastRefreshinside thestatuscached closure is not synchronized and may cause a data race ifSoc/statusis called concurrently; consider protectinglastRefreshwith a mutex or using an atomic type.
Prompt for AI Agents
## Overall Comments
- The comment in `NewProvider` says "Poll at most every 5 min" but the cache duration is set to `min(cache, pollInterval)`, which allows polling more frequently than 5 minutes if `cache` is smaller; consider using `max(cache, pollInterval)` (or equivalent) to enforce the intended upper bound.
- The mutation of `impl.lastRefresh` inside the `status` cached closure is not synchronized and may cause a data race if `Soc`/`status` is called concurrently; consider protecting `lastRefresh` with a mutex or using an atomic type.
## Individual Comments
### Comment 1
<location path="vehicle/toyota/provider.go" line_range="52-54" />
+var _ api.Resurrector = (*Provider)(nil)
+
+func (v *Provider) WakeUp() error {
+ return v.refresh()
+}
+
**suggestion (bug_risk):** Consider updating lastRefresh in WakeUp to avoid an immediate redundant refresh
`WakeUp` only calls `v.refresh()` and leaves `lastRefresh` unchanged, so a subsequent `Status` call will see an old/zero `lastRefresh` and likely trigger another POST immediately. If the goal is to avoid this extra POST after a wake-up, consider updating `lastRefresh` on successful `WakeUp`:
```go
func (v *Provider) WakeUp() error {
if err := v.refresh(); err != nil {
return err
}
v.lastRefresh = time.Now()
return nil
}
```
If the extra POST is intentional, it may be worth checking API rate limits and side effects.
```suggestion
func (v *Provider) WakeUp() error {
if err := v.refresh(); err != nil {
return err
}
v.lastRefresh = time.Now()
return nil
}
```
Help me be more useful! Please click or on each comment and I'll use the feedback to improve your reviews.
| } | ||
| } | ||
| return res, err | ||
| }, min(cache, pollInterval)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will now refresh every 5min instead of 15min. Are we sure that the api survives this?
| impl.status = util.Cached(func() (Status, error) { | ||
| res, err := api.Status(vin) | ||
| if err == nil && strings.EqualFold(res.Payload.ChargingStatus, "charging") && time.Since(impl.lastRefresh) >= refreshInterval { | ||
| impl.lastRefresh = time.Now() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the lastRefresh is already handled inside refresh()- seems the logic is unclear where this really needs to happen?