> ## Documentation Index
> Fetch the complete documentation index at: https://docs.telemax.com.au/llms.txt
> Use this file to discover all available pages before exploring further.

# Vehicle Health Dashboard

> Combine live position, battery predictions, and DTC codes into a single per-vehicle health view using GetLastPositionData, GetCompanyVehiclesBatteryHealth, and devices/{id}/dtc-codes.

## Overview

Three endpoints together give a complete picture of a vehicle's current health:

| Endpoint                                    | Data                                                        |
| ------------------------------------------- | ----------------------------------------------------------- |
| `POST /api/GetLastPositionData`             | Live voltage, ignition state, speed, odometer, last GPS fix |
| `POST /api/GetCompanyVehiclesBatteryHealth` | Battery voltage trend and day-ahead prediction results      |
| `GET /api/devices/{id}/dtc-codes`           | Active OBD-II / CAN fault codes from the vehicle handler    |

All three join on the **vehicle ID** — `DeviceId` in position data, `vehicleId` in battery health, and the `{id}` path parameter in DTC codes. These are all the same legacy integer vehicle identifier.

<Note>
  DTC `description` and `severity` fields are present in the schema but are empty in the current backend. Display the raw `code` value and resolve meaning externally via the SAE J1979 or ISO 15031-6 standard. See [Known Issues](/v1/quirks).
</Note>

***

## Step 1 — Get live position and status

`GetLastPositionData` returns the latest GPS fix for a single vehicle. Key health fields:

| Field      | Type    | Description                                         |
| ---------- | ------- | --------------------------------------------------- |
| `DeviceId` | integer | Vehicle identifier — join key                       |
| `Voltage`  | double  | External battery voltage (V)                        |
| `Ignition` | boolean | Whether ignition is currently on                    |
| `Speed`    | double  | Last reported speed (km/h)                          |
| `Odometer` | double  | Total odometer reading (km)                         |
| `UtcTime`  | string  | When this GPS fix was recorded (UTC, no `Z` suffix) |

```bash cURL theme={null}
curl -s -X POST \
  "https://api.telemax.com.au/api/GetLastPositionData?id=1000" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

```python Python theme={null}
import requests

def get_position(vehicle_id: int, token: str) -> dict:
    r = requests.post(
        "https://api.telemax.com.au/api/GetLastPositionData",
        params={"id": vehicle_id},
        headers={"Authorization": f"Bearer {token}"},
    )
    r.raise_for_status()
    return r.json()
```

***

## Step 2 — Get battery health predictions

`GetCompanyVehiclesBatteryHealth` returns a paged list of battery predictions. Iterate over all pages using `numberOfPages`:

```python Python theme={null}
def get_all_battery_health(company_id: int, token: str) -> list:
    results = []
    page = 1
    while True:
        r = requests.post(
            "https://api.telemax.com.au/api/GetCompanyVehiclesBatteryHealth",
            params={"compId": company_id, "page": page, "pageSize": 50},
            headers={"Authorization": f"Bearer {token}"},
        )
        r.raise_for_status()
        data = r.json()
        results.extend(data.get("items", []))
        if page >= data.get("numberOfPages", 1):
            break
        page += 1
    return results
```

Each item in `items` contains:

| Field               | Description                   |
| ------------------- | ----------------------------- |
| `vehicleId`         | Vehicle identifier (join key) |
| `currentVoltage`    | Current battery voltage (V)   |
| `predictionResults` | Array of prediction objects   |

Each prediction object has:

* `BatteryPredictionValue` — label such as `"Good"`, `"Fair"`, `"Poor"`
* `PredictionConfidence` — label such as `"High"`, `"Medium"`, `"Low"`
* `predictionForDay` — integer (days ahead this prediction applies to)

***

## Step 3 — Get DTC fault codes

`GET /api/devices/{id}/dtc-codes` returns the active diagnostic trouble codes from the vehicle's OBD-II / CAN handler. The response is an **array of arrays** — flatten and deduplicate by `code`:

```python Python theme={null}
def get_dtc_codes(vehicle_id: int, token: str) -> list[str]:
    r = requests.get(
        f"https://api.telemax.com.au/api/devices/{vehicle_id}/dtc-codes",
        headers={"Authorization": f"Bearer {token}"},
    )
    r.raise_for_status()
    raw = r.json()          # array-of-arrays of DtcDto
    seen = set()
    codes = []
    for group in raw:
        for dtc in group:
            code = dtc.get("code", "")
            if code and code not in seen:
                seen.add(code)
                codes.append(code)
    return codes
```

<Warning>
  `description` and `severity` on `DtcDto` are empty in the current backend. Only `code` is populated. Resolve fault meanings via the SAE J1979 / ISO 15031-6 standard or your OEM documentation.
</Warning>

***

## Step 4 — Join on vehicle ID

With all three data sources fetched, merge them into a single per-vehicle record:

<CodeGroup>
  ```python Python theme={null}
  import os, requests

  API_BASE   = "https://api.telemax.com.au"
  COMPANY_ID = 85
  VEHICLE_IDS = [1000, 1001, 1002]   # replace with your fleet

  # — fetch helpers (get_token() assumed from authentication module) —

  def build_health_record(vehicle_id: int, token: str) -> dict:
      pos     = get_position(vehicle_id, token)
      battery = {b["vehicleId"]: b for b in get_all_battery_health(COMPANY_ID, token)}
      dtcs    = get_dtc_codes(vehicle_id, token)

      batt_entry = battery.get(vehicle_id, {})
      top_pred   = next(
          (p for p in sorted(
              batt_entry.get("predictionResults", []),
              key=lambda p: p.get("predictionForDay", 0)
          )),
          None,
      )

      return {
          "name":       pos.get("DeviceName", str(vehicle_id)),
          "voltage_v":  pos.get("Voltage"),
          "ignition":   pos.get("Ignition"),
          "speed_kmh":  pos.get("Speed"),
          "odometer_km": pos.get("Odometer"),
          "last_seen":  pos.get("UtcTime"),
          "battery_pred": top_pred["BatteryPredictionValue"] if top_pred else "N/A",
          "dtc_codes":  dtcs,
      }

  token = get_token()
  print(f"{'Vehicle':<25} {'V':>5} {'Ign':<5} {'Battery':>8} {'DTCs'}")
  print("-" * 60)
  for vid in VEHICLE_IDS:
      h = build_health_record(vid, token)
      dtc_str = ", ".join(h["dtc_codes"]) or "none"
      ign_str = "ON" if h["ignition"] else "off"
      print(
          f"{h['name']:<25} {h['voltage_v']:>5.1f} {ign_str:<5} "
          f"{h['battery_pred']:>8}  {dtc_str}"
      )
  ```

  ```javascript JavaScript theme={null}
  const API_BASE = "https://api.telemax.com.au";
  const COMPANY_ID = 85;
  const VEHICLE_IDS = [1000, 1001, 1002];

  // getToken() assumed available from your auth module

  async function getPosition(vehicleId, token) {
    const res = await fetch(`${API_BASE}/api/GetLastPositionData?id=${vehicleId}`, {
      method: "POST",
      headers: { Authorization: `Bearer ${token}` },
    });
    return res.json();
  }

  async function getAllBatteryHealth(companyId, token) {
    const items = [];
    let page = 1;
    while (true) {
      const res = await fetch(
        `${API_BASE}/api/GetCompanyVehiclesBatteryHealth?compId=${companyId}&page=${page}&pageSize=50`,
        { method: "POST", headers: { Authorization: `Bearer ${token}` } },
      );
      const data = await res.json();
      items.push(...(data.items ?? []));
      if (page >= (data.numberOfPages ?? 1)) break;
      page++;
    }
    return Object.fromEntries(items.map(b => [b.vehicleId, b]));
  }

  async function getDtcCodes(vehicleId, token) {
    const res = await fetch(`${API_BASE}/api/devices/${vehicleId}/dtc-codes`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    const raw = await res.json();  // array-of-arrays
    const codes = [...new Set(raw.flat().map(d => d.code).filter(Boolean))];
    return codes;
  }

  async function buildDashboard() {
    const token = await getToken();
    const batteryMap = await getAllBatteryHealth(COMPANY_ID, token);

    const records = await Promise.all(
      VEHICLE_IDS.map(async (vid) => {
        const [pos, dtcs] = await Promise.all([
          getPosition(vid, token),
          getDtcCodes(vid, token),
        ]);
        const batt = batteryMap[vid];
        const topPred = batt?.predictionResults?.sort(
          (a, b) => a.predictionForDay - b.predictionForDay
        )[0];
        return {
          name:        pos.DeviceName ?? String(vid),
          voltageV:    pos.Voltage,
          ignition:    pos.Ignition,
          batteryPred: topPred?.BatteryPredictionValue ?? "N/A",
          dtcCodes:    dtcs,
          lastSeen:    pos.UtcTime,
        };
      }),
    );

    console.table(records.map(r => ({
      Vehicle:    r.name,
      "V":        r.voltageV?.toFixed(1),
      Ignition:   r.ignition ? "ON" : "off",
      Battery:    r.batteryPred,
      DTCs:       r.dtcCodes.join(", ") || "none",
      "Last seen": r.lastSeen,
    })));
  }

  buildDashboard();
  ```
</CodeGroup>

***

## Limitations

* **DTC description and severity are empty.** The `DtcDto.description` and `DtcDto.severity` fields are present in the schema but the backend does not populate them. Display the raw `code` value and resolve the meaning externally using the SAE J1979 or ISO 15031-6 fault code database.
* **Safety score is a separate call.** `GetSafetyScore` returns per-trip `riskLevel` / `riskScore` (0–100) / `duration` / `distance` for a single vehicle over a date range. It is not included here because it operates at trip granularity, not vehicle-snapshot granularity — use it alongside trip replay for driver safety dashboards.
* **Battery prediction results are company-wide.** `GetCompanyVehiclesBatteryHealth` fetches all vehicles in the company. For large fleets, cache this response and join locally rather than calling it once per vehicle.

***

## Gotchas

* **`DeviceId` = `vehicleId` = DTC `{id}`.** All three endpoints use the same integer vehicle identifier under different field names.
* **`UtcTime` has no `Z` suffix** but is always UTC — treat it as UTC when parsing.
* **Voltage is external battery voltage.** `InternalBatteryVoltage` (on the `PositionDto`) is the device's internal backup battery, not the vehicle's 12 V battery. Use `Voltage` for vehicle health.
