Refactor to support Wallbe dip 10 enable/disable

This commit is contained in:
andig 2020-03-18 16:16:35 +01:00
parent 3c503b333d
commit da74ce17fd
48 changed files with 1274 additions and 1393 deletions

2
.gitignore vendored
View file

@ -13,3 +13,5 @@ audi.*
credentials.json
token.json
assets/js/debug.js
evcc
__debug_bin

View file

@ -2,3 +2,5 @@ issues:
exclude:
- "func `..Wallbe..showIO` is unused"
- "func `..Wallbe..showIOs` is unused"
- .regOverchargeProtect. is unused
- .regActualCurrent. is unused

View file

@ -26,7 +26,6 @@ FROM alpine
# Import from builder.
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
# Copy our static executable
COPY --from=builder /build/evcc /usr/local/bin/evcc

View file

@ -33,7 +33,8 @@ build:
publish-images:
@echo Version: $(VERSION) $(BUILD_DATE)
seihon publish -v "$(TAG_NAME)" -v "latest" --image-name andig/evcc-base --base-runtime-image alpine --dry-run=false --targets=arm.v6,amd64
# seihon publish -v "$(TAG_NAME)" -v "latest" --image-name andig/evcc1 --base-runtime-image alpine --dry-run=false --targets=arm.v6,amd64
docker build -t "andig/evcc:latest" . && docker push andig/evcc:latest
test-release:
goreleaser --snapshot --skip-publish --rm-dist

View file

@ -4,6 +4,17 @@
EVCC is an EV Charge Controller implemented in [Go](2). It comes with a bundled implementation for Wallbe chargers but supports any type of charger or meter through scripting and integration with MQTT.
Features:
- support for Wallbe chargers
- support for any charger, meter or EV using scripting
- integration with home automation - supports shell scripts and MQTT
- soft ramp-up/ramp-down of charge current
- electric contactor protection
- clean, non-bloat user interface
![Screenshot](docs/screenshot.png)
**NOTE:** You are using this software **entirely** at your own risk. It is your responsibility to verify it is working as intended.
## Background
@ -15,21 +26,27 @@ Hence, for a simplified and stricter implementation of an EV charge controller,
- typed language with ability for systematic testing - achieved by using [Go](2)
- structured cnofiguration - supports YAML-based [config file](evcc.dist.yaml)
- avoidance of feature bloat, simple and clean UI - utilizes [Bootstrap](3)
- integration with home automation - supports shell scripts and MQTT
- containerized operation beyond Raspbery Pi - provide multi-arch [Docker Image](4)
- support for multiple load points - tbd
## User interface
EVCC features a clean, non-bloat user interface:
![Screenshot](docs/screenshot.png)
## Installation
To build EVCC from source, [Go](2) 1.13 is required:
### Hardware
make
EVCC requires a supported charger and a combination of grid, PV and charge meter.
Charger and meters MUST be professionally installed.
#### Wallbe Charger
Wallbe chargers are supported out of the box. The Wallbe must be connected using Ethernet. If not configured, the default address `192.168.0.8:502` is used.
To allow controlling charge start/stop, the Wallbe physical configuration must be modified. This requires opening the Wallbe and should only be done by professionals. Once opened, DIP 10 must be set to ON:
![dip10](docs/dip10.jpeg)
More information on interacting with Wallbe chargers can be found at [GoingElectric](https://www.goingelectric.de/forum/viewtopic.php?p=1212583). Use with care.
### Software
The preferred way of running EVCC is using the docker image:
@ -43,56 +60,52 @@ To run EVCC with given config file and UI on port 7070:
docker run -v $(pwd)/evcc.dist.yaml:/etc/evcc.yaml -p 7070:7070 andig/evcc-bundle
To build EVCC from source, [Go](2) 1.13 is required:
make
## Configuration
### Charge Modes
Three operation modes are supported:
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,
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.
### PV generator configuration
For both PV modes, 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
- **PV meter**: Configuring a *PV meter* is the simplest option. *PV meter* measures the PV generation. The charger is allowed to consume:
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
Charge Power = PV Meter Power - Residual Power
The *Residual Power* is a configurable assumption how much power remaining facilities beside the charger use.
The *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 kWh. The charger is then allowed to consume:
#### Grid meter
Δ Charge Power = Grid Meter Power - Residual Power
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. The charger is then allowed to consume:
rounded down and capped at zero and
Δ Charge Power = Grid Meter Power - Residual Power
Charge Power = Current Charge Power + Δ Charge Power
rounded down and capped at zero and
Charge Power = Current Charge Power + Δ Charge Power
In this setup, *residual power* is used margin to account for fluctuations in PV production that may be faster than EVCC's control loop.
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.
### Charger configuration
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* and preferably also *total energy*.
- **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* and preferably also *total energy*.
If *total energy* is supplied, it can be used to calculate the *charged energy* for the current charging cycle.
#### No charge meter
If not 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)
- **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).
## Implementation
@ -102,23 +115,22 @@ EVCC consists of four basic elements: *Charger*, *Meter*, *SoC* and *Loadpoint*.
Charger is reponsible for EV state handling:
- `Status()`
- `Enabled()`
- `Enable(enable bool)`
- `ActualCurrent()`
- `MaxCurrent(current int64)` (`ChargeController` only)
- `Status()`: get charge controller status (`A...F`)
- `Enabled()`: get charger availablity
- `Enable()`: set charger availability
- `MaxCurrent()`: set maximum allowed charge current in A
Available charger implementations are:
- `wallbe`: implements the interface to the Wallbe Eco chargers
- `configurable`: default charger implementation using configurable plugins for accessing device data
- `default`: default charger implementation using configurable plugins for integrating any type of charger
### Meter
Meters provide data about power and energy consumption:
- `CurrentPower()`
- `TotalEnergy()` (`MeterEnergy` only)
- `CurrentPower()`: power in W
- `TotalEnergy()`: energy in kWh (optional)
Meter has a single implementaton where meter readings- power and energy- can be configured to be delivered by plugin.
@ -126,13 +138,13 @@ Meter has a single implementaton where meter readings- power and energy- can be
SoC represents a specific EV battery. Configuring a SoC allows to define it's `Capacity (kWh)` and dynamically provide:
- `ChargeState()`
- `ChargeState()`: state of charge in %
If SoC is configured and assigned to the charger, charge status and remaining charge duration become available in the user interface.
### Loadpoint
Loadpoint controls the Charger behaviour according to the operations mode- off, now, pv + minimum or pv only.
Loadpoint controls the Charger behaviour according to the operations mode- *off*, *now*, *PV + minimum* or *PV only*.
## Plugins
@ -140,7 +152,7 @@ Plugins are used to implement accessing and updating generic data sources. EVCC
- `mqtt`: this plugin allows to read values from MQTT topics. This is particularly useful for meters, e.g. when meter data is already available on MQTT. See [MBMD](5) for an example how to get Modbus meter data into MQTT.
This plugin type is read-only and does not provide write access.
- `exec`: the exec plugin executes external scripts to read or update data. This plugin is useful to implement any type of external functionality.
- `script`: the script plugin executes external scripts to read or update data. This plugin is useful to implement any type of external functionality.
When using plugins for *write* access, the actual data is provided as variable in form of `${var[:format]}`. The variable is replaced with the actual data before the plugin is executed.

View file

@ -2,19 +2,19 @@ package api
import "time"
//go:generate mockgen -package mock -destination ../mock/mock.go github.com/andig/evcc/api Charger,ChargeController,Meter,MeterEnergy
//go:generate mockgen -package mock -destination ../mock/mock.go github.com/andig/evcc/api Charger,Meter,MeterEnergy
// Meter is able to provide current power at metering point
type Meter interface {
CurrentPower() (float64, error)
}
// ChargeMode are charge modes modeled after OpenWB
type ChargeMode string
// MeterEnergy is able to provide current power at metering point
type MeterEnergy interface {
TotalEnergy() (float64, error)
}
const (
ModeOff ChargeMode = "off"
ModeNow ChargeMode = "now"
ModeMinPV ChargeMode = "minpv"
ModePV ChargeMode = "pv"
)
// ChargeStatus is the EVSE models charging status from A to F
// ChargeStatus is the EV's charging status from A to F
type ChargeStatus string
const (
@ -27,17 +27,21 @@ const (
StatusF ChargeStatus = "F" // Fzg. angeschlossen: ja Laden möglich: nein
)
// Meter is able to provide current power in W
type Meter interface {
CurrentPower() (float64, error)
}
// MeterEnergy is able to provide current energy in kWh
type MeterEnergy interface {
TotalEnergy() (float64, error)
}
// Charger is able to provide current charging status and to enable/disabler charging
type Charger interface {
Status() (ChargeStatus, error)
Enabled() (bool, error)
Enable(enable bool) error
ActualCurrent() (int64, error)
// MaxCurrent(current int64) error
}
// ChargeController provides controlling of the charger's max allowed power
type ChargeController interface {
MaxCurrent(current int64) error
}
@ -46,21 +50,11 @@ type ChargeTimer interface {
ChargingTime() (time.Duration, error)
}
// ChargeRater provides charged energy amount
// ChargeRater provides charged energy amount in kWh
type ChargeRater interface {
ChargedEnergy() (float64, error)
}
// ChargeMode are charge modes modeled after OpenWB
type ChargeMode string
const (
ModeOff ChargeMode = "off"
ModeNow ChargeMode = "now"
ModeMinPV ChargeMode = "minpv"
ModePV ChargeMode = "pv"
)
// SoC represents the EV battery's state
type SoC interface {
ChargeState() (float64, error)

View file

@ -36,19 +36,6 @@
</head>
<body>
<div class="container" id="error">
<div class="toast" data-delay="2000" style="position: absolute; z-index: 2000; top: 1rem; right: 1rem;">
<div class="toast-header">
<strong class="mr-auto">Error</strong>
<small v-if="error.status">HTTP {{error.status}}</small>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">{{error.message}}</div>
</div>
</div>
<div id="app">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="https://github.com/andig/evcc"><i class="text-primary far fa-leaf mr-2"></i>evcc</a>
@ -71,6 +58,19 @@
<router-view></router-view>
</div>
<div id="error">
<div class="toast" data-delay="2000" style="position: absolute; top: 4rem; right: 0.5rem;">
<div class="toast-header">
<strong class="mr-auto"><i class="text-danger far fa-exclamation-triangle"></i> Error</strong>
<small v-if="error.status">HTTP {{error.status}}</small>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">{{error.message}}</div>
</div>
</div>
<script type="text/x-template" id="embed-template">
<div class="container mx-auto text-center">
<h1 class="display-4 pt-3" v-if="title">{{title}}</h1>
@ -80,7 +80,7 @@
<iframe v-bind:src="iframe" v-if="iframe">
</a>
<span v-else>
<img v-bind:src="img" v-if="img">
<img class="img-fluid" v-bind:src="img" v-if="img">
<iframe v-bind:src="iframe" v-if="iframe">
</span>
</div>
@ -148,11 +148,9 @@
</div>
<div class="card-body">
<h2 class="card-title pricing-card-title">
{{fmt(state.chargeCurrent)}} <small class="text-muted">A</small>
<span class="text-muted">/</span>
{{fmt(state.chargePower)}} <small class="text-muted">{{fmtUnit(state.chargePower)}}W</small>
</h2>
<p>Strom/Leistung <i class="text-primary far fa-leaf" v-if="state.chargeCurrent"></i></p>
<p>Ladeleistung <i class="text-primary far fa-leaf" v-if="state.chargePower"></i></p>
</div>
</div>
</div>

View file

@ -28,10 +28,9 @@ let store = {
charging: null,
gridPower: null,
pvPower: null,
chargeCurrent: null,
chargePower: null,
chargeDuration: null,
chargeEstimate: null,
chargeEstimate: -1,
chargedEnergy: null,
socCharge: "—"
},
@ -54,6 +53,17 @@ let store = {
}
};
//
// Heartbeat
//
window.setInterval(function() {
axios.get("health").catch(function(res) {
res.message = "Server unavailable";
error.raise(res)
});
}, 5000);
//
// Components
//
@ -62,7 +72,6 @@ const error = new Vue({
el: '#error',
data: {
error: {},
timeoutHandle: null,
},
methods: {
raise: function(error) {
@ -123,7 +132,7 @@ Vue.component("datapanel", {
},
fmtDuration: function(d) {
if (d < 0) {
return "&mdash;";
return '—';
}
var seconds = "0" + (d % 60);
var minutes = "0" + (Math.floor(d / 60) % 60);
@ -144,8 +153,13 @@ Vue.component("datapanel", {
window.setTimeout(self.connect, 1000);
};
ws.onmessage = function(evt) {
var msg = JSON.parse(evt.data);
store.update(msg);
try {
var msg = JSON.parse(evt.data);
store.update(msg);
}
catch (e) {
console.error(e, evt.data)
}
};
}
},

View file

@ -1,4 +1,4 @@
package core
package charger
import (
"github.com/andig/evcc/api"
@ -7,24 +7,24 @@ import (
// Charger is an api.Charger implementation with configurable getters and setters.
type Charger struct {
statusG provider.StringGetter
actualCurrentG provider.IntGetter
enabledG provider.BoolGetter
enableS provider.BoolSetter
statusG provider.StringGetter
enabledG provider.BoolGetter
enableS provider.BoolSetter
maxCurrentS provider.IntSetter
}
// NewCharger creates a new charger
func NewCharger(
statusG provider.StringGetter,
actualCurrentG provider.IntGetter,
enabledG provider.BoolGetter,
enableS provider.BoolSetter,
maxCurrentS provider.IntSetter,
) api.Charger {
return &Charger{
statusG: statusG,
actualCurrentG: actualCurrentG,
enabledG: enabledG,
enableS: enableS,
statusG: statusG,
enabledG: enabledG,
enableS: enableS,
maxCurrentS: maxCurrentS,
}
}
@ -38,11 +38,6 @@ func (m *Charger) Status() (api.ChargeStatus, error) {
return api.ChargeStatus(s), nil
}
// ActualCurrent implements the Charger.ActualCurrent interface
func (m *Charger) ActualCurrent() (int64, error) {
return m.actualCurrentG()
}
// Enabled implements the Charger.Enabled interface
func (m *Charger) Enabled() (bool, error) {
return m.enabledG()
@ -52,3 +47,8 @@ func (m *Charger) Enabled() (bool, error) {
func (m *Charger) Enable(enable bool) error {
return m.enableS(enable)
}
// MaxCurrent implements the Charger.MaxCurrent API
func (m *Charger) MaxCurrent(current int64) error {
return m.maxCurrentS(current)
}

View file

@ -1,7 +1,8 @@
package core
package charger
import (
"encoding/binary"
"fmt"
"time"
"github.com/andig/evcc/api"
@ -14,6 +15,7 @@ const (
regStatus = 100
regChargeTime = 102
regActualCurrent = 300
regEnable = 400
regOverchargeProtect = 409
regMaxCurrent = 528
@ -31,14 +33,17 @@ const (
// Upon setting a differnt, non-zero current, over overcurrent protection is
// disabled if current was equal 0A at this time.
type Wallbe struct {
log *api.Logger
client modbus.Client
handler *modbus.TCPClientHandler
overchargeProtect bool
log *api.Logger
client modbus.Client
handler *modbus.TCPClientHandler
}
// NewWallbe creates a Wallbe charger
func NewWallbe(conn string) api.Charger {
if conn == "" {
conn = "192.168.0.8:502"
}
handler := modbus.NewTCPClientHandler(conn)
client := modbus.NewClient(handler)
@ -52,22 +57,18 @@ func NewWallbe(conn string) api.Charger {
handler: handler,
}
// init overcharge value
if op, err := wb.overChargeEnabled(); err != nil {
wb.log.ERROR.Printf("init overcharge protect: %v", err)
} else {
wb.overchargeProtect = op
}
// wb.showIOs()
return wb
}
// showIOs logs all input/output register values and their configurations
func (wb *Wallbe) showIOs() {
// inputs
wb.showIO("LD", 520, 200)
wb.showIO("EN", 521, 201)
wb.showIO("ML", 522, 202)
wb.showIO("XR", 523, 203)
wb.showIO("LD", 520, 200) // 200 = EN?
wb.showIO("EN", 521, 201) // 201 = XR?
wb.showIO("ML", 522, 202) // 202 = LD?
wb.showIO("XR", 523, 203) // 203 = ML?
wb.showIO("IN", 524, 208)
// outputs
@ -83,21 +84,24 @@ func (wb *Wallbe) showIOs() {
}
}
// showIOs logs a single input/output register's values and their configurations
func (wb *Wallbe) showIO(input string, definition uint16, status uint16) {
var def uint16
var val byte
if b, err := wb.client.ReadHoldingRegisters(definition, 1); err != nil {
b, err := wb.client.ReadHoldingRegisters(definition, 1)
if err != nil {
wb.log.FATAL.Printf("%s definition %v", input, err)
} else {
def = binary.BigEndian.Uint16(b)
return
}
def = binary.BigEndian.Uint16(b)
if b, err := wb.client.ReadDiscreteInputs(status, 1); err != nil {
b, err = wb.client.ReadDiscreteInputs(status, 1)
if err != nil {
wb.log.FATAL.Printf("%s status %v", input, err)
} else {
val = b[0]
return
}
val = b[0]
wb.log.DEBUG.Printf("%s = %d (%d)", input, val, def)
}
@ -116,36 +120,8 @@ func (wb *Wallbe) Status() (api.ChargeStatus, error) {
// Enabled implements the Charger.Enabled interface
func (wb *Wallbe) Enabled() (bool, error) {
return true, nil
}
// Enable implements the Charger.Enable interface
func (wb *Wallbe) Enable(enable bool) error {
if !enable {
return wb.MaxCurrent(0) // set max current to zero
}
return nil
}
// ActualCurrent implements the Charger.ActualCurrent interface
func (wb *Wallbe) ActualCurrent() (int64, error) {
b, err := wb.client.ReadHoldingRegisters(regActualCurrent, 1)
wb.log.TRACE.Printf("read actual current (%d): %0 X", regActualCurrent, b)
if err != nil {
wb.handler.Close()
return 0, err
}
u := binary.BigEndian.Uint16(b)
return int64(u / 10), nil
}
// overChargeEnabled reads respective coil. Overcharge is only enabled
// when setting max current to 0 to disable the wallbox.
func (wb *Wallbe) overChargeEnabled() (bool, error) {
b, err := wb.client.ReadCoils(regOverchargeProtect, 1)
wb.log.TRACE.Printf("read overcharge protect (%d): %0 X", regOverchargeProtect, b)
b, err := wb.client.ReadCoils(regEnable, 1)
wb.log.TRACE.Printf("read charge enable (%d): %0 X", regEnable, b)
if err != nil {
wb.handler.Close()
return false, err
@ -154,30 +130,15 @@ func (wb *Wallbe) overChargeEnabled() (bool, error) {
return b[0] == 1, nil
}
// overChargeEnable sets overcharge coil
func (wb *Wallbe) overChargeEnable(enable bool) error {
// Enable implements the Charger.Enable interface
func (wb *Wallbe) Enable(enable bool) error {
var u uint16
if enable {
u = 0xFF00
}
b, err := wb.client.WriteSingleCoil(regOverchargeProtect, u)
wb.log.TRACE.Printf("write overcharge protect %d %0X: %0 X", regOverchargeProtect, u, b)
if err != nil {
wb.handler.Close()
} else {
wb.overchargeProtect = enable
}
return err
}
// maxCurrent sets max current
func (wb *Wallbe) maxCurrent(current int64) error {
u := uint16(current * 10)
b, err := wb.client.WriteSingleRegister(regMaxCurrent, u)
wb.log.TRACE.Printf("write max current %d %0X: %0 X", regMaxCurrent, u, b)
b, err := wb.client.WriteSingleCoil(regEnable, u)
wb.log.TRACE.Printf("write charge enable %d %0X: %0 X", regEnable, u, b)
if err != nil {
wb.handler.Close()
}
@ -186,19 +147,17 @@ func (wb *Wallbe) maxCurrent(current int64) error {
}
// MaxCurrent implements the Charger.MaxCurrent interface
// Setting current to 0 will always enable overcharge protection.
// If EV draw current in this state, the charger will go into error state E.
// In this state, charger will no longer notice car disconnecting (state A).
func (wb *Wallbe) MaxCurrent(current int64) error {
var err error
if current == 0 && !wb.overchargeProtect {
err = wb.overChargeEnable(true)
} else if current > 0 && wb.overchargeProtect {
err = wb.overChargeEnable(false)
if current < 6 {
return fmt.Errorf("invalid current %d", current)
}
if err == nil {
err = wb.maxCurrent(current)
u := uint16(current * 10)
b, err := wb.client.WriteSingleRegister(regMaxCurrent, u)
wb.log.TRACE.Printf("write max current %d %0X: %0 X", regMaxCurrent, u, b)
if err != nil {
wb.handler.Close()
}
return err

View file

@ -32,11 +32,12 @@ type mqttConfig struct {
}
type providerConfig struct {
Type string
Topic string
Cmd string
Timeout time.Duration
Cache time.Duration
Type string
Topic string
Cmd string
Multiplier float64
Timeout time.Duration
Cache time.Duration
}
type meterConfig struct {
@ -54,11 +55,10 @@ type chargerConfig struct {
URI string
// composite charger
Status *providerConfig // Charger
ActualCurrent *providerConfig // Charger
MaxCurrent *providerConfig // ChargeController
Enable *providerConfig // Charger
Enabled *providerConfig // Charger
Status *providerConfig // Charger
MaxCurrent *providerConfig // ChargeController
Enable *providerConfig // Charger
Enabled *providerConfig // Charger
}
type socConfig struct {
@ -80,6 +80,8 @@ type loadPointConfig struct {
Phases int64
MinCurrent int64
MaxCurrent int64
Steepness int64
GuardDuration time.Duration
Voltage float64
ResidualPower float64
}

View file

@ -49,37 +49,16 @@ func boolGetter(pc *providerConfig) (res provider.BoolGetter) {
return
}
func intProvider(pc *providerConfig) (res provider.IntGetter) {
switch pc.Type {
case "mqtt":
if pc.Timeout == 0 {
pc.Timeout = mqttTimeout
}
res = mq.IntGetter(pc.Topic, pc.Timeout)
case "script":
if pc.Timeout == 0 {
pc.Timeout = execTimeout
}
exec := provider.NewScriptProvider(pc.Timeout)
res = exec.IntGetter(pc.Cmd)
default:
log.FATAL.Printf("invalid provider type %s", pc.Type)
}
if pc.Cache > 0 {
res = provider.NewCacheGetter(res, pc.Cache).IntGetter
}
return
}
func floatGetter(pc *providerConfig) (res provider.FloatGetter) {
switch pc.Type {
case "mqtt":
if pc.Timeout == 0 {
pc.Timeout = mqttTimeout
}
res = mq.FloatGetter(pc.Topic, pc.Timeout)
if pc.Multiplier == 0 {
pc.Multiplier = 1
}
res = mq.FloatGetter(pc.Topic, pc.Multiplier, pc.Timeout)
case "script":
if pc.Timeout == 0 {
pc.Timeout = execTimeout

View file

@ -21,32 +21,22 @@ meters:
if err := viper.ReadConfig(bytes.NewBuffer([]byte(yaml))); err != nil {
t.Error(err)
}
// var conf Config
// if err := viper.UnmarshalExact(&conf); err != nil {
// t.Error(err)
// }
// _ = configureMeters(conf)
}
func TestChargerConfig(t *testing.T) {
yaml := `
chargers:
- name: test
type: configurable
status:
status:
type: script
cmd: script
actualCurrent:
enable:
type: script
cmd: script
enable:
enabled:
type: script
cmd: script
enabled:
type: script
cmd: script
maxCurrent:
maxCurrent:
type: script
cmd: script
- name: wallbe
@ -57,11 +47,4 @@ chargers:
if err := viper.ReadConfig(bytes.NewBuffer([]byte(yaml))); err != nil {
t.Error(err)
}
// var conf Config
// if err := viper.UnmarshalExact(&conf); err != nil {
// t.Error(err)
// }
// _ = configureChargers(conf)
}

View file

@ -1,13 +1,13 @@
package cmd
import (
"context"
"fmt"
"os"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/core"
"github.com/andig/evcc/push"
"github.com/andig/evcc/server"
"github.com/spf13/cobra"
@ -115,17 +115,10 @@ func Execute() {
}
func configureLogging(level string) {
// f, err := os.OpenFile("evcc.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
// if err != nil {
// log.FATAL.Fatal(err)
// }
api.OutThreshold = api.LogLevelToThreshold(level)
api.LogThreshold = api.OutThreshold
api.Loggers(func(name string, logger *api.Logger) {
logger.SetStdoutThreshold(api.OutThreshold)
// logger.SetLogThreshold(api.OutThreshold)
// logger.SetLogOutput(f)
})
}
@ -143,15 +136,6 @@ func checkVersion() {
}
}
// cycle executes loadpoint update and publishes all paramters
func cycle(lp *core.LoadPoint, tick time.Duration, updateChan chan core.Param) {
lp.Update()
ctx, cancel := context.WithTimeout(context.Background(), tick)
lp.Publish(ctx, updateChan)
cancel()
}
func run(cmd *cobra.Command, args []string) {
level, _ := cmd.PersistentFlags().GetString("log")
configureLogging(level)
@ -174,35 +158,42 @@ func run(cmd *cobra.Command, args []string) {
uri := viper.GetString("uri")
log.INFO.Println("listening at", uri)
loadPoints := loadConfig(conf)
// setup messaging
// if conf.Pushover.App != "" {
// po := server.NewMessenger(conf.Pushover.App, conf.Pushover.Recipients)
// po.Send("Wallbe", "Charge started", "Charging started in PV mode")
// }
notificationChan := make(chan push.Event, 1)
notificationHub := &push.Hub{}
if conf.Pushover.App != "" {
notificationHub.PushOver = push.NewMessenger(conf.Pushover.App, conf.Pushover.Recipients)
}
loadPoints := loadConfig(conf, notificationChan)
go notificationHub.Run(notificationChan)
// create webserver
hub := server.NewSocketHub()
httpd := server.NewHttpd(uri, conf.Menu, loadPoints[0], hub)
socketHub := server.NewSocketHub()
httpd := server.NewHttpd(uri, conf.Menu, loadPoints[0], socketHub)
// start broadcasting values
updateChan := make(chan core.Param)
go hub.Run(updateChan)
uiChan := make(chan core.Param)
triggerChan := make(chan struct{})
go socketHub.Run(uiChan, triggerChan)
// start all loadpoints
for _, lp := range loadPoints {
lp.Dump()
// update loop
go func(lp *core.LoadPoint) {
tick := conf.Interval
cycle(lp, tick, updateChan)
for range time.Tick(tick) {
cycle(lp, tick, updateChan)
}
}(lp)
lp.Prepare(uiChan, notificationChan)
go lp.Run(conf.Interval)
}
// handle UI update requests whenever browser connects
go func() {
for range triggerChan {
for _, lp := range loadPoints {
lp.Update()
}
}
}()
log.FATAL.Println(httpd.ListenAndServe())
}

View file

@ -6,9 +6,11 @@ import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/charger"
"github.com/andig/evcc/core"
"github.com/andig/evcc/core/wrapper"
"github.com/andig/evcc/provider"
"github.com/andig/evcc/push"
"github.com/spf13/viper"
)
@ -27,10 +29,9 @@ func clientID() string {
func configureMeters(conf config) (meters map[string]api.Meter) {
meters = make(map[string]api.Meter)
for _, mc := range conf.Meters {
m := core.NewMeter(
floatGetter(mc.Power),
)
m := core.NewMeter(floatGetter(mc.Power))
// decorate Meter with MeterEnergy
if mc.Energy != nil {
m = &wrapper.CompositeMeter{
Meter: m,
@ -49,25 +50,16 @@ func configureChargers(conf config) (chargers map[string]api.Charger) {
switch cc.Type {
case "wallbe":
c = core.NewWallbe(cc.URI)
c = charger.NewWallbe(cc.URI)
case "default", "configurable":
c = core.NewCharger(
c = charger.NewCharger(
stringGetter(cc.Status),
intProvider(cc.ActualCurrent),
boolGetter(cc.Enabled),
boolSetter("enable", cc.Enable),
intSetter("current", cc.MaxCurrent),
)
// if chargecontroller specified build composite charger
if cc.MaxCurrent != nil {
c = &wrapper.CompositeCharger{
Charger: c,
ChargeController: core.NewChargeController(
intSetter("current", cc.MaxCurrent),
),
}
}
default:
log.FATAL.Fatalf("invalid charger type '%s'", cc.Type)
}
@ -80,39 +72,46 @@ func configureChargers(conf config) (chargers map[string]api.Charger) {
func configureSoCs(conf config) (socs map[string]api.SoC) {
socs = make(map[string]api.SoC)
for _, sc := range conf.SoCs {
soc := core.NewSoC(
sc.Capacity,
sc.Title,
floatGetter(sc.Charge),
)
soc := core.NewSoC(sc.Capacity, sc.Title, floatGetter(sc.Charge))
socs[sc.Name] = soc
}
return
}
func configureLoadPoint(lp *core.LoadPoint, lpc loadPointConfig) {
// TODO rewrite based on https://github.com/spf13/viper/pull/863
func configureLoadPoint(lp *core.LoadPoint, lpc loadPointConfig, subv *viper.Viper) {
// for _, key := range []string{"charger", "gridmeter", "pvmeter", "chargemeter", "soc"} {
// delete(kv, key)
// }
// config := &mapstructure.DecoderConfig{
// WeaklyTypedInput: true,
// DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
// Result: lp,
// }
// decoder, err := mapstructure.NewDecoder(config)
// if err != nil {
// log.FATAL.Fatalf("configuring loadpoints failed: %v", err)
// }
// if err := decoder.Decode(kv); err != nil {
// log.FATAL.Fatalf("configuring loadpoints failed: %v", err)
// }
// we can ignore the error here as UnmarshalExact has been called before
_ = subv.UnmarshalExact(lp)
if lpc.Mode != "" {
lp.Synced().SetMode(api.ChargeMode(lpc.Mode))
}
if lpc.MinCurrent > 0 {
lp.MinCurrent = lpc.MinCurrent
}
if lpc.MaxCurrent > 0 {
lp.MaxCurrent = lpc.MaxCurrent
}
if lpc.Voltage > 0 {
lp.Voltage = lpc.Voltage
}
if lpc.Phases > 0 {
lp.Phases = lpc.Phases
}
if lpc.ResidualPower != 0 {
lp.ResidualPower = lpc.ResidualPower
// workaround for golangs yaml off=0 conversion
if lpc.Mode == "0" {
lpc.Mode = api.ModeOff
}
lp.Mode = lpc.Mode // don't use SetMode here as that will block on channel send
}
}
func loadConfig(conf config) (loadPoints []*core.LoadPoint) {
func loadConfig(conf config, eventsChan chan push.Event) (loadPoints []*core.LoadPoint) {
if viper.Get("mqtt") != nil {
mq = provider.NewMqttClient(conf.Mqtt.Broker, conf.Mqtt.User, conf.Mqtt.Password, clientID(), 1)
}
@ -121,15 +120,18 @@ func loadConfig(conf config) (loadPoints []*core.LoadPoint) {
chargers := configureChargers(conf)
socs := configureSoCs(conf)
for _, lpc := range conf.LoadPoints {
charger, ok := chargers[lpc.Charger]
if !ok {
for idx, lpc := range conf.LoadPoints {
// configure loadpoint
lp := core.NewLoadPoint()
subv := viper.SubSlice("loadpoints")[idx]
configureLoadPoint(lp, lpc, subv)
// assign charger
if charger, ok := chargers[lpc.Charger]; ok {
lp.Charger = charger
} else {
log.FATAL.Fatalf("invalid charger '%s'", lpc.Charger)
}
lp := core.NewLoadPoint(
lpc.Name,
charger,
)
// assign meters
for _, m := range []struct {
@ -158,12 +160,6 @@ func loadConfig(conf config) (loadPoints []*core.LoadPoint) {
}
}
// assign remaing config
configureLoadPoint(lp, lpc)
// add loadpoint helper objects
lp.Prepare()
loadPoints = append(loadPoints, lp)
}

View file

@ -1,24 +0,0 @@
package core
import (
"github.com/andig/evcc/api"
"github.com/andig/evcc/provider"
)
// ChargeController is an api.ChargeController implementation with
// configurable getters and setters.
type ChargeController struct {
maxCurrentS provider.IntSetter
}
// NewChargeController creates a new charge controller
func NewChargeController(maxCurrentS provider.IntSetter) api.ChargeController {
return &ChargeController{
maxCurrentS: maxCurrentS,
}
}
// MaxCurrent implements the ChargeController.MaxCurrent API
func (m *ChargeController) MaxCurrent(current int64) error {
return m.maxCurrentS(current)
}

View file

@ -3,39 +3,33 @@ package core
import (
"fmt"
"math"
"sync"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/core/wrapper"
"github.com/andig/evcc/push"
evbus "github.com/asaskevich/EventBus"
)
const (
evChargeCurrent = "ChargeCurrent" // update fakeChargeMeter
evChargePower = "ChargePower" // update chargeRater
evStartCharge = "StartCharge" // update chargeTimer
evStopCharge = "StopCharge" // update chargeTimer
var (
status = map[bool]string{false: "disable", true: "enable"}
presence = map[bool]string{false: "—", true: "✓"}
)
var status = map[bool]string{false: "disable", true: "enable"}
var presence = map[bool]string{false: "—", true: "✓"}
// powerToCurrent is a helper function to convert power to per-phase current
func powerToCurrent(power, voltage float64, phases int64) float64 {
return power / (float64(phases) * voltage)
}
// minInt calculates minimum of two integer values
func minInt(x, y int64) int64 {
if x < y {
return x
}
return y
func powerToCurrent(power, voltage float64, phases int64) int64 {
return int64(power / (float64(phases) * voltage))
}
// LoadPoint is responsible for controlling charge depending on
// SoC needs and power availability.
type LoadPoint struct {
bus evbus.Bus
sync.Mutex // guard status
bus evbus.Bus // event bus
triggerChan chan struct{} // API updates
notificationChan chan<- push.Event // notifications
uiChan chan<- Param // client push messages
Name string
Charger api.Charger
@ -49,110 +43,82 @@ type LoadPoint struct {
SoC api.SoC // SoC
// options
Steepness int64 // Step size of current change
Phases int64 // SOC phases. Required for converting power and current.
MinCurrent int64 // PV mode: start current Min+PV mode: min current
MaxCurrent int64 // Max allowed current. Physically ensured by the charge controller
Voltage float64 // Operating voltage. 230V for Germany.
ResidualPower float64 // PV meter only: household usage. Grid meter: household safety margin
state *State // state variables
// cached state
Mode api.ChargeMode // Charge mode, garded by mux
status api.ChargeStatus // Charger status
targetCurrent int64 // Allowed current. Between MinCurrent and MaxCurrent.
enabled bool // Charger enabled state
charging bool // Charging cycle
gridPower float64 // Grid power
pvPower float64 // PV power
chargePower float64 // Charging power
// contactor switch guard
guardUpdated time.Time // charger enabled/disabled timestamp
GuardDuration time.Duration // charger enable/disable minimum holding time
}
// NewLoadPoint creates a LoadPoint with sane defaults
func NewLoadPoint(name string, charger api.Charger) *LoadPoint {
state := &State{
mode: api.ModeNow,
status: api.StatusA,
targetCurrent: -1,
}
func NewLoadPoint() *LoadPoint {
return &LoadPoint{
bus: evbus.New(),
state: state,
Name: name,
Phases: 1,
Voltage: 230, // V
MinCurrent: 6, // A
MaxCurrent: 16, // A
Charger: charger,
bus: evbus.New(),
triggerChan: make(chan struct{}, 1),
Name: "Main",
Phases: 1,
Voltage: 230, // V
MinCurrent: 6, // A
MaxCurrent: 16, // A
Steepness: 1, // A
targetCurrent: 0, // A
GuardDuration: 10 * time.Minute,
}
}
// Synced returns the synchronized loadpoint state
func (lp *LoadPoint) Synced() *State {
return lp.state
}
func (lp *LoadPoint) chargeMeterPresent() bool {
_, isWrapped := lp.ChargeMeter.(*wrapper.ChargeMeter)
return !isWrapped
}
// Dump loadpoint configuration
func (lp *LoadPoint) Dump() {
soc := lp.SoC != nil
grid := lp.GridMeter != nil
pv := lp.PVMeter != nil
charge := lp.chargeMeterPresent()
log.INFO.Printf("%s config: soc %s grid %s pv %s charge %s",
lp.Name, presence[soc], presence[grid], presence[pv], presence[charge])
log.INFO.Printf("%s charge mode: %s", lp.Name, lp.state.Mode())
}
// Prepare loadpoint configuration by adding missing helper elements
func (lp *LoadPoint) Prepare() {
// ensure charge meter exists
if lp.ChargeMeter == nil {
m := &wrapper.ChargeMeter{
Phases: lp.Phases,
Voltage: lp.Voltage,
}
_ = lp.bus.Subscribe(evChargeCurrent, m.SetChargeCurrent)
_ = lp.bus.Subscribe(evStopCharge, func() {
m.SetChargeCurrent(0)
})
lp.ChargeMeter = m
}
// ensure charge meter can supply total energy
if lp.ChargeRater == nil {
rt := wrapper.NewChargeRater(lp.Name, lp.ChargeMeter)
_ = lp.bus.Subscribe(evChargePower, rt.SetChargePower)
_ = lp.bus.Subscribe(evStartCharge, rt.StartCharge)
_ = lp.bus.Subscribe(evStopCharge, rt.StopCharge)
lp.ChargeRater = rt
}
// ensure charge timer exists
if lp.ChargeTimer == nil {
if ct, ok := lp.Charger.(api.ChargeTimer); ok {
lp.ChargeTimer = ct
} else {
ct := wrapper.NewChargeTimer()
_ = lp.bus.Subscribe(evStartCharge, ct.StartCharge)
_ = lp.bus.Subscribe(evStopCharge, ct.StopCharge)
lp.ChargeTimer = ct
}
}
}
func (lp *LoadPoint) chargeMode(mode api.ChargeMode) {
log.INFO.Printf("%s set charge mode: %s", lp.Name, string(mode))
lp.state.SetMode(mode)
// connected returns the EVs connection state
func (lp *LoadPoint) connected() bool {
return lp.status == api.StatusB || lp.status == api.StatusC
}
// chargerEnable switches charging on or off. Minimum cycle duration is guaranteed.
func (lp *LoadPoint) chargerEnable(enable bool) error {
log.INFO.Printf("%s charger %s", lp.Name, status[enable])
return lp.Charger.Enable(enable)
if lp.targetCurrent != 0 && lp.targetCurrent != lp.MinCurrent {
log.FATAL.Fatal("charger enable/disable called without setting min current first")
}
if time.Since(lp.guardUpdated) < lp.GuardDuration {
log.TRACE.Printf("%s min cycle time violated - ignoring charger %s", lp.Name, status[enable])
return nil
}
err := lp.Charger.Enable(enable)
if err == nil {
lp.enabled = enable // cache
log.INFO.Printf("%s charger %s", lp.Name, status[enable])
lp.guardUpdated = time.Now()
if !enable {
lp.bus.Publish(evChargeCurrent, int64(0))
}
}
return err
}
// chargingCycle detects charge cycle start and stop events and manages the
// charge energy counter and charge timer. It guards against duplicate invocation.
func (lp *LoadPoint) chargingCycle(enable bool) {
if enable == lp.state.Charging() {
if enable == lp.charging {
return
}
lp.charging = enable
if enable {
log.INFO.Printf("%s start charging ->", lp.Name)
lp.bus.Publish(evStartCharge)
@ -160,211 +126,229 @@ func (lp *LoadPoint) chargingCycle(enable bool) {
log.INFO.Printf("%s stop charging <-", lp.Name)
lp.bus.Publish(evStopCharge)
}
lp.state.SetCharging(enable)
}
// updateChargerEnabled checks charger enabled state
func (lp *LoadPoint) updateChargerEnabled() bool {
enabled, err := lp.Charger.Enabled()
if err != nil {
log.ERROR.Printf("%s charger error: %v", lp.Name, err)
return false
}
log.TRACE.Printf("%s charger: %sd", lp.Name, status[enabled])
if !enabled {
// stop cycle if running
lp.chargingCycle(false)
lp.chargeMode(api.ModeOff)
}
return enabled
}
// updateChargeStatus updates car status and stops charging if car disconnected
func (lp *LoadPoint) updateChargeStatus() bool {
func (lp *LoadPoint) updateChargeStatus() api.ChargeStatus {
// abort if no vehicle connected
status, err := lp.Charger.Status()
if err != nil {
log.ERROR.Printf("%s charger error: %v", lp.Name, err)
return false
return api.StatusNone
}
log.TRACE.Printf("%s charger car status: %s", lp.Name, status)
log.TRACE.Printf("%s charger status: %s", lp.Name, status)
if status != lp.state.Status() {
lp.state.SetStatus(status)
if lp.Connected() {
if prevStatus := lp.status; status != prevStatus {
lp.status = status
// connected
if prevStatus == api.StatusA {
log.INFO.Printf("%s car connected (%s)", lp.Name, string(status))
} else {
}
// disconnected
if status == api.StatusA {
log.INFO.Printf("%s car disconnected", lp.Name)
lp.chargingCycle(false)
}
if lp.Charging() {
lp.chargingCycle(true)
}
// start/stop charging cycle
lp.chargingCycle(status == api.StatusC)
}
return lp.Connected()
}
// updateChargeCurrentAndPower retrieves chargers actual current and charge meter power.
// Charger actual current is published and may update the charge power.
// This mechanism ensures that fake charge meter and charge energy are updated.
func (lp *LoadPoint) updateChargeCurrentAndPower() (current int64, power float64, err error) {
current, err = lp.Charger.ActualCurrent()
if err == nil {
log.TRACE.Printf("%s charge current: %dA", lp.Name, current)
} else {
return 0, 0, fmt.Errorf("charger error: %v", err)
}
power, err = lp.ChargeMeter.CurrentPower()
if err == nil {
log.TRACE.Printf("%s charge power: %.0fW", lp.Name, power)
lp.state.SetChargePower(power)
// update charge rater with power
lp.bus.Publish(evChargePower, power)
} else {
return 0, 0, fmt.Errorf("charge meter error: %v", err)
}
return current, power, nil
return status
}
// setTargetCurrent guards setting current against changing to identical value
// and violating MaxCurrent
func (lp *LoadPoint) setTargetCurrent(chargeCurrent, targetChargeCurrent int64) error {
if targetChargeCurrent > lp.MaxCurrent {
targetChargeCurrent = lp.MaxCurrent
log.WARN.Printf("%s hard limit charge current: %dA", lp.Name, targetChargeCurrent)
func (lp *LoadPoint) setTargetCurrent(targetCurrentIn int64) error {
targetCurrent := clamp(targetCurrentIn, lp.MinCurrent, lp.MaxCurrent)
if targetCurrent != targetCurrentIn {
log.WARN.Printf("%s hard limit charge current: %dA", lp.Name, targetCurrent)
}
if lp.state.TargetCurrent() != targetChargeCurrent {
log.INFO.Printf("%s set max charge current: %dA", lp.Name, targetChargeCurrent)
if err := lp.Charger.(api.ChargeController).MaxCurrent(targetChargeCurrent); err != nil {
return fmt.Errorf("charge controller error: %v", err)
if lp.targetCurrent != targetCurrent {
log.DEBUG.Printf("%s set charge current: %dA", lp.Name, targetCurrent)
if err := lp.Charger.MaxCurrent(targetCurrent); err != nil {
return fmt.Errorf("%s charge controller error: %v", lp.Name, err)
}
lp.state.SetTargetCurrent(targetChargeCurrent)
lp.targetCurrent = targetCurrent // cache
}
// publish in case grid meter not available
if !lp.Charging() {
chargeCurrent = 0
}
lp.bus.Publish(evChargeCurrent, minInt(chargeCurrent, targetChargeCurrent))
lp.bus.Publish(evChargeCurrent, targetCurrent)
return nil
}
// Update reevaluates meters and charger state
func (lp *LoadPoint) Update() {
// check if charging is enabled && car connected
if enabled := lp.updateChargerEnabled(); !enabled {
return
// rampUpDown moves stepwise towards target current. If target current is reached
// during this process, true is returned otherwise false.
func (lp *LoadPoint) rampUpDown(target int64) (bool, error) {
current := lp.targetCurrent
if current == target {
return true, nil
}
if connected := lp.updateChargeStatus(); !connected {
var step int64
if current < target {
step = min(current+lp.Steepness, target)
} else if current > target {
step = max(current-lp.Steepness, target)
}
step = clamp(step, lp.MinCurrent, lp.MaxCurrent)
if err := lp.setTargetCurrent(step); err != nil {
return false, err
}
// end of ramp reached?
if step == target {
return true, nil
}
return false, nil
}
// rampOff ramps down charging current to minimum and then turns off
func (lp *LoadPoint) rampOff() error {
if lp.enabled {
finished, err := lp.rampUpDown(lp.MinCurrent)
if err != nil {
return err
}
if finished {
return lp.chargerEnable(false)
}
}
return nil
}
// rampUp ramps up charging current to maximum and then turns off
func (lp *LoadPoint) rampOn(target int64) error {
if !lp.enabled {
if err := lp.setTargetCurrent(lp.MinCurrent); err != nil {
return err
}
return lp.chargerEnable(true)
}
_, err := lp.rampUpDown(target)
return err
}
func (lp *LoadPoint) targetChargePower() float64 {
var targetChargePower float64
// use grid meter if available, pv meter else
if lp.GridMeter != nil {
// grid power must be negative for export
targetChargePower = lp.chargePower - lp.gridPower - lp.ResidualPower
log.DEBUG.Printf("%s target power: %.0fW = %.0fW charge - %.0fW grid - %.0fW residual", lp.Name, targetChargePower, lp.chargePower, lp.gridPower, lp.ResidualPower)
} else {
targetChargePower = math.Abs(lp.pvPower) - lp.ResidualPower
log.DEBUG.Printf("%s target power: %.0fW = %.0fW pv - %.0fW residual", lp.Name, targetChargePower, lp.pvPower, lp.ResidualPower)
}
return targetChargePower
}
// updateModePV sets "minpv" or "pv" load modes
func (lp *LoadPoint) updateModePV(mode api.ChargeMode) error {
targetChargePower := lp.targetChargePower()
// get max charge current
targetChargeCurrent := clamp(powerToCurrent(targetChargePower, lp.Voltage, lp.Phases), 0, lp.MaxCurrent)
if targetChargeCurrent < lp.MinCurrent {
switch mode {
case api.ModeMinPV:
targetChargeCurrent = lp.MinCurrent
case api.ModePV:
targetChargeCurrent = 0
}
}
log.TRACE.Printf("%s target charge current: %dA", lp.Name, targetChargeCurrent)
if targetChargeCurrent == 0 {
return lp.rampOff()
}
return lp.rampOn(targetChargeCurrent)
}
// updateMeter updates and publishes single meter
func (lp *LoadPoint) updateMeter(name string, meter api.Meter, power *float64) {
var err error
*power, err = meter.CurrentPower()
if err != nil {
log.ERROR.Printf("%s %v", lp.Name, err)
return
}
// abort if dumb charge controller
if _, chargeController := lp.Charger.(api.ChargeController); !chargeController {
log.ERROR.Printf("%s no charge controller assigned", lp.Name)
log.TRACE.Printf("%s %s power: %.1fW", lp.Name, name, *power)
lp.uiChan <- Param{Key: name + "Power", Val: *power}
}
// update is the main control function. It reevaluates meters and charger state
func (lp *LoadPoint) update() {
lp.updateChargeStatus()
lp.uiChan <- Param{Key: "mode", Val: string(lp.GetMode())}
lp.uiChan <- Param{Key: "connected", Val: lp.connected()}
lp.uiChan <- Param{Key: "charging", Val: lp.charging}
lp.updateMeter("grid", lp.GridMeter, &lp.gridPower)
lp.updateMeter("pv", lp.PVMeter, &lp.pvPower)
lp.updateMeter("charge", lp.ChargeMeter, &lp.chargePower)
// update ChargeRater
lp.bus.Publish(evChargePower, lp.chargePower)
// check if car connected and ready for charging
if !lp.connected() {
return
}
// execute loading strategy
var err error
mode := lp.state.Mode()
switch mode {
switch mode := lp.GetMode(); mode {
case api.ModeOff:
err = lp.rampOff()
case api.ModeNow:
err = lp.ApplyModeNow()
err = lp.rampOn(lp.MaxCurrent)
case api.ModeMinPV, api.ModePV:
err = lp.ApplyModePV(mode)
if lp.charging {
err = lp.updateModePV(mode)
}
}
if err != nil {
log.ERROR.Printf("%s error: %v", lp.Name, err)
log.ERROR.Println(err)
}
lp.uiChan <- Param{Key: "chargedEnergy", Val: 1e3 * lp.chargedEnergy()}
lp.uiChan <- Param{Key: "chargeDuration", Val: lp.chargeDuration()}
lp.publishSoC()
}
// ApplyModeNow updates charging current and sets maximum current
func (lp *LoadPoint) ApplyModeNow() error {
// get charger current
chargeCurrent, _, err := lp.updateChargeCurrentAndPower()
if err != nil {
return err
// Run is the loadpoint main control loop. It reacts to trigger events by
// updating measurements and executing control logic.
func (lp *LoadPoint) Run(interval time.Duration) {
ticker := time.NewTicker(interval)
lp.triggerChan <- struct{}{} // start immediately
for {
select {
case <-ticker.C:
lp.update()
case <-lp.triggerChan:
lp.update()
ticker.Stop()
ticker = time.NewTicker(interval)
}
}
// get max charge current
targetChargeCurrent := lp.MaxCurrent
log.DEBUG.Printf("%s target current: %dA (max)", lp.Name, targetChargeCurrent)
// set max charge current
return lp.setTargetCurrent(chargeCurrent, targetChargeCurrent)
}
// ApplyModePV sets "minpv" or "pv" load modes
func (lp *LoadPoint) ApplyModePV(mode api.ChargeMode) error {
// get charger current and charge meter power
chargeCurrent, chargePower, err := lp.updateChargeCurrentAndPower()
if err != nil {
return err
}
var targetChargePower float64
// use grid meter if available, pv meter else
if lp.GridMeter != nil {
gridPower, err := lp.GridMeter.CurrentPower()
if err != nil {
return fmt.Errorf("grid meter: %v", err)
}
log.DEBUG.Printf("%s grid power: %.0fW", lp.Name, gridPower)
// grid power must be negative for export
deltaChargePower := -gridPower - lp.ResidualPower
targetChargePower = chargePower + deltaChargePower
log.DEBUG.Printf("%s target power: %.0fW = %.0fW charge - %.0fW grid - %.0fW residual", lp.Name, targetChargePower, chargePower, gridPower, lp.ResidualPower)
} else {
pvPower, err := lp.PVMeter.CurrentPower()
if err != nil {
return fmt.Errorf("pv meter: %v", err)
}
log.DEBUG.Printf("%s pv power: %.0fW", lp.Name, pvPower)
pvPower = math.Abs(pvPower)
targetChargePower = pvPower - lp.ResidualPower
log.DEBUG.Printf("%s target power: %.0fW = %.0fW pv - %.0fW residual", lp.Name, targetChargePower, pvPower, lp.ResidualPower)
}
// get max charge current
targetChargePower = math.Max(targetChargePower, 0)
targetChargeCurrent := int64(powerToCurrent(targetChargePower, lp.Voltage, lp.Phases))
if tc := targetChargeCurrent; targetChargeCurrent < lp.MinCurrent {
switch mode {
case api.ModeMinPV:
// charger cannot go below 6A - clamp at min current
targetChargeCurrent = lp.MinCurrent
case api.ModePV:
// If charger is already using less than min current it is probably
// at the end of the charging cycle. Charge can continue without
// further limiting the current, otherwise stop charging.
if chargeCurrent > targetChargeCurrent {
// disable charger
targetChargeCurrent = 0
}
}
log.DEBUG.Printf("%s target current: %dA -> %dA override", lp.Name, tc, targetChargeCurrent)
} else {
log.DEBUG.Printf("%s target current: %dA", lp.Name, targetChargeCurrent)
}
// set max charge current
return lp.setTargetCurrent(chargeCurrent, targetChargeCurrent)
}

View file

@ -1,13 +1,10 @@
package core
import (
"context"
"errors"
"math"
"sync"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/core/wrapper"
)
// Param is the broadcast channel data type
@ -33,13 +30,13 @@ type Configuration struct {
// Configuration returns meter configuration
func (lp *LoadPoint) Configuration() Configuration {
c := Configuration{
Mode: string(lp.state.Mode()),
Mode: string(lp.GetMode()),
Phases: lp.Phases,
MinCurrent: lp.MinCurrent,
MaxCurrent: lp.MaxCurrent,
GridMeter: lp.GridMeter != nil,
PVMeter: lp.PVMeter != nil,
ChargeMeter: lp.chargeMeterPresent(),
ChargeMeter: lp.hasChargeMeter(),
}
if lp.SoC != nil {
@ -53,32 +50,54 @@ func (lp *LoadPoint) Configuration() Configuration {
return c
}
// ChargeMode updates charge mode
func (lp *LoadPoint) ChargeMode(mode api.ChargeMode) error {
// pv modes require GridMeter
if mode == api.ModeMinPV || mode == api.ModePV {
// check if charger is controllable
_, chargerControllable := lp.Charger.(api.ChargeController)
if (lp.PVMeter == nil && lp.GridMeter == nil) || !chargerControllable {
return errors.New("invalid charge mode: " + string(mode))
}
}
if lp.state.Mode() != mode {
// start cycle detection async from http call
go lp.chargingCycle(mode != api.ModeOff)
// update mode and enable/disable charger
lp.chargeMode(mode)
return lp.chargerEnable(mode != api.ModeOff)
}
return nil
func (lp *LoadPoint) hasChargeMeter() bool {
_, isWrapped := lp.ChargeMeter.(*wrapper.ChargeMeter)
return lp.ChargeMeter != nil && !isWrapped
}
// ChargeDuration returns for how long the charge cycle has been running
func (lp *LoadPoint) ChargeDuration() time.Duration {
// Dump loadpoint configuration
func (lp *LoadPoint) Dump() {
soc := lp.SoC != nil
grid := lp.GridMeter != nil
pv := lp.PVMeter != nil
log.INFO.Printf("%s config: soc %s grid %s pv %s charge %s", lp.Name,
presence[soc],
presence[grid],
presence[pv],
presence[lp.hasChargeMeter()],
)
log.INFO.Printf("%s charge mode: %s", lp.Name, lp.GetMode())
}
// Update triggers loadpoint to run main control loop and push messages to UI socket
func (lp *LoadPoint) Update() {
select {
case lp.triggerChan <- struct{}{}: // non-blocking send
default:
}
}
// GetMode returns loadpoint charge mode
func (lp *LoadPoint) GetMode() api.ChargeMode {
lp.Lock()
defer lp.Unlock()
return lp.Mode
}
// SetMode sets loadpoint charge mode
func (lp *LoadPoint) SetMode(mode api.ChargeMode) {
lp.Lock()
defer lp.Unlock()
log.INFO.Printf("%s set charge mode: %s", lp.Name, string(mode))
lp.Mode = mode
lp.Update()
}
// chargeDuration returns for how long the charge cycle has been running
func (lp *LoadPoint) chargeDuration() time.Duration {
d, err := lp.ChargeTimer.ChargingTime()
if err != nil {
log.ERROR.Printf("%s charge timer error: %v", lp.Name, err)
@ -86,8 +105,8 @@ func (lp *LoadPoint) ChargeDuration() time.Duration {
return d
}
// ChargedEnergy returns energy consumption since charge start in Wh
func (lp *LoadPoint) ChargedEnergy() float64 {
// chargedEnergy returns energy consumption since charge start in kWh
func (lp *LoadPoint) chargedEnergy() float64 {
f, err := lp.ChargeRater.ChargedEnergy()
if err != nil {
log.ERROR.Printf("%s charge rater error: %v", lp.Name, err)
@ -95,123 +114,39 @@ func (lp *LoadPoint) ChargedEnergy() float64 {
return f
}
// Connected returns the EVs connection state
func (lp *LoadPoint) Connected() bool {
return lp.state.Status() != api.StatusA
}
// Charging returns the EVs charging state
func (lp *LoadPoint) Charging() bool {
return lp.state.Status() == api.StatusC
}
// SoCChargeState returns the soc battery charge state
func (lp *LoadPoint) SoCChargeState() float64 {
if !lp.Connected() {
return math.NaN()
}
f, err := lp.SoC.ChargeState()
if err != nil {
log.ERROR.Printf("%s soc error: %v", lp.Name, err)
return math.NaN()
}
lp.state.SetSocCharge(f)
return f
}
// ChargeRemainingDuration returns the remaining charge time
func (lp *LoadPoint) ChargeRemainingDuration() time.Duration {
if !lp.state.Charging() {
// remainingChargeDuration returns the remaining charge time
func (lp *LoadPoint) remainingChargeDuration(chargePercent float64) time.Duration {
if !lp.charging {
return -1
}
if currentPower := lp.state.ChargePower(); currentPower > 0 {
if lp.chargePower > 0 {
if soc, ok := lp.SoC.(*SoC); ok {
whRemaining := (1 - lp.state.SocCharge()/100.0) * 1000.0 * float64(soc.Capacity)
return time.Duration(float64(time.Hour) * whRemaining / currentPower)
whRemaining := (1 - chargePercent/100.0) * 1e3 * float64(soc.Capacity)
return time.Duration(float64(time.Hour) * whRemaining / lp.chargePower)
}
}
return -1
}
func (lp *LoadPoint) publishMeter(name string, meter api.Meter, clientPush chan Param) {
if f, err := meter.CurrentPower(); err == nil {
key := name + "Power"
log.TRACE.Printf("%s %s power: %.1fW", lp.Name, name, f)
clientPush <- Param{Key: key, Val: f}
} else {
log.ERROR.Printf("%s %v", lp.Name, err)
// publish state of charge and remaining charge duration
func (lp *LoadPoint) publishSoC() {
if lp.SoC == nil {
return
}
}
func (lp *LoadPoint) publishCharging(clientPush chan Param) {
if lp.Charging() {
if f, err := lp.Charger.ActualCurrent(); err == nil {
log.TRACE.Printf("%s charge current: %dA", lp.Name, f)
clientPush <- Param{Key: "chargeCurrent", Val: f}
} else {
log.ERROR.Printf("%s update charger current failed: %v", lp.Name, err)
if lp.connected() {
f, err := lp.SoC.ChargeState()
if err == nil {
log.TRACE.Printf("%s soc charge: %.1f%%", lp.Name, f)
lp.uiChan <- Param{Key: "socCharge", Val: f}
lp.uiChan <- Param{Key: "chargeEstimate", Val: lp.remainingChargeDuration(f)}
return
}
} else {
clientPush <- Param{Key: "chargeCurrent", Val: 0.0}
log.ERROR.Printf("%s soc error: %v", lp.Name, err)
}
}
func (lp *LoadPoint) publishSoC(clientPush chan Param) {
f := lp.SoCChargeState()
if math.IsNaN(f) {
log.TRACE.Printf("%s soc charge: —", lp.Name)
clientPush <- Param{Key: "socCharge", Val: "—"}
} else {
log.TRACE.Printf("%s soc charge: %.1f%%", lp.Name, f)
clientPush <- Param{Key: "socCharge", Val: f}
}
clientPush <- Param{Key: "chargeEstimate", Val: lp.ChargeRemainingDuration()}
}
// Publish publishes loadpoint data to broadcast channel
func (lp *LoadPoint) Publish(ctx context.Context, clientPush chan Param) {
clientPush <- Param{Key: "mode", Val: string(lp.state.Mode())}
clientPush <- Param{Key: "connected", Val: lp.Connected()}
clientPush <- Param{Key: "charging", Val: lp.Charging()}
var wg sync.WaitGroup
for name, meter := range map[string]api.Meter{
"grid": lp.GridMeter,
"pv": lp.PVMeter,
"charge": lp.ChargeMeter,
} {
if meter != nil {
wg.Add(1)
go func(name string, meter api.Meter) {
lp.publishMeter(name, meter, clientPush)
wg.Done()
}(name, meter)
}
}
wg.Add(1)
go func() {
lp.publishCharging(clientPush)
wg.Done()
}()
if lp.SoC != nil {
wg.Add(1)
go func() {
lp.publishSoC(clientPush)
wg.Done()
}()
}
wg.Wait()
clientPush <- Param{Key: "chargedEnergy", Val: lp.ChargedEnergy()}
clientPush <- Param{Key: "chargeDuration", Val: lp.ChargeDuration()}
lp.uiChan <- Param{Key: "socCharge", Val: "—"}
lp.uiChan <- Param{Key: "chargeEstimate", Val: -1}
}

View file

@ -1,141 +0,0 @@
package core
import (
"testing"
"github.com/andig/evcc/api"
"github.com/andig/evcc/core/wrapper"
"github.com/andig/evcc/mock"
"github.com/golang/mock/gomock"
)
func TestChargerEnableNoChange(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := mock.NewMockCharger(ctrl)
c.EXPECT().
Enabled().
Return(false, nil)
lp := NewLoadPoint("lp1", c)
if err := lp.chargerEnable(false); err != nil {
t.Error(err)
}
}
func TestChargerEnableChange(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := mock.NewMockCharger(ctrl)
c.EXPECT().
Enabled().
Return(false, nil)
c.EXPECT().
Enable(true).
Return(nil)
lp := NewLoadPoint("lp1", c)
if err := lp.chargerEnable(true); err != nil {
t.Error(err)
}
}
func TestEVNotConnected(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := mock.NewMockCharger(ctrl)
c.EXPECT().
Status().
Return(api.StatusA, nil)
lp := NewLoadPoint("lp1", c)
if res := lp.updateChargeStatus(); res {
t.Error("ev not disconnected")
}
}
func TestChargerEnabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := mock.NewMockCharger(ctrl)
c.EXPECT().
Enabled().
Return(true, nil)
lp := NewLoadPoint("lp1", c)
if enabled := lp.updateChargerEnabled(); !enabled {
t.Error("charger not enabled")
}
}
func TestChargerDisabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := mock.NewMockCharger(ctrl)
c.EXPECT().
Enabled().
Return(false, nil)
lp := NewLoadPoint("lp1", c)
lp.state.SetMode(api.ModeNow)
if enabled := lp.updateChargerEnabled(); enabled {
t.Error("charger not disabled")
}
}
func TestEVConnected(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := mock.NewMockCharger(ctrl)
c.EXPECT().
Status().
Return(api.StatusC, nil)
lp := NewLoadPoint("lp1", c)
if res := lp.updateChargeStatus(); !res {
t.Error("ev not connected")
}
}
func TestStartCharging(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := mock.NewMockCharger(ctrl)
mm := mock.NewMockMeter(ctrl)
me := mock.NewMockMeterEnergy(ctrl)
me.EXPECT().
TotalEnergy().
Return(0.0, nil)
lp := NewLoadPoint("lp1", c)
lp.state.SetCharging(false)
lp.ChargeMeter = &wrapper.CompositeMeter{Meter: mm, MeterEnergy: me}
lp.chargingCycle(true)
}
func TestStopCharging(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := mock.NewMockCharger(ctrl)
mm := mock.NewMockMeter(ctrl)
me := mock.NewMockMeterEnergy(ctrl)
me.EXPECT().
TotalEnergy().
Return(10.0, nil)
lp := NewLoadPoint("lp1", c)
lp.state.SetCharging(true)
lp.ChargeMeter = &wrapper.CompositeMeter{Meter: mm, MeterEnergy: me}
lp.chargingCycle(false)
}

108
core/loadpoint_handler.go Normal file
View file

@ -0,0 +1,108 @@
package core
import (
"github.com/andig/evcc/api"
"github.com/andig/evcc/core/wrapper"
"github.com/andig/evcc/push"
)
const (
evcc = "evcc" // notification sender
evChargeCurrent = "ChargeCurrent" // update fakeChargeMeter
evChargePower = "ChargePower" // update chargeRater
evStartCharge = "StartCharge" // update chargeTimer
evStopCharge = "StopCharge" // update chargeTimer
)
// evChargeStartHandler sends external start event
func (lp *LoadPoint) evChargeStartHandler() {
lp.notificationChan <- push.Event{
EventId: push.ChargeStart,
Sender: evcc,
Attributes: map[string]interface{}{
"lp": lp.Name,
"mode": lp.GetMode(),
},
}
}
// evChargeStartHandler sends external stop event
func (lp *LoadPoint) evChargeStopHandler() {
energy, err := lp.ChargeRater.ChargedEnergy()
if err != nil {
log.ERROR.Printf("%s %v", lp.Name, err)
}
lp.notificationChan <- push.Event{
EventId: push.ChargeStop,
Sender: evcc,
Attributes: map[string]interface{}{
"lp": lp.Name,
"energy": energy,
},
}
}
// evChargeCurrentHandler updates proxy charge meter's charge current.
// If physical charge meter is present this handler is not used.
func (lp *LoadPoint) evChargeCurrentHandler(m *wrapper.ChargeMeter) func(para ...interface{}) {
return func(para ...interface{}) {
current := para[0].(int64)
if current > 0 && lp.status != api.StatusC {
current = 0
}
m.SetChargeCurrent(current)
}
}
// Prepare loadpoint configuration by adding missing helper elements
func (lp *LoadPoint) Prepare(uiChan chan<- Param, notificationChan chan<- push.Event) {
lp.notificationChan = notificationChan
lp.uiChan = uiChan
if lp.PVMeter == nil && lp.GridMeter == nil {
log.FATAL.Fatal("missing either PV or Grid meter - aborting")
}
// ensure charge meter exists
if lp.ChargeMeter == nil {
if mt, ok := lp.Charger.(api.Meter); ok {
lp.ChargeMeter = mt
} else {
mt := &wrapper.ChargeMeter{
Phases: lp.Phases,
Voltage: lp.Voltage,
}
_ = lp.bus.Subscribe(evChargeCurrent, lp.evChargeCurrentHandler(mt))
_ = lp.bus.Subscribe(evStopCharge, func() {
mt.SetChargeCurrent(0)
})
lp.ChargeMeter = mt
}
}
// ensure charge rater exists
if rt, ok := lp.Charger.(api.ChargeRater); ok {
lp.ChargeRater = rt
} else {
rt := wrapper.NewChargeRater(lp.Name, lp.ChargeMeter)
_ = lp.bus.Subscribe(evChargePower, rt.SetChargePower)
_ = lp.bus.Subscribe(evStartCharge, rt.StartCharge)
_ = lp.bus.Subscribe(evStopCharge, rt.StopCharge)
lp.ChargeRater = rt
}
// ensure charge timer exists
if ct, ok := lp.Charger.(api.ChargeTimer); ok {
lp.ChargeTimer = ct
} else {
ct := wrapper.NewChargeTimer()
_ = lp.bus.Subscribe(evStartCharge, ct.StartCharge)
_ = lp.bus.Subscribe(evStopCharge, ct.StopCharge)
lp.ChargeTimer = ct
}
// event handlers
_ = lp.bus.Subscribe(evStartCharge, lp.evChargeStartHandler)
_ = lp.bus.Subscribe(evStopCharge, lp.evChargeStopHandler)
}

View file

@ -1,113 +0,0 @@
package core
import (
"testing"
"github.com/andig/evcc/api"
"github.com/andig/evcc/core/wrapper"
"github.com/andig/evcc/mock"
"github.com/golang/mock/gomock"
)
type testCase struct {
Mode api.ChargeMode
MinCurrent, MaxCurrent int64
ActualCurrent int64
CurrentPower float64
ExpectedCurrent interface{}
}
func mockedLoadPoint(ctrl *gomock.Controller, tc testCase) *LoadPoint {
cr := mock.NewMockCharger(ctrl)
cr.EXPECT().
Status().
Return(api.StatusC, nil)
cr.EXPECT().
Enabled().
Return(true, nil)
cr.EXPECT().
ActualCurrent().
Return(tc.ActualCurrent, nil)
m := mock.NewMockMeter(ctrl)
m.EXPECT().
CurrentPower().
Return(tc.CurrentPower, nil)
cc := mock.NewMockChargeController(ctrl)
if expectedCurrent, ok := tc.ExpectedCurrent.(int64); ok {
cc.EXPECT().
MaxCurrent(gomock.Eq(expectedCurrent)).
Return(nil)
}
lp := NewLoadPoint("lp1", &wrapper.CompositeCharger{
Charger: cr,
ChargeController: cc,
})
lp.GridMeter = m
lp.state.SetMode(tc.Mode)
if tc.MinCurrent > 0 {
lp.MinCurrent = tc.MinCurrent
}
if tc.MaxCurrent > 0 {
lp.MaxCurrent = tc.MaxCurrent
}
return lp
}
func TestEVConnectedAndEnabledNowMode(t *testing.T) {
cases := []testCase{
testCase{api.ModeNow, 0, 0, 0, 0.0, 16},
testCase{api.ModeNow, 0, 32, 0, 0.0, 32},
testCase{api.ModeNow, 16, 32, 0, 0.0, 32},
}
for _, c := range cases {
ctrl := gomock.NewController(t)
lp := mockedLoadPoint(ctrl, c)
lp.Update()
ctrl.Finish()
}
}
func TestEVConnectedAndEnabledPMinVMode(t *testing.T) {
cases := []testCase{
testCase{api.ModeMinPV, 0, 0, 5, 0.0, nil},
testCase{api.ModeMinPV, 0, 0, 10, 1150.0, 5},
testCase{api.ModeMinPV, 0, 0, 5, -1150.0, 10},
testCase{api.ModeMinPV, 14, 0, 5, -1150.0, 14}, // 14A > 10A
}
for _, c := range cases {
ctrl := gomock.NewController(t)
lp := mockedLoadPoint(ctrl, c)
lp.Update()
ctrl.Finish()
}
}
func TestEVConnectedAndEnabledPVMode(t *testing.T) {
cases := []testCase{
testCase{api.ModePV, 0, 0, 5, 0.0, nil},
testCase{api.ModePV, 0, 0, 10, 1150.0, 5},
testCase{api.ModePV, 0, 0, 5, -1150.0, 10},
testCase{api.ModePV, 14, 0, 5, -1150.0, 0}, // 14A > 10A
}
for _, c := range cases {
ctrl := gomock.NewController(t)
lp := mockedLoadPoint(ctrl, c)
lp.Update()
ctrl.Finish()
}
}

28
core/math.go Normal file
View file

@ -0,0 +1,28 @@
package core
// min calculates minimum of two integer values
func min(x, y int64) int64 {
if x < y {
return x
}
return y
}
// max calculates maximum of two integer values
func max(x, y int64) int64 {
if x > y {
return x
}
return y
}
// clamp calculates minimum of two integer values
func clamp(x, min, max int64) int64 {
if x <= min {
return min
}
if x >= max {
return max
}
return x
}

View file

@ -21,3 +21,20 @@ func NewMeter(currentPowerG provider.FloatGetter) api.Meter {
func (m *Meter) CurrentPower() (float64, error) {
return m.currentPowerG()
}
// MeterEnergy is an api.MeterEnergy implementation with configurable getters and setters.
type MeterEnergy struct {
totalEnergyG provider.FloatGetter
}
// NewMeterEnergy creates a new charger
func NewMeterEnergy(totalEnergyG provider.FloatGetter) api.MeterEnergy {
return &MeterEnergy{
totalEnergyG: totalEnergyG,
}
}
// TotalEnergy implements the Meter.TotalEnergy interface
func (m *MeterEnergy) TotalEnergy() (float64, error) {
return m.totalEnergyG()
}

View file

@ -1,23 +0,0 @@
package core
import (
"github.com/andig/evcc/api"
"github.com/andig/evcc/provider"
)
// MeterEnergy is an api.MeterEnergy implementation with configurable getters and setters.
type MeterEnergy struct {
totalEnergyG provider.FloatGetter
}
// NewMeterEnergy creates a new charger
func NewMeterEnergy(totalEnergyG provider.FloatGetter) api.MeterEnergy {
return &MeterEnergy{
totalEnergyG: totalEnergyG,
}
}
// TotalEnergy implements the Meter.TotalEnergy interface
func (m *MeterEnergy) TotalEnergy() (float64, error) {
return m.totalEnergyG()
}

View file

@ -1,101 +0,0 @@
package core
import (
"sync"
"sync/atomic"
"github.com/andig/evcc/api"
)
// State provides synchronized access to guarded variables
// NOTE: State uses int32 instead of bool for mutex-free atomic bool access
// as demonstrated in https://github.com/tevino/abool/blob/master/bool.go
type State struct {
sync.RWMutex
mode api.ChargeMode // current charge mode
status api.ChargeStatus // current connection state
charging int32 // current charging state
targetCurrent int64 // previous target current
chargePower float64 // previous charge power
socCharge float64 // previous soc charge
}
// Mode synchronizes access to mode
func (s *State) Mode() api.ChargeMode {
s.RLock()
defer s.RUnlock()
return s.mode
}
// SetMode synchronizes access to mode
func (s *State) SetMode(mode api.ChargeMode) {
s.Lock()
defer s.Unlock()
s.mode = mode
}
// Status synchronizes access to status
func (s *State) Status() api.ChargeStatus {
s.Lock()
defer s.Unlock()
return s.status
}
// SetStatus synchronizes access to status
func (s *State) SetStatus(status api.ChargeStatus) {
s.Lock()
defer s.Unlock()
s.status = status
}
// Charging synchronizes access to charging
func (s *State) Charging() bool {
return atomic.LoadInt32(&s.charging) == 1
}
// SetCharging synchronizes access to charging
func (s *State) SetCharging(charging bool) {
if charging {
atomic.StoreInt32(&s.charging, 1)
} else {
atomic.StoreInt32(&s.charging, 0)
}
}
// TargetCurrent synchronizes access to targetCurrent
func (s *State) TargetCurrent() int64 {
return atomic.LoadInt64(&s.targetCurrent)
}
// SetTargetCurrent synchronizes access to targetCurrent
func (s *State) SetTargetCurrent(targetCurrent int64) {
atomic.StoreInt64(&s.targetCurrent, targetCurrent)
}
// ChargePower synchronizes access to chargePower
func (s *State) ChargePower() float64 {
s.RLock()
defer s.RUnlock()
return s.chargePower
}
// SetChargePower synchronizes access to chargePower
func (s *State) SetChargePower(chargePower float64) {
s.Lock()
defer s.Unlock()
s.chargePower = chargePower
}
// SocCharge synchronizes access to socCharge
func (s *State) SocCharge() float64 {
s.RLock()
defer s.RUnlock()
return s.socCharge
}
// SetSocCharge synchronizes access to socCharge
func (s *State) SetSocCharge(socCharge float64) {
s.Lock()
defer s.Unlock()
s.socCharge = socCharge
}

View file

@ -1,15 +0,0 @@
package core
import (
"testing"
"github.com/andig/evcc/api"
)
func TestWallbe(t *testing.T) {
var c api.Charger = NewWallbe("192.168.0.8:502")
if _, ok := c.(api.ChargeController); !ok {
t.Error("not a charge controller")
}
}

View file

@ -1,6 +1,16 @@
package wrapper
import "sync"
import (
"sync"
"github.com/andig/evcc/api"
)
// CompositeMeter decorates a Meter with MeterEnergy.
type CompositeMeter struct {
api.Meter
api.MeterEnergy
}
// ChargeMeter is a replacement for a physical charge meter.
// It uses the charger's actual or max current to calculate power consumption.

View file

@ -0,0 +1,36 @@
package wrapper
import (
"testing"
"github.com/golang/mock/gomock"
)
func TestProxyMeterSinglePhase(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
tc := []struct {
phases int64
voltage float64
current int64
power float64
}{
{1, 100, 10, 1000},
{3, 200, 1, 600},
}
for _, tc := range tc {
m := ChargeMeter{
Phases: tc.phases,
Voltage: tc.voltage,
}
m.SetChargeCurrent(tc.current)
if p, err := m.CurrentPower(); p != tc.power || err != nil {
t.Errorf("power: %.1f %v", p, err)
}
}
}

View file

@ -55,6 +55,8 @@ func (cr *ChargeRater) StartCharge() {
}
}
// StopCharge records meter stop energy. If meter does not supply TotalEnergy,
// stop time is recorded and accumulating energy though SetChargePower stopped.
func (cr *ChargeRater) StopCharge() {
cr.Lock()
defer cr.Unlock()
@ -72,7 +74,7 @@ func (cr *ChargeRater) StopCharge() {
}
}
// SetChargePower increments consumed energy by amount in Wh since last update
// SetChargePower increments consumed energy by amount in kWh since last update
func (cr *ChargeRater) SetChargePower(power float64) {
cr.Lock()
defer cr.Unlock()
@ -84,14 +86,15 @@ func (cr *ChargeRater) SetChargePower(power float64) {
// update energy amount if not provided by meter
if _, ok := cr.meter.(api.MeterEnergy); !ok {
log.TRACE.Printf("%s set charge power: %.0fW", cr.name, power)
// convert energy into Wh
cr.chargedEnergy += power * float64(cr.clck.Since(cr.start)) / float64(time.Hour)
// convert power to energy in kWh
cr.chargedEnergy += power / 1e3 * float64(cr.clck.Since(cr.start)) / float64(time.Hour)
// move timestamp
cr.start = cr.clck.Now()
}
}
// ChargedEnergy returns energy consumption since charge start in Wh
// ChargedEnergy implements the ChargeRater interface.
// It returns energy consumption since charge start in kWh.
func (cr *ChargeRater) ChargedEnergy() (float64, error) {
cr.Lock()
defer cr.Unlock()

View file

@ -34,7 +34,7 @@ func TestNoMeter(t *testing.T) {
f, err := cr.ChargedEnergy()
if f != 1 || err != nil {
t.Error(f, err)
t.Errorf("energy: %.1f %v", f, err)
}
}
func TestWrappedMeter(t *testing.T) {
@ -73,6 +73,6 @@ func TestWrappedMeter(t *testing.T) {
f, err := cr.ChargedEnergy()
if f != 1 || err != nil {
t.Error(f, err)
t.Errorf("energy: %.1f %v", f, err)
}
}

View file

@ -7,6 +7,7 @@ import (
"github.com/benbjohnson/clock"
)
// ChargeTimer measures charging time between start and stop events
type ChargeTimer struct {
sync.Mutex
clck clock.Clock
@ -24,6 +25,7 @@ func NewChargeTimer() *ChargeTimer {
}
}
// StartCharge signals charge timer start
func (m *ChargeTimer) StartCharge() {
m.Lock()
defer m.Unlock()
@ -33,6 +35,7 @@ func (m *ChargeTimer) StartCharge() {
m.duration = 0
}
// StopCharge signals charge timer stop
func (m *ChargeTimer) StopCharge() {
m.Lock()
defer m.Unlock()

View file

@ -1,17 +0,0 @@
package wrapper
import (
"github.com/andig/evcc/api"
)
// CompositeCharger combines Charger and ChargeController
type CompositeCharger struct {
api.Charger
api.ChargeController
}
// CompositeMeter combines Meter and MeterEnergy
type CompositeMeter struct {
api.Meter
api.MeterEnergy
}

BIN
docs/dip10.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

View file

@ -1,14 +1,16 @@
uri: 0.0.0.0:7070
interval: 10s
uri: 0.0.0.0:7070 # uri for ui
interval: 10s # control cycle interval
mqtt:
broker: nas.fritz.box:1883
broker: localhost:1883 # mqtt broker address
# user:
# password:
meters:
- name: netz
power:
type: mqtt
topic: mbmd/sdm1-1/Power
- name: grid
power: # power reading
type: mqtt # use mqtt
topic: mbmd/sdm1-1/Power # mqtt topic
timeout: 10s # don't use older values
- name: pv
power:
@ -17,18 +19,22 @@ meters:
timeout: 10s # don't use older values
- name: charge
power:
type: script
cmd: /bin/sh -c "echo 0"
type: script # use script
cmd: /bin/sh -c "echo 0" # actual command
timeout: 3s # kill script after 3 seconds
chargers:
- name: wallbe
type: wallbe
uri: 192.168.0.8:502
type: wallbe # Wallbe charger using Phoenix Contact controller
uri: 192.168.0.8:502 # ModBus address
loadpoints:
- name: lp1
charger: wallbe
gridmeter: netz
pvmeter: pv
chargemeter: charge
- name: lp1 # name for logging
steepness: 1 # raise/lower charge current in 1A steps
guardduration: 10m # switch charger contactor not more often than this (default 10m)
maxcurrent: 16 # maximum charge current (default 16A)
phases: 3 # ev phases (default 3)
charger: wallbe # charger
gridmeter: grid # grid meter
pvmeter: pv # pv meter
chargemeter: charge # charge meter

15
go.mod
View file

@ -6,7 +6,8 @@ require (
github.com/asaskevich/EventBus v0.0.0-20180315140547-d46933a94f05
github.com/benbjohnson/clock v1.0.0
github.com/eclipse/paho.mqtt.golang v1.2.0
github.com/golang/mock v1.3.1
github.com/fsnotify/fsnotify v1.4.8 // indirect
github.com/golang/mock v1.4.1
github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/gorilla/handlers v1.4.2
@ -17,7 +18,9 @@ require (
github.com/grid-x/serial v0.0.0-20191104121038-e24bc9bf6f08 // indirect
github.com/hashicorp/go-version v1.2.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mjibson/esc v0.2.0
github.com/pelletier/go-toml v1.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v0.0.6
@ -25,9 +28,11 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.2
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 // indirect
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c // indirect
golang.org/x/text v0.3.2 // indirect
gopkg.in/ini.v1 v1.52.0 // indirect
golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect
golang.org/x/tools v0.0.0-20200311090712-aafaee8bce8c
gopkg.in/ini.v1 v1.54.0 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
)
replace github.com/spf13/viper => github.com/andig/viper v1.6.3-0.20200308172723-deb8393798ec

203
go.sum
View file

@ -1,33 +1,57 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/andig/viper v1.6.3-0.20200308172723-deb8393798ec h1:wjG6C1Qnz3GhKcQLXHfdv0Fjxu/MMDLoi+m7U6V+W20=
github.com/andig/viper v1.6.3-0.20200308172723-deb8393798ec/go.mod h1:Q7UGn6tgGeGEAvsBAqoMHp9BO+bbyNHmt8hj1bqhCrQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/EventBus v0.0.0-20180315140547-d46933a94f05 h1:Shem5lRG4gJyrrg9YMIl7dOQazyWCq0Daz4LjompZ28=
github.com/asaskevich/EventBus v0.0.0-20180315140547-d46933a94f05/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=
github.com/benbjohnson/clock v1.0.0 h1:78Jk/r6m4wCi6sndMpty7A//t4dw/RW5fV4ZgDVfX1w=
github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0=
github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.8 h1:vmjGRBE6skJRlqYIcSsh9B823pS/MqsvxevqbMnBPbI=
github.com/fsnotify/fsnotify v1.4.8/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
@ -37,16 +61,27 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.1 h1:ocYkMQY5RrXTYgXl7ICpV0IXwlEQGwKIsery4gyXa1U=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
@ -66,13 +101,34 @@ github.com/grid-x/serial v0.0.0-20191104121038-e24bc9bf6f08/go.mod h1:kdOd86/VGF
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
@ -87,23 +143,40 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mjibson/esc v0.2.0 h1:k96hdaR9Z+nMcnDwNrOvhdBqtjyMrbVyxLpsRCdP2mA=
github.com/mjibson/esc v0.2.0/go.mod h1:9Hw9gxxfHulMF5OJKCyhYD7PzlSdhzXyaGEBRPH1OPs=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@ -114,7 +187,10 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@ -141,74 +217,157 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E=
github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2oeKxpIUmtiDV5sn71VgeQgg6vcE7k=
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ=
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262 h1:qsl9y/CJx34tuA7QCPNp86JNJe4spst6Ff8MjvPUdPg=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200311090712-aafaee8bce8c h1:9WR4YuzLDuQMqEmLQrG0DiMmE2/HvX1dlrujzjmNVFg=
golang.org/x/tools v0.0.0-20200311090712-aafaee8bce8c/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.52.0 h1:j+Lt/M1oPPejkniCg1TkWE2J3Eh1oZTsHSXzMTzUXn4=
gopkg.in/ini.v1 v1.52.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.54.0 h1:oM5ElzbIi7gwLnNbPX2M25ED1vSAK3B6dex50eS/6Fs=
gopkg.in/ini.v1 v1.54.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -218,3 +377,9 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View file

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/andig/evcc/api (interfaces: Charger,ChargeController,Meter,MeterEnergy)
// Source: github.com/andig/evcc/api (interfaces: Charger,Meter,MeterEnergy)
// Package mock is a generated GoMock package.
package mock
@ -33,21 +33,6 @@ func (m *MockCharger) EXPECT() *MockChargerMockRecorder {
return m.recorder
}
// ActualCurrent mocks base method
func (m *MockCharger) ActualCurrent() (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ActualCurrent")
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ActualCurrent indicates an expected call of ActualCurrent
func (mr *MockChargerMockRecorder) ActualCurrent() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActualCurrent", reflect.TypeOf((*MockCharger)(nil).ActualCurrent))
}
// Enable mocks base method
func (m *MockCharger) Enable(arg0 bool) error {
m.ctrl.T.Helper()
@ -77,6 +62,20 @@ func (mr *MockChargerMockRecorder) Enabled() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enabled", reflect.TypeOf((*MockCharger)(nil).Enabled))
}
// MaxCurrent mocks base method
func (m *MockCharger) MaxCurrent(arg0 int64) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MaxCurrent", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// MaxCurrent indicates an expected call of MaxCurrent
func (mr *MockChargerMockRecorder) MaxCurrent(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxCurrent", reflect.TypeOf((*MockCharger)(nil).MaxCurrent), arg0)
}
// Status mocks base method
func (m *MockCharger) Status() (api.ChargeStatus, error) {
m.ctrl.T.Helper()
@ -92,43 +91,6 @@ func (mr *MockChargerMockRecorder) Status() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockCharger)(nil).Status))
}
// MockChargeController is a mock of ChargeController interface
type MockChargeController struct {
ctrl *gomock.Controller
recorder *MockChargeControllerMockRecorder
}
// MockChargeControllerMockRecorder is the mock recorder for MockChargeController
type MockChargeControllerMockRecorder struct {
mock *MockChargeController
}
// NewMockChargeController creates a new mock instance
func NewMockChargeController(ctrl *gomock.Controller) *MockChargeController {
mock := &MockChargeController{ctrl: ctrl}
mock.recorder = &MockChargeControllerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockChargeController) EXPECT() *MockChargeControllerMockRecorder {
return m.recorder
}
// MaxCurrent mocks base method
func (m *MockChargeController) MaxCurrent(arg0 int64) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MaxCurrent", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// MaxCurrent indicates an expected call of MaxCurrent
func (mr *MockChargeControllerMockRecorder) MaxCurrent(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxCurrent", reflect.TypeOf((*MockChargeController)(nil).MaxCurrent), arg0)
}
// MockMeter is a mock of Meter interface
type MockMeter struct {
ctrl *gomock.Controller

View file

@ -14,6 +14,10 @@ import (
var log = api.NewLogger("exec")
func truish(s string) bool {
return s == "1" || strings.ToLower(s) == "true" || strings.ToLower(s) == "on"
}
// Script implements shell script-based providers and setters
type Script struct {
timeout time.Duration

View file

@ -7,10 +7,6 @@ import (
"strings"
)
func truish(s string) bool {
return s == "1" || strings.ToLower(s) == "true" || strings.ToLower(s) == "on"
}
var re = regexp.MustCompile(`\${(\w+)(:([a-zA-Z0-9%.]+))?}`)
// replaceFormatted replaces all occurrances of ${key} with val from the kv map.

View file

@ -100,10 +100,11 @@ func (m *MqttClient) listen(topic string, callback func(string)) {
}
// FloatGetter creates handler for float64 from MQTT topic that returns cached value
func (m *MqttClient) FloatGetter(topic string, timeout time.Duration) FloatGetter {
func (m *MqttClient) FloatGetter(topic string, multiplier float64, timeout time.Duration) FloatGetter {
h := &msgHandler{
topic: topic,
timeout: timeout,
topic: topic,
multiplier: multiplier,
timeout: timeout,
}
m.Listen(topic, h.Receive)
@ -133,11 +134,12 @@ func (m *MqttClient) WaitForToken(token mqtt.Token) {
}
type msgHandler struct {
mux sync.Mutex
updated time.Time
timeout time.Duration
topic string
payload string
mux sync.Mutex
updated time.Time
timeout time.Duration
multiplier float64
topic string
payload string
}
func (h *msgHandler) Receive(payload string) {
@ -163,7 +165,7 @@ func (h *msgHandler) floatGetter() (float64, error) {
return 0, fmt.Errorf("%s invalid: '%s'", h.topic, h.payload)
}
return val, nil
return h.multiplier * val, nil
}
func (h *msgHandler) intGetter() (int64, error) {

62
push/event.go Normal file
View file

@ -0,0 +1,62 @@
package push
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/andig/evcc/api"
)
var log = api.NewLogger("push")
type EventId int
const (
ChargeStart EventId = iota
ChargeStop
)
type Event struct {
EventId EventId
Sender string
Attributes map[string]interface{}
}
func (e Event) Apply(template string) (string, error) {
return replaceFormatted(template, e.Attributes)
}
var re = regexp.MustCompile(`\${(\w+)(:([a-zA-Z0-9%.]+))?}`)
// replaceFormatted replaces all occurrances of ${key} with val from the kv map.
// All keys of kv must exist inside the string to apply replacements to
func replaceFormatted(s string, kv map[string]interface{}) (string, error) {
matches := re.FindAllStringSubmatch(s, -1)
for len(matches) > 0 {
for _, m := range matches {
key := m[1]
val, ok := kv[key]
if !ok {
return "", errors.New("could not find match for " + m[0])
}
// apply format
format := m[3]
if format != "" {
val = fmt.Sprintf(format, val)
}
// update string
literalMatch := m[0]
s = strings.ReplaceAll(s, literalMatch, fmt.Sprintf("%v", val))
}
// update matches
matches = re.FindAllStringSubmatch(s, -1)
}
return s, nil
}

34
push/push.go Normal file
View file

@ -0,0 +1,34 @@
package push
var definitions = map[EventId]struct{ title, msg string }{
ChargeStart: {
title: "Charge started",
msg: "Loadpoint ${lp} started charging in \"${mode}\" mode",
},
ChargeStop: {
title: "Charge finished",
msg: "Loadpoint ${lp} finished charging. Charged ${energy:%.1f}kWh in ${duration}.",
},
}
// Hub subscribes to event notifications and sends them to client devices
type Hub struct {
PushOver *PushOver
}
// Run is the Hub's main publishing loop
func (h *Hub) Run(events <-chan Event) {
for ev := range events {
if h.PushOver == nil {
continue
}
definition, ok := definitions[ev.EventId]
if !ok {
log.ERROR.Printf("invalid event %v", ev.EventId)
break
}
go h.PushOver.Send(ev, definition.title, definition.msg)
}
}

View file

@ -1,6 +1,8 @@
package server
package push
import "github.com/gregdel/pushover"
import (
"github.com/gregdel/pushover"
)
// PushOver implements the pushover messenger
type PushOver struct {
@ -10,6 +12,10 @@ type PushOver struct {
// NewMessenger creates new pushover messenger
func NewMessenger(app string, recipients []string) *PushOver {
if app == "" {
app = "evcc"
}
po := &PushOver{
app: pushover.New(app),
}
@ -22,9 +28,14 @@ func NewMessenger(app string, recipients []string) *PushOver {
}
// Send sends to all receivers
func (po *PushOver) Send(sender, msg, title string) {
func (po *PushOver) Send(event Event, title, msg string) {
msg, err := event.Apply(msg)
if err != nil {
log.ERROR.Printf("invalid message template: %v", err)
}
message := pushover.NewMessageWithTitle(msg, title)
message.DeviceName = sender
message.DeviceName = event.Sender
for _, recipient := range po.recipients {
go func(recipient *pushover.Recipient) {

View file

@ -3292,92 +3292,94 @@ XrGOak570AInS3ILAAD//1fqgUqwAQAA
"/index.html": {
name: "index.html",
local: "../assets/index.html",
size: 12615,
size: 12513,
modtime: 1566640112,
compressed: `
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
of9vE/8KAAD//zxQK95HMQAA
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
//8ATA4i4TAAAA==
`,
},
"/js/app.js": {
name: "app.js",
local: "../assets/js/app.js",
size: 4506,
size: 4719,
modtime: 1566640112,
compressed: `
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
TwAAAP//+fCcS28SAAA=
`,
},

View file

@ -19,6 +19,7 @@ const (
liveAssets = false
)
// MenuConfig is used to inject the menu configuration into the UI
type MenuConfig struct {
Title string
Subtitle string
@ -27,10 +28,6 @@ type MenuConfig struct {
Link string
}
type errorModeJSON struct {
Error error `json:"error"`
}
type chargeModeJSON struct {
Mode string `json:"mode"`
}
@ -41,10 +38,11 @@ type route struct {
HandlerFunc http.HandlerFunc
}
// loadPoint is the well-definied minimal interface for accessing loadpoint methods
type loadPoint interface {
GetMode() api.ChargeMode
SetMode(api.ChargeMode)
Configuration() core.Configuration
Synced() *core.State
ChargeMode(api.ChargeMode) error
}
// routeLogger traces matched routes including their executing time
@ -96,10 +94,22 @@ func jsonHandler(h http.Handler) http.Handler {
})
}
// HealthHandler returns current charge mode
func HealthHandler(lp loadPoint) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res := struct{ OK bool }{OK: true}
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(res); err != nil {
log.ERROR.Printf("httpd: failed to encode JSON: %v", err)
}
}
}
// ConfigHandler returns current charge mode
func ConfigHandler(lp loadPoint) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res := lp.(*core.LoadPoint).Configuration()
res := lp.Configuration()
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(res); err != nil {
@ -112,7 +122,7 @@ func ConfigHandler(lp loadPoint) http.HandlerFunc {
func CurrentChargeModeHandler(lp loadPoint) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res := chargeModeJSON{
Mode: string(lp.Synced().Mode()),
Mode: string(lp.GetMode()),
}
w.WriteHeader(http.StatusOK)
@ -133,21 +143,10 @@ func ChargeModeHandler(lp loadPoint) http.HandlerFunc {
return
}
if err := lp.ChargeMode(api.ChargeMode(mode)); err != nil {
log.ERROR.Printf("httpd: failed to set charge mode: %v", err)
w.WriteHeader(http.StatusBadRequest)
res := errorModeJSON{
Error: err,
}
if err := json.NewEncoder(w).Encode(res); err != nil {
log.ERROR.Printf("httpd: failed to encode JSON: %v", err)
}
return
}
lp.SetMode(api.ChargeMode(mode))
res := chargeModeJSON{
Mode: string(lp.Synced().Mode()),
Mode: string(lp.GetMode()),
}
w.WriteHeader(http.StatusOK)
@ -165,8 +164,13 @@ func SocketHandler(hub *SocketHub) http.HandlerFunc {
}
// NewHttpd creates HTTP server with configured routes for loadpoint
func NewHttpd(url string, links []MenuConfig, lp *core.LoadPoint, hub *SocketHub) *http.Server {
func NewHttpd(url string, links []MenuConfig, lp loadPoint, hub *SocketHub) *http.Server {
var routes = []route{
route{
[]string{"GET"},
"/health",
HealthHandler(lp),
},
route{
[]string{"GET"},
"/config",

View file

@ -86,20 +86,18 @@ func NewSocketHub() *SocketHub {
}
func (h *SocketHub) encode(v core.Param) ([]byte, error) {
var s string
switch val := v.Val.(type) {
case string:
s := fmt.Sprintf("{\"%s\": \"%s\"}", v.Key, val)
return []byte(s), nil
s = fmt.Sprintf("{\"%s\": \"%s\"}", v.Key, val)
case float64:
s := fmt.Sprintf("{\"%s\": %.3f}", v.Key, val)
return []byte(s), nil
s = fmt.Sprintf("{\"%s\": %.3f}", v.Key, val)
case time.Duration:
s := fmt.Sprintf("{\"%s\": %d}", v.Key, int64(val.Seconds()))
return []byte(s), nil
s = fmt.Sprintf("{\"%s\": %d}", v.Key, int64(val.Seconds()))
default:
s := fmt.Sprintf("{\"%s\": %v}", v.Key, val)
return []byte(s), nil
s = fmt.Sprintf("{\"%s\": %v}", v.Key, val)
}
return []byte(s), nil
}
func (h *SocketHub) broadcast(i core.Param) {
@ -121,21 +119,22 @@ func (h *SocketHub) broadcast(i core.Param) {
}
// Run starts data and status distribution
func (h *SocketHub) Run(in <-chan core.Param) {
func (h *SocketHub) Run(in <-chan core.Param, triggerChan chan<- struct{}) {
for {
select {
case client := <-h.register:
h.clients[client] = true
triggerChan <- struct{}{} // trigger loadpoint update
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case obj, ok := <-in:
case msg, ok := <-in:
if !ok {
return // break if channel closed
}
h.broadcast(obj)
h.broadcast(msg)
}
}
}

View file

@ -32,9 +32,7 @@ COPY --from=builder /etc/passwd /etc/passwd
# Copy our static executable.
COPY --from=builder /build/evcc /usr/local/bin/evcc
# Use an unprivileged user.
USER appuser
# UI and API port
EXPOSE 7070
# Run the binary

9
tools.go Normal file
View file

@ -0,0 +1,9 @@
// +build tools
package main
import (
_ "github.com/golang/mock/mockgen"
_ "github.com/mjibson/esc"
_ "golang.org/x/tools/cmd/stringer"
)