diff --git a/tariff/embed.go b/tariff/embed.go index 5b171e2205..7ba2dbe2b7 100644 --- a/tariff/embed.go +++ b/tariff/embed.go @@ -1,10 +1,27 @@ package tariff +import "github.com/evcc-io/evcc/tariff/fixed" + type embed struct { - Charges float64 `mapstructure:"charges"` - Tax float64 `mapstructure:"tax"` + Charges float64 `mapstructure:"charges"` + Tax float64 `mapstructure:"tax"` + Zones fixed.ZoneConfig `mapstructure:"zones"` + zones fixed.Zones +} + +func (t *embed) parse() error { + zz, err := t.Zones.Parse(t.Charges) + if err == nil { + t.zones = zz + } + return err } func (t *embed) totalPrice(price float64) float64 { return (price + t.Charges) * (1 + t.Tax) } + +// TODO remove +func (t *embed) totalPriceZonesCharges(price, charges float64) float64 { + return (price + charges) * (1 + t.Tax) +} diff --git a/tariff/energinet.go b/tariff/energinet.go index 8b4f94cc5b..cd3c911b6b 100644 --- a/tariff/energinet.go +++ b/tariff/energinet.go @@ -42,6 +42,10 @@ func NewEnerginetFromConfig(other map[string]interface{}) (api.Tariff, error) { return nil, errors.New("missing region") } + if err := cc.embed.parse(); err != nil { + return nil, err + } + t := &Energinet{ embed: &cc.embed, log: util.NewLogger("energinet"), @@ -79,13 +83,28 @@ func (t *Energinet) run(done chan error) { continue } + charges, err := t.zones.Rates(time.Now()) + if err != nil { + once.Do(func() { done <- err }) + t.log.ERROR.Println(err) + continue + } + data := make(api.Rates, 0, len(res.Records)) for _, r := range res.Records { date, _ := time.Parse("2006-01-02T15:04:05", r.HourUTC) + + charge, err := charges.Current(date) + if err != nil { + once.Do(func() { done <- err }) + t.log.ERROR.Println(err) + continue + } + ar := api.Rate{ Start: date.Local(), End: date.Add(time.Hour).Local(), - Price: t.totalPrice(r.SpotPriceDKK / 1e3), + Price: t.totalPriceZonesCharges(r.SpotPriceDKK/1e3, charge.Price), } data = append(data, ar) } diff --git a/tariff/entsoe.go b/tariff/entsoe.go index 7394e15623..4a010fbe26 100644 --- a/tariff/entsoe.go +++ b/tariff/entsoe.go @@ -57,6 +57,10 @@ func NewEntsoeFromConfig(other map[string]interface{}) (api.Tariff, error) { return nil, err } + if err := cc.embed.parse(); err != nil { + return nil, err + } + log := util.NewLogger("entsoe").Redact(cc.Securitytoken) t := &Entsoe{ @@ -85,7 +89,6 @@ func NewEntsoeFromConfig(other map[string]interface{}) (api.Tariff, error) { func (t *Entsoe) run(done chan error) { var once sync.Once - bo := newBackoff() // Data updated by ESO every half hour, but we only need data every hour to stay current. @@ -154,12 +157,26 @@ func (t *Entsoe) run(done chan error) { continue } + charges, err := t.zones.Rates(time.Now()) + if err != nil { + once.Do(func() { done <- err }) + t.log.ERROR.Println(err) + continue + } + data := make(api.Rates, 0, len(res)) for _, r := range res { + charge, err := charges.Current(r.Start) + if err != nil { + once.Do(func() { done <- err }) + t.log.ERROR.Println(err) + continue + } + ar := api.Rate{ Start: r.Start, End: r.End, - Price: t.totalPrice(r.Value), + Price: t.totalPriceZonesCharges(r.Value, charge.Price), } data = append(data, ar) } diff --git a/tariff/fixed.go b/tariff/fixed.go index 71f5753a10..654737dc32 100644 --- a/tariff/fixed.go +++ b/tariff/fixed.go @@ -1,15 +1,10 @@ package tariff import ( - "fmt" - "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 { @@ -27,107 +22,30 @@ func init() { func NewFixedFromConfig(other map[string]interface{}) (api.Tariff, error) { var cc struct { Price float64 - Zones []struct { - Price float64 - Days, Hours string - } + Zones fixed.ZoneConfig } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } + zones, err := cc.Zones.Parse(cc.Price) + if err != nil { + return nil, err + } + t := &Fixed{ clock: clock.New(), dynamic: len(cc.Zones) >= 1, + zones: zones, } - for _, z := range cc.Zones { - days, err := fixed.ParseDays(z.Days) - if err != nil { - return nil, err - } - - hours, err := fixed.ParseTimeRanges(z.Hours) - if err != nil && z.Hours != "" { - return nil, err - } - - if len(hours) == 0 { - t.zones = append(t.zones, fixed.Zone{ - Price: z.Price, - Days: days, - }) - continue - } - - for _, h := range hours { - t.zones = append(t.zones, fixed.Zone{ - Price: z.Price, - Days: days, - Hours: h, - }) - } - } - - 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 := 0; i < 7; i++ { - dow := fixed.Day((int(start.Weekday()) + i) % 7) - - zones := t.zones.ForDay(dow) - if len(zones) == 0 { - return nil, fmt.Errorf("no zones for weekday %d", dow) - } - - dayStart := start.AddDate(0, 0, i) - markers := zones.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{ - Price: zone.Price, - Start: ts, - End: end, - } - - res = append(res, rate) - } - } - - return res, nil + return t.zones.Rates(t.clock.Now()) } // Type implements the api.Tariff interface diff --git a/tariff/fixed/config.go b/tariff/fixed/config.go new file mode 100644 index 0000000000..ceca97745f --- /dev/null +++ b/tariff/fixed/config.go @@ -0,0 +1,49 @@ +package fixed + +import "sort" + +type ZoneConfig []struct { + Price float64 + Days, Hours string +} + +func (zz ZoneConfig) Parse(base float64) (Zones, error) { + var res Zones + + for _, z := range zz { + days, err := ParseDays(z.Days) + if err != nil { + return nil, err + } + + hours, err := ParseTimeRanges(z.Hours) + if err != nil && z.Hours != "" { + return nil, err + } + + if len(hours) == 0 { + res = append(res, Zone{ + Price: z.Price, + Days: days, + }) + continue + } + + for _, h := range hours { + res = append(res, Zone{ + Price: z.Price, + Days: days, + Hours: h, + }) + } + } + + sort.Sort(res) + + // prepend catch-all zone + res = append([]Zone{ + {Price: base}, // full week is implicit + }, res...) + + return res, nil +} diff --git a/tariff/fixed/zone.go b/tariff/fixed/zone.go index ba67c59bb4..cdeff87125 100644 --- a/tariff/fixed/zone.go +++ b/tariff/fixed/zone.go @@ -1,7 +1,12 @@ package fixed import ( + "fmt" "slices" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/jinzhu/now" ) type Zone struct { @@ -75,3 +80,53 @@ HOURS: return res } + +// Rates implements the api.Tariff interface +func (r Zones) Rates(tNow time.Time) (api.Rates, error) { + var res api.Rates + + start := now.With(tNow.Local()).BeginningOfDay() + for i := 0; i < 7; i++ { + dow := Day((int(start.Weekday()) + i) % 7) + + zones := r.ForDay(dow) + if len(zones) == 0 { + return nil, fmt.Errorf("no zones for weekday %d", dow) + } + + dayStart := start.AddDate(0, 0, i) + markers := zones.TimeTableMarkers() + + for i, m := range markers { + ts := dayStart.Add(time.Minute * time.Duration(m.Minutes())) + + var zone *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{ + Price: zone.Price, + Start: ts, + End: end, + } + + res = append(res, rate) + } + } + + return res, nil +}