diff --git a/api/globalconfig/types.go b/api/globalconfig/types.go index 4e2ce2c04..d21127c80 100644 --- a/api/globalconfig/types.go +++ b/api/globalconfig/types.go @@ -140,6 +140,7 @@ type Tariffs struct { FeedIn config.Typed Co2 config.Typed Planner config.Typed + Solar config.Typed } type Network struct { diff --git a/api/tariff.go b/api/tariff.go index dcb5f6dbf..07bba41f7 100644 --- a/api/tariff.go +++ b/api/tariff.go @@ -1,6 +1,7 @@ package api //go:generate enumer -type TariffType -trimprefix TariffType -transform=lower -text +//go:generate enumer -type TariffUsage -trimprefix TariffUsage -transform=lower type TariffType int @@ -10,4 +11,16 @@ const ( TariffTypePriceDynamic TariffTypePriceForecast TariffTypeCo2 + TariffTypeSolar +) + +type TariffUsage int + +const ( + _ TariffUsage = iota + TariffUsageCo2 + TariffUsageFeedin + TariffUsageGrid + TariffUsagePlanner + TariffUsageSolar ) diff --git a/api/tarifftype_enumer.go b/api/tarifftype_enumer.go index b44f8cd7b..12272d9bc 100644 --- a/api/tarifftype_enumer.go +++ b/api/tarifftype_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _TariffTypeName = "pricestaticpricedynamicpriceforecastco2" +const _TariffTypeName = "pricestaticpricedynamicpriceforecastco2solar" -var _TariffTypeIndex = [...]uint8{0, 11, 23, 36, 39} +var _TariffTypeIndex = [...]uint8{0, 11, 23, 36, 39, 44} -const _TariffTypeLowerName = "pricestaticpricedynamicpriceforecastco2" +const _TariffTypeLowerName = "pricestaticpricedynamicpriceforecastco2solar" func (i TariffType) String() string { i -= 1 @@ -29,9 +29,10 @@ func _TariffTypeNoOp() { _ = x[TariffTypePriceDynamic-(2)] _ = x[TariffTypePriceForecast-(3)] _ = x[TariffTypeCo2-(4)] + _ = x[TariffTypeSolar-(5)] } -var _TariffTypeValues = []TariffType{TariffTypePriceStatic, TariffTypePriceDynamic, TariffTypePriceForecast, TariffTypeCo2} +var _TariffTypeValues = []TariffType{TariffTypePriceStatic, TariffTypePriceDynamic, TariffTypePriceForecast, TariffTypeCo2, TariffTypeSolar} var _TariffTypeNameToValueMap = map[string]TariffType{ _TariffTypeName[0:11]: TariffTypePriceStatic, @@ -42,6 +43,8 @@ var _TariffTypeNameToValueMap = map[string]TariffType{ _TariffTypeLowerName[23:36]: TariffTypePriceForecast, _TariffTypeName[36:39]: TariffTypeCo2, _TariffTypeLowerName[36:39]: TariffTypeCo2, + _TariffTypeName[39:44]: TariffTypeSolar, + _TariffTypeLowerName[39:44]: TariffTypeSolar, } var _TariffTypeNames = []string{ @@ -49,6 +52,7 @@ var _TariffTypeNames = []string{ _TariffTypeName[11:23], _TariffTypeName[23:36], _TariffTypeName[36:39], + _TariffTypeName[39:44], } // TariffTypeString retrieves an enum value from the enum constants string name. diff --git a/api/tariffusage_enumer.go b/api/tariffusage_enumer.go new file mode 100644 index 000000000..fcd32e6e9 --- /dev/null +++ b/api/tariffusage_enumer.go @@ -0,0 +1,91 @@ +// Code generated by "enumer -type TariffUsage -trimprefix TariffUsage -transform=lower"; DO NOT EDIT. + +package api + +import ( + "fmt" + "strings" +) + +const _TariffUsageName = "co2feedingridplannersolar" + +var _TariffUsageIndex = [...]uint8{0, 3, 9, 13, 20, 25} + +const _TariffUsageLowerName = "co2feedingridplannersolar" + +func (i TariffUsage) String() string { + i -= 1 + if i < 0 || i >= TariffUsage(len(_TariffUsageIndex)-1) { + return fmt.Sprintf("TariffUsage(%d)", i+1) + } + return _TariffUsageName[_TariffUsageIndex[i]:_TariffUsageIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _TariffUsageNoOp() { + var x [1]struct{} + _ = x[TariffUsageCo2-(1)] + _ = x[TariffUsageFeedin-(2)] + _ = x[TariffUsageGrid-(3)] + _ = x[TariffUsagePlanner-(4)] + _ = x[TariffUsageSolar-(5)] +} + +var _TariffUsageValues = []TariffUsage{TariffUsageCo2, TariffUsageFeedin, TariffUsageGrid, TariffUsagePlanner, TariffUsageSolar} + +var _TariffUsageNameToValueMap = map[string]TariffUsage{ + _TariffUsageName[0:3]: TariffUsageCo2, + _TariffUsageLowerName[0:3]: TariffUsageCo2, + _TariffUsageName[3:9]: TariffUsageFeedin, + _TariffUsageLowerName[3:9]: TariffUsageFeedin, + _TariffUsageName[9:13]: TariffUsageGrid, + _TariffUsageLowerName[9:13]: TariffUsageGrid, + _TariffUsageName[13:20]: TariffUsagePlanner, + _TariffUsageLowerName[13:20]: TariffUsagePlanner, + _TariffUsageName[20:25]: TariffUsageSolar, + _TariffUsageLowerName[20:25]: TariffUsageSolar, +} + +var _TariffUsageNames = []string{ + _TariffUsageName[0:3], + _TariffUsageName[3:9], + _TariffUsageName[9:13], + _TariffUsageName[13:20], + _TariffUsageName[20:25], +} + +// TariffUsageString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func TariffUsageString(s string) (TariffUsage, error) { + if val, ok := _TariffUsageNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _TariffUsageNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to TariffUsage values", s) +} + +// TariffUsageValues returns all values of the enum +func TariffUsageValues() []TariffUsage { + return _TariffUsageValues +} + +// TariffUsageStrings returns a slice of all String values of the enum +func TariffUsageStrings() []string { + strs := make([]string, len(_TariffUsageNames)) + copy(strs, _TariffUsageNames) + return strs +} + +// IsATariffUsage returns "true" if the value is listed in the enum definition. "false" otherwise +func (i TariffUsage) IsATariffUsage() bool { + for _, v := range _TariffUsageValues { + if i == v { + return true + } + } + return false +} diff --git a/cmd/setup.go b/cmd/setup.go index 4c35a6adf..ea517042e 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -755,11 +755,12 @@ func tariffInstance(name string, conf config.Typed) (api.Tariff, error) { return instance, nil } -func configureTariff(name string, conf config.Typed, t *api.Tariff) error { +func configureTariff(u api.TariffUsage, conf config.Typed, t *api.Tariff) error { if conf.Type == "" { return nil } + name := u.String() res, err := tariffInstance(name, conf) if err != nil { return &DeviceError{name, err} @@ -786,10 +787,11 @@ func configureTariffs(conf globalconfig.Tariffs) (*tariff.Tariffs, error) { } var eg errgroup.Group - eg.Go(func() error { return configureTariff("grid", conf.Grid, &tariffs.Grid) }) - eg.Go(func() error { return configureTariff("feedin", conf.FeedIn, &tariffs.FeedIn) }) - eg.Go(func() error { return configureTariff("co2", conf.Co2, &tariffs.Co2) }) - eg.Go(func() error { return configureTariff("planner", conf.Planner, &tariffs.Planner) }) + eg.Go(func() error { return configureTariff(api.TariffUsageGrid, conf.Grid, &tariffs.Grid) }) + eg.Go(func() error { return configureTariff(api.TariffUsageFeedin, conf.FeedIn, &tariffs.FeedIn) }) + eg.Go(func() error { return configureTariff(api.TariffUsageCo2, conf.Co2, &tariffs.Co2) }) + eg.Go(func() error { return configureTariff(api.TariffUsagePlanner, conf.Planner, &tariffs.Planner) }) + eg.Go(func() error { return configureTariff(api.TariffUsageSolar, conf.Solar, &tariffs.Solar) }) if err := eg.Wait(); err != nil { return nil, &ClassError{ClassTariff, err} diff --git a/cmd/tariff.go b/cmd/tariff.go index c4e949c58..c2fc5343d 100644 --- a/cmd/tariff.go +++ b/cmd/tariff.go @@ -42,12 +42,14 @@ func runTariff(cmd *cobra.Command, args []string) { name = args[0] } - for key, tf := range map[string]api.Tariff{ - "grid": tariffs.Grid, - "feedin": tariffs.FeedIn, - "co2": tariffs.Co2, - "planner": tariffs.Planner, + for u, tf := range map[api.TariffUsage]api.Tariff{ + api.TariffUsageGrid: tariffs.Grid, + api.TariffUsageFeedin: tariffs.FeedIn, + api.TariffUsageCo2: tariffs.Co2, + api.TariffUsagePlanner: tariffs.Planner, + api.TariffUsageSolar: tariffs.Solar, } { + key := u.String() if name != "" && key != name { continue } @@ -65,8 +67,20 @@ func runTariff(cmd *cobra.Command, args []string) { fatal(err) } + unit := "Price/Cost" + switch tf.Type() { + case api.TariffTypeCo2: + unit += "Footprint (gCO2/kWh)" + case api.TariffTypeSolar: + unit = "Yield (W)" + default: + if c := conf.Tariffs.Currency; c != "" { + unit += fmt.Sprintf(" (%s/kWh)", c) + } + } + tw := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) - fmt.Fprintln(tw, "From\tTo\tPrice/Cost") + fmt.Fprintln(tw, "From\tTo\t"+unit) const format = "2006-01-02 15:04:05" for _, r := range rates { diff --git a/core/keys/site.go b/core/keys/site.go index b121cf096..fe00adcc7 100644 --- a/core/keys/site.go +++ b/core/keys/site.go @@ -26,6 +26,7 @@ const ( TariffGrid = "tariffGrid" TariffPriceHome = "tariffPriceHome" TariffPriceLoadpoints = "tariffPriceLoadpoints" + TariffSolar = "tariffSolar" Vehicles = "vehicles" // meters diff --git a/core/site.go b/core/site.go index 27a6ff869..7e0f5ccdc 100644 --- a/core/site.go +++ b/core/site.go @@ -151,7 +151,7 @@ func (site *Site) Boot(log *util.Logger, loadpoints []*Loadpoint, tariffs *tarif }) } - tariff := site.GetTariff(PlannerTariff) + tariff := site.GetTariff(api.TariffUsagePlanner) // give loadpoints access to vehicles and database for _, lp := range loadpoints { @@ -757,8 +757,8 @@ func (site *Site) greenShare(powerFrom float64, powerTo float64) float64 { // effectivePrice calculates the real energy price based on self-produced and grid-imported energy. func (site *Site) effectivePrice(greenShare float64) *float64 { - if grid, err := site.tariffs.CurrentGridPrice(); err == nil { - feedin, err := site.tariffs.CurrentFeedInPrice() + if grid, err := tariff.CurrentPrice(site.GetTariff(api.TariffUsageGrid)); err == nil { + feedin, err := tariff.CurrentPrice(site.GetTariff(api.TariffUsageFeedin)) if err != nil { feedin = 0 } @@ -770,7 +770,7 @@ func (site *Site) effectivePrice(greenShare float64) *float64 { // effectiveCo2 calculates the amount of emitted co2 based on self-produced and grid-imported energy. func (site *Site) effectiveCo2(greenShare float64) *float64 { - if co2, err := site.tariffs.CurrentCo2(); err == nil { + if co2, err := tariff.CurrentPrice(site.GetTariff(api.TariffUsageCo2)); err == nil { effCo2 := co2 * (1 - greenShare) return &effCo2 } @@ -781,26 +781,29 @@ func (site *Site) publishTariffs(greenShareHome float64, greenShareLoadpoints fl site.publish(keys.GreenShareHome, greenShareHome) site.publish(keys.GreenShareLoadpoints, greenShareLoadpoints) - if gridPrice, err := site.tariffs.CurrentGridPrice(); err == nil { - site.publish(keys.TariffGrid, gridPrice) + if v, err := tariff.CurrentPrice(site.GetTariff(api.TariffUsageGrid)); err == nil { + site.publish(keys.TariffGrid, v) } - if feedInPrice, err := site.tariffs.CurrentFeedInPrice(); err == nil { - site.publish(keys.TariffFeedIn, feedInPrice) + if v, err := tariff.CurrentPrice(site.GetTariff(api.TariffUsageFeedin)); err == nil { + site.publish(keys.TariffFeedIn, v) } - if co2, err := site.tariffs.CurrentCo2(); err == nil { - site.publish(keys.TariffCo2, co2) + if v, err := tariff.CurrentPrice(site.GetTariff(api.TariffUsageCo2)); err == nil { + site.publish(keys.TariffCo2, v) } - if price := site.effectivePrice(greenShareHome); price != nil { - site.publish(keys.TariffPriceHome, price) + if v, err := tariff.CurrentPrice(site.GetTariff(api.TariffUsageSolar)); err == nil { + site.publish(keys.TariffSolar, v) } - if co2 := site.effectiveCo2(greenShareHome); co2 != nil { - site.publish(keys.TariffCo2Home, co2) + if v := site.effectivePrice(greenShareHome); v != nil { + site.publish(keys.TariffPriceHome, v) } - if price := site.effectivePrice(greenShareLoadpoints); price != nil { - site.publish(keys.TariffPriceLoadpoints, price) + if v := site.effectiveCo2(greenShareHome); v != nil { + site.publish(keys.TariffCo2Home, v) } - if co2 := site.effectiveCo2(greenShareLoadpoints); co2 != nil { - site.publish(keys.TariffCo2Loadpoints, co2) + if v := site.effectivePrice(greenShareLoadpoints); v != nil { + site.publish(keys.TariffPriceLoadpoints, v) + } + if v := site.effectiveCo2(greenShareLoadpoints); v != nil { + site.publish(keys.TariffCo2Loadpoints, v) } } @@ -926,7 +929,7 @@ func (site *Site) prepare() { site.publish(keys.ResidualPower, site.GetResidualPower()) site.publish(keys.Currency, site.tariffs.Currency) - if tariff := site.GetTariff(PlannerTariff); tariff != nil { + if tariff := site.GetTariff(api.TariffUsagePlanner); tariff != nil { site.publish(keys.SmartCostType, tariff.Type()) } else { site.publish(keys.SmartCostType, nil) diff --git a/core/site/api.go b/core/site/api.go index 56d5b4a4e..f78d27aa2 100644 --- a/core/site/api.go +++ b/core/site/api.go @@ -55,7 +55,7 @@ type API interface { // // GetTariff returns the respective tariff - GetTariff(string) api.Tariff + GetTariff(api.TariffUsage) api.Tariff // // battery control diff --git a/core/site_api.go b/core/site_api.go index 93ca3ef54..bf0b8beaf 100644 --- a/core/site_api.go +++ b/core/site_api.go @@ -17,12 +17,6 @@ var _ site.API = (*Site)(nil) var ErrBatteryNotConfigured = errors.New("battery not configured") -const ( - GridTariff = "grid" - FeedinTariff = "feedin" - PlannerTariff = "planner" -) - // isConfigurable checks if the meter is configurable func isConfigurable(ref string) bool { dev, _ := config.Meters().ByName(ref) @@ -272,39 +266,10 @@ func (site *Site) SetResidualPower(power float64) error { } // GetTariff returns the respective tariff if configured or nil -func (site *Site) GetTariff(tariff string) api.Tariff { +func (site *Site) GetTariff(tariff api.TariffUsage) api.Tariff { site.RLock() defer site.RUnlock() - - switch tariff { - case GridTariff: - return site.tariffs.Grid - - case FeedinTariff: - return site.tariffs.FeedIn - - case PlannerTariff: - switch { - case site.tariffs.Planner != nil: - // prio 0: manually set planner tariff - return site.tariffs.Planner - - case site.tariffs.Grid != nil && site.tariffs.Grid.Type() == api.TariffTypePriceForecast: - // prio 1: grid tariff with forecast - return site.tariffs.Grid - - case site.tariffs.Co2 != nil: - // prio 2: co2 tariff - return site.tariffs.Co2 - - default: - // prio 3: static grid tariff - return site.tariffs.Grid - } - - default: - return nil - } + return site.tariffs.Get(tariff) } // GetBatteryDischargeControl returns the battery control mode (no discharge only) diff --git a/core/site_battery.go b/core/site_battery.go index 8b4895e2e..dd287f89d 100644 --- a/core/site_battery.go +++ b/core/site_battery.go @@ -78,7 +78,7 @@ func (site *Site) applyBatteryMode(mode api.BatteryMode) error { } func (site *Site) plannerRates() (api.Rates, error) { - tariff := site.GetTariff(PlannerTariff) + tariff := site.GetTariff(api.TariffUsagePlanner) if tariff == nil || tariff.Type() == api.TariffTypePriceStatic { return nil, nil } diff --git a/server/http_site_handler.go b/server/http_site_handler.go index 4f26430f6..4eb017c98 100644 --- a/server/http_site_handler.go +++ b/server/http_site_handler.go @@ -251,7 +251,13 @@ func healthHandler(site site.API) http.HandlerFunc { func tariffHandler(site site.API) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - tariff := vars["tariff"] + val := vars["tariff"] + + tariff, err := api.TariffUsageString(val) + if err != nil { + jsonError(w, http.StatusNotFound, err) + return + } t := site.GetTariff(tariff) if t == nil { diff --git a/tariff/solcast.go b/tariff/solcast.go new file mode 100644 index 000000000..fb8d58280 --- /dev/null +++ b/tariff/solcast.go @@ -0,0 +1,121 @@ +package tariff + +import ( + "errors" + "fmt" + "slices" + "sync" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/tariff/solcast" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" +) + +type Solcast struct { + *request.Helper + log *util.Logger + sites []string + data *util.Monitor[api.Rates] +} + +var _ api.Tariff = (*Solcast)(nil) + +func init() { + registry.Add("solcast", NewSolcastFromConfig) +} + +func NewSolcastFromConfig(other map[string]interface{}) (api.Tariff, error) { + var cc struct { + Site string + Token string + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + if cc.Site == "" { + return nil, errors.New("missing site id") + } + // TODO multiple sites + // if len(cc.Site) > 1 { + // return nil, errors.New("multiple sites not supported (yet)") + // } + + if cc.Token == "" { + return nil, errors.New("missing token") + } + + log := util.NewLogger("solcast").Redact(cc.Token) + + t := &Solcast{ + log: log, + sites: []string{cc.Site}, + Helper: request.NewHelper(log), + data: util.NewMonitor[api.Rates](2 * time.Hour), + } + + t.Client.Transport = transport.BearerAuth(cc.Token, t.Client.Transport) + + done := make(chan error) + go t.run(done) + err := <-done + + return t, err +} + +func (t *Solcast) run(done chan error) { + var once sync.Once + + for ; true; <-time.Tick(time.Hour) { + var res solcast.Forecasts + + if err := backoff.Retry(func() error { + for _, site := range t.sites { + uri := fmt.Sprintf("https://api.solcast.com.au/rooftop_sites/%s/forecasts?format=json", site) + if err := t.GetJSON(uri, &res); err != nil { + return err + } + } + return nil + }, bo()); err != nil { + once.Do(func() { done <- err }) + + t.log.ERROR.Println(err) + continue + } + + once.Do(func() { close(done) }) + + data := make(api.Rates, 0, len(res.Forecasts)) + + for _, r := range res.Forecasts { + data = append(data, api.Rate{ + Start: r.PeriodEnd.Add(-r.Period.Duration()).Local(), + End: r.PeriodEnd.Local(), + Price: r.PvEstimate * 1e3, + }) + } + + mergeRates(t.data, data) + once.Do(func() { close(done) }) + } +} + +// Rates implements the api.Tariff interface +func (t *Solcast) Rates() (api.Rates, error) { + var res api.Rates + err := t.data.GetFunc(func(val api.Rates) { + res = slices.Clone(val) + }) + return res, err +} + +// Type implements the api.Tariff interface +func (t *Solcast) Type() api.TariffType { + return api.TariffTypeSolar +} diff --git a/tariff/solcast/types.go b/tariff/solcast/types.go new file mode 100644 index 000000000..a37af2efb --- /dev/null +++ b/tariff/solcast/types.go @@ -0,0 +1,34 @@ +package solcast + +import ( + "time" + + "github.com/dylanmei/iso8601" +) + +type Forecasts struct { + Forecasts []Forecast +} + +type Forecast struct { + PvEstimate float64 `json:"pv_estimate"` + PvEstimate10 float64 `json:"pv_estimate10"` + PvEstimate90 float64 `json:"pv_estimate90"` + PeriodEnd time.Time `json:"period_end"` + Period Duration +} + +type Duration time.Duration + +func (d *Duration) Duration() time.Duration { + return time.Duration(*d) +} + +func (d *Duration) UnmarshalJSON(b []byte) error { + val, err := iso8601.ParseDuration(string(b)) + if err != nil { + return err + } + *d = Duration(val) + return nil +} diff --git a/tariff/tariffs.go b/tariff/tariffs.go index ffeb024cf..0e6219691 100644 --- a/tariff/tariffs.go +++ b/tariff/tariffs.go @@ -8,11 +8,11 @@ import ( ) type Tariffs struct { - Currency currency.Unit - Grid, FeedIn, Co2, Planner api.Tariff + Currency currency.Unit + Grid, FeedIn, Co2, Planner, Solar api.Tariff } -func currentPrice(t api.Tariff) (float64, error) { +func CurrentPrice(t api.Tariff) (float64, error) { if t != nil { if rr, err := t.Rates(); err == nil { if r, err := rr.Current(time.Now()); err == nil { @@ -23,20 +23,41 @@ func currentPrice(t api.Tariff) (float64, error) { return 0, api.ErrNotAvailable } -// CurrentGridPrice returns the current grid price. -func (t *Tariffs) CurrentGridPrice() (float64, error) { - return currentPrice(t.Grid) -} +func (t *Tariffs) Get(u api.TariffUsage) api.Tariff { + switch u { + case api.TariffUsageCo2: + return t.Co2 -// CurrentFeedInPrice returns the current feed-in price. -func (t *Tariffs) CurrentFeedInPrice() (float64, error) { - return currentPrice(t.FeedIn) -} + case api.TariffUsageFeedin: + return t.FeedIn -// CurrentCo2 determines the grids co2 emission. -func (t *Tariffs) CurrentCo2() (float64, error) { - if t.Co2 != nil { - return currentPrice(t.Co2) + case api.TariffUsageGrid: + return t.Grid + + // TODO solar + case api.TariffUsagePlanner: + switch { + case t.Planner != nil: + // prio 0: manually set planner tariff + return t.Planner + + case t.Grid != nil && t.Grid.Type() == api.TariffTypePriceForecast: + // prio 1: grid tariff with forecast + return t.Grid + + case t.Co2 != nil: + // prio 2: co2 tariff + return t.Co2 + + default: + // prio 3: static grid tariff + return t.Grid + } + + case api.TariffUsageSolar: + return t.Solar + + default: + return nil } - return 0, api.ErrNotAvailable } diff --git a/templates/definition/tariff/forecast-solar.yaml b/templates/definition/tariff/forecast-solar.yaml new file mode 100644 index 000000000..e0b947ff5 --- /dev/null +++ b/templates/definition/tariff/forecast-solar.yaml @@ -0,0 +1,31 @@ +template: forecast-solar +products: + - brand: Forecast.Solar +requirements: + evcc: ["skiptest"] +group: solar +params: + - name: lat + required: true + - name: lon + required: true + - name: dec + required: true + - name: az + required: true + - name: kwp + required: true + - name: apikey + advanced: true +render: | + type: custom + tariff: solar + forecast: + source: http + uri: https://api.forecast.solar/{{ if .apikey }}{{ .apikey }}/{{ end }}estimate/{{ .lat }}/{{ .lon }}/{{ .dec }}/{{ .az }}/{{ .kwp }}?time=utc&no_sun=1 + jq: | + [ .result.watts | to_entries.[] | { + "start": (.key | strptime("%FT%T%z") | strftime("%FT%TZ")), + "end": (.key | strptime("%FT%T%z") | mktime+3600 | strftime("%FT%TZ")), + "price": .value + } ] | tostring diff --git a/templates/definition/tariff/solcast.yaml b/templates/definition/tariff/solcast.yaml new file mode 100644 index 000000000..270d5e0ba --- /dev/null +++ b/templates/definition/tariff/solcast.yaml @@ -0,0 +1,15 @@ +template: solcast +products: + - brand: Solcast +requirements: + evcc: ["skiptest"] +group: solar +params: + - name: site + required: true + - name: token + required: true +render: | + type: solcast + site: {{ .site }} + token: {{ .token }} diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml index 6cf8293fd..682ad7288 100644 --- a/util/templates/defaults.yaml +++ b/util/templates/defaults.yaml @@ -606,3 +606,6 @@ devicegroups: co2: de: CO₂ Vorhersage en: CO₂ forecast + solar: + de: PV Vorhersage + en: PV forecast