Powerwall: add dynamic battery parameters (#32742)

This commit is contained in:
mfuchs1984 2026-08-12 15:55:19 +02:00 • committed by GitHub
parent fb18f33d78
commit 0ac70c7e39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 75 additions and 29 deletions

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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 }}

View file

@ -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 }}