Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 24 additions & 1 deletion homeassistant/components/rainbird/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
Expand All @@ -24,7 +25,7 @@
from .const import ATTR_CONFIG_ENTRY_ID, ATTR_DURATION, CONF_SERIAL_NUMBER, CONF_ZONES
from .coordinator import RainbirdUpdateCoordinator

PLATFORMS = [Platform.SWITCH, Platform.SENSOR, Platform.BINARY_SENSOR]
PLATFORMS = [Platform.SWITCH, Platform.SENSOR, Platform.BINARY_SENSOR, Platform.NUMBER]

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -117,11 +118,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

async def set_rain_delay(call: ServiceCall) -> None:
"""Service call to delay automatic irrigigation."""

entry_id = call.data[ATTR_CONFIG_ENTRY_ID]
duration = call.data[ATTR_DURATION]
if entry_id not in hass.data[DOMAIN]:
raise HomeAssistantError(f"Config entry id does not exist: {entry_id}")
coordinator = hass.data[DOMAIN][entry_id]

entity_registry = er.async_get(hass)
entity_ids = (
entry.entity_id
for entry in er.async_entries_for_config_entry(entity_registry, entry_id)
if entry.unique_id == f"{coordinator.serial_number}-rain-delay"
)
async_create_issue(
hass,
DOMAIN,
"deprecated_raindelay",
breaks_in_ha_version="2023.4.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_raindelay",
translation_placeholders={
"alternate_target": next(entity_ids, "unknown"),
},
)

await coordinator.controller.set_rain_delay(duration)

hass.services.async_register(
Expand Down
61 changes: 61 additions & 0 deletions homeassistant/components/rainbird/number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""The number platform for rainbird."""
from __future__ import annotations

import logging

from homeassistant.components.number import NumberEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import RainbirdUpdateCoordinator

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entry for a Rain Bird number platform."""
async_add_entities(
[
RainDelayNumber(
hass.data[DOMAIN][config_entry.entry_id],
)
]
)


class RainDelayNumber(CoordinatorEntity[RainbirdUpdateCoordinator], NumberEntity):
"""A number implemnetaiton for the rain delay."""

_attr_native_min_value = 0
_attr_native_max_value = 14
_attr_native_step = 1
_attr_native_unit_of_measurement = UnitOfTime.DAYS
_attr_icon = "mdi:water-off"
_attr_name = "Rain delay"
_attr_has_entity_name = True

def __init__(
self,
coordinator: RainbirdUpdateCoordinator,
) -> None:
"""Initialize the Rain Bird sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.serial_number}-rain-delay"
self._attr_device_info = coordinator.device_info

@property
def native_value(self) -> float | None:
"""Return the value reported by the sensor."""
return self.coordinator.data.rain_delay

async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
await self.coordinator.controller.set_rain_delay(value)
11 changes: 11 additions & 0 deletions homeassistant/components/rainbird/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@
"deprecated_yaml": {
"title": "The Rain Bird YAML configuration is being removed",
"description": "Configuring Rain Bird in configuration.yaml is being removed in Home Assistant 2023.4.\n\nYour configuration has been imported into the UI automatically, however default per-zone irrigation times are no longer supported. Remove the Rain Bird YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
},
"deprecated_raindelay": {
"title": "The Rain Bird Rain Delay Service is being removed",
"fix_flow": {
"step": {
"confirm": {
"title": "The Rain Bird Rain Delay Service is being removed",
"description": "The Rain Bird service `rainbird.set_rain_delay` is being removed and replaced by a Number entity for managing the rain delay. Any existing automations or scripts will need to be updated to use `number.set_value` with a target of `{alternate_target}` instead."
}
}
}
}
}
}
11 changes: 11 additions & 0 deletions homeassistant/components/rainbird/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@
}
},
"issues": {
"deprecated_raindelay": {
"fix_flow": {
"step": {
"confirm": {
"description": "The Rain Bird service `rainbird.set_rain_delay` is being removed and replaced by a Number entity for managing the rain delay. Any existing automations or scripts will need to be updated to use `number.set_value` with a target of `{alternate_target}` instead.",
"title": "The Rain Bird Rain Delay Service is being removed"
}
}
},
"title": "The Rain Bird Rain Delay Service is being removed"
},
"deprecated_yaml": {
"description": "Configuring Rain Bird in configuration.yaml is being removed in Home Assistant 2023.4.\n\nYour configuration has been imported into the UI automatically, however default per-zone irrigation times are no longer supported. Remove the Rain Bird YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
"title": "The Rain Bird YAML configuration is being removed"
Expand Down
13 changes: 11 additions & 2 deletions tests/components/rainbird/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, issue_registry as ir

from .conftest import (
ACK_ECHO,
Expand Down Expand Up @@ -102,7 +102,7 @@ async def test_communication_failure(
] == config_entry_states


@pytest.mark.parametrize("platforms", [[Platform.SENSOR]])
@pytest.mark.parametrize("platforms", [[Platform.NUMBER, Platform.SENSOR]])
async def test_rain_delay_service(
hass: HomeAssistant,
setup_integration: ComponentSetup,
Expand Down Expand Up @@ -131,6 +131,15 @@ async def test_rain_delay_service(

assert len(aioclient_mock.mock_calls) == 1

issue_registry: ir.IssueRegistry = ir.async_get(hass)
issue = issue_registry.async_get_issue(
domain=DOMAIN, issue_id="deprecated_raindelay"
)
assert issue
assert issue.translation_placeholders == {
"alternate_target": "number.rain_bird_controller_rain_delay"
}


async def test_rain_delay_invalid_config_entry(
hass: HomeAssistant,
Expand Down
87 changes: 87 additions & 0 deletions tests/components/rainbird/test_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Tests for rainbird number platform."""


import pytest

from homeassistant.components import number
from homeassistant.components.rainbird import DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr

from .conftest import (
ACK_ECHO,
RAIN_DELAY,
RAIN_DELAY_OFF,
SERIAL_NUMBER,
ComponentSetup,
mock_response,
)

from tests.test_util.aiohttp import AiohttpClientMocker


@pytest.fixture
def platforms() -> list[str]:
"""Fixture to specify platforms to test."""
return [Platform.NUMBER]


@pytest.mark.parametrize(
"rain_delay_response,expected_state",
[(RAIN_DELAY, "16"), (RAIN_DELAY_OFF, "0")],
)
async def test_number_values(
hass: HomeAssistant,
setup_integration: ComponentSetup,
expected_state: str,
) -> None:
"""Test sensor platform."""

assert await setup_integration()

raindelay = hass.states.get("number.rain_bird_controller_rain_delay")
assert raindelay is not None
assert raindelay.state == expected_state
assert raindelay.attributes == {
"friendly_name": "Rain Bird Controller Rain delay",
"icon": "mdi:water-off",
"min": 0,
"max": 14,
"mode": "auto",
"step": 1,
"unit_of_measurement": "d",
}


async def test_set_value(
hass: HomeAssistant,
setup_integration: ComponentSetup,
aioclient_mock: AiohttpClientMocker,
responses: list[str],
config_entry: ConfigEntry,
) -> None:
"""Test setting the rain delay number."""

assert await setup_integration()

device_registry = dr.async_get(hass)
device = device_registry.async_get_device({(DOMAIN, SERIAL_NUMBER)})
assert device
assert device.name == "Rain Bird Controller"

aioclient_mock.mock_calls.clear()
responses.append(mock_response(ACK_ECHO))

await hass.services.async_call(
number.DOMAIN,
number.SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: "number.rain_bird_controller_rain_delay",
number.ATTR_VALUE: 3,
},
blocking=True,
)

assert len(aioclient_mock.mock_calls) == 1