Initial commit
This commit is contained in:
commit
3c503b333d
98 changed files with 43274 additions and 0 deletions
30
core/wrapper/chargemeter.go
Normal file
30
core/wrapper/chargemeter.go
Normal 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
118
core/wrapper/chargerater.go
Normal 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
|
||||
}
|
||||
78
core/wrapper/chargerater_test.go
Normal file
78
core/wrapper/chargerater_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
53
core/wrapper/chargetimer.go
Normal file
53
core/wrapper/chargetimer.go
Normal 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
|
||||
}
|
||||
25
core/wrapper/chargetimer_test.go
Normal file
25
core/wrapper/chargetimer_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
17
core/wrapper/composites.go
Normal file
17
core/wrapper/composites.go
Normal 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
5
core/wrapper/log.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package wrapper
|
||||
|
||||
import "github.com/andig/evcc/api"
|
||||
|
||||
var log = api.NewLogger("wrap")
|
||||
Loading…
Add table
Add a link
Reference in a new issue