Repository: home-assistant/core
Pull request #144075: Add flow detection to Rachio hose timer
Author: login: brg468
Labels: name: cla-signed
name: new-feature
name: integration: rachio
name: Quality Scale: No score
Created: 2025-05-01T21:04:53Z | Merged: 2025-05-14T13:01:01Z

<!--
  You are amazing! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template! (unless instructed).
-->


## Proposed change
<!--
  Describe the big picture of your changes here to communicate to the
  maintainers why we should accept this pull request. If it fixes a bug
  or resolves a feature request, be sure to link to that issue in the
  additional information section.
-->
Rewrite binary sensor to use entity descriptions and add flow detected binary sensor to the hose timer.

Using a problem sensor for flow not detected. The reason I didn't choose moisture sensor was I didn't want the state of the sensor to change every time the valve turns on if flow is detected (normal condition), which would make it harder to use as a trigger IMO. Open to suggestions on other ways to handle it though. 

## Type of change
<!--
  What type of change does your PR introduce to Home Assistant?
  NOTE: Please, check only 1! box!
  If your PR requires multiple boxes to be checked, you'll most likely need to
  split it into multiple PRs. This makes things easier and faster to code review.
-->

- [ ] Dependency upgrade
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New integration (thank you!)
- [x] 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)
- [x] Code quality improvements to existing code or addition of tests

## Additional information
<!--
  Details are important, and help maintainers processing your PR.
  Please be sure to fill out additional details, if applicable.
-->

- 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: 

## Checklist
<!--
  Put an `x` in the boxes that apply. You can also fill these out after
  creating the PR. If you're unsure about any of them, don't hesitate to ask.
  We're here to help! This is simply a reminder of what we are going to look
  for before merging your code.
-->

- [x] The code change is tested and works locally.
- [x] Local tests pass. **Your PR cannot be merged unless tests pass**
- [x] There is no commented out code in this PR.
- [x] I have followed the [development checklist][dev-checklist]
- [x] I have followed the [perfect PR recommendations][perfect-pr]
- [x] The code has been formatted using Ruff (`ruff format homeassistant tests`)
- [ ] Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]

If the code communicates with devices, web services, or third-party tools:

- [ ] The [manifest file][manifest-docs] 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 link to the changelog, or at minimum a diff between library versions is added to the PR description.

<!--
  This project is very active and we have a high turnover of pull requests.

  Unfortunately, the number of incoming pull requests is higher than what our
  reviewers can review and merge so there is a long backlog of pull requests
  waiting for review. You can help here!
  
  By reviewing another pull request, you will help raise the code quality of
  that pull request and the final review will be faster. This way the general
  pace of pull request reviews will go up and your wait time will go down.
  
  When picking a pull request to review, try to choose one that hasn't yet
  been reviewed.

  Thanks for helping out!
-->

To help with the load of incoming pull requests:

- [ ] I have reviewed two other [open pull requests][prs] in this repository.

[prs]: https://github.com/home-assistant/core/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+-label%3Awaiting-for-upstream+sort%3Acreated-desc+review%3Anone+-status%3Afailure

<!--
  Thank you for contributing <3

  Below, some useful links you could explore:
-->
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
[manifest-docs]: https://developers.home-assistant.io/docs/creating_integration_manifest/
[quality-scale]: https://developers.home-assistant.io/docs/integration_quality_scale_index/
[docs-repository]: https://github.com/home-assistant/home-assistant.io
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr



Diff:
diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py
index 0c502a98c9a764..be379a23cabb35 100644
--- a/homeassistant/components/rachio/binary_sensor.py
+++ b/homeassistant/components/rachio/binary_sensor.py
@@ -1,28 +1,31 @@
 """Integration with the Rachio Iro sprinkler system controller."""
 
-from abc import abstractmethod
+from collections.abc import Callable
+from dataclasses import dataclass
 import logging
 from typing import Any
 
 from homeassistant.components.binary_sensor import (
     BinarySensorDeviceClass,
     BinarySensorEntity,
+    BinarySensorEntityDescription,
 )
 from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import EntityCategory
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.entity import Entity
 from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
 
 from .const import (
-    DOMAIN,
-    KEY_BATTERY_STATUS,
+    DOMAIN as DOMAIN_RACHIO,
+    KEY_BATTERY,
+    KEY_DETECT_FLOW,
     KEY_DEVICE_ID,
-    KEY_LOW,
+    KEY_FLOW,
+    KEY_ONLINE,
+    KEY_RAIN_SENSOR,
     KEY_RAIN_SENSOR_TRIPPED,
-    KEY_REPLACE,
-    KEY_REPORTED_STATE,
-    KEY_STATE,
     KEY_STATUS,
     KEY_SUBTYPE,
     SIGNAL_RACHIO_CONTROLLER_UPDATE,
@@ -30,7 +33,7 @@
     STATUS_ONLINE,
 )
 from .coordinator import RachioUpdateCoordinator
-from .device import RachioPerson
+from .device import RachioIro, RachioPerson
 from .entity import RachioDevice, RachioHoseTimerEntity
 from .webhooks import (
     SUBTYPE_COLD_REBOOT,
@@ -43,6 +46,67 @@
 _LOGGER = logging.getLogger(__name__)
 
 
+@dataclass(frozen=True, kw_only=True)
+class RachioControllerBinarySensorDescription(BinarySensorEntityDescription):
+    """Describe a Rachio controller binary sensor."""
+
+    update_received: Callable[[str], bool | None]
+    is_on: Callable[[RachioIro], bool]
+    signal_string: str
+
+
+CONTROLLER_BINARY_SENSOR_TYPES: tuple[RachioControllerBinarySensorDescription, ...] = (
+    RachioControllerBinarySensorDescription(
+        key=KEY_ONLINE,
+        device_class=BinarySensorDeviceClass.CONNECTIVITY,
+        signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE,
+        update_received={
+            SUBTYPE_ONLINE: True,
+            SUBTYPE_COLD_REBOOT: True,
+            SUBTYPE_OFFLINE: False,
+        }.get,
+    ),
+    RachioControllerBinarySensorDescription(
+        key=KEY_RAIN_SENSOR,
+        translation_key="rain",
+        device_class=BinarySensorDeviceClass.MOISTURE,
+        signal_string=SIGNAL_RACHIO_RAIN_SENSOR_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_RAIN_SENSOR_TRIPPED],
+        update_received={
+            SUBTYPE_RAIN_SENSOR_DETECTION_ON: True,
+            SUBTYPE_RAIN_SENSOR_DETECTION_OFF: False,
+        }.get,
+    ),
+)
+
+
+@dataclass(frozen=True, kw_only=True)
+class RachioHoseTimerBinarySensorDescription(BinarySensorEntityDescription):
+    """Describe a Rachio hose timer binary sensor."""
+
+    value_fn: Callable[[RachioHoseTimerEntity], bool]
+    exists_fn: Callable[[dict[str, Any]], bool] = lambda _: True
+
+
+HOSE_TIMER_BINARY_SENSOR_TYPES: tuple[RachioHoseTimerBinarySensorDescription, ...] = (
+    RachioHoseTimerBinarySensorDescription(
+        key=KEY_BATTERY,
+        entity_category=EntityCategory.DIAGNOSTIC,
+        device_class=BinarySensorDeviceClass.BATTERY,
+        value_fn=lambda device: device.battery,
+    ),
+    RachioHoseTimerBinarySensorDescription(
+        key=KEY_FLOW,
+        device_class=BinarySensorDeviceClass.PROBLEM,
+        entity_category=EntityCategory.DIAGNOSTIC,
+        translation_key="flow",
+        value_fn=lambda device: device.no_flow_detected,
+        exists_fn=lambda valve: valve[KEY_DETECT_FLOW],
+    ),
+)
+
+
 async def async_setup_entry(
     hass: HomeAssistant,
     config_entry: ConfigEntry,
@@ -55,23 +119,38 @@ async def async_setup_entry(
 
 def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Entity]:
     entities: list[Entity] = []
-    person: RachioPerson = hass.data[DOMAIN][config_entry.entry_id]
-    for controller in person.controllers:
-        entities.append(RachioControllerOnlineBinarySensor(controller))
-        entities.append(RachioRainSensor(controller))
+    person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id]
+    entities.extend(
+        RachioControllerBinarySensor(controller, description)
+        for controller in person.controllers
+        for description in CONTROLLER_BINARY_SENSOR_TYPES
+    )
     entities.extend(
-        RachioHoseTimerBattery(valve, base_station.status_coordinator)
+        RachioHoseTimerBinarySensor(valve, base_station.status_coordinator, description)
         for base_station in person.base_stations
         for valve in base_station.status_coordinator.data.values()
+        for description in HOSE_TIMER_BINARY_SENSOR_TYPES
+        if description.exists_fn(valve)
     )
     return entities
 
 
 class RachioControllerBinarySensor(RachioDevice, BinarySensorEntity):
-    """Represent a binary sensor that reflects a Rachio state."""
+    """Represent a binary sensor that reflects a Rachio controller state."""
 
+    entity_description: RachioControllerBinarySensorDescription
     _attr_has_entity_name = True
 
+    def __init__(
+        self,
+        controller: RachioIro,
+        description: RachioControllerBinarySensorDescription,
+    ) -> None:
+        """Initialize a controller binary sensor."""
+        super().__init__(controller)
+        self.entity_description = description
+        self._attr_unique_id = f"{controller.controller_id}-{description.key}"
+
     @callback
     def _async_handle_any_update(self, *args, **kwargs) -> None:
         """Determine whether an update event applies to this device."""
@@ -82,97 +161,49 @@ def _async_handle_any_update(self, *args, **kwargs) -> None:
         # For this device
         self._async_handle_update(args, kwargs)
 
-    @abstractmethod
-    def _async_handle_update(self, *args, **kwargs) -> None:
-        """Handle an update to the state of this sensor."""
-
-
-class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
-    """Represent a binary sensor that reflects if the controller is online."""
-
-    _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
-
-    @property
-    def unique_id(self) -> str:
-        """Return a unique id for this entity."""
-        return f"{self._controller.controller_id}-online"
-
     @callback
     def _async_handle_update(self, *args, **kwargs) -> None:
         """Handle an update to the state of this sensor."""
-        if args[0][0][KEY_SUBTYPE] in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT):
-            self._attr_is_on = True
-        elif args[0][0][KEY_SUBTYPE] == SUBTYPE_OFFLINE:
-            self._attr_is_on = False
-
-        self.async_write_ha_state()
-
-    async def async_added_to_hass(self) -> None:
-        """Subscribe to updates."""
-        self._attr_is_on = self._controller.init_data[KEY_STATUS] == STATUS_ONLINE
-
-        self.async_on_remove(
-            async_dispatcher_connect(
-                self.hass,
-                SIGNAL_RACHIO_CONTROLLER_UPDATE,
-                self._async_handle_any_update,
+        if (
+            updated_state := self.entity_description.update_received(
+                args[0][0][KEY_SUBTYPE]
             )
-        )
-
-
-class RachioRainSensor(RachioControllerBinarySensor):
-    """Represent a binary sensor that reflects the status of the rain sensor."""
-
-    _attr_device_class = BinarySensorDeviceClass.MOISTURE
-    _attr_translation_key = "rain"
-
-    @property
-    def unique_id(self) -> str:
-        """Return a unique id for this entity."""
-        return f"{self._controller.controller_id}-rain_sensor"
-
-    @callback
-    def _async_handle_update(self, *args, **kwargs) -> None:
-        """Handle an update to the state of this sensor."""
-        if args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_SENSOR_DETECTION_ON:
-            self._attr_is_on = True
-        elif args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_SENSOR_DETECTION_OFF:
-            self._attr_is_on = False
+        ) is not None:
+            self._attr_is_on = updated_state
 
         self.async_write_ha_state()
 
     async def async_added_to_hass(self) -> None:
         """Subscribe to updates."""
-        self._attr_is_on = self._controller.init_data[KEY_RAIN_SENSOR_TRIPPED]
+        self._attr_is_on = self.entity_description.is_on(self._controller)
 
         self.async_on_remove(
             async_dispatcher_connect(
                 self.hass,
-                SIGNAL_RACHIO_RAIN_SENSOR_UPDATE,
+                self.entity_description.signal_string,
                 self._async_handle_any_update,
             )
         )
 
 
-class RachioHoseTimerBattery(RachioHoseTimerEntity, BinarySensorEntity):
-    """Represents a battery sensor for a smart hose timer."""
+class RachioHoseTimerBinarySensor(RachioHoseTimerEntity, BinarySensorEntity):
+    """Represents a binary sensor for a smart hose timer."""
 
-    _attr_device_class = BinarySensorDeviceClass.BATTERY
+    entity_description: RachioHoseTimerBinarySensorDescription
 
     def __init__(
-        self, data: dict[str, Any], coordinator: RachioUpdateCoordinator
+        self,
+        data: dict[str, Any],
+        coordinator: RachioUpdateCoordinator,
+        description: RachioHoseTimerBinarySensorDescription,
     ) -> None:
-        """Initialize a smart hose timer battery sensor."""
+        """Initialize a smart hose timer binary sensor."""
         super().__init__(data, coordinator)
-        self._attr_unique_id = f"{self.id}-battery"
+        self.entity_description = description
+        self._attr_unique_id = f"{self.id}-{description.key}"
+        self._update_attr()
 
     @callback
     def _update_attr(self) -> None:
         """Handle updated coordinator data."""
-        data = self.coordinator.data[self.id]
-
-        self._static_attrs = data[KEY_STATE][KEY_REPORTED_STATE]
-        self._attr_is_on = self._static_attrs[KEY_BATTERY_STATUS] in [
-            KEY_LOW,
-            KEY_REPLACE,
-        ]
+        self._attr_is_on = self.entity_description.value_fn(self)
diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py
index ad670fc3608ce1..08a09f309f64de 100644
--- a/homeassistant/components/rachio/const.py
+++ b/homeassistant/components/rachio/const.py
@@ -25,10 +25,12 @@
 KEY_NAME = "name"
 KEY_MODEL = "model"
 KEY_ON = "on"
+KEY_ONLINE = "online"
 KEY_DURATION = "totalDuration"
 KEY_DURATION_MINUTES = "duration"
 KEY_RAIN_DELAY = "rainDelayExpirationDate"
 KEY_RAIN_DELAY_END = "endTime"
+KEY_RAIN_SENSOR = "rain_sensor"
 KEY_RAIN_SENSOR_TRIPPED = "rainSensorTripped"
 KEY_STATUS = "status"
 KEY_SUBTYPE = "subType"
@@ -57,6 +59,8 @@
 KEY_CONNECTED = "connected"
 KEY_CURRENT_STATUS = "lastWateringAction"
 KEY_DETECT_FLOW = "detectFlow"
+KEY_BATTERY = "battery"
+KEY_FLOW = "flow"
 KEY_BATTERY_STATUS = "batteryStatus"
 KEY_LOW = "LOW"
 KEY_REPLACE = "REPLACE"
diff --git a/homeassistant/components/rachio/entity.py b/homeassistant/components/rachio/entity.py
index 056abe9145b335..10657a1f0e98a8 100644
--- a/homeassistant/components/rachio/entity.py
+++ b/homeassistant/components/rachio/entity.py
@@ -12,9 +12,14 @@
 from .const import (
     DEFAULT_NAME,
     DOMAIN,
+    KEY_BATTERY_STATUS,
     KEY_CONNECTED,
+    KEY_CURRENT_STATUS,
+    KEY_FLOW_DETECTED,
     KEY_ID,
+    KEY_LOW,
     KEY_NAME,
+    KEY_REPLACE,
     KEY_REPORTED_STATE,
     KEY_STATE,
 )
@@ -70,17 +75,29 @@ def __init__(
             manufacturer=DEFAULT_NAME,
             configuration_url="https://app.rach.io",
         )
-        self._update_attr()
+
+    @property
+    def reported_state(self) -> dict[str, Any]:
+        """Return the reported state."""
+        return self.coordinator.data[self.id][KEY_STATE][KEY_REPORTED_STATE]
 
     @property
     def available(self) -> bool:
         """Return if the entity is available."""
-        return (
-            super().available
-            and self.coordinator.data[self.id][KEY_STATE][KEY_REPORTED_STATE][
-                KEY_CONNECTED
-            ]
-        )
+        return super().available and self.reported_state[KEY_CONNECTED]
+
+    @property
+    def battery(self) -> bool:
+        """Return the battery status."""
+        return self.reported_state[KEY_BATTERY_STATUS] in [KEY_LOW, KEY_REPLACE]
+
+    @property
+    def no_flow_detected(self) -> bool:
+        """Return true if valve is on and flow is not detected."""
+        if status := self.reported_state.get(KEY_CURRENT_STATUS):
+            # Since this is a problem indicator we need the opposite of the API state
+            return not status.get(KEY_FLOW_DETECTED, True)
+        return False
 
     @abstractmethod
     def _update_attr(self) -> None:
diff --git a/homeassistant/components/rachio/strings.json b/homeassistant/components/rachio/strings.json
index d51a1d5f92078b..ea3c8911463d38 100644
--- a/homeassistant/components/rachio/strings.json
+++ b/homeassistant/components/rachio/strings.json
@@ -31,6 +31,9 @@
     "binary_sensor": {
       "rain": {
         "name": "Rain"
+      },
+      "flow": {
+        "name": "Flow"
       }
     },
     "calendar": {
diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py
index 0edccf02320bf6..e2c5d66b967175 100644
--- a/homeassistant/components/rachio/switch.py
+++ b/homeassistant/components/rachio/switch.py
@@ -37,9 +37,7 @@
     KEY_ON,
     KEY_RAIN_DELAY,
     KEY_RAIN_DELAY_END,
-    KEY_REPORTED_STATE,
     KEY_SCHEDULE_ID,
-    KEY_STATE,
     KEY_SUBTYPE,
     KEY_SUMMARY,
     KEY_TYPE,
@@ -548,6 +546,7 @@ def __init__(self, person, base, data, coordinator) -> None:
         self._person = person
         self._base = base
         self._attr_unique_id = f"{self.id}-valve"
+        self._update_attr()
 
     def turn_on(self, **kwargs: Any) -> None:
         """Turn on this valve."""
@@ -575,7 +574,5 @@ def turn_off(self, **kwargs: Any) -> None:
     @callback
     def _update_attr(self) -> None:
         """Handle updated coordinator data."""
-        data = self.coordinator.data[self.id]
-
-        self._static_attrs = data[KEY_STATE][KEY_REPORTED_STATE]
+        self._static_attrs = self.reported_state
         self._attr_is_on = KEY_CURRENT_STATUS in self._static_attrs


Reviews:
user: login: bdraco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T22:49:18Z
user: login: brg468
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:15:10Z
user: login: joostlek
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-09T16:59:35Z
user: login: brg468
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T11:37:31Z
user: login: brg468
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T17:37:02Z
user: login: joostlek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T17:41:26Z
user: login: brg468
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T17:44:33Z
user: login: brg468
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T15:56:42Z
user: login: joostlek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:59:13Z
user: login: brg468
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T16:03:26Z
user: login: joostlek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T16:06:13Z
user: login: joostlek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T16:07:20Z
user: login: joostlek
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-13T16:11:44Z
user: login: bdraco
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-14T04:14:42Z

Review comments:
diff_hunk: @@ -82,97 +148,64 @@ def _async_handle_any_update(self, *args, **kwargs) -> None:
         # For this device
         self._async_handle_update(args, kwargs)
 
-    @abstractmethod
-    def _async_handle_update(self, *args, **kwargs) -> None:
-        """Handle an update to the state of this sensor."""
-
-
-class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
-    """Represent a binary sensor that reflects if the controller is online."""
-
-    _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
-
-    @property
-    def unique_id(self) -> str:
-        """Return a unique id for this entity."""
-        return f"{self._controller.controller_id}-online"
-
     @callback
     def _async_handle_update(self, *args, **kwargs) -> None:
         """Handle an update to the state of this sensor."""
-        if args[0][0][KEY_SUBTYPE] in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT):
-            self._attr_is_on = True
-        elif args[0][0][KEY_SUBTYPE] == SUBTYPE_OFFLINE:
-            self._attr_is_on = False
+        if self.entity_description.key == KEY_ONLINE:
+            if args[0][0][KEY_SUBTYPE] in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT):
+                self._attr_is_on = True
+            elif args[0][0][KEY_SUBTYPE] == SUBTYPE_OFFLINE:
+                self._attr_is_on = False
+        elif self.entity_description.key == KEY_RAIN_SENSOR:
+            if args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_SENSOR_DETECTION_ON:
+                self._attr_is_on = True
+            elif args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_SENSOR_DETECTION_OFF:
+                self._attr_is_on = False
path: homeassistant/components/rachio/binary_sensor.py
user: login: bdraco
body: Let's add the fields to entity description subclass so we don't have to have all of the logic here
created_at: 2025-05-02T22:49:18Z
author_association: MEMBER
diff_hunk: @@ -82,97 +148,64 @@ def _async_handle_any_update(self, *args, **kwargs) -> None:
         # For this device
         self._async_handle_update(args, kwargs)
 
-    @abstractmethod
-    def _async_handle_update(self, *args, **kwargs) -> None:
-        """Handle an update to the state of this sensor."""
-
-
-class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
-    """Represent a binary sensor that reflects if the controller is online."""
-
-    _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
-
-    @property
-    def unique_id(self) -> str:
-        """Return a unique id for this entity."""
-        return f"{self._controller.controller_id}-online"
-
     @callback
     def _async_handle_update(self, *args, **kwargs) -> None:
         """Handle an update to the state of this sensor."""
-        if args[0][0][KEY_SUBTYPE] in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT):
-            self._attr_is_on = True
-        elif args[0][0][KEY_SUBTYPE] == SUBTYPE_OFFLINE:
-            self._attr_is_on = False
+        if self.entity_description.key == KEY_ONLINE:
+            if args[0][0][KEY_SUBTYPE] in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT):
+                self._attr_is_on = True
+            elif args[0][0][KEY_SUBTYPE] == SUBTYPE_OFFLINE:
+                self._attr_is_on = False
+        elif self.entity_description.key == KEY_RAIN_SENSOR:
+            if args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_SENSOR_DETECTION_ON:
+                self._attr_is_on = True
+            elif args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_SENSOR_DETECTION_OFF:
+                self._attr_is_on = False
path: homeassistant/components/rachio/binary_sensor.py
user: login: brg468
body: I think that's what you were looking for, let me know if not.
created_at: 2025-05-05T13:15:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,6 +46,68 @@
 _LOGGER = logging.getLogger(__name__)
 
 
+@dataclass(frozen=True, kw_only=True)
+class RachioControllerBinarySensorDescription(BinarySensorEntityDescription):
+    """Describe a Rachio controller binary sensor."""
+
+    update_received: Callable[[str], bool | None]
+    is_on: Callable[[RachioIro], bool]
+    signal_string: str
+
+
+CONTROLLER_BINARY_SENSOR_TYPES: tuple[RachioControllerBinarySensorDescription, ...] = (
+    RachioControllerBinarySensorDescription(
+        key=KEY_ONLINE,
+        device_class=BinarySensorDeviceClass.CONNECTIVITY,
+        signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE,
+        update_received=lambda state: True
+        if state in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT)
+        else False
+        if state == SUBTYPE_OFFLINE
+        else None,
path: homeassistant/components/rachio/binary_sensor.py
user: login: joostlek
body: Please wrap multiline lambdas in parenthesis
created_at: 2025-05-09T16:57:30Z
author_association: MEMBER
diff_hunk: @@ -43,6 +46,68 @@
 _LOGGER = logging.getLogger(__name__)
 
 
+@dataclass(frozen=True, kw_only=True)
+class RachioControllerBinarySensorDescription(BinarySensorEntityDescription):
+    """Describe a Rachio controller binary sensor."""
+
+    update_received: Callable[[str], bool | None]
+    is_on: Callable[[RachioIro], bool]
+    signal_string: str
+
+
+CONTROLLER_BINARY_SENSOR_TYPES: tuple[RachioControllerBinarySensorDescription, ...] = (
+    RachioControllerBinarySensorDescription(
+        key=KEY_ONLINE,
+        device_class=BinarySensorDeviceClass.CONNECTIVITY,
+        signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE,
+        update_received=lambda state: True
+        if state in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT)
+        else False
+        if state == SUBTYPE_OFFLINE
+        else None,
+    ),
+    RachioControllerBinarySensorDescription(
+        key=KEY_RAIN_SENSOR,
+        translation_key="rain",
+        device_class=BinarySensorDeviceClass.MOISTURE,
+        signal_string=SIGNAL_RACHIO_RAIN_SENSOR_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_RAIN_SENSOR_TRIPPED],
+        update_received=lambda state: True
+        if state == SUBTYPE_RAIN_SENSOR_DETECTION_ON
+        else False
+        if state == SUBTYPE_RAIN_SENSOR_DETECTION_OFF
+        else None,
+    ),
path: homeassistant/components/rachio/binary_sensor.py
user: login: joostlek
body: idem
created_at: 2025-05-09T16:57:35Z
author_association: MEMBER
diff_hunk: @@ -43,6 +46,68 @@
 _LOGGER = logging.getLogger(__name__)
 
 
+@dataclass(frozen=True, kw_only=True)
+class RachioControllerBinarySensorDescription(BinarySensorEntityDescription):
+    """Describe a Rachio controller binary sensor."""
+
+    update_received: Callable[[str], bool | None]
+    is_on: Callable[[RachioIro], bool]
+    signal_string: str
+
+
+CONTROLLER_BINARY_SENSOR_TYPES: tuple[RachioControllerBinarySensorDescription, ...] = (
+    RachioControllerBinarySensorDescription(
+        key=KEY_ONLINE,
+        device_class=BinarySensorDeviceClass.CONNECTIVITY,
+        signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE,
+        update_received=lambda state: True
+        if state in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT)
+        else False
+        if state == SUBTYPE_OFFLINE
+        else None,
+    ),
+    RachioControllerBinarySensorDescription(
+        key=KEY_RAIN_SENSOR,
+        translation_key="rain",
+        device_class=BinarySensorDeviceClass.MOISTURE,
+        signal_string=SIGNAL_RACHIO_RAIN_SENSOR_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_RAIN_SENSOR_TRIPPED],
+        update_received=lambda state: True
+        if state == SUBTYPE_RAIN_SENSOR_DETECTION_ON
+        else False
+        if state == SUBTYPE_RAIN_SENSOR_DETECTION_OFF
+        else None,
+    ),
path: homeassistant/components/rachio/binary_sensor.py
user: login: joostlek
body: this one might be a dict lookup
created_at: 2025-05-09T16:57:54Z
author_association: MEMBER
diff_hunk: @@ -56,22 +121,41 @@ async def async_setup_entry(
 def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Entity]:
     entities: list[Entity] = []
     person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id]
-    for controller in person.controllers:
-        entities.append(RachioControllerOnlineBinarySensor(controller))
-        entities.append(RachioRainSensor(controller))
     entities.extend(
-        RachioHoseTimerBattery(valve, base_station.status_coordinator)
-        for base_station in person.base_stations
-        for valve in base_station.status_coordinator.data.values()
+        RachioControllerBinarySensor(controller, description)
+        for controller in person.controllers
+        for description in CONTROLLER_BINARY_SENSOR_TYPES
     )
+    for base_station in person.base_stations:
+        entities.extend(
path: homeassistant/components/rachio/binary_sensor.py
user: login: joostlek
body: you can move the for loop to the list comprehension
created_at: 2025-05-09T16:58:16Z
author_association: MEMBER
diff_hunk: @@ -56,22 +121,41 @@ async def async_setup_entry(
 def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Entity]:
     entities: list[Entity] = []
     person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id]
-    for controller in person.controllers:
-        entities.append(RachioControllerOnlineBinarySensor(controller))
-        entities.append(RachioRainSensor(controller))
     entities.extend(
-        RachioHoseTimerBattery(valve, base_station.status_coordinator)
-        for base_station in person.base_stations
-        for valve in base_station.status_coordinator.data.values()
+        RachioControllerBinarySensor(controller, description)
+        for controller in person.controllers
+        for description in CONTROLLER_BINARY_SENSOR_TYPES
     )
+    for base_station in person.base_stations:
+        entities.extend(
+            RachioHoseTimerBinarySensor(
+                valve, base_station.status_coordinator, description
+            )
+            for valve in base_station.status_coordinator.data.values()
+            for description in HOSE_TIMER_BINARY_SENSOR_TYPES
+            if description.exists_fn(valve)
+        )
     return entities
 
 
 class RachioControllerBinarySensor(RachioDevice, BinarySensorEntity):
-    """Represent a binary sensor that reflects a Rachio state."""
+    """Represent a binary sensor that reflects a Rachio controller state."""
 
+    entity_description: RachioControllerBinarySensorDescription
     _attr_has_entity_name = True
 
+    def __init__(
+        self,
+        controller: RachioIro,
+        description: RachioControllerBinarySensorDescription,
+    ) -> None:
+        """Initialize a controller binary sensor."""
+        super().__init__(controller)
+        self.entity_description = description
+        self._attr_unique_id = (
+            f"{self._controller.controller_id}-{self.entity_description.key}"
path: homeassistant/components/rachio/binary_sensor.py
user: login: joostlek
body: ```suggestion
            f"{controller.controller_id}-{description.key}"
```
created_at: 2025-05-09T16:58:33Z
author_association: MEMBER
diff_hunk: @@ -82,97 +166,46 @@ def _async_handle_any_update(self, *args, **kwargs) -> None:
         # For this device
         self._async_handle_update(args, kwargs)
 
-    @abstractmethod
-    def _async_handle_update(self, *args, **kwargs) -> None:
-        """Handle an update to the state of this sensor."""
-
-
-class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
-    """Represent a binary sensor that reflects if the controller is online."""
-
-    _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
-
-    @property
-    def unique_id(self) -> str:
-        """Return a unique id for this entity."""
-        return f"{self._controller.controller_id}-online"
-
-    @callback
-    def _async_handle_update(self, *args, **kwargs) -> None:
-        """Handle an update to the state of this sensor."""
-        if args[0][0][KEY_SUBTYPE] in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT):
-            self._attr_is_on = True
-        elif args[0][0][KEY_SUBTYPE] == SUBTYPE_OFFLINE:
-            self._attr_is_on = False
-
-        self.async_write_ha_state()
-
-    async def async_added_to_hass(self) -> None:
-        """Subscribe to updates."""
-        self._attr_is_on = self._controller.init_data[KEY_STATUS] == STATUS_ONLINE
-
-        self.async_on_remove(
-            async_dispatcher_connect(
-                self.hass,
-                SIGNAL_RACHIO_CONTROLLER_UPDATE,
-                self._async_handle_any_update,
-            )
-        )
-
-
-class RachioRainSensor(RachioControllerBinarySensor):
-    """Represent a binary sensor that reflects the status of the rain sensor."""
-
-    _attr_device_class = BinarySensorDeviceClass.MOISTURE
-    _attr_translation_key = "rain"
-
-    @property
-    def unique_id(self) -> str:
-        """Return a unique id for this entity."""
-        return f"{self._controller.controller_id}-rain_sensor"
-
     @callback
     def _async_handle_update(self, *args, **kwargs) -> None:
         """Handle an update to the state of this sensor."""
-        if args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_SENSOR_DETECTION_ON:
-            self._attr_is_on = True
-        elif args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_SENSOR_DETECTION_OFF:
-            self._attr_is_on = False
+        updated_state = self.entity_description.update_received(args[0][0][KEY_SUBTYPE])
+        if updated_state is not None:
+            self._attr_is_on = updated_state
 
         self.async_write_ha_state()
 
     async def async_added_to_hass(self) -> None:
         """Subscribe to updates."""
-        self._attr_is_on = self._controller.init_data[KEY_RAIN_SENSOR_TRIPPED]
+        self._attr_is_on = self.entity_description.is_on(self._controller)
 
         self.async_on_remove(
             async_dispatcher_connect(
                 self.hass,
-                SIGNAL_RACHIO_RAIN_SENSOR_UPDATE,
+                self.entity_description.signal_string,
                 self._async_handle_any_update,
             )
         )
 
 
-class RachioHoseTimerBattery(RachioHoseTimerEntity, BinarySensorEntity):
-    """Represents a battery sensor for a smart hose timer."""
+class RachioHoseTimerBinarySensor(RachioHoseTimerEntity, BinarySensorEntity):
+    """Represents a binary sensor for a smart hose timer."""
 
-    _attr_device_class = BinarySensorDeviceClass.BATTERY
+    entity_description: RachioHoseTimerBinarySensorDescription
 
     def __init__(
-        self, data: dict[str, Any], coordinator: RachioUpdateCoordinator
+        self,
+        data: dict[str, Any],
+        coordinator: RachioUpdateCoordinator,
+        description: RachioHoseTimerBinarySensorDescription,
     ) -> None:
-        """Initialize a smart hose timer battery sensor."""
+        """Initialize a smart hose timer binary sensor."""
         super().__init__(data, coordinator)
-        self._attr_unique_id = f"{self.id}-battery"
+        self.entity_description = description
+        self._attr_unique_id = f"{self.id}-{self.entity_description.key}"
path: homeassistant/components/rachio/binary_sensor.py
user: login: joostlek
body: ```suggestion
        self._attr_unique_id = f"{self.id}-{description.key}"
```
created_at: 2025-05-09T16:58:51Z
author_association: MEMBER
diff_hunk: @@ -31,6 +31,9 @@
     "binary_sensor": {
       "rain": {
         "name": "Rain"
+      },
+      "flow": {
+        "name": "Flow"
path: homeassistant/components/rachio/strings.json
user: login: joostlek
body: Does this entity name sound logical? Since the device class is problem so it would mean that there is no flow when the state is `on` right?
created_at: 2025-05-09T16:59:33Z
author_association: MEMBER
diff_hunk: @@ -31,6 +31,9 @@
     "binary_sensor": {
       "rain": {
         "name": "Rain"
+      },
+      "flow": {
+        "name": "Flow"
path: homeassistant/components/rachio/strings.json
user: login: brg468
body: I was struggling with how to name this too. The frontend and automation editor either show OK or Problem so I was going with the flow being in one of those states for the name because it was easier to understand. Open to suggestions however. 
created_at: 2025-05-10T11:37:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,6 +46,68 @@
 _LOGGER = logging.getLogger(__name__)
 
 
+@dataclass(frozen=True, kw_only=True)
+class RachioControllerBinarySensorDescription(BinarySensorEntityDescription):
+    """Describe a Rachio controller binary sensor."""
+
+    update_received: Callable[[str], bool | None]
+    is_on: Callable[[RachioIro], bool]
+    signal_string: str
+
+
+CONTROLLER_BINARY_SENSOR_TYPES: tuple[RachioControllerBinarySensorDescription, ...] = (
+    RachioControllerBinarySensorDescription(
+        key=KEY_ONLINE,
+        device_class=BinarySensorDeviceClass.CONNECTIVITY,
+        signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE,
+        update_received=lambda state: True
+        if state in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT)
+        else False
+        if state == SUBTYPE_OFFLINE
+        else None,
+    ),
+    RachioControllerBinarySensorDescription(
+        key=KEY_RAIN_SENSOR,
+        translation_key="rain",
+        device_class=BinarySensorDeviceClass.MOISTURE,
+        signal_string=SIGNAL_RACHIO_RAIN_SENSOR_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_RAIN_SENSOR_TRIPPED],
+        update_received=lambda state: True
+        if state == SUBTYPE_RAIN_SENSOR_DETECTION_ON
+        else False
+        if state == SUBTYPE_RAIN_SENSOR_DETECTION_OFF
+        else None,
+    ),
path: homeassistant/components/rachio/binary_sensor.py
user: login: brg468
body: I just left as is, it seemed possible as a dict lookup but then I had issues with MyPy complaining about unnecessary lambdas and wanted a named function. 
created_at: 2025-05-12T17:37:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,6 +46,68 @@
 _LOGGER = logging.getLogger(__name__)
 
 
+@dataclass(frozen=True, kw_only=True)
+class RachioControllerBinarySensorDescription(BinarySensorEntityDescription):
+    """Describe a Rachio controller binary sensor."""
+
+    update_received: Callable[[str], bool | None]
+    is_on: Callable[[RachioIro], bool]
+    signal_string: str
+
+
+CONTROLLER_BINARY_SENSOR_TYPES: tuple[RachioControllerBinarySensorDescription, ...] = (
+    RachioControllerBinarySensorDescription(
+        key=KEY_ONLINE,
+        device_class=BinarySensorDeviceClass.CONNECTIVITY,
+        signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE,
+        update_received=lambda state: True
+        if state in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT)
+        else False
+        if state == SUBTYPE_OFFLINE
+        else None,
+    ),
+    RachioControllerBinarySensorDescription(
+        key=KEY_RAIN_SENSOR,
+        translation_key="rain",
+        device_class=BinarySensorDeviceClass.MOISTURE,
+        signal_string=SIGNAL_RACHIO_RAIN_SENSOR_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_RAIN_SENSOR_TRIPPED],
+        update_received=lambda state: True
+        if state == SUBTYPE_RAIN_SENSOR_DETECTION_ON
+        else False
+        if state == SUBTYPE_RAIN_SENSOR_DETECTION_OFF
+        else None,
+    ),
path: homeassistant/components/rachio/binary_sensor.py
user: login: joostlek
body: ```py
        update_received={
            SUBTYPE_RAIN_SENSOR_DETECTION_ON: True,
            SUBTYPE_RAIN_SENSOR_DETECTION_OFF: False,
        }.get,
```
created_at: 2025-05-12T17:41:26Z
author_association: MEMBER
diff_hunk: @@ -43,6 +46,68 @@
 _LOGGER = logging.getLogger(__name__)
 
 
+@dataclass(frozen=True, kw_only=True)
+class RachioControllerBinarySensorDescription(BinarySensorEntityDescription):
+    """Describe a Rachio controller binary sensor."""
+
+    update_received: Callable[[str], bool | None]
+    is_on: Callable[[RachioIro], bool]
+    signal_string: str
+
+
+CONTROLLER_BINARY_SENSOR_TYPES: tuple[RachioControllerBinarySensorDescription, ...] = (
+    RachioControllerBinarySensorDescription(
+        key=KEY_ONLINE,
+        device_class=BinarySensorDeviceClass.CONNECTIVITY,
+        signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE,
+        update_received=lambda state: True
+        if state in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT)
+        else False
+        if state == SUBTYPE_OFFLINE
+        else None,
+    ),
+    RachioControllerBinarySensorDescription(
+        key=KEY_RAIN_SENSOR,
+        translation_key="rain",
+        device_class=BinarySensorDeviceClass.MOISTURE,
+        signal_string=SIGNAL_RACHIO_RAIN_SENSOR_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_RAIN_SENSOR_TRIPPED],
+        update_received=lambda state: True
+        if state == SUBTYPE_RAIN_SENSOR_DETECTION_ON
+        else False
+        if state == SUBTYPE_RAIN_SENSOR_DETECTION_OFF
+        else None,
+    ),
path: homeassistant/components/rachio/binary_sensor.py
user: login: brg468
body: I was overthinking it. Will change. Thx
created_at: 2025-05-12T17:44:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,6 +46,68 @@
 _LOGGER = logging.getLogger(__name__)
 
 
+@dataclass(frozen=True, kw_only=True)
+class RachioControllerBinarySensorDescription(BinarySensorEntityDescription):
+    """Describe a Rachio controller binary sensor."""
+
+    update_received: Callable[[str], bool | None]
+    is_on: Callable[[RachioIro], bool]
+    signal_string: str
+
+
+CONTROLLER_BINARY_SENSOR_TYPES: tuple[RachioControllerBinarySensorDescription, ...] = (
+    RachioControllerBinarySensorDescription(
+        key=KEY_ONLINE,
+        device_class=BinarySensorDeviceClass.CONNECTIVITY,
+        signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE,
+        update_received=lambda state: True
+        if state in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT)
+        else False
+        if state == SUBTYPE_OFFLINE
+        else None,
path: homeassistant/components/rachio/binary_sensor.py
user: login: brg468
body: For that matter this should work here then shouldn't it?

```
 RachioControllerBinarySensorDescription(
        key=KEY_ONLINE,
        device_class=BinarySensorDeviceClass.CONNECTIVITY,
        signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE,
        is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE,
        update_received={
            SUBTYPE_ONLINE: True,
            SUBTYPE_COLD_REBOOT: True,
            SUBTYPE_OFFLINE: False,
        }.get,
```
created_at: 2025-05-13T15:56:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,6 +46,69 @@
 _LOGGER = logging.getLogger(__name__)
 
 
+@dataclass(frozen=True, kw_only=True)
+class RachioControllerBinarySensorDescription(BinarySensorEntityDescription):
+    """Describe a Rachio controller binary sensor."""
+
+    update_received: Callable[[str], bool | None]
+    is_on: Callable[[RachioIro], bool]
+    signal_string: str
+
+
+CONTROLLER_BINARY_SENSOR_TYPES: tuple[RachioControllerBinarySensorDescription, ...] = (
+    RachioControllerBinarySensorDescription(
+        key=KEY_ONLINE,
+        device_class=BinarySensorDeviceClass.CONNECTIVITY,
+        signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE,
+        update_received=lambda state: (
+            True
+            if state in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT)
+            else False
+            if state == SUBTYPE_OFFLINE
+            else None
+        ),
path: homeassistant/components/rachio/binary_sensor.py
user: login: joostlek
body: Maybe you could do the same here well but then with 3 keys in the dict, since I believe those dicts look way cleaner than this if else tree. I might pick it even though this uses an `in`
created_at: 2025-05-13T15:59:12Z
author_association: MEMBER
diff_hunk: @@ -43,6 +46,69 @@
 _LOGGER = logging.getLogger(__name__)
 
 
+@dataclass(frozen=True, kw_only=True)
+class RachioControllerBinarySensorDescription(BinarySensorEntityDescription):
+    """Describe a Rachio controller binary sensor."""
+
+    update_received: Callable[[str], bool | None]
+    is_on: Callable[[RachioIro], bool]
+    signal_string: str
+
+
+CONTROLLER_BINARY_SENSOR_TYPES: tuple[RachioControllerBinarySensorDescription, ...] = (
+    RachioControllerBinarySensorDescription(
+        key=KEY_ONLINE,
+        device_class=BinarySensorDeviceClass.CONNECTIVITY,
+        signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE,
+        update_received=lambda state: (
+            True
+            if state in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT)
+            else False
+            if state == SUBTYPE_OFFLINE
+            else None
+        ),
path: homeassistant/components/rachio/binary_sensor.py
user: login: brg468
body: Like this? Works as expected
```
 RachioControllerBinarySensorDescription(
        key=KEY_ONLINE,
        device_class=BinarySensorDeviceClass.CONNECTIVITY,
        signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE,
        is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE,
        update_received={
            SUBTYPE_ONLINE: True,
            SUBTYPE_COLD_REBOOT: True,
            SUBTYPE_OFFLINE: False,
        }.get,
```
created_at: 2025-05-13T16:03:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,6 +46,69 @@
 _LOGGER = logging.getLogger(__name__)
 
 
+@dataclass(frozen=True, kw_only=True)
+class RachioControllerBinarySensorDescription(BinarySensorEntityDescription):
+    """Describe a Rachio controller binary sensor."""
+
+    update_received: Callable[[str], bool | None]
+    is_on: Callable[[RachioIro], bool]
+    signal_string: str
+
+
+CONTROLLER_BINARY_SENSOR_TYPES: tuple[RachioControllerBinarySensorDescription, ...] = (
+    RachioControllerBinarySensorDescription(
+        key=KEY_ONLINE,
+        device_class=BinarySensorDeviceClass.CONNECTIVITY,
+        signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE,
+        is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE,
+        update_received=lambda state: (
+            True
+            if state in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT)
+            else False
+            if state == SUBTYPE_OFFLINE
+            else None
+        ),
path: homeassistant/components/rachio/binary_sensor.py
user: login: joostlek
body: I personally think this is indeed a better code style than 2 chained if statements

(I think there could be a reason for performance, but I think I prefer this cleaneliness over the slight improvement)
created_at: 2025-05-13T16:06:13Z
author_association: MEMBER
diff_hunk: @@ -82,97 +163,46 @@ def _async_handle_any_update(self, *args, **kwargs) -> None:
         # For this device
         self._async_handle_update(args, kwargs)
 
-    @abstractmethod
-    def _async_handle_update(self, *args, **kwargs) -> None:
-        """Handle an update to the state of this sensor."""
-
-
-class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
-    """Represent a binary sensor that reflects if the controller is online."""
-
-    _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
-
-    @property
-    def unique_id(self) -> str:
-        """Return a unique id for this entity."""
-        return f"{self._controller.controller_id}-online"
-
     @callback
     def _async_handle_update(self, *args, **kwargs) -> None:
         """Handle an update to the state of this sensor."""
-        if args[0][0][KEY_SUBTYPE] in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT):
-            self._attr_is_on = True
-        elif args[0][0][KEY_SUBTYPE] == SUBTYPE_OFFLINE:
-            self._attr_is_on = False
+        updated_state = self.entity_description.update_received(args[0][0][KEY_SUBTYPE])
+        if updated_state is not None:
path: homeassistant/components/rachio/binary_sensor.py
user: login: joostlek
body: ```suggestion
        if (updated_state := self.entity_description.update_received(args[0][0][KEY_SUBTYPE])) is not None:
```
created_at: 2025-05-13T16:07:20Z
author_association: MEMBER

PR comments:
user: login: home-assistant[bot]
created_at: 2025-05-01T21:05:00Z
body: 
Hey there @bdraco, @rfverbruggen, mind taking a look at this pull request as it has been labeled with an integration (`rachio`) you are listed as a [code owner](https://github.com/home-assistant/core/blob/dev/CODEOWNERS#L1228) for? Thanks!

<details>
<summary>Code owner commands</summary>


Code owners of `rachio` can trigger bot actions by commenting:

- `@home-assistant close` Closes the pull request.
- `@home-assistant rename Awesome new title` Renames the pull request.
- `@home-assistant reopen` Reopen the pull request.
- `@home-assistant unassign rachio` Removes the current integration label and assignees on the pull request, add the integration domain after the command.
- `@home-assistant add-label needs-more-information` Add a label (needs-more-information, problem in dependency, problem in custom component) to the pull request.
- `@home-assistant remove-label needs-more-information` Remove a label (needs-more-information, problem in dependency, problem in custom component) on the pull request.

</details>


author_association: CONTRIBUTOR
user: login: home-assistant[bot]
created_at: 2025-05-09T16:59:37Z
body: <!-- ReviewDrafterComment -->
Please take a look at the requested changes, and use the **Ready for review** button when you are done, thanks :+1:

[_Learn more about our pull request process._](https://developers.home-assistant.io/docs/review-process#prs-are-being-drafted-when-changes-are-needed)

author_association: CONTRIBUTOR
user: login: brg468
created_at: 2025-05-13T16:16:56Z
body: Thanks @joostlek!
author_association: CONTRIBUTOR
user: login: bdraco
created_at: 2025-05-14T04:15:19Z
body: I pushed some readability improvements. If it works for you, its good to merge from my perspective
author_association: MEMBER
user: login: brg468
created_at: 2025-05-14T12:47:16Z
body: Looks good to me. Thanks @bdraco and @joostlek!
author_association: CONTRIBUTOR