Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Irrigation Controllers

LocalSky’s IrrigationController port abstracts the act of firing valves. The same engine output (zone X for Y seconds) dispatches to any supported controller. Pick the one that fits your hardware.

Supported controllers

ControllerPathCloud required?Hardware cost (US$)Status
OpenSprinkler (boxed)Direct HTTP on LANNo130-180Shipped
OpenSprinkler PiDirect HTTP on LANNo~80 (Pi) + relay boardShipped
DIY / ESP32 (HTTP)Direct HTTP on LANNo5-40 ESP32 + valvesShipped
DIY / ESP32 (MQTT)MQTT (ESPHome, Tasmota, Z2M)No5-40 ESP32 + valvesShipped
Home Assistant service callHA RESTNo (HA local)Whatever HA drivesShipped
Rachio Gen 2/3Rachio cloud APIYes130-250Shipped
Hunter HydrawiseHydrawise cloud APIYes130-300Shipped
Orbit B-hyveB-hyve cloud APIYes80-150Shipped
Rain BirdRain Bird cloud APIYes100-300Shipped
DryRunNo-opNoNoneShipped

Prices are US retail; availability and cost vary by region. Rachio, B-hyve, Hydrawise, and Rain Bird are sold mostly through North American retail; OpenSprinkler and ESP32 hardware ship worldwide, which makes them the natural picks outside North America too. All four cloud controllers are offered in the controller picker as of 0.7.

Rolling your own with an ESP32 or another relay board? See DIY & ESP32 controllers for the two supported paths (a small HTTP contract, or MQTT) with copy-and-flash reference firmware, beginner to advanced.

OpenSprinkler (the ideal)

OpenSprinkler is LocalSky’s reference controller for one reason: it speaks a documented HTTP API on the LAN with no cloud dependency. No telemetry to a vendor, no account required, no app subscription. The hardware is open-source (schematic + firmware) and the protocol has been stable for years.

Hardware options

  • OpenSprinkler 3.x boxed (24 stations, US$180), the canonical choice for an outdoor enclosure.
  • OpenSprinkler 3.x bare PCB (US$130), DIY mount.
  • OpenSprinkler Pi: a Pi HAT + relay board. Cheaper if you have a spare Pi.
  • OpenSprinkler OSPi-Plus: newer board, more I/O.

Firmware 2.1.9 or newer is required.

LocalSky integration

[[controllers]]
id = "os_main"
default = true
enabled = true
kind = "opensprinkler_direct"
[controllers.config]
host = "192.0.2.10"
port = 80
password_md5 = "<md5 of plaintext password>"
poll_interval_s = 10

The first-run wizard or /settings/controllers does this for you. The password_md5 is computed client-side at config time; the plaintext never leaves your browser.

What LocalSky uses

  • GET /jc for status (zone states, water level %, rain sensor, firmware version)
  • GET /cm for manual station start/stop
  • GET /cv for stop-all
  • GET /jl for run-history backfill

LocalSky never touches the program/schedule storage on the OS device. Schedules live in LocalSky’s engine; the controller is just a valve-firing API.

Where OpenSprinkler shines

  • Direct LAN control means no cloud lag, no service outages, no app required
  • Detailed status JSON (water level, rain sensor, flow meter, per-station runtime)
  • Native run-history endpoint enables LocalSky’s restart-recovery + audit
  • Active open-source community

Where OpenSprinkler falls short

  • HTTP only (no TLS by default; put it behind a reverse proxy if you must expose it)
  • MD5 password (legacy crypto; not a deal-breaker on a LAN but not great)
  • 24-station boxed limit (chain a slave for more)

Home Assistant service call (legacy continuity)

If you already drive irrigation through Home Assistant, OpenSprinkler integration, Irrigation Unlimited, Rachio HACS, ESPHome sprinkler, LocalSky can dispatch through HA service calls without replumbing anything.

[[controllers]]
id = "ha_main"
default = true
enabled = true
kind = "ha_service_call"
[controllers.config]
base_url = "http://homeassistant.local:8123"
bearer_token = "${HA_LONG_LIVED_TOKEN}"
start_service = "script.os_zone_toggle"
stop_service = "opensprinkler.stop"
[controllers.config.zone_entity_map]
back_yard = "switch.back_yard_zone"
front_yard = "switch.front_yard_zone"

LocalSky’s payload to HA is normalized: {"entity_id": "<from map>", "duration_s": <seconds>, "minutes": <float>}. Your HA-side script or service template picks the field it understands.

Use cases:

  • Migrating from an HA-driven irrigation setup without re-wiring schedules
  • Using a controller LocalSky has no native adapter for yet, when a Home Assistant integration for it already exists
  • Wanting irrigation runs to flow through HA’s automation engine for additional logic

ESP32 / DIY (ESPHome, Tasmota, custom)

An ESP32 with a relay board is a smart irrigation controller for ~$15-40 in parts. LocalSky drives it two ways, both first-class and both covered in detail on the DIY & ESP32 controllers page:

  • MQTT (mqtt_command): for boards that already speak MQTT (ESPHome, Tasmota, Zigbee2MQTT, a bare relay). Optional state/availability/flow readback. The bundled ESPHome reference firmware uses this path.
  • HTTP (http_generic): for a self-contained board with no broker. LocalSky polls a small REST contract, so Test connection + Scan zones work in the wizard. A copy-and-flash ESP32 Arduino sketch ships in examples/http/.

A native ESPHome protobuf adapter (esphome_native) is scaffolded but not yet built, so it is not offered in the controller picker. Use MQTT or HTTP above for ESPHome hardware today.

Cloud controllers (Rachio, Hydrawise, B-hyve, Rain Bird)

Four vendor controllers are driven natively through their own clouds; all ship in 0.7 and appear in the controller picker under “Cloud account”. Each authenticates with your vendor account (an API token, or account email + password) and maps LocalSky zone slugs to that controller’s zones/stations. Put secrets in env vars and interpolate them with ${...} so they never sit in the config in cleartext.

You do not need Home Assistant for any of these; the native adapter talks to the vendor cloud directly. (Driving one through HA with ha_service_call is still an option if you already do that.)

Rachio Gen 2/3

Uses a Rachio API token. Map each zone slug to its Rachio zone UUID.

[[controllers]]
id = "rachio_main"
default = true
enabled = true
kind = "rachio"
[controllers.config]
api_token = "${RACHIO_API_TOKEN}"
device_id = "..."   # Rachio device id
[controllers.config.zone_uuid_map]
back_yard = "..."   # Rachio zone UUID

Hunter Hydrawise

Uses a Hydrawise API key. controller_id scopes commands when your account has more than one controller; map each zone slug to its Hydrawise relay id.

[[controllers]]
id = "hydrawise_main"
default = true
enabled = true
kind = "hydrawise"
[controllers.config]
api_key = "${HYDRAWISE_API_KEY}"
controller_id = 0            # controller serial / id
[controllers.config.zone_relay_map]
back_yard = 1                # Hydrawise relay_id

Orbit B-hyve

Signs in with your B-hyve account email and password. device_id (from the account’s device list) scopes commands; map each zone slug to its B-hyve station number (1-based).

[[controllers]]
id = "bhyve_main"
default = true
enabled = true
kind = "bhyve"
[controllers.config]
email = "${BHYVE_EMAIL}"
password = "${BHYVE_PASSWORD}"
device_id = "..."            # from the account's /v1/devices list
[controllers.config.zone_station_map]
back_yard = 1                # B-hyve station number (1-based)

Rain Bird

Signs in with your Rain Bird account email and password. controller_id comes from your account’s controller list; map each zone slug to its Rain Bird station number (1-based). base_url defaults to the production endpoint and only needs setting if Rain Bird rotates hosts.

[[controllers]]
id = "rainbird_main"
default = true
enabled = true
kind = "rainbird"
[controllers.config]
email = "${RAINBIRD_EMAIL}"
password = "${RAINBIRD_PASSWORD}"
controller_id = "..."                       # from the account's controller list
base_url = "https://rdz-rest.rainbird.com"  # default; override only if the host changes
[controllers.config.zone_station_map]
back_yard = 1                               # Rain Bird station number (1-based)

DryRun (no-op)

For testing, demos, and CI. DryRun records intent (with optional simulated runs that write to the SQLite history) but never fires anything.

[[controllers]]
id = "dry"
default = true
kind = "dry_run"
[controllers.config]
simulate_runs = true   # write fake completed runs into history for dashboard population

LOCALSKY_DEMO=1 env auto-creates this controller.

Multi-controller setups

The ControllerRegistry supports any number of controllers. Use cases:

  • Primary + backup: production OS device + DryRun for safety during config changes
  • Geographic split: front-yard OS + back-yard ESPHome on different LAN subnets
  • HA-bridged + direct: legacy HA-driven zones + new direct-controlled zones in the same deployment

Per-zone controller_id in ZoneConfig picks which controller fires that zone. Exactly one controller must have default = true; new zones inherit that.

Editing and renaming controllers

Controller IDs are editable, even after zones are linked. When you rename a controller (in Settings > Devices), every zone that points to it migrates to the new id automatically, so there are no dangling references and no manual fixup. The default controller flag migrates the same way: change which controller is the default and new unassigned zones inherit it.

Adding a new controller

Open src/controllers/<name>.rs, implement the IrrigationController trait:

#![allow(unused)]
fn main() {
#[async_trait]
impl IrrigationController for MyController {
    fn id(&self) -> &str { &self.id }
    fn supports(&self) -> ControllerCaps { ... }
    async fn run_zone(&self, slug: &str, duration_s: u32) -> ControllerResult<RunHandle> { ... }
    async fn stop_zone(&self, slug: &str) -> ControllerResult<()> { ... }
    async fn stop_all(&self) -> ControllerResult<()> { ... }
    async fn status(&self) -> ControllerResult<ControllerStatus> { ... }
    async fn run_history(&self, since_epoch: i64) -> ControllerResult<Vec<RunRecord>> { ... }
}
}

Add a variant to ControllerKind in src/config/schema.rs. Wire construction in src/runtime.rs::build_controllers. ~100-200 lines total.

See src/controllers/dry_run.rs for the minimal example, src/controllers/opensprinkler_direct.rs for a full HTTP-API integration.