diff --git a/REFERENCE.md b/REFERENCE.md index 0de0d868..c49865f8 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -383,6 +383,42 @@ Returns a list of all the service records performed on the vehicle, filtered by --- +### `diagnostic_system_status(self)` + +Retrieve the status of various diagnostic systems in the vehicle. + +#### Return + +| Value | Type | Description | +| :---------------------------- | :--------------------- | :------------------------------------------------------------------- | +| `DiagnosticSystemStatus` | typing.NamedTuple | The returned object with diagnostic system statuses data | +| `DiagnosticSystemStatus.systems` | List[Dict] | List of system statuses, each with `system_id`, `status`, and `description` | +| `DiagnosticSystemStatus.meta` | collections.namedtuple | Smartcar response headers (`request_id`, `data_age`, and/or `unit_system`) | + +Each system entry contains: +- `system_id` (String): Unique identifier for the system. +- `status` (String): Status of the system, either "OK" or "ALERT". +- `description` (String, optional): Additional context or description for the status, if any. + +--- + +### `diagnostic_trouble_codes(self)` + +Retrieve active diagnostic trouble codes (DTCs) for the vehicle. + +#### Return + +| Value | Type | Description | +| :------------------------- | :--------------------- | :------------------------------------------------------- | +| `DiagnosticTroubleCodes` | typing.NamedTuple | The returned object with active diagnostic trouble codes | +| `DiagnosticTroubleCodes.active_codes` | List[Dict] | List of active DTCs, each with `code` and `timestamp` | +| `DiagnosticTroubleCodes.meta` | collections.namedtuple | Smartcar response headers (`request_id`, `data_age`, and/or `unit_system`) | + +Each trouble code entry contains: +- `code` (String): The DTC code representing the issue. +- `timestamp` (String, optional): ISO 8601 timestamp when the code was triggered. May be `null` if unavailable. + +--- ### `attributes(self)` Returns a single vehicle object, containing identifying information. diff --git a/smartcar/helpers.py b/smartcar/helpers.py index 4cfe1295..6fe883f1 100644 --- a/smartcar/helpers.py +++ b/smartcar/helpers.py @@ -121,6 +121,8 @@ def format_path_and_attribute_for_batch(raw_path: str) -> tuple: "tires/pressure": "tire_pressure", "": "attributes", "security": "lock_status", + "diagnostics/system_status": "diagnostic_system_status", + "diagnostics/dtcs": "diagnostic_trouble_codes", } formatted_path = raw_path[1:] if raw_path[0] == "/" else raw_path formatted_attribute = mapper.get(formatted_path, formatted_path) diff --git a/smartcar/types.py b/smartcar/types.py index a721dc96..d5abe666 100644 --- a/smartcar/types.py +++ b/smartcar/types.py @@ -186,6 +186,25 @@ def format_capabilities(capabilities_list: List[dict]) -> List[Capability]: ChargeLimit = NamedTuple("ChargeLimit", [("limit", float), ("meta", namedtuple)]) +DiagnosticSystem = NamedTuple( + "DiagnosticSystem", + [("system_id", str), ("status", str), ("description", Optional[str])], +) + +DiagnosticSystemStatus = NamedTuple( + "DiagnosticSystemStatus", + [("systems", List[DiagnosticSystem]), ("meta", namedtuple)], +) + +DiagnosticTroubleCode = NamedTuple( + "DiagnosticTroubleCode", [("code", str), ("timestamp", Optional[datetime.datetime])] +) + +DiagnosticTroubleCodes = NamedTuple( + "DiagnosticTroubleCodes", + [("active_codes", List[DiagnosticTroubleCode]), ("meta", namedtuple)], +) + class ServiceCost: total_cost: Optional[float] = None @@ -431,6 +450,24 @@ def select_named_tuple(path: str, response_or_dict) -> NamedTuple: elif path == "service/history": return ServiceHistory(data, headers) + elif path == "diagnostics/system_status": + systems = [ + DiagnosticSystem( + system_id=item["systemId"], + status=item["status"], + description=item.get("description"), + ) + for item in data["systems"] + ] + return DiagnosticSystemStatus(systems=systems, meta=headers) + + elif path == "diagnostics/dtcs": + active_codes = [ + DiagnosticTroubleCode(code=item["code"], timestamp=item.get("timestamp")) + for item in data["activeCodes"] + ] + return DiagnosticTroubleCodes(active_codes=active_codes, meta=headers) + elif path == "permissions": return Permissions( data["permissions"], diff --git a/smartcar/vehicle.py b/smartcar/vehicle.py index 6d594fe8..8c6856a4 100644 --- a/smartcar/vehicle.py +++ b/smartcar/vehicle.py @@ -219,6 +219,44 @@ def service_history( response = helpers.requester("GET", url, headers=headers, params=params) return types.select_named_tuple(path, response) + def diagnostic_system_status(self) -> types.DiagnosticSystemStatus: + """ + GET Vehicle.diagnostic_system_status + + Returns: + DiagnosticSystemStatus: NamedTuple("DiagnosticSystemStatus", [ + ("systems", List[DiagnosticSystem]), + ("meta", namedtuple) + ]) + + Raises: + SmartcarException + """ + path = "diagnostics/system_status" + url = self._format_url(path) + headers = self._get_headers() + response = helpers.requester("GET", url, headers=headers) + return types.select_named_tuple(path, response) + + def diagnostic_trouble_codes(self) -> types.DiagnosticTroubleCodes: + """ + GET Vehicle.diagnostic_trouble_codes + + Returns: + DiagnosticTroubleCodes: NamedTuple("DiagnosticTroubleCodes", [ + ("active_codes", List[DiagnosticTroubleCode]), + ("meta", namedtuple) + ]) + + Raises: + SmartcarException + """ + path = "diagnostics/dtcs" + url = self._format_url(path) + headers = self._get_headers() + response = helpers.requester("GET", url, headers=headers) + return types.select_named_tuple(path, response) + def location(self) -> types.Location: """ GET Vehicle.location diff --git a/tests/conftest.py b/tests/conftest.py index 3030b556..18a43949 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -163,6 +163,7 @@ def access_ford(client): "required:control_charge", "control_navigation", "read_service_history", + "read_diagnostics", ] ), "FORD", diff --git a/tests/e2e/test_vehicle.py b/tests/e2e/test_vehicle.py index 4fb5a3b0..3a8a0858 100644 --- a/tests/e2e/test_vehicle.py +++ b/tests/e2e/test_vehicle.py @@ -170,6 +170,46 @@ def test_service_history(ford_car): assert response._fields == ("items", "meta") +def test_diagnostic_system_status(ford_car): + diagnostic_status = ford_car.diagnostic_system_status() + assert diagnostic_status is not None + assert isinstance(diagnostic_status, types.DiagnosticSystemStatus) + assert diagnostic_status._fields == ("systems", "meta") + + for system in diagnostic_status.systems: + assert isinstance(system, types.DiagnosticSystem) + + +def test_diagnostic_trouble_codes(ford_car): + dtc_response = ford_car.diagnostic_trouble_codes() + assert dtc_response is not None + assert isinstance(dtc_response, types.DiagnosticTroubleCodes) + assert dtc_response._fields == ("active_codes", "meta") + + for code in dtc_response.active_codes: + assert isinstance(code, types.DiagnosticTroubleCode) + + +def test_batch_diagnostics(ford_car): + batch_response = ford_car.batch(["/diagnostics/system_status", "/diagnostics/dtcs"]) + assert batch_response is not None + assert batch_response._fields == ( + "diagnostic_system_status", + "diagnostic_trouble_codes", + "meta", + ) + + diagnostic_status = batch_response.diagnostic_system_status() + assert diagnostic_status is not None + for system in diagnostic_status.systems: + assert isinstance(system, types.DiagnosticSystem) + + dtc_response = batch_response.diagnostic_trouble_codes() + assert dtc_response is not None + for code in dtc_response.active_codes: + assert isinstance(code, types.DiagnosticTroubleCode) + + def test_batch_success(chevy_volt): batch = chevy_volt.batch( [