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,
}