evcc-io/charger/openwb-native_linux.go

206 lines
5.6 KiB
Go

package charger
import (
"context"
"fmt"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/charger/openwb/native"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/modbus"
"github.com/warthog618/go-gpiocdev"
)
const minCpWaitTime time.Duration = 5 * time.Second
// openWbGpioLines holds GPIO lines for a single charge point
type openWbGpioLines struct {
cp *gpiocdev.Line
ph1 *gpiocdev.Line
ph3 *gpiocdev.Line
}
// OpenWbNative charger implementation
type OpenWbNative struct {
api.Charger
log *util.Logger
rfId native.RfIdContainer
cpWait time.Duration
connector int
chargeState api.ChargeStatus
gpio openWbGpioLines
}
// gpioAction defines a single GPIO pin operation with timing
type gpioAction struct {
pin func()
delay time.Duration
}
func init() {
registry.AddCtx("openwb-native", NewOpenWbNativeFromConfig)
}
//go:generate go tool decorate -o openwb-native_decorators_linux.go -f decorateOpenWbNative -b *OpenWbNative -r api.Charger -t api.ChargerEx,api.PhaseSwitcher,api.Identifier
// NewOpenWbNativeFromConfig creates an OpenWbNative charger from generic config
func NewOpenWbNativeFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) {
cc := struct {
Phases1p3p bool
RfId string
CpWait time.Duration
Connector int
Chip string
modbus.Settings `mapstructure:",squash"`
}{
Chip: "gpiochip0",
Settings: modbus.Settings{
Baudrate: 9600,
Comset: "8N1",
ID: 1,
},
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if (cc.Connector < 1) || (cc.Connector > 2) {
return nil, fmt.Errorf("invalid connector value: %d", cc.Connector)
}
if cc.CpWait < minCpWaitTime {
return nil, fmt.Errorf("invalid cpwait value: %v, needs to be greater than %s", cc.CpWait, minCpWaitTime)
}
return NewOpenWbNative(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID, cc.Phases1p3p, cc.RfId, cc.CpWait, cc.Connector, cc.Chip)
}
// NewOpenWbNative creates OpenWbNative charger
func NewOpenWbNative(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8, hasPhases1p3p bool, rfIdVidPid string, cpWait time.Duration, connector int, chip string) (api.Charger, error) {
log := util.NewLogger("openwb-native")
log.DEBUG.Printf("Creating OpenWB native with 3 phases %t, rfid %s, cpwait %s, connector %d", hasPhases1p3p, rfIdVidPid, cpWait.String(), connector)
evse, err := NewEvseDIN(ctx, uri, device, comset, baudrate, proto, slaveID)
if err != nil {
return nil, err
}
wb := &OpenWbNative{
Charger: evse,
log: log,
cpWait: cpWait,
connector: connector,
chargeState: api.StatusNone,
}
var (
phases1p3p func(int) error
identify func() (string, error)
maxCurrentMillis func(float64) error
)
if ex, ok := evse.(api.ChargerEx); ok {
maxCurrentMillis = ex.MaxCurrentMillis
}
// configure special external hardware features
if hasPhases1p3p {
phases1p3p = wb.phases1p3p
}
if rfIdVidPid != "" {
err := native.NewRFIDHandler(rfIdVidPid, ctx, &wb.rfId, log)
if err != nil {
return nil, err
}
identify = wb.identify
}
// initialize GPIO lines and set pins to output
pins := native.ChargePoints[connector-1]
for _, gpioConfig := range []struct {
dst **gpiocdev.Line
pin int
}{
{&wb.gpio.cp, pins.PIN_CP},
{&wb.gpio.ph1, pins.PIN_1P},
{&wb.gpio.ph3, pins.PIN_3P},
} {
line, err := gpiocdev.RequestLine(chip, gpioConfig.pin, gpiocdev.AsOutput(0))
if err != nil {
return nil, fmt.Errorf("failed to open GPIO pin %d: %w", gpioConfig.pin, err)
}
*gpioConfig.dst = line
}
return decorateOpenWbNative(wb, maxCurrentMillis, phases1p3p, identify), nil
}
// Status implements the api.Charger interface
func (wb *OpenWbNative) Status() (api.ChargeStatus, error) {
res, err := wb.Charger.Status()
if err != nil {
return api.StatusA, err
}
if wb.chargeState != api.StatusA && res == api.StatusA {
// Status changed from connected/charging to not connected, discard rfid
wb.rfId.Set("")
}
wb.chargeState = res
return res, nil
}
// phases1p3p implements the api.PhaseSwitcher interface
func (wb *OpenWbNative) phases1p3p(phases int) error {
return wb.gpioSwitchPhases(phases)
}
var _ api.Resurrector = (*OpenWbNative)(nil)
// WakeUp implements the api.Resurrector interface
func (wb *OpenWbNative) WakeUp() error {
return wb.runGpioSequence([]gpioAction{
{pin: func() { wb.gpio.cp.SetValue(1) }, delay: wb.cpWait},
{pin: func() { wb.gpio.cp.SetValue(0) }, delay: 0},
})
}
// runGpioSequence executes a sequence of GPIO operations
func (wb *OpenWbNative) runGpioSequence(seq []gpioAction) error {
if err := wb.Enable(false); err != nil {
return err
}
for _, a := range seq {
a.pin()
if a.delay > 0 {
time.Sleep(a.delay)
}
}
return wb.Enable(true)
}
// gpioSwitchPhases toggles the GPIOs to switch between 1-phase and 3-phase charging
func (wb *OpenWbNative) gpioSwitchPhases(phases int) error {
phLine := wb.gpio.ph3
if phases == 1 {
phLine = wb.gpio.ph1
}
return wb.runGpioSequence([]gpioAction{
{pin: func() { wb.gpio.cp.SetValue(1) }, delay: time.Second}, // enable phases switch relay (NO), disconnect CP
{pin: func() { phLine.SetValue(1) }, delay: wb.cpWait / 2}, // move latching relay to desired position
{pin: func() { phLine.SetValue(0) }, delay: wb.cpWait / 2}, // lock latching relay
{pin: func() { wb.gpio.cp.SetValue(0) }, delay: time.Second}, // disable phase switching, reconnect CP
})
}
// Identify implements the api.Identifier interface
func (wb *OpenWbNative) identify() (string, error) {
return wb.rfId.Get(), nil
}