diff --git a/mytoyota/api.py b/mytoyota/api.py index ca39992b..a9dc0ce4 100644 --- a/mytoyota/api.py +++ b/mytoyota/api.py @@ -13,6 +13,7 @@ VEHICLE_HEALTH_STATUS_ENDPOINT, VEHICLE_LOCATION_ENDPOINT, VEHICLE_NOTIFICATION_HISTORY_ENDPOINT, + VEHICLE_SERVICE_HISTORY_ENDPONT, VEHICLE_TELEMETRY_ENDPOINT, VEHICLE_TRIPS_ENDPOINT, ) @@ -20,6 +21,7 @@ from mytoyota.models.endpoints.electric import ElectricResponseModel from mytoyota.models.endpoints.location import LocationResponseModel from mytoyota.models.endpoints.notifications import NotificationResponseModel +from mytoyota.models.endpoints.service_history import ServiceHistoryResponseModel from mytoyota.models.endpoints.status import RemoteStatusResponseModel from mytoyota.models.endpoints.telemetry import TelemetryResponseModel from mytoyota.models.endpoints.trips import TripsResponseModel @@ -242,3 +244,22 @@ async def get_trips_endpoint( # noqa: PLR0913 ) _LOGGER.debug(msg=f"Parsed 'TripsResponseModel': {parsed_response}") return parsed_response + + async def get_service_history_endpoint(self, vin: str) -> ServiceHistoryResponseModel: + """Get the current servic history. + + Response includes service category, date and dealer. + + Args: + ---- + vin: str: The vehicles VIN + + Returns: + ------- + ServicHistoryResponseModel: A pydantic model for the service history response + """ + parsed_response = await self._request_and_parse( + ServiceHistoryResponseModel, "GET", VEHICLE_SERVICE_HISTORY_ENDPONT, vin=vin + ) + _LOGGER.debug(msg=f"Parsed 'ServiceHistoryResponseModel': {parsed_response}") + return parsed_response diff --git a/mytoyota/const.py b/mytoyota/const.py index 4027e3cf..c0a8bf27 100644 --- a/mytoyota/const.py +++ b/mytoyota/const.py @@ -56,6 +56,7 @@ VEHICLE_TELEMETRY_ENDPOINT = "/v3/telemetry" VEHICLE_NOTIFICATION_HISTORY_ENDPOINT = "/v2/notification/history" VEHICLE_TRIPS_ENDPOINT = "/v1/trips?from={from_date}&to={to_date}&route={route}&summary={summary}&limit={limit}&offset={offset}" # noqa: E501 +VEHICLE_SERVICE_HISTORY_ENDPONT = "/v1/servicehistory/vehicle/summary" # Timestamps UNLOCK_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" diff --git a/mytoyota/models/endpoints/service_history.py b/mytoyota/models/endpoints/service_history.py new file mode 100644 index 00000000..b5c1e800 --- /dev/null +++ b/mytoyota/models/endpoints/service_history.py @@ -0,0 +1,69 @@ +"""Toyota Connected Services API - Service History Models.""" + +from datetime import date +from typing import Any, List, Optional + +from pydantic import BaseModel, Field + +from mytoyota.models.endpoints.common import StatusModel + + +class ServiceHistoryModel(BaseModel): + """Represents a service history record. + + Attributes + ---------- + customer_created_record (bool): Indicates if the record was created by the customer. + mileage (Optional[int]): The mileage at the time of the service. + notes (Any): Additional notes about the service. + operations_performed (Any): The operations performed during the service. + ro_number (Any): The RO (Repair Order) number associated with the service. + service_category (str): The category of the service. + service_date (date): The date of the service. + service_history_id (str): The ID of the service history record. + service_provider (str): The service provider. + servicing_dealer (Any): The dealer that performed the service. + unit (Optional[str]): The unit associated with the service mileage. + + """ + + customer_created_record: bool = Field(alias="customerCreatedRecord") + mileage: Optional[int] = None + notes: Any + operations_performed: Any = Field(alias="operationsPerformed") + ro_number: Any = Field(alias="roNumber") + service_category: str = Field(alias="serviceCategory") + service_date: date = Field(alias="serviceDate") + service_history_id: str = Field(alias="serviceHistoryId") + service_provider: str = Field(alias="serviceProvider") + servicing_dealer: Any = Field(alias="servicingDealer") + unit: Optional[str] = None + + +class ServiceHistoriesModel(BaseModel): + r"""Model representing a list of service histories. + + Attributes + ---------- + service_histories (List[Optional[ServiceHistoryModel]]): A list of all service histories. + Defaults to []. + + """ + + service_histories: List[Optional[ServiceHistoryModel]] = Field( + alias="serviceHistories", default=[] + ) + + +class ServiceHistoryResponseModel(StatusModel): + """Model representing a service history response. + + Inherits from StatusModel. + + Attributes + ---------- + payload (Optional[ServiceHistoriesModel]): The service history payload. Defaults to None. + + """ + + payload: Optional[ServiceHistoriesModel] = None diff --git a/mytoyota/models/service_history.py b/mytoyota/models/service_history.py new file mode 100644 index 00000000..8479bb49 --- /dev/null +++ b/mytoyota/models/service_history.py @@ -0,0 +1,132 @@ +"""models for vehicle service history.""" +from datetime import date +from typing import Any, Optional + +from mytoyota.models.endpoints.service_history import ServiceHistoryModel +from mytoyota.utils.conversions import convert_distance + + +class ServiceHistory: + """ServiceHistory.""" + + def __init__( + self, + service_history: ServiceHistoryModel, + metric: bool = True, + ): + """Initialise ServiceHistory.""" + self._service_history = service_history + self._distance_unit: str = "km" if metric else "mi" + + def __repr__(self): + """Representation of the model.""" + return " ".join( + [ + f"{k}={getattr(self, k)!s}" + for k, v in type(self).__dict__.items() + if isinstance(v, property) + ], + ) + + @property + def service_date(self) -> date: + """The date of the service. + + Returns + ------- + date: The date of the service. + """ + return self._service_history.service_date + + @property + def customer_created_record(self) -> bool: + """Indication whether it is an entry created by the user. + + Returns + ------- + str: Category of notification + """ + return self._service_history.customer_created_record + + @property + def odometer(self) -> Optional[float]: + """Odometer distance at the time of servicing. + + Returns + ------- + int: Odometer distance at the time of servicing + in the current selected units + + """ + if ( + self._service_history is not None + and self._service_history.unit is not None + and self._service_history.mileage is not None + ): + return convert_distance( + self._distance_unit, + self._service_history.unit, + self._service_history.mileage, + ) + else: + return None + + @property + def notes(self) -> Any: + """Additional notes about the service. + + Returns + ------- + Any: Additional notes about the service + """ + return self._service_history.notes + + @property + def operations_performed(self) -> Any: + """The operations performed during the service. + + Returns + ------- + Any: The operations performed during the service + """ + return self._service_history.operations_performed + + @property + def ro_number(self) -> Any: + """The RO (Repair Order) number associated with the service. + + Returns + ------- + Any: The RO (Repair Order) number associated with the service + """ + return self._service_history.ro_number + + @property + def service_category(self) -> str: + """The category of the service. + + Returns + ------- + str: The category of the service. + """ + return self._service_history.service_category + + @property + def service_provider(self) -> str: + """The service provider. + + Returns + ------- + str: The service provider + """ + return self._service_history.service_provider + + @property + def servicing_dealer(self) -> Any: + """Dealer that performed the service. + + Returns + ------- + Any: The dealer that performed the service + """ + return self._service_history.servicing_dealer diff --git a/mytoyota/models/vehicle.py b/mytoyota/models/vehicle.py index e72aa90a..e016dd9b 100644 --- a/mytoyota/models/vehicle.py +++ b/mytoyota/models/vehicle.py @@ -18,6 +18,7 @@ from mytoyota.models.location import Location from mytoyota.models.lock_status import LockStatus from mytoyota.models.nofication import Notification +from mytoyota.models.service_history import ServiceHistory from mytoyota.models.summary import Summary, SummaryType from mytoyota.models.trips import Trip from mytoyota.utils.helpers import add_with_none @@ -75,6 +76,11 @@ def __init__(self, api: Api, vehicle_info: VehicleGuidModel, metric: bool = True "capable": vehicle_info.extended_capabilities.vehicle_status, "function": partial(self._api.get_remote_status_endpoint, vin=vehicle_info.vin), }, + { + "name": "service_history", + "capable": vehicle_info.features.service_history, + "function": partial(self._api.get_service_history_endpoint, vin=vehicle_info.vin), + }, ] self._endpoint_collect = [ (endpoint["name"], endpoint["function"]) @@ -199,15 +205,46 @@ def notifications(self) -> Optional[List[Notification]]: """ if "notifications" in self._endpoint_data: - ret = [] + ret: List[Notification] = [] for p in self._endpoint_data["notifications"].payload: - for n in p.notifications: - ret.append(Notification(n)) + ret.extend(Notification(n) for n in p.notifications) + return ret + + return None + + @property + def service_history(self) -> Optional[List[ServiceHistory]]: + r"""Returns a list of service history entries for the vehicle. + + Returns + ------- + Optional[List[ServiceHistory]]: A list of service history entries for the vehicle, + or None if not supported. + """ + if "service_history" in self._endpoint_data: + ret: List[ServiceHistory] = [] + payload = self._endpoint_data["service_history"].payload + ret.extend( + ServiceHistory(service_history) for service_history in payload.service_histories + ) return ret return None + def get_latest_service_history(self) -> Optional[ServiceHistory]: + r"""Return the latest service history entry for the vehicle. + + Returns + ------- + Optional[ServiceHistory]: A service history entry for the vehicle, + ordered by date and service_category. None if not supported or unknown. + + """ + if self.service_history is not None: + return max(self.service_history, key=lambda x: (x.service_date, x.service_category)) + return None + @property def lock_status(self) -> Optional[LockStatus]: """Returns the latest lock status of Doors & Windows. @@ -429,7 +466,9 @@ def _generate_weekly_summaries(self, summary) -> List[Summary]: build_hdc = copy.copy(week_histograms[0].hdc) build_summary = copy.copy(week_histograms[0].summary) start_date = Arrow( - week_histograms[0].year, week_histograms[0].month, week_histograms[0].day + week_histograms[0].year, + week_histograms[0].month, + week_histograms[0].day, ) for histogram in week_histograms[1:]: @@ -437,10 +476,18 @@ def _generate_weekly_summaries(self, summary) -> List[Summary]: build_summary += histogram.summary end_date = Arrow( - week_histograms[-1].year, week_histograms[-1].month, week_histograms[-1].day + week_histograms[-1].year, + week_histograms[-1].month, + week_histograms[-1].day, ) ret.append( - Summary(build_summary, self._metric, start_date.date(), end_date.date(), build_hdc) + Summary( + build_summary, + self._metric, + start_date.date(), + end_date.date(), + build_hdc, + ) ) return ret diff --git a/simple_client_example.py b/simple_client_example.py index db48a701..7797797c 100644 --- a/simple_client_example.py +++ b/simple_client_example.py @@ -60,6 +60,8 @@ async def get_information(): pp.pprint(f"Lock Status: {car.lock_status}") # Notifications pp.pprint(f"Notifications: {[[x] for x in car.notifications]}") + # Service history + pp.pprint(f"Latest service: {car.get_latest_service_history()}") # Summary # pp.pprint( # f"Summary: {[[x] for x in await car.get_summary(date.today() - timedelta(days=7), date.today(), summary_type=SummaryType.DAILY)]}" # noqa: E501 # pylint: disable=C0301 diff --git a/tests/data/endpoints/v1_service_history.json b/tests/data/endpoints/v1_service_history.json new file mode 100644 index 00000000..525cf6ed --- /dev/null +++ b/tests/data/endpoints/v1_service_history.json @@ -0,0 +1,67 @@ +{ + "payload": { + "serviceHistories": [ + { + "customerCreatedRecord": false, + "mileage": "30087", + "notes": null, + "operationsPerformed": null, + "roNumber": null, + "serviceCategory": "HSC", + "serviceDate": "2023-11-16", + "serviceHistoryId": "12345678", + "serviceProvider": "Provider 1", + "servicingDealer": null, + "unit": "km" + }, + { + "customerCreatedRecord": false, + "mileage": "21738", + "notes": null, + "operationsPerformed": null, + "roNumber": null, + "serviceCategory": "HSC", + "serviceDate": "2022-12-20", + "serviceHistoryId": "1234567", + "serviceProvider": "Provider 2", + "servicingDealer": null, + "unit": "km" + }, + { + "customerCreatedRecord": false, + "mileage": null, + "notes": null, + "operationsPerformed": null, + "roNumber": null, + "serviceCategory": "VDE", + "serviceDate": "2020-11-19", + "serviceHistoryId": "123456", + "serviceProvider": "Provider 3", + "servicingDealer": null, + "unit": null + }, + { + "customerCreatedRecord": false, + "mileage": "10", + "notes": null, + "operationsPerformed": null, + "roNumber": null, + "serviceCategory": "RPR", + "serviceDate": "2020-11-19", + "serviceHistoryId": "12345", + "serviceProvider": "Provider 1", + "servicingDealer": null, + "unit": "km" + } + ] + }, + "status": { + "messages": [ + { + "description": "Request Processed successfully", + "detailedDescription": "Request Processed successfully", + "responseCode": "OSH-001" + } + ] + } +} diff --git a/tests/test_api.py b/tests/test_api.py index 6245a4f8..dc2dbe35 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -13,12 +13,14 @@ VEHICLE_HEALTH_STATUS_ENDPOINT, VEHICLE_LOCATION_ENDPOINT, VEHICLE_NOTIFICATION_HISTORY_ENDPOINT, + VEHICLE_SERVICE_HISTORY_ENDPONT, VEHICLE_TELEMETRY_ENDPOINT, VEHICLE_TRIPS_ENDPOINT, ) from mytoyota.models.endpoints.electric import ElectricResponseModel from mytoyota.models.endpoints.location import LocationResponseModel from mytoyota.models.endpoints.notifications import NotificationResponseModel +from mytoyota.models.endpoints.service_history import ServiceHistoryResponseModel # from mytoyota.models.endpoints.account import AccountResponseModel from mytoyota.models.endpoints.status import RemoteStatusResponseModel @@ -89,6 +91,13 @@ "v2_notification", "notification-happy", ), + ( + "GET", + VEHICLE_SERVICE_HISTORY_ENDPONT, + ServiceHistoryResponseModel, + "v1_service_history", + "service_history-happy", + ), ( "GET", VEHICLE_TRIPS_ENDPOINT.format(