Refactor loadpoint configuration (#46)
This commit is contained in:
parent
bcebf03cdb
commit
62ffaa59a1
12 changed files with 263 additions and 250 deletions
|
|
@ -5,7 +5,7 @@ import (
|
|||
)
|
||||
|
||||
// DecodeOther uses mapstructure to decode into target structure. Unused keys cause errors.
|
||||
func DecodeOther(log *Logger, other map[string]interface{}, cc interface{}) {
|
||||
func DecodeOther(log *Logger, other interface{}, cc interface{}) {
|
||||
decoderConfig := &mapstructure.DecoderConfig{
|
||||
Result: cc,
|
||||
ErrorUnused: true,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/provider"
|
||||
"github.com/andig/evcc/server"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -17,14 +18,11 @@ var chargerCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
rootCmd.AddCommand(chargerCmd)
|
||||
configureCommand(chargerCmd)
|
||||
}
|
||||
|
||||
func runCharger(cmd *cobra.Command, args []string) {
|
||||
level, _ := cmd.PersistentFlags().GetString("log")
|
||||
configureLogging(level)
|
||||
configureLogging()
|
||||
log.INFO.Printf("evcc %s (%s)", server.Version, server.Commit)
|
||||
|
||||
// load config
|
||||
|
|
@ -35,17 +33,17 @@ func runCharger(cmd *cobra.Command, args []string) {
|
|||
provider.MQTT = provider.NewMqttClient(conf.Mqtt.Broker, conf.Mqtt.User, conf.Mqtt.Password, clientID(), 1)
|
||||
}
|
||||
|
||||
chargers := configureChargers(conf)
|
||||
cp := &ConfigProvider{}
|
||||
cp.configureChargers(conf)
|
||||
|
||||
chargers := cp.chargers
|
||||
if len(args) == 1 {
|
||||
arg := args[0]
|
||||
chargers = map[string]api.Charger{arg: cp.Charger(arg)}
|
||||
}
|
||||
|
||||
for name, v := range chargers {
|
||||
if len(args) == 1 {
|
||||
if target := args[0]; name != target {
|
||||
if _, ok := chargers[target]; !ok {
|
||||
log.FATAL.Fatalf("charger not found: %s", target)
|
||||
}
|
||||
continue
|
||||
}
|
||||
} else if len(chargers) != 1 {
|
||||
if len(chargers) != 1 {
|
||||
fmt.Println(name)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/charger"
|
||||
"github.com/andig/evcc/core"
|
||||
"github.com/andig/evcc/push"
|
||||
"github.com/andig/evcc/server"
|
||||
"github.com/andig/evcc/vehicle"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
|
|
@ -17,9 +20,14 @@ type config struct {
|
|||
Menu []server.MenuConfig
|
||||
Messaging messagingConfig
|
||||
Meters []namedConfig
|
||||
Chargers []typedConfig
|
||||
Vehicles []typedConfig
|
||||
LoadPoints []loadPointConfig
|
||||
Chargers []qualifiedConfig
|
||||
Vehicles []qualifiedConfig
|
||||
LoadPoints []core.Config
|
||||
}
|
||||
|
||||
type qualifiedConfig struct {
|
||||
Name, Type string
|
||||
Other map[string]interface{} `mapstructure:",remain"`
|
||||
}
|
||||
|
||||
type namedConfig struct {
|
||||
|
|
@ -28,18 +36,13 @@ type namedConfig struct {
|
|||
}
|
||||
|
||||
type typedConfig struct {
|
||||
Name, Type string
|
||||
Other map[string]interface{} `mapstructure:",remain"`
|
||||
Type string
|
||||
Other map[string]interface{} `mapstructure:",remain"`
|
||||
}
|
||||
|
||||
type messagingConfig struct {
|
||||
Events map[string]push.EventTemplate
|
||||
Services []messagingService
|
||||
}
|
||||
|
||||
type messagingService struct {
|
||||
Type string
|
||||
Other map[string]interface{} `mapstructure:",remain"`
|
||||
Services []typedConfig
|
||||
}
|
||||
|
||||
type mqttConfig struct {
|
||||
|
|
@ -56,19 +59,63 @@ type influxConfig struct {
|
|||
Interval time.Duration
|
||||
}
|
||||
|
||||
type loadPointConfig struct {
|
||||
Name string
|
||||
GridMeter string // api.Meter
|
||||
PVMeter string // api.Meter
|
||||
ChargeMeter string // api.Meter
|
||||
Charger string // api.Charger
|
||||
Vehicle string // api.Vehicle
|
||||
Mode api.ChargeMode
|
||||
Phases int64
|
||||
MinCurrent int64
|
||||
MaxCurrent int64
|
||||
Steepness int64
|
||||
GuardDuration time.Duration
|
||||
Voltage float64
|
||||
ResidualPower float64
|
||||
// ConfigProvider provides configuration items
|
||||
type ConfigProvider struct {
|
||||
meters map[string]api.Meter
|
||||
chargers map[string]api.Charger
|
||||
vehicles map[string]api.Vehicle
|
||||
}
|
||||
|
||||
// Meter provides meters by name
|
||||
func (c *ConfigProvider) Meter(name string) api.Meter {
|
||||
if meter, ok := c.meters[name]; ok {
|
||||
return meter
|
||||
}
|
||||
log.FATAL.Fatalf("config: invalid meter %s", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Charger provides chargers by name
|
||||
func (c *ConfigProvider) Charger(name string) api.Charger {
|
||||
if charger, ok := c.chargers[name]; ok {
|
||||
return charger
|
||||
}
|
||||
log.FATAL.Fatalf("config: invalid charger %s", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Vehicle provides vehicles by name
|
||||
func (c *ConfigProvider) Vehicle(name string) api.Vehicle {
|
||||
if vehicle, ok := c.vehicles[name]; ok {
|
||||
return vehicle
|
||||
}
|
||||
log.FATAL.Fatalf("config: invalid vehicle %s", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConfigProvider) configure(conf config) {
|
||||
c.configureMeters(conf)
|
||||
c.configureChargers(conf)
|
||||
c.configureVehicles(conf)
|
||||
}
|
||||
|
||||
func (c *ConfigProvider) configureMeters(conf config) {
|
||||
c.meters = make(map[string]api.Meter)
|
||||
for _, cc := range conf.Meters {
|
||||
c.meters[cc.Name] = core.NewMeterFromConfig(log, cc.Other)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConfigProvider) configureChargers(conf config) {
|
||||
c.chargers = make(map[string]api.Charger)
|
||||
for _, cc := range conf.Chargers {
|
||||
c.chargers[cc.Name] = charger.NewFromConfig(log, cc.Type, cc.Other)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConfigProvider) configureVehicles(conf config) {
|
||||
c.vehicles = make(map[string]api.Vehicle)
|
||||
for _, cc := range conf.Vehicles {
|
||||
c.vehicles[cc.Name] = vehicle.NewFromConfig(log, cc.Type, cc.Other)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/andig/evcc/provider"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
|
|
@ -35,6 +36,11 @@ func TestDistConfig(t *testing.T) {
|
|||
log.FATAL.Fatalf("config: failed parsing config file %s: %v", cfgFile, err)
|
||||
}
|
||||
|
||||
// setup mqtt
|
||||
if viper.Get("mqtt") != nil {
|
||||
provider.MQTT = provider.NewMqttClient(conf.Mqtt.Broker, conf.Mqtt.User, conf.Mqtt.Password, clientID(), 1)
|
||||
}
|
||||
|
||||
// check config is valid
|
||||
loadConfig(conf, nil)
|
||||
}
|
||||
|
|
|
|||
24
cmd/meter.go
24
cmd/meter.go
|
|
@ -3,6 +3,7 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/provider"
|
||||
"github.com/andig/evcc/server"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -17,14 +18,11 @@ var meterCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
rootCmd.AddCommand(meterCmd)
|
||||
configureCommand(meterCmd)
|
||||
}
|
||||
|
||||
func runMeter(cmd *cobra.Command, args []string) {
|
||||
level, _ := cmd.PersistentFlags().GetString("log")
|
||||
configureLogging(level)
|
||||
configureLogging()
|
||||
log.INFO.Printf("evcc %s (%s)", server.Version, server.Commit)
|
||||
|
||||
// load config
|
||||
|
|
@ -35,17 +33,17 @@ func runMeter(cmd *cobra.Command, args []string) {
|
|||
provider.MQTT = provider.NewMqttClient(conf.Mqtt.Broker, conf.Mqtt.User, conf.Mqtt.Password, clientID(), 1)
|
||||
}
|
||||
|
||||
meters := configureMeters(conf)
|
||||
cp := &ConfigProvider{}
|
||||
cp.configureMeters(conf)
|
||||
|
||||
meters := cp.meters
|
||||
if len(args) == 1 {
|
||||
arg := args[0]
|
||||
meters = map[string]api.Meter{arg: cp.Meter(arg)}
|
||||
}
|
||||
|
||||
for name, v := range meters {
|
||||
if len(args) == 1 {
|
||||
if target := args[0]; name != target {
|
||||
if _, ok := meters[target]; !ok {
|
||||
log.FATAL.Fatalf("meter not found: %s", target)
|
||||
}
|
||||
continue
|
||||
}
|
||||
} else if len(meters) != 1 {
|
||||
if len(meters) != 1 {
|
||||
fmt.Println(name)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -114,9 +114,12 @@ func Execute() {
|
|||
}
|
||||
}
|
||||
|
||||
func configureLogging(level string) {
|
||||
func configureLogging() {
|
||||
level := viper.GetString("log")
|
||||
|
||||
api.OutThreshold = api.LogLevelToThreshold(level)
|
||||
api.LogThreshold = api.OutThreshold
|
||||
|
||||
api.Loggers(func(name string, logger *api.Logger) {
|
||||
logger.SetStdoutThreshold(api.OutThreshold)
|
||||
})
|
||||
|
|
@ -158,12 +161,12 @@ func tee(in chan core.Param) (chan core.Param, <-chan core.Param) {
|
|||
}
|
||||
|
||||
func run(cmd *cobra.Command, args []string) {
|
||||
configureLogging(viper.GetString("log"))
|
||||
configureLogging()
|
||||
log.INFO.Printf("evcc %s (%s)", server.Version, server.Commit)
|
||||
|
||||
// load config and re-configure logging after reading config file
|
||||
conf := loadConfigFile(cfgFile)
|
||||
configureLogging(viper.GetString("log"))
|
||||
configureLogging()
|
||||
|
||||
go checkVersion()
|
||||
|
||||
|
|
|
|||
112
cmd/setup.go
112
cmd/setup.go
|
|
@ -6,10 +6,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/charger"
|
||||
"github.com/andig/evcc/core"
|
||||
"github.com/andig/evcc/push"
|
||||
"github.com/andig/evcc/vehicle"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
|
|
@ -36,108 +34,22 @@ func clientID() string {
|
|||
return fmt.Sprintf("evcc-%d", pid)
|
||||
}
|
||||
|
||||
func configureMeters(conf config) (meters map[string]api.Meter) {
|
||||
meters = make(map[string]api.Meter)
|
||||
for _, cc := range conf.Meters {
|
||||
meters[cc.Name] = core.NewMeterFromConfig(log, cc.Other)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func configureChargers(conf config) (chargers map[string]api.Charger) {
|
||||
chargers = make(map[string]api.Charger)
|
||||
for _, cc := range conf.Chargers {
|
||||
chargers[cc.Name] = charger.NewFromConfig(log, cc.Type, cc.Other)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func configureVehicles(conf config) (vehicles map[string]api.Vehicle) {
|
||||
vehicles = make(map[string]api.Vehicle)
|
||||
for _, cc := range conf.Vehicles {
|
||||
vehicles[cc.Name] = vehicle.NewFromConfig(log, cc.Type, cc.Other)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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 != "" {
|
||||
// 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, eventsChan chan push.Event) (loadPoints []*core.LoadPoint) {
|
||||
meters := configureMeters(conf)
|
||||
chargers := configureChargers(conf)
|
||||
vehicles := configureVehicles(conf)
|
||||
cp := &ConfigProvider{}
|
||||
cp.configure(conf)
|
||||
|
||||
for idx, lpc := range conf.LoadPoints {
|
||||
// configure loadpoint
|
||||
lp := core.NewLoadPoint()
|
||||
subv := viper.SubSlice("loadpoints")[idx]
|
||||
configureLoadPoint(lp, lpc, subv)
|
||||
// slice of loadpoints
|
||||
lps, ok := viper.AllSettings()["loadpoints"]
|
||||
if !ok {
|
||||
log.FATAL.Fatal("config: missing loadpoints")
|
||||
}
|
||||
|
||||
// assign charger
|
||||
if charger, ok := chargers[lpc.Charger]; ok {
|
||||
lp.Charger = charger
|
||||
} else {
|
||||
log.FATAL.Fatalf("invalid charger '%s'", lpc.Charger)
|
||||
}
|
||||
|
||||
// assign meters
|
||||
for _, m := range []struct {
|
||||
key string
|
||||
meter *api.Meter
|
||||
}{
|
||||
{lpc.GridMeter, &lp.GridMeter},
|
||||
{lpc.ChargeMeter, &lp.ChargeMeter},
|
||||
{lpc.PVMeter, &lp.PVMeter},
|
||||
} {
|
||||
if m.key != "" {
|
||||
if impl, ok := meters[m.key]; ok {
|
||||
*m.meter = impl
|
||||
} else {
|
||||
log.FATAL.Fatalf("invalid meter '%s'", m.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// assign socs
|
||||
if lpc.Vehicle != "" {
|
||||
if impl, ok := vehicles[lpc.Vehicle]; ok {
|
||||
lp.Vehicle = impl
|
||||
} else {
|
||||
log.FATAL.Fatalf("invalid vehicle '%s'", lpc.Vehicle)
|
||||
}
|
||||
}
|
||||
// decode slice into slice of maps
|
||||
var lpm []map[string]interface{}
|
||||
api.DecodeOther(log, lps, &lpm)
|
||||
|
||||
for _, lpc := range lpm {
|
||||
lp := core.NewLoadPointFromConfig(log, cp, lpc)
|
||||
loadPoints = append(loadPoints, lp)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/provider"
|
||||
"github.com/andig/evcc/server"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -17,14 +18,11 @@ var vehicleCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
rootCmd.AddCommand(vehicleCmd)
|
||||
configureCommand(vehicleCmd)
|
||||
}
|
||||
|
||||
func runVehicle(cmd *cobra.Command, args []string) {
|
||||
level, _ := cmd.PersistentFlags().GetString("log")
|
||||
configureLogging(level)
|
||||
configureLogging()
|
||||
log.INFO.Printf("evcc %s (%s)", server.Version, server.Commit)
|
||||
|
||||
// load config
|
||||
|
|
@ -35,17 +33,17 @@ func runVehicle(cmd *cobra.Command, args []string) {
|
|||
provider.MQTT = provider.NewMqttClient(conf.Mqtt.Broker, conf.Mqtt.User, conf.Mqtt.Password, clientID(), 1)
|
||||
}
|
||||
|
||||
vehicles := configureVehicles(conf)
|
||||
cp := &ConfigProvider{}
|
||||
cp.configureVehicles(conf)
|
||||
|
||||
vehicles := cp.vehicles
|
||||
if len(args) == 1 {
|
||||
arg := args[0]
|
||||
vehicles = map[string]api.Vehicle{arg: cp.Vehicle(arg)}
|
||||
}
|
||||
|
||||
for name, v := range vehicles {
|
||||
if len(args) == 1 {
|
||||
if target := args[0]; name != target {
|
||||
if _, ok := vehicles[target]; !ok {
|
||||
log.FATAL.Fatalf("charger not found: %s", target)
|
||||
}
|
||||
continue
|
||||
}
|
||||
} else if len(vehicles) != 1 {
|
||||
if len(vehicles) != 1 {
|
||||
fmt.Println(name)
|
||||
}
|
||||
|
||||
|
|
|
|||
38
core/api.go
38
core/api.go
|
|
@ -42,30 +42,30 @@ func (lp *LoadPoint) Configuration() Configuration {
|
|||
Phases: lp.Phases,
|
||||
MinCurrent: lp.MinCurrent,
|
||||
MaxCurrent: lp.MaxCurrent,
|
||||
GridMeter: lp.GridMeter != nil,
|
||||
PVMeter: lp.PVMeter != nil,
|
||||
GridMeter: lp.gridMeter != nil,
|
||||
PVMeter: lp.pvMeter != nil,
|
||||
ChargeMeter: lp.hasChargeMeter(),
|
||||
}
|
||||
|
||||
if lp.Vehicle != nil {
|
||||
if lp.vehicle != nil {
|
||||
c.SoC = true
|
||||
c.SoCCapacity = lp.Vehicle.Capacity()
|
||||
c.SoCTitle = lp.Vehicle.Title()
|
||||
c.SoCCapacity = lp.vehicle.Capacity()
|
||||
c.SoCTitle = lp.vehicle.Title()
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (lp *LoadPoint) hasChargeMeter() bool {
|
||||
_, isWrapped := lp.ChargeMeter.(*wrapper.ChargeMeter)
|
||||
return lp.ChargeMeter != nil && !isWrapped
|
||||
_, isWrapped := lp.chargeMeter.(*wrapper.ChargeMeter)
|
||||
return lp.chargeMeter != nil && !isWrapped
|
||||
}
|
||||
|
||||
// Dump loadpoint configuration
|
||||
func (lp *LoadPoint) Dump() {
|
||||
vehicle := lp.Vehicle != nil
|
||||
grid := lp.GridMeter != nil
|
||||
pv := lp.PVMeter != nil
|
||||
vehicle := lp.vehicle != nil
|
||||
grid := lp.gridMeter != nil
|
||||
pv := lp.pvMeter != nil
|
||||
log.INFO.Printf("%s config: vehicle %s grid %s pv %s charge %s", lp.Name,
|
||||
presence[vehicle],
|
||||
presence[grid],
|
||||
|
|
@ -73,9 +73,9 @@ func (lp *LoadPoint) Dump() {
|
|||
presence[lp.hasChargeMeter()],
|
||||
)
|
||||
|
||||
_, power := lp.Charger.(api.Meter)
|
||||
_, energy := lp.Charger.(api.ChargeRater)
|
||||
_, timer := lp.Charger.(api.ChargeTimer)
|
||||
_, power := lp.charger.(api.Meter)
|
||||
_, energy := lp.charger.(api.ChargeRater)
|
||||
_, timer := lp.charger.(api.ChargeTimer)
|
||||
log.INFO.Printf("%s charger: power %s energy %s timer %s", lp.Name,
|
||||
presence[power],
|
||||
presence[energy],
|
||||
|
|
@ -115,7 +115,7 @@ func (lp *LoadPoint) SetMode(mode api.ChargeMode) {
|
|||
|
||||
// chargeDuration returns for how long the charge cycle has been running
|
||||
func (lp *LoadPoint) chargeDuration() time.Duration {
|
||||
d, err := lp.ChargeTimer.ChargingTime()
|
||||
d, err := lp.chargeTimer.ChargingTime()
|
||||
if err != nil {
|
||||
log.ERROR.Printf("%s charge timer error: %v", lp.Name, err)
|
||||
}
|
||||
|
|
@ -124,7 +124,7 @@ func (lp *LoadPoint) chargeDuration() time.Duration {
|
|||
|
||||
// chargedEnergy returns energy consumption since charge start in kWh
|
||||
func (lp *LoadPoint) chargedEnergy() float64 {
|
||||
f, err := lp.ChargeRater.ChargedEnergy()
|
||||
f, err := lp.chargeRater.ChargedEnergy()
|
||||
if err != nil {
|
||||
log.ERROR.Printf("%s charge rater error: %v", lp.Name, err)
|
||||
}
|
||||
|
|
@ -137,8 +137,8 @@ func (lp *LoadPoint) remainingChargeDuration(chargePercent float64) time.Duratio
|
|||
return -1
|
||||
}
|
||||
|
||||
if lp.chargePower > 0 && lp.Vehicle != nil {
|
||||
whRemaining := (1 - chargePercent/100.0) * 1e3 * float64(lp.Vehicle.Capacity())
|
||||
if lp.chargePower > 0 && lp.vehicle != nil {
|
||||
whRemaining := (1 - chargePercent/100.0) * 1e3 * float64(lp.vehicle.Capacity())
|
||||
return time.Duration(float64(time.Hour) * whRemaining / lp.chargePower)
|
||||
}
|
||||
|
||||
|
|
@ -147,12 +147,12 @@ func (lp *LoadPoint) remainingChargeDuration(chargePercent float64) time.Duratio
|
|||
|
||||
// publish state of charge and remaining charge duration
|
||||
func (lp *LoadPoint) publishSoC() {
|
||||
if lp.Vehicle == nil {
|
||||
if lp.vehicle == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if lp.connected() {
|
||||
f, err := lp.Vehicle.ChargeState()
|
||||
f, err := lp.vehicle.ChargeState()
|
||||
if err == nil {
|
||||
log.DEBUG.Printf("%s vehicle charge: %.1f%%", lp.Name, f)
|
||||
lp.publish("socCharge", f)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,28 @@ func powerToCurrent(power, voltage float64, phases int64) int64 {
|
|||
return int64(power / (float64(phases) * voltage))
|
||||
}
|
||||
|
||||
// Config contains the public loadpoint configuration
|
||||
type Config struct {
|
||||
Name string
|
||||
Mode api.ChargeMode // Charge mode, guarded by mutex
|
||||
|
||||
// options
|
||||
Sensitivity int64 // Step size of current change
|
||||
Phases int64 // 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
|
||||
|
||||
ChargerRef string `mapstructure:"charger"` // Charger reference
|
||||
GridMeterRef string `mapstructure:"gridmeter"` // Grid usage meter reference
|
||||
PVMeterRef string `mapstructure:"pvmeter"` // PV generation meter reference
|
||||
ChargeMeterRef string `mapstructure:"chargemeter"` // Charger usage meter reference
|
||||
VehicleRef string `mapstructure:"vehicle"` // Vehicle reference
|
||||
|
||||
GuardDuration time.Duration // charger enable/disable minimum holding time
|
||||
}
|
||||
|
||||
// LoadPoint is responsible for controlling charge depending on
|
||||
// SoC needs and power availability.
|
||||
type LoadPoint struct {
|
||||
|
|
@ -43,27 +65,19 @@ type LoadPoint struct {
|
|||
notificationChan chan<- push.Event // notifications
|
||||
uiChan chan<- Param // client push messages
|
||||
|
||||
Name string
|
||||
Charger api.Charger
|
||||
ChargeTimer api.ChargeTimer
|
||||
ChargeRater api.ChargeRater
|
||||
Config `mapstructure:",squash"` // exposed public configuration
|
||||
|
||||
chargeTimer api.ChargeTimer
|
||||
chargeRater api.ChargeRater
|
||||
|
||||
// meters
|
||||
GridMeter api.Meter // Grid usage meter
|
||||
PVMeter api.Meter // PV generation meter
|
||||
ChargeMeter api.Meter // Charger usage meter
|
||||
Vehicle api.Vehicle // Vehicle
|
||||
|
||||
// options
|
||||
Steepness int64 // Step size of current change
|
||||
Phases int64 // 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
|
||||
charger api.Charger // Charger
|
||||
gridMeter api.Meter // Grid usage meter
|
||||
pvMeter api.Meter // PV generation meter
|
||||
chargeMeter api.Meter // Charger usage meter
|
||||
vehicle api.Vehicle // Vehicle
|
||||
|
||||
// cached state
|
||||
Mode api.ChargeMode // Charge mode, guarded by mutex
|
||||
status api.ChargeStatus // Charger status
|
||||
targetCurrent int64 // Allowed current. Between MinCurrent and MaxCurrent.
|
||||
enabled bool // Charger enabled state
|
||||
|
|
@ -73,26 +87,63 @@ type LoadPoint struct {
|
|||
chargePower float64 // Charging power
|
||||
|
||||
// contactor switch guard
|
||||
guardUpdated time.Time // charger enabled/disabled timestamp
|
||||
GuardDuration time.Duration // charger enable/disable minimum holding time
|
||||
guardUpdated time.Time // charger enabled/disabled timestamp
|
||||
}
|
||||
|
||||
// configProvider gives access to configuration repository
|
||||
type configProvider interface {
|
||||
Meter(string) api.Meter
|
||||
Charger(string) api.Charger
|
||||
Vehicle(string) api.Vehicle
|
||||
}
|
||||
|
||||
// NewLoadPointFromConfig creates a new loadpoint
|
||||
func NewLoadPointFromConfig(log *api.Logger, cp configProvider, other map[string]interface{}) *LoadPoint {
|
||||
lp := NewLoadPoint()
|
||||
api.DecodeOther(log, other, &lp)
|
||||
|
||||
if lp.ChargerRef != "" {
|
||||
lp.charger = cp.Charger(lp.ChargerRef)
|
||||
} else {
|
||||
log.FATAL.Fatal("config: missing charger")
|
||||
}
|
||||
if lp.PVMeterRef == "" && lp.GridMeterRef == "" {
|
||||
log.FATAL.Fatal("config: missing either pv or grid meter")
|
||||
}
|
||||
if lp.GridMeterRef != "" {
|
||||
lp.gridMeter = cp.Meter(lp.GridMeterRef)
|
||||
}
|
||||
if lp.ChargeMeterRef != "" {
|
||||
lp.chargeMeter = cp.Meter(lp.ChargeMeterRef)
|
||||
}
|
||||
if lp.PVMeterRef != "" {
|
||||
lp.pvMeter = cp.Meter(lp.PVMeterRef)
|
||||
}
|
||||
if lp.VehicleRef != "" {
|
||||
lp.vehicle = cp.Vehicle(lp.VehicleRef)
|
||||
}
|
||||
|
||||
return lp
|
||||
}
|
||||
|
||||
// NewLoadPoint creates a LoadPoint with sane defaults
|
||||
func NewLoadPoint() *LoadPoint {
|
||||
return &LoadPoint{
|
||||
clock: clock.New(),
|
||||
bus: evbus.New(),
|
||||
triggerChan: make(chan struct{}, 1),
|
||||
Name: "Main",
|
||||
Mode: api.ModeOff,
|
||||
clock: clock.New(),
|
||||
bus: evbus.New(),
|
||||
triggerChan: make(chan struct{}, 1),
|
||||
Config: Config{
|
||||
Name: "main",
|
||||
Mode: api.ModeOff,
|
||||
Phases: 1,
|
||||
Voltage: 230, // V
|
||||
MinCurrent: 6, // A
|
||||
MaxCurrent: 16, // A
|
||||
Sensitivity: 10, // A
|
||||
GuardDuration: 10 * time.Minute,
|
||||
},
|
||||
status: api.StatusNone,
|
||||
Phases: 1,
|
||||
Voltage: 230, // V
|
||||
MinCurrent: 6, // A
|
||||
MaxCurrent: 16, // A
|
||||
Steepness: 10, // A
|
||||
targetCurrent: 0, // A
|
||||
GuardDuration: 10 * time.Minute,
|
||||
targetCurrent: 0, // A
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,12 +174,12 @@ func (lp *LoadPoint) evChargeStartHandler() {
|
|||
|
||||
// evChargeStartHandler sends external stop event
|
||||
func (lp *LoadPoint) evChargeStopHandler() {
|
||||
energy, err := lp.ChargeRater.ChargedEnergy()
|
||||
energy, err := lp.chargeRater.ChargedEnergy()
|
||||
if err != nil {
|
||||
log.ERROR.Printf("%s charged energy: %v", lp.Name, err)
|
||||
}
|
||||
|
||||
duration, err := lp.ChargeTimer.ChargingTime()
|
||||
duration, err := lp.chargeTimer.ChargingTime()
|
||||
if err != nil {
|
||||
log.ERROR.Printf("%s charge duration: %v", lp.Name, err)
|
||||
}
|
||||
|
|
@ -162,14 +213,14 @@ func (lp *LoadPoint) Prepare(uiChan chan<- Param, notificationChan chan<- push.E
|
|||
lp.notificationChan = notificationChan
|
||||
lp.uiChan = uiChan
|
||||
|
||||
if lp.PVMeter == nil && lp.GridMeter == nil {
|
||||
log.FATAL.Fatal("missing either PV or Grid meter - aborting")
|
||||
if lp.pvMeter == nil && lp.gridMeter == nil {
|
||||
log.FATAL.Fatal("missing either pv or grid meter")
|
||||
}
|
||||
|
||||
// ensure charge meter exists
|
||||
if lp.ChargeMeter == nil {
|
||||
if mt, ok := lp.Charger.(api.Meter); ok {
|
||||
lp.ChargeMeter = mt
|
||||
if lp.chargeMeter == nil {
|
||||
if mt, ok := lp.charger.(api.Meter); ok {
|
||||
lp.chargeMeter = mt
|
||||
} else {
|
||||
mt := &wrapper.ChargeMeter{
|
||||
Phases: lp.Phases,
|
||||
|
|
@ -179,29 +230,29 @@ func (lp *LoadPoint) Prepare(uiChan chan<- Param, notificationChan chan<- push.E
|
|||
_ = lp.bus.Subscribe(evStopCharge, func() {
|
||||
mt.SetChargeCurrent(0)
|
||||
})
|
||||
lp.ChargeMeter = mt
|
||||
lp.chargeMeter = mt
|
||||
}
|
||||
}
|
||||
|
||||
// ensure charge rater exists
|
||||
if rt, ok := lp.Charger.(api.ChargeRater); ok {
|
||||
lp.ChargeRater = rt
|
||||
if rt, ok := lp.charger.(api.ChargeRater); ok {
|
||||
lp.chargeRater = rt
|
||||
} else {
|
||||
rt := wrapper.NewChargeRater(lp.Name, lp.ChargeMeter)
|
||||
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
|
||||
lp.chargeRater = rt
|
||||
}
|
||||
|
||||
// ensure charge timer exists
|
||||
if ct, ok := lp.Charger.(api.ChargeTimer); ok {
|
||||
lp.ChargeTimer = ct
|
||||
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
|
||||
lp.chargeTimer = ct
|
||||
}
|
||||
|
||||
// event handlers
|
||||
|
|
@ -209,7 +260,7 @@ func (lp *LoadPoint) Prepare(uiChan chan<- Param, notificationChan chan<- push.E
|
|||
_ = lp.bus.Subscribe(evStopCharge, lp.evChargeStopHandler)
|
||||
|
||||
// read initial enabled state
|
||||
enabled, err := lp.Charger.Enabled()
|
||||
enabled, err := lp.charger.Enabled()
|
||||
if err == nil {
|
||||
lp.enabled = enabled
|
||||
log.INFO.Printf("%s charger %sd", lp.Name, status[lp.enabled])
|
||||
|
|
@ -245,7 +296,7 @@ func (lp *LoadPoint) chargerEnable(enable bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
err := lp.Charger.Enable(enable)
|
||||
err := lp.charger.Enable(enable)
|
||||
if err == nil {
|
||||
lp.enabled = enable // cache
|
||||
log.INFO.Printf("%s charger %s", lp.Name, status[enable])
|
||||
|
|
@ -281,7 +332,7 @@ func (lp *LoadPoint) chargingCycle(enable bool) {
|
|||
// updateChargeStatus updates car status and stops charging if car disconnected
|
||||
func (lp *LoadPoint) updateChargeStatus() api.ChargeStatus {
|
||||
// abort if no vehicle connected
|
||||
status, err := lp.Charger.Status()
|
||||
status, err := lp.charger.Status()
|
||||
if err != nil {
|
||||
log.ERROR.Printf("%s charger error: %v", lp.Name, err)
|
||||
return api.StatusNone
|
||||
|
|
@ -324,7 +375,7 @@ func (lp *LoadPoint) setTargetCurrent(targetCurrentIn int64) error {
|
|||
|
||||
if lp.targetCurrent != targetCurrent {
|
||||
log.DEBUG.Printf("%s set charge current: %dA", lp.Name, targetCurrent)
|
||||
if err := lp.Charger.MaxCurrent(targetCurrent); err != nil {
|
||||
if err := lp.charger.MaxCurrent(targetCurrent); err != nil {
|
||||
return fmt.Errorf("%s charge controller error: %v", lp.Name, err)
|
||||
}
|
||||
|
||||
|
|
@ -345,9 +396,9 @@ func (lp *LoadPoint) rampUpDown(target int64) error {
|
|||
|
||||
var step int64
|
||||
if current < target {
|
||||
step = min(current+lp.Steepness, target)
|
||||
step = min(current+lp.Sensitivity, target)
|
||||
} else if current > target {
|
||||
step = max(current-lp.Steepness, target)
|
||||
step = max(current-lp.Sensitivity, target)
|
||||
}
|
||||
|
||||
step = clamp(step, lp.MinCurrent, lp.MaxCurrent)
|
||||
|
|
@ -442,9 +493,9 @@ func (lp *LoadPoint) updateMeters() (err error) {
|
|||
}
|
||||
|
||||
// read PV meter before charge meter
|
||||
retryMeter("grid", lp.GridMeter, &lp.gridPower)
|
||||
retryMeter("pv", lp.PVMeter, &lp.pvPower)
|
||||
retryMeter("charge", lp.ChargeMeter, &lp.chargePower)
|
||||
retryMeter("grid", lp.gridMeter, &lp.gridPower)
|
||||
retryMeter("pv", lp.pvMeter, &lp.pvPower)
|
||||
retryMeter("charge", lp.chargeMeter, &lp.chargePower)
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ func TestNew(t *testing.T) {
|
|||
if lp.MaxCurrent != lpMaxCurrent {
|
||||
t.Errorf("MaxCurrent %v", lp.MaxCurrent)
|
||||
}
|
||||
if lp.Steepness != 10 {
|
||||
t.Errorf("Steepness %v", lp.Steepness)
|
||||
if lp.Sensitivity != 10 {
|
||||
t.Errorf("Sensitivity %v", lp.Sensitivity)
|
||||
}
|
||||
if lp.status != api.StatusNone {
|
||||
t.Errorf("status %v", lp.status)
|
||||
|
|
@ -55,14 +55,14 @@ func newLoadPoint(charger api.Charger, pv, gm, cm api.Meter) *LoadPoint {
|
|||
lp.clock = clock.NewMock()
|
||||
lp.clock.(*clock.Mock).Add(time.Hour)
|
||||
|
||||
lp.Charger = charger
|
||||
lp.PVMeter = pv
|
||||
lp.GridMeter = gm
|
||||
lp.charger = charger
|
||||
lp.pvMeter = pv
|
||||
lp.gridMeter = gm
|
||||
|
||||
// prevent assigning a nil pointer sake of
|
||||
// https://groups.google.com/forum/#!topic/golang-nuts/wnH302gBa4I/discussion
|
||||
if !(cm == nil || reflect.ValueOf(cm).IsNil()) {
|
||||
lp.ChargeMeter = cm
|
||||
lp.chargeMeter = cm
|
||||
}
|
||||
|
||||
uiChan := make(chan Param)
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ loadpoints:
|
|||
gridmeter: grid # grid meter
|
||||
pvmeter: pv # pv meter
|
||||
chargemeter: charge # charge meter
|
||||
steepness: 1 # raise/lower charge current in 1A steps
|
||||
sensitivity: 1 # current raise/lower steps size (default 1A)
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue