Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solar forecast: keep today's rates when merging #19069

Merged
merged 3 commits into from
Feb 22, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions tariff/helper.go
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import (
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/config"
"github.com/evcc-io/evcc/util/request"
"github.com/jinzhu/now"
)

// Name returns the tariff type name
@@ -40,9 +41,13 @@ func backoffPermanentError(err error) error {
return err
}

// mergeRates merges new rates into existing rates,
// keeping current slots from the existing rates.
// mergeRates blends new and existing rates, keeping existing rates after current hour
func mergeRates(data *util.Monitor[api.Rates], new api.Rates) {
mergeRatesAfter(data, new, now.With(time.Now()).BeginningOfHour())
}

// mergeRatesAfter blends new and existing rates, keeping existing rates after timestamp
func mergeRatesAfter(data *util.Monitor[api.Rates], new api.Rates, now time.Time) {
new.Sort()

var newStart time.Time
@@ -51,15 +56,18 @@ func mergeRates(data *util.Monitor[api.Rates], new api.Rates) {
}

data.SetFunc(func(old api.Rates) api.Rates {
now := time.Now()

var between api.Rates
for _, r := range old {
if (r.Start.Before(newStart) || newStart.IsZero()) && r.End.After(now) {
if (r.Start.Before(newStart) && !r.Start.Before(now) || newStart.IsZero()) && r.End.After(now) {
between = append(between, r)
}
}

return append(between, new...)
})
}

// BeginningOfDay returns the beginning of the current day
func BeginningOfDay() time.Time {
return now.With(time.Now()).BeginningOfDay()
}
55 changes: 55 additions & 0 deletions tariff/helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package tariff

import (
"testing"
"time"

"github.com/benbjohnson/clock"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/jinzhu/now"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestMergeRatesAfter(t *testing.T) {
clock := clock.NewMock()
rate := func(start int, val float64) api.Rate {
return api.Rate{
Start: clock.Now().Add(time.Duration(start) * time.Hour),
End: clock.Now().Add(time.Duration(start+1) * time.Hour),
Price: val,
}
}

old := api.Rates{rate(1, 1), rate(2, 2)}
new := api.Rates{rate(2, 2), rate(3, 3)}
combined := api.Rates{rate(1, 1), rate(2, 2), rate(3, 3)}

data := util.NewMonitor[api.Rates](time.Hour)
data.WithClock(clock)

data.Set(old)
res, err := data.Get()
require.NoError(t, err)
assert.Equal(t, old, res)

for i, tc := range []struct {
new, expected api.Rates
ts time.Time
}{
{new, combined, clock.Now()},
{nil, combined, clock.Now()},
{new, combined, clock.Now().Add(time.Hour)},
{new, combined, now.With(clock.Now().Add(time.Hour + 30*time.Minute)).BeginningOfHour()},
{new, new, clock.Now().Add(2 * time.Hour)},
} {
t.Logf("%d. %+v", i+1, tc)

mergeRatesAfter(data, tc.new, tc.ts)

res, err := data.Get()
require.NoError(t, err)
assert.Equal(t, tc.expected, res)
}
}
2 changes: 1 addition & 1 deletion tariff/solcast.go
Original file line number Diff line number Diff line change
@@ -108,7 +108,7 @@ func (t *Solcast) run(interval time.Duration, done chan error) {
data = append(data, rr)
}

mergeRates(t.data, data)
mergeRatesAfter(t.data, data, BeginningOfDay())
once.Do(func() { close(done) })
}
}
8 changes: 7 additions & 1 deletion tariff/tariff.go
Original file line number Diff line number Diff line change
@@ -112,7 +112,13 @@ func (t *Tariff) run(forecastG func() (string, error), done chan error, interval
continue
}

mergeRates(t.data, data)
// only prune rates older than current period
periodStart := now.With(time.Now()).BeginningOfHour()
if t.typ == api.TariffTypeSolar {
periodStart = BeginningOfDay()
}
mergeRatesAfter(t.data, data, periodStart)

once.Do(func() { close(done) })
}
}
18 changes: 14 additions & 4 deletions util/monitor.go
Original file line number Diff line number Diff line change
@@ -4,12 +4,14 @@ import (
"sync"
"time"

"github.com/benbjohnson/clock"
"github.com/evcc-io/evcc/api"
)

// Monitor monitors values for regular updates
type Monitor[T any] struct {
val T
clock clock.Clock
mu sync.RWMutex
once sync.Once
done chan struct{}
@@ -19,10 +21,18 @@ type Monitor[T any] struct {

// NewMonitor created a new monitor with given timeout
func NewMonitor[T any](timeout time.Duration) *Monitor[T] {
return &Monitor[T]{
res := &Monitor[T]{
clock: clock.New(),
done: make(chan struct{}),
timeout: timeout,
}

return res
}

// WithClock sets the a clock for debugging
func (m *Monitor[T]) WithClock(clock clock.Clock) {
m.clock = clock
}

// Set updates the current value and timestamp
@@ -36,7 +46,7 @@ func (m *Monitor[T]) SetFunc(set func(T) T) {
defer m.mu.Unlock()

m.val = set(m.val)
m.updated = time.Now()
m.updated = m.clock.Now()

m.once.Do(func() { close(m.done) })
}
@@ -66,7 +76,7 @@ func (m *Monitor[T]) GetFunc(get func(T)) error {
}
}

if time.Since(m.updated) > m.timeout {
if m.clock.Since(m.updated) > m.timeout {
err := api.ErrOutdated

// wait once on very first call
@@ -83,7 +93,7 @@ func (m *Monitor[T]) GetFunc(get func(T)) error {
case <-m.done:
// got value and updated timestamp
err = nil
case <-time.After(m.timeout):
case <-m.clock.After(m.timeout):
}

m.mu.RLock()