From 6ce1bbf15408e19ffb6b14a7b47cfb4a592b57dc Mon Sep 17 00:00:00 2001 From: Till Steinbach Date: Thu, 9 Jan 2025 21:49:51 +0100 Subject: [PATCH] try to deal with access messages from the broker --- .../skoda/connector.py | 29 +++++----- .../skoda/mqtt_client.py | 57 +++++++++++++++++-- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/carconnectivity_connectors/skoda/connector.py b/src/carconnectivity_connectors/skoda/connector.py index 94aaf07..b9b9954 100644 --- a/src/carconnectivity_connectors/skoda/connector.py +++ b/src/carconnectivity_connectors/skoda/connector.py @@ -312,7 +312,7 @@ def update_vehicles(self) -> None: if vehicle_to_update.capabilities.has_capability('AIR_CONDITIONING'): vehicle_to_update = self.fetch_air_conditioning(vehicle_to_update) - def fetch_charging(self, vehicle: SkodaElectricVehicle) -> SkodaElectricVehicle: + def fetch_charging(self, vehicle: SkodaElectricVehicle, no_cache: bool = False) -> SkodaElectricVehicle: """ Fetches the charging information for a given Skoda electric vehicle. @@ -329,7 +329,7 @@ def fetch_charging(self, vehicle: SkodaElectricVehicle) -> SkodaElectricVehicle: if vehicle.charging is None: raise ValueError('Vehicle has no charging object') url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}' - data: Dict[str, Any] | None = self._fetch_data(url, session=self.session) + data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache) if data is not None: if 'carCapturedTimestamp' in data and data['carCapturedTimestamp'] is not None: captured_at: datetime = robust_time_parse(data['carCapturedTimestamp']) @@ -372,7 +372,7 @@ def fetch_charging(self, vehicle: SkodaElectricVehicle) -> SkodaElectricVehicle: log_extra_keys(LOG_API, 'charging data', data, {'carCapturedTimestamp', 'status'}) return vehicle - def fetch_position(self, vehicle: SkodaVehicle) -> SkodaVehicle: + def fetch_position(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle: """ Fetches the position of the given Skoda vehicle and updates its position attributes. @@ -392,7 +392,7 @@ def fetch_position(self, vehicle: SkodaVehicle) -> SkodaVehicle: if vehicle.position is None: raise ValueError('Vehicle has no charging object') url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/maps/positions?vin={vin}' - data: Dict[str, Any] | None = self._fetch_data(url, session=self.session) + data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache) if data is not None: if 'positions' in data and data['positions'] is not None: for position_dict in data['positions']: @@ -426,7 +426,7 @@ def fetch_position(self, vehicle: SkodaVehicle) -> SkodaVehicle: vehicle.position.position_type._set_value(None) # pylint: disable=protected-access return vehicle - def fetch_air_conditioning(self, vehicle: SkodaVehicle) -> SkodaVehicle: + def fetch_air_conditioning(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle: """ Fetches the air conditioning data for a given Skoda vehicle and updates the vehicle object with the retrieved data. @@ -451,7 +451,7 @@ def fetch_air_conditioning(self, vehicle: SkodaVehicle) -> SkodaVehicle: if vehicle.position is None: raise ValueError('Vehicle has no charging object') url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}' - data: Dict[str, Any] | None = self._fetch_data(url, session=self.session) + data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache) if data is not None: if 'carCapturedTimestamp' in data and data['carCapturedTimestamp'] is not None: captured_at: datetime = robust_time_parse(data['carCapturedTimestamp']) @@ -529,7 +529,7 @@ def fetch_air_conditioning(self, vehicle: SkodaVehicle) -> SkodaVehicle: 'targetTemperature', 'outsideTemperature'}) return vehicle - def fetch_vehicle_details(self, vehicle: SkodaVehicle) -> SkodaVehicle: + def fetch_vehicle_details(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle: """ Fetches the details of a vehicle from the Skoda API. @@ -544,7 +544,7 @@ def fetch_vehicle_details(self, vehicle: SkodaVehicle) -> SkodaVehicle: raise APIError('VIN is missing') url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/garage/vehicles/{vin}?' \ 'connectivityGenerations=MOD1&connectivityGenerations=MOD2&connectivityGenerations=MOD3&connectivityGenerations=MOD4' - vehicle_data: Dict[str, Any] | None = self._fetch_data(url, self.session) + vehicle_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache) if vehicle_data: if 'softwareVersion' in vehicle_data and vehicle_data['softwareVersion'] is not None: vehicle.software.version._set_value(vehicle_data['softwareVersion']) # pylint: disable=protected-access @@ -582,7 +582,7 @@ def fetch_vehicle_details(self, vehicle: SkodaVehicle) -> SkodaVehicle: log_extra_keys(LOG_API, 'api/v2/garage/vehicles/VIN', vehicle_data, {'softwareVersion'}) return vehicle - def fetch_driving_range(self, vehicle: SkodaVehicle) -> SkodaVehicle: + def fetch_driving_range(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle: """ Fetches the driving range data for a given Skoda vehicle and updates the vehicle object accordingly. @@ -605,7 +605,7 @@ def fetch_driving_range(self, vehicle: SkodaVehicle) -> SkodaVehicle: if vin is None: raise APIError('VIN is missing') url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/vehicle-status/{vin}/driving-range' - range_data: Dict[str, Any] | None = self._fetch_data(url, self.session) + range_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache) if range_data: captured_at: datetime = robust_time_parse(range_data['carCapturedTimestamp']) # Check vehicle type and if it does not match the current vehicle type, create a new vehicle object using copy constructor @@ -692,7 +692,7 @@ def fetch_driving_range(self, vehicle: SkodaVehicle) -> SkodaVehicle: 'secondaryEngineRange'}) return vehicle - def fetch_vehicle_status_second_api(self, vehicle: SkodaVehicle) -> SkodaVehicle: + def fetch_vehicle_status_second_api(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle: """ Fetches the status of a vehicle from other Skoda API. @@ -706,7 +706,7 @@ def fetch_vehicle_status_second_api(self, vehicle: SkodaVehicle) -> SkodaVehicle if vin is None: raise APIError('VIN is missing') url = f'https://api.connect.skoda-auto.cz/api/v2/vehicle-status/{vin}' - vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url, self.session) + vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache) if vehicle_status_data: if 'remote' in vehicle_status_data and vehicle_status_data['remote'] is not None: vehicle_status_data = vehicle_status_data['remote'] @@ -881,10 +881,11 @@ def _record_elapsed(self, elapsed: timedelta) -> None: """ self._elapsed.append(elapsed) - def _fetch_data(self, url, session, force=False, allow_empty=False, allow_http_error=False, allowed_errors=None) -> Optional[Dict[str, Any]]: # noqa: C901 + def _fetch_data(self, url, session, no_cache=False, allow_empty=False, allow_http_error=False, + allowed_errors=None) -> Optional[Dict[str, Any]]: # noqa: C901 data: Optional[Dict[str, Any]] = None cache_date: Optional[datetime] = None - if not force and (self.max_age is not None and session.cache is not None and url in session.cache): + if not no_cache and (self.max_age is not None and session.cache is not None and url in session.cache): data, cache_date_string = session.cache[url] cache_date = datetime.fromisoformat(cache_date_string) if data is None or self.max_age is None \ diff --git a/src/carconnectivity_connectors/skoda/mqtt_client.py b/src/carconnectivity_connectors/skoda/mqtt_client.py index 6f6fb40..16d5d8b 100644 --- a/src/carconnectivity_connectors/skoda/mqtt_client.py +++ b/src/carconnectivity_connectors/skoda/mqtt_client.py @@ -7,6 +7,7 @@ import uuid import ssl import json +import threading from datetime import timedelta, timezone from paho.mqtt.client import Client @@ -57,6 +58,8 @@ def __init__(self, skoda_connector: Connector) -> None: self.on_subscribe = self._on_subscribe_callback self.subscribed_topics: Set[str] = set() + self.delayed_access_function_timers: Dict[str, threading.Timer] = {} + self.tls_set(cert_reqs=ssl.CERT_NONE) def connect(self, *args, **kwargs) -> MQTTErrorCode: @@ -437,7 +440,7 @@ def _on_message_callback(self, mqttc, obj, msg) -> None: # noqa: C901 return # service_events - match = re.match(r'^(?P[0-9a-fA-F-]+)/(?P[A-Z0-9]+)/service-event/(?P[a-zA-Z0-9-_]+)$', msg.topic) + match = re.match(r'^(?P[0-9a-fA-F-]+)/(?P[A-Z0-9]+)/service-event/(?P[a-zA-Z0-9-_/]+)$', msg.topic) if match: user_id: str = match.group('user_id') vin: str = match.group('vin') @@ -481,7 +484,7 @@ def _on_message_callback(self, mqttc, obj, msg) -> None: # noqa: C901 # If charging state changed, fetch charging again if old_charging_state != charging_state: try: - self._skoda_connector.fetch_charging(vehicle) + self._skoda_connector.fetch_charging(vehicle, no_cache=True) except CarConnectivityError as e: LOG.error('Error while fetching charging: %s', e) if 'timeToFinish' in data['data'] and data['data']['timeToFinish'] is not None \ @@ -507,12 +510,58 @@ def _on_message_callback(self, mqttc, obj, msg) -> None: # noqa: C901 vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin) if isinstance(vehicle, SkodaVehicle): try: - self._skoda_connector.fetch_air_conditioning(vehicle) + self._skoda_connector.fetch_air_conditioning(vehicle, no_cache=True) except CarConnectivityError as e: LOG.error('Error while fetching charging: %s', e) LOG_API.info('Received event name %s service event %s for vehicle %s from user %s: %s', data['name'], service_event, vin, user_id, msg.payload) return + elif service_event == 'vehicle-status/access': + if 'name' in data and data['name'] == 'change-access': + if 'data' in data and data['data'] is not None: + vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin) + if isinstance(vehicle, SkodaVehicle): + def delayed_access_function(vehicle: SkodaVehicle): + """ + Function to be executed after a delay of two seconds. + """ + vin = vehicle.id + self.delayed_access_function_timers.pop(vin) + if vehicle.capabilities is not None and vehicle.capabilities.enabled \ + and vehicle.capabilities.has_capability('CHARGING') and isinstance(vehicle, SkodaElectricVehicle): + try: + self._skoda_connector.fetch_charging(vehicle, no_cache=True) + except CarConnectivityError as e: + LOG.error('Error while fetching charging: %s', e) + if vehicle.capabilities is not None and vehicle.capabilities.enabled \ + and vehicle.capabilities.has_capability('PARKING_POSITION'): + try: + self._skoda_connector.fetch_position(vehicle, no_cache=True) + except CarConnectivityError as e: + LOG.error('Error while fetching position: %s', e) + if vehicle.capabilities is not None and vehicle.capabilities.enabled \ + and vehicle.capabilities.has_capability('AIR_CONDITIONING'): + try: + self._skoda_connector.fetch_air_conditioning(vehicle, no_cache=True) + except CarConnectivityError as e: + LOG.error('Error while fetching air conditioning: %s', e) + try: + self._skoda_connector.fetch_vehicle_status_second_api(vehicle, no_cache=True) + except CarConnectivityError as e: + LOG.error('Error while fetching status second API: %s', e) + try: + self._skoda_connector.fetch_driving_range(vehicle, no_cache=True) + except CarConnectivityError as e: + LOG.error('Error while fetching driving range: %s', e) + + if vin in self.delayed_access_function_timers: + self.delayed_access_function_timers[vin].cancel() + self.delayed_access_function_timers[vin] = threading.Timer(2.0, delayed_access_function, kwargs={'vehicle': vehicle}) + self.delayed_access_function_timers[vin].start() + + LOG_API.info('Received event name %s service event %s for vehicle %s from user %s: %s', data['name'], + service_event, vin, user_id, msg.payload) + return LOG_API.info('Received unknown service event %s for vehicle %s from user %s: %s', service_event, vin, user_id, msg.payload) return # service_events @@ -530,7 +579,7 @@ def _on_message_callback(self, mqttc, obj, msg) -> None: # noqa: C901 if data['status'] == 'COMPLETED_SUCCESS': LOG.debug('Received %s operation request for vehicle %s from user %s', operation_request, vin, user_id) try: - self._skoda_connector.fetch_air_conditioning(vehicle) + self._skoda_connector.fetch_air_conditioning(vehicle, no_cache=True) except CarConnectivityError as e: LOG.error('Error while fetching air-conditioning: %s', e) return