Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
48bbb21
Rainbird config flow
allenporter Dec 24, 2022
ffd2b5f
Add options for irrigation time and deprecate yaml
allenporter Jan 6, 2023
e16b0c6
Combine exception handling paths to get 100% test coverage
allenporter Jan 6, 2023
49dcb7c
Bump the rainird config deprecation release
allenporter Jan 6, 2023
797cf9e
Apply suggestions from code review
allenporter Jan 6, 2023
fb8ce86
Merge branch 'rainbird-config-entry' of github.com:allenporter/home-a…
allenporter Jan 6, 2023
cf019cc
Remove unnecessary sensor/binary sensor and address some PR feedback
allenporter Jan 6, 2023
868a2de
Simplify configuration flow and options based on PR feedback
allenporter Jan 6, 2023
8c1aa07
Consolidate data update coordinators to simplify overall integration
allenporter Jan 6, 2023
1e019a6
Fix type error on python3.9
allenporter Jan 6, 2023
f82a533
Handle yaml name import
allenporter Jan 6, 2023
9b12dee
Fix naming import post serialization
allenporter Jan 6, 2023
0cc4ec9
Parallelize requests to the device
allenporter Jan 6, 2023
9f8e2d7
Complete conversion to entity service
allenporter Jan 6, 2023
d8eb12e
Update homeassistant/components/rainbird/switch.py
allenporter Jan 6, 2023
63f43fb
Update homeassistant/components/rainbird/config_flow.py
allenporter Jan 6, 2023
009c08d
Remove unused import
allenporter Jan 7, 2023
b91275a
Set default duration in options used in tests
allenporter Jan 7, 2023
15edee1
Add separate devices for each sprinkler zone and update service to us…
allenporter Jan 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 98 additions & 35 deletions homeassistant/components/rainbird/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,40 @@
import asyncio
import logging

import async_timeout
from pyrainbird.async_client import (
AsyncRainbirdClient,
AsyncRainbirdController,
RainbirdApiException,
)
import voluptuous as vol

from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_FRIENDLY_NAME,
CONF_HOST,
CONF_PASSWORD,
CONF_TRIGGER_TIME,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType

from .const import (
ATTR_DURATION,
CONF_ZONES,
DEVICE_INFO,
MANUFACTURER,
RAINBIRD_CONTROLLER,
SENSOR_TYPE_RAINDELAY,
SENSOR_TYPE_RAINSENSOR,
SERIAL_NUMBER,
TIMEOUT_SECONDS,
)
from .coordinator import RainbirdUpdateCoordinator

Expand Down Expand Up @@ -61,47 +70,101 @@
extra=vol.ALLOW_EXTRA,
)

SERVICE_SET_RAIN_DELAY = "set_rain_delay"
SERVICE_SCHEMA_RAIN_DELAY = vol.Schema(
{
vol.Required(ATTR_DURATION): cv.positive_float,
}
)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Rain Bird component."""
return all(
await asyncio.gather(
*[
_setup_controller(hass, controller_config, config)
for controller_config in config[DOMAIN]
]
if DOMAIN not in config:
return True

for controller_config in config[DOMAIN]:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=controller_config,
)
)

async_create_issue(
hass,
DOMAIN,
"deprecated_yaml",
breaks_in_ha_version="2023.3.0",
Comment thread
allenporter marked this conversation as resolved.
Outdated
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
)

return True

async def _setup_controller(hass, controller_config, config):
"""Set up a controller."""
server = controller_config[CONF_HOST]
password = controller_config[CONF_PASSWORD]
client = AsyncRainbirdClient(async_get_clientsession(hass), server, password)
controller = AsyncRainbirdController(client)
try:
await controller.get_serial_number()
except RainbirdApiException as exc:
_LOGGER.error("Unable to setup controller: %s", exc)
return False

rain_coordinator = RainbirdUpdateCoordinator(hass, controller.get_rain_sensor_state)
delay_coordinator = RainbirdUpdateCoordinator(hass, controller.get_rain_delay)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the config entry for Rain Bird."""
hass.data.setdefault(DOMAIN, {})

for platform in PLATFORMS:
hass.async_create_task(
discovery.async_load_platform(
hass,
platform,
DOMAIN,
{
RAINBIRD_CONTROLLER: controller,
SENSOR_TYPE_RAINSENSOR: rain_coordinator,
SENSOR_TYPE_RAINDELAY: delay_coordinator,
**controller_config,
},
config,
)
controller = AsyncRainbirdController(
AsyncRainbirdClient(
async_get_clientsession(hass),
entry.data[CONF_HOST],
entry.data[CONF_PASSWORD],
)
)

try:
async with async_timeout.timeout(TIMEOUT_SECONDS):
serial_number = await controller.get_serial_number()
except (RainbirdApiException, asyncio.TimeoutError) as err:
raise ConfigEntryNotReady(f"Error talking to controller: {str(err)}") from err

device_info = DeviceInfo(
default_name=MANUFACTURER,
identifiers={(DOMAIN, serial_number)},
manufacturer=MANUFACTURER,
)
rain_coordinator = RainbirdUpdateCoordinator(
hass, "Rain", controller.get_rain_sensor_state
)
delay_coordinator = RainbirdUpdateCoordinator(
hass, "Rain delay", controller.get_rain_delay
)

hass.data[DOMAIN][entry.entry_id] = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I recommend using a dataclass to store multiple items in hass.data.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Based on this comment and the comment about initializing the update coordinators in the platforms, I decided to just simplify the overall interactions between the device and the integration, moving all the rpcs into a single data update coordinator. This required changing the tests, so i added additional test fixtures to help manage the request setup and ordering expected by the integration.

The consequence of this is now a single update coordinator handles rpcs for all platforms so there isn't independence for each sensor, but in hindsight that is probably simpler overall (e.g. if the device is offline, there is just one coordinator trying to hit it rather than 3).

While I was here, I also removed some unnecessary duplicate sensors. Today there is a rain sensor and a rain delay sensor both exported as a sensor and binary sensor and it doesn't really make a lot of sense to me. I've just left the sensors that provide unique value and have updated the breaking changes in the description.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If this is too much for a single PR, I can break into smaller chunks, or move into another branch to not have to worry about leaving dev in a bad state.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's not too much.

What did the four binary sensors and sensors represent? How would a user replace the removed sensors?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There were four total sensors representing two values that were redundant. I updated the PR to describe but can reword if it is not clear.

The "rain" sensor was a binary sensor and a sensor with a string boolean value. This represents whether the device knows it is raining (I'm not sure if it's from a separate sensors only or also from a cloud weather report)

The "rain delay" represents the number of days that the sprinkler is paused due to rain. This is a sensor and also had a binary sensor when it was non-zero. The rain delay service can update this value so maybe this should just be a Number?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok.

Yeah, if the rain delay service and sensor are in sync it should be a number entity.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can deprecate the service and raise a repair issue telling users to move to the number entity and service when that's in place.

SERIAL_NUMBER: serial_number,
DEVICE_INFO: device_info,
RAINBIRD_CONTROLLER: controller,
SENSOR_TYPE_RAINSENSOR: rain_coordinator,
SENSOR_TYPE_RAINDELAY: delay_coordinator,
}

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

async def set_rain_delay(service: ServiceCall) -> None:
await controller.set_rain_delay(service.data[ATTR_DURATION])
Comment thread
allenporter marked this conversation as resolved.
Outdated

hass.services.async_register(
DOMAIN,
SERVICE_SET_RAIN_DELAY,
set_rain_delay,
schema=SERVICE_SCHEMA_RAIN_DELAY,
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""

if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)

if unload_ok and not hass.data[DOMAIN]:
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
hass.services.async_remove(DOMAIN, SERVICE_SET_RAIN_DELAY)

return unload_ok
48 changes: 32 additions & 16 deletions homeassistant/components/rainbird/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
"""Support for Rain Bird Irrigation system LNK WiFi Module."""
from __future__ import annotations

import asyncio
import logging
from typing import Union

from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity, DataUpdateCoordinator

from .const import SENSOR_TYPE_RAINDELAY, SENSOR_TYPE_RAINSENSOR
from .const import (
DEVICE_INFO,
DOMAIN,
SENSOR_TYPE_RAINDELAY,
SENSOR_TYPE_RAINSENSOR,
SERIAL_NUMBER,
)
from .coordinator import RainbirdUpdateCoordinator


_LOGGER = logging.getLogger(__name__)


BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key=SENSOR_TYPE_RAINSENSOR,
name="Rainsensor",
Expand All @@ -33,22 +43,24 @@
)


async def async_setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
config_entry: ConfigEntry,
async_add_devices: AddEntitiesCallback,
Comment thread
allenporter marked this conversation as resolved.
Outdated
) -> None:
"""Set up a Rain Bird sensor."""
if discovery_info is None:
return

async_add_entities(
[
RainBirdSensor(discovery_info[description.key], description)
for description in BINARY_SENSOR_TYPES
"""Set up entry for a Rain Bird binary_sensor."""
data = hass.data[DOMAIN][config_entry.entry_id]
await asyncio.gather(
*[
data[description.key].async_config_entry_first_refresh()
Comment thread
allenporter marked this conversation as resolved.
Outdated
for description in SENSOR_TYPES
],
True,
)
async_add_devices(
RainBirdSensor(
data[description.key], description, data[SERIAL_NUMBER], data[DEVICE_INFO]
)
for description in SENSOR_TYPES
)


Expand All @@ -61,10 +73,14 @@ def __init__(
self,
coordinator: RainbirdUpdateCoordinator[int | bool],
description: BinarySensorEntityDescription,
serial_number: str,
device_info: DeviceInfo,
) -> None:
"""Initialize the Rain Bird sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{serial_number}-{description.key}"
self._attr_device_info = device_info

@property
def is_on(self) -> bool | None:
Expand Down
Loading