{"id":50516289,"url":"https://github.com/cristoforocervino/esp32-sprinkler-queue","last_synced_at":"2026-06-03T00:04:27.365Z","repository":{"id":355702304,"uuid":"1229150694","full_name":"cristoforocervino/esp32-sprinkler-queue","owner":"cristoforocervino","description":"Queue-based sprinkler controller for ESPHome with native Home Assistant valve entities. Hard guarantee: at most one zone valve is open at any time, ever.","archived":false,"fork":false,"pushed_at":"2026-05-04T20:49:10.000Z","size":117,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-04T22:28:46.566Z","etag":null,"topics":["esp32","esphome","esphome-component","esphome-config","garden-automation","hacs","home-assistant","homeassistant","irrigation","irrigation-controller","smart-irrigation","sprinkler","valve-controller"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cristoforocervino.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-04T18:49:43.000Z","updated_at":"2026-05-04T21:23:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cristoforocervino/esp32-sprinkler-queue","commit_stats":null,"previous_names":["cristoforocervino/esp32-sprinkler-queue"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/cristoforocervino/esp32-sprinkler-queue","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cristoforocervino%2Fesp32-sprinkler-queue","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cristoforocervino%2Fesp32-sprinkler-queue/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cristoforocervino%2Fesp32-sprinkler-queue/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cristoforocervino%2Fesp32-sprinkler-queue/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cristoforocervino","download_url":"https://codeload.github.com/cristoforocervino/esp32-sprinkler-queue/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cristoforocervino%2Fesp32-sprinkler-queue/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33842020,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["esp32","esphome","esphome-component","esphome-config","garden-automation","hacs","home-assistant","homeassistant","irrigation","irrigation-controller","smart-irrigation","sprinkler","valve-controller"],"created_at":"2026-06-03T00:04:26.258Z","updated_at":"2026-06-03T00:04:27.356Z","avatar_url":"https://github.com/cristoforocervino.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ESPHome Sprinkler Queue\n\n![Drip irrigation banner](docs/banner.jpg)\n\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![ESPHome](https://img.shields.io/badge/ESPHome-2026.4%2B-1f8feb.svg)](https://esphome.io/)\n[![Validate examples](https://github.com/cristoforocervino/esp32-sprinkler-queue/actions/workflows/validate-examples.yaml/badge.svg)](https://github.com/cristoforocervino/esp32-sprinkler-queue/actions/workflows/validate-examples.yaml)\n[![Last commit](https://img.shields.io/github/last-commit/cristoforocervino/esp32-sprinkler-queue)](https://github.com/cristoforocervino/esp32-sprinkler-queue/commits/main)\n\nA queue-based sprinkler controller for [ESPHome](https://esphome.io/) with native [Home Assistant](https://www.home-assistant.io/) valve entities.\n\n**Hard guarantee: at most one zone valve is open at any time, ever.** Concurrent open requests queue up and are served one after another with a configurable inter-valve pause to let water pressure recover.\n\n---\n\n## Why this exists\n\nESPHome ships an official [`sprinkler:`](https://esphome.io/components/sprinkler/) component with its own queue and overlap policies — but that logic only kicks in when you drive watering through its actions (`sprinkler.start_full_cycle` and friends). If you instead trigger the individual valves directly — say, two automations both opening a valve at the same instant — the queue is bypassed and both valves open together, halving water pressure and on weak supply lines dropping it low enough to trip pumps or starve other appliances.\n\nThis component takes a different approach: **serialization is a hard, built-in invariant**. No matter how the commands arrive, at most one zone valve is open at any time, ever. Excess requests queue. Each valve runs for its configured duration, then closes; an inter-valve pause lets pressure recover; the next queued valve opens. Cancellations (closing a queued or active valve from HA) are honoured immediately and the queue keeps moving.\n\nWhat I deliberately don't do: scheduling, repeats, multipliers, pump logic, partial position. Those belong in HA automations, where you have a richer language than YAML for them. The firmware's job is one thing only — guarantee non-overlap and protect water pressure.\n\n![Queue serialization diagram](docs/queue-diagram.svg)\n\n---\n\n## Features\n\n- **Queue-based serialization** — only one zone valve open at a time, no exceptions\n- **PENDING state** — queued valves report `current_operation = OPENING` so HA shows \"Opening…\" with the transition animation\n- **Per-valve max duration** — configurable via a HA `number.*` entity, persisted across reboots\n- **Inter-valve pause** — global pause between valves (also a `number.*`, persisted)\n- **Optional master valve** — auto-opened whenever any zone is active; omit the block if your setup doesn't need one\n- **Optional manual master override** — opt-in HA switch to open the master valve independently of any zone, for debugging, pressure testing, or pipe flushing\n\n---\n\n## Installation\n\nIn your ESPHome device YAML:\n\n```yaml\nexternal_components:\n  - source: github://cristoforocervino/esp32-sprinkler-queue\n    components: [sprinkler_queue]\n```\n\n---\n\n## Quick start\n\nA minimal 3-zone setup with direct ESP32 GPIO pins, no master valve:\n\n```yaml\nesphome:\n  name: garden-sprinkler\n\nesp32:\n  board: esp32dev\n\nwifi:\n  ssid: !secret wifi_ssid\n  password: !secret wifi_password\n\napi:\nexternal_components:\n  - source: github://cristoforocervino/esp32-sprinkler-queue\n    components: [sprinkler_queue]\n\nsprinkler_queue:\n  pause_between_valves:\n    initial_value: 5\n  valves:\n    - name: \"Front lawn\"\n      pin: GPIO16\n      initial_duration: 600\n    - name: \"Back lawn\"\n      pin: GPIO17\n      initial_duration: 600\n    - name: \"Roses\"\n      pin: GPIO18\n      initial_duration: 300\n```\n\nFlash, add the device to Home Assistant via the ESPHome integration, and you'll have:\n\n- 3 × `valve.*` entities, each with state-based icons\n- 3 × `number.*_duration` entities (slider/box, 1–3600 s, persisted)\n- 1 × `number.pause_between_valves` (0–120 s, persisted)\n\n---\n\n## Configuration reference\n\n### Top-level options\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `id` | ID | auto | Component ID, useful for `service:` calls |\n| `pause_between_valves` | block (required) | — | Configures the inter-valve pause `number.*` entity (see below) |\n| `master_valve` | block (optional) | omitted | Configures the master valve relay + `binary_sensor.*` (see below) |\n| `valves` | list (required, ≥1) | — | List of zone valves (see below) |\n\n### `pause_between_valves` block\n\nStandard ESPHome [number](https://esphome.io/components/number/) schema, plus:\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `name` | string | `Pause between valves` | HA display name |\n| `icon` | mdi icon | `mdi:timer-pause-outline` | HA icon |\n| `initial_value` | int (s) | `5` | Initial pause if no value persisted yet |\n| `min_value` | int (s) | `0` | Minimum allowed value |\n| `max_value` | int (s) | `120` | Maximum allowed value |\n| `restore_value` | bool | `true` | Persist value across reboots |\n| `mode` | enum | `auto` | `box` for a numeric input field, `slider` for a slider |\n\n### `master_valve` block (optional)\n\nOmit the entire block if your setup doesn't have a master valve.\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `name` | string | `Master valve` | HA display name |\n| `icon` | mdi icon | `mdi:water-pump` | HA icon for the binary_sensor |\n| `pin` | pin schema | required | Output pin controlling the master relay |\n| `manual_switch` | block (optional) | omitted | Adds an HA switch to open the master manually (see below) |\n\nWhen configured, the master pin is opened together with any zone and closed when no zone is active.\n\n#### `manual_switch` sub-block (optional)\n\nOpt-in HA switch entity that lets you open the master valve **independently** of any zone — useful for debugging plumbing, pressure-testing pipes, flushing, or commissioning a new installation. Omit the block to get the standard behavior (master pin follows zones only).\n\n```yaml\nmaster_valve:\n  pin: GPIO16\n  manual_switch:                       # presence of the block enables it\n    name: \"Master valve manual\"        # optional\n    icon: \"mdi:wrench\"                 # optional\n```\n\nGenerates one extra entity: `switch.\u003cdevice\u003e_master_valve_manual`.\n\n**Behavior** (OR-logic with the queue):\n\n| Manual switch | Any zone open | Master pin |\n|---|---|---|\n| OFF | no | OFF |\n| OFF | yes | ON (normal irrigation) |\n| ON | no | **ON (debug mode)** |\n| ON | yes | ON |\n\nWhen you flip the switch OFF while a zone is still active, the master stays ON until the zone closes — the queue keeps full control as soon as the manual override releases it.\n\n**Safety**: the switch always boots **OFF** after a power cycle (no `restore_value`). A reboot will never leave the master open unattended.\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `name` | string | `Master valve manual` | HA display name |\n| `icon` | mdi icon | `mdi:wrench` | HA icon |\n\n### Per-valve entries (`valves[*]`)\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `name` | string | required | HA display name (this also becomes the entity_id slug) |\n| `pin` | pin schema | required | Output pin controlling this zone's relay |\n| `initial_duration` | int (s) | `300` | Initial max duration if no value persisted yet (1–3600) |\n\nEach valve generates two HA entities: `valve.\u003cdevice\u003e_\u003cname\u003e` and `number.\u003cdevice\u003e_\u003cname\u003e_duration`.\n\n### Pin schema\n\nThe `pin:` field accepts any [ESPHome output pin schema](https://esphome.io/guides/configuration-types/#pin-schema). A few common shapes:\n\n```yaml\n# Direct GPIO, default (active-high):\npin: GPIO16\n\n# Direct GPIO with inversion (active-low relay module):\npin: { number: GPIO17, inverted: true }\n\n# 74HC595 shift register output:\npin: { sn74hc595: relay_chain, number: 1, inverted: false }\n\n# MCP23017 I²C expander output:\npin: { mcp23xxx: io_exp, number: 0, mode: OUTPUT, inverted: true }\n\n# PCF8574 I²C expander output:\npin: { pcf8574: io_exp, number: 0, mode: OUTPUT, inverted: true }\n```\n\n---\n\n## Hardware compatibility \u0026 wiring guide\n\n### Option A — ESP32 16-channel relay board with 74HC595 (recommended for ≥ 8 valves)\n\nA common, low-cost choice for irrigation projects:\n\n🛒 [ESP32 16-channel relay board on AliExpress](https://it.aliexpress.com/item/1005007479415609.html)\n\n**Specs**:\n- ESP32-WROOM-32E onboard\n- 12V DC input (powers ESP32 via onboard buck regulator + drives 12V relay coils)\n- 16 relays controlled by **two chained 74HC595 shift registers**\n- Pin mapping: `LATCH=GPIO12`, `CLOCK=GPIO13`, `DATA=GPIO14`, `OE=GPIO5`\n- Relays are **active HIGH** (set `inverted: false`)\n- Programming: separate UART header with BOOT/IO0 + EN buttons; needs a **3.3V** USB-to-TTL adapter (FT232RL or similar)\n\n**Full YAML**:\n\n```yaml\nesphome:\n  name: garden-sprinkler\n  friendly_name: \"Garden Sprinkler\"\n\nesp32:\n  board: esp32dev\n  framework:\n    type: esp-idf\n\nwifi:\n  ssid: !secret wifi_ssid\n  password: !secret wifi_password\n\napi:\nota:\n  - platform: esphome\nlogger:\n\nexternal_components:\n  - source: github://cristoforocervino/esp32-sprinkler-queue\n    components: [sprinkler_queue]\n\n# Two chained 74HC595s, controlled by the dedicated pins on the board\nsn74hc595:\n  - id: relay_chain\n    type: gpio\n    data_pin: GPIO14\n    clock_pin: GPIO13\n    latch_pin: GPIO12\n    oe_pin: GPIO5\n    sr_count: 2  # 2 chips chained = 16 outputs\n\nsprinkler_queue:\n  pause_between_valves:\n    initial_value: 5\n    mode: box\n  master_valve:\n    pin: { sn74hc595: relay_chain, number: 0, inverted: false }\n  valves:\n    - name: \"Valve 1\"\n      pin: { sn74hc595: relay_chain, number: 1, inverted: false }\n      initial_duration: 300\n    - name: \"Valve 2\"\n      pin: { sn74hc595: relay_chain, number: 2, inverted: false }\n      initial_duration: 300\n    # ... up to 15 zones (channel 0 is the master)\n```\n\n### Option B — Generic ESP32 with direct GPIO\n\nFor 4–8 zones using a standalone relay module wired directly to ESP32 GPIO pins. Most cheap relay modules (the blue 4/8-relay boards on Amazon/AliExpress) are **active-low** — energise the input by pulling it to ground.\n\n```yaml\nesphome:\n  name: small-sprinkler\n\nesp32:\n  board: esp32dev\n\nwifi:\n  ssid: !secret wifi_ssid\n  password: !secret wifi_password\n\napi:\nota:\n  - platform: esphome\n\nexternal_components:\n  - source: github://cristoforocervino/esp32-sprinkler-queue\n    components: [sprinkler_queue]\n\nsprinkler_queue:\n  pause_between_valves:\n    initial_value: 5\n  # master_valve: omitted -\u003e no master valve in this setup\n  valves:\n    - name: \"Front lawn\"\n      pin: { number: GPIO16, inverted: true }   # active-low relay\n      initial_duration: 600\n    - name: \"Back lawn\"\n      pin: { number: GPIO17, inverted: true }\n      initial_duration: 600\n    - name: \"Vegetables\"\n      pin: { number: GPIO18, inverted: true }\n      initial_duration: 900\n    - name: \"Roses\"\n      pin: { number: GPIO19, inverted: true }\n      initial_duration: 300\n```\n\n### Option C — I²C expander (MCP23017, PCF8574)\n\nFor projects that need more zones, share GPIO with other devices, or use a custom PCB with an I²C-driven relay bank.\n\n```yaml\nesphome:\n  name: villa-sprinkler\n\nesp32:\n  board: esp32dev\n\nwifi:\n  ssid: !secret wifi_ssid\n  password: !secret wifi_password\n\napi:\nota:\n  - platform: esphome\n\nexternal_components:\n  - source: github://cristoforocervino/esp32-sprinkler-queue\n    components: [sprinkler_queue]\n\ni2c:\n  sda: GPIO21\n  scl: GPIO22\n\nmcp23017:\n  - id: io_exp\n    address: 0x20\n\nsprinkler_queue:\n  pause_between_valves:\n    initial_value: 5\n  master_valve:\n    pin: { mcp23xxx: io_exp, number: 0, mode: OUTPUT, inverted: true }\n  valves:\n    - name: \"Zone 1\"\n      pin: { mcp23xxx: io_exp, number: 1, mode: OUTPUT, inverted: true }\n      initial_duration: 300\n    - name: \"Zone 2\"\n      pin: { mcp23xxx: io_exp, number: 2, mode: OUTPUT, inverted: true }\n      initial_duration: 300\n    # ... up to 15 zones on a single MCP23017 (one pin reserved for master)\n```\n\n---\n\n## FAQ / Troubleshooting\n\n### Why does HA show \"Opening…\" for queued valves?\n\nQueued valves are physically closed but logically requested. The component maps this to `valve.current_operation = OPENING` so HA's UI conveys the right meaning: \"this valve is going to open soon, just waiting its turn\".\n\n### How do I reset persistent valve durations?\n\nThe `number.*_duration` entities use `restore_value: true`, so values persist in the ESP32's NVS flash. To wipe:\n\n- From HA: change every duration to your desired value (writes the new value to flash)\n- Or: erase the device's NVS partition with `esphome run --erase` (full re-flash)\n\n### Active-low vs active-high relays — how do I tell?\n\nMost cheap relay modules with optoisolators (the blue 4/8/16-relay boards) are **active-low**: the relay is energised when the input is pulled to ground (logic 0). Use `inverted: true`.\n\nThe `sn74hc595` board linked above is **active-high**: the relay is energised when the shift register output is high. Use `inverted: false`.\n\nWhen in doubt, set the pin to a known state at boot and watch the relay LEDs at idle. If they're all on with no zones requested, your `inverted:` flag is wrong.\n\n### Can I have more than 16 zones?\n\nYes. Either chain more 74HC595 chips (up to 256 with `sr_count`), use an MCP23017 (16 pins per chip, multiple chips on one I²C bus), or mix sources. The component imposes no upper bound — only practical limits like ESPHome's RAM.\n\n### Why is there no scheduling/programming feature?\n\nScheduling is HA's job. Use the [Schedule](https://www.home-assistant.io/integrations/schedule/) helper, [Calendar](https://www.home-assistant.io/integrations/calendar/) integration, or plain automations. This component takes commands and serializes them — that's all.\n\n---\n\n## Examples\n\nThe [`examples/`](examples) directory has full, copyable YAML files for each hardware option:\n\n- [`sn74hc595-16ch-board.yaml`](examples/sn74hc595-16ch-board.yaml) — the AliExpress 16-channel board, 15 zones + master\n- [`direct-gpio-4-zones.yaml`](examples/direct-gpio-4-zones.yaml) — generic ESP32 dev board, 4 zones, no master\n- [`mcp23017-expander.yaml`](examples/mcp23017-expander.yaml) — ESP32 + MCP23017, 8 zones + master\n\n---\n\n## Contributing\n\nIssues and PRs welcome.\n\n- Reporting a hardware compatibility issue: open an issue with your YAML config and the relevant boot logs\n- Code style: follow ESPHome's conventions (clang-format for C++, ruff for Python)\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcristoforocervino%2Fesp32-sprinkler-queue","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcristoforocervino%2Fesp32-sprinkler-queue","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcristoforocervino%2Fesp32-sprinkler-queue/lists"}