Polestar: add gRPC vehicle implementation (#30119)
This commit is contained in:
parent
8dc0edd8d9
commit
0847c2a046
9 changed files with 1138 additions and 1 deletions
|
|
@ -8,5 +8,5 @@ params:
|
||||||
- name: vin
|
- name: vin
|
||||||
example: LPSVS...
|
example: LPSVS...
|
||||||
render: |
|
render: |
|
||||||
type: polestar
|
type: polestar-grpc
|
||||||
{{ include "vehicle-base" . }}
|
{{ include "vehicle-base" . }}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package polestar
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
"github.com/evcc-io/evcc/util"
|
"github.com/evcc-io/evcc/util"
|
||||||
"github.com/evcc-io/evcc/util/request"
|
"github.com/evcc-io/evcc/util/request"
|
||||||
"github.com/hasura/go-graphql-client"
|
"github.com/hasura/go-graphql-client"
|
||||||
|
|
@ -82,3 +83,29 @@ func (v *API) CarTelemetry(ctx context.Context, vin string) (CarTelemetryData, e
|
||||||
|
|
||||||
return filteredData, err
|
return filteredData, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Odometer returns the odometer reading in km for the given VIN.
|
||||||
|
// The gRPC battery service does not expose odometer data, so it is queried
|
||||||
|
// via GraphQL. Only the odometer field is requested, avoiding the removed
|
||||||
|
// battery chargingStatus field that breaks the full telematics query.
|
||||||
|
func (v *API) Odometer(ctx context.Context, vin string) (float64, error) {
|
||||||
|
var res struct {
|
||||||
|
CarTelematicsV2 struct {
|
||||||
|
Odometer []OdometerData
|
||||||
|
} `graphql:"carTelematicsV2(vins: $vins)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := v.client.Query(ctx, &res, map[string]any{
|
||||||
|
"vins": []string{vin},
|
||||||
|
}, graphql.OperationName("CarTelematicsV2")); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range res.CarTelematicsV2.Odometer {
|
||||||
|
if o.VIN == vin {
|
||||||
|
return float64(o.OdometerMeters) / 1e3, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, api.ErrNotAvailable
|
||||||
|
}
|
||||||
|
|
|
||||||
147
vehicle/polestar/grpc.go
Normal file
147
vehicle/polestar/grpc.go
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
package polestar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
"github.com/evcc-io/evcc/util"
|
||||||
|
"github.com/evcc-io/evcc/util/request"
|
||||||
|
"github.com/evcc-io/evcc/vehicle/polestar/pb"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Polestar gRPC battery API.
|
||||||
|
//
|
||||||
|
// Polestar removed the chargingStatus field from the GraphQL API. The gRPC
|
||||||
|
// battery service still exposes the charger connection and charging status.
|
||||||
|
// Protocol reconstructed by the pypolestar project, see
|
||||||
|
// https://github.com/pypolestar/pypolestar.
|
||||||
|
const (
|
||||||
|
// c3DiscoveryURI resolves the dynamic gRPC host of the battery service
|
||||||
|
c3DiscoveryURI = "https://cnepmob.volvocars.com"
|
||||||
|
c3DiscoveryAccept = "application/volvo.cloud.cnepmob.v1+json"
|
||||||
|
|
||||||
|
// defaultGrpcPort is used when discovery does not return a port
|
||||||
|
defaultGrpcPort = 443
|
||||||
|
)
|
||||||
|
|
||||||
|
// GrpcAPI is the Polestar gRPC battery API client
|
||||||
|
type GrpcAPI struct {
|
||||||
|
client pb.BatteryServiceClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGrpcAPI creates a Polestar gRPC battery API client. The token source is
|
||||||
|
// shared with the GraphQL API so both reuse the same OAuth2 session.
|
||||||
|
func NewGrpcAPI(log *util.Logger, ts oauth2.TokenSource) (*GrpcAPI, error) {
|
||||||
|
host, err := discoverC3Host(log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("c3 discovery: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := grpc.NewClient(host,
|
||||||
|
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})),
|
||||||
|
grpc.WithPerRPCCredentials(tokenCredentials{ts}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GrpcAPI{
|
||||||
|
client: pb.NewBatteryServiceClient(conn),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenCredentials adapts an oauth2.TokenSource to grpc.PerRPCCredentials,
|
||||||
|
// adding the bearer token as gRPC authorization metadata on every call.
|
||||||
|
type tokenCredentials struct {
|
||||||
|
oauth2.TokenSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c tokenCredentials) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
|
||||||
|
token, err := c.Token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]string{
|
||||||
|
"authorization": token.Type() + " " + token.AccessToken,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tokenCredentials) RequireTransportSecurity() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// discoverC3Host resolves the C3 gRPC host:port via the cnepmob discovery endpoint
|
||||||
|
func discoverC3Host(log *util.Logger) (string, error) {
|
||||||
|
req, err := request.New(http.MethodGet, c3DiscoveryURI, nil, map[string]string{
|
||||||
|
"Accept": c3DiscoveryAccept,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res struct {
|
||||||
|
C3 struct {
|
||||||
|
GrpcHost string `json:"grpcHost"`
|
||||||
|
GrpcPort int `json:"grpcPort"`
|
||||||
|
} `json:"c3"`
|
||||||
|
}
|
||||||
|
if err := request.NewHelper(log).DoJSON(req, &res); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.C3.GrpcHost == "" {
|
||||||
|
return "", errors.New("missing grpc host")
|
||||||
|
}
|
||||||
|
|
||||||
|
port := res.C3.GrpcPort
|
||||||
|
if port == 0 {
|
||||||
|
port = defaultGrpcPort
|
||||||
|
}
|
||||||
|
|
||||||
|
return net.JoinHostPort(res.C3.GrpcHost, strconv.Itoa(port)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Battery returns the latest battery state for the given VIN
|
||||||
|
func (v *GrpcAPI) Battery(ctx context.Context, vin string) (*pb.Battery, error) {
|
||||||
|
ctx = metadata.AppendToOutgoingContext(ctx, "vin", vin)
|
||||||
|
|
||||||
|
res, err := v.client.GetLatestBattery(ctx, &pb.GetBatteryRequest{
|
||||||
|
Id: uuid.NewString(),
|
||||||
|
Vin: vin,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, retryable(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
battery := res.GetBattery()
|
||||||
|
if battery == nil {
|
||||||
|
return nil, api.ErrNotAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
return battery, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// retryable marks transient gRPC errors as api.ErrMustRetry so util.Cached
|
||||||
|
// retries them on the next cycle instead of caching the failure and delaying
|
||||||
|
// recovery via exponential back-off.
|
||||||
|
func retryable(err error) error {
|
||||||
|
switch status.Code(err) {
|
||||||
|
case codes.Unavailable, codes.DeadlineExceeded:
|
||||||
|
return fmt.Errorf("%w: %w", api.ErrMustRetry, err)
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
505
vehicle/polestar/pb/polestar.pb.go
Normal file
505
vehicle/polestar/pb/polestar.pb.go
Normal file
|
|
@ -0,0 +1,505 @@
|
||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.36.11
|
||||||
|
// protoc v7.34.1
|
||||||
|
// source: polestar.proto
|
||||||
|
|
||||||
|
// Polestar gRPC API (PCCS vehicle battery service).
|
||||||
|
//
|
||||||
|
// Polestar removed the chargingStatus field from the GraphQL carTelematicsV2
|
||||||
|
// API. The gRPC battery service still exposes the charger connection and
|
||||||
|
// charging status. Protocol reconstructed by the pypolestar project from the
|
||||||
|
// Polestar Android app, see https://github.com/pypolestar/pypolestar.
|
||||||
|
//
|
||||||
|
// The proto package name is significant: it forms the gRPC method path
|
||||||
|
// (/services.vehiclestates.battery.BatteryService/GetLatestBattery) expected
|
||||||
|
// by the server.
|
||||||
|
//
|
||||||
|
// Regenerate with:
|
||||||
|
// protoc --go_out=. --go_opt=paths=source_relative \
|
||||||
|
// --go-grpc_out=. --go-grpc_opt=paths=source_relative polestar.proto
|
||||||
|
|
||||||
|
package pb
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
unsafe "unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChargerConnectionStatus int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChargerConnectionStatus_CHARGER_CONNECTION_STATUS_UNSPECIFIED ChargerConnectionStatus = 0
|
||||||
|
ChargerConnectionStatus_CHARGER_CONNECTION_STATUS_CONNECTED ChargerConnectionStatus = 1
|
||||||
|
ChargerConnectionStatus_CHARGER_CONNECTION_STATUS_DISCONNECTED ChargerConnectionStatus = 2
|
||||||
|
ChargerConnectionStatus_CHARGER_CONNECTION_STATUS_FAULT ChargerConnectionStatus = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enum value maps for ChargerConnectionStatus.
|
||||||
|
var (
|
||||||
|
ChargerConnectionStatus_name = map[int32]string{
|
||||||
|
0: "CHARGER_CONNECTION_STATUS_UNSPECIFIED",
|
||||||
|
1: "CHARGER_CONNECTION_STATUS_CONNECTED",
|
||||||
|
2: "CHARGER_CONNECTION_STATUS_DISCONNECTED",
|
||||||
|
3: "CHARGER_CONNECTION_STATUS_FAULT",
|
||||||
|
}
|
||||||
|
ChargerConnectionStatus_value = map[string]int32{
|
||||||
|
"CHARGER_CONNECTION_STATUS_UNSPECIFIED": 0,
|
||||||
|
"CHARGER_CONNECTION_STATUS_CONNECTED": 1,
|
||||||
|
"CHARGER_CONNECTION_STATUS_DISCONNECTED": 2,
|
||||||
|
"CHARGER_CONNECTION_STATUS_FAULT": 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x ChargerConnectionStatus) Enum() *ChargerConnectionStatus {
|
||||||
|
p := new(ChargerConnectionStatus)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ChargerConnectionStatus) String() string {
|
||||||
|
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ChargerConnectionStatus) Descriptor() protoreflect.EnumDescriptor {
|
||||||
|
return file_polestar_proto_enumTypes[0].Descriptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ChargerConnectionStatus) Type() protoreflect.EnumType {
|
||||||
|
return &file_polestar_proto_enumTypes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ChargerConnectionStatus) Number() protoreflect.EnumNumber {
|
||||||
|
return protoreflect.EnumNumber(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ChargerConnectionStatus.Descriptor instead.
|
||||||
|
func (ChargerConnectionStatus) EnumDescriptor() ([]byte, []int) {
|
||||||
|
return file_polestar_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChargingStatus int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChargingStatus_CHARGING_STATUS_UNSPECIFIED ChargingStatus = 0
|
||||||
|
ChargingStatus_CHARGING_STATUS_CHARGING ChargingStatus = 1
|
||||||
|
ChargingStatus_CHARGING_STATUS_IDLE ChargingStatus = 2
|
||||||
|
ChargingStatus_CHARGING_STATUS_DONE ChargingStatus = 3
|
||||||
|
ChargingStatus_CHARGING_STATUS_FAULT ChargingStatus = 4
|
||||||
|
ChargingStatus_CHARGING_STATUS_SCHEDULED ChargingStatus = 5
|
||||||
|
ChargingStatus_CHARGING_STATUS_DISCHARGING ChargingStatus = 6
|
||||||
|
ChargingStatus_CHARGING_STATUS_ERROR ChargingStatus = 7
|
||||||
|
ChargingStatus_CHARGING_STATUS_SMART_CHARGING ChargingStatus = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enum value maps for ChargingStatus.
|
||||||
|
var (
|
||||||
|
ChargingStatus_name = map[int32]string{
|
||||||
|
0: "CHARGING_STATUS_UNSPECIFIED",
|
||||||
|
1: "CHARGING_STATUS_CHARGING",
|
||||||
|
2: "CHARGING_STATUS_IDLE",
|
||||||
|
3: "CHARGING_STATUS_DONE",
|
||||||
|
4: "CHARGING_STATUS_FAULT",
|
||||||
|
5: "CHARGING_STATUS_SCHEDULED",
|
||||||
|
6: "CHARGING_STATUS_DISCHARGING",
|
||||||
|
7: "CHARGING_STATUS_ERROR",
|
||||||
|
8: "CHARGING_STATUS_SMART_CHARGING",
|
||||||
|
}
|
||||||
|
ChargingStatus_value = map[string]int32{
|
||||||
|
"CHARGING_STATUS_UNSPECIFIED": 0,
|
||||||
|
"CHARGING_STATUS_CHARGING": 1,
|
||||||
|
"CHARGING_STATUS_IDLE": 2,
|
||||||
|
"CHARGING_STATUS_DONE": 3,
|
||||||
|
"CHARGING_STATUS_FAULT": 4,
|
||||||
|
"CHARGING_STATUS_SCHEDULED": 5,
|
||||||
|
"CHARGING_STATUS_DISCHARGING": 6,
|
||||||
|
"CHARGING_STATUS_ERROR": 7,
|
||||||
|
"CHARGING_STATUS_SMART_CHARGING": 8,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x ChargingStatus) Enum() *ChargingStatus {
|
||||||
|
p := new(ChargingStatus)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ChargingStatus) String() string {
|
||||||
|
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ChargingStatus) Descriptor() protoreflect.EnumDescriptor {
|
||||||
|
return file_polestar_proto_enumTypes[1].Descriptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ChargingStatus) Type() protoreflect.EnumType {
|
||||||
|
return &file_polestar_proto_enumTypes[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ChargingStatus) Number() protoreflect.EnumNumber {
|
||||||
|
return protoreflect.EnumNumber(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ChargingStatus.Descriptor instead.
|
||||||
|
func (ChargingStatus) EnumDescriptor() ([]byte, []int) {
|
||||||
|
return file_polestar_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Timestamp struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Seconds int64 `protobuf:"varint,1,opt,name=seconds,proto3" json:"seconds,omitempty"`
|
||||||
|
Nanos int32 `protobuf:"varint,2,opt,name=nanos,proto3" json:"nanos,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Timestamp) Reset() {
|
||||||
|
*x = Timestamp{}
|
||||||
|
mi := &file_polestar_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Timestamp) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Timestamp) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Timestamp) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_polestar_proto_msgTypes[0]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Timestamp.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Timestamp) Descriptor() ([]byte, []int) {
|
||||||
|
return file_polestar_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Timestamp) GetSeconds() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Seconds
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Timestamp) GetNanos() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Nanos
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type Battery struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Timestamp *Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||||
|
BatteryChargeLevelPercentage float64 `protobuf:"fixed64,2,opt,name=battery_charge_level_percentage,json=batteryChargeLevelPercentage,proto3" json:"battery_charge_level_percentage,omitempty"`
|
||||||
|
EstimatedDistanceToEmptyKm int32 `protobuf:"varint,4,opt,name=estimated_distance_to_empty_km,json=estimatedDistanceToEmptyKm,proto3" json:"estimated_distance_to_empty_km,omitempty"`
|
||||||
|
EstimatedChargingTimeToFullMinutes int32 `protobuf:"varint,5,opt,name=estimated_charging_time_to_full_minutes,json=estimatedChargingTimeToFullMinutes,proto3" json:"estimated_charging_time_to_full_minutes,omitempty"`
|
||||||
|
ChargerConnectionStatus ChargerConnectionStatus `protobuf:"varint,6,opt,name=charger_connection_status,json=chargerConnectionStatus,proto3,enum=services.vehiclestates.battery.ChargerConnectionStatus" json:"charger_connection_status,omitempty"`
|
||||||
|
ChargingStatus ChargingStatus `protobuf:"varint,7,opt,name=charging_status,json=chargingStatus,proto3,enum=services.vehiclestates.battery.ChargingStatus" json:"charging_status,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Battery) Reset() {
|
||||||
|
*x = Battery{}
|
||||||
|
mi := &file_polestar_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Battery) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Battery) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Battery) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_polestar_proto_msgTypes[1]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Battery.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Battery) Descriptor() ([]byte, []int) {
|
||||||
|
return file_polestar_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Battery) GetTimestamp() *Timestamp {
|
||||||
|
if x != nil {
|
||||||
|
return x.Timestamp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Battery) GetBatteryChargeLevelPercentage() float64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.BatteryChargeLevelPercentage
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Battery) GetEstimatedDistanceToEmptyKm() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.EstimatedDistanceToEmptyKm
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Battery) GetEstimatedChargingTimeToFullMinutes() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.EstimatedChargingTimeToFullMinutes
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Battery) GetChargerConnectionStatus() ChargerConnectionStatus {
|
||||||
|
if x != nil {
|
||||||
|
return x.ChargerConnectionStatus
|
||||||
|
}
|
||||||
|
return ChargerConnectionStatus_CHARGER_CONNECTION_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Battery) GetChargingStatus() ChargingStatus {
|
||||||
|
if x != nil {
|
||||||
|
return x.ChargingStatus
|
||||||
|
}
|
||||||
|
return ChargingStatus_CHARGING_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetBatteryRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
Vin string `protobuf:"bytes,2,opt,name=vin,proto3" json:"vin,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetBatteryRequest) Reset() {
|
||||||
|
*x = GetBatteryRequest{}
|
||||||
|
mi := &file_polestar_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetBatteryRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*GetBatteryRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *GetBatteryRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_polestar_proto_msgTypes[2]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use GetBatteryRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*GetBatteryRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_polestar_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetBatteryRequest) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetBatteryRequest) GetVin() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Vin
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetBatteryResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
Vin string `protobuf:"bytes,2,opt,name=vin,proto3" json:"vin,omitempty"`
|
||||||
|
Battery *Battery `protobuf:"bytes,3,opt,name=battery,proto3" json:"battery,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetBatteryResponse) Reset() {
|
||||||
|
*x = GetBatteryResponse{}
|
||||||
|
mi := &file_polestar_proto_msgTypes[3]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetBatteryResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*GetBatteryResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *GetBatteryResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_polestar_proto_msgTypes[3]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use GetBatteryResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*GetBatteryResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_polestar_proto_rawDescGZIP(), []int{3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetBatteryResponse) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetBatteryResponse) GetVin() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Vin
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetBatteryResponse) GetBattery() *Battery {
|
||||||
|
if x != nil {
|
||||||
|
return x.Battery
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_polestar_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
const file_polestar_proto_rawDesc = "" +
|
||||||
|
"\n" +
|
||||||
|
"\x0epolestar.proto\x12\x1eservices.vehiclestates.battery\";\n" +
|
||||||
|
"\tTimestamp\x12\x18\n" +
|
||||||
|
"\aseconds\x18\x01 \x01(\x03R\aseconds\x12\x14\n" +
|
||||||
|
"\x05nanos\x18\x02 \x01(\x05R\x05nanos\"\x80\x04\n" +
|
||||||
|
"\aBattery\x12G\n" +
|
||||||
|
"\ttimestamp\x18\x01 \x01(\v2).services.vehiclestates.battery.TimestampR\ttimestamp\x12E\n" +
|
||||||
|
"\x1fbattery_charge_level_percentage\x18\x02 \x01(\x01R\x1cbatteryChargeLevelPercentage\x12B\n" +
|
||||||
|
"\x1eestimated_distance_to_empty_km\x18\x04 \x01(\x05R\x1aestimatedDistanceToEmptyKm\x12S\n" +
|
||||||
|
"'estimated_charging_time_to_full_minutes\x18\x05 \x01(\x05R\"estimatedChargingTimeToFullMinutes\x12s\n" +
|
||||||
|
"\x19charger_connection_status\x18\x06 \x01(\x0e27.services.vehiclestates.battery.ChargerConnectionStatusR\x17chargerConnectionStatus\x12W\n" +
|
||||||
|
"\x0fcharging_status\x18\a \x01(\x0e2..services.vehiclestates.battery.ChargingStatusR\x0echargingStatus\"5\n" +
|
||||||
|
"\x11GetBatteryRequest\x12\x0e\n" +
|
||||||
|
"\x02id\x18\x01 \x01(\tR\x02id\x12\x10\n" +
|
||||||
|
"\x03vin\x18\x02 \x01(\tR\x03vin\"y\n" +
|
||||||
|
"\x12GetBatteryResponse\x12\x0e\n" +
|
||||||
|
"\x02id\x18\x01 \x01(\tR\x02id\x12\x10\n" +
|
||||||
|
"\x03vin\x18\x02 \x01(\tR\x03vin\x12A\n" +
|
||||||
|
"\abattery\x18\x03 \x01(\v2'.services.vehiclestates.battery.BatteryR\abattery*\xbe\x01\n" +
|
||||||
|
"\x17ChargerConnectionStatus\x12)\n" +
|
||||||
|
"%CHARGER_CONNECTION_STATUS_UNSPECIFIED\x10\x00\x12'\n" +
|
||||||
|
"#CHARGER_CONNECTION_STATUS_CONNECTED\x10\x01\x12*\n" +
|
||||||
|
"&CHARGER_CONNECTION_STATUS_DISCONNECTED\x10\x02\x12#\n" +
|
||||||
|
"\x1fCHARGER_CONNECTION_STATUS_FAULT\x10\x03*\x9d\x02\n" +
|
||||||
|
"\x0eChargingStatus\x12\x1f\n" +
|
||||||
|
"\x1bCHARGING_STATUS_UNSPECIFIED\x10\x00\x12\x1c\n" +
|
||||||
|
"\x18CHARGING_STATUS_CHARGING\x10\x01\x12\x18\n" +
|
||||||
|
"\x14CHARGING_STATUS_IDLE\x10\x02\x12\x18\n" +
|
||||||
|
"\x14CHARGING_STATUS_DONE\x10\x03\x12\x19\n" +
|
||||||
|
"\x15CHARGING_STATUS_FAULT\x10\x04\x12\x1d\n" +
|
||||||
|
"\x19CHARGING_STATUS_SCHEDULED\x10\x05\x12\x1f\n" +
|
||||||
|
"\x1bCHARGING_STATUS_DISCHARGING\x10\x06\x12\x19\n" +
|
||||||
|
"\x15CHARGING_STATUS_ERROR\x10\a\x12\"\n" +
|
||||||
|
"\x1eCHARGING_STATUS_SMART_CHARGING\x10\b2\x8b\x01\n" +
|
||||||
|
"\x0eBatteryService\x12y\n" +
|
||||||
|
"\x10GetLatestBattery\x121.services.vehiclestates.battery.GetBatteryRequest\x1a2.services.vehiclestates.battery.GetBatteryResponseB-Z+github.com/evcc-io/evcc/vehicle/polestar/pbb\x06proto3"
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_polestar_proto_rawDescOnce sync.Once
|
||||||
|
file_polestar_proto_rawDescData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_polestar_proto_rawDescGZIP() []byte {
|
||||||
|
file_polestar_proto_rawDescOnce.Do(func() {
|
||||||
|
file_polestar_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_polestar_proto_rawDesc), len(file_polestar_proto_rawDesc)))
|
||||||
|
})
|
||||||
|
return file_polestar_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_polestar_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
|
||||||
|
var file_polestar_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
||||||
|
var file_polestar_proto_goTypes = []any{
|
||||||
|
(ChargerConnectionStatus)(0), // 0: services.vehiclestates.battery.ChargerConnectionStatus
|
||||||
|
(ChargingStatus)(0), // 1: services.vehiclestates.battery.ChargingStatus
|
||||||
|
(*Timestamp)(nil), // 2: services.vehiclestates.battery.Timestamp
|
||||||
|
(*Battery)(nil), // 3: services.vehiclestates.battery.Battery
|
||||||
|
(*GetBatteryRequest)(nil), // 4: services.vehiclestates.battery.GetBatteryRequest
|
||||||
|
(*GetBatteryResponse)(nil), // 5: services.vehiclestates.battery.GetBatteryResponse
|
||||||
|
}
|
||||||
|
var file_polestar_proto_depIdxs = []int32{
|
||||||
|
2, // 0: services.vehiclestates.battery.Battery.timestamp:type_name -> services.vehiclestates.battery.Timestamp
|
||||||
|
0, // 1: services.vehiclestates.battery.Battery.charger_connection_status:type_name -> services.vehiclestates.battery.ChargerConnectionStatus
|
||||||
|
1, // 2: services.vehiclestates.battery.Battery.charging_status:type_name -> services.vehiclestates.battery.ChargingStatus
|
||||||
|
3, // 3: services.vehiclestates.battery.GetBatteryResponse.battery:type_name -> services.vehiclestates.battery.Battery
|
||||||
|
4, // 4: services.vehiclestates.battery.BatteryService.GetLatestBattery:input_type -> services.vehiclestates.battery.GetBatteryRequest
|
||||||
|
5, // 5: services.vehiclestates.battery.BatteryService.GetLatestBattery:output_type -> services.vehiclestates.battery.GetBatteryResponse
|
||||||
|
5, // [5:6] is the sub-list for method output_type
|
||||||
|
4, // [4:5] is the sub-list for method input_type
|
||||||
|
4, // [4:4] is the sub-list for extension type_name
|
||||||
|
4, // [4:4] is the sub-list for extension extendee
|
||||||
|
0, // [0:4] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_polestar_proto_init() }
|
||||||
|
func file_polestar_proto_init() {
|
||||||
|
if File_polestar_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_polestar_proto_rawDesc), len(file_polestar_proto_rawDesc)),
|
||||||
|
NumEnums: 2,
|
||||||
|
NumMessages: 4,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_polestar_proto_goTypes,
|
||||||
|
DependencyIndexes: file_polestar_proto_depIdxs,
|
||||||
|
EnumInfos: file_polestar_proto_enumTypes,
|
||||||
|
MessageInfos: file_polestar_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_polestar_proto = out.File
|
||||||
|
file_polestar_proto_goTypes = nil
|
||||||
|
file_polestar_proto_depIdxs = nil
|
||||||
|
}
|
||||||
68
vehicle/polestar/pb/polestar.proto
Normal file
68
vehicle/polestar/pb/polestar.proto
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
// Polestar gRPC API (PCCS vehicle battery service).
|
||||||
|
//
|
||||||
|
// Polestar removed the chargingStatus field from the GraphQL carTelematicsV2
|
||||||
|
// API. The gRPC battery service still exposes the charger connection and
|
||||||
|
// charging status. Protocol reconstructed by the pypolestar project from the
|
||||||
|
// Polestar Android app, see https://github.com/pypolestar/pypolestar.
|
||||||
|
//
|
||||||
|
// The proto package name is significant: it forms the gRPC method path
|
||||||
|
// (/services.vehiclestates.battery.BatteryService/GetLatestBattery) expected
|
||||||
|
// by the server.
|
||||||
|
//
|
||||||
|
// Regenerate with:
|
||||||
|
// protoc --go_out=. --go_opt=paths=source_relative \
|
||||||
|
// --go-grpc_out=. --go-grpc_opt=paths=source_relative polestar.proto
|
||||||
|
|
||||||
|
package services.vehiclestates.battery;
|
||||||
|
|
||||||
|
option go_package = "github.com/evcc-io/evcc/vehicle/polestar/pb";
|
||||||
|
|
||||||
|
enum ChargerConnectionStatus {
|
||||||
|
CHARGER_CONNECTION_STATUS_UNSPECIFIED = 0;
|
||||||
|
CHARGER_CONNECTION_STATUS_CONNECTED = 1;
|
||||||
|
CHARGER_CONNECTION_STATUS_DISCONNECTED = 2;
|
||||||
|
CHARGER_CONNECTION_STATUS_FAULT = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ChargingStatus {
|
||||||
|
CHARGING_STATUS_UNSPECIFIED = 0;
|
||||||
|
CHARGING_STATUS_CHARGING = 1;
|
||||||
|
CHARGING_STATUS_IDLE = 2;
|
||||||
|
CHARGING_STATUS_DONE = 3;
|
||||||
|
CHARGING_STATUS_FAULT = 4;
|
||||||
|
CHARGING_STATUS_SCHEDULED = 5;
|
||||||
|
CHARGING_STATUS_DISCHARGING = 6;
|
||||||
|
CHARGING_STATUS_ERROR = 7;
|
||||||
|
CHARGING_STATUS_SMART_CHARGING = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Timestamp {
|
||||||
|
int64 seconds = 1;
|
||||||
|
int32 nanos = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Battery {
|
||||||
|
Timestamp timestamp = 1;
|
||||||
|
double battery_charge_level_percentage = 2;
|
||||||
|
int32 estimated_distance_to_empty_km = 4;
|
||||||
|
int32 estimated_charging_time_to_full_minutes = 5;
|
||||||
|
ChargerConnectionStatus charger_connection_status = 6;
|
||||||
|
ChargingStatus charging_status = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetBatteryRequest {
|
||||||
|
string id = 1;
|
||||||
|
string vin = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetBatteryResponse {
|
||||||
|
string id = 1;
|
||||||
|
string vin = 2;
|
||||||
|
Battery battery = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
service BatteryService {
|
||||||
|
rpc GetLatestBattery(GetBatteryRequest) returns (GetBatteryResponse);
|
||||||
|
}
|
||||||
136
vehicle/polestar/pb/polestar_grpc.pb.go
Normal file
136
vehicle/polestar/pb/polestar_grpc.pb.go
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.6.0
|
||||||
|
// - protoc v7.34.1
|
||||||
|
// source: polestar.proto
|
||||||
|
|
||||||
|
// Polestar gRPC API (PCCS vehicle battery service).
|
||||||
|
//
|
||||||
|
// Polestar removed the chargingStatus field from the GraphQL carTelematicsV2
|
||||||
|
// API. The gRPC battery service still exposes the charger connection and
|
||||||
|
// charging status. Protocol reconstructed by the pypolestar project from the
|
||||||
|
// Polestar Android app, see https://github.com/pypolestar/pypolestar.
|
||||||
|
//
|
||||||
|
// The proto package name is significant: it forms the gRPC method path
|
||||||
|
// (/services.vehiclestates.battery.BatteryService/GetLatestBattery) expected
|
||||||
|
// by the server.
|
||||||
|
//
|
||||||
|
// Regenerate with:
|
||||||
|
// protoc --go_out=. --go_opt=paths=source_relative \
|
||||||
|
// --go-grpc_out=. --go-grpc_opt=paths=source_relative polestar.proto
|
||||||
|
|
||||||
|
package pb
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.64.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
|
const (
|
||||||
|
BatteryService_GetLatestBattery_FullMethodName = "/services.vehiclestates.battery.BatteryService/GetLatestBattery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BatteryServiceClient is the client API for BatteryService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type BatteryServiceClient interface {
|
||||||
|
GetLatestBattery(ctx context.Context, in *GetBatteryRequest, opts ...grpc.CallOption) (*GetBatteryResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type batteryServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBatteryServiceClient(cc grpc.ClientConnInterface) BatteryServiceClient {
|
||||||
|
return &batteryServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *batteryServiceClient) GetLatestBattery(ctx context.Context, in *GetBatteryRequest, opts ...grpc.CallOption) (*GetBatteryResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetBatteryResponse)
|
||||||
|
err := c.cc.Invoke(ctx, BatteryService_GetLatestBattery_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatteryServiceServer is the server API for BatteryService service.
|
||||||
|
// All implementations must embed UnimplementedBatteryServiceServer
|
||||||
|
// for forward compatibility.
|
||||||
|
type BatteryServiceServer interface {
|
||||||
|
GetLatestBattery(context.Context, *GetBatteryRequest) (*GetBatteryResponse, error)
|
||||||
|
mustEmbedUnimplementedBatteryServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedBatteryServiceServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedBatteryServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedBatteryServiceServer) GetLatestBattery(context.Context, *GetBatteryRequest) (*GetBatteryResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetLatestBattery not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedBatteryServiceServer) mustEmbedUnimplementedBatteryServiceServer() {}
|
||||||
|
func (UnimplementedBatteryServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeBatteryServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to BatteryServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeBatteryServiceServer interface {
|
||||||
|
mustEmbedUnimplementedBatteryServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterBatteryServiceServer(s grpc.ServiceRegistrar, srv BatteryServiceServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedBatteryServiceServer was
|
||||||
|
// embedded by pointer and is nil. This will cause panics if an
|
||||||
|
// unimplemented method is ever invoked, so we test this at initialization
|
||||||
|
// time to prevent it from happening at runtime later due to I/O.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&BatteryService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _BatteryService_GetLatestBattery_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetBatteryRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(BatteryServiceServer).GetLatestBattery(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: BatteryService_GetLatestBattery_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(BatteryServiceServer).GetLatestBattery(ctx, req.(*GetBatteryRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatteryService_ServiceDesc is the grpc.ServiceDesc for BatteryService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var BatteryService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "services.vehiclestates.battery.BatteryService",
|
||||||
|
HandlerType: (*BatteryServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "GetLatestBattery",
|
||||||
|
Handler: _BatteryService_GetLatestBattery_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "polestar.proto",
|
||||||
|
}
|
||||||
106
vehicle/polestar/provider_grpc.go
Normal file
106
vehicle/polestar/provider_grpc.go
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
package polestar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
"github.com/evcc-io/evcc/util"
|
||||||
|
"github.com/evcc-io/evcc/vehicle/polestar/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GrpcProvider implements the vehicle API using the Polestar gRPC battery
|
||||||
|
// service for charge state and the GraphQL API for odometer.
|
||||||
|
type GrpcProvider struct {
|
||||||
|
batteryG func() (*pb.Battery, error)
|
||||||
|
odometerG func() (float64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGrpcProvider creates a Polestar gRPC vehicle data provider
|
||||||
|
func NewGrpcProvider(grpc *GrpcAPI, api *API, vin string, timeout, cache time.Duration) *GrpcProvider {
|
||||||
|
return &GrpcProvider{
|
||||||
|
batteryG: util.Cached(func() (*pb.Battery, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
return grpc.Battery(ctx, vin)
|
||||||
|
}, cache),
|
||||||
|
odometerG: util.Cached(func() (float64, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
return api.Odometer(ctx, vin)
|
||||||
|
}, cache),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ api.Battery = (*GrpcProvider)(nil)
|
||||||
|
|
||||||
|
// Soc implements the api.Battery interface
|
||||||
|
func (v *GrpcProvider) Soc() (float64, error) {
|
||||||
|
res, err := v.batteryG()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return res.GetBatteryChargeLevelPercentage(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ api.ChargeState = (*GrpcProvider)(nil)
|
||||||
|
|
||||||
|
// Status implements the api.ChargeState interface
|
||||||
|
func (v *GrpcProvider) Status() (api.ChargeStatus, error) {
|
||||||
|
res, err := v.batteryG()
|
||||||
|
if err != nil {
|
||||||
|
return api.StatusNone, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch res.GetChargingStatus() {
|
||||||
|
case pb.ChargingStatus_CHARGING_STATUS_CHARGING, pb.ChargingStatus_CHARGING_STATUS_SMART_CHARGING:
|
||||||
|
return api.StatusC, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.GetChargerConnectionStatus() == pb.ChargerConnectionStatus_CHARGER_CONNECTION_STATUS_CONNECTED {
|
||||||
|
return api.StatusB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.StatusA, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ api.VehicleRange = (*GrpcProvider)(nil)
|
||||||
|
|
||||||
|
// Range implements the api.VehicleRange interface
|
||||||
|
func (v *GrpcProvider) Range() (int64, error) {
|
||||||
|
res, err := v.batteryG()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int64(res.GetEstimatedDistanceToEmptyKm()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ api.VehicleFinishTimer = (*GrpcProvider)(nil)
|
||||||
|
|
||||||
|
// FinishTime implements the api.VehicleFinishTimer interface
|
||||||
|
func (v *GrpcProvider) FinishTime() (time.Time, error) {
|
||||||
|
res, err := v.batteryG()
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
minutes := res.GetEstimatedChargingTimeToFullMinutes()
|
||||||
|
if minutes <= 0 {
|
||||||
|
return time.Time{}, api.ErrNotAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// anchor the relative remaining time to the API's capture timestamp
|
||||||
|
base := time.Now()
|
||||||
|
if ts := res.GetTimestamp(); ts.GetSeconds() > 0 {
|
||||||
|
base = time.Unix(ts.GetSeconds(), int64(ts.GetNanos()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.Add(time.Duration(minutes) * time.Minute), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ api.VehicleOdometer = (*GrpcProvider)(nil)
|
||||||
|
|
||||||
|
// Odometer implements the api.VehicleOdometer interface
|
||||||
|
func (v *GrpcProvider) Odometer() (float64, error) {
|
||||||
|
return v.odometerG()
|
||||||
|
}
|
||||||
73
vehicle/polestar/provider_grpc_test.go
Normal file
73
vehicle/polestar/provider_grpc_test.go
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
package polestar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
"github.com/evcc-io/evcc/vehicle/polestar/pb"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGrpcStatus(t *testing.T) {
|
||||||
|
tc := []struct {
|
||||||
|
name string
|
||||||
|
connection pb.ChargerConnectionStatus
|
||||||
|
charging pb.ChargingStatus
|
||||||
|
expected api.ChargeStatus
|
||||||
|
}{
|
||||||
|
{"disconnected", pb.ChargerConnectionStatus_CHARGER_CONNECTION_STATUS_DISCONNECTED, pb.ChargingStatus_CHARGING_STATUS_IDLE, api.StatusA},
|
||||||
|
{"unspecified", pb.ChargerConnectionStatus_CHARGER_CONNECTION_STATUS_UNSPECIFIED, pb.ChargingStatus_CHARGING_STATUS_UNSPECIFIED, api.StatusA},
|
||||||
|
{"connected idle", pb.ChargerConnectionStatus_CHARGER_CONNECTION_STATUS_CONNECTED, pb.ChargingStatus_CHARGING_STATUS_IDLE, api.StatusB},
|
||||||
|
{"connected done", pb.ChargerConnectionStatus_CHARGER_CONNECTION_STATUS_CONNECTED, pb.ChargingStatus_CHARGING_STATUS_DONE, api.StatusB},
|
||||||
|
{"charging", pb.ChargerConnectionStatus_CHARGER_CONNECTION_STATUS_CONNECTED, pb.ChargingStatus_CHARGING_STATUS_CHARGING, api.StatusC},
|
||||||
|
{"smart charging", pb.ChargerConnectionStatus_CHARGER_CONNECTION_STATUS_CONNECTED, pb.ChargingStatus_CHARGING_STATUS_SMART_CHARGING, api.StatusC},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tc {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
v := &GrpcProvider{
|
||||||
|
batteryG: func() (*pb.Battery, error) {
|
||||||
|
return &pb.Battery{
|
||||||
|
ChargerConnectionStatus: tc.connection,
|
||||||
|
ChargingStatus: tc.charging,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := v.Status()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.expected, status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcFinishTime(t *testing.T) {
|
||||||
|
captured := time.Date(2026, 5, 22, 12, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
v := &GrpcProvider{
|
||||||
|
batteryG: func() (*pb.Battery, error) {
|
||||||
|
return &pb.Battery{
|
||||||
|
EstimatedChargingTimeToFullMinutes: 30,
|
||||||
|
Timestamp: &pb.Timestamp{Seconds: captured.Unix()},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// finish time is anchored to the API capture timestamp, not time.Now()
|
||||||
|
ft, err := v.FinishTime()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, captured.Add(30*time.Minute).Equal(ft))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcFinishTimeNotCharging(t *testing.T) {
|
||||||
|
v := &GrpcProvider{
|
||||||
|
batteryG: func() (*pb.Battery, error) {
|
||||||
|
return &pb.Battery{EstimatedChargingTimeToFullMinutes: 0}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := v.FinishTime()
|
||||||
|
assert.ErrorIs(t, err, api.ErrNotAvailable)
|
||||||
|
}
|
||||||
75
vehicle/polestargrpc.go
Normal file
75
vehicle/polestargrpc.go
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
package vehicle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
"github.com/evcc-io/evcc/util"
|
||||||
|
"github.com/evcc-io/evcc/util/request"
|
||||||
|
"github.com/evcc-io/evcc/vehicle/polestar"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PolestarGRPC is an api.Vehicle implementation for Polestar cars using the
|
||||||
|
// gRPC battery API. It restores the charging status that Polestar removed from
|
||||||
|
// the GraphQL API, see https://github.com/evcc-io/evcc/issues/30071.
|
||||||
|
type PolestarGRPC struct {
|
||||||
|
*embed
|
||||||
|
*polestar.GrpcProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.Add("polestar-grpc", NewPolestarGRPCFromConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPolestarGRPCFromConfig creates a new vehicle
|
||||||
|
func NewPolestarGRPCFromConfig(other map[string]any) (api.Vehicle, error) {
|
||||||
|
cc := struct {
|
||||||
|
embed `mapstructure:",squash"`
|
||||||
|
User, Password string
|
||||||
|
VIN string
|
||||||
|
Cache time.Duration
|
||||||
|
Timeout time.Duration
|
||||||
|
}{
|
||||||
|
Cache: interval,
|
||||||
|
Timeout: request.Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := util.DecodeOther(other, &cc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log := util.NewLogger("polestar").Redact(cc.User, cc.Password, cc.VIN)
|
||||||
|
|
||||||
|
v := &PolestarGRPC{
|
||||||
|
embed: &cc.embed,
|
||||||
|
}
|
||||||
|
|
||||||
|
identity, err := polestar.NewIdentity(log, cc.User, cc.Password)
|
||||||
|
if err != nil {
|
||||||
|
return v, fmt.Errorf("login failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
api := polestar.NewAPI(log, identity)
|
||||||
|
|
||||||
|
vehicle, err := ensureVehicleEx(cc.VIN, func() ([]polestar.ConsumerCar, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), cc.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
return api.Vehicles(ctx)
|
||||||
|
}, func(v polestar.ConsumerCar) (string, error) {
|
||||||
|
return v.VIN, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcAPI, err := polestar.NewGrpcAPI(log, identity)
|
||||||
|
if err != nil {
|
||||||
|
return v, fmt.Errorf("grpc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v.GrpcProvider = polestar.NewGrpcProvider(grpcAPI, api, vehicle.VIN, cc.Timeout, cc.Cache)
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue