Skip to content

Commit 857563a

Browse files
author
Valeriano Manassero
committed
Subaru Solterra vehicle integration
1 parent 76d841c commit 857563a

File tree

8 files changed

+582
-0
lines changed

8 files changed

+582
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
template: subaru
2+
products:
3+
- brand: Subaru
4+
requirements:
5+
description:
6+
de: |
7+
Benötigt Subaru Connected Services Account.
8+
en: |
9+
Requires Subaru Connected Services Account.
10+
params:
11+
- preset: vehicle-common
12+
- name: user
13+
required: true
14+
- name: password
15+
required: true
16+
- name: vin
17+
example: JF...
18+
- name: cache
19+
default: 15m
20+
render: |
21+
type: subaru
22+
{{ include "vehicle-common" . }}
23+
user: {{ .user }}
24+
password: {{ .password }}
25+
vin: {{ .vin }}
26+
cache: {{ .cache }}

vehicle/subaru.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package vehicle
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/evcc-io/evcc/api"
8+
"github.com/evcc-io/evcc/util"
9+
"github.com/evcc-io/evcc/vehicle/subaru"
10+
)
11+
12+
// Subaru is an api.Vehicle implementation for Subaru cars
13+
type Subaru struct {
14+
*embed
15+
*subaru.Provider
16+
}
17+
18+
func init() {
19+
registry.Add("subaru", NewSubaruFromConfig)
20+
}
21+
22+
// NewSubaruFromConfig creates a new vehicle
23+
func NewSubaruFromConfig(other map[string]interface{}) (api.Vehicle, error) {
24+
cc := struct {
25+
embed `mapstructure:",squash"`
26+
User, Password, VIN string
27+
Cache time.Duration
28+
}{
29+
Cache: interval,
30+
}
31+
32+
if err := util.DecodeOther(other, &cc); err != nil {
33+
return nil, err
34+
}
35+
36+
if cc.User == "" || cc.Password == "" {
37+
return nil, api.ErrMissingCredentials
38+
}
39+
40+
v := &Subaru{
41+
embed: &cc.embed,
42+
}
43+
44+
log := util.NewLogger("subaru").Redact(cc.User, cc.Password, cc.VIN)
45+
identity := subaru.NewIdentity(log)
46+
47+
err := identity.Login(cc.User, cc.Password)
48+
if err != nil {
49+
return v, fmt.Errorf("login failed: %w", err)
50+
}
51+
52+
api := subaru.NewAPI(log, identity)
53+
54+
cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
55+
if err == nil {
56+
v.Provider = subaru.NewProvider(api, cc.VIN, cc.Cache)
57+
}
58+
59+
return v, err
60+
}

vehicle/subaru/api.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package subaru
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"fmt"
8+
"net/http"
9+
"time"
10+
11+
"github.com/evcc-io/evcc/util"
12+
"github.com/evcc-io/evcc/util/request"
13+
"golang.org/x/oauth2"
14+
)
15+
16+
const (
17+
BaseUrl = "https://b2c-login.toyota-europe.com"
18+
ApiBaseUrl = "https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io"
19+
AccessTokenPath = "oauth2/realms/root/realms/alliance-subaru/access_token"
20+
AuthenticationPath = "json/realms/root/realms/alliance-subaru/authenticate?authIndexType=service&authIndexValue=oneapp"
21+
AuthorizationPath = "oauth2/realms/root/realms/alliance-subaru/authorize?client_id=8c4921b0b08901fef389ce1af49c4e10.subaru.com&scope=openid+profile+write&response_type=code&redirect_uri=com.subaru.oneapp:/oauth2Callback&code_challenge=plain&code_challenge_method=plain"
22+
VehicleGuidPath = "v2/vehicle/guid"
23+
RemoteElectricStatusPath = "v1/global/remote/electric/status"
24+
ApiKey = "tTZipv6liF74PwMfk9Ed68AQ0bISswwf3iHQdqcF"
25+
ClientRefKey = "2.17.0"
26+
)
27+
28+
type API struct {
29+
*request.Helper
30+
log *util.Logger
31+
identity *Identity
32+
clientRef string
33+
}
34+
35+
func NewAPI(log *util.Logger, identity *Identity) *API {
36+
v := &API{
37+
Helper: request.NewHelper(log),
38+
log: log,
39+
identity: identity,
40+
}
41+
42+
v.Timeout = 120 * time.Second
43+
44+
// replace client transport with authenticated transport
45+
v.Transport = &oauth2.Transport{
46+
Source: identity,
47+
Base: v.Transport,
48+
}
49+
50+
// create HMAC digest for x-client-ref header
51+
h := hmac.New(sha256.New, []byte(ClientRefKey))
52+
h.Write([]byte(v.identity.uuid))
53+
v.clientRef = hex.EncodeToString(h.Sum(nil))
54+
55+
return v
56+
}
57+
58+
func (v *API) Vehicles() ([]string, error) {
59+
uri := fmt.Sprintf("%s/%s", ApiBaseUrl, VehicleGuidPath)
60+
61+
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
62+
"Accept": request.JSONContent,
63+
"x-guid": v.identity.uuid,
64+
"x-api-key": ApiKey,
65+
"x-client-ref": v.clientRef,
66+
"x-appversion": ClientRefKey,
67+
"X-Appbrand": "S",
68+
})
69+
var resp Vehicles
70+
if err == nil {
71+
err = v.DoJSON(req, &resp)
72+
}
73+
var vehicles []string
74+
for _, v := range resp.Payload {
75+
vehicles = append(vehicles, v.VIN)
76+
}
77+
return vehicles, err
78+
}
79+
80+
func (v *API) Status(vin string) (Status, error) {
81+
uri := fmt.Sprintf("%s/%s", ApiBaseUrl, RemoteElectricStatusPath)
82+
83+
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
84+
"Accept": request.JSONContent,
85+
"x-guid": v.identity.uuid,
86+
"x-api-key": ApiKey,
87+
"x-client-ref": v.clientRef,
88+
"x-appversion": ClientRefKey,
89+
"vin": vin,
90+
})
91+
var status Status
92+
if err == nil {
93+
err = v.DoJSON(req, &status)
94+
}
95+
return status, err
96+
}

vehicle/subaru/api_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//go:build integration
2+
// +build integration
3+
4+
package subaru
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"testing"
10+
11+
"github.com/evcc-io/evcc/util"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestAPI(t *testing.T) {
16+
// Skip if no credentials provided
17+
user := os.Getenv("SUBARU_USER")
18+
password := os.Getenv("SUBARU_PASSWORD")
19+
if user == "" || password == "" {
20+
t.Skip("SUBARU_USER or SUBARU_PASSWORD not set")
21+
}
22+
23+
// Create and login identity
24+
util.LogLevel("trace", nil) // Enable trace logging
25+
log := util.NewLogger("test")
26+
identity := NewIdentity(log)
27+
err := identity.Login(user, password)
28+
require.NoError(t, err)
29+
30+
// Create API client
31+
api := NewAPI(log, identity)
32+
33+
// Test Vehicles method
34+
vehicles, err := api.Vehicles()
35+
require.NoError(t, err)
36+
fmt.Printf("Vehicles: %+v\n", vehicles)
37+
require.NotEmpty(t, vehicles, "expected at least one vehicle")
38+
39+
for _, vin := range vehicles {
40+
require.NotEmpty(t, vin, "expected non-empty VIN")
41+
}
42+
43+
// Test Status method for first vehicle
44+
status, err := api.Status(vehicles[0])
45+
require.NoError(t, err)
46+
require.NotNil(t, status)
47+
fmt.Printf("Vehicle status: %+v\n", status)
48+
}

0 commit comments

Comments
 (0)