core/homeassistant/components/rachio/binary_sensor.py

210 lines
6.8 KiB
Python

"""Integration with the Rachio Iro sprinkler system controller."""
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.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 (
KEY_BATTERY,
KEY_DETECT_FLOW,
KEY_DEVICE_ID,
KEY_FLOW,
KEY_ONLINE,
KEY_RAIN_SENSOR,
KEY_RAIN_SENSOR_TRIPPED,
KEY_STATUS,
KEY_SUBTYPE,
SIGNAL_RACHIO_CONTROLLER_UPDATE,
SIGNAL_RACHIO_RAIN_SENSOR_UPDATE,
STATUS_ONLINE,
)
from .coordinator import RachioUpdateCoordinator
from .device import RachioConfigEntry, RachioIro
from .entity import RachioDevice, RachioHoseTimerEntity
from .webhooks import (
SUBTYPE_COLD_REBOOT,
SUBTYPE_OFFLINE,
SUBTYPE_ONLINE,
SUBTYPE_RAIN_SENSOR_DETECTION_OFF,
SUBTYPE_RAIN_SENSOR_DETECTION_ON,
)
_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: RachioConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Rachio binary sensors."""
entities = await hass.async_add_executor_job(_create_entities, hass, config_entry)
async_add_entities(entities)
def _create_entities(
hass: HomeAssistant, config_entry: RachioConfigEntry
) -> list[Entity]:
entities: list[Entity] = []
person = config_entry.runtime_data
entities.extend(
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 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."""
if args[0][KEY_DEVICE_ID] != self._controller.controller_id:
# For another device
return
# For this device
self._async_handle_update(args, kwargs)
@callback
def _async_handle_update(self, *args, **kwargs) -> None:
"""Handle an update to the state of this sensor."""
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.entity_description.is_on(self._controller)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
self.entity_description.signal_string,
self._async_handle_any_update,
)
)
class RachioHoseTimerBinarySensor(RachioHoseTimerEntity, BinarySensorEntity):
"""Represents a binary sensor for a smart hose timer."""
entity_description: RachioHoseTimerBinarySensorDescription
def __init__(
self,
data: dict[str, Any],
coordinator: RachioUpdateCoordinator,
description: RachioHoseTimerBinarySensorDescription,
) -> None:
"""Initialize a smart hose timer binary sensor."""
super().__init__(data, coordinator)
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."""
self._attr_is_on = self.entity_description.value_fn(self)