evcc-io/meter/powerwall_fleet.go
2026-08-09 13:17:48 +02:00

114 lines
2.5 KiB
Go

package meter
import (
"errors"
"fmt"
"math"
"strconv"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/api/implement"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/vehicle/tesla"
teslaclient "github.com/evcc-io/tesla-proxy-client"
)
type fleetConfig struct {
powerWallConfig `mapstructure:",squash"`
tesla.FleetConfig `mapstructure:",squash"`
}
func init() {
registry.Add("powerwall-fleet", NewPowerWallFleetFromConfig)
}
// NewPowerWallFleetFromConfig creates a PowerWall meter with Fleet API battery control
func NewPowerWallFleetFromConfig(other map[string]any) (api.Meter, error) {
cc := fleetConfig{powerWallConfig: defaultPowerWallConfig()}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if err := cc.validate(); err != nil {
return nil, err
}
if err := cc.FleetConfig.Validate(); err != nil {
return nil, err
}
log := util.NewLogger("powerwall").Redact(
cc.User,
cc.Password,
cc.Credentials.ID,
cc.Credentials.Secret,
cc.Tokens.Access,
cc.Tokens.Refresh,
)
m, err := newPowerWall(log, cc.powerWallConfig)
if err != nil {
return nil, err
}
energySite, err := teslaEnergySite(log, cc.FleetConfig, cc.SiteId)
if err != nil {
return nil, err
}
implement.May(m, implement.BatteryController(cc.batterySocLimits.LimitController(func() (float64, error) {
ess, err := energySite.EnergySiteStatus()
if err != nil {
return 0, fmt.Errorf("get energy site status: %w", err)
}
return math.Round(ess.PercentageCharged), nil
}, func(limit float64) error {
return energySite.SetBatteryReserve(teslaReserveLimit(limit))
})))
return m, nil
}
// teslaReserveLimit clamps the reserve to values accepted by Tesla firmware: up to 80 or exactly 100
func teslaReserveLimit(limit float64) uint64 {
switch {
case limit >= 100:
return 100
case limit > 80:
return 80
case limit > 0:
return uint64(limit)
default: // negative or NaN
return 0
}
}
func teslaEnergySite(log *util.Logger, config tesla.FleetConfig, siteId int64) (*teslaclient.EnergySite, error) {
fleet, err := config.Client(log)
if err != nil {
return nil, err
}
tc := fleet.Client
if siteId == 0 {
products, err := tc.Products()
if err != nil {
return nil, fmt.Errorf("discover energy sites: %w", err)
}
for _, p := range products {
if p.EnergySiteId != 0 {
siteId = p.EnergySiteId
break
}
}
if siteId == 0 {
return nil, errors.New("no energy site found")
}
}
log.Redact(strconv.FormatInt(siteId, 10))
return tc.EnergySite(siteId)
}