Powerwall: add dynamic battery parameters (#32742)
This commit is contained in:
parent
fb18f33d78
commit
0ac70c7e39
5 changed files with 75 additions and 29 deletions
|
|
@ -25,23 +25,18 @@ type PowerWall struct {
|
||||||
type powerWallConfig struct {
|
type powerWallConfig struct {
|
||||||
URI, Usage, User, Password string
|
URI, Usage, User, Password string
|
||||||
Cache time.Duration
|
Cache time.Duration
|
||||||
SiteId int64 // Fleet API only, deprecated for the local meter
|
SiteId int64 // Fleet API only, deprecated for the local meter
|
||||||
RefreshToken_ string `mapstructure:"refreshToken"` // TODO deprecated
|
RefreshToken_ string `mapstructure:"refreshToken"` // TODO deprecated
|
||||||
batterySocLimits `mapstructure:",squash"`
|
MinSoc float64 // Fleet API only, backup reserve restored in normal mode
|
||||||
batteryPowerLimits `mapstructure:",squash"`
|
MaxSoc_ any `mapstructure:"maxsoc"` // TODO deprecated
|
||||||
|
MaxChargePower_ any `mapstructure:"maxchargepower"` // TODO deprecated
|
||||||
|
MaxDischargePower_ any `mapstructure:"maxdischargepower"` // TODO deprecated
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultPowerWallConfig() powerWallConfig {
|
func defaultPowerWallConfig() powerWallConfig {
|
||||||
return powerWallConfig{
|
return powerWallConfig{
|
||||||
batterySocLimits: batterySocLimits{
|
Cache: time.Second,
|
||||||
MinSoc: 20,
|
MinSoc: 20,
|
||||||
MaxSoc: 95,
|
|
||||||
},
|
|
||||||
batteryPowerLimits: batteryPowerLimits{
|
|
||||||
MaxChargePower: 4600,
|
|
||||||
MaxDischargePower: 4600,
|
|
||||||
},
|
|
||||||
Cache: time.Second,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,18 +109,36 @@ func newPowerWall(log *util.Logger, cc powerWallConfig) (*PowerWall, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.usage == "battery" {
|
if m.usage == "battery" {
|
||||||
implement.Has(m, implement.Battery(m.batterySoc))
|
opG := util.Cached(client.GetOperation, cc.Cache)
|
||||||
implement.May(m, implement.BatterySocLimiter(cc.batterySocLimits.Decorator()))
|
|
||||||
implement.May(m, implement.BatteryPowerLimiter(cc.batteryPowerLimits.Decorator()))
|
|
||||||
|
|
||||||
res, err := m.client.GetSystemStatus()
|
// capacity and power limits are static, reading them validates connectivity
|
||||||
|
status, err := client.GetSystemStatus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implement.Has(m, implement.Battery(m.batterySoc))
|
||||||
|
|
||||||
implement.Has(m, implement.BatteryCapacity(func() float64 {
|
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
|
return m, nil
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,10 @@ func NewPowerWallFleetFromConfig(other map[string]any) (api.Meter, error) {
|
||||||
return nil, err
|
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()
|
ess, err := energySite.EnergySiteStatus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("get energy site status: %w", err)
|
return 0, fmt.Errorf("get energy site status: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,38 @@ func TestPowerWallConfigDeprecatedParams(t *testing.T) {
|
||||||
decodePowerWallConfig(t, map[string]any{
|
decodePowerWallConfig(t, map[string]any{
|
||||||
"usage": "battery", "password": "secret",
|
"usage": "battery", "password": "secret",
|
||||||
"refreshToken": "token", "siteId": 123,
|
"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) {
|
func TestNewPowerWallFleetFromConfigValidation(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,17 @@ params:
|
||||||
en: optional product identifier of the energy site, use to override autodetection
|
en: optional product identifier of the energy site, use to override autodetection
|
||||||
de: optionale Product ID dieser Energy Site, zum Übersteuern der automatischen Erkennung
|
de: optionale Product ID dieser Energy Site, zum Übersteuern der automatischen Erkennung
|
||||||
- name: minsoc
|
- name: minsoc
|
||||||
|
default: 20
|
||||||
advanced: true
|
advanced: true
|
||||||
|
help:
|
||||||
|
de: Backup-Reserve, die evcc im Normalbetrieb wiederherstellt
|
||||||
|
en: Backup reserve evcc restores in normal operation
|
||||||
- name: maxsoc
|
- name: maxsoc
|
||||||
advanced: true
|
deprecated: true
|
||||||
- name: maxchargepower
|
- name: maxchargepower
|
||||||
|
deprecated: true
|
||||||
- name: maxdischargepower
|
- name: maxdischargepower
|
||||||
|
deprecated: true
|
||||||
render: |
|
render: |
|
||||||
type: powerwall-fleet
|
type: powerwall-fleet
|
||||||
uri: {{ .host }}
|
uri: {{ .host }}
|
||||||
|
|
@ -65,6 +71,3 @@ render: |
|
||||||
refresh: {{ .refreshToken }}
|
refresh: {{ .refreshToken }}
|
||||||
siteId: {{ .siteId }}
|
siteId: {{ .siteId }}
|
||||||
minsoc: {{ .minsoc }}
|
minsoc: {{ .minsoc }}
|
||||||
maxsoc: {{ .maxsoc }}
|
|
||||||
maxchargepower: {{ .maxchargepower }}
|
|
||||||
maxdischargepower: {{ .maxdischargepower }}
|
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,16 @@ params:
|
||||||
- name: siteId
|
- name: siteId
|
||||||
deprecated: true
|
deprecated: true
|
||||||
- name: minsoc
|
- name: minsoc
|
||||||
advanced: true
|
deprecated: true
|
||||||
- name: maxsoc
|
- name: maxsoc
|
||||||
advanced: true
|
deprecated: true
|
||||||
- name: maxchargepower
|
- name: maxchargepower
|
||||||
|
deprecated: true
|
||||||
- name: maxdischargepower
|
- name: maxdischargepower
|
||||||
|
deprecated: true
|
||||||
render: |
|
render: |
|
||||||
type: powerwall
|
type: powerwall
|
||||||
uri: {{ .host }}
|
uri: {{ .host }}
|
||||||
usage: {{ .usage }}
|
usage: {{ .usage }}
|
||||||
user: customer
|
user: customer
|
||||||
password: {{ .password }} # for 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