# OTO API — Reverse-Engineered Backend

This documents OTO's actual backend, discovered via DNS/traffic analysis. All
endpoints were confirmed working against a live OTO account.

## Architecture overview

OTO has **no public API**. Their support page states the product "does not
interface with any other smart home devices." Internally, the app uses:

- **Firebase Authentication** (Google Identity Toolkit) — login/token
- **Three Google Cloud Run services** — all in `us-central1`, GCP project
  `oto-test-3254b` (project number `716180884817`)
- **Cloud Firestore** — stores zone groups, schedules; 403 to direct client access

```
OTO App ──► Firebase Auth ─────────────────────────► idToken
            │
            ├──► EMS Cloud Run   (device/zone CRUD, status, run)
            ├──► Scheduler       (push schedule to device)
            └──► Unitcall        (device → cloud only; not useful for control)
```

## Authentication

All Cloud Run API calls require a Firebase id token (JWT, 1-hour TTL).

### Sign in

```
POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=AIzaSyBlkDTR_GZSnGJuXdWAtH65ceeXyHtSrIs

Body (JSON):
{
  "email": "...",
  "password": "...",
  "returnSecureToken": true
}

Response:
{
  "idToken": "<jwt>",
  "refreshToken": "<token>",
  "expiresIn": "3600",
  "localId": "<uid>"           ← your account UID, used in all EMS paths
}
```

### Refresh token

```
POST https://securetoken.googleapis.com/v1/token?key=AIzaSyBlkDTR_GZSnGJuXdWAtH65ceeXyHtSrIs

Body (application/x-www-form-urlencoded):
grant_type=refresh_token&refresh_token=<token>

Response: { "id_token": "...", "refresh_token": "...", "expires_in": "3600", "user_id": "..." }
```

## EMS — primary data API

Base URL: `https://oto-cloud-service-ems-prod-716180884817.us-central1.run.app`

**Required header on all requests:** `Authorization: Bearer <idToken>`

> ⚠️ **GET requests must NOT include `Content-Type: application/json`** — the
> service returns `400 Bad Request` when this header is present on GETs.
> Only include `Content-Type` on POST/PUT/PATCH with a body.

### List controllers (devices)

```
GET /account/{uid}/devices

Response: [
  {
    "unitName": "oto5736825",    ← stable device ID used in all other calls
    "userName": "Front Yard",   ← user-assigned name
    ...
  },
  ...
]
```

### List zones for a device

```
GET /account/{uid}/device/{deviceId}/zones

Response: [
  {
    "zoneId": "FhFU1lfRN3IhyxPX",
    "zoneName": "Left Front Yard",
    "wateringTime": 4.5,               ← minutes
    "nozzleAnglePath_cdeg": [12900],   ← nozzle angle(s) in centi-degrees
    "throwDistanceCoords_cm": [105],
    "wateringArray": [1,1,1,1,1,1,1],  ← which days of week to water
    "pathType": 2,
    ...
  },
  ...
]
```

### Get device status

```
GET /device/{deviceId}/status

Response:
{
  "pathIndex": null,      ← null = idle; 0, 1, 2, ... = actively watering that path index
  "scheduleId": "...",    ← ID of the schedule currently running (if any)
  "version": "...",
  ...
}
```

`pathIndex` is the only indicator that the device is watering. It is
device-level — there is no per-zone active state in the API. All zones on the
same device show `watering: true` while `pathIndex != null`.

### Create or update a zone

```
POST /account/{uid}/device/{deviceId}/zone
PUT  /account/{uid}/device/{deviceId}/zone/{zoneId}

Body (JSON):
{
  "zoneName": "...",
  "wateringTime": 4,
  "nozzleAnglePath_cdeg": [...],
  "throwDistanceCoords_cm": [...],
  "wateringArray": [1,1,1,1,1,1,1],
  "pathType": 2
}
```

### Trigger immediate watering — "Water Now"

> Historical note: an earlier assumption was that Water Now used
> `POST /account/{uid}/run` with a Firestore `zone_group_id`. That was wrong.
> Live capture of the OTO iOS app proved Water Now uses the Scheduler's
> `/manual-start` endpoint below, which needs **no** `zone_group_id` and **no**
> auth token. There is nothing to capture — it works out of the box.

The real endpoint lives on the **Scheduler** service (see below):
`POST {SCHEDULER}/manual-start`.

## Scheduler

Base URL: `https://oto-cloud-service-scheduler-prod-716180884817.us-central1.run.app`

### Start immediate watering ("Water Now") — CONFIRMED WORKING

This is exactly what the OTO app sends when you tap "Water Now" for a zone.

```
POST /manual-start

Headers:
  Content-Type: application/json
  (NO Authorization header — the endpoint is open and returns 200 without a token)

Body:
{
  "uid": "<uid>",
  "deviceId": "<deviceId>",
  "zoneId": "<zoneId>",
  "wateringQuantity": 12.7
}
```

`wateringQuantity` is the watering **depth in millimetres**, not a duration.
OTO's backend converts it to a runtime using the zone's own precipitation rate.
The OTO app uses `31.75` mm (≈ 1.25") for a deep soak.

```
Response 200:
{
  "message": "Manual event successfully scheduled",
  "scheduleId": "f2MMHk05",
  "schedule_item": {
    "zoneId": "<zoneId>",
    "zoneName": "Left Front Yard",
    "zoneGroupId": null,
    "irrigationQuantity": {
      "path": { "waterVolume_L": 323.94 },
      "scheduled": { "wateringDepth_mm": 31.75 }
    },
    "runtime_min": 22.95,
    "scheduleItemStatus": "SCHEDULED",
    "scheduleItemType": "MANUAL"
  }
}
```

Because the call only needs `uid` + `deviceId` + `zoneId` (all discovered via
EMS) plus a depth, **every zone works immediately** — no per-zone capture or
`zone_group_id` is required. Configure the depth with `wateringQuantity`
(global default) and/or `wateringQuantities` (per-zone map) in your Homebridge
config.

### Push schedule to device

Regenerates the device's schedule and pushes it over-the-air. Used as a
best-effort "stop" command (the device re-syncs, which may interrupt an active
run).

```
POST /schedule

Headers:
  Authorization: Bearer <idToken>
  Content-Type: application/json

Body:
{
  "uid": "<uid>",
  "deviceId": "<deviceId>",
  "shouldRegenerate": true
}

Response: "true"
```

## Unitcall

Base URL: `https://oto-cloud-service-unitcall-prod-716180884817.us-central1.run.app`

Device-to-cloud reporting service. No useful control endpoints found.

## Known devices (example account)

| unitName     | userName    |
|--------------|-------------|
| oto5736825   | Front Yard  |
| oto7531051   | Back Yard   |

## What doesn't work

| Feature | Reason |
|---|---|
| Immediate stop | No stop endpoint found; schedule-push is best-effort |
| Direct Firestore access | All paths return 403 PERMISSION_DENIED |
| Firebase Realtime DB | Empty / unused by OTO |

> Immediate per-zone **start** now works via `POST /manual-start` (see Scheduler
> section) — no `zone_group_id` needed.

## Local alternative

The community [irrigoto](https://github.com/rob-farrellrobotics/irrigoto)
firmware reflashes the device's ESP32 and exposes a local ESPHome web server.
This plugin's `local` transport talks to it without any cloud dependency.
