Skip to content

Commit 5801072

Browse files
authored
Add volvo api (#334)
1 parent d72da03 commit 5801072

File tree

2 files changed

+203
-1
lines changed

2 files changed

+203
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
1818
- simple and clean user interface
1919
- multiple [chargers](#charger): Wallbe, Phoenix (includes ESL Walli), go-eCharger, NRGkick (direct Bluetooth or via Connect device), SimpleEVSE, EVSEWifi, KEBA/BMW, openWB, Mobile Charger Connect, and any other charger using scripting
2020
- multiple [meters](#meter): ModBus (Eastron SDM, MPM3PM, SBC ALE3 and many more), Discovergy (using HTTP plugin), SMA Home Manager 2.0 and SMA Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC), Tesla PowerWall
21-
- wide support of vendor-specific [vehicles](#vehicle) interfaces (remote charge, battery and preconditioning status): Audi, BMW, Ford, Tesla, Nissan, Renault, Porsche, Volkswagen and any other vehicle using scripting
21+
- wide support of vendor-specific [vehicles](#vehicle) interfaces (remote charge, battery and preconditioning status): Audi, BMW, Ford, Tesla, Nissan, Renault, Porsche, Volkswagen, Volvo and any other vehicle using scripting
2222
- [plugins](#plugins) for integrating with hardware devices and home automation: Modbus (meters and grid inverters), HTTP, MQTT, Javascript, WebSockets and shell scripts
2323
- status notifications using [Telegram](https://telegram.org) and [PushOver](https://pushover.net)
2424
- logging using [InfluxDB](https://www.influxdata.com) and [Grafana](https://grafana.com/grafana/)
@@ -231,6 +231,7 @@ Available vehicle remote interface implementations are:
231231
- `porsche`: Porsche (Taycan)
232232
- `vw`: Volkswagen (eGolf, eUp)
233233
- `id`: Volkswagen (ID.3, ID.4)
234+
- `volvo`: Volvo
234235
- `default`: default vehicle implementation using configurable [plugins](#plugins) for integrating any type of vehicle
235236

236237
Configuration examples are documented at [andig/evcc-config#vehicles](https://github.com/andig/evcc-config#vehicles)

vehicle/volvo.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package vehicle
2+
3+
import (
4+
"encoding/base64"
5+
"fmt"
6+
"net/http"
7+
"time"
8+
9+
"github.com/andig/evcc/api"
10+
"github.com/andig/evcc/provider"
11+
"github.com/andig/evcc/util"
12+
"github.com/andig/evcc/util/request"
13+
)
14+
15+
const (
16+
volvoAPI = "https://vocapi.wirelesscar.net/customerapi/rest/v3.0"
17+
)
18+
19+
type volvoAccountResponse struct {
20+
FirstName string `json:"firstName"`
21+
LastName string `json:"lastName"`
22+
VehicleRelations []string `json:"accountVehicleRelations"`
23+
}
24+
25+
type volvoVehicleRelation struct {
26+
Account string `json:"account"`
27+
AccountID string `json:"accountId"`
28+
Vehicle string `json:"vehicle"`
29+
AccountVehicleRelation string `json:"accountVehicleRelation"`
30+
VehicleID string `json:"vehicleId"`
31+
Username string `json:"username"`
32+
Status string `json:"status"`
33+
CustomerVehicleRelationID int `json:"customerVehicleRelationId"`
34+
}
35+
36+
type volvoStatus struct {
37+
AverageFuelConsumption float32 `json:"averageFuelConsumption"`
38+
AverageFuelConsumptionTimestamp string `json:"averageFuelConsumptionTimestamp"`
39+
AverageSpeed int `json:"averageSpeed"`
40+
AverageSpeedTimestamp string `json:"averageSpeedTimestamp"`
41+
BrakeFluid string `json:"brakeFluid"`
42+
BrakeFluidTimestamp string `json:"brakeFluidTimestamp"`
43+
CarLocked bool `json:"carLocked"`
44+
CarLockedTimestamp string `json:"carLockedTimestamp"`
45+
ConnectionStatus string `json:"connectionStatus"`
46+
ConnectionStatusTimestamp string `json:"connectionStatusTimestamp"`
47+
DistanceToEmpty int `json:"distanceToEmpty"`
48+
DistanceToEmptyTimestamp string `json:"distanceToEmptyTimestamp"`
49+
EngineRunning bool `json:"engineRunning"`
50+
EngineRunningTimestamp string `json:"engineRunningTimestamp"`
51+
FuelAmount int `json:"fuelAmount"`
52+
FuelAmountLevel int `json:"fuelAmountLevel"`
53+
FuelAmountLevelTimestamp string `json:"fuelAmountLevelTimestamp"`
54+
FuelAmountTimestamp string `json:"fuelAmountTimestamp"`
55+
HvBattery struct {
56+
HvBatteryChargeStatusDerived string `json:"hvBatteryChargeStatusDerived"` // "CablePluggedInCar_Charging",
57+
HvBatteryChargeStatusDerivedTimestamp string `json:"hvBatteryChargeStatusDerivedTimestamp"` // "2021-01-19T14:44:02+0000",
58+
HvBatteryChargeModeStatus string `json:"hvBatteryChargeModeStatus"` // null,
59+
HvBatteryChargeModeStatusTimestamp string `json:"hvBatteryChargeModeStatusTimestamp"` // null,
60+
HvBatteryChargeStatus string `json:"hvBatteryChargeStatus"` // "ChargeProgress",
61+
HvBatteryChargeStatusTimestamp string `json:"hvBatteryChargeStatusTimestamp"` // "2021-01-19T14:44:02+0000",
62+
HvBatteryLevel int `json:"hvBatteryLevel"` // 72,
63+
HvBatteryLevelTimestamp string `json:"hvBatteryLevelTimestamp"` // "2021-01-19T14:44:02+0000",
64+
DistanceToHVBatteryEmpty int `json:"distanceToHVBatteryEmpty"` // 28,
65+
DistanceToHVBatteryEmptyTimestamp string `json:"distanceToHVBatteryEmptyTimestamp"` // "2021-01-19T14:44:02+0000",
66+
TimeToHVBatteryFullyCharged int `json:"timeToHVBatteryFullyCharged"` // 60,
67+
TimeToHVBatteryFullyChargedTimestamp string `json:"timeToHVBatteryFullyChargedTimestamp"` // "2021-01-19T14:44:02+0000"
68+
} `json:"hvBattery"`
69+
Odometer int `json:"odometer"`
70+
OdometerTimestamp string `json:"odometerTimestamp"`
71+
PrivacyPolicyEnabled bool `json:"privacyPolicyEnabled"`
72+
PrivacyPolicyEnabledTimestamp string `json:"privacyPolicyEnabledTimestamp"`
73+
RemoteClimatizationStatus string `json:"remoteClimatizationStatus"`
74+
RemoteClimatizationStatusTimestamp string `json:"remoteClimatizationStatusTimestamp"`
75+
ServiceWarningStatus string `json:"serviceWarningStatus"`
76+
ServiceWarningStatusTimestamp string `json:"serviceWarningStatusTimestamp"`
77+
TimeFullyAccessibleUntil string `json:"timeFullyAccessibleUntil"`
78+
TimePartiallyAccessibleUntil string `json:"timePartiallyAccessibleUntil"`
79+
TripMeter1 int `json:"tripMeter1"`
80+
TripMeter1Timestamp string `json:"tripMeter1Timestamp"`
81+
TripMeter2 int `json:"tripMeter2"`
82+
TripMeter2Timestamp string `json:"tripMeter2Timestamp"`
83+
}
84+
85+
// Volvo is an api.Vehicle implementation for Volvo cars
86+
type Volvo struct {
87+
*embed
88+
*request.Helper
89+
user, password, vin string
90+
statusG func() (interface{}, error)
91+
}
92+
93+
func init() {
94+
registry.Add("volvo", NewVolvoFromConfig)
95+
}
96+
97+
// NewVolvoFromConfig creates a new vehicle
98+
func NewVolvoFromConfig(other map[string]interface{}) (api.Vehicle, error) {
99+
cc := struct {
100+
Title string
101+
Capacity int64
102+
User, Password, VIN string
103+
Cache time.Duration
104+
}{
105+
Cache: interval,
106+
}
107+
108+
if err := util.DecodeOther(other, &cc); err != nil {
109+
return nil, err
110+
}
111+
112+
log := util.NewLogger("volvo")
113+
114+
v := &Volvo{
115+
embed: &embed{cc.Title, cc.Capacity},
116+
Helper: request.NewHelper(log),
117+
user: cc.User,
118+
password: cc.Password,
119+
vin: cc.VIN,
120+
}
121+
122+
v.statusG = provider.NewCached(v.status, cc.Cache).InterfaceGetter()
123+
124+
var err error
125+
if cc.VIN == "" {
126+
v.vin, err = findVehicle(v.vehicles())
127+
if err == nil {
128+
log.DEBUG.Printf("found vehicle: %v", v.vin)
129+
}
130+
}
131+
132+
return v, err
133+
}
134+
135+
func (v *Volvo) request(uri string) (*http.Request, error) {
136+
basicAuth := base64.StdEncoding.EncodeToString([]byte(v.user + ":" + v.password))
137+
138+
return request.New(http.MethodGet, uri, nil, map[string]string{
139+
"Authorization": fmt.Sprintf("Basic %s", basicAuth),
140+
"Content-Type": "application/json",
141+
"X-Device-Id": "Device",
142+
"X-OS-Type": "Android",
143+
"X-Originator-Type": "App",
144+
"X-OS-Version": "22",
145+
})
146+
}
147+
148+
// vehicles implements returns the list of user vehicles
149+
func (v *Volvo) vehicles() ([]string, error) {
150+
var vehicles []string
151+
152+
req, err := v.request(fmt.Sprintf("%s/customeraccounts", volvoAPI))
153+
if err == nil {
154+
var res volvoAccountResponse
155+
err = v.DoJSON(req, &res)
156+
157+
for _, rel := range res.VehicleRelations {
158+
var vehicle volvoVehicleRelation
159+
if req, err := v.request(rel); err == nil {
160+
if err = v.DoJSON(req, &vehicle); err != nil {
161+
return vehicles, err
162+
}
163+
164+
vehicles = append(vehicles, vehicle.VehicleID)
165+
}
166+
}
167+
}
168+
169+
return vehicles, err
170+
}
171+
172+
func (v *Volvo) status() (volvoStatus, error) {
173+
var res volvoStatus
174+
175+
req, err := v.request(fmt.Sprintf("%s/vehicles/%s/status", volvoAPI, v.vin))
176+
if err == nil {
177+
err = v.DoJSON(req, &res)
178+
}
179+
180+
return res, err
181+
}
182+
183+
// ChargeState implements the Vehicle.ChargeState interface
184+
func (v *Volvo) ChargeState() (float64, error) {
185+
res, err := v.statusG()
186+
if res, ok := res.(volvoStatus); err == nil && ok {
187+
return float64(res.HvBattery.HvBatteryLevel), nil
188+
}
189+
190+
return 0, err
191+
}
192+
193+
// VehicleRange implements the VehicleRange interface
194+
func (v *Volvo) VehicleRange() (int64, error) {
195+
res, err := v.statusG()
196+
if res, ok := res.(volvoStatus); err == nil && ok {
197+
return int64(res.HvBattery.DistanceToHVBatteryEmpty), nil
198+
}
199+
200+
return 0, err
201+
}

0 commit comments

Comments
 (0)