Skip to content

fix: rewrite ZoneOn handler to use working start_watering path#50

Merged
simons-plugins merged 3 commits into
mainfrom
fix/zoneon-action-handler
Apr 15, 2026
Merged

fix: rewrite ZoneOn handler to use working start_watering path#50
simons-plugins merged 3 commits into
mainfrom
fix/zoneon-action-handler

Conversation

@simons-plugins
Copy link
Copy Markdown
Owner

Summary

  • The standard Indigo Zone On sprinkler action has been broken since it was written — it crashed with KeyError 'id' on plugin.py:1567 because it accessed zone_dict["id"] / zone_dict["maxRuntime"], neither of which exist in Netro device zone dicts (those use "ith" for zone number).
  • Nobody noticed because normal control is via schedules or the custom startZoneWithDelay action, which builds the request shape correctly. The bug surfaced while building a remote irrigation control page that calls indigo.sprinkler.setActiveZone via IWS — the Indigo SDK translates that to ZoneOn, which crashed the plugin.
  • Rewrite the ZoneOn branch to mirror the working startZoneWithDelay path: validate zone index against dev.zoneMaxDurations, compute duration from the device's configured max (seconds → minutes, clamped to self.maxZoneRunTime), use _get_device_auth(dev) for key + api_version like AllZonesOff already does, and call api_client.start_watering(key, [{"id": zoneIndex, "duration": N}], api_version=api_version).
  • Drop the now-unused ZONE_START_ENDPOINT import from plugin.py. The constant itself stays defined in constants.py for potential future use.

Test plan

  • pytest tests/ → 429 passed, no regressions
  • pylint plugin.py → rating unchanged at 9.41/10, no new warnings on changed lines
  • Manual smoke test in Indigo: fire indigo.sprinkler.setActiveZone {index:N} against a real Netro controller via REST, confirm zone starts and stops correctly
  • Confirm scheduled watering (the pre-existing working path) still fires normally after install

🤖 Generated with Claude Code

…path

The standard Indigo "Zone On" sprinkler action has been broken since it
was written — it crashed with KeyError 'id' on plugin.py:1567 because it
accessed zone_dict["id"] and zone_dict["maxRuntime"], neither of which
exist in the Netro device zone dicts (those use "ith" for zone number).
Nobody noticed because normal control is via schedules or the custom
startZoneWithDelay action, which builds the request shape correctly.

Rewrite the ZoneOn branch to mirror startZoneWithDelay:
- Validate action.zoneIndex against dev.zoneMaxDurations
- Compute duration from dev.zoneMaxDurations[zoneIndex-1] (seconds),
  clamped to self.maxZoneRunTime, converted to Netro API minutes
- Use _get_device_auth(dev) for key + api_version like AllZonesOff does
- Call api_client.start_watering(key, [{"id": zoneIndex, "duration": N}],
  api_version=api_version) instead of the broken make_request path
- Resolve zone name from dev.zoneNames for user-facing logs

Also drop the now-unused ZONE_START_ENDPOINT import. The constant is
still defined for potential future use but no longer referenced here.

This unblocks remote clients that send indigo.sprinkler.setActiveZone
via IWS — the Indigo SDK translates that to a ZoneOn action, which
used to crash and now works end-to-end.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

Warning

Rate limit exceeded

@simons-plugins has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 16 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 41 minutes and 16 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66960a2c-bc7d-4178-9fa9-097a457848dd

📥 Commits

Reviewing files that changed from the base of the PR and between 04d9568 and cfc49d9.

📒 Files selected for processing (4)
  • Netro Sprinklers.indigoPlugin/Contents/Info.plist
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/api_client.py
  • Netro Sprinklers.indigoPlugin/Contents/Server Plugin/plugin.py
  • tests/test_api_client.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/zoneon-action-handler

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

simons-plugins and others added 2 commits April 15, 2026 22:41
The v1 water endpoint accepts zones as a list of per-zone dicts
(`[{"id": N, "duration": M}]`) so each zone can have its own duration.
The v2 water.json endpoint instead requires a flat array of zone
integer indices plus a single top-level `duration` that applies to
every zone in the list — sending the v1 shape to v2 returns
`HTTP 400: zones[0] is of invalid type ActionController::Parameters,
should be Integer`.

start_watering passed `zones` through unchanged regardless of version,
so both the ZoneOn action handler (fixed in 2026.4.2) and the
pre-existing startZoneWithDelay custom action were broken against v2
controllers. The custom action had never been tested end-to-end on v2;
it only surfaced now because the irrigation page started exercising
ZoneOn via IWS.

Branch inside start_watering on api_version:
- v1: send zones verbatim (preserves per-zone duration support)
- v2: collect `id` ints into a flat list, emit one `duration` from
  the first zone; warn if the caller mixed different durations since
  v2 cannot express that

Add two unit tests pinning both shapes so this can't regress silently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR review surfaced five behaviours where the ZoneOn rewrite and v2
payload reshape could silently mask real problems. Fix all five in
one go so the action path has parity with the other sibling handlers
(AllZonesOff, startZoneWithDelay) for error surfacing.

plugin.py ZoneOn branch:
- Refuse to start a zone whose configured maxRuntime is 0. Previously
  the `max(1, round(...))` floor quietly turned a disabled zone into
  a 1-minute run with no feedback; now we log an error and fire
  `startZoneFailed`.
- Check the Netro response status and fire `startZoneFailed` on
  anything other than "OK", mirroring startZoneWithDelay. A throttle
  or auth rejection was being treated as success and `activeZone`
  was being set regardless.
- Add a final `except Exception` that logs with `logger.exception`
  and fires the failure trigger. Credential lookup, stale device
  props, or a ValueError from the api_client validation block used
  to propagate out as bare tracebacks with no user-visible failure
  trigger.
- Narrow the zoneNames fallback from `(IndexError, AttributeError)`
  to `IndexError` only — a device unexpectedly missing `zoneNames`
  is a structural problem, not a cosmetic fallback case, and should
  bubble up to the final exception handler.

api_client.start_watering:
- Raise ValueError on empty zones instead of crashing with
  IndexError inside the v2 reshape.
- Validate zones shape once up front via a single loop that coerces
  id/duration to int. Missing keys or non-numeric values raise a
  ValueError with the offending index and payload, instead of bare
  KeyError/ValueError from deeper code.
- Raise ValueError instead of logging a warning when v2 is called
  with per-zone durations that differ. Netro v2 cannot express that
  intent on the wire, so silently dropping all but the first zone's
  duration is a data-loss bug; callers must split into separate
  calls or collapse durations themselves.

Tests: five new cases pinning empty-zones, missing-id, missing-
duration, v2-mixed-durations-raises, and v2-uniform-durations-
succeeds. 436 tests pass; pylint rating unchanged (no new warnings
on touched lines, the drop from 9.88 → 9.51 on api_client.py is
pure statement-count denominator).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@simons-plugins simons-plugins merged commit 5c4f050 into main Apr 15, 2026
3 checks passed
@simons-plugins simons-plugins deleted the fix/zoneon-action-handler branch April 15, 2026 21:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant