From 1c11cc46a1c49cd1bca1e9565039685f0bdd1d73 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 5 Dec 2025 11:39:39 +0100 Subject: [PATCH] Add GPIO plugin (#25806) --- api/error.go | 5 ++- charger/openwb-native.go | 3 +- charger/openwb-native_linux.go | 15 ++++---- plugin/gpio.go | 18 ++++++++++ plugin/gpio_linux.go | 63 ++++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 plugin/gpio.go create mode 100644 plugin/gpio_linux.go diff --git a/api/error.go b/api/error.go index 8bfa18a96..899e45531 100644 --- a/api/error.go +++ b/api/error.go @@ -8,7 +8,10 @@ import ( ) // ErrNotAvailable indicates that a feature is not available -var ErrNotAvailable = errors.New("not available") +var ErrNotAvailable = backoff.Permanent(errors.New("not available")) + +// ErrUnsupportedPlatform indicates unsupported hardware platform +var ErrUnsupportedPlatform error = backoff.Permanent(errors.New("unsupported platform")) // ErrMustRetry indicates that a rate-limited operation should be retried var ErrMustRetry = errors.New("must retry") diff --git a/charger/openwb-native.go b/charger/openwb-native.go index 40b7d2610..50451d433 100644 --- a/charger/openwb-native.go +++ b/charger/openwb-native.go @@ -4,7 +4,6 @@ package charger import ( "context" - "errors" "github.com/evcc-io/evcc/api" ) @@ -15,5 +14,5 @@ func init() { // NewOpenWbNativeFromConfig creates an OpenWbNative DIN charger from generic config func NewOpenWbNativeFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) { - return nil, errors.New("unsupported platform") + return nil, api.ErrUnsupportedPlatform } diff --git a/charger/openwb-native_linux.go b/charger/openwb-native_linux.go index 0bc215e54..0dbb80ed3 100644 --- a/charger/openwb-native_linux.go +++ b/charger/openwb-native_linux.go @@ -108,20 +108,12 @@ func NewOpenWbNative(ctx context.Context, uri, device, comset string, baudrate i if err := rpio.Open(); err != nil { return nil, fmt.Errorf("failed to open GPIO: %w", err) } + defer rpio.Close() for _, pin := range structs.Fields(native.ChargePoints[connector-1]) { rpio.Pin(pin.Value().(int)).Output() } - // close GPIO when context is cancelled - go func() { - <-ctx.Done() - log.DEBUG.Println("context canceled, closing GPIO") - if err := rpio.Close(); err != nil { - log.ERROR.Printf("error closing GPIO: %v", err) - } - }() - return decorateOpenWbNative(wb, phases1p3p, identify), nil } @@ -160,6 +152,11 @@ func (wb *OpenWbNative) WakeUp() error { // runGpioSequence executes a sequence of GPIO operations func (wb *OpenWbNative) runGpioSequence(seq []gpioAction) error { + if err := rpio.Open(); err != nil { + return fmt.Errorf("failed to open GPIO: %w", err) + } + defer rpio.Close() + if err := wb.Enable(false); err != nil { return err } diff --git a/plugin/gpio.go b/plugin/gpio.go new file mode 100644 index 000000000..ddecc6f5e --- /dev/null +++ b/plugin/gpio.go @@ -0,0 +1,18 @@ +//go:build !linux + +package plugin + +import ( + "context" + + "github.com/evcc-io/evcc/api" +) + +func init() { + registry.AddCtx("gpio", NewGpioPluginFromConfig) +} + +// NewGpioPluginFromConfig creates a GPIO provider +func NewGpioPluginFromConfig(ctx context.Context, other map[string]any) (Plugin, error) { + return nil, api.ErrUnsupportedPlatform +} diff --git a/plugin/gpio_linux.go b/plugin/gpio_linux.go new file mode 100644 index 000000000..8329e5ae5 --- /dev/null +++ b/plugin/gpio_linux.go @@ -0,0 +1,63 @@ +//go:build linux + +package plugin + +import ( + "context" + "fmt" + "sync" + + "github.com/evcc-io/evcc/util" + "github.com/stianeikeland/go-rpio/v4" +) + +func init() { + registry.AddCtx("gpio", NewGpioPluginFromConfig) +} + +type gpio struct { + mu sync.Mutex + pin rpio.Pin +} + +// NewGpioPluginFromConfig creates a GPIO provider +func NewGpioPluginFromConfig(ctx context.Context, other map[string]any) (Plugin, error) { + var cc struct { + Pin int + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + p := &gpio{ + pin: rpio.Pin(cc.Pin), + } + + // initialize GPIO and set pins to input + if err := rpio.Open(); err != nil { + return nil, fmt.Errorf("failed to open GPIO: %w", err) + } + defer rpio.Close() + + p.pin.Input() + + return p, nil +} + +var _ BoolGetter = (*gpio)(nil) + +// BoolGetter returns GPIO pin active +func (p *gpio) BoolGetter() (func() (bool, error), error) { + return func() (bool, error) { + p.mu.Lock() + defer p.mu.Unlock() + + if err := rpio.Open(); err != nil { + return false, fmt.Errorf("failed to open GPIO: %w", err) + } + defer rpio.Close() + + return p.pin.Read() != rpio.Low, nil + }, nil +}