293 lines
6.7 KiB
Go
293 lines
6.7 KiB
Go
package meter
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/api/implement"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/request"
|
|
"github.com/evcc-io/evcc/util/templates"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spali/go-rscp/rscp"
|
|
"github.com/spf13/cast"
|
|
)
|
|
|
|
type E3dc struct {
|
|
implement.Caps
|
|
mu sync.Mutex
|
|
dischargeLimit uint32
|
|
externalPower bool // whether to include power of external sources
|
|
usage templates.Usage // TODO check if we really want to depend on templates
|
|
conn *rscp.Client
|
|
retry func() error
|
|
}
|
|
|
|
func init() {
|
|
registry.Add("e3dc-rscp", NewE3dcFromConfig)
|
|
}
|
|
|
|
func NewE3dcFromConfig(other map[string]any) (api.Meter, error) {
|
|
cc := struct {
|
|
batteryCapacity `mapstructure:",squash"`
|
|
batteryPowerLimits `mapstructure:",squash"`
|
|
batterySocLimits `mapstructure:",squash"`
|
|
pvMaxACPower `mapstructure:",squash"`
|
|
Usage templates.Usage
|
|
Uri string
|
|
User string
|
|
Password string
|
|
Key string
|
|
ExternalPower bool
|
|
DischargeLimit uint32
|
|
Timeout time.Duration
|
|
}{
|
|
Timeout: request.Timeout,
|
|
}
|
|
|
|
if err := util.DecodeOther(other, &cc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
host, port_, err := net.SplitHostPort(util.DefaultPort(cc.Uri, 5033))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
port, _ := strconv.Atoi(port_)
|
|
|
|
cfg := rscp.ClientConfig{
|
|
Address: host,
|
|
Port: uint16(port),
|
|
Username: cc.User,
|
|
Password: cc.Password,
|
|
Key: cc.Key,
|
|
ConnectionTimeout: cc.Timeout,
|
|
SendTimeout: cc.Timeout,
|
|
ReceiveTimeout: cc.Timeout,
|
|
}
|
|
|
|
return NewE3dc(cfg, cc.Usage, cc.DischargeLimit, cc.ExternalPower, cc.batteryCapacity.Decorator(), cc.pvMaxACPower.Decorator(), cc.batterySocLimits.Decorator(), cc.batteryPowerLimits.Decorator())
|
|
}
|
|
|
|
var e3dcOnce sync.Once
|
|
|
|
func NewE3dc(cfg rscp.ClientConfig, usage templates.Usage, dischargeLimit uint32, externalPower bool, capacity, maxacpower func() float64, batterySocLimits, batteryPowerLimits func() (float64, float64)) (api.Meter, error) {
|
|
e3dcOnce.Do(func() {
|
|
log := util.NewLogger("e3dc")
|
|
rscp.Log.SetLevel(logrus.DebugLevel)
|
|
rscp.Log.SetOutput(log.TRACE.Writer())
|
|
})
|
|
|
|
conn, err := rscp.NewClient(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m := &E3dc{
|
|
Caps: implement.New(),
|
|
usage: usage,
|
|
conn: conn,
|
|
externalPower: externalPower,
|
|
dischargeLimit: dischargeLimit,
|
|
}
|
|
|
|
m.retry = func() (err error) {
|
|
m.conn.Disconnect()
|
|
m.conn, err = rscp.NewClient(cfg)
|
|
return err
|
|
}
|
|
|
|
implement.May(m, implement.BatterySocLimiter(batterySocLimits))
|
|
implement.May(m, implement.BatteryPowerLimiter(batteryPowerLimits))
|
|
implement.May(m, implement.MaxACPowerGetter(maxacpower))
|
|
|
|
if usage == templates.UsageBattery {
|
|
implement.May(m, implement.BatteryCapacity(capacity))
|
|
implement.Has(m, implement.Battery(m.batterySoc))
|
|
implement.Has(m, implement.BatteryController(m.setBatteryMode))
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// retryMessage executes a single message request with retry
|
|
func (m *E3dc) retryMessage(msg rscp.Message) (*rscp.Message, error) {
|
|
result, err := m.conn.Send(msg)
|
|
if err == nil {
|
|
return result, nil
|
|
}
|
|
|
|
if err := m.retry(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return m.conn.Send(msg)
|
|
}
|
|
|
|
// retryMessages executes a multiple message request with retry
|
|
func (m *E3dc) retryMessages(msgs []rscp.Message) ([]rscp.Message, error) {
|
|
result, err := m.conn.SendMultiple(msgs)
|
|
if err == nil {
|
|
return result, nil
|
|
}
|
|
|
|
if err := m.retry(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return m.conn.SendMultiple(msgs)
|
|
}
|
|
|
|
func (m *E3dc) CurrentPower() (float64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
switch m.usage {
|
|
case templates.UsageGrid:
|
|
res, err := m.retryMessage(*rscp.NewMessage(rscp.EMS_REQ_POWER_GRID, nil))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return rscpValue(*res, cast.ToFloat64E)
|
|
|
|
case templates.UsagePV:
|
|
res, err := m.retryMessages([]rscp.Message{
|
|
*rscp.NewMessage(rscp.EMS_REQ_POWER_PV, nil),
|
|
*rscp.NewMessage(rscp.EMS_REQ_POWER_ADD, nil),
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
values, err := rscpValues(res, cast.ToFloat64E)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if m.externalPower {
|
|
return values[0] - values[1], nil
|
|
}
|
|
|
|
return values[0], nil
|
|
|
|
case templates.UsageBattery:
|
|
res, err := m.retryMessage(*rscp.NewMessage(rscp.EMS_REQ_POWER_BAT, nil))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
pwr, err := rscpValue(*res, cast.ToFloat64E)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return -pwr, nil
|
|
|
|
default:
|
|
return 0, api.ErrNotAvailable
|
|
}
|
|
}
|
|
|
|
func (m *E3dc) batterySoc() (float64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
res, err := m.retryMessage(*rscp.NewMessage(rscp.EMS_REQ_BAT_SOC, nil))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return rscpValue(*res, cast.ToFloat64E)
|
|
}
|
|
|
|
func (m *E3dc) setBatteryMode(mode api.BatteryMode) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
var messages []rscp.Message
|
|
switch mode {
|
|
case api.BatteryNormal:
|
|
messages = []rscp.Message{
|
|
e3dcDischargeBatteryLimit(false, 0),
|
|
e3dcBatteryCharge(0),
|
|
}
|
|
case api.BatteryHold:
|
|
messages = []rscp.Message{
|
|
e3dcDischargeBatteryLimit(true, m.dischargeLimit),
|
|
e3dcBatteryCharge(0),
|
|
}
|
|
case api.BatteryCharge:
|
|
messages = []rscp.Message{
|
|
e3dcDischargeBatteryLimit(false, 0),
|
|
e3dcBatteryCharge(50000), // max. 50kWh
|
|
}
|
|
case api.BatteryHoldCharge:
|
|
messages = []rscp.Message{
|
|
e3dcDischargeBatteryLimit(false, 0),
|
|
e3dcBatteryCharge(0),
|
|
}
|
|
default:
|
|
return api.ErrNotAvailable
|
|
}
|
|
|
|
res, err := m.retryMessages(messages)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return rscpError(res...)
|
|
}
|
|
|
|
func e3dcDischargeBatteryLimit(active bool, limit uint32) rscp.Message {
|
|
contents := []rscp.Message{
|
|
*rscp.NewMessage(rscp.EMS_POWER_LIMITS_USED, active),
|
|
}
|
|
|
|
if active {
|
|
contents = append(contents, *rscp.NewMessage(rscp.EMS_MAX_DISCHARGE_POWER, limit))
|
|
}
|
|
|
|
return *rscp.NewMessage(rscp.EMS_REQ_SET_POWER_SETTINGS, contents)
|
|
}
|
|
|
|
func e3dcBatteryCharge(amount uint32) rscp.Message {
|
|
return *rscp.NewMessage(rscp.EMS_REQ_START_MANUAL_CHARGE, amount)
|
|
}
|
|
|
|
func rscpError(msg ...rscp.Message) error {
|
|
var errs []error
|
|
for _, m := range msg {
|
|
if m.DataType == rscp.Error {
|
|
errs = append(errs, errors.New(rscp.RscpError(cast.ToUint32(m.Value)).String()))
|
|
}
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
func rscpValue[T any](msg rscp.Message, fun func(any) (T, error)) (T, error) {
|
|
var zero T
|
|
if err := rscpError(msg); err != nil {
|
|
return zero, err
|
|
}
|
|
|
|
return fun(msg.Value)
|
|
}
|
|
|
|
func rscpValues[T any](msg []rscp.Message, fun func(any) (T, error)) ([]T, error) {
|
|
res := make([]T, 0, len(msg))
|
|
|
|
for _, m := range msg {
|
|
v, err := rscpValue(m, fun)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res = append(res, v)
|
|
}
|
|
|
|
return res, nil
|
|
}
|