renault-api 0.6.0__tar.gz → 0.6.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {renault_api-0.6.0 → renault_api-0.6.2}/PKG-INFO +1 -1
  2. {renault_api-0.6.0 → renault_api-0.6.2}/pyproject.toml +9 -9
  3. {renault_api-0.6.0 → renault_api-0.6.2}/pyproject.toml.orig +9 -9
  4. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/renault_client.py +9 -2
  5. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/gigya/enums.py +1 -0
  6. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/gigya/exceptions.py +16 -1
  7. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/gigya/models.py +14 -3
  8. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/kamereon/exceptions.py +6 -0
  9. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/kamereon/models.py +51 -3
  10. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/models.py +1 -1
  11. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/renault_session.py +7 -2
  12. {renault_api-0.6.0 → renault_api-0.6.2}/LICENSE.rst +0 -0
  13. {renault_api-0.6.0 → renault_api-0.6.2}/README.rst +0 -0
  14. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/__init__.py +0 -0
  15. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/__init__.py +0 -0
  16. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/__main__.py +0 -0
  17. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/charge/__init__.py +0 -0
  18. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/charge/commands.py +0 -0
  19. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/charge/control.py +0 -0
  20. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/charge/history.py +0 -0
  21. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/charge/schedule.py +0 -0
  22. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/charge/soc_levels.py +0 -0
  23. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/helpers.py +0 -0
  24. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/hvac/__init__.py +0 -0
  25. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/hvac/commands.py +0 -0
  26. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/hvac/control.py +0 -0
  27. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/hvac/history.py +0 -0
  28. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/renault_account.py +0 -0
  29. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/renault_settings.py +0 -0
  30. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/cli/renault_vehicle.py +0 -0
  31. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/const.py +0 -0
  32. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/credential.py +0 -0
  33. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/credential_store.py +0 -0
  34. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/exceptions.py +0 -0
  35. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/gigya/__init__.py +0 -0
  36. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/gigya/schemas.py +0 -0
  37. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/helpers.py +0 -0
  38. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/kamereon/__init__.py +0 -0
  39. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/kamereon/enums.py +0 -0
  40. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/kamereon/helpers.py +0 -0
  41. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/kamereon/schemas.py +0 -0
  42. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/py.typed +0 -0
  43. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/renault_account.py +0 -0
  44. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/renault_client.py +0 -0
  45. {renault_api-0.6.0 → renault_api-0.6.2}/src/renault_api/renault_vehicle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: renault-api
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Renault API
5
5
  Author: epenet
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "renault-api"
3
- version = "0.6.0"
3
+ version = "0.6.2"
4
4
  description = "Renault API"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE.rst"]
@@ -40,25 +40,25 @@ Changelog = "https://github.com/hacf-fr/renault-api/releases"
40
40
 
41
41
  [dependency-groups]
42
42
  dev = [
43
- "aiointercept==0.1.12",
43
+ "aiointercept==1.0.0",
44
44
  "codespell==2.4.3",
45
45
  "covdefaults==2.3.0",
46
- "coverage[toml]==7.16.2",
46
+ "coverage[toml]==7.17.0",
47
47
  "pre-commit-hooks==6.0.0",
48
- "prek==0.5.5",
48
+ "prek==0.5.7",
49
49
  "pytest==9.1.1",
50
- "pytest-asyncio==1.4.0",
50
+ "pytest-asyncio==1.4.1",
51
51
  "pytest-cov==7.1.0",
52
- "ruff==0.17.0",
52
+ "ruff==0.17.2",
53
53
  "syrupy==6.1.1",
54
- "ty==0.0.85",
54
+ "ty==0.0.92",
55
55
  "yamllint==1.38.0",
56
- "zizmor==1.30.1",
56
+ "zizmor==1.31.1",
57
57
  ]
58
58
  docs = [
59
59
  "furo==2025.12.19; python_version >= '3.14'",
60
60
  "myst-parser==5.1.0; python_version >= '3.14'",
61
- "sphinx==9.1.0; python_version >= '3.14'",
61
+ "sphinx==9.2.0; python_version >= '3.14'",
62
62
  "sphinx-autobuild==2025.8.25; python_version >= '3.14'",
63
63
  "sphinx-click==6.2.0; python_version >= '3.14'",
64
64
  ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "renault-api"
3
- version = "0.6.0"
3
+ version = "0.6.2"
4
4
  description = "Renault API"
5
5
  authors = [{ name = "epenet" }]
6
6
  license = "MIT"
@@ -40,26 +40,26 @@ Changelog = "https://github.com/hacf-fr/renault-api/releases"
40
40
 
41
41
  [dependency-groups]
42
42
  dev = [
43
- "aiointercept==0.1.12",
43
+ "aiointercept==1.0.0",
44
44
  "codespell==2.4.3",
45
45
  "covdefaults==2.3.0",
46
- "coverage[toml]==7.16.2",
46
+ "coverage[toml]==7.17.0",
47
47
  "pre-commit-hooks==6.0.0",
48
- "prek==0.5.5",
48
+ "prek==0.5.7",
49
49
  "pytest==9.1.1",
50
- "pytest-asyncio==1.4.0",
50
+ "pytest-asyncio==1.4.1",
51
51
  "pytest-cov==7.1.0",
52
- "ruff==0.17.0",
52
+ "ruff==0.17.2",
53
53
  "syrupy==6.1.1",
54
- "ty==0.0.85",
54
+ "ty==0.0.92",
55
55
  "yamllint==1.38.0",
56
- "zizmor==1.30.1",
56
+ "zizmor==1.31.1",
57
57
  ]
58
58
  # docs (Python >= 3.14)
59
59
  docs = [
60
60
  "furo==2025.12.19; python_version >= '3.14'",
61
61
  "myst-parser==5.1.0; python_version >= '3.14'",
62
- "sphinx==9.1.0; python_version >= '3.14'",
62
+ "sphinx==9.2.0; python_version >= '3.14'",
63
63
  "sphinx-autobuild==2025.8.25; python_version >= '3.14'",
64
64
  "sphinx-click==6.2.0; python_version >= '3.14'",
65
65
  ]
@@ -17,6 +17,7 @@ from renault_api.credential import Credential
17
17
  from renault_api.credential_store import CredentialStore
18
18
  from renault_api.exceptions import RenaultException
19
19
  from renault_api.gigya import GIGYA_LOGIN_TOKEN
20
+ from renault_api.gigya.exceptions import InvalidVerificationCodeException
20
21
  from renault_api.gigya.exceptions import PendingTwoFactorAuthenticationException
21
22
  from renault_api.helpers import get_api_keys
22
23
  from renault_api.renault_client import RenaultClient
@@ -133,8 +134,14 @@ async def _prompt_two_factor_auth(session: RenaultSession) -> None:
133
134
  )
134
135
  email_id = emails[choice - 1].id
135
136
  await session.request_two_factor_auth_code(email_id)
136
- code = click.prompt("Verification code")
137
- await session.complete_two_factor_auth(code)
137
+ while True:
138
+ code = click.prompt("Verification code")
139
+ try:
140
+ await session.complete_two_factor_auth(code)
141
+ except InvalidVerificationCodeException:
142
+ click.echo("Wrong verification code, please try again.", err=True)
143
+ else:
144
+ return
138
145
 
139
146
 
140
147
  async def login(
@@ -6,4 +6,5 @@ from enum import IntEnum
6
6
  class GigyaErrorCode(IntEnum):
7
7
  """Enum for Gigya error codes."""
8
8
 
9
+ INVALID_PARAMETER_VALUE = 400006
9
10
  PENDING_TWO_FACTOR_AUTHENTICATION = 403101
@@ -14,6 +14,7 @@ class GigyaResponseException(GigyaException):
14
14
 
15
15
  def __init__(self, error_code: int, error_details: str | None):
16
16
  """Initialise GigyaResponseException."""
17
+ super().__init__(error_code, error_details)
17
18
  self.error_code = error_code
18
19
  self.error_details = error_details
19
20
 
@@ -24,6 +25,16 @@ class InvalidCredentialsException(GigyaResponseException):
24
25
  pass
25
26
 
26
27
 
28
+ class InvalidVerificationCodeException(GigyaResponseException):
29
+ """Wrong two-factor authentication code (Gigya errorCode 400006).
30
+
31
+ Only raised when completing the email verification: the same challenge
32
+ can be retried with the right code.
33
+ """
34
+
35
+ pass
36
+
37
+
27
38
  class PendingTwoFactorAuthenticationException(GigyaResponseException):
28
39
  """Account requires two-factor authentication (Gigya errorCode 403101).
29
40
 
@@ -33,6 +44,10 @@ class PendingTwoFactorAuthenticationException(GigyaResponseException):
33
44
  """
34
45
 
35
46
  def __init__(self, error_code: int, error_details: str | None, reg_token: str):
36
- """Initialise PendingTwoFactorAuthenticationException."""
47
+ """Initialise PendingTwoFactorAuthenticationException.
48
+
49
+ `reg_token` is kept out of `args`, so that it doesn't appear in
50
+ `str()`/`repr()` of the exception, or in logged tracebacks.
51
+ """
37
52
  super().__init__(error_code, error_details)
38
53
  self.reg_token = reg_token
@@ -2,6 +2,7 @@
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from typing import Any
5
+ from typing import ClassVar
5
6
 
6
7
  from . import exceptions
7
8
  from .enums import GigyaErrorCode
@@ -19,6 +20,9 @@ COMMON_ERRRORS: list[dict[str, Any]] = [
19
20
  class GigyaResponse(BaseModel):
20
21
  """Gigya response."""
21
22
 
23
+ # Errors specific to an endpoint, checked before COMMON_ERRRORS
24
+ _SPECIFIC_ERRORS: ClassVar[list[dict[str, Any]]] = []
25
+
22
26
  errorCode: int
23
27
  errorDetails: str | None
24
28
  regToken: str | None
@@ -33,9 +37,9 @@ class GigyaResponse(BaseModel):
33
37
  raise exceptions.PendingTwoFactorAuthenticationException(
34
38
  self.errorCode, self.errorDetails, self.regToken
35
39
  )
36
- for common_error in COMMON_ERRRORS:
37
- if self.errorCode == common_error["errorCode"]:
38
- error_type = common_error["error_type"]
40
+ for known_error in (*self._SPECIFIC_ERRORS, *COMMON_ERRRORS):
41
+ if self.errorCode == known_error["errorCode"]:
42
+ error_type = known_error["error_type"]
39
43
  raise error_type(self.errorCode, self.errorDetails)
40
44
  raise exceptions.GigyaResponseException(self.errorCode, self.errorDetails)
41
45
 
@@ -160,6 +164,13 @@ class GigyaTfaSendEmailCodeResponse(GigyaResponse):
160
164
  class GigyaTfaEmailCompleteVerificationResponse(GigyaResponse):
161
165
  """Gigya response to GET on /accounts.tfa.email.completeVerification."""
162
166
 
167
+ _SPECIFIC_ERRORS: ClassVar[list[dict[str, Any]]] = [
168
+ {
169
+ "errorCode": GigyaErrorCode.INVALID_PARAMETER_VALUE,
170
+ "error_type": exceptions.InvalidVerificationCodeException,
171
+ }
172
+ ]
173
+
163
174
  providerAssertion: str | None
164
175
 
165
176
  def get_provider_assertion(self) -> str:
@@ -82,3 +82,9 @@ class ForbiddenException(KamereonResponseException):
82
82
  """The access is forbidden."""
83
83
 
84
84
  pass
85
+
86
+
87
+ class InsufficientBatteryLevelException(KamereonResponseException):
88
+ """The battery level is too low to perform this action."""
89
+
90
+ pass
@@ -63,6 +63,10 @@ COMMON_ERRRORS: list[dict[str, Any]] = [
63
63
  "errorCode": "409001",
64
64
  "error_type": exceptions.ChargeModeInProgressException,
65
65
  },
66
+ {
67
+ "errorCode": "err.func.wired.low.battery.level",
68
+ "error_type": exceptions.InsufficientBatteryLevelException,
69
+ },
66
70
  ]
67
71
 
68
72
  VEHICLE_SPECIFICATIONS: dict[str, dict[str, Any]] = {
@@ -390,6 +394,34 @@ _VEHICLE_ENDPOINTS: dict[str, dict[str, EndpointDefinition | None]] = {
390
394
  "res-state": None, # Reason: "err.func.wired.notFound"
391
395
  "soc-levels": None, # Reason: "err.func.wired.forbidden"
392
396
  },
397
+ "XBG2VE": { # DACIA SPRING MY25
398
+ "actions/charge-start": _KCM_ENDPOINTS["actions/charge-start-via-settings"],
399
+ "actions/charge-stop": None, # Reason: "err.func.wired.forbidden" (kcm pause-resume) / "err.func.wired.invalid-body-format" (default) # noqa: E501
400
+ "actions/horn-start": _DEFAULT_ENDPOINTS["actions/horn-start"],
401
+ "actions/hvac-start": _DEFAULT_ENDPOINTS["actions/hvac-start"],
402
+ # hvac-stop: "cancel" is accepted (200) but has no effect, "stop" works
403
+ "actions/hvac-stop": _KCA_ALTERNATIVE_ENDPOINTS["actions/hvac-stop"],
404
+ "actions/lights-start": _DEFAULT_ENDPOINTS["actions/lights-start"],
405
+ "actions/refresh-location": _DEFAULT_ENDPOINTS["actions/refresh-location"],
406
+ "alerts": None, # Reason: "err.func.wired.not-found"
407
+ "battery-status": _DEFAULT_ENDPOINTS["battery-status"],
408
+ "charge-history": None, # Reason: "err.func.wired.not-found"
409
+ "charge-mode": None, # Reason: "err.func.wired.forbidden"
410
+ "charge-schedule": _KCM_ENDPOINTS["charge-schedule-via-settings"],
411
+ "charges": _DEFAULT_ENDPOINTS["charges"],
412
+ "charging-settings": None, # Reason: "err.func.wired.forbidden"
413
+ "cockpit": _DEFAULT_ENDPOINTS["cockpit"],
414
+ "hvac-history": None, # Reason: "err.func.wired.not-found"
415
+ "hvac-sessions": None, # Reason: "err.func.wired.not-found"
416
+ "hvac-settings": _DEFAULT_ENDPOINTS["hvac-settings"],
417
+ "hvac-status": _DEFAULT_ENDPOINTS["hvac-status"],
418
+ "location": _DEFAULT_ENDPOINTS["location"],
419
+ "lock-status": None, # Reason: "There is no data for this vin and uid" (404)
420
+ "notification-settings": None, # Reason: "The vehicle does not have a GDC gateway" (400001) # noqa: E501
421
+ "pressure": None, # Reason: "There is no data for this vin and uid" (404)
422
+ "res-state": None, # Reason: "There is no data for this vin and uid" (404)
423
+ "soc-levels": _DEFAULT_ENDPOINTS["soc-levels"],
424
+ },
393
425
  "XCB1SE": { # SCENIC E-TECH
394
426
  "actions/charge-start": _KCM_ENDPOINTS["actions/charge-start-via-settings"],
395
427
  "actions/charge-stop": None, # Reason: "err.func.wired.forbidden" (kcm pause-resume) / "err.func.wired.invalid-body-format" (default) # noqa: E501
@@ -552,11 +584,11 @@ _VEHICLE_ENDPOINTS: dict[str, dict[str, EndpointDefinition | None]] = {
552
584
  "charge-mode": None, # Reason: "err.func.wired.forbidden"
553
585
  "charging-settings": None, # Reason: "err.func.wired.forbidden"
554
586
  "cockpit": _DEFAULT_ENDPOINTS["cockpit"], # confirmed
555
- "hvac-status": None,
587
+ "hvac-status": None, # Reason: "err.func.wired.notFound"
556
588
  "location": _DEFAULT_ENDPOINTS["location"],
557
- "lock-status": None,
589
+ "lock-status": _DEFAULT_ENDPOINTS["lock-status"], # confirmed
558
590
  "pressure": None, # Reason: 404
559
- "res-state": None,
591
+ "res-state": None, # Reason: "err.func.wired.notFound"
560
592
  "soc-levels": None, # Reason: "err.func.wired.forbidden"
561
593
  },
562
594
  "XJA1VP": { # CLIO V
@@ -1160,6 +1192,16 @@ class KamereonVehicleCockpitData(KamereonVehicleDataAttributes):
1160
1192
  totalMileage: float | None
1161
1193
 
1162
1194
 
1195
+ # Some vehicles (eg. XJB2SU, XHN1SU) report the door statuses as statusDoor*
1196
+ _LOCK_STATUS_ALIASES = {
1197
+ "doorStatusRearLeft": "statusDoorRearLeft",
1198
+ "doorStatusRearRight": "statusDoorRearRight",
1199
+ "doorStatusDriver": "statusDoorDriver",
1200
+ "doorStatusPassenger": "statusDoorPassenger",
1201
+ "hatchStatus": "statusDoorTailGate",
1202
+ }
1203
+
1204
+
1163
1205
  @dataclass
1164
1206
  class KamereonVehicleLockStatusData(KamereonVehicleDataAttributes):
1165
1207
  """Kamereon vehicle data lock-status attributes."""
@@ -1172,6 +1214,12 @@ class KamereonVehicleLockStatusData(KamereonVehicleDataAttributes):
1172
1214
  hatchStatus: str | None
1173
1215
  lastUpdateTime: str | None
1174
1216
 
1217
+ def __post_init__(self) -> None:
1218
+ """Fall back to the statusDoor* attributes used by newer vehicles."""
1219
+ for field, alias in _LOCK_STATUS_ALIASES.items():
1220
+ if getattr(self, field) is None:
1221
+ setattr(self, field, self.raw_data.get(alias))
1222
+
1175
1223
 
1176
1224
  @dataclass
1177
1225
  class KamereonVehicleResStateData(KamereonVehicleDataAttributes):
@@ -16,7 +16,7 @@ class BaseModel:
16
16
  class BaseSchema(marshmallow.Schema):
17
17
  """Base schema for Gigya and Kamereon models to exclude unknown fields."""
18
18
 
19
- class Meta:
19
+ class Meta: # ty: ignore[invalid-attribute-override] # astral-sh/ty#4711
20
20
  """Force unknown fields to 'exclude'."""
21
21
 
22
22
  unknown = marshmallow.EXCLUDE
@@ -157,10 +157,15 @@ class RenaultSession:
157
157
  ) -> None:
158
158
  """Submit the emailed one-time code and finish logging in.
159
159
 
160
+ Raises `InvalidVerificationCodeException` if the code is wrong. The
161
+ challenge stays pending, so this can be called again with the right
162
+ code, without requesting a new one.
163
+
160
164
  When `remember_device` is True (the default), Gigya should not ask
161
165
  for another code for this device for about 30 days. The device is
162
- identified by cookies, so the aiohttp session must keep its cookie
163
- jar between `login` and this call.
166
+ identified by `device_ids`, which are sent explicitly with each
167
+ request (so the aiohttp session may be shared): persist them with
168
+ `device_ids`, and restore them with `set_device_ids`.
164
169
  """
165
170
  pending_tfa = self._get_pending_tfa()
166
171
  if not pending_tfa.gigya_assertion or not pending_tfa.phv_token:
File without changes
File without changes