> ## 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 engine codes into a single per-vehicle health view using V2 endpoints.

## Overview

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

| Endpoint                                  | Data                                                            |
| ----------------------------------------- | --------------------------------------------------------------- |
| `GET /v2/api/vehicles/{id}/last-position` | Live voltage, ignition state, speed, odometer, last GPS fix     |
| `GET /v2/api/battery-health`              | Battery voltage and day-ahead prediction results (company-wide) |
| `GET /v2/api/vehicles/{id}/engine-codes`  | Active OBD-II / CAN fault codes with descriptions and severity  |

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

***

## Step 1 — Get live position and status

`GET /v2/api/vehicles/{id}/last-position` 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) |
| `isOnline` | boolean | Whether the device is currently connected           |

```bash cURL theme={null}
curl -s "https://api.telemax.com.au/v2/api/vehicles/1000/last-position" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

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

def get_position(vehicle_id: int, token: str) -> dict:
    r = requests.get(
        f"https://api.telemax.com.au/v2/api/vehicles/{vehicle_id}/last-position",
        headers={"Authorization": f"Bearer {token}"},
    )
    r.raise_for_status()
    return r.json()
```

***

## Step 2 — Get battery health predictions

`GET /v2/api/battery-health` returns a paginated list of predictions for all vehicles in the company. No `companyId` parameter is needed — scope comes from the token.

```python Python theme={null}
def get_all_battery_health(token: str) -> list:
    results = []
    page = 1
    while True:
        r = requests.get(
            "https://api.telemax.com.au/v2/api/battery-health",
            params={"page": page, "pageSize": 50},
            headers={"Authorization": f"Bearer {token}"},
        )
        r.raise_for_status()
        data = r.json()
        items = data.get("items") or []
        results.extend(items)
        if page * 50 >= data.get("totalCount", 0) or not items:
            break
        page += 1
    return results
```

Each item in `items` contains:

| Field               | Description                                                                  |
| ------------------- | ---------------------------------------------------------------------------- |
| `vehicleId`         | Vehicle identifier (join key)                                                |
| `vehicleName`       | Vehicle display name                                                         |
| `currentVoltage`    | Current battery voltage (V)                                                  |
| `band`              | Overall health band: `"Critical"`, `"Warning"`, `"Fair"`, `"Healthy"`        |
| `healthScore.score` | Health score out of 100                                                      |
| `forecast.score`    | Estimated days until maintenance needed                                      |
| `forecast.comment`  | AI-generated forecast summary                                                |
| `badges`            | Array of active badge IDs (e.g. `"HEALTHY"`, `"PARK_DRAIN"`, `"TREND_DOWN"`) |
| `daysToReplace`     | Estimated days until battery replacement recommended                         |
| `scoredOn`          | Date the health score was last calculated (`YYYY-MM-DD`)                     |

***

## Step 3 — Get engine fault codes

`GET /v2/api/vehicles/{id}/engine-codes` returns paginated fault codes with full descriptions, severity ratings, and the location where the fault was detected.

```bash cURL theme={null}
curl -s "https://api.telemax.com.au/v2/api/vehicles/1000/engine-codes?page=1&pageSize=50" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

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

Each engine code item includes:

| Field                | Description                               |
| -------------------- | ----------------------------------------- |
| `code`               | Raw OBD-II code (e.g. `P0420`)            |
| `description`        | Plain-English fault description           |
| `severity`           | `"Low"`, `"Medium"`, or `"High"`          |
| `whyThisMatters`     | Impact on vehicle operation               |
| `possibleCauses`     | Likely root causes                        |
| `recommendedActions` | Suggested remediation steps               |
| `detectedAt`         | UTC timestamp when the fault was detected |
| `location.address`   | Reverse-geocoded address at detection     |

***

## 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"
  API_KEY     = os.environ["TELEMAX_API_KEY"]
  VEHICLE_IDS = [1000, 1001, 1002]

  # get_token() assumed from your authentication module

  def build_health_record(vehicle_id: int, token: str, battery_map: dict) -> dict:
      pos   = get_position(vehicle_id, token)
      codes = get_engine_codes(vehicle_id, token)

      batt_entry = battery_map.get(vehicle_id, {})

      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"),
          "band":           batt_entry.get("band", "N/A"),
          "health_score":   batt_entry.get("healthScore", {}).get("score"),
          "days_to_replace": batt_entry.get("daysToReplace"),
          "engine_codes":   [d["code"] for d in codes],
          "engine_severity":[d.get("severity") for d in codes],
      }

  token      = get_token()
  all_batt   = get_all_battery_health(token)
  batt_map   = {b["vehicleId"]: b for b in all_batt}

  print(f"{'Vehicle':<25} {'V':>5} {'Ign':<5} {'Band':>8} {'Engine Codes'}")
  print("-" * 65)
  for vid in VEHICLE_IDS:
      h = build_health_record(vid, token, batt_map)
      codes_str = ", ".join(h["engine_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['band']:>8}  {codes_str}"
      )
  ```

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

  // getToken() assumed available from your auth module

  async function getPosition(vehicleId, token) {
    const res = await fetch(`${API_BASE}/v2/api/vehicles/${vehicleId}/last-position`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    return res.json();
  }

  async function getAllBatteryHealth(token) {
    const items = [];
    let page = 1;
    while (true) {
      const res = await fetch(
        `${API_BASE}/v2/api/battery-health?page=${page}&pageSize=50`,
        { headers: { Authorization: `Bearer ${token}` } },
      );
      const data = await res.json();
      const batch = data.items ?? [];
      items.push(...batch);
      if (page * 50 >= (data.totalCount ?? 0) || !batch.length) break;
      page++;
    }
    return Object.fromEntries(items.map(b => [b.vehicleId, b]));
  }

  async function getEngineCodes(vehicleId, token) {
    const items = [];
    let page = 1;
    while (true) {
      const res = await fetch(
        `${API_BASE}/v2/api/vehicles/${vehicleId}/engine-codes?page=${page}&pageSize=50`,
        { headers: { Authorization: `Bearer ${token}` } },
      );
      const data = await res.json();
      items.push(...(data.items ?? []));
      if (page >= (data.numberOfPages ?? 1)) break;
      page++;
    }
    return items;
  }

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

    const records = await Promise.all(
      VEHICLE_IDS.map(async (vid) => {
        const [pos, codes] = await Promise.all([
          getPosition(vid, token),
          getEngineCodes(vid, token),
        ]);
        const batt = batteryMap[vid];
        return {
          name:         pos.deviceName ?? String(vid),
          voltageV:     pos.voltage,
          ignition:     pos.ignition,
          band:         batt?.band ?? "N/A",
          healthScore:  batt?.healthScore?.score,
          daysToReplace: batt?.daysToReplace,
          engineCodes:  codes.map(d => d.code),
          lastSeen:     pos.utcTime,
        };
      }),
    );

    console.table(records.map(r => ({
      Vehicle:        r.name,
      "V":            r.voltageV?.toFixed(1),
      Ignition:       r.ignition ? "ON" : "off",
      Band:           r.band,
      "Engine Codes": r.engineCodes.join(", ") || "none",
      "Last seen":    r.lastSeen,
    })));
  }

  buildDashboard();
  ```
</CodeGroup>

***

## Limitations

* **Safety score is a separate call.** `GET /v2/api/safety-score/{id}` returns per-trip `riskLevel` / `riskScore` (0–100) / `duration` / `distance` for a single vehicle over a date range. It operates at trip granularity, not vehicle-snapshot granularity — use it alongside trip replay for driver safety dashboards.
* **Battery prediction is company-wide.** `GET /v2/api/battery-health` fetches all vehicles. For large fleets, cache this response and join locally rather than calling it once per vehicle.

***

## Gotchas

* **`deviceId` = `vehicleId` = engine-codes `{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.** Use `voltage` (the vehicle's 12 V battery) for health monitoring. Do not confuse with internal device backup battery.
* **Engine code data is paginated in V2.** Unlike V1 which returned an array-of-arrays, V2 returns a flat paginated `items` list — no flattening or deduplication needed.
