124 lines
2.6 KiB
Go
124 lines
2.6 KiB
Go
package tariff
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/benbjohnson/clock"
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/tariff/fixed"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/jinzhu/now"
|
|
)
|
|
|
|
type Fixed struct {
|
|
*embed
|
|
clock clock.Clock
|
|
zones fixed.Zones
|
|
dynamic bool
|
|
}
|
|
|
|
var _ api.Tariff = (*Fixed)(nil)
|
|
|
|
func init() {
|
|
registry.Add("fixed", NewFixedFromConfig)
|
|
}
|
|
|
|
func NewFixedFromConfig(other map[string]any) (api.Tariff, error) {
|
|
var cc struct {
|
|
embed `mapstructure:",squash"`
|
|
Price float64
|
|
Zones []fixed.ZoneSpec
|
|
}
|
|
|
|
if err := util.DecodeOther(other, &cc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := cc.embed.init(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
zones, err := fixed.ParseZones(cc.Zones)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
t := &Fixed{
|
|
embed: &cc.embed,
|
|
clock: clock.New(),
|
|
dynamic: len(cc.Zones) >= 1 || len(cc.ChargesZones_) >= 1,
|
|
zones: zones,
|
|
}
|
|
|
|
sort.Sort(t.zones)
|
|
|
|
// prepend catch-all zone
|
|
t.zones = append([]fixed.Zone{
|
|
{Price: cc.Price}, // full week is implicit
|
|
}, t.zones...)
|
|
|
|
return t, nil
|
|
}
|
|
|
|
// Rates implements the api.Tariff interface
|
|
func (t *Fixed) Rates() (api.Rates, error) {
|
|
var res api.Rates
|
|
|
|
start := now.With(t.clock.Now().Local()).BeginningOfDay()
|
|
for i := range 7 {
|
|
dayStart := start.AddDate(0, 0, i)
|
|
dow := fixed.Day((int(start.Weekday()) + i) % 7)
|
|
month := fixed.Month(dayStart.Month() - 1)
|
|
|
|
zones := t.zones.ForDayAndMonth(dow, month)
|
|
if len(zones) == 0 {
|
|
return nil, fmt.Errorf("no zones for weekday %d", dow)
|
|
}
|
|
|
|
// include chargesZones boundaries so rate changes there are not swallowed by the coarser price zone markers
|
|
markers := append(slices.Clone(zones), t.chargesZones.ForDayAndMonth(dow, month)...).TimeTableMarkers()
|
|
|
|
for i, m := range markers {
|
|
ts := dayStart.Add(time.Minute * time.Duration(m.Minutes()))
|
|
|
|
var zone *fixed.Zone
|
|
for j := len(zones) - 1; j >= 0; j-- {
|
|
if zones[j].Hours.Contains(m) {
|
|
zone = &zones[j]
|
|
break
|
|
}
|
|
}
|
|
|
|
if zone == nil {
|
|
return nil, fmt.Errorf("could not find zone for %02d:%02d", m.Hour, m.Min)
|
|
}
|
|
|
|
// end rate at end of day or next marker
|
|
end := dayStart.AddDate(0, 0, 1)
|
|
if i+1 < len(markers) {
|
|
end = dayStart.Add(time.Minute * time.Duration(markers[i+1].Minutes()))
|
|
}
|
|
|
|
rate := api.Rate{
|
|
Start: ts,
|
|
End: end,
|
|
Value: t.totalPrice(zone.Price, ts),
|
|
}
|
|
|
|
res = append(res, rate)
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// Type implements the api.Tariff interface
|
|
func (t *Fixed) Type() api.TariffType {
|
|
if t.dynamic {
|
|
return api.TariffTypePriceForecast
|
|
}
|
|
return api.TariffTypePriceStatic
|
|
}
|