From cba97832db8966eab8acbb66089a4e40611dfed8 Mon Sep 17 00:00:00 2001 From: Markus Heberling Date: Thu, 18 May 2023 14:08:05 +0200 Subject: [PATCH] Plugins: allow chaining of input/output plugins (#7836) --- cmd/demo.yaml | 57 +++++++---- provider/calc.go | 2 +- provider/combined.go | 27 +++++ provider/config.go | 203 ++++++++++++++++++++++--------------- provider/const.go | 2 +- provider/go.go | 148 +++++++++++++++++++-------- provider/http.go | 2 +- provider/javascript.go | 156 ++++++++++++++++++++-------- provider/modbus.go | 2 +- provider/mqtt.go | 2 +- provider/prometheus.go | 2 +- provider/script.go | 2 +- provider/sma.go | 2 +- provider/socket.go | 2 +- provider/socket_test.go | 2 +- provider/transformation.go | 170 +++++++++++++++++++++++++++++++ 16 files changed, 582 insertions(+), 199 deletions(-) create mode 100644 provider/combined.go create mode 100644 provider/transformation.go diff --git a/cmd/demo.yaml b/cmd/demo.yaml index 4e452d2f2..55f56b4e3 100644 --- a/cmd/demo.yaml +++ b/cmd/demo.yaml @@ -19,11 +19,7 @@ javascript: { enabled: false, vehicleSoc: 22, maxcurrent: 0, phases: 3, chargepower: 0 } ] }; - function get() { - console.log("state:", JSON.stringify(state)); - } - function set() { - console.log(param+":", val); + function logState() { console.log("state:", JSON.stringify(state)); } @@ -34,10 +30,25 @@ meters: source: js vm: shared script: | - state.pvpower = 8000+500*Math.random(); - state.batterypower = state.gridpower > 0 ? 1000 * Math.random() : 0; - state.gridpower = state.loadpoints[0].chargepower + state.loadpoints[1].chargepower + state.residualpower - state.batterypower - state.pvpower; + state.gridpower = state.loadpoints[0].chargepower + state.loadpoints[1].chargepower + state.residualpower - batterypower - pvpower; state.gridpower; + in: + - name: pvpower + type: float + config: + source: js + vm: shared + script: | + state.pvpower = 8000+500*Math.random(); + state.pvpower + - name: batterypower + type: float + config: + source: js + vm: shared + script: | + state.batterypower = state.gridpower > 0 ? 1000 * Math.random() : 0; + state.batterypower - name: pv type: custom @@ -83,10 +94,18 @@ chargers: source: js vm: shared script: | - set(); + logState(); var lp = state.loadpoints[0]; - lp.enabled = val; - if (lp.enabled) lp.chargepower = lp.maxcurrent * 230 * lp.phases; else lp.chargepower = 0; + lp.enabled = enable; + enable; + out: + - name: enable + type: bool + config: + source: js + vm: shared + script: | + if (enable) lp.chargepower = lp.maxcurrent * 230 * lp.phases; else lp.chargepower = 0; enabled: source: js vm: shared @@ -101,9 +120,9 @@ chargers: source: js vm: shared script: | - set(); + logState(); var lp = state.loadpoints[0]; - lp.maxcurrent = val; + lp.maxcurrent = maxcurrent; if (lp.enabled) lp.chargepower = lp.maxcurrent * 230 * lp.phases; else lp.chargepower = 0; - name: charger_2 @@ -112,9 +131,9 @@ chargers: source: js vm: shared script: | - set(); + logState(); var lp = state.loadpoints[1]; - lp.enabled = val; + lp.enabled = enable; if (lp.enabled) lp.chargepower = lp.maxcurrent * 230 * lp.phases; else lp.chargepower = 0; enabled: source: js @@ -130,16 +149,16 @@ chargers: source: js vm: shared script: | - set(); + logState(); var lp = state.loadpoints[1]; - lp.maxcurrent = val; + lp.maxcurrent = maxcurrent; if (lp.enabled) lp.chargepower = lp.maxcurrent * 230 * lp.phases; else lp.chargepower = 0; phases1p3p: source: js vm: shared script: | - set(); - if (val === 1) lp.phases = 1; else lp.phases = 3; + logState(); + if (phases === 1) lp.phases = 1; else lp.phases = 3; lp.phases; vehicles: diff --git a/provider/calc.go b/provider/calc.go index eb6985bc1..fafcbb16b 100644 --- a/provider/calc.go +++ b/provider/calc.go @@ -18,7 +18,7 @@ func init() { } // NewCalcFromConfig creates calc provider -func NewCalcFromConfig(other map[string]interface{}) (IntProvider, error) { +func NewCalcFromConfig(other map[string]interface{}) (Provider, error) { var cc struct { Add []Config Mul []Config diff --git a/provider/combined.go b/provider/combined.go new file mode 100644 index 000000000..46301c4fe --- /dev/null +++ b/provider/combined.go @@ -0,0 +1,27 @@ +package provider + +type combinedProvider struct { + status func() (string, error) +} + +func init() { + registry.Add("combined", NewCombinedFromConfig) + registry.Add("openwb", NewCombinedFromConfig) +} + +// NewCombinedFromConfig creates combined provider +func NewCombinedFromConfig(other map[string]interface{}) (Provider, error) { + status, err := NewOpenWBStatusProviderFromConfig(other) + if err != nil { + return nil, err + } + + o := &combinedProvider{status: status} + return o, nil +} + +func (o *combinedProvider) StringGetter() func() (string, error) { + return func() (string, error) { + return o.status() + } +} diff --git a/provider/config.go b/provider/config.go index df346cc85..6457d71b6 100644 --- a/provider/config.go +++ b/provider/config.go @@ -6,6 +6,7 @@ import ( // provider types type ( + Provider interface{} IntProvider interface { IntGetter() func() (int64, error) } @@ -24,21 +25,24 @@ type ( SetStringProvider interface { StringSetter(param string) func(string) error } + SetFloatProvider interface { + FloatSetter(param string) func(float64) error + } SetBoolProvider interface { BoolSetter(param string) func(bool) error } ) -type providerRegistry map[string]func(map[string]interface{}) (IntProvider, error) +type providerRegistry map[string]func(map[string]interface{}) (Provider, error) -func (r providerRegistry) Add(name string, factory func(map[string]interface{}) (IntProvider, error)) { +func (r providerRegistry) Add(name string, factory func(map[string]interface{}) (Provider, error)) { if _, exists := r[name]; exists { panic(fmt.Sprintf("cannot register duplicate plugin type: %s", name)) } r[name] = factory } -func (r providerRegistry) Get(name string) (func(map[string]interface{}) (IntProvider, error), error) { +func (r providerRegistry) Get(name string) (func(map[string]interface{}) (Provider, error), error) { factory, exists := r[name] if !exists { return nil, fmt.Errorf("invalid plugin source: %s", name) @@ -46,7 +50,7 @@ func (r providerRegistry) Get(name string) (func(map[string]interface{}) (IntPro return factory, nil } -var registry providerRegistry = make(map[string]func(map[string]interface{}) (IntProvider, error)) +var registry providerRegistry = make(map[string]func(map[string]interface{}) (Provider, error)) // Config is the general provider config type Config struct { @@ -55,122 +59,161 @@ type Config struct { } // NewIntGetterFromConfig creates a IntGetter from config -func NewIntGetterFromConfig(config Config) (res func() (int64, error), err error) { +func NewIntGetterFromConfig(config Config) (func() (int64, error), error) { factory, err := registry.Get(config.Source) - if err == nil { - var provider IntProvider - provider, err = factory(config.Other) - - if err == nil { - res = provider.IntGetter() - } + if err != nil { + return nil, err } - if err == nil && res == nil { - err = fmt.Errorf("invalid plugin source: %s", config.Source) + provider, err := factory(config.Other) + if err != nil { + return nil, err } - return + prov, ok := provider.(IntProvider) + if !ok { + return nil, fmt.Errorf("invalid plugin source for type int: %s", config.Source) + } + + return prov.IntGetter(), nil } // NewFloatGetterFromConfig creates a FloatGetter from config -func NewFloatGetterFromConfig(config Config) (res func() (float64, error), err error) { +func NewFloatGetterFromConfig(config Config) (func() (float64, error), error) { factory, err := registry.Get(config.Source) - if err == nil { - var provider IntProvider - provider, err = factory(config.Other) - - if prov, ok := provider.(FloatProvider); ok { - res = prov.FloatGetter() - } + if err != nil { + return nil, err } - if err == nil && res == nil { - err = fmt.Errorf("invalid plugin source: %s", config.Source) + provider, err := factory(config.Other) + if err != nil { + return nil, err } - return + prov, ok := provider.(FloatProvider) + if !ok { + return nil, fmt.Errorf("invalid plugin source for type float: %s", config.Source) + } + + return prov.FloatGetter(), nil } // NewStringGetterFromConfig creates a StringGetter from config -func NewStringGetterFromConfig(config Config) (res func() (string, error), err error) { - switch typ := config.Source; typ { - case "combined", "openwb": - res, err = NewOpenWBStatusProviderFromConfig(config.Other) - - default: - var factory func(map[string]interface{}) (IntProvider, error) - factory, err = registry.Get(typ) - if err == nil { - var provider IntProvider - provider, err = factory(config.Other) - - if prov, ok := provider.(StringProvider); ok { - res = prov.StringGetter() - } - } - - if err == nil && res == nil { - err = fmt.Errorf("invalid plugin source: %s", config.Source) - } +func NewStringGetterFromConfig(config Config) (func() (string, error), error) { + factory, err := registry.Get(config.Source) + if err != nil { + return nil, err } - return + provider, err := factory(config.Other) + if err != nil { + return nil, err + } + + prov, ok := provider.(StringProvider) + if !ok { + return nil, fmt.Errorf("invalid plugin source for type string: %s", config.Source) + } + + return prov.StringGetter(), nil } // NewBoolGetterFromConfig creates a BoolGetter from config -func NewBoolGetterFromConfig(config Config) (res func() (bool, error), err error) { +func NewBoolGetterFromConfig(config Config) (func() (bool, error), error) { factory, err := registry.Get(config.Source) - if err == nil { - var provider IntProvider - provider, err = factory(config.Other) - - if prov, ok := provider.(BoolProvider); ok { - res = prov.BoolGetter() - } + if err != nil { + return nil, err } - if err == nil && res == nil { - err = fmt.Errorf("invalid plugin source: %s", config.Source) + provider, err := factory(config.Other) + if err != nil { + return nil, err } - return + prov, ok := provider.(BoolProvider) + if !ok { + return nil, fmt.Errorf("invalid plugin source for type bool: %s", config.Source) + } + + return prov.BoolGetter(), nil } // NewIntSetterFromConfig creates a IntSetter from config -func NewIntSetterFromConfig(param string, config Config) (res func(int64) error, err error) { +func NewIntSetterFromConfig(param string, config Config) (func(int64) error, error) { factory, err := registry.Get(config.Source) - if err == nil { - var provider IntProvider - provider, err = factory(config.Other) - - if prov, ok := provider.(SetIntProvider); ok { - res = prov.IntSetter(param) - } + if err != nil { + return nil, err } - if err == nil && res == nil { - err = fmt.Errorf("invalid plugin source: %s", config.Source) + provider, err := factory(config.Other) + if err != nil { + return nil, err } - return + prov, ok := provider.(SetIntProvider) + if !ok { + return nil, fmt.Errorf("invalid plugin source for type int: %s", config.Source) + } + + return prov.IntSetter(param), nil +} + +// NewFloatSetterFromConfig creates a FloatSetter from config +func NewFloatSetterFromConfig(param string, config Config) (func(float642 float64) error, error) { + factory, err := registry.Get(config.Source) + if err != nil { + return nil, err + } + + provider, err := factory(config.Other) + if err != nil { + return nil, err + } + + prov, ok := provider.(SetFloatProvider) + if !ok { + return nil, fmt.Errorf("invalid plugin source for type float: %s", config.Source) + } + + return prov.FloatSetter(param), nil +} + +// NewStringSetterFromConfig creates a StringSetter from config +func NewStringSetterFromConfig(param string, config Config) (func(string) error, error) { + factory, err := registry.Get(config.Source) + if err != nil { + return nil, err + } + + provider, err := factory(config.Other) + if err != nil { + return nil, err + } + + prov, ok := provider.(SetStringProvider) + if !ok { + return nil, fmt.Errorf("invalid plugin source for type string: %s", config.Source) + } + + return prov.StringSetter(param), nil } // NewBoolSetterFromConfig creates a BoolSetter from config -func NewBoolSetterFromConfig(param string, config Config) (res func(bool) error, err error) { +func NewBoolSetterFromConfig(param string, config Config) (func(bool) error, error) { factory, err := registry.Get(config.Source) - if err == nil { - var provider IntProvider - provider, err = factory(config.Other) - - if prov, ok := provider.(SetBoolProvider); ok { - res = prov.BoolSetter(param) - } + if err != nil { + return nil, err } - if err == nil && res == nil { - err = fmt.Errorf("invalid plugin source: %s", config.Source) + provider, err := factory(config.Other) + if err != nil { + return nil, err } - return + prov, ok := provider.(SetBoolProvider) + if !ok { + return nil, fmt.Errorf("invalid plugin source for type bool: %s", config.Source) + } + + return prov.BoolSetter(param), nil } diff --git a/provider/const.go b/provider/const.go index 5fdc42ffe..6db48b8ed 100644 --- a/provider/const.go +++ b/provider/const.go @@ -16,7 +16,7 @@ func init() { } // NewConstFromConfig creates const provider -func NewConstFromConfig(other map[string]interface{}) (IntProvider, error) { +func NewConstFromConfig(other map[string]interface{}) (Provider, error) { var cc struct { Value string pipeline.Settings `mapstructure:",squash"` diff --git a/provider/go.go b/provider/go.go index 57e51f4bc..c445fa1ec 100644 --- a/provider/go.go +++ b/provider/go.go @@ -2,7 +2,6 @@ package provider import ( "fmt" - "reflect" "github.com/evcc-io/evcc/provider/golang" "github.com/evcc-io/evcc/util" @@ -13,6 +12,8 @@ import ( type Go struct { vm *interp.Interpreter script string + in []inputTransformation + out []outputTransformation } func init() { @@ -20,10 +21,12 @@ func init() { } // NewGoProviderFromConfig creates a Go provider -func NewGoProviderFromConfig(other map[string]interface{}) (IntProvider, error) { +func NewGoProviderFromConfig(other map[string]interface{}) (Provider, error) { var cc struct { VM string Script string + In []transformationConfig + Out []transformationConfig } if err := util.DecodeOther(other, &cc); err != nil { @@ -35,9 +38,21 @@ func NewGoProviderFromConfig(other map[string]interface{}) (IntProvider, error) return nil, err } + in, err := configureInputs(cc.In) + if err != nil { + return nil, err + } + + out, err := configureOutputs(cc.Out) + if err != nil { + return nil, err + } + p := &Go{ vm: vm, script: cc.Script, + in: in, + out: out, } return p, nil @@ -45,92 +60,135 @@ func NewGoProviderFromConfig(other map[string]interface{}) (IntProvider, error) // FloatGetter parses float from request func (p *Go) FloatGetter() func() (float64, error) { - return func() (res float64, err error) { - v, err := p.vm.Eval(p.script) - if err == nil { - if typ := reflect.TypeOf(res); v.CanConvert(typ) { - res = v.Convert(typ).Float() - } else { - err = fmt.Errorf("not a float: %v", v) - } + return func() (float64, error) { + v, err := p.handleGetter() + if err != nil { + return 0, err } - return res, err + vv, ok := v.(float64) + if !ok { + return 0, fmt.Errorf("not a float: %s", v) + } + + return vv, nil } } // IntGetter parses int64 from request func (p *Go) IntGetter() func() (int64, error) { - return func() (res int64, err error) { - v, err := p.vm.Eval(p.script) - if err == nil { - if typ := reflect.TypeOf(res); v.CanConvert(typ) { - res = v.Convert(typ).Int() - } else { - err = fmt.Errorf("not an int: %v", v) - } + return func() (int64, error) { + v, err := p.handleGetter() + if err != nil { + return 0, err } - return res, err + vv, ok := v.(int64) + if !ok { + return 0, fmt.Errorf("not a int: %s", v) + } + + return vv, nil } } // StringGetter parses string from request func (p *Go) StringGetter() func() (string, error) { - return func() (res string, err error) { - v, err := p.vm.Eval(p.script) - if err == nil { - if typ := reflect.TypeOf(res); v.CanConvert(typ) { - res = v.Convert(typ).String() - } else { - err = fmt.Errorf("not a string: %v", v) - } + return func() (string, error) { + v, err := p.handleGetter() + if err != nil { + return "", err } - return res, err + vv, ok := v.(string) + if !ok { + return "", fmt.Errorf("not a string: %s", v) + } + + return vv, nil } } // BoolGetter parses bool from request func (p *Go) BoolGetter() func() (bool, error) { - return func() (res bool, err error) { - v, err := p.vm.Eval(p.script) - if err == nil { - if typ := reflect.TypeOf(res); v.CanConvert(typ) { - res = v.Convert(typ).Bool() - } else { - err = fmt.Errorf("not a boolean: %v", v) - } + return func() (bool, error) { + v, err := p.handleGetter() + if err != nil { + return false, err } - return res, err + vv, ok := v.(bool) + if !ok { + return false, fmt.Errorf("not a bool: %s", v) + } + + return vv, nil } } -func (p *Go) paramAndEval(param string, val any) error { - _, err := p.vm.Eval(fmt.Sprintf("%s := %v;", param, val)) - if err == nil { - _, err = p.vm.Eval(p.script) +func (p *Go) handleGetter() (any, error) { + if err := transformInputs(p.in, p.setParam); err != nil { + return nil, err } + + return p.evaluate() +} + +func (p *Go) handleSetter(param string, val any) error { + if err := p.setParam(param, val); err != nil { + return err + } + + vv, err := p.evaluate() + if err != nil { + return err + } + + return transformOutputs(p.out, vv) +} + +func (p *Go) evaluate() (any, error) { + v, err := p.vm.Eval(p.script) + if err != nil { + return nil, err + } + + return normalizeValue(v.Interface()) +} + +func (p *Go) setParam(param string, val any) error { + if str, ok := val.(string); ok { + val = "\"" + str + "\"" + } + + _, err := p.vm.Eval(fmt.Sprintf("%s := %v;", param, val)) return err } +// IntSetter sends int request func (p *Go) IntSetter(param string) func(int64) error { return func(val int64) error { - return p.paramAndEval(param, val) + return p.handleSetter(param, val) + } +} + +// FloatSetter sends float request +func (p *Go) FloatSetter(param string) func(float64) error { + return func(val float64) error { + return p.handleSetter(param, val) } } // StringSetter sends string request func (p *Go) StringSetter(param string) func(string) error { return func(val string) error { - return p.paramAndEval(param, val) + return p.handleSetter(param, val) } } // BoolSetter sends bool request func (p *Go) BoolSetter(param string) func(bool) error { return func(val bool) error { - return p.paramAndEval(param, val) + return p.handleSetter(param, val) } } diff --git a/provider/http.go b/provider/http.go index 36afb5893..931c68d44 100644 --- a/provider/http.go +++ b/provider/http.go @@ -39,7 +39,7 @@ type Auth struct { } // NewHTTPProviderFromConfig creates a HTTP provider -func NewHTTPProviderFromConfig(other map[string]interface{}) (IntProvider, error) { +func NewHTTPProviderFromConfig(other map[string]interface{}) (Provider, error) { cc := struct { URI, Method string Headers map[string]string diff --git a/provider/javascript.go b/provider/javascript.go index 38065f99f..062d38725 100644 --- a/provider/javascript.go +++ b/provider/javascript.go @@ -1,6 +1,8 @@ package provider import ( + "fmt" + "github.com/evcc-io/evcc/provider/javascript" "github.com/evcc-io/evcc/util" "github.com/robertkrimen/otto" @@ -10,17 +12,21 @@ import ( type Javascript struct { vm *otto.Otto script string + in []inputTransformation + out []outputTransformation } func init() { registry.Add("js", NewJavascriptProviderFromConfig) } -// NewJavascriptProviderFromConfig creates a HTTP provider -func NewJavascriptProviderFromConfig(other map[string]interface{}) (IntProvider, error) { +// NewJavascriptProviderFromConfig creates a Javascript provider +func NewJavascriptProviderFromConfig(other map[string]interface{}) (Provider, error) { var cc struct { VM string Script string + In []transformationConfig + Out []transformationConfig } if err := util.DecodeOther(other, &cc); err != nil { @@ -32,9 +38,21 @@ func NewJavascriptProviderFromConfig(other map[string]interface{}) (IntProvider, return nil, err } + in, err := configureInputs(cc.In) + if err != nil { + return nil, err + } + + out, err := configureOutputs(cc.Out) + if err != nil { + return nil, err + } + p := &Javascript{ vm: vm, script: cc.Script, + in: in, + out: out, } return p, nil @@ -42,92 +60,140 @@ func NewJavascriptProviderFromConfig(other map[string]interface{}) (IntProvider, // FloatGetter parses float from request func (p *Javascript) FloatGetter() func() (float64, error) { - return func() (res float64, err error) { - v, err := p.vm.Eval(p.script) - if err == nil { - res, err = v.ToFloat() + return func() (float64, error) { + v, err := p.handleGetter() + if err != nil { + return 0, err } - return res, err + vv, ok := v.(float64) + if !ok { + return 0, fmt.Errorf("not a float: %s", v) + } + + return vv, nil } } // IntGetter parses int64 from request func (p *Javascript) IntGetter() func() (int64, error) { - return func() (res int64, err error) { - v, err := p.vm.Eval(p.script) - if err == nil { - res, err = v.ToInteger() + return func() (int64, error) { + v, err := p.handleGetter() + if err != nil { + return 0, err } - return res, err + vv, ok := v.(int64) + if !ok { + return 0, fmt.Errorf("not a int: %s", v) + } + + return vv, nil } } -// StringGetter sends string request +// StringGetter parses string from request func (p *Javascript) StringGetter() func() (string, error) { - return func() (res string, err error) { - v, err := p.vm.Eval(p.script) - if err == nil { - res, err = v.ToString() + return func() (string, error) { + v, err := p.handleGetter() + if err != nil { + return "", err } - return res, err + vv, ok := v.(string) + if !ok { + return "", fmt.Errorf("not a string: %s", v) + } + + return vv, nil } } // BoolGetter parses bool from request func (p *Javascript) BoolGetter() func() (bool, error) { - return func() (res bool, err error) { - v, err := p.vm.Eval(p.script) - if err == nil { - res, err = v.ToBoolean() + return func() (bool, error) { + v, err := p.handleGetter() + if err != nil { + return false, err } - return res, err + vv, ok := v.(bool) + if !ok { + return false, fmt.Errorf("not a bool: %s", v) + } + + return vv, nil } } -func (p *Javascript) setParam(param string, val interface{}) error { - err := p.vm.Set(param, val) - if err == nil { - err = p.vm.Set("param", param) +func (p *Javascript) handleGetter() (any, error) { + if err := transformInputs(p.in, p.setParam); err != nil { + return nil, err } - if err == nil { - err = p.vm.Set("val", val) + + v, err := p.evaluate() + if err != nil { + return nil, err } - return err + + return v, nil +} + +func (p *Javascript) handleSetter(param string, val any) error { + if err := p.setParam(param, val); err != nil { + return err + } + + v, err := p.evaluate() + if err != nil { + return err + } + + return transformOutputs(p.out, v) +} + +func (p *Javascript) evaluate() (any, error) { + v, err := p.vm.Eval(p.script) + if err != nil { + return nil, err + } + + vv, err := v.Export() + if err != nil { + return nil, err + } + + return normalizeValue(vv) +} + +func (p *Javascript) setParam(param string, val any) error { + return p.vm.Set(param, val) } // IntSetter sends int request func (p *Javascript) IntSetter(param string) func(int64) error { return func(val int64) error { - err := p.setParam(param, val) - if err == nil { - _, err = p.vm.Eval(p.script) - } - return err + return p.handleSetter(param, val) + } +} + +// FloatSetter sends float request +func (p *Javascript) FloatSetter(param string) func(float64) error { + return func(val float64) error { + return p.handleSetter(param, val) } } // StringSetter sends string request func (p *Javascript) StringSetter(param string) func(string) error { return func(val string) error { - err := p.setParam(param, val) - if err == nil { - _, err = p.vm.Eval(p.script) - } - return err + return p.handleSetter(param, val) } } // BoolSetter sends bool request func (p *Javascript) BoolSetter(param string) func(bool) error { return func(val bool) error { - err := p.setParam(param, val) - if err == nil { - _, err = p.vm.Eval(p.script) - } - return err + return p.handleSetter(param, val) } } diff --git a/provider/modbus.go b/provider/modbus.go index 7dd6878bb..56aa7ce9a 100644 --- a/provider/modbus.go +++ b/provider/modbus.go @@ -31,7 +31,7 @@ func init() { } // NewModbusFromConfig creates Modbus plugin -func NewModbusFromConfig(other map[string]interface{}) (IntProvider, error) { +func NewModbusFromConfig(other map[string]interface{}) (Provider, error) { cc := struct { Model string modbus.Settings `mapstructure:",squash"` diff --git a/provider/mqtt.go b/provider/mqtt.go index b5786c71a..2a589e01f 100644 --- a/provider/mqtt.go +++ b/provider/mqtt.go @@ -25,7 +25,7 @@ func init() { } // NewMqttFromConfig creates Mqtt provider -func NewMqttFromConfig(other map[string]interface{}) (IntProvider, error) { +func NewMqttFromConfig(other map[string]interface{}) (Provider, error) { cc := struct { mqtt.Config `mapstructure:",squash"` Topic, Payload string // Payload only applies to setters diff --git a/provider/prometheus.go b/provider/prometheus.go index e1c1c8ff6..6b9abb4fe 100644 --- a/provider/prometheus.go +++ b/provider/prometheus.go @@ -26,7 +26,7 @@ func init() { registry.Add("prometheus", NewPrometheusFromConfig) } -func NewPrometheusFromConfig(other map[string]interface{}) (IntProvider, error) { +func NewPrometheusFromConfig(other map[string]interface{}) (Provider, error) { cc := struct { Uri, Query string Timeout time.Duration diff --git a/provider/script.go b/provider/script.go index e84caa8dd..e10063642 100644 --- a/provider/script.go +++ b/provider/script.go @@ -37,7 +37,7 @@ func init() { } // NewScriptProviderFromConfig creates a script provider. -func NewScriptProviderFromConfig(other map[string]interface{}) (IntProvider, error) { +func NewScriptProviderFromConfig(other map[string]interface{}) (Provider, error) { cc := struct { Cmd string Timeout time.Duration diff --git a/provider/sma.go b/provider/sma.go index b7d010fc6..2eb9e5845 100644 --- a/provider/sma.go +++ b/provider/sma.go @@ -21,7 +21,7 @@ func init() { } // NewSMAFromConfig creates SMA provider -func NewSMAFromConfig(other map[string]interface{}) (IntProvider, error) { +func NewSMAFromConfig(other map[string]interface{}) (Provider, error) { cc := struct { URI, Password, Interface string Serial uint32 diff --git a/provider/socket.go b/provider/socket.go index ce294978d..4e64287f1 100644 --- a/provider/socket.go +++ b/provider/socket.go @@ -37,7 +37,7 @@ func init() { } // NewSocketProviderFromConfig creates a HTTP provider -func NewSocketProviderFromConfig(other map[string]interface{}) (IntProvider, error) { +func NewSocketProviderFromConfig(other map[string]interface{}) (Provider, error) { cc := struct { URI string Headers map[string]string diff --git a/provider/socket_test.go b/provider/socket_test.go index 1ddae0100..80a40d46d 100644 --- a/provider/socket_test.go +++ b/provider/socket_test.go @@ -46,7 +46,7 @@ func TestSocketProvider(t *testing.T) { }) require.NoError(t, err) - g := p.IntGetter() + g := p.(IntProvider).IntGetter() i, err := g() require.NoError(t, err) require.Equal(t, int64(1), i) diff --git a/provider/transformation.go b/provider/transformation.go new file mode 100644 index 000000000..ba7f56f05 --- /dev/null +++ b/provider/transformation.go @@ -0,0 +1,170 @@ +package provider + +import ( + "fmt" + "strings" +) + +type transformationConfig struct { + Name, Type string + Config Config +} + +type inputTransformation struct { + name string + function func() (any, error) +} + +type outputTransformation struct { + name string + function func(any) error +} + +func configureInputs(inConfig []transformationConfig) ([]inputTransformation, error) { + var in []inputTransformation + + for _, cc := range inConfig { + var f func() (any, error) + + switch strings.ToLower(cc.Type) { + case "bool": + ff, err := NewBoolGetterFromConfig(cc.Config) + if err != nil { + return nil, fmt.Errorf("%s: %w", cc.Name, err) + } + f = func() (any, error) { return ff() } + + case "int": + ff, err := NewIntGetterFromConfig(cc.Config) + if err != nil { + return nil, fmt.Errorf("%s: %w", cc.Name, err) + } + f = func() (any, error) { return ff() } + + case "float": + ff, err := NewFloatGetterFromConfig(cc.Config) + if err != nil { + return nil, fmt.Errorf("%s: %w", cc.Name, err) + } + f = func() (any, error) { return ff() } + + case "string": + ff, err := NewStringGetterFromConfig(cc.Config) + if err != nil { + return nil, fmt.Errorf("%s: %w", cc.Name, err) + } + f = func() (any, error) { return ff() } + + default: + return nil, fmt.Errorf("%s: Could not find converter for %s", cc.Name, cc.Type) + } + + in = append(in, inputTransformation{ + name: cc.Name, + function: f, + }) + } + return in, nil +} + +func configureOutputs(outConfig []transformationConfig) ([]outputTransformation, error) { + var out []outputTransformation + + for _, cc := range outConfig { + var f func(v any) error + + switch strings.ToLower(cc.Type) { + case "bool": + ff, err := NewBoolSetterFromConfig(cc.Name, cc.Config) + if err != nil { + return nil, fmt.Errorf("%s: %w", cc.Name, err) + } + + f = func(v any) error { + return ff(v.(bool)) + } + + case "int": + ff, err := NewIntSetterFromConfig(cc.Name, cc.Config) + if err != nil { + return nil, fmt.Errorf("%s: %w", cc.Name, err) + } + + f = func(v any) error { + return ff(v.(int64)) + } + + case "float": + ff, err := NewFloatSetterFromConfig(cc.Name, cc.Config) + if err != nil { + return nil, fmt.Errorf("%s: %w", cc.Name, err) + } + + f = func(v any) error { + return ff(v.(float64)) + } + + case "string": + ff, err := NewStringSetterFromConfig(cc.Name, cc.Config) + if err != nil { + return nil, fmt.Errorf("%s: %w", cc.Name, err) + } + + f = func(v any) error { + return ff(v.(string)) + } + + default: + return nil, fmt.Errorf("%s: invalid type %s", cc.Name, cc.Type) + } + + out = append(out, outputTransformation{ + name: cc.Name, + function: f, + }) + } + + return out, nil +} + +func transformInputs(in []inputTransformation, set func(string, any) error) error { + for _, cc := range in { + val, err := cc.function() + + if err == nil { + err = set(cc.name, val) + } + + if err != nil { + return fmt.Errorf("%s: %w", cc.name, err) + } + } + + return nil +} + +func transformOutputs(out []outputTransformation, v any) error { + for _, cc := range out { + if err := cc.function(v); err != nil { + return fmt.Errorf("%s: %w", cc.name, err) + } + } + + return nil +} + +// normalizeValue transforms compatible plugin return types to ensure only supported ones are used +func normalizeValue(val any) (any, error) { + switch v := val.(type) { + case int: + return int64(v), nil + case int32: + return int64(v), nil + case float32: + return float64(v), nil + case int64, float64, bool, string: + return v, nil + default: + return nil, fmt.Errorf("type not supported: %T", val) + } +}