diff --git a/.github/build_and_publish.yml b/.github/build_and_publish.yml new file mode 100644 index 0000000..defdbe3 --- /dev/null +++ b/.github/build_and_publish.yml @@ -0,0 +1,35 @@ +name: Build and Upload Python Package + +on: + push: + tags: + - "v*" + +jobs: + pypi-publish: + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/carconnectivity-connector-volkswagen + permissions: + id-token: write + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install build twine + - name: Build + run: | + python -m build + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + + + diff --git a/src/carconnectivity_connectors/volkswagen/auth/openid_session.py b/src/carconnectivity_connectors/volkswagen/auth/openid_session.py index 2a819e6..025be36 100644 --- a/src/carconnectivity_connectors/volkswagen/auth/openid_session.py +++ b/src/carconnectivity_connectors/volkswagen/auth/openid_session.py @@ -369,7 +369,7 @@ def request( # noqa: C901 self.login() url, headers, data = self.add_token(url, body=data, headers=headers, access_type=access_type, token=token) except MissingTokenError: - LOG.error('Missing token') + LOG.info('Missing token, need new login') self.login() url, headers, data = self.add_token(url, body=data, headers=headers, access_type=access_type, token=token) diff --git a/src/carconnectivity_connectors/volkswagen/connector.py b/src/carconnectivity_connectors/volkswagen/connector.py index 5ac8f80..0412853 100644 --- a/src/carconnectivity_connectors/volkswagen/connector.py +++ b/src/carconnectivity_connectors/volkswagen/connector.py @@ -19,6 +19,7 @@ from carconnectivity.doors import Doors from carconnectivity.windows import Windows from carconnectivity.lights import Lights +from carconnectivity.attributes import BooleanAttribute, DurationAttribute, DateAttribute from carconnectivity_connectors.base.connector import BaseConnector from carconnectivity_connectors.volkswagen.auth.session_manager import SessionManager, SessionUser, Service from carconnectivity_connectors.volkswagen.vehicle import VolkswagenVehicle, VolkswagenElectricVehicle, VolkswagenCombustionVehicle, \ @@ -47,17 +48,22 @@ class Connector(BaseConnector): Attributes: max_age (Optional[int]): Maximum age for cached data in seconds. """ - def __init__(self, car_connectivity: CarConnectivity, config: Dict) -> None: - BaseConnector.__init__(self, car_connectivity, config) + def __init__(self, connector_id: str, car_connectivity: CarConnectivity, config: Dict) -> None: + BaseConnector.__init__(self, connector_id=connector_id, car_connectivity=car_connectivity, config=config) self._background_thread: Optional[threading.Thread] = None self._stop_event = threading.Event() + self.connected: BooleanAttribute = BooleanAttribute(name="connected", parent=self) + self.interval: DurationAttribute = DurationAttribute(name="interval", parent=self) + self.last_update: DateAttribute = DateAttribute(name="last_update", parent=self) + # Configure logging if 'log_level' in config and config['log_level'] is not None: config['log_level'] = config['log_level'].upper() if config['log_level'] in logging.getLevelNamesMapping(): LOG.setLevel(config['log_level']) + self.log_level._set_value(config['log_level']) # pylint: disable=protected-access logging.getLogger('requests').setLevel(config['log_level']) logging.getLogger('urllib3').setLevel(config['log_level']) logging.getLogger('oauthlib').setLevel(config['log_level']) @@ -97,15 +103,15 @@ def __init__(self, car_connectivity: CarConnectivity, config: Dict) -> None: except FileNotFoundError as err: raise AuthenticationError(f'{netrc_filename} netrc-file was not found. Create it or provide username and password in config') from err - self.interval: int = 300 + interval: int = 300 if 'interval' in self.config: - self.interval = self.config['interval'] - if self.interval < 180: - raise ValueError('Interval must be at least 180 seconds') - - self.max_age: int = self.interval - 1 + interval = self.config['interval'] + if interval < 300: + raise ValueError('Intervall must be at least 300 seconds') + self.max_age: int = interval - 1 if 'max_age' in self.config: self.max_age = self.config['max_age'] + self.interval._set_value(timedelta(seconds=interval)) # pylint: disable=protected-access if username is None or password is None: raise AuthenticationError('Username or password not provided') @@ -123,22 +129,33 @@ def startup(self) -> None: def _background_loop(self) -> None: self._stop_event.clear() while not self._stop_event.is_set(): + interval = 300 try: - self.fetch_all() - except TooManyRequestsError: - LOG.error('Retrieval error during update. Too many requests from your account. Will try again after 15 minutes') + try: + self.fetch_all() + self.last_update._set_value(value=datetime.now()) # pylint: disable=protected-access + if self.interval.value is not None: + interval: int = self.interval.value.total_seconds() + except Exception: + self.connected._set_value(value=False) # pylint: disable=protected-access + if self.interval.value is not None: + interval: int = self.interval.value.total_seconds() + raise + except TooManyRequestsError as err: + LOG.error('Retrieval error during update. Too many requests from your account (%s). Will try again after 15 minutes', str(err)) self._stop_event.wait(900) - except RetrievalError: - LOG.error('Retrieval error during update. Will try again after configured interval of %ss', self.interval) - self._stop_event.wait(self.interval) - except APICompatibilityError: - LOG.error('API compatability error during update. Will try again after configured interval of %ss', self.interval) - self._stop_event.wait(self.interval) - except TemporaryAuthenticationError: - LOG.error('Temporary authentification error during update. Will try again after configured interval of %ss', self.interval) - self._stop_event.wait(self.interval) + except RetrievalError as err: + LOG.error('Retrieval error during update (%s). Will try again after configured interval of %ss', str(err), interval) + self._stop_event.wait(interval) + except APICompatibilityError as err: + LOG.error('API compatability error during update (%s). Will try again after configured interval of %ss', str(err), interval) + self._stop_event.wait(interval) + except TemporaryAuthenticationError as err: + LOG.error('Temporary authentification error during update (%s). Will try again after configured interval of %ss', str(err), interval) + self._stop_event.wait(interval) else: - self._stop_event.wait(self.interval) + self.connected._set_value(value=True) # pylint: disable=protected-access + self._stop_event.wait(interval) def persist(self) -> None: """ @@ -181,6 +198,7 @@ def fetch_all(self) -> None: This method calls the `fetch_vehicles` method to retrieve vehicle data. """ self.fetch_vehicles() + self.car_connectivity.transaction_end() def fetch_vehicles(self) -> None: """ @@ -562,18 +580,18 @@ def _fetch_data(self, url, session, force=False, allow_empty=False, allow_http_e elif not allow_http_error or (allowed_errors is not None and status_response.status_code not in allowed_errors): raise RetrievalError(f'Could not fetch data. Status Code was: {status_response.status_code}') except requests.exceptions.ConnectionError as connection_error: - raise RetrievalError from connection_error + raise RetrievalError(f'Connection error: {connection_error}') from connection_error except requests.exceptions.ChunkedEncodingError as chunked_encoding_error: - raise RetrievalError from chunked_encoding_error + raise RetrievalError(f'Error: {chunked_encoding_error}') from chunked_encoding_error except requests.exceptions.ReadTimeout as timeout_error: - raise RetrievalError from timeout_error + raise RetrievalError(f'Timeout during read: {timeout_error}') from timeout_error except requests.exceptions.RetryError as retry_error: - raise RetrievalError from retry_error + raise RetrievalError(f'Retrying failed: {retry_error}') from retry_error except requests.exceptions.JSONDecodeError as json_error: if allow_empty: data = None else: - raise RetrievalError from json_error + raise RetrievalError(f'JSON decode error: {json_error}') from json_error return data def get_version(self) -> str: