Skip to content

Commit

Permalink
Add mqtt interface and document REST api (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Jul 26, 2020
1 parent e948e8b commit c12d52c
Show file tree
Hide file tree
Showing 16 changed files with 359 additions and 271 deletions.
168 changes: 68 additions & 100 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
- [Getting started](#getting-started)
- [Installation](#installation)
- [Configuration](#configuration)
- [Site](#site)
- [Loadpoint](#loadpoint)
- [Charger](#charger)
- [Meter](#meter)
- [Vehicle](#vehicle)
- [Loadpoint](#loadpoint)
- [Considerations](#considerations)
- [Plugins](#plugins)
- [Modbus](#modbus-read-only)
- [MQTT](#mqtt-readwrite)
- [Script](#script-readwrite)
- [HTTP](#http-readwrite)
- [Websocket](#websocket-read-only)
- [Combined status](#combined-status-read-only)
- [Developer information](#developer-information)
- [API](#api)
- [Background](#background)

## Getting started
Expand Down Expand Up @@ -79,6 +79,12 @@ EVCC can also be run using Docker. Here's and example with given config file and
docker run -v $(pwd)/evcc.dist.yaml:/etc/evcc.yaml -p 7070:7070 andig/evcc -h
```

If using Docker with a meter or charger that requires UDP like KEBA or SMA Energy Meter, make sure that the Docker container can receive UDP messages on the relevant ports (`:7090` for KEBA and `:9522` for SMA):

```sh
docker run -p 7070:7070 -p 7090:7090/udp -p 7090:7090/udp -p 9522:9522/udp andig/evcc ...
```

To build EVCC from source, [Go](2) 1.13 is required:

make
Expand All @@ -89,7 +95,45 @@ All components **must** be installed by a certified professional.

## Configuration

The EVCC consists of four basic elements: *Charger*, *Meter* and *Vehicle* individually configured and attached to *Loadpoints*.
The EVCC consists of five basic elements: *Site* and *Loadpoints* describe the infrastructure and combine *Charger*s, *Meter*s and *Vehicle*s.

### Site

A site describes the grid connection and is responsible for managing the available power. A minimal site configuration requires a grid meter for managing EVU demand and optionally a PV or battery meter.

```yaml
site:
- title: Zuhause # display name for UI
meters:
grid: sdm630 # grid meter reference
pv: sma # pv meter reference
```
### Loadpoint
Loadpoints combine meters, charger and vehicle together and add optional configuration. A minimal loadpoint configuration requires a charger and optionally a separate charge meter. If charger has an integrated meter it will automatically be used:
```yaml
loadpoints:
- title: Garage # display name for UI
charger: wallbe # charger reference
vehicle: audi # vehicle reference
meters:
charge: sdm630 # grid meter reference
```
More options are documented in the `evcc.dist.yaml` sample configuration.

#### Charge modes

The default *charge mode* upon start of EVCC is configured on the loadpoint. Multiple charge modes are supported:

- **Off**: disable the charger, even if car gets connected.
- **Now** (**Sofortladen**): charge immediately with maximum allowed current.
- **Min + PV**: charge immediately with minimum configured current. Additionally use PV if available.
- **PV**: use PV as available. May not charge the car if PV remains dark.

In general, due to the minimum value of 5% for signalling the EV duty cycle, the charger cannot limit the current to below 6A. If the available power calculation demands a limit less than 6A, handling depends on the charge mode. In **PV** mode, the charger will be disabled until available PV power supports charging with at least 6A. In **Min + PV** mode, charging will continue at minimum current of 6A and charge current will be raised as PV power becomes available again.

### Charger

Expand Down Expand Up @@ -131,12 +175,6 @@ Compare the value to what you see as *Actual Charge Current Setting* in the Wall

KEBA chargers require UDP function to be enabled with DIP switch 1.3 = `ON`, see KEBA installation manual.

If using Docker, make sure that the Docker container can receive UDP messages on port 7090 used by KEBA by using [host networking](https://docs.docker.com/network/host/) in Docker:

```sh
docker run --network=host -p 7070:7070 andig/evcc ...
```

### Meter

Meters provide data about power and energy consumption. Available meter implementations are:
Expand Down Expand Up @@ -201,71 +239,6 @@ Available vehicle implementations are:
- `porsche`: Porsche (Taycan)
- `default`: default vehicle implementation using configurable [plugins](#plugins) for integrating any type of vehicle

### Loadpoint

A loadpoint combines meters, charger and vehicle together and adds optional configuration. A minimal loadpoint configuration needs either pv or grid meter and a charger. More meters can be added as needed:

```yaml
loadpoints:
- name: main # name for logging
charger: wallbe # charger reference
vehicle: audi # vehicle reference
meters:
grid: sdm630 # grid meter reference
pv: sma # pv meter reference
```

More options are documented in the `evcc.dist.yaml` sample configuration.

#### Charge modes

The default *charge mode* upon start of EVCC is configured on the loadpoint. Multiple charge modes are supported:

- **Off**: disable the charger, even if car gets connected.
- **Now** (**Sofortladen**): charge immediately with maximum allowed current.
- **Min + PV**: charge immediately with minimum configured current. Additionally use PV if available.
- **PV**: use PV as available. May not charge the car if PV remains dark.

In general, due to the minimum value of 5% for signalling the EV duty cycle, the charger cannot limit the current to below 6A. If the available power calculation demands a limit less than 6A, handling depends on the charge mode. In **PV** mode, the charger will be disabled until available PV power supports charging with at least 6A. In **Min + PV** mode, charging will continue at minimum current of 6A and charge current will be raised as PV power becomes available again.

### Considerations

For intelligent control of PV power usage, EVCC needs to assess how much residual PV power is available at the grid connection point and how much power the charger actually uses. Various methods are implemented to obtain this information, with different degrees of accuracy.

- **PV meter**: Configuring a *PV meter* is the simplest option. *PV meter* measures the PV generation. The charger is allowed to consume:

Charge Power = PV Meter Power - Residual Power

The *pv meter* is expected to deliver negative values for export and should not return positive values.

*Residual Power* is a configurable assumption how much power remaining facilities beside the charger use.

- **Grid meter**: Configuring a *grid meter* is the preferred option. The *grid meter* is expected to be a two-way meter (import+export) and return the current amount of grid export as negative value measured in Watt (W). The charger is then allowed to consume:

Charge Power = Current Charge Power - Grid Meter Power - Residual Power

In this setup, *residual power* is used as margin to account for fluctuations in PV production that may be faster than EVCC's control loop.

- **Battery meter**: *battery meter* is used if a home battery is installed and you want charging the EV take priority over charging the home battery. As the home battery would otherwise "grab" all available PV power, this meter measures the home battery charging power.

With *grid meter* the charger is then allowed to consume:

Charge Power = Current Charge Power - Grid Meter Power + Battery Meter Power - Residual Power

or without *grid meter*

Charge Power = PV Meter Power + Battery Meter Power - Residual Power

The *battery meter* is expected to deliver negative values when charging and positive values when discharging.

When using a *grid meter* for accurate control of PV utilization, EVCC needs to be able to determine the current charge power. There are two configurations for determining the *current charge power*:

- **Charge meter**: A *charge meter* is often integrated into the charger but can also be installed separately. EVCC expects the *charge meter* to supply *charge power* in Watt (W) and preferably *total energy* in kWh.
If *total energy* is supplied, it can be used to calculate the *charged energy* for the current charging cycle.

- **No charge meter**: If no charge meter is installed, *charge power* is deducted from *charge current* as controlled by the charger. This method is less accurate than using a *charge meter* since the EV may chose to use less power than EVCC has allowed for consumption.
If the charger supplies *total energy* for the charging cycle this value is preferred over the *charge meter*'s value (if present).

## Plugins

Plugins are used to integrate physical devices and external data sources with EVCC. Plugins support both *read* and *write* access. When using plugins for *write* access, the actual data is provided as variable in form of `${var[:format]}`. If `format` is omitted, data is formatted according to the default Go `%v` [format](https://golang.org/pkg/fmt/). The variable is replaced with the actual data before the plugin is executed.
Expand Down Expand Up @@ -467,37 +440,32 @@ charging:
topic: openWB/lp/1/boolChargeStat
```

## Developer information

EVCC has the following internal API. The full documentation is available in GoDoc format in https://pkg.go.dev/github.com/andig/evcc/api.

### Charger API

- `Status()`: get charge controller status (`A...F`)
- `Enabled()`: get charger availability
- `Enable(bool)`: set charger availability
- `MaxCurrent(int)`: set maximum allowed charge current in A

Optionally, charger can also provide:

- `CurrentPower()`: power in W (used if charge meter is not present)
## API

### Meter API
EVCC provides a REST and MQTT APIs.

- `CurrentPower()`: power in W
- `TotalEnergy()`: energy in kWh (optional)
### REST API

### Vehicle API
- `/api/config`: EVCC static configuration
- `/api/state`: EVCC dynamic state
- `/api/mode`: global charge mode, use `/api/mode/<mode>` to modify
- `/api/targetsoc`: global target SoC, use `/api/targetsoc/<soc>` to modify
- `/api/loadpoints/<id>/mode`: loadpoint charge mode, use `/api/loadpoints/<id>/mode/<mode>` to modify
- `/api/loadpoints/<id>/targetsoc`: loadpoint target SoC, use `/api/loadpoints/<id>/targetsoc/<soc>` to modify

- `Title()`: vehicle name for display in the configuration UI
- `Capacity()`: battery capacity in kWh
- `ChargeState()`: state of charge in %
### MQTT API

Optionally, vehicles can also provide:
The MQTT API follows the REST API's structure:

- `CurrentPower()`: charge power in W (used if charge meter not present)
- `ChargedEnergy()`: charged energy in kWh
- `ChargeDuration()`: charge duration
- `evcc`: root topic
- `evcc/updated`: timestamp of last update
- `evcc/site`: site dynamic state
- `evcc/site/mode`: global charge mode, write `<mode>` to `/evcc/site/mode/set` to modify
- `evcc/site/targetsoc`: global target SoC, write `<soc>` to `/evcc/site/targetsoc/set` to modify
- `evcc/loadpoints`: number of available loadpoints
- `evcc/loadpoints/<id>`: loadpoint dynamic state
- `evcc/loadpoints/<id>/mode`: loadpoint charge mode, write `<mode>` to `/evcc/loadpoints/<id>/mode/set` to modify
- `evcc/loadpoints/<id>/targetsoc`: loadpoint target SoC, write `<soc>` to `/evcc/loadpoints/<id>/targetsoc/set` to modify

## Background

Expand Down
2 changes: 1 addition & 1 deletion assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ <h1 class="display-4 pt-3" v-if="title">{{title}}</h1>

<script type="text/x-template" id="main-template">
<div class="container">
<site v-bind:state="state" v-if="configured"></site>
<site v-bind:state="state" v-if="<<.Configured>>"></site>
<div v-else>
<div class="row py-5">
<div class="col12">
Expand Down
2 changes: 1 addition & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ Vue.component("loadpoint", {
},
methods: {
api: function (func) {
return "lp/" + this.id + "/" + func;
return "loadpoint/" + this.id + "/" + func;
},
targetMode: function (mode) {
axios.post(this.api("mode") + "/" + mode).then(function (response) {
Expand Down
5 changes: 2 additions & 3 deletions cmd/charger.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"

"github.com/andig/evcc/api"
"github.com/andig/evcc/provider"
"github.com/andig/evcc/server"
"github.com/andig/evcc/util"
"github.com/spf13/cobra"
Expand All @@ -30,8 +29,8 @@ func runCharger(cmd *cobra.Command, args []string) {
conf := loadConfigFile(cfgFile)

// setup mqtt
if viper.Get("mqtt") != nil {
provider.MQTT = provider.NewMqttClient(conf.Mqtt.Broker, conf.Mqtt.User, conf.Mqtt.Password, clientID(), 1)
if conf.Mqtt.Broker != "" {
configureMQTT(conf.Mqtt, nil, nil)
}

cp := &ConfigProvider{}
Expand Down
9 changes: 2 additions & 7 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/andig/evcc/api"
"github.com/andig/evcc/charger"
"github.com/andig/evcc/meter"
"github.com/andig/evcc/provider"
"github.com/andig/evcc/push"
"github.com/andig/evcc/server"
"github.com/andig/evcc/vehicle"
Expand All @@ -16,7 +17,7 @@ type config struct {
Log string
Levels map[string]string
Interval time.Duration
Mqtt mqttConfig
Mqtt provider.MqttConfig
Influx server.InfluxConfig
Menu []server.MenuConfig
Messaging messagingConfig
Expand All @@ -42,12 +43,6 @@ type messagingConfig struct {
Services []typedConfig
}

type mqttConfig struct {
Broker string
User string
Password string
}

// ConfigProvider provides configuration items
type ConfigProvider struct {
meters map[string]api.Meter
Expand Down
5 changes: 2 additions & 3 deletions cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"os"
"testing"

"github.com/andig/evcc/provider"
"github.com/spf13/viper"
)

Expand Down Expand Up @@ -37,8 +36,8 @@ func TestDistConfig(t *testing.T) {
}

// setup mqtt
if viper.Get("mqtt") != nil {
provider.MQTT = provider.NewMqttClient(conf.Mqtt.Broker, conf.Mqtt.User, conf.Mqtt.Password, clientID(), 1)
if conf.Mqtt.Broker != "" {
configureMQTT(conf.Mqtt, nil, nil)
}

// check config is valid
Expand Down
5 changes: 2 additions & 3 deletions cmd/meter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"

"github.com/andig/evcc/api"
"github.com/andig/evcc/provider"
"github.com/andig/evcc/server"
"github.com/andig/evcc/util"
"github.com/spf13/cobra"
Expand All @@ -30,8 +29,8 @@ func runMeter(cmd *cobra.Command, args []string) {
conf := loadConfigFile(cfgFile)

// setup mqtt
if viper.Get("mqtt") != nil {
provider.MQTT = provider.NewMqttClient(conf.Mqtt.Broker, conf.Mqtt.User, conf.Mqtt.Password, clientID(), 1)
if conf.Mqtt.Broker != "" {
configureMQTT(conf.Mqtt, nil, nil)
}

cp := &ConfigProvider{}
Expand Down
17 changes: 8 additions & 9 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"os"
"time"

"github.com/andig/evcc/provider"
"github.com/andig/evcc/server"
"github.com/andig/evcc/server/updater"
"github.com/andig/evcc/util"
Expand Down Expand Up @@ -125,21 +124,21 @@ func run(cmd *cobra.Command, args []string) {
uri := viper.GetString("uri")
log.INFO.Println("listening at", uri)

// setup mqtt
if viper.Get("mqtt") != nil {
provider.MQTT = provider.NewMqttClient(conf.Mqtt.Broker, conf.Mqtt.User, conf.Mqtt.Password, clientID(), 1)
}

// setup loadpoints
site := loadConfig(conf)

// start broadcasting values
tee := &Tee{}

// value cache
cache := util.NewCache()
go cache.Run(tee.Attach())

// setup loadpoints
site := loadConfig(conf)

// setup mqtt
if conf.Mqtt.Broker != "" {
configureMQTT(conf.Mqtt, site, tee.Attach())
}

// setup database
if conf.Influx.URL != "" {
configureDatabase(conf.Influx, site.LoadPoints(), tee.Attach())
Expand Down
Loading

0 comments on commit c12d52c

Please sign in to comment.