Redesign phase handling (#2613)
This commit is contained in:
parent
bf169061c0
commit
63c74bc0a9
8 changed files with 501 additions and 337 deletions
|
|
@ -11,7 +11,7 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/evcc-io/evcc/api Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,VehiclePhases,ChargeRater,Battery
|
||||
//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/evcc-io/evcc/api Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery
|
||||
|
||||
// ChargeMode are charge modes modeled after OpenWB
|
||||
type ChargeMode string
|
||||
|
|
@ -143,6 +143,7 @@ type Vehicle interface {
|
|||
Battery
|
||||
Title() string
|
||||
Capacity() int64
|
||||
Phases() int
|
||||
Identifiers() []string
|
||||
OnIdentified() ActionConfig
|
||||
}
|
||||
|
|
@ -172,11 +173,6 @@ type VehiclePosition interface {
|
|||
Position() (float64, float64, error)
|
||||
}
|
||||
|
||||
// VehiclePhases returns the number of supported phases
|
||||
type VehiclePhases interface {
|
||||
Phases() int
|
||||
}
|
||||
|
||||
// VehicleStartCharge starts the charging session on the vehicle side
|
||||
type VehicleStartCharge interface {
|
||||
StartCharge() error
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ type LoadPoint struct {
|
|||
GuardDuration time.Duration // charger enable/disable minimum holding time
|
||||
|
||||
enabled bool // Charger enabled state
|
||||
activePhases int // Charger active phases as used by vehicle
|
||||
measuredPhases int // Charger physically measured phases
|
||||
chargeCurrent float64 // Charger current limit
|
||||
guardUpdated time.Time // Charger enabled/disabled timestamp
|
||||
socUpdated time.Time // SoC updated timestamp (poll: connected)
|
||||
|
|
@ -235,9 +235,9 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
|
|||
lp.charger = cp.Charger(lp.ChargerRef)
|
||||
lp.configureChargerType(lp.charger)
|
||||
|
||||
// ensure 1p setup for switchable charger (https://github.com/evcc-io/evcc/issues/1572)
|
||||
if _, ok := lp.charger.(api.ChargePhases); ok && lp.Phases != 0 {
|
||||
lp.log.WARN.Printf("ignoring phases config (%dp) for switchable charger", lp.Phases)
|
||||
// TODO handle delayed scale-down
|
||||
if _, ok := lp.charger.(api.ChargePhases); ok && lp.GetPhases() != 0 {
|
||||
lp.log.WARN.Printf("ignoring phases config (%dp) for switchable charger", lp.GetPhases())
|
||||
lp.setPhases(0)
|
||||
}
|
||||
|
||||
|
|
@ -424,6 +424,9 @@ func (lp *LoadPoint) evVehicleConnectHandler() {
|
|||
func (lp *LoadPoint) evVehicleDisconnectHandler() {
|
||||
lp.log.INFO.Println("car disconnected")
|
||||
|
||||
// phases are unknown when vehicle disconnects
|
||||
lp.resetMeasuredPhases()
|
||||
|
||||
// energy and duration
|
||||
lp.publish("chargedEnergy", lp.chargedEnergy)
|
||||
lp.publish("connectedDuration", lp.clock.Since(lp.connectedTime))
|
||||
|
|
@ -476,7 +479,7 @@ func (lp *LoadPoint) evChargeCurrentHandler(current float64) {
|
|||
// If physical charge meter is present this handler is not used.
|
||||
// The actual value is published by the evChargeCurrentHandler
|
||||
func (lp *LoadPoint) evChargeCurrentWrappedMeterHandler(current float64) {
|
||||
power := current * float64(lp.activePhases) * Voltage
|
||||
power := current * float64(lp.activePhases()) * Voltage
|
||||
|
||||
if !lp.enabled || lp.GetStatus() != api.StatusC {
|
||||
// if disabled we cannot be charging
|
||||
|
|
@ -517,9 +520,6 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
|
|||
lp.pushChan = pushChan
|
||||
lp.lpChan = lpChan
|
||||
|
||||
// assume all phases are active
|
||||
lp.activePhases = lp.Phases
|
||||
|
||||
// event handlers
|
||||
_ = lp.bus.Subscribe(evChargeStart, lp.evChargeStartHandler)
|
||||
_ = lp.bus.Subscribe(evChargeStop, lp.evChargeStopHandler)
|
||||
|
|
@ -533,7 +533,7 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
|
|||
lp.publish("minCurrent", lp.MinCurrent)
|
||||
lp.publish("maxCurrent", lp.MaxCurrent)
|
||||
lp.publish("phases", lp.Phases)
|
||||
lp.publish("activePhases", lp.activePhases)
|
||||
lp.publish("activePhases", lp.activePhases())
|
||||
lp.publish("hasVehicle", len(lp.vehicles) > 0)
|
||||
|
||||
lp.Lock()
|
||||
|
|
@ -815,8 +815,6 @@ func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) {
|
|||
|
||||
lp.applyAction(vehicle.OnIdentified())
|
||||
|
||||
lp.setVehiclePhases()
|
||||
|
||||
lp.progress.Reset()
|
||||
} else {
|
||||
lp.socEstimator = nil
|
||||
|
|
@ -983,67 +981,36 @@ func (lp *LoadPoint) resetPVTimerIfRunning(typ ...string) {
|
|||
lp.publishTimer(pvTimer, 0, timerInactive)
|
||||
}
|
||||
|
||||
// setVehiclePhases sets the expected active phases by the vehicle
|
||||
func (lp *LoadPoint) setVehiclePhases() {
|
||||
if v, ok := lp.vehicle.(api.VehiclePhases); ok {
|
||||
if phases := v.Phases(); phases > 0 {
|
||||
lp.log.DEBUG.Printf("vehicle phases: %dp", phases)
|
||||
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
|
||||
if phases > lp.Phases {
|
||||
phases = lp.Phases
|
||||
}
|
||||
|
||||
lp.activePhases = phases
|
||||
lp.publish("activePhases", lp.activePhases)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scalePhasesIfAvailable scales if api.ChargePhases is available
|
||||
func (lp *LoadPoint) scalePhasesIfAvailable(phases int) error {
|
||||
err := lp.scalePhases(phases)
|
||||
if errors.Is(err, api.ErrNotAvailable) {
|
||||
return nil
|
||||
if _, ok := lp.charger.(api.ChargePhases); ok {
|
||||
return lp.scalePhases(phases)
|
||||
}
|
||||
return err
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setPhases sets the number of enabled phases without modifying the charger
|
||||
func (lp *LoadPoint) setPhases(phases int) {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
|
||||
if lp.Phases != phases {
|
||||
if lp.GetPhases() != phases {
|
||||
lp.Lock()
|
||||
lp.Phases = phases
|
||||
lp.publish("phases", lp.Phases)
|
||||
lp.phaseTimer = time.Time{}
|
||||
lp.Unlock()
|
||||
|
||||
if phases < lp.activePhases {
|
||||
// When scaling down, charger will temporarily disable. During this time, activePhases will not be updated
|
||||
// since all currents are zero. This will lead to inconsistent state when scaling is triggered again
|
||||
// (1p configured vs 3p active). Update activePhases to reflect the current state of the charger.
|
||||
lp.activePhases = phases
|
||||
lp.publish("activePhases", lp.activePhases)
|
||||
} else {
|
||||
// When scaling up, charger will offer more phases than vehicle can use.
|
||||
// Adjust to enable subsequent PV restart at lower powers than full 3p.
|
||||
lp.setVehiclePhases()
|
||||
}
|
||||
lp.publish("phases", lp.Phases)
|
||||
lp.publishTimer(phaseTimer, 0, timerInactive)
|
||||
|
||||
lp.resetMeasuredPhases()
|
||||
}
|
||||
}
|
||||
|
||||
// scalePhases adjusts the number of active phases and returns the appropriate charging current.
|
||||
// Returns api.ErrNotAvailable if api.ChargePhases is not available.
|
||||
func (lp *LoadPoint) scalePhases(phases int) error {
|
||||
if phases != 1 && phases != 3 {
|
||||
return fmt.Errorf("invalid number of phases: %d", phases)
|
||||
}
|
||||
|
||||
cp, ok := lp.charger.(api.ChargePhases)
|
||||
if !ok {
|
||||
return api.ErrNotAvailable
|
||||
panic("charger does not implement api.ChargePhases")
|
||||
}
|
||||
|
||||
if lp.GetPhases() != phases {
|
||||
|
|
@ -1060,9 +1027,6 @@ func (lp *LoadPoint) scalePhases(phases int) error {
|
|||
// update setting
|
||||
lp.setPhases(phases)
|
||||
|
||||
// disable phase timer
|
||||
lp.phaseTimer = time.Time{}
|
||||
|
||||
// allow pv mode to re-enable charger right away
|
||||
lp.elapsePVTimer()
|
||||
}
|
||||
|
|
@ -1074,29 +1038,22 @@ func (lp *LoadPoint) scalePhases(phases int) error {
|
|||
func (lp *LoadPoint) pvScalePhases(availablePower, minCurrent, maxCurrent float64) bool {
|
||||
phases := lp.GetPhases()
|
||||
|
||||
// observed phase state inconsistency (https://github.com/evcc-io/evcc/issues/1572, https://github.com/evcc-io/evcc/issues/2230)
|
||||
if phases > 0 && phases < lp.activePhases {
|
||||
lp.log.WARN.Printf("ignoring inconsistent phases: %dp < %dp observed active", phases, lp.activePhases)
|
||||
|
||||
// if 3p->1p change is slow and we're no longer charging, we'll correct the observed phases here
|
||||
if lp.GetStatus() == api.StatusB {
|
||||
lp.activePhases = 1
|
||||
}
|
||||
}
|
||||
|
||||
// this can happen the first time for a 1p3p-capable charger, see https://github.com/evcc-io/evcc/issues/2520
|
||||
if phases == 0 && lp.activePhases == 0 {
|
||||
lp.log.DEBUG.Printf("assuming initial phase state: 3p")
|
||||
lp.phaseTimer = elapsed
|
||||
lp.activePhases = 3
|
||||
// observed phase state inconsistency
|
||||
// - https://github.com/evcc-io/evcc/issues/1572
|
||||
// - https://github.com/evcc-io/evcc/issues/2230
|
||||
// - https://github.com/evcc-io/evcc/issues/2613
|
||||
measuredPhases := lp.getMeasuredPhases()
|
||||
if phases > 0 && phases < measuredPhases {
|
||||
lp.log.WARN.Printf("ignoring inconsistent phases: %dp < %dp observed active", phases, measuredPhases)
|
||||
}
|
||||
|
||||
var waiting bool
|
||||
targetCurrent := powerToCurrent(availablePower, lp.activePhases)
|
||||
activePhases := lp.activePhases()
|
||||
targetCurrent := powerToCurrent(availablePower, activePhases)
|
||||
|
||||
// scale down phases
|
||||
if targetCurrent < minCurrent && (phases == 0 || phases == 3) && lp.activePhases > 1 {
|
||||
lp.log.DEBUG.Printf("available power below %dp min threshold of %.0fW", lp.activePhases, float64(lp.activePhases)*Voltage*minCurrent)
|
||||
if targetCurrent < minCurrent && activePhases > 1 {
|
||||
lp.log.DEBUG.Printf("available power below %dp min threshold of %.0fW", activePhases, float64(activePhases)*Voltage*minCurrent)
|
||||
|
||||
if lp.phaseTimer.IsZero() {
|
||||
lp.log.DEBUG.Printf("start phase disable timer: %v", lp.Disable.Delay)
|
||||
|
|
@ -1120,8 +1077,11 @@ func (lp *LoadPoint) pvScalePhases(availablePower, minCurrent, maxCurrent float6
|
|||
lp.log.DEBUG.Printf("phase disable timer remaining: %v", (lp.Disable.Delay - elapsed).Round(time.Second))
|
||||
}
|
||||
|
||||
maxPhases := lp.maxActivePhases()
|
||||
scalable := maxPhases > 1 && phases < maxPhases
|
||||
|
||||
// scale up phases
|
||||
if min3pCurrent := powerToCurrent(availablePower, 3); min3pCurrent >= minCurrent && (phases == 0 || phases == 1) {
|
||||
if min3pCurrent := powerToCurrent(availablePower, 3); min3pCurrent >= minCurrent && scalable {
|
||||
lp.log.DEBUG.Printf("available power above 3p min threshold of %.0fW", 3*Voltage*minCurrent)
|
||||
|
||||
if lp.phaseTimer.IsZero() {
|
||||
|
|
@ -1129,10 +1089,10 @@ func (lp *LoadPoint) pvScalePhases(availablePower, minCurrent, maxCurrent float6
|
|||
lp.phaseTimer = lp.clock.Now()
|
||||
}
|
||||
|
||||
lp.publishTimer(phaseTimer, lp.Disable.Delay, phaseScale3p)
|
||||
lp.publishTimer(phaseTimer, lp.Enable.Delay, phaseScale3p)
|
||||
|
||||
elapsed := lp.clock.Since(lp.phaseTimer)
|
||||
if elapsed >= lp.Disable.Delay {
|
||||
if elapsed >= lp.Enable.Delay {
|
||||
lp.log.DEBUG.Println("phase enable timer elapsed")
|
||||
if err := lp.scalePhases(3); err == nil {
|
||||
lp.log.DEBUG.Printf("switched phases: 3p @ %.0fW", availablePower)
|
||||
|
|
@ -1157,6 +1117,7 @@ func (lp *LoadPoint) pvScalePhases(availablePower, minCurrent, maxCurrent float6
|
|||
return false
|
||||
}
|
||||
|
||||
// TODO move up to timer functions
|
||||
func (lp *LoadPoint) publishTimer(name string, delay time.Duration, action string) {
|
||||
timer := lp.pvTimer
|
||||
if name == phaseTimer {
|
||||
|
|
@ -1196,10 +1157,11 @@ func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter
|
|||
|
||||
// calculate target charge current from delta power and actual current
|
||||
effectiveCurrent := lp.effectiveCurrent()
|
||||
deltaCurrent := powerToCurrent(-sitePower, lp.activePhases)
|
||||
activePhases := lp.activePhases()
|
||||
deltaCurrent := powerToCurrent(-sitePower, activePhases)
|
||||
targetCurrent := math.Max(effectiveCurrent+deltaCurrent, 0)
|
||||
|
||||
lp.log.DEBUG.Printf("max charge current: %.3gA = %.3gA + %.3gA (%.0fW @ %dp)", targetCurrent, effectiveCurrent, deltaCurrent, sitePower, lp.activePhases)
|
||||
lp.log.DEBUG.Printf("max charge current: %.3gA = %.3gA + %.3gA (%.0fW @ %dp)", targetCurrent, effectiveCurrent, deltaCurrent, sitePower, activePhases)
|
||||
|
||||
// in MinPV mode or under special conditions return at least minCurrent
|
||||
if (mode == api.ModeMinPV || batteryBuffered || lp.climateActive()) && targetCurrent < minCurrent {
|
||||
|
|
@ -1337,9 +1299,12 @@ func (lp *LoadPoint) updateChargeCurrents() {
|
|||
}
|
||||
|
||||
if phases >= 1 {
|
||||
lp.activePhases = phases
|
||||
lp.log.DEBUG.Printf("detected phases: %dp %.3gA", lp.activePhases, lp.chargeCurrents)
|
||||
lp.publish("activePhases", lp.activePhases)
|
||||
lp.Lock()
|
||||
lp.measuredPhases = phases
|
||||
lp.Unlock()
|
||||
|
||||
lp.log.DEBUG.Printf("detected phases: %dp", phases)
|
||||
lp.publish("activePhases", phases)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
|
|
@ -106,11 +107,16 @@ func (lp *LoadPoint) GetPhases() int {
|
|||
|
||||
// SetPhases sets loadpoint enabled phases
|
||||
func (lp *LoadPoint) SetPhases(phases int) error {
|
||||
if _, ok := lp.charger.(api.ChargePhases); !ok {
|
||||
lp.setPhases(phases)
|
||||
return nil
|
||||
if phases != 1 && phases != 3 {
|
||||
return fmt.Errorf("invalid number of phases: %d", phases)
|
||||
}
|
||||
return lp.scalePhases(phases)
|
||||
|
||||
if _, ok := lp.charger.(api.ChargePhases); ok {
|
||||
return lp.scalePhases(phases)
|
||||
}
|
||||
|
||||
lp.setPhases(phases)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTargetCharge sets loadpoint charge targetSoC
|
||||
|
|
@ -209,9 +215,9 @@ func (lp *LoadPoint) GetMinPower() float64 {
|
|||
return Voltage * lp.GetMinCurrent()
|
||||
}
|
||||
|
||||
// GetMaxPower returns the max loadpoint power taking active phases into account
|
||||
// GetMaxPower returns the max loadpoint power taking vehicle capabilities and phase scaling into account
|
||||
func (lp *LoadPoint) GetMaxPower() float64 {
|
||||
return Voltage * lp.GetMaxCurrent() * float64(lp.GetPhases())
|
||||
return Voltage * lp.GetMaxCurrent() * float64(lp.maxActivePhases())
|
||||
}
|
||||
|
||||
// setRemainingDuration sets the estimated remaining charging duration
|
||||
|
|
|
|||
80
core/loadpoint_phases.go
Normal file
80
core/loadpoint_phases.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
)
|
||||
|
||||
// resetMeasuredPhases resets measured phases to unknown on vehicle disconnect, phase switch or phase api call
|
||||
func (lp *LoadPoint) resetMeasuredPhases() {
|
||||
lp.Lock()
|
||||
lp.measuredPhases = 0
|
||||
lp.Unlock()
|
||||
|
||||
lp.publish("activePhases", lp.activePhases())
|
||||
}
|
||||
|
||||
// getMeasuredPhases provides synchronized access to measuredPhases
|
||||
func (lp *LoadPoint) getMeasuredPhases() int {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
return lp.measuredPhases
|
||||
}
|
||||
|
||||
// assume 3p for switchable charger during startup
|
||||
const unknownPhases = 3
|
||||
|
||||
func min(i ...int) int {
|
||||
v := math.MaxInt
|
||||
for _, i := range i {
|
||||
if i < v {
|
||||
v = i
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func expect(phases int) int {
|
||||
if phases > 0 {
|
||||
return phases
|
||||
}
|
||||
return unknownPhases
|
||||
}
|
||||
|
||||
// activePhases returns the number of expectedly active phases for the meter.
|
||||
// If unknown for 1p3p chargers during startup it will assume 3p.
|
||||
func (lp *LoadPoint) activePhases() int {
|
||||
physical := lp.GetPhases()
|
||||
vehicle := lp.getVehiclePhases()
|
||||
measured := lp.getMeasuredPhases()
|
||||
|
||||
return min(expect(vehicle), expect(physical), expect(measured))
|
||||
}
|
||||
|
||||
// maxActivePhases returns the maximum number of active phases for the meter.
|
||||
func (lp *LoadPoint) maxActivePhases() int {
|
||||
physical := lp.GetPhases()
|
||||
measured := lp.getMeasuredPhases()
|
||||
vehicle := lp.getVehiclePhases()
|
||||
|
||||
// during 1p or unknown config, 1p measured is not a restriction
|
||||
if physical <= 1 || vehicle == 1 {
|
||||
measured = 0
|
||||
}
|
||||
|
||||
// if 1p3p supported then assume 3p
|
||||
if _, ok := lp.charger.(api.ChargePhases); ok {
|
||||
physical = 3
|
||||
}
|
||||
|
||||
return min(expect(vehicle), expect(physical), expect(measured))
|
||||
}
|
||||
|
||||
func (lp *LoadPoint) getVehiclePhases() int {
|
||||
if lp.vehicle != nil {
|
||||
return lp.vehicle.Phases()
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
296
core/loadpoint_phases_test.go
Normal file
296
core/loadpoint_phases_test.go
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
evbus "github.com/asaskevich/EventBus"
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/mock"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
type testCase struct {
|
||||
// capable=0 signals 1p3p as set during loadpoint init
|
||||
// physical/vehicle=0 signals unknown
|
||||
// measuredPhases<>0 signals previous measurement
|
||||
capable, physical, vehicle, measuredPhases, actExpected, maxExpected int
|
||||
// scaling expectation: d=down, u=up, du=both
|
||||
scale string
|
||||
}
|
||||
|
||||
var (
|
||||
phaseTests = []testCase{
|
||||
// 1p
|
||||
{1, 1, 0, 0, 1, 1, ""},
|
||||
{1, 1, 0, 1, 1, 1, ""},
|
||||
{1, 1, 1, 0, 1, 1, ""},
|
||||
{1, 1, 2, 0, 1, 1, ""},
|
||||
{1, 1, 3, 0, 1, 1, ""},
|
||||
// 3p
|
||||
{3, 3, 0, 0, unknownPhases, 3, ""},
|
||||
{3, 3, 0, 1, 1, 1, ""},
|
||||
{3, 3, 0, 2, 2, 2, ""},
|
||||
{3, 3, 0, 3, 3, 3, ""},
|
||||
{3, 3, 1, 0, 1, 1, ""},
|
||||
{3, 3, 2, 0, 2, 2, ""},
|
||||
{3, 3, 3, 0, 3, 3, ""},
|
||||
// 1p3p initial
|
||||
{0, 0, 0, 0, unknownPhases, 3, "du"},
|
||||
{0, 0, 0, 1, 1, 3, "u"},
|
||||
{0, 0, 0, 2, 2, 3, "du"},
|
||||
{0, 0, 0, 3, 3, 3, "du"},
|
||||
{0, 0, 1, 0, 1, 1, ""},
|
||||
{0, 0, 2, 0, 2, 2, "du"},
|
||||
{0, 0, 3, 0, 3, 3, "du"},
|
||||
// 1p3p, 1 currently active
|
||||
{0, 1, 0, 0, 1, 3, "u"},
|
||||
{0, 1, 0, 1, 1, 3, "u"},
|
||||
// {0, 1, 0, 2, 2,2,"u"}, // 2p active > 1p configured must not happen
|
||||
// {0, 1, 0, 3, 3,3,"u"}, // 3p active > 1p configured must not happen
|
||||
{0, 1, 1, 0, 1, 1, ""},
|
||||
{0, 1, 2, 0, 1, 2, "u"},
|
||||
{0, 1, 3, 0, 1, 3, "u"},
|
||||
// 1p3p, 3 currently active
|
||||
{0, 3, 0, 0, unknownPhases, 3, "d"},
|
||||
{0, 3, 0, 1, 1, 1, ""},
|
||||
{0, 3, 0, 2, 2, 2, "d"},
|
||||
{0, 3, 0, 3, 3, 3, "d"},
|
||||
{0, 3, 1, 0, 1, 1, ""},
|
||||
{0, 3, 2, 0, 2, 2, "d"},
|
||||
{0, 3, 3, 0, 3, 3, "d"},
|
||||
}
|
||||
)
|
||||
|
||||
func testScale(t *testing.T, lp *LoadPoint, power float64, direction string, tc testCase) {
|
||||
act := lp.activePhases()
|
||||
max := lp.maxActivePhases()
|
||||
scaled := lp.pvScalePhases(power, minA, maxA)
|
||||
|
||||
if strings.Contains(tc.scale, direction[0:1]) {
|
||||
if !scaled {
|
||||
t.Errorf("%v act=%d max=%d missing scale %s", tc, act, max, direction)
|
||||
}
|
||||
} else if scaled {
|
||||
t.Errorf("%v act=%d max=%d unexpected scale %s", tc, act, max, direction)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPvScalePhases(t *testing.T) {
|
||||
clock := clock.NewMock()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
for _, tc := range phaseTests {
|
||||
t.Log(tc)
|
||||
|
||||
plainCharger := mock.NewMockCharger(ctrl)
|
||||
plainCharger.EXPECT().Enabled().Return(true, nil)
|
||||
plainCharger.EXPECT().MaxCurrent(int64(minA)).Return(nil) // MaxCurrentEx not implemented
|
||||
|
||||
// 1p3p
|
||||
var phaseCharger *mock.MockChargePhases
|
||||
if tc.capable == 0 {
|
||||
phaseCharger = mock.NewMockChargePhases(ctrl)
|
||||
}
|
||||
|
||||
vehicle := mock.NewMockVehicle(ctrl)
|
||||
vehicle.EXPECT().Phases().Return(tc.vehicle).MinTimes(1)
|
||||
|
||||
lp := &LoadPoint{
|
||||
log: util.NewLogger("foo"),
|
||||
bus: evbus.New(),
|
||||
clock: clock,
|
||||
chargeMeter: &Null{}, // silence nil panics
|
||||
chargeRater: &Null{}, // silence nil panics
|
||||
chargeTimer: &Null{}, // silence nil panics
|
||||
progress: NewProgress(0, 10), // silence nil panics
|
||||
wakeUpTimer: NewTimer(), // silence nil panics
|
||||
Mode: api.ModeNow,
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
vehicle: vehicle,
|
||||
Phases: tc.physical,
|
||||
}
|
||||
|
||||
if phaseCharger != nil {
|
||||
lp.charger = struct {
|
||||
*mock.MockCharger
|
||||
*mock.MockChargePhases
|
||||
}{
|
||||
plainCharger, phaseCharger,
|
||||
}
|
||||
} else {
|
||||
lp.charger = struct {
|
||||
*mock.MockCharger
|
||||
}{
|
||||
plainCharger,
|
||||
}
|
||||
}
|
||||
|
||||
attachListeners(t, lp)
|
||||
|
||||
lp.measuredPhases = tc.measuredPhases
|
||||
if tc.measuredPhases > 0 && tc.vehicle > 0 {
|
||||
t.Fatalf("%v invalid test case", tc)
|
||||
}
|
||||
|
||||
if lp.Phases != tc.physical {
|
||||
t.Error("wrong phases", lp.Phases, tc.physical)
|
||||
}
|
||||
|
||||
if phs := lp.activePhases(); phs != tc.actExpected {
|
||||
t.Errorf("expected active %d, got %d", tc.actExpected, phs)
|
||||
}
|
||||
if phs := lp.maxActivePhases(); phs != tc.maxExpected {
|
||||
t.Errorf("expected max %d, got %d", tc.maxExpected, phs)
|
||||
}
|
||||
ctrl.Finish()
|
||||
|
||||
// scaling
|
||||
if phaseCharger != nil {
|
||||
// scale down
|
||||
min1p := 1 * minA * Voltage
|
||||
lp.phaseTimer = time.Time{}
|
||||
|
||||
plainCharger.EXPECT().Enable(false).Return(nil).MaxTimes(1)
|
||||
phaseCharger.EXPECT().Phases1p3p(1).Return(nil).MaxTimes(1)
|
||||
|
||||
testScale(t, lp, min1p, "down", tc)
|
||||
ctrl.Finish()
|
||||
|
||||
// scale up
|
||||
min3p := 3 * minA * Voltage
|
||||
lp.phaseTimer = time.Time{}
|
||||
lp.Phases = tc.physical // reset to initial state
|
||||
|
||||
plainCharger.EXPECT().Enable(false).Return(nil).MaxTimes(1)
|
||||
phaseCharger.EXPECT().Phases1p3p(3).Return(nil).MaxTimes(1)
|
||||
|
||||
testScale(t, lp, min3p, "up", tc)
|
||||
ctrl.Finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPvScalePhasesTimer(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
charger := &struct {
|
||||
*mock.MockCharger
|
||||
*mock.MockChargePhases
|
||||
}{
|
||||
mock.NewMockCharger(ctrl),
|
||||
mock.NewMockChargePhases(ctrl),
|
||||
}
|
||||
|
||||
dt := time.Minute
|
||||
Voltage = 230 // V
|
||||
|
||||
tc := []struct {
|
||||
desc string
|
||||
phases, measuredPhases int
|
||||
availablePower float64
|
||||
toPhases int
|
||||
res bool
|
||||
prepare func(lp *LoadPoint)
|
||||
}{
|
||||
// switch up from 1p/1p configured/active
|
||||
{"1/1->3, not enough power", 1, 1, 0, 1, false, nil},
|
||||
{"1/1->3, kickoff", 1, 1, 3 * Voltage * minA, 1, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = time.Time{}
|
||||
}},
|
||||
{"1/1->3, timer running", 1, 1, 3 * Voltage * minA, 1, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now()
|
||||
}},
|
||||
{"1/1->3, timer elapsed", 1, 1, 3 * Voltage * minA, 3, true, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now().Add(-dt)
|
||||
}},
|
||||
|
||||
// omit to switch up (again) from 3p/1p configured/active
|
||||
{"3/1->3, not enough power", 3, 1, 0, 3, false, nil},
|
||||
{"3/1->3, kickoff", 3, 1, 3 * Voltage * minA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = time.Time{}
|
||||
}},
|
||||
{"3/1->3, timer running", 3, 1, 3 * Voltage * minA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now()
|
||||
}},
|
||||
{"3/1->3, timer elapsed", 3, 1, 3 * Voltage * minA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now().Add(-dt)
|
||||
}},
|
||||
|
||||
// omit to switch down from 3p/1p configured/active
|
||||
{"3/1->1, not enough power", 3, 1, 0, 3, false, nil},
|
||||
{"3/1->1, kickoff", 3, 1, 1 * Voltage * minA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = time.Time{}
|
||||
}},
|
||||
{"3/1->1, timer running", 3, 1, 1 * Voltage * minA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now()
|
||||
}},
|
||||
{"3/1->1, timer elapsed", 3, 1, 1 * Voltage * minA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now().Add(-dt)
|
||||
}},
|
||||
|
||||
// switch down from 3p/3p configured/active
|
||||
{"3/3->1, enough power", 3, 3, 1 * Voltage * maxA, 3, false, nil},
|
||||
{"3/3->1, kickoff", 3, 3, 1 * Voltage * maxA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = time.Time{}
|
||||
}},
|
||||
{"3/3->1, timer running", 3, 3, 1 * Voltage * maxA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now()
|
||||
}},
|
||||
{"3/3->1, timer elapsed", 3, 3, 1 * Voltage * maxA, 1, true, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now().Add(-dt)
|
||||
}},
|
||||
|
||||
// error states from 1p/3p misconfig - no correction for time being (stay at 1p)
|
||||
{"1/3->1, enough power", 1, 3, 1 * Voltage * maxA, 1, false, nil},
|
||||
{"1/3->1, kickoff, correct phase setting", 1, 3, 1 * Voltage * maxA, 1, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = time.Time{}
|
||||
}},
|
||||
{"1/3->1, timer running, correct phase setting", 1, 3, 1 * Voltage * maxA, 1, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now()
|
||||
}},
|
||||
{"1/3->1, switch not executed", 1, 3, 1 * Voltage * maxA, 1, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now().Add(-dt)
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range tc {
|
||||
t.Logf("%+v", tc)
|
||||
clock := clock.NewMock()
|
||||
|
||||
lp := &LoadPoint{
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clock,
|
||||
charger: charger,
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
Phases: tc.phases,
|
||||
measuredPhases: tc.measuredPhases,
|
||||
Enable: ThresholdConfig{
|
||||
Delay: dt,
|
||||
},
|
||||
Disable: ThresholdConfig{
|
||||
Delay: dt,
|
||||
},
|
||||
}
|
||||
|
||||
if tc.prepare != nil {
|
||||
tc.prepare(lp)
|
||||
}
|
||||
|
||||
if tc.res {
|
||||
charger.MockChargePhases.EXPECT().Phases1p3p(tc.toPhases).Return(nil)
|
||||
}
|
||||
|
||||
if res := lp.pvScalePhases(tc.availablePower, minA, maxA); tc.res != res {
|
||||
t.Errorf("expected %v, got %v", tc.res, res)
|
||||
} else {
|
||||
if lp.Phases != tc.toPhases {
|
||||
t.Errorf("expected %dp, got %dp", tc.toPhases, lp.Phases)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -176,7 +176,8 @@ func TestUpdatePowerZero(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPVHysteresis(t *testing.T) {
|
||||
dt := time.Minute
|
||||
const dt = time.Minute
|
||||
const phases = 3
|
||||
type se struct {
|
||||
site float64
|
||||
delay time.Duration // test case delay since start
|
||||
|
|
@ -196,10 +197,10 @@ func TestPVHysteresis(t *testing.T) {
|
|||
}},
|
||||
// enable when threshold not configured but min power met
|
||||
{false, 0, 0, []se{
|
||||
{-6 * 100 * 10, 0, 0},
|
||||
{-6 * 100 * 10, 1, 0},
|
||||
{-6 * 100 * 10, dt - 1, 0},
|
||||
{-6 * 100 * 10, dt + 1, minA},
|
||||
{-6 * 100 * phases, 0, 0},
|
||||
{-6 * 100 * phases, 1, 0},
|
||||
{-6 * 100 * phases, dt - 1, 0},
|
||||
{-6 * 100 * phases, dt + 1, minA},
|
||||
}},
|
||||
// keep disabled when threshold not configured
|
||||
{false, 0, 0, []se{
|
||||
|
|
@ -216,11 +217,11 @@ func TestPVHysteresis(t *testing.T) {
|
|||
{-400, dt + 1, 0},
|
||||
}},
|
||||
// keep disabled when threshold (higher minCurrent) not met
|
||||
{false, -7 * 100 * 10, 0, []se{
|
||||
{-6 * 100 * 10, 0, 0},
|
||||
{-6 * 100 * 10, 1, 0},
|
||||
{-6 * 100 * 10, dt - 1, 0},
|
||||
{-6 * 100 * 10, dt + 1, 0},
|
||||
{false, -7 * 100 * phases, 0, []se{
|
||||
{-6 * 100 * phases, 0, 0},
|
||||
{-6 * 100 * phases, 1, 0},
|
||||
{-6 * 100 * phases, dt - 1, 0},
|
||||
{-6 * 100 * phases, dt + 1, 0},
|
||||
}},
|
||||
// enable when threshold met
|
||||
{false, -500, 0, []se{
|
||||
|
|
@ -231,17 +232,17 @@ func TestPVHysteresis(t *testing.T) {
|
|||
}},
|
||||
// keep enabled at max
|
||||
{true, 500, 0, []se{
|
||||
{-16 * 100 * 10, 0, maxA},
|
||||
{-16 * 100 * 10, 1, maxA},
|
||||
{-16 * 100 * 10, dt - 1, maxA},
|
||||
{-16 * 100 * 10, dt + 1, maxA},
|
||||
{-16 * 100 * phases, 0, maxA},
|
||||
{-16 * 100 * phases, 1, maxA},
|
||||
{-16 * 100 * phases, dt - 1, maxA},
|
||||
{-16 * 100 * phases, dt + 1, maxA},
|
||||
}},
|
||||
// keep enabled at min
|
||||
{true, 500, 0, []se{
|
||||
{-6 * 100 * 10, 0, minA},
|
||||
{-6 * 100 * 10, 1, minA},
|
||||
{-6 * 100 * 10, dt - 1, minA},
|
||||
{-6 * 100 * 10, dt + 1, minA},
|
||||
{-6 * 100 * phases, 0, minA},
|
||||
{-6 * 100 * phases, 1, minA},
|
||||
{-6 * 100 * phases, dt - 1, minA},
|
||||
{-6 * 100 * phases, dt + 1, minA},
|
||||
}},
|
||||
// keep enabled at min (negative threshold)
|
||||
{true, 0, 500, []se{
|
||||
|
|
@ -269,10 +270,10 @@ func TestPVHysteresis(t *testing.T) {
|
|||
// reset enable timer when threshold not met while timer active and threshold not configured
|
||||
{false, 0, 0, []se{
|
||||
{-6*100*10 - 1, dt + 1, 0},
|
||||
{-6 * 100 * 10, dt + 1, 0},
|
||||
{-6 * 100 * 10, dt + 2, 0},
|
||||
{-6 * 100 * 10, 2 * dt, 0},
|
||||
{-6 * 100 * 10, 2*dt + 1, minA},
|
||||
{-6 * 100 * phases, dt + 1, 0},
|
||||
{-6 * 100 * phases, dt + 2, 0},
|
||||
{-6 * 100 * phases, 2 * dt, 0},
|
||||
{-6 * 100 * phases, 2*dt + 1, minA},
|
||||
}},
|
||||
// reset disable timer when threshold not met while timer active
|
||||
{true, 0, 500, []se{
|
||||
|
|
@ -296,13 +297,13 @@ func TestPVHysteresis(t *testing.T) {
|
|||
|
||||
Voltage = 100
|
||||
lp := &LoadPoint{
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clck,
|
||||
charger: charger,
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
Phases: 10,
|
||||
activePhases: 10,
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clck,
|
||||
charger: charger,
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
Phases: phases,
|
||||
measuredPhases: phases,
|
||||
Enable: ThresholdConfig{
|
||||
Threshold: tc.enable,
|
||||
Delay: dt,
|
||||
|
|
@ -338,24 +339,26 @@ func TestPVHysteresis(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPVHysteresisForStatusOtherThanC(t *testing.T) {
|
||||
const phases = 3
|
||||
|
||||
clck := clock.NewMock()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
Voltage = 100
|
||||
lp := &LoadPoint{
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clck,
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
Phases: 10,
|
||||
activePhases: 10,
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clck,
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
Phases: phases,
|
||||
measuredPhases: phases,
|
||||
}
|
||||
|
||||
// not connected, test PV mode logic short-circuited
|
||||
lp.status = api.StatusA
|
||||
|
||||
// maxCurrent will read enabled state in PV mode
|
||||
sitePower := -float64(lp.Phases)*minA*Voltage + 1 // 1W below min power
|
||||
sitePower := -float64(phases)*minA*Voltage + 1 // 1W below min power
|
||||
current := lp.pvMaxCurrent(api.ModePV, sitePower, false)
|
||||
|
||||
if current != 0 {
|
||||
|
|
@ -373,6 +376,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
|
|||
|
||||
// wrap vehicle with estimator
|
||||
vehicle.EXPECT().Capacity().Return(int64(10))
|
||||
vehicle.EXPECT().Phases().Return(0).AnyTimes()
|
||||
socEstimator := soc.NewEstimator(util.NewLogger("foo"), charger, vehicle, false)
|
||||
|
||||
lp := &LoadPoint{
|
||||
|
|
@ -817,170 +821,3 @@ func TestVehicleDetectByID(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScalePhases(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
charger := &struct {
|
||||
*mock.MockCharger
|
||||
*mock.MockChargePhases
|
||||
}{
|
||||
mock.NewMockCharger(ctrl),
|
||||
mock.NewMockChargePhases(ctrl),
|
||||
}
|
||||
|
||||
dt := time.Minute
|
||||
Voltage = 230 // V
|
||||
|
||||
tc := []struct {
|
||||
desc string
|
||||
phases, activePhases int
|
||||
availablePower float64
|
||||
toPhases int
|
||||
res bool
|
||||
prepare func(lp *LoadPoint)
|
||||
}{
|
||||
// switch up from 1p/1p configured/active
|
||||
{"1/1->3, not enough power", 1, 1, 0, 1, false, nil},
|
||||
{"1/1->3, kickoff", 1, 1, 3 * Voltage * minA, 1, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = time.Time{}
|
||||
}},
|
||||
{"1/1->3, timer running", 1, 1, 3 * Voltage * minA, 1, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now()
|
||||
}},
|
||||
{"1/1->3, timer elapsed", 1, 1, 3 * Voltage * minA, 3, true, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now().Add(-dt)
|
||||
}},
|
||||
|
||||
// omit to switch up (again) from 3p/1p configured/active
|
||||
{"3/1->3, not enough power", 3, 1, 0, 3, false, nil},
|
||||
{"3/1->3, kickoff", 3, 1, 3 * Voltage * minA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = time.Time{}
|
||||
}},
|
||||
{"3/1->3, timer running", 3, 1, 3 * Voltage * minA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now()
|
||||
}},
|
||||
{"3/1->3, timer elapsed", 3, 1, 3 * Voltage * minA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now().Add(-dt)
|
||||
}},
|
||||
|
||||
// omit to switch down from 3p/1p configured/active
|
||||
{"3/1->1, not enough power", 3, 1, 0, 3, false, nil},
|
||||
{"3/1->1, kickoff", 3, 1, 1 * Voltage * minA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = time.Time{}
|
||||
}},
|
||||
{"3/1->1, timer running", 3, 1, 1 * Voltage * minA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now()
|
||||
}},
|
||||
{"3/1->1, timer elapsed", 3, 1, 1 * Voltage * minA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now().Add(-dt)
|
||||
}},
|
||||
|
||||
// switch down from 3p/3p configured/active
|
||||
{"3/3->1, enough power", 3, 3, 1 * Voltage * maxA, 3, false, nil},
|
||||
{"3/3->1, kickoff", 3, 3, 1 * Voltage * maxA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = time.Time{}
|
||||
}},
|
||||
{"3/3->1, timer running", 3, 3, 1 * Voltage * maxA, 3, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now()
|
||||
}},
|
||||
{"3/3->1, timer elapsed", 3, 3, 1 * Voltage * maxA, 1, true, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now().Add(-dt)
|
||||
}},
|
||||
|
||||
// error states from 1p/3p misconfig - no correction for time being (stay at 1p)
|
||||
{"1/3->1, enough power", 1, 3, 1 * Voltage * maxA, 1, false, nil},
|
||||
{"1/3->1, kickoff, correct phase setting", 1, 3, 1 * Voltage * maxA, 1, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = time.Time{}
|
||||
}},
|
||||
{"1/3->1, timer running, correct phase setting", 1, 3, 1 * Voltage * maxA, 1, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now()
|
||||
}},
|
||||
{"1/3->1, switch not executed", 1, 3, 1 * Voltage * maxA, 1, false, func(lp *LoadPoint) {
|
||||
lp.phaseTimer = lp.clock.Now().Add(-dt)
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range tc {
|
||||
t.Logf("%+v", tc)
|
||||
clock := clock.NewMock()
|
||||
|
||||
lp := &LoadPoint{
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clock,
|
||||
charger: charger,
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
Phases: tc.phases,
|
||||
activePhases: tc.activePhases,
|
||||
Enable: ThresholdConfig{
|
||||
Delay: dt,
|
||||
},
|
||||
Disable: ThresholdConfig{
|
||||
Delay: dt,
|
||||
},
|
||||
}
|
||||
|
||||
if tc.prepare != nil {
|
||||
tc.prepare(lp)
|
||||
}
|
||||
|
||||
if tc.res {
|
||||
charger.MockChargePhases.EXPECT().Phases1p3p(tc.toPhases).Return(nil)
|
||||
}
|
||||
|
||||
if res := lp.pvScalePhases(tc.availablePower, minA, maxA); tc.res != res {
|
||||
t.Errorf("expected %v, got %v", tc.res, res)
|
||||
} else {
|
||||
if lp.Phases != tc.toPhases {
|
||||
t.Errorf("expected %dp, got %dp", tc.toPhases, lp.Phases)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVehiclePhases(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
vehicle := &struct {
|
||||
*mock.MockVehicle
|
||||
*mock.MockVehiclePhases
|
||||
}{
|
||||
mock.NewMockVehicle(ctrl),
|
||||
mock.NewMockVehiclePhases(ctrl),
|
||||
}
|
||||
|
||||
tc := []struct {
|
||||
phases, activePhases, vehiclePhases int
|
||||
res int
|
||||
}{
|
||||
{1, 1, 0, 1}, // leave as-is
|
||||
{3, 1, 0, 1}, // leave as-is
|
||||
{3, 3, 0, 3}, // leave as-is
|
||||
{1, 1, 1, 1}, // leave as-is
|
||||
{3, 1, 1, 1}, // leave as-is
|
||||
{3, 3, 1, 1}, // limit to 1p
|
||||
{1, 1, 2, 1}, // leave as-is
|
||||
{3, 1, 2, 2}, // limit to 2p
|
||||
{3, 3, 2, 2}, // limit to 2p
|
||||
{1, 1, 3, 1}, // leave as-is
|
||||
{3, 1, 3, 3}, // limit to 3p
|
||||
{3, 3, 3, 3}, // leave as-is
|
||||
}
|
||||
|
||||
for _, tc := range tc {
|
||||
t.Logf("%+v", tc)
|
||||
|
||||
lp := &LoadPoint{
|
||||
log: util.NewLogger("foo"),
|
||||
vehicle: vehicle,
|
||||
Phases: tc.phases,
|
||||
activePhases: tc.activePhases,
|
||||
}
|
||||
|
||||
vehicle.MockVehiclePhases.EXPECT().Phases().Return(tc.vehiclePhases)
|
||||
|
||||
lp.setVehiclePhases()
|
||||
if lp.activePhases != tc.res {
|
||||
t.Errorf("expected %v, got %v", tc.res, lp.activePhases)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,VehiclePhases,ChargeRater,Battery)
|
||||
// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery)
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
|
@ -346,6 +346,20 @@ func (mr *MockVehicleMockRecorder) OnIdentified() *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnIdentified", reflect.TypeOf((*MockVehicle)(nil).OnIdentified))
|
||||
}
|
||||
|
||||
// Phases mocks base method.
|
||||
func (m *MockVehicle) Phases() int {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Phases")
|
||||
ret0, _ := ret[0].(int)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Phases indicates an expected call of Phases.
|
||||
func (mr *MockVehicleMockRecorder) Phases() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Phases", reflect.TypeOf((*MockVehicle)(nil).Phases))
|
||||
}
|
||||
|
||||
// SoC mocks base method.
|
||||
func (m *MockVehicle) SoC() (float64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
@ -375,43 +389,6 @@ func (mr *MockVehicleMockRecorder) Title() *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Title", reflect.TypeOf((*MockVehicle)(nil).Title))
|
||||
}
|
||||
|
||||
// MockVehiclePhases is a mock of VehiclePhases interface.
|
||||
type MockVehiclePhases struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockVehiclePhasesMockRecorder
|
||||
}
|
||||
|
||||
// MockVehiclePhasesMockRecorder is the mock recorder for MockVehiclePhases.
|
||||
type MockVehiclePhasesMockRecorder struct {
|
||||
mock *MockVehiclePhases
|
||||
}
|
||||
|
||||
// NewMockVehiclePhases creates a new mock instance.
|
||||
func NewMockVehiclePhases(ctrl *gomock.Controller) *MockVehiclePhases {
|
||||
mock := &MockVehiclePhases{ctrl: ctrl}
|
||||
mock.recorder = &MockVehiclePhasesMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockVehiclePhases) EXPECT() *MockVehiclePhasesMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Phases mocks base method.
|
||||
func (m *MockVehiclePhases) Phases() int {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Phases")
|
||||
ret0, _ := ret[0].(int)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Phases indicates an expected call of Phases.
|
||||
func (mr *MockVehiclePhasesMockRecorder) Phases() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Phases", reflect.TypeOf((*MockVehiclePhases)(nil).Phases))
|
||||
}
|
||||
|
||||
// MockChargeRater is a mock of ChargeRater interface.
|
||||
type MockChargeRater struct {
|
||||
ctrl *gomock.Controller
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ func New(w api.Vehicle, err error) (api.Vehicle, error) {
|
|||
return v, nil
|
||||
}
|
||||
|
||||
var _ api.Vehicle = (*Wrapper)(nil)
|
||||
|
||||
// Title implements the api.Vehicle interface
|
||||
func (v *Wrapper) Title() string {
|
||||
return "unavailable"
|
||||
|
|
@ -30,6 +32,11 @@ func (v *Wrapper) Capacity() int64 {
|
|||
return 0
|
||||
}
|
||||
|
||||
// Phases implements the api.Vehicle interface
|
||||
func (v *Wrapper) Phases() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Identifiers implements the api.Vehicle interface
|
||||
func (v *Wrapper) Identifiers() []string {
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue