diff --git a/api/error.go b/api/error.go index 3a05b207b..817ab0a08 100644 --- a/api/error.go +++ b/api/error.go @@ -14,6 +14,9 @@ var ErrSponsorRequired = errors.New("sponsorship required, see https://github.co // ErrMissingCredentials indicates that user/password are missing var ErrMissingCredentials = errors.New("missing credentials") +// ErrOutdated indicates that result is outdated +var ErrOutdated = errors.New("outdated") + // ErrTimeout is the error returned when a timeout happened. // Modeled after context.DeadlineError var ErrTimeout error = errTimeoutError{} diff --git a/cmd/tariff.go b/cmd/tariff.go new file mode 100644 index 000000000..4dfc5a326 --- /dev/null +++ b/cmd/tariff.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/evcc-io/evcc/tariff" + "github.com/spf13/cobra" +) + +// tariffCmd represents the vehicle command +var tariffCmd = &cobra.Command{ + Use: "tariff [name]", + Short: "Query configured tariff", + Run: runTariff, +} + +func init() { + rootCmd.AddCommand(tariffCmd) + tariffCmd.PersistentFlags().StringP(flagName, "n", "", fmt.Sprintf(flagNameDescription, "vehicle")) +} + +func runTariff(cmd *cobra.Command, args []string) { + // load config + if err := loadConfigFile(&conf); err != nil { + fatal(err) + } + + // setup environment + if err := configureEnvironment(cmd, conf); err != nil { + fatal(err) + } + + name := cmd.Flags().Lookup(flagName).Value.String() + + for key, cc := range map[string]typedConfig{ + "grid": conf.Tariffs.Grid, + "feedin": conf.Tariffs.FeedIn, + "planner": conf.Tariffs.Planner, + } { + if cc.Type == "" || (name != "" && key != name) { + continue + } + + if name == "" { + fmt.Println(key + ":") + } + + tf, err := tariff.NewFromConfig(cc.Type, cc.Other) + if err != nil { + fatal(err) + } + + rates, err := tf.Rates() + if err != nil { + fatal(err) + } + + tw := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) + fmt.Fprintln(tw, "From\tTo\tPrice/Cost") + const format = "2006-01-02 15:04:05" + + for _, r := range rates { + fmt.Fprintf(tw, "%s\t%s\t%.3f\n", r.Start.Local().Format(format), r.End.Local().Format(format), r.Price) + } + tw.Flush() + + fmt.Println() + } + + // wait for shutdown + <-shutdownDoneC() +} diff --git a/tariff/awattar.go b/tariff/awattar.go index 83a336142..69788ddb7 100644 --- a/tariff/awattar.go +++ b/tariff/awattar.go @@ -13,10 +13,11 @@ import ( ) type Awattar struct { - mux sync.Mutex - log *util.Logger - uri string - data api.Rates + mux sync.Mutex + log *util.Logger + uri string + data api.Rates + updated time.Time } var _ api.Tariff = (*Awattar)(nil) @@ -47,12 +48,15 @@ func NewAwattarFromConfig(other map[string]interface{}) (api.Tariff, error) { t.log.WARN.Println("cheap rate configuration has been replaced by target charging and is deprecated") } - go t.Run() + done := make(chan error) + go t.run(done) + err := <-done - return t, nil + return t, err } -func (t *Awattar) Run() { +func (t *Awattar) run(done chan error) { + var once sync.Once client := request.NewHelper(t.log) for ; true; <-time.NewTicker(time.Hour).C { @@ -62,9 +66,12 @@ func (t *Awattar) Run() { continue } - t.mux.Lock() - t.data = make(api.Rates, 0, len(res.Data)) + once.Do(func() { close(done) }) + t.mux.Lock() + t.updated = time.Now() + + t.data = make(api.Rates, 0, len(res.Data)) for _, r := range res.Data { ar := api.Rate{ Start: r.StartTimestamp, @@ -82,5 +89,5 @@ func (t *Awattar) Run() { func (t *Awattar) Rates() (api.Rates, error) { t.mux.Lock() defer t.mux.Unlock() - return append([]api.Rate{}, t.data...), nil + return append([]api.Rate{}, t.data...), outdatedError(t.updated, time.Hour) } diff --git a/tariff/electricitymaps.go b/tariff/electricitymaps.go index b34b4797a..2d6d2af06 100644 --- a/tariff/electricitymaps.go +++ b/tariff/electricitymaps.go @@ -15,11 +15,12 @@ import ( type ElectricityMaps struct { *request.Helper - log *util.Logger - mux sync.Mutex - uri string - zone string - data []CarbonIntensitySlot + log *util.Logger + mux sync.Mutex + uri string + zone string + data []CarbonIntensitySlot + updated time.Time } type CarbonIntensity struct { @@ -29,8 +30,8 @@ type CarbonIntensity struct { } type CarbonIntensitySlot struct { - CarbonIntensity float64 // : 626, - Datetime time.Time // : "2022-12-12T16:00:00.000Z" + CarbonIntensity float64 // 626, + Datetime time.Time // "2022-12-12T16:00:00.000Z" } var _ api.Tariff = (*ElectricityMaps)(nil) @@ -68,12 +69,15 @@ func NewElectricityMapsFromConfig(other map[string]interface{}) (api.Tariff, err }), } - go t.Run() + done := make(chan error) + go t.run(done) + err := <-done - return t, nil + return t, err } -func (t *ElectricityMaps) Run() { +func (t *ElectricityMaps) run(done chan error) { + var once sync.Once uri := fmt.Sprintf("%s/carbon-intensity/forecast?zone=%s", t.uri, t.zone) for ; true; <-time.NewTicker(time.Hour).C { @@ -83,11 +87,16 @@ func (t *ElectricityMaps) Run() { err = errors.New(res.Error) } + once.Do(func() { done <- err }) + t.log.ERROR.Println(err) continue } + once.Do(func() { close(done) }) + t.mux.Lock() + t.updated = time.Now() t.data = res.Forecast t.mux.Unlock() } @@ -107,5 +116,5 @@ func (t *ElectricityMaps) Rates() (api.Rates, error) { res = append(res, ar) } - return res, nil + return res, outdatedError(t.updated, time.Hour) } diff --git a/tariff/tariffs.go b/tariff/tariffs.go index 08aef0737..4a11c1c37 100644 --- a/tariff/tariffs.go +++ b/tariff/tariffs.go @@ -1,6 +1,8 @@ package tariff import ( + "time" + "github.com/evcc-io/evcc/api" "golang.org/x/text/currency" ) @@ -22,3 +24,11 @@ func NewTariffs(currency currency.Unit, grid, feedin, planner api.Tariff) *Tarif Planner: planner, } } + +// outdatedError returns api.ErrOutdated if t is older than 2*d +func outdatedError(t time.Time, d time.Duration) error { + if time.Since(t) > 2*d { + return api.ErrOutdated + } + return nil +} diff --git a/tariff/tibber.go b/tariff/tibber.go index 684a04d9c..0fbc50f00 100644 --- a/tariff/tibber.go +++ b/tariff/tibber.go @@ -14,11 +14,12 @@ import ( ) type Tibber struct { - mux sync.Mutex - log *util.Logger - homeID string - client *tibber.Client - data api.Rates + mux sync.Mutex + log *util.Logger + homeID string + client *tibber.Client + data api.Rates + updated time.Time } var _ api.Tariff = (*Tibber)(nil) @@ -62,12 +63,16 @@ func NewTibberFromConfig(other map[string]interface{}) (api.Tariff, error) { t.log.WARN.Println("cheap rate configuration has been replaced by target charging and is deprecated") } - go t.Run() + done := make(chan error) + go t.run(done) + err := <-done - return t, nil + return t, err } -func (t *Tibber) Run() { +func (t *Tibber) run(done chan error) { + var once sync.Once + for ; true; <-time.NewTicker(time.Hour).C { var res struct { Viewer struct { @@ -88,11 +93,16 @@ func (t *Tibber) Run() { cancel() if err != nil { + once.Do(func() { done <- err }) + t.log.ERROR.Println(err) continue } + once.Do(func() { close(done) }) + t.mux.Lock() + t.updated = time.Now() pi := res.Viewer.Home.CurrentSubscription.PriceInfo t.data = make(api.Rates, 0, len(pi.Today)+len(pi.Tomorrow)) @@ -119,5 +129,5 @@ func (t *Tibber) rates(pi []tibber.PriceInfo) api.Rates { func (t *Tibber) Rates() (api.Rates, error) { t.mux.Lock() defer t.mux.Unlock() - return append([]api.Rate{}, t.data...), nil + return append([]api.Rate{}, t.data...), outdatedError(t.updated, time.Hour) }