114 lines
2.5 KiB
Go
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)
|
|
}
|