chore: simplify drivesomethinggreater (#30391)

This commit is contained in:
andig 2026-06-01 14:51:35 +02:00 • committed by GitHub
parent 471cebdb12
commit 5409ed21a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 55 additions and 106 deletions

View file

@ -3,6 +3,7 @@ package request
import (
"bytes"
"io"
"mime"
"net/http"
"net/http/httputil"
"strconv"
@ -100,6 +101,13 @@ func headerContainsToken(h http.Header, key, token string) bool {
return false
}
// binaryContent reports whether the response carries a binary body whose raw
// bytes are not useful in the trace log, such as a downloaded archive.
func binaryContent(h http.Header) bool {
mediatype, _, _ := mime.ParseMediaType(h.Get("Content-Type"))
return mediatype == "application/octet-stream"
}
// copy of http.drainBody
func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
if b == nil || b == http.NoBody {
@ -174,12 +182,14 @@ func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
resMetric.WithLabelValues(req.URL.Hostname(), strconv.Itoa(resp.StatusCode)).Add(1)
if !isWebSocketReq {
// skip binary bodies (e.g. downloaded archives) - their raw bytes are noise in the trace log
logBody := !binaryContent(resp.Header)
if LogHeaders {
if body, err := httputil.DumpResponse(resp, true); err == nil {
if body, err := httputil.DumpResponse(resp, logBody); err == nil {
bld.WriteString("\n\n")
bld.Write(bytes.TrimSpace(body[:min(LogMaxLen, len(body))]))
}
} else {
} else if logBody {
if save, resp.Body, err = drainBody(resp.Body); err == nil {
err = dump(save, bld)
}

View file

@ -120,36 +120,12 @@ func TestResolveBrand(t *testing.T) {
assert.False(t, ok)
}
func TestContentDatasets(t *testing.T) {
list := []dataset{
{Name: "20260531090000_WVWZZZ.zip"},
{Name: "20260531080000_WVWZZZ.zip"},
{Name: "20260531091500_WVWZZZ_no_content_found.zip"},
}
content, err := contentDatasets(list)
require.NoError(t, err)
require.Len(t, content, 2, "no-content placeholder dropped")
assert.Equal(t, "20260531080000_WVWZZZ.zip", content[0].Name, "oldest first")
assert.Equal(t, "20260531090000_WVWZZZ.zip", content[1].Name, "newest last")
assert.Equal(t, time.Date(2026, 5, 31, 8, 0, 0, 0, time.UTC), content[0].Timestamp, "timestamp parsed")
// no-content placeholders are skipped without parsing
empty, err := contentDatasets([]dataset{{Name: "x_no_content_found.zip"}})
require.NoError(t, err)
assert.Empty(t, empty)
// a content dataset with an unparseable timestamp is an error
_, err = contentDatasets([]dataset{{Name: "no-timestamp.zip"}})
require.Error(t, err)
}
func TestPending(t *testing.T) {
content := make([]dataset, 0, 11)
for i := range 10 {
content = append(content, dataset{
Name: fmt.Sprintf("20260531%02d0000_WVWZZZ.zip", i),
Timestamp: time.Date(2026, 5, 31, i, 0, 0, 0, time.UTC),
CreatedOn: time.Date(2026, 5, 31, i, 0, 0, 0, time.UTC),
}) // hour i, oldest first
}
@ -163,13 +139,13 @@ func TestPending(t *testing.T) {
assert.Len(t, pending(content[:3], time.Time{}), 3)
// high-water at the newest merged dataset: nothing new to download
after := content[len(content)-1].Timestamp
after := content[len(content)-1].CreatedOn
assert.Empty(t, pending(content, after))
// a newer dataset arrives
content = append(content, dataset{
Name: "20260531100000_WVWZZZ.zip",
Timestamp: time.Date(2026, 5, 31, 10, 0, 0, 0, time.UTC),
CreatedOn: time.Date(2026, 5, 31, 10, 0, 0, 0, time.UTC),
})
got = pending(content, after)
require.Len(t, got, 1)
@ -197,32 +173,6 @@ func TestMerge(t *testing.T) {
assert.Equal(t, "200", dst[FieldRange].Value, "new field added")
}
func TestDatasetTime(t *testing.T) {
ref := time.Date(2026, 5, 31, 8, 0, 0, 0, time.UTC)
tc := []struct {
d dataset
expected time.Time
err bool
}{
{dataset{Name: "20260531080000_WVWZZZ_no_content_found.zip"}, ref, false}, // real portal format
{dataset{Name: "20260531080000_WVWZZZ.zip"}, ref, false}, // content file
{dataset{CreatedOn: "2026-05-31T08:00:00Z"}, ref, false}, // createdOn fallback
{dataset{CreatedOn: "2026-05-31T08:00:00Z", Name: "no-stamp.zip"}, ref, false}, // name unparseable, createdOn used
{dataset{Name: "no-timestamp.zip"}, time.Time{}, true}, // nothing parseable
}
for _, tc := range tc {
got, err := tc.d.time()
if tc.err {
assert.Error(t, err, "dataset %+v", tc.d)
continue
}
require.NoError(t, err, "dataset %+v", tc.d)
assert.Equal(t, tc.expected, got, "dataset %+v", tc.d)
}
}
// TestResetDelay verifies the cache reset is scheduled for when the portal is
// expected to deliver the dataset following the one just read.
func TestResetDelay(t *testing.T) {

View file

@ -64,13 +64,13 @@ func resetDelay(ts, now time.Time) time.Duration {
}
// lookup returns the first present, non-empty value among the given field names
func lookup(data map[string]point, fields ...string) (string, bool) {
func lookup(data map[string]point, fields ...string) *point {
for _, f := range fields {
if v, ok := data[f]; ok && v.Value != "" {
return v.Value, true
if v, ok := data[f]; ok {
return new(v)
}
}
return "", false
return nil
}
var _ api.Battery = (*Provider)(nil)
@ -82,8 +82,8 @@ func (v *Provider) Soc() (float64, error) {
return 0, err
}
if s, ok := lookup(data, FieldSoc, FieldHvSoc); ok {
return strconv.ParseFloat(s, 64)
if p := lookup(data, FieldSoc, FieldHvSoc); p != nil {
return strconv.ParseFloat(p.Value, 64)
}
return 0, api.ErrNotAvailable
@ -98,14 +98,32 @@ func (v *Provider) Range() (int64, error) {
return 0, err
}
if s, ok := lookup(data, FieldRange, FieldRangePrimary); ok {
f, err := strconv.ParseFloat(s, 64)
if p := lookup(data, FieldRange, FieldRangePrimary); p != nil {
f, err := strconv.ParseFloat(p.Value, 64)
return int64(f), err
}
return 0, api.ErrNotAvailable
}
var _ api.VehicleFinishTimer = (*Provider)(nil)
// FinishTime implements the api.VehicleFinishTimer interface
func (v *Provider) FinishTime() (time.Time, error) {
data, err := v.statusG()
if err != nil {
return time.Time{}, err
}
if p := lookup(data, FieldRemainingTime); p != nil && p.Value != "65535" {
if v, err := strconv.ParseInt(p.Value, 0, 64); err == nil {
return p.Timestamp.Add(time.Duration(v) * time.Minute), nil
}
}
return time.Time{}, api.ErrNotAvailable
}
var _ api.VehicleOdometer = (*Provider)(nil)
// Odometer implements the api.VehicleOdometer interface
@ -115,8 +133,8 @@ func (v *Provider) Odometer() (float64, error) {
return 0, err
}
if s, ok := lookup(data, FieldOdometer); ok {
return strconv.ParseFloat(s, 64)
if p := lookup(data, FieldOdometer); p != nil {
return strconv.ParseFloat(p.Value, 64)
}
return 0, api.ErrNotAvailable
@ -133,11 +151,11 @@ func (v *Provider) Status() (api.ChargeStatus, error) {
return status, err
}
if s, ok := lookup(data, FieldPlugState); ok && strings.EqualFold(s, "connected") {
if p := lookup(data, FieldPlugState); p != nil && strings.EqualFold(p.Value, "connected") {
status = api.StatusB
}
if s, ok := lookup(data, FieldChargingState); ok && strings.EqualFold(s, "charging") {
if p := lookup(data, FieldChargingState); p != nil && strings.EqualFold(p.Value, "charging") {
status = api.StatusC
}
@ -153,8 +171,8 @@ func (v *Provider) GetLimitSoc() (int64, error) {
return 0, err
}
if s, ok := lookup(data, FieldTargetSoc); ok {
f, err := strconv.ParseFloat(s, 64)
if p := lookup(data, FieldTargetSoc); p != nil {
f, err := strconv.ParseFloat(p.Value, 64)
return int64(f), err
}

View file

@ -66,12 +66,8 @@ func (s *store) update() (time.Time, error) {
var newest time.Time
for _, d := range list {
t, err := d.time()
if err != nil {
return time.Time{}, err
}
if t.After(newest) {
newest = t
if d.CreatedOn.After(newest) {
newest = d.CreatedOn
}
}
@ -94,8 +90,8 @@ func (s *store) update() (time.Time, error) {
merge(s.data, data)
// advance the high-water mark so this dataset is never downloaded again
if d.Timestamp.After(s.after) {
s.after = d.Timestamp
if d.CreatedOn.After(s.after) {
s.after = d.CreatedOn
}
if !initial {
@ -146,7 +142,7 @@ func pending(content []dataset, after time.Time) []dataset {
res := make([]dataset, 0, len(content))
for _, d := range content {
if d.Timestamp.After(after) {
if d.CreatedOn.After(after) {
res = append(res, d)
}
}

View file

@ -5,7 +5,6 @@ import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"slices"
"strings"
@ -78,26 +77,7 @@ func (v Vehicle) Name() string {
// createdOn field.
type dataset struct {
Name string `json:"name"`
CreatedOn string `json:"createdOn"`
Timestamp time.Time `json:"-"`
}
// nameTime parses the compact timestamp the portal prefixes to a dataset file
// name, e.g. 20260531102941_WAUZZZ..._no_content_found.zip.
func nameTime(name string) (time.Time, error) {
prefix, _, _ := strings.Cut(name, "_")
return time.Parse("20060102150405", prefix)
}
// time parses the delivery time the dataset carries. The portal embeds it in the
// file name and also delivers it as the createdOn field; the file name is
// preferred and createdOn is the fallback. An error is returned when neither
// carries a parseable timestamp.
func (d dataset) time() (time.Time, error) {
if t, err := nameTime(d.Name); err == nil {
return t, nil
}
return time.Parse(time.RFC3339, d.CreatedOn)
CreatedOn time.Time `json:"createdOn"`
}
// dataPoint is a single data point as delivered in the dataset JSON document
@ -129,6 +109,7 @@ const (
FieldChargingState = "charging_state"
FieldPlugState = "plug_state"
FieldTargetSoc = "settings.target_soc"
FieldRemainingTime = "remaining_charging_time"
)
// contentDatasets returns the datasets that actually carry content, with their
@ -143,17 +124,11 @@ func contentDatasets(list []dataset) ([]dataset, error) {
continue
}
t, err := d.time()
if err != nil {
return nil, fmt.Errorf("dataset %q: %w", d.Name, err)
}
d.Timestamp = t
content = append(content, d)
}
slices.SortStableFunc(content, func(a, b dataset) int {
return a.Timestamp.Compare(b.Timestamp)
return a.CreatedOn.Compare(b.CreatedOn)
})
return content, nil