Refactor to support Wallbe dip 10 enable/disable
This commit is contained in:
parent
3c503b333d
commit
da74ce17fd
48 changed files with 1274 additions and 1393 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -13,3 +13,5 @@ audi.*
|
|||
credentials.json
|
||||
token.json
|
||||
assets/js/debug.js
|
||||
evcc
|
||||
__debug_bin
|
||||
|
|
|
|||
|
|
@ -2,3 +2,5 @@ issues:
|
|||
exclude:
|
||||
- "func `..Wallbe..showIO` is unused"
|
||||
- "func `..Wallbe..showIOs` is unused"
|
||||
- .regOverchargeProtect. is unused
|
||||
- .regActualCurrent. is unused
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
3
Makefile
3
Makefile
|
|
@ -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
|
||||
|
|
|
|||
94
README.md
94
README.md
|
|
@ -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
|
||||
|
||||

|
||||
|
||||
**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:
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
48
api/api.go
48
api/api.go
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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">×</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">×</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>
|
||||
|
|
|
|||
|
|
@ -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 "—";
|
||||
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)
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
63
cmd/root.go
63
cmd/root.go
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
100
cmd/setup.go
100
cmd/setup.go
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
108
core/loadpoint_handler.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
28
core/math.go
Normal 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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
101
core/state.go
101
core/state.go
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
36
core/wrapper/chargemeter_test.go
Normal file
36
core/wrapper/chargemeter_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
BIN
docs/dip10.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 230 KiB |
|
|
@ -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
15
go.mod
|
|
@ -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
203
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
68
mock/mock.go
68
mock/mock.go
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
62
push/event.go
Normal 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
34
push/push.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
154
server/assets.go
154
server/assets.go
|
|
@ -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=
|
||||
`,
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
9
tools.go
Normal 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"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue