207 lines
5.3 KiB
Go
207 lines
5.3 KiB
Go
package charger
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/api/implement"
|
|
"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
|
|
implement.Caps
|
|
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)
|
|
}
|
|
|
|
// 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,
|
|
Delay: 200 * time.Millisecond,
|
|
},
|
|
}
|
|
|
|
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.Settings, cc.Phases1p3p, cc.RfId, cc.CpWait, cc.Connector, cc.Chip)
|
|
}
|
|
|
|
// NewOpenWbNative creates OpenWbNative charger
|
|
func NewOpenWbNative(ctx context.Context, settings modbus.Settings, hasPhases1p3p bool, rfIdVidPid string, cpWait time.Duration, connector int, chip string) (api.Charger, error) {
|
|
log := util.NewLogger("openwb-native")
|
|
|
|
evse, err := NewEvseDIN(ctx, settings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
wb := &OpenWbNative{
|
|
Charger: evse,
|
|
Caps: implement.New(),
|
|
log: log,
|
|
cpWait: cpWait,
|
|
connector: connector,
|
|
chargeState: api.StatusNone,
|
|
}
|
|
|
|
if ex, ok := api.Cap[api.ChargerEx](evse); ok {
|
|
implement.May(wb, implement.ChargerEx(ex.MaxCurrentMillis))
|
|
}
|
|
|
|
// configure special external hardware features
|
|
if hasPhases1p3p {
|
|
implement.May(wb, implement.PhaseSwitcher(wb.phases1p3p))
|
|
}
|
|
|
|
if rfIdVidPid != "" {
|
|
err := native.NewRFIDHandler(rfIdVidPid, ctx, &wb.rfId, log)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
implement.May(wb, implement.Identifier(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
|
|
}
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
wb.gpio.cp.Close()
|
|
wb.gpio.ph1.Close()
|
|
wb.gpio.ph3.Close()
|
|
}()
|
|
|
|
return wb, 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 []string{wb.rfId.Get()}, nil
|
|
}
|