Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HTTP provider #72

Merged
merged 4 commits into from
Apr 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
- [Modbus](#modbus-read-only)
- [MQTT](#mqtt-readwrite)
- [Script](#script-readwrite)
- [HTTP](#http-readwrite)
- [Combined status](#combined-status-read-only)
- [Developer](#developer)
- [Background](#background)
Expand Down Expand Up @@ -320,6 +321,28 @@ cmd: /home/user/my-script.sh ${enable:%b} # format boolean enable as 0/1
timeout: 5s
```

### HTTP (read/write)

The `http` plugin executes HTTP requests to read or update data. Includes the ability to read and parse JSON using jq-like queries.

Sample read configuration:

```yaml
type: http
uri: https://volkszaehler/api/data/<uuid>.json?from=now
method: GET # default HTTP method
headers:
- content-type: application/json
jq: .data.tuples.[0].[1] # parse response json
```

Sample write configuration:

```yaml
...
body: %v # only applicable for PUT or POST requests
```

### Combined status (read only)

The `combined` status plugin is used to convert a mixed boolean status of plugged/charging into an EVCC-compatible charger status of A..F. It is typically used together with OpenWB MQTT integration.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/gregdel/pushover v0.0.0-20200330145937-ee607c681498
github.com/grid-x/modbus v0.0.0-20200108122021-57d05a9f1e1a
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d
github.com/itchyny/gojq v0.10.1
github.com/joeshaw/carwings v0.0.0-20191118152321-61b46581307a
github.com/jsgoecke/tesla v0.0.0-20190206234002-112508e1374e
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
Expand Down
70 changes: 42 additions & 28 deletions go.sum

Large diffs are not rendered by default.

20 changes: 16 additions & 4 deletions provider/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ func scriptFromConfig(log *util.Logger, other map[string]interface{}) scriptConf
// NewFloatGetterFromConfig creates a FloatGetter from config
func NewFloatGetterFromConfig(log *util.Logger, config Config) (res FloatGetter) {
switch strings.ToLower(config.Type) {
case "http":
res = NewHTTPProviderFromConfig(log, config.Other).FloatGetter
case "mqtt":
pc := mqttFromConfig(log, config.Other)
res = MQTT.FloatGetter(pc.Topic, pc.Multiplier, pc.Timeout)
Expand All @@ -84,6 +86,8 @@ func NewFloatGetterFromConfig(log *util.Logger, config Config) (res FloatGetter)
// NewIntGetterFromConfig creates a IntGetter from config
func NewIntGetterFromConfig(log *util.Logger, config Config) (res IntGetter) {
switch strings.ToLower(config.Type) {
case "http":
res = NewHTTPProviderFromConfig(log, config.Other).IntGetter
case "mqtt":
pc := mqttFromConfig(log, config.Other)
res = MQTT.IntGetter(pc.Topic, int64(pc.Multiplier), pc.Timeout)
Expand All @@ -105,6 +109,8 @@ func NewIntGetterFromConfig(log *util.Logger, config Config) (res IntGetter) {
// NewStringGetterFromConfig creates a StringGetter from config
func NewStringGetterFromConfig(log *util.Logger, config Config) (res StringGetter) {
switch strings.ToLower(config.Type) {
case "http":
res = NewHTTPProviderFromConfig(log, config.Other).StringGetter
case "mqtt":
pc := mqttFromConfig(log, config.Other)
res = MQTT.StringGetter(pc.Topic, pc.Timeout)
Expand All @@ -126,6 +132,8 @@ func NewStringGetterFromConfig(log *util.Logger, config Config) (res StringGette
// NewBoolGetterFromConfig creates a BoolGetter from config
func NewBoolGetterFromConfig(log *util.Logger, config Config) (res BoolGetter) {
switch strings.ToLower(config.Type) {
case "http":
res = NewHTTPProviderFromConfig(log, config.Other).BoolGetter
case "mqtt":
pc := mqttFromConfig(log, config.Other)
res = MQTT.BoolGetter(pc.Topic, pc.Timeout)
Expand All @@ -145,13 +153,15 @@ func NewBoolGetterFromConfig(log *util.Logger, config Config) (res BoolGetter) {
// NewIntSetterFromConfig creates a IntSetter from config
func NewIntSetterFromConfig(log *util.Logger, param string, config Config) (res IntSetter) {
switch strings.ToLower(config.Type) {
case "http":
res = NewHTTPProviderFromConfig(log, config.Other).IntSetter
case "mqtt":
pc := mqttFromConfig(log, config.Other)
res = MQTT.IntSetter(param, pc.Topic, pc.Payload)
case "script":
pc := scriptFromConfig(log, config.Other)
exec := NewScriptProvider(pc.Timeout)
res = exec.IntSetter(param, pc.Cmd)
script := NewScriptProvider(pc.Timeout)
res = script.IntSetter(param, pc.Cmd)
default:
log.FATAL.Fatalf("invalid setter type %s", config.Type)
}
Expand All @@ -161,13 +171,15 @@ func NewIntSetterFromConfig(log *util.Logger, param string, config Config) (res
// NewBoolSetterFromConfig creates a BoolSetter from config
func NewBoolSetterFromConfig(log *util.Logger, param string, config Config) (res BoolSetter) {
switch strings.ToLower(config.Type) {
case "http":
res = NewHTTPProviderFromConfig(log, config.Other).BoolSetter
case "mqtt":
pc := mqttFromConfig(log, config.Other)
res = MQTT.BoolSetter(param, pc.Topic, pc.Payload)
case "script":
pc := scriptFromConfig(log, config.Other)
exec := NewScriptProvider(pc.Timeout)
res = exec.BoolSetter(param, pc.Cmd)
script := NewScriptProvider(pc.Timeout)
res = script.BoolSetter(param, pc.Cmd)
default:
log.FATAL.Fatalf("invalid setter type %s", config.Type)
}
Expand Down
142 changes: 142 additions & 0 deletions provider/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package provider

import (
"fmt"
"io"
"math"
"net/http"
"strconv"
"strings"

"github.com/andig/evcc/util"
"github.com/andig/evcc/util/jq"
"github.com/itchyny/gojq"
)

// HTTP implements HTTP request provider
type HTTP struct {
log *util.Logger
*util.HTTPHelper
url, method string
headers map[string]string
body string
scale float64
jq *gojq.Query
}

// NewHTTPProviderFromConfig creates a HTTP provider
func NewHTTPProviderFromConfig(log *util.Logger, other map[string]interface{}) *HTTP {
cc := struct {
URI, Method string
Headers map[string]string
Body string
Jq string
Scale float64
}{}
util.DecodeOther(log, other, &cc)

logger := util.NewLogger("http")

p := &HTTP{
log: logger,
HTTPHelper: util.NewHTTPHelper(logger),
url: cc.URI,
method: cc.Method,
headers: cc.Headers,
body: cc.Body,
scale: cc.Scale,
}

if cc.Jq != "" {
op, err := gojq.Parse(cc.Jq)
if err != nil {
log.FATAL.Fatalf("config: invalid jq query: %s", p.jq)
}

p.jq = op
}

return p
}

// request executed the configured request
func (p *HTTP) request(body ...string) ([]byte, error) {
var b io.Reader
if len(body) == 1 {
b = strings.NewReader(body[0])
}

// empty method becomes GET
req, err := http.NewRequest(strings.ToUpper(p.method), p.url, b)
if err == nil {
for k, v := range p.headers {
req.Header.Add(k, v)
}
return p.Request(req)
}

return []byte{}, err
}

// FloatGetter parses float from request
func (p *HTTP) FloatGetter() (float64, error) {
s, err := p.StringGetter()
if err != nil {
return 0, err
}

f, err := strconv.ParseFloat(s, 64)
if err == nil && p.scale > 0 {
f *= p.scale
}

return f, err
}

// IntGetter parses int64 from request
func (p *HTTP) IntGetter() (int64, error) {
f, err := p.FloatGetter()
return int64(math.Round(f)), err
}

// StringGetter sends string request
func (p *HTTP) StringGetter() (string, error) {
b, err := p.request()
if err != nil {
return string(b), err
}

if p.jq != nil {
v, err := jq.Query(p.jq, b)
return fmt.Sprintf("%v", v), err
}

return string(b), err
}

// BoolGetter parses bool from request
func (p *HTTP) BoolGetter() (bool, error) {
s, err := p.StringGetter()
return util.Truish(s), err
}

// IntSetter sends int request
func (p *HTTP) IntSetter(param int64) error {
body := util.FormatValue(p.body, param)
_, err := p.request(body)
return err
}

// StringSetter sends string request
func (p *HTTP) StringSetter(param string) error {
body := util.FormatValue(p.body, param)
_, err := p.request(body)
return err
}

// BoolSetter sends bool request
func (p *HTTP) BoolSetter(param bool) error {
body := util.FormatValue(p.body, param)
_, err := p.request(body)
return err
}
File renamed without changes.
33 changes: 33 additions & 0 deletions util/jq/jq.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package jq

import (
"encoding/json"

"github.com/itchyny/gojq"
"github.com/pkg/errors"
)

// Query executes a compiled jq query against given input. It expects a single result only.
func Query(query *gojq.Query, input []byte) (interface{}, error) {
var j interface{}
if err := json.Unmarshal(input, &j); err != nil {
return j, err
}

iter := query.Run(j)

v, ok := iter.Next()
if !ok {
return nil, errors.New("jq: empty result")
}

if err, ok := v.(error); ok {
return nil, errors.Wrap(err, "jq: query failed")
}

if _, ok := iter.Next(); ok {
return nil, errors.New("jq: too many results")
}

return v, nil
}