API: publish forecast as arrays with unix timestamps (BC) (#32391)

This commit is contained in:
andig 2026-08-06 11:49:33 +02:00 • committed by GitHub
parent 9d0bc5a865
commit 45e781dac0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 361 additions and 223 deletions

View file

@ -10,6 +10,7 @@ import (
"github.com/evcc-io/evcc/tariff"
"github.com/evcc-io/evcc/util"
"github.com/jinzhu/now"
"github.com/samber/lo"
)
type solarDetails struct {
@ -25,6 +26,20 @@ type dailyDetails struct {
Complete bool `json:"complete"`
}
// forecastRates publishes rates as [start, end, value] with the timestamps in
// unix seconds. The forecast is the largest payload evcc sends and RFC3339
// timestamps are two thirds of it.
func forecastRates(rr api.Rates) [][]float64 {
// keep nil for empty rates: shards are published without omitempty
if len(rr) == 0 {
return nil
}
return lo.Map(rr, func(r api.Rate, _ int) []float64 {
return []float64{float64(r.Start.Unix()), float64(r.End.Unix()), r.Value}
})
}
// greenShare returns
// - the current green share, calculated for the part of the consumption between powerFrom and powerTo
// the consumption below powerFrom will get the available green power first
@ -101,18 +116,18 @@ func (site *Site) publishTariffs(greenShareHome float64, greenShareLoadpoints fl
}
fc := struct {
Co2 api.Rates `json:"co2,omitempty"`
FeedIn api.Rates `json:"feedin,omitempty"`
Grid api.Rates `json:"grid,omitempty"`
Planner api.Rates `json:"planner,omitempty"`
Co2 [][]float64 `json:"co2,omitempty"`
FeedIn [][]float64 `json:"feedin,omitempty"`
Grid [][]float64 `json:"grid,omitempty"`
Planner [][]float64 `json:"planner,omitempty"`
Solar *solarDetails `json:"solar,omitempty"`
Temperature api.Rates `json:"temperature,omitempty"`
Temperature [][]float64 `json:"temperature,omitempty"`
}{
Co2: tariff.Rates(site.GetTariff(api.TariffUsageCo2)),
FeedIn: tariff.Rates(site.GetTariff(api.TariffUsageFeedIn)),
Planner: tariff.Rates(site.GetTariff(api.TariffUsagePlanner)),
Grid: tariff.Rates(site.GetTariff(api.TariffUsageGrid)),
Temperature: tariff.Rates(site.GetTariff(api.TariffUsageTemperature)),
Co2: forecastRates(tariff.Rates(site.GetTariff(api.TariffUsageCo2))),
FeedIn: forecastRates(tariff.Rates(site.GetTariff(api.TariffUsageFeedIn))),
Planner: forecastRates(tariff.Rates(site.GetTariff(api.TariffUsagePlanner))),
Grid: forecastRates(tariff.Rates(site.GetTariff(api.TariffUsageGrid))),
Temperature: forecastRates(tariff.Rates(site.GetTariff(api.TariffUsageTemperature))),
}
// calculate adjusted solar rates

69
core/site_tariffs_test.go Normal file
View file

@ -0,0 +1,69 @@
package core
import (
"encoding/json"
"testing"
"time"
"github.com/evcc-io/evcc/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestForecastRates(t *testing.T) {
start := time.Unix(1735689600, 0)
for _, tc := range []struct {
desc string
rates api.Rates
want string
}{
{desc: "nil", rates: nil, want: "null"},
{desc: "empty", rates: api.Rates{}, want: "null"},
{
desc: "slots",
rates: api.Rates{
{Start: start, End: start.Add(time.Hour), Value: 0.25},
{Start: start.Add(time.Hour), End: start.Add(2 * time.Hour), Value: -0.1},
},
want: "[[1735689600,1735693200,0.25],[1735693200,1735696800,-0.1]]",
},
} {
t.Run(tc.desc, func(t *testing.T) {
b, err := json.Marshal(forecastRates(tc.rates))
require.NoError(t, err)
assert.Equal(t, tc.want, string(b))
})
}
}
func TestTimeseriesMarshal(t *testing.T) {
start := time.Unix(1735689600, 0)
for _, tc := range []struct {
desc string
ts timeseries
want string
}{
{desc: "nil", ts: nil, want: "null"},
{desc: "empty", ts: timeseries{}, want: "[]"},
{
desc: "entries",
ts: timeseries{
{Timestamp: start, Value: 1000},
{Timestamp: start.Add(time.Hour), Value: 0},
},
want: "[[1735689600,1000],[1735693200,0]]",
},
} {
t.Run(tc.desc, func(t *testing.T) {
b, err := json.Marshal(tc.ts)
require.NoError(t, err)
assert.Equal(t, tc.want, string(b))
b, err = tc.ts.MarshalBytes()
require.NoError(t, err)
assert.Equal(t, tc.want, string(b))
})
}
}

View file

@ -17,6 +17,20 @@ func (ts timeseries) MarshalBytes() ([]byte, error) {
return json.Marshal(ts)
}
// MarshalJSON publishes entries as [ts, val] with the timestamp in unix seconds
func (ts timeseries) MarshalJSON() ([]byte, error) {
if ts == nil {
return []byte("null"), nil
}
res := make([][]float64, 0, len(ts))
for _, e := range ts {
res = append(res, []float64{float64(e.Timestamp.Unix()), e.Value})
}
return json.Marshal(res)
}
type tsEntry struct {
Timestamp time.Time `json:"ts"`
Value float64 `json:"val"`