Powerwall: add dynamic battery parameters (#32742)
This commit is contained in:
parent
fb18f33d78
commit
0ac70c7e39
5 changed files with 75 additions and 29 deletions
|
|
@ -27,21 +27,16 @@ type powerWallConfig struct {
|
|||
Cache time.Duration
|
||||
SiteId int64 // Fleet API only, deprecated for the local meter
|
||||
RefreshToken_ string `mapstructure:"refreshToken"` // TODO deprecated
|
||||
batterySocLimits `mapstructure:",squash"`
|
||||
batteryPowerLimits `mapstructure:",squash"`
|
||||
MinSoc float64 // Fleet API only, backup reserve restored in normal mode
|
||||
MaxSoc_ any `mapstructure:"maxsoc"` // TODO deprecated
|
||||
MaxChargePower_ any `mapstructure:"maxchargepower"` // TODO deprecated
|
||||
MaxDischargePower_ any `mapstructure:"maxdischargepower"` // TODO deprecated
|
||||
}
|
||||
|
||||
func defaultPowerWallConfig() powerWallConfig {
|
||||
return powerWallConfig{
|
||||
batterySocLimits: batterySocLimits{
|
||||
MinSoc: 20,
|
||||
MaxSoc: 95,
|
||||
},
|
||||
batteryPowerLimits: batteryPowerLimits{
|
||||
MaxChargePower: 4600,
|
||||
MaxDischargePower: 4600,
|
||||
},
|
||||
Cache: time.Second,
|
||||
MinSoc: 20,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,18 +109,36 @@ func newPowerWall(log *util.Logger, cc powerWallConfig) (*PowerWall, error) {
|
|||
}
|
||||
|
||||
if m.usage == "battery" {
|
||||
implement.Has(m, implement.Battery(m.batterySoc))
|
||||
implement.May(m, implement.BatterySocLimiter(cc.batterySocLimits.Decorator()))
|
||||
implement.May(m, implement.BatteryPowerLimiter(cc.batteryPowerLimits.Decorator()))
|
||||
opG := util.Cached(client.GetOperation, cc.Cache)
|
||||
|
||||
res, err := m.client.GetSystemStatus()
|
||||
// capacity and power limits are static, reading them validates connectivity
|
||||
status, err := client.GetSystemStatus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
implement.Has(m, implement.Battery(m.batterySoc))
|
||||
|
||||
implement.Has(m, implement.BatteryCapacity(func() float64 {
|
||||
return res.NominalFullPackEnergy / 1e3
|
||||
return status.NominalFullPackEnergy / 1e3
|
||||
}))
|
||||
|
||||
// backup reserve is the lower discharge limit, the powerwall has no upper soc limit
|
||||
implement.Has(m, implement.BatterySocLimiter(func() (float64, float64) {
|
||||
op, err := opG()
|
||||
if err != nil {
|
||||
log.ERROR.Println("battery soc limits:", err)
|
||||
return 0, 100
|
||||
}
|
||||
return op.BackupReservePercent, 100
|
||||
}))
|
||||
|
||||
if status.MaxApparentPower > 0 {
|
||||
// inverter apparent power applies to charging and discharging alike
|
||||
implement.Has(m, implement.BatteryPowerLimiter(func() (float64, float64) {
|
||||
return status.MaxApparentPower, status.MaxApparentPower
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
|
|
|
|||
|
|
@ -55,7 +55,10 @@ func NewPowerWallFleetFromConfig(other map[string]any) (api.Meter, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
implement.May(m, implement.BatteryController(cc.batterySocLimits.LimitController(func() (float64, error) {
|
||||
// charge mode raises the reserve to full, normal mode restores the configured minimum
|
||||
limits := batterySocLimits{MinSoc: cc.MinSoc, MaxSoc: 100}
|
||||
|
||||
implement.Has(m, implement.BatteryController(limits.LimitController(func() (float64, error) {
|
||||
ess, err := energySite.EnergySiteStatus()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("get energy site status: %w", err)
|
||||
|
|
|
|||
|
|
@ -70,9 +70,38 @@ func TestPowerWallConfigDeprecatedParams(t *testing.T) {
|
|||
decodePowerWallConfig(t, map[string]any{
|
||||
"usage": "battery", "password": "secret",
|
||||
"refreshToken": "token", "siteId": 123,
|
||||
"maxsoc": 95, "maxchargepower": 4600, "maxdischargepower": 4600,
|
||||
})
|
||||
}
|
||||
|
||||
// battery control must fall back to the default reserve, not to zero
|
||||
func TestPowerWallFleetReserve(t *testing.T) {
|
||||
cc := decodePowerWallConfig(t, map[string]any{"usage": "battery", "password": "secret"})
|
||||
limits := batterySocLimits{MinSoc: cc.MinSoc, MaxSoc: 100}
|
||||
|
||||
var reserve float64
|
||||
ctrl := limits.LimitController(
|
||||
func() (float64, error) { return 50, nil },
|
||||
func(limit float64) error { reserve = limit; return nil },
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
mode api.BatteryMode
|
||||
want float64
|
||||
}{
|
||||
{mode: api.BatteryNormal, want: 20},
|
||||
{mode: api.BatteryHold, want: 50},
|
||||
{mode: api.BatteryCharge, want: 100},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.mode.String(), func(t *testing.T) {
|
||||
require.NoError(t, ctrl(tc.mode))
|
||||
assert.Equal(t, tc.want, reserve)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPowerWallFleetFromConfigValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -47,11 +47,17 @@ params:
|
|||
en: optional product identifier of the energy site, use to override autodetection
|
||||
de: optionale Product ID dieser Energy Site, zum Übersteuern der automatischen Erkennung
|
||||
- name: minsoc
|
||||
default: 20
|
||||
advanced: true
|
||||
help:
|
||||
de: Backup-Reserve, die evcc im Normalbetrieb wiederherstellt
|
||||
en: Backup reserve evcc restores in normal operation
|
||||
- name: maxsoc
|
||||
advanced: true
|
||||
deprecated: true
|
||||
- name: maxchargepower
|
||||
deprecated: true
|
||||
- name: maxdischargepower
|
||||
deprecated: true
|
||||
render: |
|
||||
type: powerwall-fleet
|
||||
uri: {{ .host }}
|
||||
|
|
@ -65,6 +71,3 @@ render: |
|
|||
refresh: {{ .refreshToken }}
|
||||
siteId: {{ .siteId }}
|
||||
minsoc: {{ .minsoc }}
|
||||
maxsoc: {{ .maxsoc }}
|
||||
maxchargepower: {{ .maxchargepower }}
|
||||
maxdischargepower: {{ .maxdischargepower }}
|
||||
|
|
|
|||
|
|
@ -17,18 +17,16 @@ params:
|
|||
- name: siteId
|
||||
deprecated: true
|
||||
- name: minsoc
|
||||
advanced: true
|
||||
deprecated: true
|
||||
- name: maxsoc
|
||||
advanced: true
|
||||
deprecated: true
|
||||
- name: maxchargepower
|
||||
deprecated: true
|
||||
- name: maxdischargepower
|
||||
deprecated: true
|
||||
render: |
|
||||
type: powerwall
|
||||
uri: {{ .host }}
|
||||
usage: {{ .usage }}
|
||||
user: customer
|
||||
password: {{ .password }} # for user 'customer'
|
||||
minsoc: {{ .minsoc }}
|
||||
maxsoc: {{ .maxsoc }}
|
||||
maxchargepower: {{ .maxchargepower }}
|
||||
maxdischargepower: {{ .maxdischargepower }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue