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

Tariff: add dynamic/zones charges #12871

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 19 additions & 2 deletions tariff/embed.go
Original file line number Diff line number Diff line change
@@ -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)
}
21 changes: 20 additions & 1 deletion tariff/energinet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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)
}
Expand Down
21 changes: 19 additions & 2 deletions tariff/entsoe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down
98 changes: 8 additions & 90 deletions tariff/fixed.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
Expand Down
49 changes: 49 additions & 0 deletions tariff/fixed/config.go
Original file line number Diff line number Diff line change
@@ -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
}
55 changes: 55 additions & 0 deletions tariff/fixed/zone.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package fixed

import (
"fmt"
"slices"
"time"

"github.com/evcc-io/evcc/api"
"github.com/jinzhu/now"
)

type Zone struct {
Expand Down Expand Up @@ -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
}