Initial commit

This commit is contained in:
andig 2019-12-06 17:27:05 +01:00
commit 3c503b333d
98 changed files with 43274 additions and 0 deletions

View file

@ -0,0 +1,30 @@
package wrapper
import "sync"
// ChargeMeter is a replacement for a physical charge meter.
// It uses the charger's actual or max current to calculate power consumption.
type ChargeMeter struct {
sync.Mutex
Phases int64
Voltage float64
power float64
}
func currentToPower(current, voltage float64, phases int64) float64 {
return float64(phases) * current * voltage
}
// SetChargeCurrent updates meter's current power based on actual current
func (m *ChargeMeter) SetChargeCurrent(current int64) {
m.Lock()
defer m.Unlock()
m.power = currentToPower(float64(current), m.Voltage, m.Phases)
}
// CurrentPower implements the Meter.CurrentPower interface
func (m *ChargeMeter) CurrentPower() (float64, error) {
m.Lock()
defer m.Unlock()
return m.power, nil
}

118
core/wrapper/chargerater.go Normal file
View file

@ -0,0 +1,118 @@
package wrapper
import (
"fmt"
"sync"
"time"
"github.com/andig/evcc/api"
"github.com/benbjohnson/clock"
)
// ChargeRater is responsible for providing charged energy amount
// by implementing api.ChargeRater. It uses the charge meter's TotalEnergy or
// keeps track of consumed energy by regularly updating consumed power.
type ChargeRater struct {
sync.Mutex
clck clock.Clock
name string
meter api.Meter
charging bool
start time.Time
startEnergy float64
chargedEnergy float64
}
// NewChargeRater creates charge rater and initializes realtime clock
func NewChargeRater(name string, meter api.Meter) *ChargeRater {
return &ChargeRater{
clck: clock.New(),
name: name,
meter: meter,
}
}
// StartCharge records meter start energy. If meter does not supply TotalEnergy,
// start time is recorded and charged energy set to zero.
func (cr *ChargeRater) StartCharge() {
cr.Lock()
defer cr.Unlock()
// time is needed if MeterEnergy is not supported
cr.charging = true
cr.start = cr.clck.Now()
// get end energy amount
if m, ok := cr.meter.(api.MeterEnergy); ok {
if f, err := m.TotalEnergy(); err == nil {
cr.startEnergy = f
log.TRACE.Printf("%s charge start energy: %.0fkWh", cr.name, f)
} else {
log.ERROR.Printf("%s charge meter error %v", cr.name, err)
}
} else {
cr.chargedEnergy = 0
}
}
func (cr *ChargeRater) StopCharge() {
cr.Lock()
defer cr.Unlock()
cr.charging = false
// get end energy amount
if m, ok := cr.meter.(api.MeterEnergy); ok {
if f, err := m.TotalEnergy(); err == nil {
cr.chargedEnergy = f - cr.startEnergy
log.TRACE.Printf("%s final charge energy: %.0fkWh", cr.name, cr.chargedEnergy)
} else {
log.ERROR.Printf("%s charge meter error %v", cr.name, err)
}
}
}
// SetChargePower increments consumed energy by amount in Wh since last update
func (cr *ChargeRater) SetChargePower(power float64) {
cr.Lock()
defer cr.Unlock()
if !cr.charging {
return
}
// 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)
// move timestamp
cr.start = cr.clck.Now()
}
}
// ChargedEnergy returns energy consumption since charge start in Wh
func (cr *ChargeRater) ChargedEnergy() (float64, error) {
cr.Lock()
defer cr.Unlock()
// return previously charged energy
if !cr.charging {
return cr.chargedEnergy, nil
}
// get current energy amount
if m, ok := cr.meter.(api.MeterEnergy); ok {
f, err := m.TotalEnergy()
if err == nil {
log.TRACE.Printf("%s charge energy: %.0fkWh", cr.name, cr.chargedEnergy)
return f - cr.startEnergy, nil
}
return 0, fmt.Errorf("%s charge meter error %v", cr.name, err)
}
// return charged energy sofar if meter is not used
return cr.chargedEnergy, nil
}

View file

@ -0,0 +1,78 @@
package wrapper
import (
"testing"
"time"
"github.com/andig/evcc/mock"
"github.com/benbjohnson/clock"
"github.com/golang/mock/gomock"
)
func TestNoMeter(t *testing.T) {
cr := NewChargeRater("foo", nil)
clck := clock.NewMock()
cr.clck = clck
cr.StartCharge()
// 1kWh
clck.Add(time.Hour)
cr.SetChargePower(1e3)
cr.SetChargePower(1e3) // should be ignored as time is identical
// 0kWh
clck.Add(time.Hour)
cr.SetChargePower(0)
cr.StopCharge()
// 1kWh - not counted
clck.Add(time.Hour)
cr.SetChargePower(1e3)
f, err := cr.ChargedEnergy()
if f != 1 || err != nil {
t.Error(f, err)
}
}
func TestWrappedMeter(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mm := mock.NewMockMeter(ctrl)
me := mock.NewMockMeterEnergy(ctrl)
cm := &CompositeMeter{mm, me}
me.EXPECT().
TotalEnergy().
Return(2.0, nil)
me.EXPECT().
TotalEnergy().
Return(3.0, nil)
cr := NewChargeRater("foo", cm)
clck := clock.NewMock()
cr.clck = clck
cr.StartCharge()
// ignored with meter present
clck.Add(time.Hour)
cr.SetChargePower(1e3)
clck.Add(time.Hour)
cr.SetChargePower(0)
cr.StopCharge()
// ignored with meter present
clck.Add(time.Hour)
cr.SetChargePower(1e3)
f, err := cr.ChargedEnergy()
if f != 1 || err != nil {
t.Error(f, err)
}
}

View file

@ -0,0 +1,53 @@
package wrapper
import (
"sync"
"time"
"github.com/benbjohnson/clock"
)
type ChargeTimer struct {
sync.Mutex
clck clock.Clock
charging bool
start time.Time
duration time.Duration
}
// NewChargeTimer creates ChargeTimer for tracking duration between
// start and stop events
func NewChargeTimer() *ChargeTimer {
return &ChargeTimer{
clck: clock.New(),
}
}
func (m *ChargeTimer) StartCharge() {
m.Lock()
defer m.Unlock()
m.charging = true
m.start = m.clck.Now()
m.duration = 0
}
func (m *ChargeTimer) StopCharge() {
m.Lock()
defer m.Unlock()
m.charging = false
m.duration = m.clck.Since(m.start)
}
// ChargingTime implements the api.ChargeTimer interface
func (m *ChargeTimer) ChargingTime() (time.Duration, error) {
m.Lock()
defer m.Unlock()
if m.charging {
return m.clck.Since(m.start), nil
}
return m.duration, nil
}

View file

@ -0,0 +1,25 @@
package wrapper
import (
"testing"
"time"
"github.com/benbjohnson/clock"
)
func TestTimer(t *testing.T) {
ct := NewChargeTimer()
clck := clock.NewMock()
ct.clck = clck
ct.StartCharge()
clck.Add(time.Hour)
ct.StopCharge()
clck.Add(time.Hour)
d, err := ct.ChargingTime()
if d != time.Hour || err != nil {
t.Error(d, err)
}
}

View file

@ -0,0 +1,17 @@
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
}

5
core/wrapper/log.go Normal file
View file

@ -0,0 +1,5 @@
package wrapper
import "github.com/andig/evcc/api"
var log = api.NewLogger("wrap")