diff --git a/cmd/flags.go b/cmd/flags.go index 0922d33a1..be33c747c 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -12,8 +12,10 @@ const ( flagHeaders = "log-headers" flagHeadersDescription = "Log headers" - flagBatteryMode = "battery-mode" - flagBatteryModeDescription = "Set battery mode (normal, hold, charge)" + flagBatteryMode = "battery-mode" + flagBatteryModeDescription = "Set battery mode (normal, hold, charge)" + flagBatteryModeWait = "battery-mode-wait" + flagBatteryModeWaitDescription = "Wait given duration during which potential watchdogs are active" flagCurrent = "current" flagCurrentDescription = "Set maximum current" diff --git a/cmd/meter.go b/cmd/meter.go index 3373fd3a6..9742061f0 100644 --- a/cmd/meter.go +++ b/cmd/meter.go @@ -1,6 +1,8 @@ package cmd import ( + "time" + "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util/config" "github.com/spf13/cobra" @@ -17,6 +19,7 @@ var meterCmd = &cobra.Command{ func init() { rootCmd.AddCommand(meterCmd) meterCmd.Flags().StringP(flagBatteryMode, "b", "", flagBatteryModeDescription) + meterCmd.Flags().DurationP(flagBatteryModeWait, "w", 0, flagBatteryModeWaitDescription) } func runMeter(cmd *cobra.Command, args []string) { @@ -58,6 +61,11 @@ func runMeter(cmd *cobra.Command, args []string) { log.FATAL.Fatalln("set battery mode:", err) } } + + if d, err := cmd.Flags().GetDuration(flagBatteryModeWait); d > 0 && err == nil { + log.FATAL.Println("waiting for:", d) + time.Sleep(d) + } } } diff --git a/provider/watchdog.go b/provider/watchdog.go new file mode 100644 index 000000000..4261b4b0b --- /dev/null +++ b/provider/watchdog.go @@ -0,0 +1,130 @@ +package provider + +import ( + "context" + "strconv" + "sync" + "time" + + "github.com/evcc-io/evcc/util" +) + +type watchdogProvider struct { + mu sync.Mutex + log *util.Logger + reset *string + set Config + timeout time.Duration + cancel func() +} + +func init() { + registry.Add("watchdog", NewWatchDogFromConfig) +} + +// NewWatchDogFromConfig creates watchDog provider +func NewWatchDogFromConfig(other map[string]interface{}) (Provider, error) { + var cc struct { + Reset *string + Set Config + Timeout time.Duration + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + o := &watchdogProvider{ + log: util.NewLogger("watchdog"), + reset: cc.Reset, + set: cc.Set, + timeout: cc.Timeout, + } + + return o, nil +} + +func (o *watchdogProvider) wdt(ctx context.Context, set func() error) { + tick := time.NewTicker(o.timeout / 2) + for range tick.C { + select { + case <-ctx.Done(): + tick.Stop() + return + default: + if err := set(); err != nil { + o.log.ERROR.Println(err) + } + } + } +} + +var _ SetIntProvider = (*watchdogProvider)(nil) + +func (o *watchdogProvider) IntSetter(param string) (func(int64) error, error) { + set, err := NewIntSetterFromConfig(param, o.set) + if err != nil { + return nil, err + } + + var reset *int64 + if o.reset != nil { + val, err := strconv.ParseInt(*o.reset, 10, 64) + if err != nil { + return nil, err + } + reset = &val + } + + return func(val int64) error { + o.mu.Lock() + + // stop wdt on new write + if o.cancel != nil { + o.cancel() + o.cancel = nil + } + + // start wdt on non-reset value + if reset == nil || val != *reset { + var ctx context.Context + ctx, o.cancel = context.WithCancel(context.Background()) + + go o.wdt(ctx, func() error { + return set(val) + }) + } + + o.mu.Unlock() + + return set(val) + }, err +} + +// var _ SetFloatProvider = (*watchdogProvider)(nil) + +// func (o *watchdogProvider) FloatSetter(param string) (func(float64) error, error) { +// set, err := NewFloatSetterFromConfig(param, o.set) +// if err != nil { +// return nil, err +// } + +// val, err := strconv.ParseFloat(o.str, 64) +// return func(_ float64) error { +// return set(val) +// }, err +// } + +// var _ SetBoolProvider = (*watchdogProvider)(nil) + +// func (o *watchdogProvider) BoolSetter(param string) (func(bool) error, error) { +// set, err := NewBoolSetterFromConfig(param, o.set) +// if err != nil { +// return nil, err +// } + +// val, err := strconv.ParseBool(o.str) +// return func(_ bool) error { +// return set(val) +// }, err +// } diff --git a/templates/definition/meter/solaredge-hybrid.yaml b/templates/definition/meter/solaredge-hybrid.yaml index 7ca2ee423..d492afdd2 100644 --- a/templates/definition/meter/solaredge-hybrid.yaml +++ b/templates/definition/meter/solaredge-hybrid.yaml @@ -5,8 +5,12 @@ products: generic: Hybrid Inverter requirements: description: - de: Nur ein System kann und darf auf den Wechselrichter zugreifen! - en: Only one system may access the inverter! + de: | + Nur ein System kann und darf auf den Wechselrichter zugreifen! + Für die optionale Batteriesteuerung muss StorageConf_CtrlMode (0xE004) auf 4 "Remote" stehen. + en: | + Only one system may access the inverter! + For optional battery control, StorageConf_CtrlMode (0xE004) must be at 4 "Remote". params: - name: usage choice: ["grid", "pv", "battery"] @@ -18,6 +22,10 @@ params: - name: timeout - name: capacity advanced: true + - name: watchdog + type: duration + default: 60s + advanced: true render: | type: custom {{- if eq .usage "grid" }} @@ -66,19 +74,34 @@ render: | type: holding decode: float32s batterymode: - source: map - values: - 1: 7 # normal - 2: 1 # hold - 3: 3 # charge + source: sequence set: - source: modbus - {{- include "modbus" . | indent 4 }} - timeout: {{ .timeout }} - register: - address: 0xE00A # Battery mode - type: writesingle - decode: uint16 + - source: const + value: {{ (toDuration .watchdog).Seconds }} + set: + source: watchdog + timeout: {{ .watchdog }} # re-write at timeout/2 + set: + source: modbus + {{- include "modbus" . | indent 8 }} + timeout: {{ .timeout }} + register: + address: 0xE00B # StorageRemoteCtrl_CommandTimeout + type: writesingle + decode: uint16 + - source: map + values: + 1: 7 # normal + 2: 1 # hold + 3: 3 # charge + set: + source: modbus + {{- include "modbus" . | indent 6 }} + timeout: {{ .timeout }} + register: + address: 0xE00D # StorageRemoteCtrl_CommandMode + type: writesingle + decode: uint16 {{- if .capacity }} capacity: {{ .capacity }} # kWh {{- end }} diff --git a/util/templates/render_instance.go b/util/templates/render_instance.go index c69b878ce..8ef99856f 100644 --- a/util/templates/render_instance.go +++ b/util/templates/render_instance.go @@ -2,6 +2,7 @@ package templates import ( "errors" + "fmt" "github.com/evcc-io/evcc/util" "gopkg.in/yaml.v3" @@ -34,6 +35,7 @@ func RenderInstance(class Class, other map[string]interface{}) (*Instance, error return nil, err } + fmt.Println(string(b)) var instance Instance if err = yaml.Unmarshal(b, &instance); err == nil && instance.Type == "" { err = errors.New("empty instance type- check for missing usage") diff --git a/util/templates/utils.go b/util/templates/utils.go index 6d916e8ab..6360dc5bc 100644 --- a/util/templates/utils.go +++ b/util/templates/utils.go @@ -6,6 +6,7 @@ import ( "net/url" "strings" "text/template" + "time" "github.com/Masterminds/sprig/v3" "gopkg.in/yaml.v3" @@ -59,6 +60,13 @@ func FuncMap(tmpl *template.Template) *template.Template { "urlEncode": func(v string) string { return url.QueryEscape(v) }, + "toDuration": func(v string) time.Duration { + d, err := time.ParseDuration(v) + if err != nil { + panic(err) + } + return d + }, } return tmpl.Funcs(sprig.TxtFuncMap()).Funcs(funcMap)