diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 4fcd1b906a..045ebb20d7 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -149,6 +149,8 @@ jobs: - name: Lint uses: golangci/golangci-lint-action@v9 + env: + GOEXPERIMENT: jsonv2 with: version: latest args: --timeout 5m diff --git a/Makefile b/Makefile index 20159a6862..6ca6cc5880 100644 --- a/Makefile +++ b/Makefile @@ -46,23 +46,23 @@ ui:: npm run build assets:: - go generate ./... + GOEXPERIMENT=jsonv2 go generate ./... docs:: - go generate github.com/evcc-io/evcc/util/templates/... + GOEXPERIMENT=jsonv2 go generate github.com/evcc-io/evcc/util/templates/... lint:: - golangci-lint run - go tool modernize -test -c 0 -stringsbuilder=false -omitzero=false ./... + GOEXPERIMENT=jsonv2 golangci-lint run + GOEXPERIMENT=jsonv2 go tool modernize -test -c 0 -stringsbuilder=false -omitzero=false ./... modernize: - go tool modernize -test -fix -stringsbuilder=false -omitzero=false ./... + GOEXPERIMENT=jsonv2 go tool modernize -test -fix -stringsbuilder=false -omitzero=false ./... lint-ui:: npm run lint license:: - go run github.com/google/go-licenses/v2@latest check \ + GOEXPERIMENT=jsonv2 go run github.com/google/go-licenses/v2@latest check \ --ignore github.com/cespare/xxhash \ --ignore github.com/coder/websocket \ --ignore github.com/cronokirby/saferith \ @@ -80,7 +80,7 @@ test-ui:: test:: @echo "Running testsuite" - CGO_ENABLED=0 go test $(BUILD_TAGS) ./... + GOEXPERIMENT=jsonv2 CGO_ENABLED=0 go test $(BUILD_TAGS) ./... porcelain:: gofmt -w -l $$(find . -name '*.go') @@ -89,7 +89,7 @@ porcelain:: build:: @echo Version: $(VERSION) $(SHA) $(BUILD_DATE) - CGO_ENABLED=0 go build -v $(BUILD_TAGS) $(BUILD_ARGS) + GOEXPERIMENT=jsonv2 CGO_ENABLED=0 go build -v $(BUILD_TAGS) $(BUILD_ARGS) snapshot:: goreleaser --snapshot --skip publish --clean diff --git a/assets/js/components/Sessions/SessionDetailsModal.vue b/assets/js/components/Sessions/SessionDetailsModal.vue index 60d0e643d7..13eae287b0 100644 --- a/assets/js/components/Sessions/SessionDetailsModal.vue +++ b/assets/js/components/Sessions/SessionDetailsModal.vue @@ -80,7 +80,7 @@ ) }}
- {{ fmtDurationNs(session.chargeDuration) }} + {{ fmtDuration(session.chargeDuration) }} (~{{ fmtW(avgPower) }})
@@ -194,8 +194,7 @@ export default defineComponent({ return this.session.chargedEnergy * 1e3; }, avgPower() { - const hours = this.session.chargeDuration / 1e9 / 3600; - return this.chargedEnergy / hours; + return this.chargedEnergy / (this.session.chargeDuration / 3600); }, solarEnergy() { return this.chargedEnergy * (this.session.solarPercentage / 100); diff --git a/assets/js/components/Sessions/SessionTable.vue b/assets/js/components/Sessions/SessionTable.vue index e05577fe5b..d29483e619 100644 --- a/assets/js/components/Sessions/SessionTable.vue +++ b/assets/js/components/Sessions/SessionTable.vue @@ -242,7 +242,7 @@ export default defineComponent({ unit: "h:mm", total: this.chargeDuration, value: (session) => session.chargeDuration, - format: (value) => this.fmtDurationNs(value, false, "h"), + format: (value) => this.fmtDuration(value, false, "h"), }, { name: "avgPower", @@ -250,7 +250,7 @@ export default defineComponent({ total: this.avgPower, value: (session) => { if (session.chargedEnergy && session.chargeDuration) { - return session.chargedEnergy / this.nsToHours(session.chargeDuration); + return session.chargedEnergy / (session.chargeDuration / 3600); } return null; }, @@ -339,7 +339,7 @@ export default defineComponent({ .reduce( (total, s) => { total.energy += s.chargedEnergy; - total.hours += this.nsToHours(s.chargeDuration); + total.hours += s.chargeDuration / 3600; return total; }, { energy: 0, hours: 0 } @@ -398,9 +398,6 @@ export default defineComponent({ }, }, methods: { - nsToHours(ns: number) { - return ns / 1e9 / 3600; - }, filterByLoadpoint(session: Session) { return !this.loadpointFilter || session.loadpoint === this.loadpointFilter; }, diff --git a/core/loadpoint/config.go b/core/loadpoint/config.go index 3bc38d83d2..7ef83962cc 100644 --- a/core/loadpoint/config.go +++ b/core/loadpoint/config.go @@ -17,19 +17,19 @@ type StaticConfig struct { type DynamicConfig struct { // dynamic config - Title string `json:"title"` - DefaultMode string `json:"defaultMode"` - Priority int `json:"priority"` - PhasesConfigured int `json:"phasesConfigured"` - MinCurrent float64 `json:"minCurrent"` - MaxCurrent float64 `json:"maxCurrent"` - SmartCostLimit *float64 `json:"smartCostLimit"` - SmartFeedInPriorityLimit *float64 `json:"smartFeedInPriorityLimit"` - PlanEnergy float64 `json:"planEnergy"` - PlanTime time.Time `json:"planTime"` - PlanPrecondition int64 `json:"planPrecondition"` - LimitEnergy float64 `json:"limitEnergy"` - LimitSoc int `json:"limitSoc"` + Title string `json:"title"` + DefaultMode string `json:"defaultMode"` + Priority int `json:"priority"` + PhasesConfigured int `json:"phasesConfigured"` + MinCurrent float64 `json:"minCurrent"` + MaxCurrent float64 `json:"maxCurrent"` + SmartCostLimit *float64 `json:"smartCostLimit"` + SmartFeedInPriorityLimit *float64 `json:"smartFeedInPriorityLimit"` + PlanEnergy float64 `json:"planEnergy"` + PlanTime time.Time `json:"planTime"` + PlanPrecondition time.Duration `json:"planPrecondition"` + LimitEnergy float64 `json:"limitEnergy"` + LimitSoc int `json:"limitSoc"` Thresholds ThresholdsConfig `json:"thresholds"` Soc SocConfig `json:"soc"` @@ -59,7 +59,7 @@ func (payload DynamicConfig) Apply(lp API) error { lp.SetSmartCostLimit(payload.SmartCostLimit) lp.SetSmartFeedInPriorityLimit(payload.SmartFeedInPriorityLimit) lp.SetThresholds(payload.Thresholds) - lp.SetPlanEnergy(payload.PlanTime, time.Duration(payload.PlanPrecondition)*time.Second, payload.PlanEnergy) + lp.SetPlanEnergy(payload.PlanTime, payload.PlanPrecondition, payload.PlanEnergy) lp.SetLimitEnergy(payload.LimitEnergy) lp.SetLimitSoc(payload.LimitSoc) diff --git a/server/http_config_loadpoint_handler.go b/server/http_config_loadpoint_handler.go index 90972c6bbc..3e71b6759c 100644 --- a/server/http_config_loadpoint_handler.go +++ b/server/http_config_loadpoint_handler.go @@ -41,7 +41,7 @@ func getLoadpointDynamicConfig(lp loadpoint.API) loadpoint.DynamicConfig { Soc: lp.GetSocConfig(), PlanEnergy: planEnergy, PlanTime: planTime, - PlanPrecondition: int64(planPrecondition.Seconds()), + PlanPrecondition: planPrecondition, LimitEnergy: lp.GetLimitEnergy(), LimitSoc: lp.GetLimitSoc(), } diff --git a/server/http_loadpoint_handler.go b/server/http_loadpoint_handler.go index a8deae57c2..34709bd9fb 100644 --- a/server/http_loadpoint_handler.go +++ b/server/http_loadpoint_handler.go @@ -16,20 +16,11 @@ import ( ) type PlanResponse struct { - PlanId int `json:"planId"` - PlanTime time.Time `json:"planTime"` - Duration int64 `json:"duration"` - Precondition int64 `json:"precondition"` - Plan api.Rates `json:"plan"` - Power float64 `json:"power"` -} - -type PlanPreviewResponse struct { - PlanTime time.Time `json:"planTime"` - Duration int64 `json:"duration"` - Precondition int64 `json:"precondition"` - Plan api.Rates `json:"plan"` - Power float64 `json:"power"` + PlanTime time.Time `json:"planTime"` + Duration time.Duration `json:"duration"` + Precondition time.Duration `json:"precondition"` + Plan api.Rates `json:"plan"` + Power float64 `json:"power"` } // planHandler returns the current plan @@ -44,15 +35,19 @@ func planHandler(lp loadpoint.API) http.HandlerFunc { requiredDuration := lp.GetPlanRequiredDuration(goal, maxPower) plan := lp.GetPlan(planTime, requiredDuration, precondition) - res := PlanResponse{ - PlanId: id, - PlanTime: planTime, - Duration: int64(requiredDuration.Seconds()), - Precondition: int64(precondition.Seconds()), - Plan: plan, - Power: maxPower, + res := struct { + PlanId int `json:"planId"` + PlanResponse `json:",inline"` + }{ + PlanId: id, + PlanResponse: PlanResponse{ + PlanTime: planTime, + Duration: requiredDuration, + Precondition: precondition, + Plan: plan, + Power: maxPower, + }, } - jsonWrite(w, res) } } @@ -101,10 +96,10 @@ func staticPlanPreviewHandler(lp loadpoint.API) http.HandlerFunc { requiredDuration := lp.GetPlanRequiredDuration(goal, maxPower) plan := lp.GetPlan(planTime, requiredDuration, precondition) - res := PlanPreviewResponse{ + res := PlanResponse{ PlanTime: planTime, - Duration: int64(requiredDuration.Seconds()), - Precondition: int64(precondition.Seconds()), + Duration: requiredDuration, + Precondition: precondition, Plan: plan, Power: maxPower, } @@ -153,10 +148,10 @@ func repeatingPlanPreviewHandler(lp loadpoint.API) http.HandlerFunc { requiredDuration := lp.GetPlanRequiredDuration(soc, maxPower) plan := lp.GetPlan(planTime, requiredDuration, precondition) - res := PlanPreviewResponse{ + res := PlanResponse{ PlanTime: planTime, - Duration: int64(requiredDuration.Seconds()), - Precondition: int64(precondition.Seconds()), + Duration: requiredDuration, + Precondition: precondition, Plan: plan, Power: maxPower, } @@ -197,12 +192,12 @@ func planEnergyHandler(lp loadpoint.API) http.HandlerFunc { ts, precondition, energy := lp.GetPlanEnergy() res := struct { - Energy float64 `json:"energy"` - Precondition int64 `json:"precondition"` - Time time.Time `json:"time"` + Energy float64 `json:"energy"` + Precondition time.Duration `json:"precondition"` + Time time.Time `json:"time"` }{ Energy: energy, - Precondition: int64(precondition.Seconds()), + Precondition: precondition, Time: ts, } diff --git a/server/http_site_handler.go b/server/http_site_handler.go index eca99afa1b..788bc4de28 100644 --- a/server/http_site_handler.go +++ b/server/http_site_handler.go @@ -1,7 +1,7 @@ package server import ( - "encoding/json" + "encoding/json/v2" "errors" "fmt" "io" @@ -80,8 +80,22 @@ func jsonHandler(h http.Handler) http.Handler { }) } +func jsonMarshalers() *json.Marshalers { + return json.JoinMarshalers( + json.MarshalFunc(func(d time.Duration) ([]byte, error) { + return fmt.Append(nil, int(d.Seconds())), nil + }), + json.MarshalFunc(func(ts time.Time) ([]byte, error) { + if ts.IsZero() { + return []byte("null"), nil + } + return json.Marshal(ts) + }), + ) +} + func jsonWrite(w http.ResponseWriter, data any) { - json.NewEncoder(w).Encode(data) + json.MarshalWrite(w, data, json.WithMarshalers(jsonMarshalers())) } func jsonError(w http.ResponseWriter, status int, err error) { @@ -336,7 +350,7 @@ func adminPasswordValid(authObject auth.Auth, password string) bool { func getBackup(authObject auth.Auth) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req loginRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if err := json.UnmarshalRead(r.Body, &req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -463,7 +477,7 @@ func resetDatabase(authObject auth.Auth, shutdown func()) http.HandlerFunc { Settings bool `json:"settings"` } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if err := json.UnmarshalRead(r.Body, &req); err != nil { jsonError(w, http.StatusBadRequest, err) return } diff --git a/server/http_vehicle_handler.go b/server/http_vehicle_handler.go index 6a2f9169e1..dc914d4ac2 100644 --- a/server/http_vehicle_handler.go +++ b/server/http_vehicle_handler.go @@ -107,12 +107,12 @@ func planSocHandler(site site.API) http.HandlerFunc { ts, precondition, soc = v.GetPlanSoc() res := struct { - Soc int `json:"soc"` - Precondition int64 `json:"precondition"` - Time time.Time `json:"time"` + Soc int `json:"soc"` + Precondition time.Duration `json:"precondition"` + Time time.Time `json:"time"` }{ Soc: soc, - Precondition: int64(precondition.Seconds()), + Precondition: precondition, Time: ts, }