Add solar forecast using Solcast and Forecast.Solar (#18269)
This commit is contained in:
parent
7e8e182425
commit
01faac1d7e
18 changed files with 415 additions and 90 deletions
|
|
@ -140,6 +140,7 @@ type Tariffs struct {
|
|||
FeedIn config.Typed
|
||||
Co2 config.Typed
|
||||
Planner config.Typed
|
||||
Solar config.Typed
|
||||
}
|
||||
|
||||
type Network struct {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
91
api/tariffusage_enumer.go
Normal file
91
api/tariffusage_enumer.go
Normal file
|
|
@ -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
|
||||
}
|
||||
12
cmd/setup.go
12
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}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const (
|
|||
TariffGrid = "tariffGrid"
|
||||
TariffPriceHome = "tariffPriceHome"
|
||||
TariffPriceLoadpoints = "tariffPriceLoadpoints"
|
||||
TariffSolar = "tariffSolar"
|
||||
Vehicles = "vehicles"
|
||||
|
||||
// meters
|
||||
|
|
|
|||
41
core/site.go
41
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)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ type API interface {
|
|||
//
|
||||
|
||||
// GetTariff returns the respective tariff
|
||||
GetTariff(string) api.Tariff
|
||||
GetTariff(api.TariffUsage) api.Tariff
|
||||
|
||||
//
|
||||
// battery control
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
121
tariff/solcast.go
Normal file
121
tariff/solcast.go
Normal file
|
|
@ -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
|
||||
}
|
||||
34
tariff/solcast/types.go
Normal file
34
tariff/solcast/types.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
31
templates/definition/tariff/forecast-solar.yaml
Normal file
31
templates/definition/tariff/forecast-solar.yaml
Normal file
|
|
@ -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
|
||||
15
templates/definition/tariff/solcast.yaml
Normal file
15
templates/definition/tariff/solcast.yaml
Normal file
|
|
@ -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 }}
|
||||
|
|
@ -606,3 +606,6 @@ devicegroups:
|
|||
co2:
|
||||
de: CO₂ Vorhersage
|
||||
en: CO₂ forecast
|
||||
solar:
|
||||
de: PV Vorhersage
|
||||
en: PV forecast
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue