Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add initial ServiceHistoryResponseModel #312

Merged
merged 13 commits into from
Jan 23, 2024
21 changes: 21 additions & 0 deletions mytoyota/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
VEHICLE_HEALTH_STATUS_ENDPOINT,
VEHICLE_LOCATION_ENDPOINT,
VEHICLE_NOTIFICATION_HISTORY_ENDPOINT,
VEHICLE_SERVICE_HISTORY_ENDPONT,
VEHICLE_TELEMETRY_ENDPOINT,
VEHICLE_TRIPS_ENDPOINT,
)
from mytoyota.controller import Controller
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
Expand Down Expand Up @@ -242,3 +244,22 @@
)
_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(

Check warning on line 261 in mytoyota/api.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/api.py#L261

Added line #L261 was not covered by tests
ServiceHistoryResponseModel, "GET", VEHICLE_SERVICE_HISTORY_ENDPONT, vin=vin
)
_LOGGER.debug(msg=f"Parsed 'ServiceHistoryResponseModel': {parsed_response}")
return parsed_response

Check warning on line 265 in mytoyota/api.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/api.py#L264-L265

Added lines #L264 - L265 were not covered by tests
1 change: 1 addition & 0 deletions mytoyota/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
69 changes: 69 additions & 0 deletions mytoyota/models/endpoints/service_history.py
Original file line number Diff line number Diff line change
@@ -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
132 changes: 132 additions & 0 deletions mytoyota/models/service_history.py
Original file line number Diff line number Diff line change
@@ -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"

Check warning on line 19 in mytoyota/models/service_history.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/service_history.py#L18-L19

Added lines #L18 - L19 were not covered by tests

def __repr__(self):
"""Representation of the model."""
return " ".join(

Check warning on line 23 in mytoyota/models/service_history.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/service_history.py#L23

Added line #L23 was not covered by tests
[
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

Check warning on line 39 in mytoyota/models/service_history.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/service_history.py#L39

Added line #L39 was not covered by tests

@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

Check warning on line 49 in mytoyota/models/service_history.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/service_history.py#L49

Added line #L49 was not covered by tests

@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 (

Check warning on line 61 in mytoyota/models/service_history.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/service_history.py#L61

Added line #L61 was not covered by tests
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(

Check warning on line 66 in mytoyota/models/service_history.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/service_history.py#L66

Added line #L66 was not covered by tests
self._distance_unit,
self._service_history.unit,
self._service_history.mileage,
)
else:
return None

Check warning on line 72 in mytoyota/models/service_history.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/service_history.py#L72

Added line #L72 was not covered by tests

@property
def notes(self) -> Any:
"""Additional notes about the service.

Returns
-------
Any: Additional notes about the service
"""
return self._service_history.notes

Check warning on line 82 in mytoyota/models/service_history.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/service_history.py#L82

Added line #L82 was not covered by tests

@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

Check warning on line 92 in mytoyota/models/service_history.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/service_history.py#L92

Added line #L92 was not covered by tests

@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

Check warning on line 102 in mytoyota/models/service_history.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/service_history.py#L102

Added line #L102 was not covered by tests

@property
def service_category(self) -> str:
"""The category of the service.

Returns
-------
str: The category of the service.
"""
return self._service_history.service_category

Check warning on line 112 in mytoyota/models/service_history.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/service_history.py#L112

Added line #L112 was not covered by tests

@property
def service_provider(self) -> str:
"""The service provider.

Returns
-------
str: The service provider
"""
return self._service_history.service_provider

Check warning on line 122 in mytoyota/models/service_history.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/service_history.py#L122

Added line #L122 was not covered by tests

@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

Check warning on line 132 in mytoyota/models/service_history.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/service_history.py#L132

Added line #L132 was not covered by tests
59 changes: 53 additions & 6 deletions mytoyota/models/vehicle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -75,6 +76,11 @@
"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"])
Expand Down Expand Up @@ -199,15 +205,46 @@

"""
if "notifications" in self._endpoint_data:
ret = []
ret: List[Notification] = []

Check warning on line 208 in mytoyota/models/vehicle.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/vehicle.py#L208

Added line #L208 was not covered by tests
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

Check warning on line 211 in mytoyota/models/vehicle.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/vehicle.py#L210-L211

Added lines #L210 - L211 were not covered by tests

return None

Check warning on line 213 in mytoyota/models/vehicle.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/vehicle.py#L213

Added line #L213 was not covered by tests

@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(

Check warning on line 228 in mytoyota/models/vehicle.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/vehicle.py#L225-L228

Added lines #L225 - L228 were not covered by tests
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

Check warning on line 246 in mytoyota/models/vehicle.py

View check run for this annotation

Codecov / codecov/patch

mytoyota/models/vehicle.py#L244-L246

Added lines #L244 - L246 were not covered by tests

@property
def lock_status(self) -> Optional[LockStatus]:
"""Returns the latest lock status of Doors & Windows.
Expand Down Expand Up @@ -429,18 +466,28 @@
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:]:
add_with_none(build_hdc, histogram.hdc)
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
Expand Down
2 changes: 2 additions & 0 deletions simple_client_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading