Skip to content

Commit

Permalink
Add HTTP provider with jq ability (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig committed Apr 30, 2020
1 parent 532e6bd commit db71cf6
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 32 deletions.
23 changes: 23 additions & 0 deletions README.md
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
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
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
@@ -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
@@ -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
}

0 comments on commit db71cf6

Please sign in to comment.