Skip to content

Add IrrigationSystem accessory support to HomeKit integration - #177434

Open
donavanbecker wants to merge 26 commits into
home-assistant:devfrom
donavanbecker:donavanbecker-homekit-irrigation-system
Open

Add IrrigationSystem accessory support to HomeKit integration#177434
donavanbecker wants to merge 26 commits into
home-assistant:devfrom
donavanbecker:donavanbecker-homekit-irrigation-system

Conversation

@donavanbecker

Copy link
Copy Markdown

Proposed change

Add native support for grouping multiple valve entities into a single HomeKit Irrigation System accessory. Previously the HomeKit integration exposed each valve entity as a separate HomeKit Valve accessory.

With this change, users can optionally group two or more valve entities into one HomeKit IrrigationSystem accessory — presenting them as a sprinkler tile in Apple Home with individual zone controls. When valve entities are included in the bridge, a new "Valve configuration" options step appears (after the climate configuration step) where users can multi-select which valves to group as an irrigation system. Selecting fewer than two leaves the valves as individual accessories.

How it works:

  • The first selected valve entity becomes the primary and hosts the IrrigationSystem service; remaining selected valves are linked child Valve services (zones)
  • State changes in HA update each zone's Active/InUse characteristics and the overall system Active/InUse state
  • Activating/deactivating a zone from Apple Home calls valve.open_valve / valve.close_valve on the corresponding HA entity
  • Deactivating the top-level Irrigation System from Apple Home closes all grouped valves
  • Each zone supports SetDuration (stored locally; defaults 5 min) and RemainingDuration

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:
  • Link to documentation pull request:
  • Link to developer documentation pull request:
  • Link to frontend pull request:

Files changed:

File Change
const.py Added TYPE_IRRIGATION_SYSTEM, CONF_LINKED_IRRIGATION_VALVES, CONF_IRRIGATION_CONTROLLER, SERV_IRRIGATION_SYSTEM, CHAR_PROGRAM_MODE
type_switches.py Added IrrigationSystem accessory class (registered as "IrrigationSystem") with primary service + linked child Valve services, state sync, and HomeKit ↔ HA command routing
accessories.py Updated valve domain routing: detect TYPE_IRRIGATION_SYSTEM config → IrrigationSystem; CONF_IRRIGATION_CONTROLLER (linked valve) → skip; else → Valve
config_flow.py Added async_step_valves options flow step with multi-select EntitySelector; wired into existing step chain after climate step
strings.json Added "valves" step strings

Config flow mockup — new "Valve configuration" step:

┌─────────────────────────────────────────────────────────────────┐
│  Valve configuration                                            │
│                                                                 │
│  Select two or more valve entities to group them as a single    │
│  HomeKit Irrigation System accessory. Leave empty to expose     │
│  each valve individually.                                       │
│                                                                 │
│  Valves to group as Irrigation System                           │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  🔎 Search entities...                                  │   │
│  │  ✓  valve.front_lawn     Front Lawn                     │   │
│  │  ✓  valve.back_lawn      Back Lawn                      │   │
│  │  ✓  valve.garden_beds    Garden Beds                    │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│                                              [ Submit ]         │
└─────────────────────────────────────────────────────────────────┘

Result in Apple Home:

┌──────────────────────────┐
│  🌿  Irrigation          │  ← IrrigationSystem tile
│       3 zones            │
│                          │
│  ┌────┐ ┌────┐ ┌──────┐  │
│  │Zone│ │Zone│ │Zone  │  │  ← Individual valve zones
│  │ 1  │ │ 2  │ │  3   │  │
│  └────┘ └────┘ └──────┘  │
└──────────────────────────┘

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

Copilot AI review requested due to automatic review settings July 28, 2026 02:54
@home-assistant home-assistant Bot added cla-signed integration: homekit new-feature Top 100 Integration is ranked within the top 100 by usage Top 200 Integration is ranked within the top 200 by usage Top 50 Integration is ranked within the top 50 by usage labels Jul 28, 2026
@home-assistant

Copy link
Copy Markdown
Contributor

Hey there @bdraco, mind taking a look at this pull request as it has been labeled with an integration (homekit) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of homekit can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant mark-draft Mark the pull request as draft.
  • @home-assistant ready-for-review Remove the draft status from the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign homekit Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant update-branch Update the pull request branch with the base branch.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) on the pull request.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds HomeKit Irrigation System accessories that group multiple Home Assistant valves.

Changes:

  • Adds grouped valve configuration.
  • Implements irrigation services and command routing.
  • Updates accessory selection and translations.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
type_switches.py Implements irrigation accessories.
config_flow.py Adds valve grouping options.
accessories.py Routes grouped valves.
const.py Adds irrigation constants.
strings.json Adds option-step text.
Comments suppressed due to low confidence (1)

homeassistant/components/homekit/type_switches.py:756

  • Implement the duration lifecycle before exposing SetDuration/RemainingDuration. The selected duration is only stored in an otherwise-unused dictionary entry: opening a zone never schedules its close or updates either remaining-duration characteristic, so a five-minute HomeKit run can stay open indefinitely while reporting zero seconds remaining. Start/cancel a per-zone timer and update the zone/system countdowns, or omit these characteristics until real duration state is available.
    def _set_valve_duration(self, entity_id: str, value: int) -> None:
        """Store the HomeKit-requested run duration for a valve."""
        chars = self._valve_chars.get(entity_id)
        if chars:
            chars["duration"] = max(int(value), 0)

Comment thread homeassistant/components/homekit/type_switches.py
Comment thread homeassistant/components/homekit/config_flow.py Outdated
Comment thread homeassistant/components/homekit/type_switches.py
Copilot AI review requested due to automatic review settings July 28, 2026 03:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 28, 2026 03:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 28, 2026 03:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 28, 2026 03:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Comment thread homeassistant/components/homekit/config_flow.py Outdated
Comment thread homeassistant/components/homekit/type_switches.py Outdated
Comment thread homeassistant/components/homekit/type_switches.py
Comment thread tests/components/homekit/test_type_switches.py
Copilot AI review requested due to automatic review settings July 28, 2026 03:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (3)

homeassistant/components/homekit/type_switches.py:754

  • Handle primary-valve unavailable transitions before the base callback filters them out. HomeAccessory.async_update_state_callback returns for unavailable/unknown states, so this override never sets the primary zone or system StatusFault and can leave prior Active/InUse values latched; override event handling for those states or subscribe the primary through the same multi-valve path.
    @callback
    @override
    def async_update_state_callback(self, new_state: State | None) -> None:
        """Handle primary valve state changes, including unavailable/unknown."""

homeassistant/components/homekit/type_switches.py:775

  • Preserve locally selected durations across Home Assistant state updates. _duration_from_state returns 300 when a valve has no duration attribute, so the next open/close event overwrites any value saved by _set_valve_duration; return a sentinel for no device duration and only replace the cached local value when an attribute is actually present.
        if chars is None:
            return
        has_fault = state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)

tests/components/homekit/test_type_switches.py:1261

  • Assert the fault characteristic this test is meant to cover. The test only checks Active/InUse, which would also pass if StatusFault remained NO_FAULT, so the unavailable-zone behavior can regress undetected.

    front_chars = acc._valve_chars["valve.front_lawn"]
    assert front_chars["duration"] == 900

Comment thread homeassistant/components/homekit/type_switches.py Outdated
Comment thread homeassistant/components/homekit/type_switches.py
Comment thread homeassistant/components/homekit/config_flow.py
Copilot AI review requested due to automatic review settings July 28, 2026 03:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (2)

homeassistant/components/homekit/type_switches.py:695

  • Implement the SetDuration countdown before exposing this characteristic. HomeKit's duration contract requires an active valve to transition back to inactive when the countdown expires, but this implementation only stores the value and leaves RemainingDuration static, so a five-minute zone can remain open indefinitely; track a deadline/timer per zone, close it on expiry, and derive remaining time from that deadline.
            char_set_duration = serv_valve.configure_char(
                CHAR_SET_DURATION,
                value=initial_duration,
                properties=IRRIGATION_DURATION_PROPERTIES,
                setter_callback=lambda v, eid=entity_id: self._set_valve_duration(

homeassistant/components/homekit/type_switches.py:586

  • Report ProgramMode as “No Program Scheduled.” HAP defines that value as 0; value 1 means “Program Scheduled,” so the current constant advertises a schedule that this accessory does not implement.
HK_PROGRAM_MODE_NO_SCHEDULE_ACTIVE = 1

Comment thread homeassistant/components/homekit/type_switches.py
Copilot AI review requested due to automatic review settings July 28, 2026 03:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (3)

homeassistant/components/homekit/type_switches.py:835

  • Enforce the selected duration when opening a zone. SetDuration is only stored/displayed here: the open_valve call is never followed by close_valve, and without device duration attributes RemainingDuration stays at the original value, so a five-minute HomeKit run can remain open indefinitely. Track a per-zone deadline, schedule/cancel its close callback, and derive remaining time from that deadline (or do not expose these duration characteristics without a backing implementation).
    def _set_system_active(self, value: int) -> None:
        """Close all valves when HomeKit deactivates the irrigation system."""
        if not value:
            for entity_id in self._valve_entity_ids:
                self.hass.async_create_task(

homeassistant/components/homekit/type_switches.py:757

  • Keep accessory availability independent of the arbitrary primary zone. Before this override runs, HomeAccessory.async_update_event_state_callback() sets available solely from self.entity_id; when the first selected valve becomes unavailable, the entire multi-zone accessory becomes unavailable even though the other linked zones are usable and this code advertises per-zone faults. Override availability for this grouped accessory to remain available while any zone exists, relying on the zone/system StatusFault characteristics for individual failures.
                self._update_system_state()
            return
        self._sync_valve_chars(new_state.entity_id, new_state)
        self._update_system_state()

homeassistant/components/homekit/type_switches.py:747

  • Mark a removed linked valve as faulted and inactive instead of dropping the event. A state-removal event has new_state=None; returning here preserves the zone's previous Active, InUse, and remaining-duration values indefinitely and can leave the whole irrigation system shown as running. Add shared missing-state handling that clears the zone and sets its/system StatusFault (also usable when a linked state is absent during initialization).
    def _async_linked_valve_state_changed(
        self, event: Event[EventStateChangedData]
    ) -> None:

Comment thread homeassistant/components/homekit/type_switches.py
@donavanbecker
donavanbecker force-pushed the donavanbecker-homekit-irrigation-system branch 3 times, most recently from d70f308 to 8aeefba Compare July 31, 2026 02:47
donavanbecker and others added 26 commits July 30, 2026 23:38
Add support for grouping multiple valve entities into a single HomeKit
IrrigationSystem accessory instead of individual Valve accessories.

Changes:
- Add IrrigationSystem accessory class in type_switches.py that creates
  a primary IrrigationSystem service with linked child Valve services,
  one per grouped valve entity. Supports Active, InUse, ProgramMode,
  RemainingDuration on the system level and Active, InUse, ValveType,
  SetDuration, RemainingDuration per zone. Subscribes to state changes
  for both the primary and linked valve entities.
- Add async_step_valves options flow step that lets users select two or
  more valve entities to group as one Irrigation System. Fits into the
  existing flow after the climate step. Stores config via CONF_TYPE and
  CONF_LINKED_IRRIGATION_VALVES on the primary valve entity config entry
  and CONF_IRRIGATION_CONTROLLER on each linked valve.
- Update accessories.py to route valve entities to the IrrigationSystem
  or Valve type based on entity_config, and skip linked valve entities
  that are already managed by an irrigation system group.
- Add new constants: SERV_IRRIGATION_SYSTEM, CHAR_PROGRAM_MODE,
  TYPE_IRRIGATION_SYSTEM, CONF_LINKED_IRRIGATION_VALVES,
  CONF_IRRIGATION_CONTROLLER.
- Add strings.json entry for the new valves options step.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Filter grouped valve defaults to included valves in options flow
- Resync commanded irrigation valves when service calls fail
- Add irrigation accessory mapping and config-flow regression tests
- Add irrigation switch behavior tests for durations and failure resync

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use type: ignore[untyped-decorator] on the IrrigationSystem run
method decorator so mypy suppresses the correct error code.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace temporary list used only for iteration with an in-place tuple
(and tuple default) to satisfy pylint consider-using-tuple.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace list concatenation with iterable unpacking to satisfy Ruff
RUF005 in IrrigationSystem initialization.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Run ruff-format and ruff-check hooks and commit the resulting
auto-fixes so CI working tree remains clean.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Clear stale irrigation entity metadata globally before regrouping
- Preserve HomeKit-selected duration when valve has no duration attrs
- Handle primary unavailable/unknown updates in irrigation accessory
- Extend irrigation tests for status fault and duration persistence

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Run prettier on homeassistant/components/homekit/strings.json to match
CI hook output and keep prek working tree clean.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…and invalid attrs

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Deduplicate grouped valves with dict.fromkeys before length check to
  prevent self-referential controllers and duplicate Valve services
- Use dt_util.as_utc() on parsed end_time to normalize naive/aware
  datetimes before subtracting utcnow(), avoiding TypeError

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Track runtime updates for device-provided remaining/end_time values
  without scheduling local auto-close service calls
- Reuse a normalized end_time helper for remaining calculation
- Add test coverage ensuring end_time-based remaining counts down
  without extra state events

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@donavanbecker
donavanbecker force-pushed the donavanbecker-homekit-irrigation-system branch from 8aeefba to 89dccb6 Compare July 31, 2026 04:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed integration: homekit new-feature Quality Scale: No score Top 50 Integration is ranked within the top 50 by usage Top 100 Integration is ranked within the top 100 by usage Top 200 Integration is ranked within the top 200 by usage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants