package charger import ( "fmt" "strconv" "strings" "time" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/charger/openwb" "github.com/evcc-io/evcc/plugin" "github.com/evcc-io/evcc/plugin/mqtt" "github.com/evcc-io/evcc/util" ) func init() { registry.Add("openwb", NewOpenWBFromConfig) } // OpenWB configures generic charger and charge meter for an openWB loadpoint type OpenWB struct { current int64 enabled bool statusG func() (string, error) currentS func(int64) error currentPowerG func() (float64, error) totalEnergyG func() (float64, error) currentsG []func() (float64, error) wakeupS func(int64) error authS func(string) error } //go:generate go tool decorate -f decorateOpenWB -b *OpenWB -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.Battery,Soc,func() (float64, error)" // NewOpenWBFromConfig creates a new configurable charger func NewOpenWBFromConfig(other map[string]any) (api.Charger, error) { cc := struct { mqtt.Config `mapstructure:",squash"` Topic string Timeout time.Duration ID int Phases1p3p, DC bool }{ Topic: openwb.RootTopic, Timeout: openwb.Timeout, ID: 1, } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } log := util.NewLogger("openwb") return NewOpenWB(log, cc.Config, cc.ID, cc.Topic, cc.Phases1p3p, cc.DC, cc.Timeout) } // NewOpenWB creates a new configurable charger func NewOpenWB(log *util.Logger, mqttconf mqtt.Config, id int, topic string, p1p3, dc bool, timeout time.Duration) (api.Charger, error) { client, err := mqtt.RegisteredClientOrDefault(log, mqttconf) if err != nil { return nil, err } // timeout handler h, err := plugin.NewMqtt(log, client, fmt.Sprintf("%s/system/%s", topic, openwb.TimestampTopic), timeout).StringGetter() if err != nil { return nil, err } to := plugin.NewTimeoutHandler(h) mq := func(subtopic string) *plugin.Mqtt { return plugin.NewMqtt(log, client, fmt.Sprintf("%s/lp/%d/%s", topic, id, subtopic), 0) } // check if loadpoint configured configured, err := plugin.NewMqtt(log, client, fmt.Sprintf("%s/lp/%d/%s", topic, id, openwb.ConfiguredTopic), timeout).BoolGetter() if err != nil { return nil, err } if isConfigured, err := configured(); err != nil || !isConfigured { return nil, fmt.Errorf("loadpoint %d is not configured", id) } // adapt plugged/charging to status pluggedG, err := to.BoolGetter(mq(openwb.PluggedTopic)) if err != nil { return nil, err } chargingG, err := to.BoolGetter(mq(openwb.ChargingTopic)) if err != nil { return nil, err } statusG, err := plugin.NewCombinedPlugin(pluggedG, chargingG).StringGetter() if err != nil { return nil, err } // setters currentTopic := openwb.SlaveChargeCurrentTopic if id == 2 { // TODO remove after https://github.com/snaptec/openWB/issues/1757 currentTopic = "Lp2" + openwb.SlaveChargeCurrentTopic } currentS, err := plugin.NewMqtt(log, client, fmt.Sprintf("%s/set/isss/%s", topic, currentTopic), timeout).WithRetained().IntSetter("current") if err != nil { return nil, err } cpTopic := openwb.SlaveCPInterruptTopic if id == 2 { // TODO remove after https://github.com/snaptec/openWB/issues/1757 cpTopic = strings.TrimSuffix(cpTopic, "1") + "2" } wakeupS, err := plugin.NewMqtt(log, client, fmt.Sprintf("%s/set/isss/%s", topic, cpTopic), timeout).WithRetained().IntSetter("cp") if err != nil { return nil, err } authS, err := plugin.NewMqtt(log, client, fmt.Sprintf("%s/set/chargepoint/%d/set/%s", topic, id, openwb.RfidTopic), timeout).WithRetained().StringSetter("rfid") if err != nil { return nil, err } // meter getters currentPowerG, err := to.FloatGetter(mq(openwb.ChargePowerTopic)) if err != nil { return nil, err } totalEnergyG, err := to.FloatGetter(mq(openwb.ChargeTotalEnergyTopic)) if err != nil { return nil, err } var currentsG []func() (float64, error) for i := 1; i <= 3; i++ { current, err := to.FloatGetter(mq(openwb.CurrentTopic + strconv.Itoa(i))) if err != nil { return nil, err } currentsG = append(currentsG, current) } c := &OpenWB{ currentS: currentS, statusG: statusG, currentPowerG: currentPowerG, totalEnergyG: totalEnergyG, currentsG: currentsG, wakeupS: wakeupS, authS: authS, } // heartbeat heartbeatS, err := plugin.NewMqtt(log, client, fmt.Sprintf("%s/set/isss/%s", topic, openwb.SlaveHeartbeatTopic), timeout).WithRetained().IntSetter("heartbeat") if err != nil { return nil, err } go func() { for range time.Tick(openwb.HeartbeatInterval) { if err := heartbeatS(1); err != nil { log.ERROR.Printf("heartbeat: %v", err) } } }() // optional capabilities var phases func(int) error if p1p3 { phasesTopic := openwb.SlavePhasesTopic if id == 2 { // TODO remove after https://github.com/snaptec/openWB/issues/1757 phasesTopic += "Lp2" } phasesS, err := plugin.NewMqtt(log, client, fmt.Sprintf("%s/set/isss/%s", topic, phasesTopic), timeout).WithRetained().IntSetter("phases") if err != nil { return nil, err } phases = func(phases int) error { return phasesS(int64(phases)) } } var soc func() (float64, error) if dc { soc, err = to.FloatGetter(mq(openwb.VehicleSocTopic)) if err != nil { return nil, err } } return decorateOpenWB(c, phases, soc), nil } func (m *OpenWB) Enable(enable bool) error { var current int64 if enable { current = m.current } err := m.currentS(current) if err == nil { m.enabled = enable } return err } func (m *OpenWB) Enabled() (bool, error) { return verifyEnabled(m, m.enabled) } func (m *OpenWB) Status() (api.ChargeStatus, error) { status, err := m.statusG() if err != nil { return api.StatusNone, err } return api.ChargeStatusString(status) } func (m *OpenWB) MaxCurrent(current int64) error { err := m.currentS(current) if err == nil { m.current = current } return err } var _ api.Meter = (*OpenWB)(nil) // CurrentPower implements the api.Meter interface func (m *OpenWB) CurrentPower() (float64, error) { return m.currentPowerG() } var _ api.MeterEnergy = (*OpenWB)(nil) // TotalEnergy implements the api.MeterEnergy interface func (m *OpenWB) TotalEnergy() (float64, error) { return m.totalEnergyG() } var _ api.PhaseCurrents = (*OpenWB)(nil) // Currents implements the api.PhaseCurrents interface func (m *OpenWB) Currents() (float64, float64, float64, error) { var res []float64 for _, currentG := range m.currentsG { c, err := currentG() if err != nil { return 0, 0, 0, err } res = append(res, c) } return res[0], res[1], res[2], nil } var _ api.Authorizer = (*OpenWB)(nil) // Authorize implements the api.Authorizer interface func (m *OpenWB) Authorize(key string) error { return m.authS(key) } var _ api.Resurrector = (*OpenWB)(nil) // WakeUp implements the api.Resurrector interface func (m *OpenWB) WakeUp() error { return m.wakeupS(1) }