Skip to content

Commit

Permalink
SolarEdge: fix battery control (#10906)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig committed Nov 26, 2023
1 parent 2ea1c55 commit ea2fe72
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 16 deletions.
6 changes: 4 additions & 2 deletions cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions cmd/meter.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
}
}

Expand Down
130 changes: 130 additions & 0 deletions provider/watchdog.go
Original file line number Diff line number Diff line change
@@ -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
// }
51 changes: 37 additions & 14 deletions templates/definition/meter/solaredge-hybrid.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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" }}
Expand Down Expand Up @@ -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 }}
Expand Down
2 changes: 2 additions & 0 deletions util/templates/render_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package templates

import (
"errors"
"fmt"

"github.com/evcc-io/evcc/util"
"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -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")
Expand Down
8 changes: 8 additions & 0 deletions util/templates/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/url"
"strings"
"text/template"
"time"

"github.com/Masterminds/sprig/v3"
"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit ea2fe72

Please sign in to comment.