Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SolarEdge: fix battery control #10906

Merged
merged 11 commits into from
Nov 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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