mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +00:00
Add flow detection to Rachio hose timer (#144075)
* flow binary sensor * rename property * Move const and update coordinator reference * update controller descriptions * Address review comments * Use lookup for rain sensor * Update online binary sensor * make it a bit more readable --------- Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
ef99658919
commit
b0ff4b5841
@ -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 @@ from .const import (
|
||||
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 @@ from .webhooks import (
|
||||
_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(
|
||||
RachioHoseTimerBattery(valve, base_station.status_coordinator)
|
||||
RachioControllerBinarySensor(controller, description)
|
||||
for controller in person.controllers
|
||||
for description in CONTROLLER_BINARY_SENSOR_TYPES
|
||||
)
|
||||
entities.extend(
|
||||
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 @@ class RachioControllerBinarySensor(RachioDevice, BinarySensorEntity):
|
||||
# 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 (
|
||||
updated_state := self.entity_description.update_received(
|
||||
args[0][0][KEY_SUBTYPE]
|
||||
)
|
||||
) 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_STATUS] == STATUS_ONLINE
|
||||
self._attr_is_on = self.entity_description.is_on(self._controller)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_RACHIO_CONTROLLER_UPDATE,
|
||||
self.entity_description.signal_string,
|
||||
self._async_handle_any_update,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class RachioRainSensor(RachioControllerBinarySensor):
|
||||
"""Represent a binary sensor that reflects the status of the rain sensor."""
|
||||
class RachioHoseTimerBinarySensor(RachioHoseTimerEntity, BinarySensorEntity):
|
||||
"""Represents a binary sensor for a smart hose timer."""
|
||||
|
||||
_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
|
||||
|
||||
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.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_RACHIO_RAIN_SENSOR_UPDATE,
|
||||
self._async_handle_any_update,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class RachioHoseTimerBattery(RachioHoseTimerEntity, BinarySensorEntity):
|
||||
"""Represents a battery 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)
|
||||
|
@ -25,10 +25,12 @@ KEY_ID = "id"
|
||||
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_STATE = "state"
|
||||
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"
|
||||
|
@ -12,9 +12,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
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 @@ class RachioHoseTimerEntity(CoordinatorEntity[RachioUpdateCoordinator]):
|
||||
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:
|
||||
|
@ -31,6 +31,9 @@
|
||||
"binary_sensor": {
|
||||
"rain": {
|
||||
"name": "Rain"
|
||||
},
|
||||
"flow": {
|
||||
"name": "Flow"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
|
@ -37,9 +37,7 @@ from .const import (
|
||||
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 @@ class RachioValve(RachioHoseTimerEntity, SwitchEntity):
|
||||
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 @@ class RachioValve(RachioHoseTimerEntity, SwitchEntity):
|
||||
@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
|
||||
|
Loading…
x
Reference in New Issue
Block a user