npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

homebridge-oto

v1.0.0

Published

Homebridge plugin for Oto smart sprinklers

Readme

homebridge-oto

A Homebridge plugin for OTO smart sprinklers. It exposes every OTO zone as a HomeKit Irrigation Valve accessory so you can check watering status, control zones, and build automations — using nothing but your existing OTO app credentials.

OTO has no official API and no HomeKit support. This plugin works by reverse-engineering the same Google Cloud backend the OTO mobile app uses. All endpoints were confirmed against a live OTO account.


Contents


How the plugin works

Architecture

Homebridge
└── OtoPlatform  (DynamicPlatformPlugin)
    ├── OtoApiClient  (transport facade)
    │   ├── CloudTransport   ← Firebase Auth + OTO Cloud Run APIs
    │   └── LocalTransport   ← irrigoto ESPHome firmware (local LAN, no cloud)
    └── SprinklerAccessory × N  (one per zone)
        ├── Valve service     (Active, InUse, ValveType=IRRIGATION, SetDuration)
        └── AccessoryInformation service

OtoPlatform calls getControllers() at startup to discover devices, then getZones() on each device to build the zone list. One SprinklerAccessory is registered in Homebridge per zone.

Authentication and token refresh

CloudTransport authenticates lazily — the first API call triggers a Firebase email/password sign-in. The resulting idToken (JWT, valid 1 hour) is attached as Authorization: Bearer <token> on every subsequent request. Before each call the transport checks if the token expires within the next 60 seconds; if so it uses the refreshToken to silently obtain a fresh idToken without re-entering credentials. If the refresh token is itself stale the transport falls back to a full re-login.

Status polling and caching

Each SprinklerAccessory runs an independent 30-second timer that calls getZoneStatus(). The underlying status call is GET /device/{deviceId}/status on the EMS service, which returns a pathIndex field: null means the device is idle; any integer means it is actively running a zone.

Because a device with 8 zones would otherwise fire 8 concurrent status requests on every tick, CloudTransport keeps a 6-second in-memory cache of each device's last status response. All zones on the same device share the cached response within that window, collapsing N requests into one.

One important nuance: pathIndex is a device-level indicator, not a zone-level one. OTO's API does not tell you which specific zone is running — only that the device is running something. As a result, all zones on the same device report watering: true simultaneously while the device is active. This is the best accuracy the available API allows.

HomeKit valve mapping

Each zone becomes a HomeKit Valve service with ValveType set to IRRIGATION. HomeKit requires both Active (user intent) and InUse (physical state) to be set; the plugin keeps them in sync on every status update and on every start/stop command. SetDuration (1–60 minutes) is exposed and stored locally; it is passed to startZone() when the valve is turned on.

Transport switching

Set "transport": "local" to use the LocalTransport instead of the cloud. It talks to a device running the community irrigoto ESPHome firmware over your LAN via ESPHome's built-in web-server REST API (GET /switch/zone_N, POST /switch/zone_N/turn_on, etc.). No OTO credentials required, but it requires opening the device and reflashing the ESP32.


Discovered REST APIs

OTO's backend consists of three Google Cloud Run services in us-central1 (GCP project oto-test-3254b, project number 716180884817) and standard Firebase Auth endpoints.

Firebase Authentication

Sign in

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

Content-Type: application/json
Body:
{
  "email": "[email protected]",
  "password": "...",
  "returnSecureToken": true
}

Response 200:
{
  "idToken":      "<jwt>",          // bearer token for all Cloud Run calls
  "refreshToken": "<token>",        // long-lived; exchange for new idToken
  "expiresIn":    "3600",           // seconds until idToken expires
  "localId":      "<uid>"           // Firebase UID; used in all /account/{uid}/... paths
}

Refresh token

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

Content-Type: application/x-www-form-urlencoded
Body: grant_type=refresh_token&refresh_token=<refreshToken>

Response 200:
{
  "id_token":      "<new jwt>",
  "refresh_token": "<new refresh token>",
  "expires_in":    "3600",
  "user_id":       "<uid>"
}

EMS — primary data service

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

All requests require: Authorization: Bearer <idToken>

Critical: GET requests to EMS must not include a Content-Type header. The service returns 400 Bad Request if Content-Type: application/json is present on a GET. Only set Content-Type on requests that send a body (POST, PUT, PATCH).

List controllers (devices)

GET /account/{uid}/devices

Response 200: array of device objects
[
  {
    "unitName": "oto5736825",   // stable device ID used in all other paths
    "userName": "Front Yard",   // user-assigned name from OTO app
    ...
  }
]

List zones

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

Response 200: array of zone objects
[
  {
    "zoneId":                  "FhFU1lfRN3IhyxPX",
    "zoneName":                "Left Front Yard",
    "wateringTime":            4.5,          // minutes
    "nozzleAnglePath_cdeg":    [12900],      // nozzle sweep angle(s) in centi-degrees
    "throwDistanceCoords_cm":  [105],
    "wateringArray":           [1,1,1,1,1,1,1], // which days of the week to water (Sun–Sat)
    "pathType":                2
  },
  ...
]

Create a zone

POST /account/{uid}/device/{deviceId}/zone
Content-Type: application/json

Body: same fields as zone object above (omit zoneId; server assigns it)

Response 200: the created zone object including its new zoneId

Update a zone

PUT /account/{uid}/device/{deviceId}/zone/{zoneId}
Content-Type: application/json

Body: zone fields to update

Get device status

GET /device/{deviceId}/status

Response 200:
{
  "pathIndex":  null,          // null = device idle; integer = device actively running a zone
  "scheduleId": "FwO1l4Y0",   // ID of the schedule currently executing, if any
  "version":    "...",         // firmware version
  ...
}

pathIndex is the only real-time activity indicator. It is device-level — the API does not reveal which specific zone is running, only that the device is busy.

Trigger immediate watering

Immediate watering is not an EMS endpoint. "Water Now" is handled by the Scheduler service's POST /manual-start — see Scheduler service below. (An earlier POST /account/{uid}/run + zone_group_id theory turned out to be a dead end; live app capture proved /manual-start is what the app uses.)


Scheduler service

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

Start immediate watering — "Water Now" ✅

Exactly what the OTO app sends when you tap Water Now on a zone. Confirmed by live capture of the iOS app.

POST /manual-start
Content-Type: application/json
(no Authorization header — the endpoint is open and returns 200 without a token)

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

Response 200:
{
  "message": "Manual event successfully scheduled",
  "scheduleId": "f2MMHk05",
  "schedule_item": { "runtime_min": 22.95, "scheduleItemType": "MANUAL", "zoneGroupId": null, ... }
}

wateringQuantity is a watering depth in millimetres (not a duration); the backend converts it to a runtime using the zone's precipitation rate. The app uses 31.75 mm (≈ 1.25") for a deep soak. Since the call only needs values the plugin already discovers, every zone works with no extra setup.

Push schedule to device

Regenerates the device's schedule from the server-side record and transmits it to the device over-the-air. The device re-syncs its schedule; in practice this can interrupt an active watering run.

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

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

Response 200: "true"

The plugin uses this as a best-effort stop command since no dedicated stop endpoint was found.


Unitcall service

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

Device-to-cloud telemetry only. No useful control endpoints were found.


Firestore (blocked)

OTO stores zone groups, schedules, and other config in Cloud Firestore under project oto-test-3254b. All direct Firestore REST API calls return 403 PERMISSION_DENIED — the security rules deny client access entirely. The Cloud Run services access Firestore via the Firebase Admin SDK server-side.


Water Now — how it works

Water Now works out of the box — no proxy capture, no zone_group_id, no per-zone setup. Tapping a valve on in HomeKit calls the Scheduler's POST /manual-start (see above) with values the plugin already discovers.

Watering amount (depth, not duration)

OTO is depth-based: /manual-start takes a wateringQuantity in millimetres and the backend computes the runtime from each zone's precipitation rate. HomeKit's valve duration slider does not map cleanly to this, so the plugin ignores it and sends a configurable depth instead:

  • wateringQuantity — global default depth in mm (plugin default: 12.7, ≈ 0.5")
  • wateringQuantities — optional per-zone overrides, keyed by zoneId
{
  "platforms": [
    {
      "platform": "Oto",
      "transport": "cloud",
      "email": "[email protected]",
      "password": "your-password",
      "wateringQuantity": 12.7,
      "wateringQuantities": {
        "FhFU1lfRN3IhyxPX": 25.4,
        "cCVzq2E6dBwMLUrP": 19.05
      }
    }
  ]
}

For reference, the OTO app sends 31.75 mm (≈ 1.25") for a deep soak, which on one test zone produced a ~23-minute run. Start conservative and tune to taste.

What the plugin does

  • Discovers all your OTO devices and zones automatically
  • Polls and reports real-time watering status
  • Exposes every zone as a HomeKit Irrigation Valve you can turn on/off
  • Supports HomeKit automations on zone state

Note on stop: OTO exposes no dedicated stop endpoint. Turning a valve off pushes a schedule regeneration as a best-effort interrupt; the device also stops on its own at the end of the computed run.


Installation

Prerequisites

  • Node.js 18 or later
  • A running Homebridge instance
  • An OTO account with at least one device set up in the OTO app

Install via Homebridge UI

Search for homebridge-oto in the Homebridge plugin search and install from there.

Install via npm

npm install -g homebridge-oto

Homebridge will pick it up automatically on the next restart.


Configuration

Add a platform block to your Homebridge config.json (typically at ~/.homebridge/config.json):

Minimal — status + Water Now

{
  "platforms": [
    {
      "platform": "Oto",
      "name": "OTO Sprinklers",
      "transport": "cloud",
      "email": "[email protected]",
      "password": "your-oto-password"
    }
  ]
}

This discovers all devices and zones, shows live watering status, and lets you turn any zone on/off in HomeKit — Water Now works with no extra setup. The default watering depth is 12.7 mm (≈ 0.5").

Full cloud config — custom watering amounts

{
  "platforms": [
    {
      "platform": "Oto",
      "name": "OTO Sprinklers",
      "transport": "cloud",
      "email": "[email protected]",
      "password": "your-oto-password",
      "wateringQuantity": 12.7,
      "wateringQuantities": {
        "FhFU1lfRN3IhyxPX": 25.4,
        "cCVzq2E6dBwMLUrP": 19.05
      },
      "updateInterval": 30
    }
  ]
}

wateringQuantity is the default watering depth in mm; wateringQuantities overrides it per zone (keyed by zoneId). See Water Now — how it works.

Local config — irrigoto ESPHome firmware

{
  "platforms": [
    {
      "platform": "Oto",
      "name": "OTO Sprinklers",
      "transport": "local",
      "localDevices": ["irrigoto-ab12cd.local", "irrigoto-ef34gh.local"],
      "updateInterval": 30
    }
  ]
}

All options

| Option | Type | When required | Description | |---|---|---|---| | platform | string | always | Must be "Oto" | | name | string | | Label shown in Homebridge logs (default: "Oto") | | transport | string | | "cloud" (default) or "local" | | email | string | cloud | Your OTO account email | | password | string | cloud | Your OTO account password | | wateringQuantity | number | | Default Water Now depth in mm (default: 12.7) | | wateringQuantities | object | | Per-zone depth overrides, keyed by zoneId | | localDevices | string[] | local | irrigoto device hostnames or IPs | | updateInterval | integer | | Status poll interval in seconds, 10–600 (default: 30) |


Local development

git clone https://github.com/jonnadul/homebridge-oto.git
cd homebridge-oto
npm install

Build once:

npm run build

Watch mode (rebuilds on save):

npm run watch

Run tests:

npm test

Lint:

npm run lint

Link into a local Homebridge for live testing:

# In the plugin repo
npm link

# In your Homebridge directory (or globally)
npm link homebridge-oto

# Restart Homebridge
homebridge -D

The dist/ directory is the compiled output. Homebridge loads dist/index.js as the plugin entry point (set in package.json "main").

Source layout

| File | Purpose | |---|---| | src/index.ts | Plugin entry point — registers OtoPlatform with Homebridge | | src/platform.ts | OtoPlatform — discovers devices/zones, manages accessory lifecycle | | src/transport.ts | Shared types (OtoController, OtoZone, OtoTransport interface, OtoClientConfig) | | src/cloudTransport.ts | CloudTransport — Firebase Auth + OTO Cloud Run API | | src/localTransport.ts | LocalTransport — irrigoto ESPHome web-server REST | | src/otoApi.ts | OtoApiClient facade — selects transport from config | | src/sprinklerAccessory.ts | SprinklerAccessory — HomeKit Valve per zone | | config.schema.json | Homebridge UI config schema | | docs/OTO_API.md | Extended API reference and proxy capture walkthrough |


Troubleshooting

"OTO sign-in failed"

Your email or password is wrong, or the OTO backend is unreachable. Verify you can log into the OTO mobile app with the same credentials.

Zones not appearing

  1. Check the Homebridge log for errors from OtoPlatform
  2. Confirm your OTO account has at least one device configured and online in the OTO app
  3. Try restarting Homebridge — the platform retries discovery every 60 seconds on failure

A zone waters too long or too little

The plugin sends a watering depth (wateringQuantity, in mm), and OTO converts it to a runtime from the zone's precipitation rate. Lower the depth for a shorter run, or set a per-zone value in wateringQuantities. See Water Now — how it works.

"no direct stop endpoint found"

When you turn a valve off, the plugin pushes your OTO schedule to the device (the closest available stop command). The device will stop watering when the current schedule segment ends. This is a backend API limitation.

Status is wrong — all zones show watering at once

OTO's /device/{deviceId}/status endpoint reports only that the device is running, not which specific zone. While any zone on a device is active, all valves for that device will show InUse in HomeKit. This is the best accuracy the API provides.

Token errors after a long gap

Firebase id tokens expire after 1 hour. The plugin refreshes them automatically using the long-lived refresh token. If Homebridge was suspended for an extended period (days), the refresh token itself may have expired — restart Homebridge to force a full re-login.


Disclaimer

This plugin is not affiliated with or endorsed by OTO Lawn Care. It works by reverse-engineering OTO's backend from traffic observed on a personal account. OTO may change their backend at any time, which could break this plugin. Use it at your own risk, and always verify your irrigation schedule is running correctly in the OTO app.

License

Apache 2.0 — see LICENSE.