myvsb-developer-api 0.1.2__py3-none-any.whl

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.
@@ -0,0 +1,3 @@
1
+ SANDBOX_BASE_URL = "https://developer.sandbox.connect.skoda-auto.cz"
2
+ PRODUCTION_BASE_URL = "https://developer.connect.skoda-auto.cz"
3
+ TEST_BASE_URL = "https://public.test-api.connect.skoda-auto.cz"
@@ -0,0 +1,37 @@
1
+ from typing import Optional
2
+
3
+
4
+ class OpenApiError(Exception):
5
+ """Obecná chyba při komunikaci se Škoda API."""
6
+ def __init__(self, message: str, status_code: Optional[int] = None) -> None:
7
+ super().__init__(message)
8
+ self.status_code = status_code
9
+
10
+
11
+ class OpenApiAuthenticationError(OpenApiError):
12
+ """Error 401 - Invalid or Expired X-API-Key."""
13
+ def __init__(self, message: str = "Invalid or expired X-API-Key.") -> None:
14
+ super().__init__(message, status_code=401)
15
+
16
+
17
+ class OpenApiForbiddenError(OpenApiError):
18
+ """Error 403 - Forbidden access to the requested resource."""
19
+ def __init__(self, message: str = "Access to the requested resource is forbidden.") -> None:
20
+ super().__init__(message, status_code=403)
21
+
22
+
23
+ class OpenApiVehicleNotFoundError(OpenApiError):
24
+ """Error 404 - Vehicle with this VIN does not exist."""
25
+ def __init__(self, message: str = "Vehicle with this VIN does not exist.") -> None:
26
+ super().__init__(message, status_code=404)
27
+
28
+
29
+ class OpenApiRateLimitError(OpenApiError):
30
+ """Error 429 - Rate limit exceeded or weak 12V battery."""
31
+ def __init__(self, message: str = "Rate limit exceeded or weak 12V battery.") -> None:
32
+ super().__init__(message, status_code=429)
33
+
34
+ class OpenApiServerError(OpenApiError):
35
+ """Error 500 / 504 - Server error or timeout."""
36
+ def __init__(self, message: str, status_code: int = 500) -> None:
37
+ super().__init__(message, status_code=status_code)
@@ -0,0 +1,118 @@
1
+ import logging
2
+ from typing import Any, Optional, List
3
+ from aiohttp import ClientSession
4
+ from .rest_api import SkodaRestAPI
5
+ from models.vehicle import VehicleResponse
6
+ from models.vehicle_status import VehicleStatus
7
+
8
+ class OpenAPIClient:
9
+ """Client for interacting with rest API and used in HomeAssistant."""
10
+
11
+ def __init__(self, api_key: str, session: ClientSession) -> None:
12
+ self.rest_api = SkodaRestAPI(api_key=api_key, session=session)
13
+
14
+ async def get_vehicle(self, vin: str, include: Optional[List[str]] = None) -> VehicleResponse:
15
+ """Get the actual vehicle data and return the parsed Pydantic model."""
16
+ endpoint_result = await self.rest_api.get_vehicle(vin, include=include)
17
+ return endpoint_result.result
18
+
19
+ async def get_vehicle_status(self, vin: str, include: Optional[List[str]] = None) -> VehicleStatus:
20
+ """Get the actual vehicle status and return the parsed Pydantic model."""
21
+ endpoint_result = await self.rest_api.get_vehicle_status(vin, include=include)
22
+ return endpoint_result.result
23
+
24
+ async def start_air_conditioning(self, vin: str, temperature: float) -> None:
25
+ """Starts the air conditioning for the vehicle with the given VIN."""
26
+ payload = {"targetTemperature": temperature}
27
+ await self.rest_api.start_air_conditioning(vin, payload=payload)
28
+
29
+ async def stop_air_conditioning(self, vin: str) -> None:
30
+ """Stops the air_conditioning of the vehicle and return None if it was succesfull else catch an exception in HomeAssistant"""
31
+ await self.rest_api.stop_air_conditioning(vin)
32
+
33
+ async def start_auxiliary_heating(self, vin: str, spin: str, duration_in_seconds: int = 120, start_mode: str = "HEATING", target_temperature: Optional[float] = None, unit: str = "CELSIUS") -> None:
34
+ """Starts the auxiliary heating for the vehicle with the given VIN."""
35
+ payload = {
36
+ "spin": spin,
37
+ "durationInSeconds": duration_in_seconds,
38
+ "startMode": start_mode
39
+ }
40
+ # If there is target temperature, add it into the payload
41
+ if target_temperature is not None:
42
+ payload["targetTemperature"] = {
43
+ "value": target_temperature,
44
+ "unit": unit
45
+ }
46
+
47
+ await self.rest_api.start_auxiliary_heating(vin, payload=payload)
48
+
49
+ async def stop_auxiliary_heating(self, vin: str) -> None:
50
+ """Stops the auxiliary_heating of the vehicle and return None if it was succesfull else catch an exception in HomeAssistant"""
51
+ await self.rest_api.stop_auxiliary_heating(vin)
52
+
53
+ async def start_active_ventilation(self, vin: str) -> None:
54
+ """Starts the active_ventilation for the vehicle with the given VIN."""
55
+ await self.rest_api.start_active_ventilation(vin)
56
+
57
+ async def stop_active_ventilation(self, vin: str) -> None:
58
+ """Stops the active_ventilation of the vehicle and return None if it was succesfull else catch an exception in HomeAssistant"""
59
+ await self.rest_api.stop_active_ventilation(vin)
60
+
61
+ async def start_charging(self, vin: str) -> None:
62
+ """Starts the charging process for the vehicle with the given VIN."""
63
+ await self.rest_api.start_charging(vin)
64
+
65
+ async def stop_charging(self, vin: str) -> None:
66
+ """Stops the charging process for the vehicle with the given VIN."""
67
+ await self.rest_api.stop_charging(vin)
68
+
69
+ async def connect(self):
70
+ """TODO: Implementation of connect mechanism."""
71
+ pass
72
+
73
+ async def disconnect(self) -> None:
74
+ """TODO: Implementation of disconnect mechanism."""
75
+ pass
76
+
77
+ async def verify_spin(self, spin: str) -> bool:
78
+ """TODO: Implementation of SPIN verification."""
79
+ pass
80
+
81
+ """
82
+ ======= NOT IMPLEMENTED FUNCTIONS - CAN BE USED SOMETIME IN THE FUTURE =======
83
+ """
84
+ async def refresh_auxiliary_heating(self, vin: str) -> None:
85
+ """Refreshes the auxiliary heating status of the vehicle with the given VIN."""
86
+ pass
87
+
88
+ async def set_reduced_current_limit(self, vin: str) -> None:
89
+ """Sets the charging current limit (e.g., to REDUCED or MAXIMUM) for the vehicle with the given VIN."""
90
+ pass
91
+
92
+ async def set_seats_heating(self, vin: str) -> None:
93
+ """Configures the seats heating settings for the vehicle with the given VIN."""
94
+ pass
95
+
96
+ async def start_window_heating(self, vin: str) -> None:
97
+ """Starts the electric window heating for the vehicle with the given VIN."""
98
+ pass
99
+
100
+ async def stop_window_heating(self, vin: str) -> None:
101
+ """Stops the window heating of the vehicle and return None if it was succesfull else catch an exception in HomeAssistant."""
102
+ pass
103
+
104
+ async def set_windows_heating(self, vin: str) -> None:
105
+ """Configures the front and rear windows heating settings for the vehicle with the given VIN."""
106
+ pass
107
+
108
+ async def set_auto_unlock_plug(self, vin: str) -> None:
109
+ """Sets the automatic unlocking behavior of the charging plug (e.g., PERMANENT or OFF) for the vehicle with the given VIN."""
110
+ pass
111
+ async def set_charge_limit(self, vin: str, limit: int) -> None:
112
+ """Sets the maximum charging limit in percent for the vehicle with the given VIN."""
113
+ pass
114
+
115
+ async def set_target_temperature(self, vin: str, temperature: float) -> None:
116
+ """Sets the target temperature for AC or auxiliary heating for the vehicle with the given VIN."""
117
+ pass
118
+ # TODO: Add more methods for other endpoints as needed
@@ -0,0 +1,257 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import Generic, Optional, TypeVar, Any, List
4
+ from dataclasses import dataclass
5
+ from aiohttp import ClientResponseError, ClientSession
6
+ from .const import SANDBOX_BASE_URL, PRODUCTION_BASE_URL, TEST_BASE_URL
7
+ from models.vehicle import VehicleResponse
8
+ from models.vehicle_status import VehicleStatus
9
+ from models.vehicle import Odometer
10
+ from models.air_conditioning import AirConditioning
11
+ from models.parking_position import ParkingPosition
12
+ from models.auxiliary_heating import AuxiliaryHeating
13
+ from models.charging import Charging
14
+ from models.active_ventilation import ActiveVentilation
15
+ from .exceptions import (
16
+ OpenApiError,
17
+ OpenApiAuthenticationError,
18
+ OpenApiForbiddenError,
19
+ OpenApiVehicleNotFoundError,
20
+ OpenApiRateLimitError,
21
+ )
22
+
23
+ T = TypeVar("T")
24
+ _LOGGER = logging.getLogger(__name__)
25
+
26
+ class GetEndpointResult(Generic[T]):
27
+ """Wrapper for the result of a GET request to an endpoint, containing the URL, raw JSON data, and the parsed Pydantic model."""
28
+ def __init__(self, url: str, raw_json: Any, result: T) -> None:
29
+ self.url = url
30
+ self.raw_json = raw_json
31
+ self.result = result # Final Pydantic model after parsing raw Json.
32
+
33
+ class PostEndpointResult:
34
+ """Wrapper for the result of a POST request to an endpoint, containing the URL, status code, and headers."""
35
+ def __init__(self, url: str, status: int, headers: dict) -> None:
36
+ self.url = url
37
+ self.status = status
38
+ self.headers = headers # Užitečné např. pro sledování RateLimit-Remaining nebo X-API-Key-Expires-At
39
+
40
+
41
+ class SkodaRestAPI:
42
+ """Rest API client for interacting with the Škoda Open API."""
43
+
44
+ def __init__(self, api_key: str, session: ClientSession) -> None:
45
+ self._api_key = api_key
46
+ self._session = session
47
+ self._base_url = TEST_BASE_URL # Default to TEST_BASE_URL; can be changed to SANDBOX_BASE_URL or PRODUCTION_BASE_URL as needed.
48
+ #TODO will be exteded with methods for authorization, token management, and other endpoints as needed.
49
+
50
+ async def _make_post_request(self, url: str, json_data: Optional[dict] = None) -> int:
51
+ """Helper method to make POST requests to the Škoda API."""
52
+ full_url = f"{self._base_url}{url}"
53
+ headers = {
54
+ "X-API-Key": self._api_key,
55
+ "Accept": "application/json",
56
+ "Content-Type": "application/json",
57
+ }
58
+
59
+ try:
60
+ async with self._session.post(full_url, headers=headers, json=json_data) as response:
61
+ if response.status not in (200, 202):
62
+ match response.status:
63
+ case 401: raise OpenApiAuthenticationError("Invalid or expired X-API-Key.")
64
+ case 403: raise OpenApiForbiddenError("Access forbidden. User not allowed to execute operation.")
65
+ case 404: raise OpenApiVehicleNotFoundError("Vehicle with this VIN does not exist.")
66
+ case 429: raise OpenApiRateLimitError("Rate limit exceeded or insufficient battery level.")
67
+ case 400: raise OpenApiError("Bad request (e.g. validation or VIN issues).")
68
+
69
+ return response.status
70
+ except Exception as err:
71
+ if isinstance(err, OpenApiError):
72
+ raise
73
+ raise OpenApiError(f"There has been an error when trying to make the POST request: {err}") from err
74
+
75
+ async def _make_get_request(self, url: str, params: Optional[dict] = None) -> Any:
76
+ """Helper method to make GET requests to the Škoda API."""
77
+ full_url = f"{self._base_url}{url}"
78
+ headers = {
79
+ "X-API-Key": self._api_key,
80
+ "Accept": "application/json",
81
+ }
82
+
83
+ try:
84
+ async with self._session.get(full_url, headers=headers, params=params) as response:
85
+ if response.status not in (200, 202):
86
+ match response.status:
87
+ case 401: raise OpenApiAuthenticationError("Invalid or expired X-API-Key.")
88
+ case 403: raise OpenApiForbiddenError("Access to the requested resource is forbidden.")
89
+ case 404: raise OpenApiVehicleNotFoundError("Vehicle with this VIN does not exist.")
90
+ case 429: raise OpenApiRateLimitError("Rate limit exceeded or weak 12V battery.")
91
+ return await response.json()
92
+ except Exception as err:
93
+ raise OpenApiError(f"There has been an error when trying to make the request: {err}") from err
94
+
95
+ # =========================================================================
96
+ # ENDPOINT METHODS - GET
97
+ # =========================================================================
98
+ async def get_vehicle(self, vin: str, include: Optional[List[str]] = None) -> GetEndpointResult[VehicleResponse]:
99
+ """Call the GET /api/v1/vehicles/{vin} endpoint to retrieve vehicle data for a given VIN."""
100
+ url = f"/api/v1/vehicles/{vin}"
101
+ params = {}
102
+ if include:
103
+ params["include"] = ",".join(include)
104
+
105
+ raw_json = await self._make_get_request(url, params=params)
106
+
107
+ # Deserializace raw_json into Pydantic model VehicleResponse
108
+ full_response = VehicleResponse.model_validate(raw_json)
109
+
110
+ # Pack and retunr the result in GetEndpointResult
111
+ return GetEndpointResult(url=url, raw_json=raw_json, result=full_response)
112
+
113
+ async def get_vehicle_status(self, vin: str, include: Optional[List[str]] = None) -> GetEndpointResult[Optional[VehicleStatus]]:
114
+ """Retrieve ONLY the vehicle status by filtering via the 'include' parameter."""
115
+ url = f"/api/v1/vehicles/{vin}"
116
+ # Set the 'include' parameter to 'status' to retrieve only the vehicle status
117
+ params = {"include": "status"}
118
+
119
+ raw_json = await self._make_get_request(url, params=params)
120
+ # Parse whole response into VehicleResponse model
121
+ full_response = VehicleResponse.model_validate(raw_json)
122
+ status_model = full_response.vehicle.status
123
+
124
+ return GetEndpointResult(url=url, raw_json=raw_json, result=status_model)
125
+
126
+ async def get_air_conditioning(self, vin: str, include: Optional[List[str]] = None) -> GetEndpointResult[Optional[AirConditioning]]:
127
+ """Retrieve ONLY the vehicles air conditioning object by filtering via the 'include' parameter."""
128
+ url = f"/api/v1/vehicles/{vin}"
129
+ params = {"include": "airConditioning"}
130
+
131
+ raw_json = await self._make_get_request(url, params=params)
132
+
133
+ full_response = VehicleResponse.model_validate(raw_json)
134
+ status_model = full_response.vehicle.air_conditioning
135
+
136
+ return GetEndpointResult(url=url, raw_json=raw_json, result=status_model)
137
+
138
+ async def get_parking_positions(self, vin: str, include: Optional[List[str]] = None) -> GetEndpointResult[Optional[ParkingPosition]]:
139
+ """Retrieve ONLY the vehicle parking positions object by filtering via the 'include' parameter."""
140
+ url = f"/api/v1/vehicles/{vin}"
141
+ params = {"include": "parkingPosition"}
142
+
143
+ raw_json = await self._make_get_request(url, params=params)
144
+
145
+ full_response = VehicleResponse.model_validate(raw_json)
146
+ status_model = full_response.vehicle.parking_position
147
+
148
+ return GetEndpointResult(url=url, raw_json=raw_json, result=status_model)
149
+
150
+ async def get_auxiliary_heating(self, vin: str, include: Optional[List[str]] = None) -> GetEndpointResult[Optional[AuxiliaryHeating]]:
151
+ """Retrieve ONLY the vehicle auxiliary heating by filtering via the 'include' parameter."""
152
+ url = f"/api/v1/vehicles/{vin}"
153
+ params = {"include": "auxiliaryHeating"}
154
+
155
+ raw_json = await self._make_get_request(url, params=params)
156
+
157
+ full_response = VehicleResponse.model_validate(raw_json)
158
+ status_model = full_response.vehicle.auxiliary_heating
159
+
160
+ return GetEndpointResult(url=url, raw_json=raw_json, result=status_model)
161
+
162
+ async def get_odometer(self, vin: str, include: Optional[List[str]] = None) -> GetEndpointResult[Optional[Odometer]]:
163
+ """Retrieve ONLY the vehicle odometer object by filtering via the 'include' parameter."""
164
+ url = f"/api/v1/vehicles/{vin}"
165
+ params = {"include": "odometer"}
166
+
167
+ raw_json = await self._make_get_request(url, params=params)
168
+
169
+ full_response = VehicleResponse.model_validate(raw_json)
170
+ status_model = full_response.vehicle.odometer
171
+
172
+ return GetEndpointResult(url=url, raw_json=raw_json, result=status_model)
173
+
174
+ async def get_charging(self, vin: str, include: Optional[List[str]] = None) -> GetEndpointResult[Optional[Charging]]:
175
+ """Retrieve ONLY the vehicle charging object by filtering via the 'include' parameter."""
176
+ url = f"/api/v1/vehicles/{vin}"
177
+ params = {"include": "charging"}
178
+
179
+ raw_json = await self._make_get_request(url, params=params)
180
+
181
+ full_response = VehicleResponse.model_validate(raw_json)
182
+ status_model = full_response.vehicle.charging
183
+
184
+ return GetEndpointResult(url=url, raw_json=raw_json, result=status_model)
185
+
186
+ async def get_active_ventilation(self, vin: str, include: Optional[List[str]] = None) -> GetEndpointResult[Optional[ActiveVentilation]]:
187
+ """Retrieve ONLY the vehicle active ventilation object by filtering via the 'include' parameter."""
188
+ url = f"/api/v1/vehicles/{vin}"
189
+ params = {"include": "activeVentilation"}
190
+
191
+ raw_json = await self._make_get_request(url, params=params)
192
+
193
+ full_response = VehicleResponse.model_validate(raw_json)
194
+ status_model = full_response.vehicle.active_ventilation
195
+
196
+ return GetEndpointResult(url=url, raw_json=raw_json, result=status_model)
197
+
198
+ # =========================================================================
199
+ # ENDPOINT METHODS - POST
200
+ # =========================================================================
201
+ async def start_air_conditioning(self, vin: str, payload: dict) -> PostEndpointResult:
202
+ """Starts the air conditioning for the vehicle with the given VIN. The payload should contain any necessary parameters for starting the air conditioning."""
203
+ url = f"/api/v1/vehicles/{vin}/air-conditioning/start"
204
+
205
+ status = await self._make_post_request(url, json_data=payload)
206
+ return PostEndpointResult(url=url, status=status, headers={})
207
+
208
+ async def stop_air_conditioning(self, vin: str) -> PostEndpointResult:
209
+ """Stops the air_conditioning for the given VIN"""
210
+ url = f"/api/v1/vehicles/{vin}/air-conditioning/stop"
211
+
212
+ status = await self._make_post_request(url)
213
+ return PostEndpointResult(url=url, status=status, headers={})
214
+
215
+ async def start_auxiliary_heating(self, vin: str, payload: dict) -> PostEndpointResult:
216
+ """Starts the auxiliary heating for the vehicle with the given VIN. The payload should contain any necessary parameters for starting the auxiliary heating."""
217
+ url = f"/api/v1/vehicles/{vin}/auxiliary-heating/start"
218
+
219
+ status = await self._make_post_request(url, json_data=payload)
220
+ return PostEndpointResult(url=url, status=status, headers={})
221
+
222
+ async def stop_auxiliary_heating(self, vin: str) -> PostEndpointResult:
223
+ """Stops the auxiliary heating for the given VIN"""
224
+ url = f"/api/v1/vehicles/{vin}/auxiliary-heating/stop"
225
+
226
+ status = await self._make_post_request(url)
227
+ return PostEndpointResult(url=url, status=status, headers={})
228
+
229
+ async def start_active_ventilation(self, vin: str, payload: dict) -> PostEndpointResult:
230
+ """Starts the active ventilation for the vehicle with the given VIN. The payload should contain any necessary parameters for starting the active ventilation."""
231
+ url = f"/api/v1/vehicles/{vin}/active-ventilation/start"
232
+
233
+ status = await self._make_post_request(url, json_data=payload)
234
+ return PostEndpointResult(url=url, status=status, headers={})
235
+
236
+ async def stop_active_ventilation(self, vin: str) -> PostEndpointResult:
237
+ """Stops the active ventilation for the given VIN"""
238
+ url = f"/api/v1/vehicles/{vin}/active-ventilation/stop"
239
+
240
+ status = await self._make_post_request(url)
241
+ return PostEndpointResult(url=url, status=status, headers={})
242
+
243
+ async def start_charging(self, vin: str) -> PostEndpointResult:
244
+ """Starts the charging process for the vehicle with the given VIN."""
245
+ url = f"/api/v1/vehicles/{vin}/charging/start"
246
+
247
+ status = await self._make_post_request(url)
248
+ return PostEndpointResult(url=url, status=status, headers={})
249
+
250
+ async def stop_charging(self, vin: str) -> PostEndpointResult:
251
+ """Stops the charging process for the given VIN"""
252
+ url = f"/api/v1/vehicles/{vin}/charging/stop"
253
+
254
+ status = await self._make_post_request(url)
255
+ return PostEndpointResult(url=url, status=status, headers={})
256
+
257
+ #TODO rest of the methods
File without changes
@@ -0,0 +1,12 @@
1
+ from typing import Optional
2
+ from .base_model import BaseModel
3
+ from .common import TargetTemperature
4
+ from .enums import (
5
+ VentilationState
6
+ )
7
+
8
+ class ActiveVentilation(BaseModel):
9
+ """Active ventilation status."""
10
+ state: VentilationState
11
+ duration_in_seconds: Optional[int] = None
12
+ car_captured_timestamp: Optional[str] = None
@@ -0,0 +1,23 @@
1
+ from typing import Optional
2
+
3
+ from .base_model import BaseModel
4
+ from .common import TargetTemperature
5
+ from .enums import OnOffState, AirConditioningState
6
+
7
+
8
+ class WindowHeating(BaseModel):
9
+ """State of the electric window heating."""
10
+ enabled: Optional[bool] = None
11
+ front: Optional[OnOffState] = None
12
+ rear: Optional[OnOffState] = None
13
+
14
+
15
+ class AirConditioning(BaseModel):
16
+ """Information about the vehicle's air conditioning."""
17
+ state: AirConditioningState
18
+ target_temperature: Optional[TargetTemperature] = None
19
+ estimated_reach_of_target_temperature_at: Optional[str] = None
20
+ air_conditioning_without_external_power: Optional[bool] = None
21
+ air_conditioning_at_unlock: Optional[bool] = None
22
+ window_heating: Optional[WindowHeating] = None
23
+ car_captured_timestamp: Optional[str] = None
@@ -0,0 +1,16 @@
1
+ from typing import Optional
2
+ from .base_model import BaseModel
3
+ from .common import TargetTemperature
4
+ from .enums import (
5
+ AuxiliaryHeatingState,
6
+ AuxiliaryHeatingStartMode,
7
+ )
8
+
9
+ class AuxiliaryHeating(BaseModel):
10
+ """Auxiliary heating status."""
11
+ state: AuxiliaryHeatingState
12
+ start_mode: Optional[AuxiliaryHeatingStartMode] = None
13
+ duration_in_seconds: Optional[int] = None
14
+ target_temperature: Optional[TargetTemperature] = None
15
+ estimated_reach_of_target_temperature_at: Optional[str] = None
16
+ car_captured_timestamp: Optional[str] = None
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+
3
+ def to_camel_case(snake_str: str) -> str:
4
+ words = snake_str.split('_')
5
+ return words[0] + ''.join(x.title() for x in words[1:])
6
+
7
+ class BaseModel(BaseModel):
8
+ model_config = ConfigDict(
9
+ alias_generator=to_camel_case,
10
+ populate_by_name=True,
11
+ )
12
+
13
+
@@ -0,0 +1,44 @@
1
+ from typing import Optional, List
2
+
3
+ from .base_model import BaseModel
4
+ from .enums import (
5
+ ChargingState,
6
+ ChargeType,
7
+ ChargeMode,
8
+ ChargeCareModeState,
9
+ AutoUnlockPlugState,
10
+ MaxChargeCurrentAcState
11
+ )
12
+
13
+ class BatteryStatus(BaseModel):
14
+ """Battery status information."""
15
+ remaining_cruising_range_in_meters: Optional[int] = None
16
+ state_of_charge_in_percent: Optional[int] = None
17
+
18
+ class ChargingStatus(BaseModel):
19
+ """Charging status information."""
20
+ charging_rate_in_kilometers_per_hour: Optional[float] = None
21
+ charge_power_in_kw: Optional[float] = None
22
+ remaining_time_to_fully_charged_in_minutes: Optional[int] = None
23
+ fully_charged_at: Optional[str] = None
24
+ state: Optional[ChargingState] = None
25
+ charge_type: Optional[ChargeType] = None
26
+ battery: Optional[BatteryStatus] = None
27
+
28
+ class ChargingSettings(BaseModel):
29
+ """Information about charging settings."""
30
+ target_state_of_charge_in_percent: Optional[int] = None
31
+ battery_care_mode_target_value_in_percent: Optional[int] = None
32
+ preferred_charge_mode: Optional[ChargeMode] = None
33
+ available_charge_modes: Optional[List[ChargeMode]] = None
34
+ charging_care_mode: Optional[ChargeCareModeState] = None
35
+ auto_unlock_plug_when_charged: Optional[AutoUnlockPlugState] = None
36
+ max_charge_current_ac: Optional[MaxChargeCurrentAcState] = None
37
+ max_charge_current_ac_ampere: Optional[int] = None
38
+
39
+ class Charging(BaseModel):
40
+ """Charging represents information about charging and battery settings."""
41
+ is_vehicle_in_saved_location: bool
42
+ status: Optional[ChargingStatus] = None
43
+ settings: Optional[ChargingSettings] = None
44
+ car_captured_timestamp: Optional[str] = None
@@ -0,0 +1,20 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ from pydantic import Field
4
+
5
+ from .base_model import BaseModel
6
+ from .enums import (
7
+ TemperatureUnit,
8
+ VehicleErrorState
9
+ )
10
+
11
+
12
+ class TargetTemperature(BaseModel):
13
+ """Target temperature settings for the vehicle."""
14
+ value: float
15
+ unit: TemperatureUnit
16
+
17
+ class VehicleError(BaseModel):
18
+ """Error information related to the vehicle."""
19
+ type: VehicleErrorState
20
+ description: str
@@ -0,0 +1,21 @@
1
+ from typing import Optional
2
+
3
+ from .base_model import BaseModel
4
+
5
+
6
+ class EngineRange(BaseModel):
7
+ """Details of the vehicle's engine range."""
8
+ engine_type: Optional[str] = None
9
+ current_soc_in_percent: Optional[int] = None
10
+ current_fuel_level_in_percent: Optional[int] = None
11
+ remaining_range_in_km: Optional[int] = None
12
+
13
+
14
+ class DrivingRange(BaseModel):
15
+ """Details of the vehicle's fuel status and driving range."""
16
+ car_type: Optional[str] = None
17
+ ad_blue_range: Optional[int] = None
18
+ total_range_in_km: Optional[int] = None
19
+ primary_engine_range: Optional[EngineRange] = None
20
+ secondary_engine_range: Optional[EngineRange] = None
21
+ car_captured_timestamp: Optional[str] = None
@@ -0,0 +1,132 @@
1
+ from enum import Enum
2
+
3
+ class TemperatureUnit(str, Enum):
4
+ CELSIUS = "CELSIUS"
5
+ FAHRENHEIT = "FAHRENHEIT"
6
+ UNKNOWN = "UNKNOWN"
7
+
8
+ class OnOffState(str, Enum):
9
+ ON = "ON"
10
+ OFF = "OFF"
11
+ UNKNOWN = "UNKNOWN"
12
+ UNSUPPORTED = "UNSUPPORTED"
13
+
14
+ class LockState(str, Enum):
15
+ LOCKED = "LOCKED"
16
+ UNLOCKED = "UNLOCKED"
17
+ UNKNOWN = "UNKNOWN"
18
+
19
+ class OpenCloseState(str, Enum):
20
+ OPEN = "OPEN"
21
+ CLOSED = "CLOSED"
22
+ UNKNOWN = "UNKNOWN"
23
+ UNSUPPORTED = "UNSUPPORTED"
24
+
25
+ class YesNoState(str, Enum):
26
+ YES = "YES"
27
+ NO = "NO"
28
+ UNKNOWN = "UNKNOWN"
29
+
30
+ class DoorsState(str, Enum):
31
+ YES = "YES"
32
+ NO = "NO"
33
+ TRUNK_OPENED = "TRUNK_OPENED"
34
+ OPENED = "OPENED"
35
+ UNKNOWN = "UNKNOWN"
36
+
37
+ class AuxiliaryHeatingState(str, Enum):
38
+ HEATING = "HEATING"
39
+ VENTILATION = "VENTILATION"
40
+ OFF = "OFF"
41
+ HEATING_AUXILIARY = "HEATING_AUXILIARY"
42
+ UNKNOWN = "UNKNOWN"
43
+ UNSUPPORTED = "UNSUPPORTED"
44
+
45
+ class AuxiliaryHeatingStartMode(str, Enum):
46
+ HEATING = "HEATING"
47
+ VENTILATION = "VENTILATION"
48
+
49
+ class VentilationState(str, Enum):
50
+ VENTILATION = "VENTILATION"
51
+ PREHEATING = "PREHEATING"
52
+ OFF = "OFF"
53
+ UNKNOWN = "UNKNOWN"
54
+ UNSUPPORTED = "UNSUPPORTED"
55
+
56
+ class HeaterSourceState(str, Enum):
57
+ AUTOMATIC = "AUTOMATIC"
58
+ ELECTRIC = "ELECTRIC"
59
+
60
+ class ChargeType(str, Enum):
61
+ AC = "AC"
62
+ DC = "DC"
63
+ OFF = "OFF"
64
+
65
+ class ChargingState(str, Enum):
66
+ CONNECT_CABLE = "CONNECT_CABLE"
67
+ CHARGING = "CHARGING"
68
+ CONSERVING = "CONSERVING"
69
+ READY_FOR_CHARGING = "READY_FOR_CHARGING"
70
+ DISCHARGING = "DISCHARGING"
71
+ CHARGING_INTERRUPTED = "CHARGING_INTERRUPTED"
72
+
73
+ class ChargeMode(str, Enum):
74
+ MANUAL = "MANUAL"
75
+ TIMER = "TIMER"
76
+ TIMER_CHARGING_WITH_CLIMATISATION = "TIMER_CHARGING_WITH_CLIMATISATION"
77
+ PREFERRED_CHARGING_TIMES = "PREFERRED_CHARGING_TIMES"
78
+ ONLY_OWN_CURRENT = "ONLY_OWN_CURRENT"
79
+ IMMEDIATE_DISCHARGING = "IMMEDIATE_DISCHARGING"
80
+ HOME_STORAGE_CHARGING = "HOME_STORAGE_CHARGING"
81
+
82
+ class ChargeCareModeState(str, Enum):
83
+ ACTIVATED = "ACTIVATED"
84
+ DEACTIVATED = "DEACTIVATED"
85
+
86
+ class AutoUnlockPlugState(str, Enum):
87
+ PERMANENT = "PERMANENT"
88
+ OFF = "OFF"
89
+
90
+ class MaxChargeCurrentAcState(str, Enum):
91
+ REDUCED = "REDUCED"
92
+ MAXIMUM = "MAXIMUM"
93
+
94
+ class MovementState(str, Enum):
95
+ IN_MOTION = "IN_MOTION"
96
+ PARKED = "PARKED"
97
+
98
+ class VehicleErrorState(str, Enum):
99
+ VEHICLE_STATUS_UNSUPPORTED = "VEHICLE_STATUS_UNSUPPORTED"
100
+ VEHICLE_STATUS_DISABLED = "VEHICLE_STATUS_DISABLED"
101
+ VEHICLE_STATUS_UNAVAILABLE = "VEHICLE_STATUS_UNAVAILABLE"
102
+ DRIVING_RANGE_UNSUPPORTED = "DRIVING_RANGE_UNSUPPORTED"
103
+ DRIVING_RANGE_DISABLED = "DRIVING_RANGE_DISABLED"
104
+ DRIVING_RANGE_UNAVAILABLE = "DRIVING_RANGE_UNAVAILABLE"
105
+ ODOMETER_UNSUPPORTED = "ODOMETER_UNSUPPORTED"
106
+ ODOMETER_DISABLED = "ODOMETER_DISABLED"
107
+ ODOMETER_UNAVAILABLE = "ODOMETER_UNAVAILABLE"
108
+ PARKING_POSITION_UNSUPPORTED = "PARKING_POSITION_UNSUPPORTED"
109
+ PARKING_POSITION_DISABLED = "PARKING_POSITION_DISABLED"
110
+ PARKING_POSITION_UNAVAILABLE = "PARKING_POSITION_UNAVAILABLE"
111
+ AIR_CONDITIONING_UNSUPPORTED = "AIR_CONDITIONING_UNSUPPORTED"
112
+ AIR_CONDITIONING_DISABLED = "AIR_CONDITIONING_DISABLED"
113
+ AIR_CONDITIONING_UNAVAILABLE = "AIR_CONDITIONING_UNAVAILABLE"
114
+ AUXILIARY_HEATING_UNSUPPORTED = "AUXILIARY_HEATING_UNSUPPORTED"
115
+ AUXILIARY_HEATING_DISABLED = "AUXILIARY_HEATING_DISABLED"
116
+ AUXILIARY_HEATING_UNAVAILABLE = "AUXILIARY_HEATING_UNAVAILABLE"
117
+ ACTIVE_VENTILATION_UNSUPPORTED = "ACTIVE_VENTILATION_UNSUPPORTED"
118
+ ACTIVE_VENTILATION_DISABLED = "ACTIVE_VENTILATION_DISABLED"
119
+ ACTIVE_VENTILATION_UNAVAILABLE = "ACTIVE_VENTILATION_UNAVAILABLE"
120
+ CHARGING_UNSUPPORTED = "CHARGING_UNSUPPORTED"
121
+ CHARGING_DISABLED = "CHARGING_DISABLED"
122
+ CHARGING_UNAVAILABLE = "CHARGING_UNAVAILABLE"
123
+
124
+ class AirConditioningState(str, Enum):
125
+ OFF = "OFF"
126
+ COOLING = "COOLING"
127
+ HEATING = "HEATING"
128
+ HEATING_AUXILIARY = "HEATING_AUXILIARY"
129
+ VENTILATION = "VENTILATION"
130
+ COMPLETED = "COMPLETED"
131
+ UNKNOWN = "UNKNOWN"
132
+ UNSUPPORTED = "UNSUPPORTED"
@@ -0,0 +1,17 @@
1
+ from typing import Optional
2
+ from .base_model import BaseModel
3
+ from .enums import (
4
+ MovementState
5
+ )
6
+
7
+ class GPRCoordinates(BaseModel):
8
+ """GPR coordinates of the vehicle."""
9
+ latitude: float
10
+ longitude: float
11
+
12
+ class ParkingPosition(BaseModel):
13
+ """Parking position information."""
14
+ state: MovementState
15
+ gps_coordinates: Optional[GPRCoordinates] = None
16
+ formatted_address: Optional[str] = None
17
+
@@ -0,0 +1,41 @@
1
+ from datetime import datetime
2
+ from typing import List, Optional
3
+ from pydantic import Field
4
+
5
+ from .base_model import BaseModel
6
+
7
+ from .vehicle_status import VehicleStatus
8
+ from .common import VehicleError
9
+ from .parking_position import ParkingPosition
10
+ from .auxiliary_heating import AuxiliaryHeating
11
+ from .active_ventilation import ActiveVentilation
12
+ from .charging import Charging
13
+ from .driving_range import DrivingRange
14
+ from .air_conditioning import AirConditioning
15
+
16
+ class Odometer(BaseModel):
17
+ """Odometer of the vehicle."""
18
+ mileage_in_km: int
19
+ car_captured_timestamp: Optional[str]
20
+
21
+ class VehicleObject(BaseModel):
22
+ """Pack whole vehicle object into one object for the vehicle."""
23
+ name: Optional[str] = None
24
+ vin: str
25
+ license_plate: Optional[str] = None
26
+ render_url: Optional[str] = None
27
+ odometer: Optional[Odometer] = None
28
+ status: Optional[VehicleStatus] = None
29
+ parking_position: Optional[ParkingPosition] = None
30
+ auxiliary_heating: Optional[AuxiliaryHeating] = None
31
+ active_ventilation: Optional[ActiveVentilation] = None
32
+ air_conditioning: Optional[AirConditioning] = None
33
+ driving_range: Optional[DrivingRange] = None
34
+ charging: Optional[Charging] = None
35
+
36
+ class VehicleResponse(BaseModel):
37
+ """Response object for vehicle data."""
38
+ vehicle: VehicleObject
39
+ errors: List[VehicleError] = Field(default_factory=list)
40
+
41
+
@@ -0,0 +1,33 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ from pydantic import Field
4
+
5
+ from .base_model import BaseModel
6
+ from .enums import (
7
+ LockState,
8
+ DoorsState,
9
+ YesNoState,
10
+ OpenCloseState,
11
+ OnOffState
12
+ )
13
+
14
+ class OverallVehicleStatus(BaseModel):
15
+ """Overall status of the vehicle."""
16
+ doors_locked: DoorsState
17
+ locked: YesNoState
18
+ doors: OpenCloseState
19
+ windows: OpenCloseState
20
+ lights: OnOffState
21
+ reliable_lock_status: Optional[LockState] = None
22
+
23
+ class VehicleStatusDetail(BaseModel):
24
+ """Status of individual components of the vehicle."""
25
+ sunroof: OpenCloseState
26
+ trunk: OpenCloseState
27
+ bonnet: OpenCloseState
28
+
29
+ class VehicleStatus(BaseModel):
30
+ """Pack all the objects into one object for the vehicle status."""
31
+ overall: OverallVehicleStatus
32
+ detail: VehicleStatusDetail
33
+ car_captured_timestamp: Optional[str] = None
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: myvsb-developer-api
3
+ Version: 0.1.2
4
+ Summary: Asynchronous Python library for the MyVSB Developer API
5
+ Author-email: net0045 <stepan.netolicky@vsb.cz>
6
+ Project-URL: Homepage, https://github.com/mobility-lab-vsb/myvsb-developer-api
7
+ Project-URL: Bug Tracker, https://github.com/mobility-lab-vsb/myvsb-developer-api/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Framework :: AsyncIO
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: aiohttp>=3.9.0
16
+ Requires-Dist: pydantic>=2.5.0
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest>=9.0.0; extra == "test"
19
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "test"
20
+ Requires-Dist: aresponses>=3.0.0; extra == "test"
21
+ Dynamic: license-file
22
+
23
+ # vsb-developer-api
24
+ VSB developer API is a Python library to interact with the new VSB API for connected-vehicle data and remote control of VSB vehicles.
@@ -0,0 +1,21 @@
1
+ myskoda_openapi/api_layer/const.py,sha256=akV2Cpvlpk92BFv8QEi4ggMmG5u1p2iVzVqODpJJKd4,196
2
+ myskoda_openapi/api_layer/exceptions.py,sha256=Ue7VO-E7S3aelmx3PhP0f1pxLp3Bd4UhRFsfXq8FGhc,1484
3
+ myskoda_openapi/api_layer/open_api_client.py,sha256=581Ngn5loPTKjPzk1a9lXBH4mnSkRG89f1ufQalR4zc,5592
4
+ myskoda_openapi/api_layer/rest_api.py,sha256=khFH5arVNBOpKx8eSIIL9JdV9kAjcsnbmUz2t6Nf--o,13292
5
+ myskoda_openapi/api_layer/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ myskoda_openapi/models/active_ventilation.py,sha256=cezsslMRA_uJ4rXZQIf4dLB-rrh4O-xU6-gw71koOt8,343
7
+ myskoda_openapi/models/air_conditioning.py,sha256=YKgIvW-Fhg6eJMurH5Zm7H7WKBummz9-r8KbgtGwpv8,821
8
+ myskoda_openapi/models/auxiliary_heating.py,sha256=-6Xa_ggwXvjWqhV2GB7VWV6AhwkS4hENaQWEsug32hs,568
9
+ myskoda_openapi/models/base_model.py,sha256=CegdMq2ASpixeGMw1QzRfmvRipPwWX4tfmle-rNkEgw,320
10
+ myskoda_openapi/models/charging.py,sha256=NQnxMdzl9EZsOP_M5a7dxGM2upWX3ozH2BSI9XGzioI,1704
11
+ myskoda_openapi/models/common.py,sha256=r41LSkDt5V9OvbT2wdsCu55rv1DcuIsZmxsq3NwaGyE,456
12
+ myskoda_openapi/models/driving_range.py,sha256=qQh39bEWd-Ou3cWb5CoAlmPXeFy8kbgLh4xBKO63vgM,713
13
+ myskoda_openapi/models/enums.py,sha256=yOcqbtGsj1kSyem3nLnBcf0SxSJ32vn2RH3LB-pAppk,4048
14
+ myskoda_openapi/models/parking_position.py,sha256=K_GLaeZqT2Fl7OQ9emAL3D_ohbmfXtc9ykdV3P4NjiY,419
15
+ myskoda_openapi/models/vehicle.py,sha256=3uffRuu2XKUENkRqNZvpV2pCy5Gew2QzTgm09kuqFHE,1384
16
+ myskoda_openapi/models/vehicle_status.py,sha256=GjgzIcQwC73wyq9hVhxKZgNnB1IzGIzi5lCYC4tThkQ,888
17
+ myvsb_developer_api-0.1.2.dist-info/licenses/LICENSE,sha256=JH8dA4ELqb8p0QcClibxxAuMyew7AElvtO3Xgcm4Yzo,1109
18
+ myvsb_developer_api-0.1.2.dist-info/METADATA,sha256=BqgWw8MaZzpX-7oKplXBk5hShxzAntcFOSM1lZQCQ4M,1019
19
+ myvsb_developer_api-0.1.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
20
+ myvsb_developer_api-0.1.2.dist-info/top_level.txt,sha256=Y4aqNREBD9ZK3NZl-DEL3Au3DMTcmiKJkc8TQGN7bXA,16
21
+ myvsb_developer_api-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mobility Lab @ VSB - Technical University of Ostrava
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ myskoda_openapi