carconnectivity-connector-skoda 0.1__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.

Potentially problematic release.


This version of carconnectivity-connector-skoda might be problematic. Click here for more details.

@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Till Steinbach
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,123 @@
1
+ Metadata-Version: 2.2
2
+ Name: carconnectivity-connector-skoda
3
+ Version: 0.1
4
+ Summary: CarConnectivity connector for Skoda services
5
+ Author: Till Steinbach
6
+ License: MIT License
7
+
8
+ Copyright (c) 2021 Till Steinbach
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Classifier: Development Status :: 3 - Alpha
29
+ Classifier: License :: OSI Approved :: MIT License
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: Programming Language :: Python :: 3.9
32
+ Classifier: Programming Language :: Python :: 3.10
33
+ Classifier: Programming Language :: Python :: 3.11
34
+ Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Programming Language :: Python :: 3.13
36
+ Classifier: Topic :: Software Development :: Libraries
37
+ Requires-Python: >=3.8
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: carconnectivity~=0.1.0
41
+ Requires-Dist: oauthlib~=3.2.2
42
+ Requires-Dist: requests~=2.32.3
43
+ Requires-Dist: jwt~=1.3.1
44
+
45
+
46
+
47
+ # CarConnectivity Connector for Volkswagen Vehicles
48
+ [![GitHub sourcecode](https://img.shields.io/badge/Source-GitHub-green)](https://github.com/tillsteinbach/CarConnectivity-connector-skoda/)
49
+ [![GitHub release (latest by date)](https://img.shields.io/github/v/release/tillsteinbach/CarConnectivity-connector-skoda)](https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/latest)
50
+ [![GitHub](https://img.shields.io/github/license/tillsteinbach/CarConnectivity-connector-skoda)](https://github.com/tillsteinbach/CarConnectivity-connector-skoda/blob/master/LICENSE)
51
+ [![GitHub issues](https://img.shields.io/github/issues/tillsteinbach/CarConnectivity-connector-skoda)](https://github.com/tillsteinbach/CarConnectivity-connector-skoda/issues)
52
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/carconnectivity-connector-skoda?label=PyPI%20Downloads)](https://pypi.org/project/carconnectivity-connector-skoda/)
53
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/carconnectivity-connector-skoda)](https://pypi.org/project/carconnectivity-connector-skoda/)
54
+ [![Donate at PayPal](https://img.shields.io/badge/Donate-PayPal-2997d8)](https://www.paypal.com/donate?hosted_button_id=2BVFF5GJ9SXAJ)
55
+ [![Sponsor at Github](https://img.shields.io/badge/Sponsor-GitHub-28a745)](https://github.com/sponsors/tillsteinbach)
56
+
57
+ ## CarConnectivity will become the successor of [WeConnect-python](https://github.com/tillsteinbach/WeConnect-python) in 2025 with similar functionality but support for other brands beyond Volkswagen!
58
+
59
+ [CarConnectivity](https://github.com/tillsteinbach/CarConnectivity) is a python API to connect to various car services. This connector enables the integration of skoda vehicles through the WeConnect API. Look at [CarConnectivity](https://github.com/tillsteinbach/CarConnectivity) for other supported brands.
60
+
61
+ ## Configuration
62
+ In your carconnectivity.json configuration add a section for the skoda connector like this:
63
+ ```
64
+ {
65
+ "carConnectivity": {
66
+ "connectors": [
67
+ {
68
+ "type": "skoda",
69
+ "config": {
70
+ "username": "test@test.de",
71
+ "password": "testpassword123"
72
+ }
73
+ }
74
+ ]
75
+ }
76
+ }
77
+ ```
78
+ ### Credentials
79
+ If you do not want to provide your username or password inside the configuration you have to create a ".netrc" file at the appropriate location (usually this is your home folder):
80
+ ```
81
+ # For WeConnect
82
+ machine skoda
83
+ login test@test.de
84
+ password testpassword123
85
+ ```
86
+ In this case the configuration needs to look like this:
87
+ ```
88
+ {
89
+ "carConnectivity": {
90
+ "connectors": [
91
+ {
92
+ "type": "skoda",
93
+ "config": {
94
+ }
95
+ }
96
+ ]
97
+ }
98
+ }
99
+ ```
100
+
101
+ You can also provide the location of the netrc file in the configuration.
102
+ ```
103
+ {
104
+ "carConnectivity": {
105
+ "connectors": [
106
+ {
107
+ "type": "skoda",
108
+ "config": {
109
+ "netrc": "/some/path/on/your/filesystem"
110
+ }
111
+ }
112
+ ]
113
+ }
114
+ }
115
+ ```
116
+ The optional S-PIN needed for some commands can be provided in the account section of the netrc:
117
+ ```
118
+ # For WeConnect
119
+ machine skoda
120
+ login test@test.de
121
+ password testpassword123
122
+ account 1234
123
+ ```
@@ -0,0 +1,22 @@
1
+ carconnectivity_connectors/skoda/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ carconnectivity_connectors/skoda/_version.py,sha256=7V2ukxSfzXlOjRj21NN9XMWrUwTZJIQZggSzFRx5qs8,406
3
+ carconnectivity_connectors/skoda/capability.py,sha256=SnK9xVsqDA5EcWtBznzfWxe6gIpkYdjgY3UzNIc3OCY,4198
4
+ carconnectivity_connectors/skoda/charging.py,sha256=rG_GoDPetjyjWCyV6l65hgEDU6ths6MkMQ0KL25rbVU,6663
5
+ carconnectivity_connectors/skoda/climatization.py,sha256=-Nk4tO5C5_YYNQfUIUWBL7mGgR6-J0_pOZplLK8p_ms,1627
6
+ carconnectivity_connectors/skoda/command_impl.py,sha256=dtJsiuhwSuR1PXz7AMrdBowHqPxw6DKSAQKk4Z63GG8,2957
7
+ carconnectivity_connectors/skoda/connector.py,sha256=F-Fcp-qEfhp27VYE8iWf9q3Oy37AYq7DK0ouLDbMkuA,118512
8
+ carconnectivity_connectors/skoda/error.py,sha256=EnzzDxxJ1fswYT5QnMOVSebfoAcqoPmHKcG5i0Tqk3E,2405
9
+ carconnectivity_connectors/skoda/mqtt_client.py,sha256=lfHJfKOl-FBVd5hV6cS6ZMpZ53ktXyVc4lafvQls-Tk,37748
10
+ carconnectivity_connectors/skoda/vehicle.py,sha256=_ALtlBy7sKVHmqpqAhWNbMd9dto915_SdNWcRi_AqYU,3088
11
+ carconnectivity_connectors/skoda/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ carconnectivity_connectors/skoda/auth/auth_util.py,sha256=dGLUbUre0HBsTg_Ii5vW34f8DLrCykYJYCyzEvUBBEE,4434
13
+ carconnectivity_connectors/skoda/auth/my_skoda_session.py,sha256=lSh23SFJs8opjmPwHTv-KNIKDep_WY4aItSP4Zq7bT8,10396
14
+ carconnectivity_connectors/skoda/auth/openid_session.py,sha256=LusWi2FZZIL3buodGXZKUR0naLhhqeYv0uRW4V3wI2w,16842
15
+ carconnectivity_connectors/skoda/auth/session_manager.py,sha256=Uf1vujuDBYUCAXhYToOsZkgbTtfmY3Qe0ICTfwomBpI,2899
16
+ carconnectivity_connectors/skoda/auth/skoda_web_session.py,sha256=cjzMkzx473Sh-4RgZAQULeRRcxB1MboddldCVM_y5LE,10640
17
+ carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
18
+ carconnectivity_connector_skoda-0.1.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
19
+ carconnectivity_connector_skoda-0.1.dist-info/METADATA,sha256=2xIdgk49zxy287g-EgS_FR2alIUWPzwiCUvXSZdVj_Q,5331
20
+ carconnectivity_connector_skoda-0.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
21
+ carconnectivity_connector_skoda-0.1.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
22
+ carconnectivity_connector_skoda-0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ carconnectivity_connectors
File without changes
@@ -0,0 +1,16 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '0.1'
16
+ __version_tuple__ = version_tuple = (0, 1)
File without changes
@@ -0,0 +1,141 @@
1
+
2
+ """
3
+ This module provides utility functions and classes for handling authentication and parsing HTML forms
4
+ and scripts for the Volkswagen car connectivity connector.
5
+ """
6
+ from __future__ import annotations
7
+ from typing import TYPE_CHECKING
8
+
9
+ import json
10
+ import re
11
+ from html.parser import HTMLParser
12
+
13
+ if TYPE_CHECKING:
14
+ from typing import Optional, Dict
15
+
16
+
17
+ def add_bearer_auth_header(token, headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
18
+ """
19
+ Adds a Bearer token to the Authorization header.
20
+
21
+ Args:
22
+ token (str): The Bearer token to be added to the headers.
23
+ headers (Optional[Dict[str, str]]): An optional dictionary of headers to which the Authorization header will be added.
24
+ If not provided, a new dictionary will be created.
25
+
26
+ Returns:
27
+ Dict[str, str]: The headers dictionary with the added Authorization header.
28
+ """
29
+ headers = headers or {}
30
+ headers['Authorization'] = f'Bearer {token}'
31
+ return headers
32
+
33
+
34
+ class HTMLFormParser(HTMLParser):
35
+ """
36
+ A custom HTML parser to extract form data from HTML content.
37
+ """
38
+ def __init__(self, form_id) -> None:
39
+ super().__init__()
40
+ self._form_id = form_id
41
+ self._inside_form: bool = False
42
+ self.target = None
43
+ self.data = {}
44
+
45
+ def _get_attr(self, attrs, name):
46
+ for attr in attrs:
47
+ if attr[0] == name:
48
+ return attr[1]
49
+ return None
50
+
51
+ def handle_starttag(self, tag, attrs) -> None:
52
+ if self._inside_form and tag == 'input':
53
+ self.handle_input(attrs)
54
+ return
55
+
56
+ if tag == 'form' and self._get_attr(attrs, 'id') == self._form_id:
57
+ self._inside_form = True
58
+ self.target = self._get_attr(attrs, 'action')
59
+
60
+ def handle_endtag(self, tag) -> None:
61
+ if tag == 'form' and self._inside_form:
62
+ self._inside_form = False
63
+
64
+ def handle_input(self, attrs) -> None:
65
+ if not self._inside_form:
66
+ return
67
+
68
+ name = self._get_attr(attrs, 'name')
69
+ value = self._get_attr(attrs, 'value')
70
+
71
+ if name:
72
+ self.data[name] = value
73
+
74
+
75
+ class ScriptFormParser(HTMLParser):
76
+ fields: list[str] = []
77
+ targetField: str = ''
78
+
79
+ def __init__(self):
80
+ super().__init__()
81
+ self._inside_script = False
82
+ self.data = {}
83
+ self.target = None
84
+
85
+ def handle_starttag(self, tag, attrs) -> None:
86
+ if not self._inside_script and tag == 'script':
87
+ self._inside_script = True
88
+
89
+ def handle_endtag(self, tag) -> None:
90
+ if self._inside_script and tag == 'script':
91
+ self._inside_script = False
92
+
93
+ def handle_data(self, data) -> None:
94
+ if not self._inside_script:
95
+ return
96
+
97
+ match: re.Match[str] | None = re.search(r'templateModel: (.*?),\n', data)
98
+ if not match:
99
+ return
100
+
101
+ result = json.loads(match.group(1))
102
+ self.target = result.get(self.targetField, None)
103
+ self.data = {k: v for k, v in result.items() if k in self.fields}
104
+
105
+ match2 = re.search(r'csrf_token: \'(.*?)\'', data)
106
+ if match2:
107
+ self.data['_csrf'] = match2.group(1)
108
+
109
+
110
+ class CredentialsFormParser(ScriptFormParser):
111
+ fields: list[str] = ['relayState', 'hmac', 'registerCredentialsPath', 'error', 'errorCode']
112
+ targetField: str = 'postAction'
113
+
114
+
115
+ class TermsAndConditionsFormParser(ScriptFormParser):
116
+ fields: list[str] = ['relayState', 'hmac', 'countryOfResidence', 'legalDocuments']
117
+ targetField: str = 'loginUrl'
118
+
119
+ def handle_data(self, data) -> None:
120
+ if not self._inside_script:
121
+ return
122
+
123
+ super().handle_data(data)
124
+
125
+ if 'countryOfResidence' in self.data:
126
+ self.data['countryOfResidence'] = self.data['countryOfResidence'].upper()
127
+
128
+ if 'legalDocuments' not in self.data:
129
+ return
130
+
131
+ for key in self.data['legalDocuments'][0]:
132
+ # Skip unnecessary keys
133
+ if key in ('skipLink', 'declineLink', 'majorVersion', 'minorVersion', 'changeSummary'):
134
+ continue
135
+
136
+ # Move values under a new key while converting boolean values to 'yes' or 'no'
137
+ v = self.data['legalDocuments'][0][key]
138
+ self.data[f'legalDocuments[0].{key}'] = ('yes' if v else 'no') if isinstance(v, bool) else v
139
+
140
+ # Remove the original object
141
+ del self.data['legalDocuments']
@@ -0,0 +1,29 @@
1
+ """Implements a custom Retry class that allows for blacklisting certain status codes that will not retry."""
2
+ from urllib3.util.retry import Retry
3
+
4
+
5
+ class BlacklistRetry(Retry):
6
+ """
7
+ BlacklistRetry class extends the Retry class to include a blacklist of status codes that should not be retried.
8
+ """
9
+ def __init__(self, status_blacklist=None, **kwargs) -> None:
10
+ self.status_blacklist = status_blacklist
11
+ super().__init__(**kwargs)
12
+
13
+ def is_retry(self, method, status_code, has_retry_after=False) -> bool:
14
+ """
15
+ Determines if a request should be retried based on the HTTP method, status code,
16
+ and the presence of a 'Retry-After' header.
17
+
18
+ Args:
19
+ method (str): The HTTP method of the request (e.g., 'GET', 'POST').
20
+ status_code (int): The HTTP status code of the response.
21
+ has_retry_after (bool): Indicates if the response contains a 'Retry-After' header.
22
+
23
+ Returns:
24
+ bool: True if the request should be retried, False otherwise.
25
+ """
26
+ if self.status_blacklist is not None and status_code in self.status_blacklist:
27
+ return False
28
+ else:
29
+ return super().is_retry(method, status_code, has_retry_after)
@@ -0,0 +1,224 @@
1
+ """
2
+ Module implements the WeConnect Session handling.
3
+ """
4
+ from __future__ import annotations
5
+ from typing import TYPE_CHECKING
6
+
7
+ import json
8
+ import logging
9
+ import base64
10
+ import hashlib
11
+ import random
12
+ import string
13
+
14
+ from urllib.parse import parse_qsl, urlparse
15
+
16
+ import requests
17
+ from requests.models import CaseInsensitiveDict
18
+
19
+ from oauthlib.common import add_params_to_uri, generate_nonce, to_unicode
20
+ from oauthlib.oauth2 import InsecureTransportError
21
+ from oauthlib.oauth2 import is_secure_transport
22
+
23
+ from carconnectivity.errors import AuthenticationError, RetrievalError, TemporaryAuthenticationError
24
+
25
+ from carconnectivity_connectors.skoda.auth.openid_session import AccessType
26
+ from carconnectivity_connectors.skoda.auth.skoda_web_session import SkodaWebSession
27
+
28
+ if TYPE_CHECKING:
29
+ from typing import Set
30
+
31
+
32
+ LOG: logging.Logger = logging.getLogger("carconnectivity.connectors.skoda.auth")
33
+
34
+
35
+ class MySkodaSession(SkodaWebSession):
36
+ """
37
+ MySkodaSession class handles the authentication and session management for Volkswagen's WeConnect service.
38
+ """
39
+ def __init__(self, session_user, **kwargs) -> None:
40
+ super(MySkodaSession, self).__init__(client_id='7f045eee-7003-4379-9968-9355ed2adb06@apps_vw-dilab_com',
41
+ refresh_url='https://mysmob.api.connect.skoda-auto.cz/api/v1/authentication/refresh-token?tokenType=CONNECT',
42
+ scope='address badge birthdate cars driversLicense dealers email mileage mbb nationalIdentifier openid phone profession profile vin',
43
+ redirect_uri='myskoda://redirect/login/',
44
+ session_user=session_user,
45
+ **kwargs)
46
+
47
+ self.headers = CaseInsensitiveDict({
48
+ 'user-agent': 'Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 '
49
+ 'Chrome/74.0.3729.185 Mobile Safari/537.36',
50
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,'
51
+ 'application/signed-exchange;v=b3',
52
+ 'accept-language': 'en-US,en;q=0.9',
53
+ 'accept-encoding': 'gzip, deflate',
54
+ 'x-requested-with': 'cz.skodaauto.connect',
55
+ 'upgrade-insecure-requests': '1',
56
+ })
57
+
58
+ def login(self):
59
+ super(MySkodaSession, self).login()
60
+
61
+ verifier = "".join(random.choices(string.ascii_uppercase + string.digits, k=16))
62
+ verifier_hash = hashlib.sha256(verifier.encode("utf-8")).digest()
63
+ code_challenge = base64.b64encode(verifier_hash).decode("utf-8").replace("+", "-").replace("/", "_").rstrip("=")
64
+ # retrieve authorization URL
65
+ authorization_url = self.authorization_url(url='https://identity.vwgroup.io/oidc/v1/authorize', prompt='login', code_challenge=code_challenge,
66
+ code_challenge_method='s256')
67
+ # perform web authentication
68
+ response = self.do_web_auth(authorization_url)
69
+ # fetch tokens from web authentication response
70
+ self.fetch_tokens('https://mysmob.api.connect.skoda-auto.cz/api/v1/authentication/exchange-authorization-code?tokenType=CONNECT',
71
+ authorization_response=response, verifier=verifier)
72
+
73
+ def refresh(self) -> None:
74
+ # refresh tokens from refresh endpoint
75
+ self.refresh_tokens(
76
+ 'https://mysmob.api.connect.skoda-auto.cz/api/v1/authentication/refresh-token?tokenType=CONNECT',
77
+ )
78
+
79
+ def fetch_tokens(
80
+ self,
81
+ token_url,
82
+ authorization_response,
83
+ verifier,
84
+ **_
85
+ ):
86
+ """
87
+ Fetches tokens using the given token URL using the tokens from authorization response.
88
+
89
+ Args:
90
+ token_url (str): The URL to request the tokens from.
91
+ authorization_response (str, optional): The authorization response containing the tokens. Defaults to None.
92
+ **_ : Additional keyword arguments.
93
+
94
+ Returns:
95
+ dict: A dictionary containing the fetched tokens if successful.
96
+ None: If the tokens could not be fetched.
97
+
98
+ Raises:
99
+ TemporaryAuthenticationError: If the token request fails due to a temporary WeConnect failure.
100
+ """
101
+ # take token from authorization response (those are stored in self.token now!)
102
+ self.parse_from_fragment(authorization_response)
103
+
104
+ if self.token is not None and all(key in self.token for key in ('code', 'id_token')):
105
+ # Generate json body for token request
106
+ body: str = json.dumps(
107
+ {
108
+ 'redirectUri': 'myskoda://redirect/login/',
109
+ 'code': self.token['code'],
110
+ 'verifier': verifier
111
+ })
112
+
113
+ request_headers: CaseInsensitiveDict = self.headers # pyright: ignore reportAssignmentType
114
+ request_headers['accept'] = 'application/json'
115
+ request_headers['content-type'] = 'application/json'
116
+
117
+ # request tokens from token_url
118
+ token_response = self.post(token_url, headers=request_headers, data=body, allow_redirects=False,
119
+ access_type=AccessType.NONE) # pyright: ignore reportCallIssue
120
+ if token_response.status_code != requests.codes['ok']:
121
+ raise TemporaryAuthenticationError(f'Token could not be fetched due to temporary WeConnect failure: {token_response.status_code}')
122
+ # parse token from response body
123
+ token = self.parse_from_body(token_response.text)
124
+ return token
125
+ return None
126
+
127
+ def parse_from_body(self, token_response, state=None):
128
+ """
129
+ Fix strange token naming before parsing it with OAuthlib.
130
+ """
131
+ try:
132
+ # Tokens are in body of response in json format
133
+ token = json.loads(token_response)
134
+ except json.decoder.JSONDecodeError as err:
135
+ raise TemporaryAuthenticationError('Token could not be refreshed due to temporary WeConnect failure: json could not be decoded') from err
136
+ found_tokens: Set[str] = set()
137
+ # Fix token keys, we want access_token instead of accessToken
138
+ if 'accessToken' in token:
139
+ found_tokens.add('accessToken')
140
+ token['access_token'] = token.pop('accessToken')
141
+ # Fix token keys, we want id_token instead of idToken
142
+ if 'idToken' in token:
143
+ found_tokens.add('idToken')
144
+ token['id_token'] = token.pop('idToken')
145
+ # Fix token keys, we want refresh_token instead of refreshToken
146
+ if 'refreshToken' in token:
147
+ found_tokens.add('refreshToken')
148
+ token['refresh_token'] = token.pop('refreshToken')
149
+ LOG.debug(f'Found tokens in answer: {found_tokens}')
150
+ # generate json from fixed dict
151
+ fixed_token_response = to_unicode(json.dumps(token)).encode("utf-8")
152
+ # Let OAuthlib parse the token
153
+ return super(MySkodaSession, self).parse_from_body(token_response=fixed_token_response)
154
+
155
+ def refresh_tokens(
156
+ self,
157
+ token_url,
158
+ refresh_token=None,
159
+ auth=None,
160
+ timeout=None,
161
+ headers=None,
162
+ verify=True,
163
+ proxies=None,
164
+ **_
165
+ ):
166
+ """
167
+ Refreshes the authentication tokens using the provided refresh token.
168
+ Args:
169
+ token_url (str): The URL to request new tokens from.
170
+ refresh_token (str, optional): The refresh token to use. Defaults to None.
171
+ auth (tuple, optional): Authentication credentials. Defaults to None.
172
+ timeout (float or tuple, optional): How long to wait for the server to send data before giving up. Defaults to None.
173
+ headers (dict, optional): Headers to include in the request. Defaults to None.
174
+ verify (bool, optional): Whether to verify the server's TLS certificate. Defaults to True.
175
+ proxies (dict, optional): Proxies to use for the request. Defaults to None.
176
+ **_ (dict): Additional arguments.
177
+ Raises:
178
+ ValueError: If no token endpoint is set for auto_refresh.
179
+ InsecureTransportError: If the token URL is not secure.
180
+ AuthenticationError: If the server requests new authorization.
181
+ TemporaryAuthenticationError: If the token could not be refreshed due to a temporary server failure.
182
+ RetrievalError: If the status code from the server is not recognized.
183
+ Returns:
184
+ dict: The new tokens.
185
+ """
186
+ LOG.info('Refreshing tokens')
187
+ if not token_url:
188
+ raise ValueError("No token endpoint set for auto_refresh.")
189
+
190
+ if not is_secure_transport(token_url):
191
+ raise InsecureTransportError()
192
+
193
+ refresh_token = refresh_token or self.refresh_token
194
+ if refresh_token is None:
195
+ self.login()
196
+ return self.token
197
+
198
+ # Generate json body for token request
199
+ body: str = json.dumps(
200
+ {
201
+ 'token': refresh_token,
202
+ })
203
+
204
+ request_headers: CaseInsensitiveDict = self.headers # pyright: ignore reportAssignmentType
205
+ request_headers['accept'] = 'application/json'
206
+ request_headers['content-type'] = 'application/json'
207
+
208
+ try:
209
+ # request tokens from token_url
210
+ token_response = self.post(token_url, headers=request_headers, data=body, allow_redirects=False,
211
+ access_type=AccessType.NONE) # pyright: ignore reportCallIssue
212
+ if token_response.status_code == requests.codes['ok']:
213
+ # parse token from response body
214
+ token = self.parse_from_body(token_response.text)
215
+ return token
216
+ elif token_response.status_code == requests.codes['unauthorized']:
217
+ LOG.info('Refreshing tokens failed: Server requests new authorization, will login now')
218
+ self.login()
219
+ return self.token
220
+ else:
221
+ raise TemporaryAuthenticationError(f'Token could not be fetched due to temporary MySkoda failure: {token_response.status_code}')
222
+ except ConnectionError:
223
+ self.login()
224
+ return self.token