Skip to content

Commit 9ca3ba6

Browse files
authored
Review vehicle endpoints (#1536)
* Add full list of endpoints * Adjust * More * Mypy * Adjust * Coverage * Adjust * Adjust
1 parent 0dfc3f3 commit 9ca3ba6

File tree

9 files changed

+312
-82
lines changed

9 files changed

+312
-82
lines changed

src/renault_api/exceptions.py

+13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Exceptions for Renault API."""
22

3+
from typing import Optional
4+
35

46
class RenaultException(Exception): # noqa: N818
57
"""Base exception for Renault API errors."""
@@ -11,3 +13,14 @@ class NotAuthenticatedException(RenaultException): # noqa: N818
1113
"""You are not authenticated, or authentication has expired."""
1214

1315
pass
16+
17+
18+
class EndpointNotAvailableError(RenaultException):
19+
"""The endpoint is not available for this model."""
20+
21+
def __init__(self, endpoint: str, model_code: Optional[str]) -> None:
22+
self.endpoint = endpoint
23+
self.model_code = model_code
24+
25+
def __str__(self) -> str:
26+
return f"Endpoint '{self.endpoint}' not available for model '{self.model_code}'"

src/renault_api/kamereon/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
DATA_ENDPOINTS = _KCA_GET_ENDPOINTS
5151
ACTION_ENDPOINTS = _KCA_POST_ENDPOINTS
5252

53+
ACCOUNT_ENDPOINT_ROOT = "/commerce/v1/accounts/{account_id}/kamereon"
54+
5355

5456
def get_commerce_url(root_url: str) -> str:
5557
"""Get the Kamereon base commerce url."""

src/renault_api/kamereon/models.py

+77-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
import logging
5+
from collections.abc import Mapping
56
from dataclasses import dataclass
67
from typing import Any
78
from typing import Optional
@@ -84,6 +85,49 @@
8485
},
8586
}
8687

88+
_DEFAULT_ENDPOINTS: dict[str, str] = {
89+
"battery-status": "/kca/car-adapter/v2/cars/{vin}/battery-status",
90+
"charge-history": "/kca/car-adapter/v1/cars/{vin}/charge-history",
91+
"charge-mode": "/kca/car-adapter/v1/cars/{vin}/charge-mode",
92+
"charges": "/kca/car-adapter/v1/cars/{vin}/charges",
93+
"charging-settings": "/kca/car-adapter/v1/cars/{vin}/charging-settings",
94+
"cockpit": "/kca/car-adapter/v1/cars/{vin}/cockpit",
95+
"hvac-history": "/kca/car-adapter/v1/cars/{vin}/hvac-history",
96+
"hvac-sessions": "/kca/car-adapter/v1/cars/{vin}/hvac-sessions",
97+
"hvac-settings": "/kca/car-adapter/v1/cars/{vin}/hvac-settings",
98+
"hvac-status": "/kca/car-adapter/v1/cars/{vin}/hvac-status",
99+
"location": "/kca/car-adapter/v1/cars/{vin}/location",
100+
"lock-status": "/kca/car-adapter/v1/cars/{vin}/lock-status",
101+
"notification-settings": "/kca/car-adapter/v1/cars/{vin}/notification-settings",
102+
"pressure": "/kca/car-adapter/v1/cars/{vin}/pressure",
103+
"res-state": "/kca/car-adapter/v1/cars/{vin}/res-state",
104+
}
105+
106+
_VEHICLE_ENDPOINTS: dict[str, dict[str, Optional[str]]] = {
107+
"X101VE": { # ZOE phase 1
108+
"battery-status": _DEFAULT_ENDPOINTS["battery-status"], # confirmed
109+
"charge-mode": _DEFAULT_ENDPOINTS["charge-mode"], # confirmed
110+
"cockpit": _DEFAULT_ENDPOINTS["cockpit"], # confirmed
111+
"hvac-status": _DEFAULT_ENDPOINTS["hvac-status"], # confirmed
112+
"location": None, # not supported
113+
"lock-status": None, # not supported
114+
"pressure": None, # not supported
115+
"res-state": None, # not supported
116+
},
117+
"XJA1VP": { # CLIO V
118+
"hvac-status": None,
119+
},
120+
"XJB1SU": { # CAPTUR II
121+
"hvac-status": None,
122+
},
123+
"XCB1VE": { # MEGANE E-TECH
124+
"lock-status": None,
125+
},
126+
"XCB1SE": { # SCENIC E-TECH
127+
"lock-status": None,
128+
},
129+
}
130+
87131

88132
@dataclass
89133
class KamereonResponseError(BaseModel):
@@ -261,11 +305,7 @@ def reports_charging_power_in_watts(self) -> bool:
261305
def supports_endpoint(self, endpoint: str) -> bool:
262306
"""Return True if model supports specified endpoint."""
263307
# Default to True for unknown vehicles
264-
if self.model and self.model.code:
265-
return VEHICLE_SPECIFICATIONS.get( # type:ignore[no-any-return]
266-
self.model.code, {}
267-
).get(f"support-endpoint-{endpoint}", True)
268-
return True # pragma: no cover
308+
return self.get_endpoint(endpoint) is not None
269309

270310
def warns_on_method(self, method: str) -> Optional[str]:
271311
"""Return warning message if model trigger a warning on the method call."""
@@ -285,6 +325,38 @@ def controls_action_via_kcm(self, action: str) -> bool:
285325
).get(f"control-{action}-via-kcm", False)
286326
return False # pragma: no cover
287327

328+
def get_endpoints(self) -> Mapping[str, Optional[str]]:
329+
"""Return model endpoints."""
330+
model_code = self.get_model_code()
331+
if not model_code:
332+
# Model code not available
333+
return _DEFAULT_ENDPOINTS # pragma: no cover
334+
335+
if model_code not in _VEHICLE_ENDPOINTS:
336+
# Model not documented
337+
_LOGGER.warning(
338+
"Model %s is not documented, using default endpoints",
339+
self.get_model_code(),
340+
)
341+
return _DEFAULT_ENDPOINTS
342+
343+
return _VEHICLE_ENDPOINTS[model_code]
344+
345+
def get_endpoint(self, endpoint: str) -> Optional[str]:
346+
"""Return model endpoint"""
347+
endpoints = self.get_endpoints()
348+
349+
if endpoint not in endpoints:
350+
# Endpoint not documented
351+
_LOGGER.warning(
352+
"Endpoint %s for model %s is not documented, using default endpoints",
353+
endpoint,
354+
self.get_model_code(),
355+
)
356+
return _DEFAULT_ENDPOINTS.get(endpoint)
357+
358+
return endpoints[endpoint]
359+
288360

289361
@dataclass
290362
class KamereonVehiclesLink(BaseModel):

src/renault_api/renault_session.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Optional
77

88
import aiohttp
9+
from marshmallow.schema import Schema
910

1011
from . import gigya
1112
from . import kamereon
@@ -154,7 +155,12 @@ async def _get_jwt(self) -> str:
154155
return jwt
155156

156157
async def http_request(
157-
self, method: str, endpoint: str, json: Optional[dict[str, Any]] = None
158+
self,
159+
method: str,
160+
endpoint: str,
161+
json: Optional[dict[str, Any]] = None,
162+
*,
163+
schema: Optional[Schema] = None,
158164
) -> models.KamereonResponse:
159165
"""GET to specified endpoint."""
160166
url = (await self._get_kamereon_root_url()) + endpoint
@@ -167,6 +173,7 @@ async def http_request(
167173
gigya_jwt=await self._get_jwt(),
168174
params=params,
169175
json=json,
176+
schema=schema,
170177
)
171178

172179
async def get_person(self) -> models.KamereonPersonResponse:

src/renault_api/renault_vehicle.py

+32-55
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
import aiohttp
1111

1212
from .credential_store import CredentialStore
13+
from .exceptions import EndpointNotAvailableError
1314
from .exceptions import RenaultException
15+
from .kamereon import ACCOUNT_ENDPOINT_ROOT
1416
from .kamereon import models
1517
from .kamereon import schemas
1618
from .renault_session import RenaultSession
@@ -77,6 +79,25 @@ def vin(self) -> str:
7779
"""Get vin."""
7880
return self._vin
7981

82+
async def _get_vehicle_data(
83+
self, endpoint: str
84+
) -> models.KamereonVehicleDataResponse:
85+
"""GET to /v{endpoint_version}/cars/{vin}/{endpoint}."""
86+
details = await self.get_details()
87+
full_endpoint = details.get_endpoint(endpoint)
88+
if full_endpoint is None:
89+
raise EndpointNotAvailableError(endpoint, details.get_model_code())
90+
91+
full_endpoint = ACCOUNT_ENDPOINT_ROOT.replace(
92+
"{account_id}", self.account_id
93+
) + full_endpoint.replace("{vin}", self.vin)
94+
95+
schema = schemas.KamereonVehicleDataResponseSchema
96+
return cast(
97+
models.KamereonVehicleDataResponse,
98+
await self.session.http_request("GET", full_endpoint, schema=schema),
99+
)
100+
80101
async def get_details(self) -> models.KamereonVehicleDetails:
81102
"""Get vehicle details."""
82103
if self._vehicle_details:
@@ -124,119 +145,79 @@ async def get_contracts(self) -> list[models.KamereonVehicleContract]:
124145

125146
async def get_battery_status(self) -> models.KamereonVehicleBatteryStatusData:
126147
"""Get vehicle battery status."""
127-
response = await self.session.get_vehicle_data(
128-
account_id=self.account_id,
129-
vin=self.vin,
130-
endpoint="battery-status",
131-
)
148+
response = await self._get_vehicle_data("battery-status")
132149
return cast(
133150
models.KamereonVehicleBatteryStatusData,
134151
response.get_attributes(schemas.KamereonVehicleBatteryStatusDataSchema),
135152
)
136153

137154
async def get_tyre_pressure(self) -> models.KamereonVehicleTyrePressureData:
138155
"""Get vehicle tyre pressure."""
139-
response = await self.session.get_vehicle_data(
140-
account_id=self.account_id,
141-
vin=self.vin,
142-
endpoint="pressure",
143-
)
156+
response = await self._get_vehicle_data("pressure")
144157
return cast(
145158
models.KamereonVehicleTyrePressureData,
146159
response.get_attributes(schemas.KamereonVehicleTyrePressureDataSchema),
147160
)
148161

149162
async def get_location(self) -> models.KamereonVehicleLocationData:
150163
"""Get vehicle location."""
151-
response = await self.session.get_vehicle_data(
152-
account_id=self.account_id,
153-
vin=self.vin,
154-
endpoint="location",
155-
)
164+
response = await self._get_vehicle_data("location")
156165
return cast(
157166
models.KamereonVehicleLocationData,
158167
response.get_attributes(schemas.KamereonVehicleLocationDataSchema),
159168
)
160169

161170
async def get_hvac_status(self) -> models.KamereonVehicleHvacStatusData:
162171
"""Get vehicle hvac status."""
163-
response = await self.session.get_vehicle_data(
164-
account_id=self.account_id,
165-
vin=self.vin,
166-
endpoint="hvac-status",
167-
)
172+
response = await self._get_vehicle_data("hvac-status")
168173
return cast(
169174
models.KamereonVehicleHvacStatusData,
170175
response.get_attributes(schemas.KamereonVehicleHvacStatusDataSchema),
171176
)
172177

173178
async def get_hvac_settings(self) -> models.KamereonVehicleHvacSettingsData:
174179
"""Get vehicle hvac settings (schedule+mode)."""
175-
response = await self.session.get_vehicle_data(
176-
account_id=self.account_id,
177-
vin=self.vin,
178-
endpoint="hvac-settings",
179-
)
180+
response = await self._get_vehicle_data("hvac-settings")
180181
return cast(
181182
models.KamereonVehicleHvacSettingsData,
182183
response.get_attributes(schemas.KamereonVehicleHvacSettingsDataSchema),
183184
)
184185

185186
async def get_charge_mode(self) -> models.KamereonVehicleChargeModeData:
186187
"""Get vehicle charge mode."""
187-
response = await self.session.get_vehicle_data(
188-
account_id=self.account_id,
189-
vin=self.vin,
190-
endpoint="charge-mode",
191-
)
188+
response = await self._get_vehicle_data("charge-mode")
192189
return cast(
193190
models.KamereonVehicleChargeModeData,
194191
response.get_attributes(schemas.KamereonVehicleChargeModeDataSchema),
195192
)
196193

197194
async def get_cockpit(self) -> models.KamereonVehicleCockpitData:
198195
"""Get vehicle cockpit."""
199-
response = await self.session.get_vehicle_data(
200-
account_id=self.account_id,
201-
vin=self.vin,
202-
endpoint="cockpit",
203-
)
196+
response = await self._get_vehicle_data("cockpit")
204197
return cast(
205198
models.KamereonVehicleCockpitData,
206199
response.get_attributes(schemas.KamereonVehicleCockpitDataSchema),
207200
)
208201

209202
async def get_lock_status(self) -> models.KamereonVehicleLockStatusData:
210203
"""Get vehicle lock status."""
211-
response = await self.session.get_vehicle_data(
212-
account_id=self.account_id,
213-
vin=self.vin,
214-
endpoint="lock-status",
215-
)
204+
response = await self._get_vehicle_data("lock-status")
216205
return cast(
217206
models.KamereonVehicleLockStatusData,
218207
response.get_attributes(schemas.KamereonVehicleLockStatusDataSchema),
219208
)
220209

221210
async def get_res_state(self) -> models.KamereonVehicleResStateData:
222211
"""Get vehicle res state."""
223-
response = await self.session.get_vehicle_data(
224-
account_id=self.account_id,
225-
vin=self.vin,
226-
endpoint="res-state",
227-
)
212+
response = await self._get_vehicle_data("res-state")
228213
return cast(
229214
models.KamereonVehicleResStateData,
230215
response.get_attributes(schemas.KamereonVehicleResStateDataSchema),
231216
)
232217

233218
async def get_charging_settings(self) -> models.KamereonVehicleChargingSettingsData:
234219
"""Get vehicle charging settings."""
235-
response = await self.session.get_vehicle_data(
236-
account_id=self.account_id,
237-
vin=self.vin,
238-
endpoint="charging-settings",
239-
)
220+
response = await self._get_vehicle_data("charging-settings")
240221
return cast(
241222
models.KamereonVehicleChargingSettingsData,
242223
response.get_attributes(schemas.KamereonVehicleChargingSettingsDataSchema),
@@ -246,11 +227,7 @@ async def get_notification_settings(
246227
self,
247228
) -> models.KamereonVehicleNotificationSettingsData:
248229
"""Get vehicle notification settings."""
249-
response = await self.session.get_vehicle_data(
250-
account_id=self.account_id,
251-
vin=self.vin,
252-
endpoint="notification-settings",
253-
)
230+
response = await self._get_vehicle_data("notification-settings")
254231
return cast(
255232
models.KamereonVehicleNotificationSettingsData,
256233
response.get_attributes(

0 commit comments

Comments
 (0)