diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index af2bb92bce5..7b41a3f2f5e 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( ) 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 @@ -24,7 +25,7 @@ from homeassistant.helpers.typing import ConfigType 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__) @@ -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( diff --git a/homeassistant/components/rainbird/number.py b/homeassistant/components/rainbird/number.py new file mode 100644 index 00000000000..ac1ea961870 --- /dev/null +++ b/homeassistant/components/rainbird/number.py @@ -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) diff --git a/homeassistant/components/rainbird/strings.json b/homeassistant/components/rainbird/strings.json index 642612b11d2..f950146f160 100644 --- a/homeassistant/components/rainbird/strings.json +++ b/homeassistant/components/rainbird/strings.json @@ -32,6 +32,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." + } + } + } } } } diff --git a/homeassistant/components/rainbird/translations/en.json b/homeassistant/components/rainbird/translations/en.json index 6e6d014f8f4..86fafc8b771 100644 --- a/homeassistant/components/rainbird/translations/en.json +++ b/homeassistant/components/rainbird/translations/en.json @@ -19,6 +19,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" diff --git a/tests/components/rainbird/test_init.py b/tests/components/rainbird/test_init.py index 7a8eb17bf1d..e8e9f76d312 100644 --- a/tests/components/rainbird/test_init.py +++ b/tests/components/rainbird/test_init.py @@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigEntryState 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, @@ -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, @@ -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, diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py new file mode 100644 index 00000000000..e5480da6ee3 --- /dev/null +++ b/tests/components/rainbird/test_number.py @@ -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