Add config flow for Rain Bird (#85271)

* Rainbird config flow

Convert rainbird to a config flow. Still need to handle irrigation numbers.

* Add options for irrigation time and deprecate yaml

* Combine exception handling paths to get 100% test coverage

* Bump the rainird config deprecation release

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Remove unnecessary sensor/binary sensor and address some PR feedback

* Simplify configuration flow and options based on PR feedback

* Consolidate data update coordinators to simplify overall integration

* Fix type error on python3.9

* Handle yaml name import

* Fix naming import post serialization

* Parallelize requests to the device

* Complete conversion to entity service

* Update homeassistant/components/rainbird/switch.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/rainbird/config_flow.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Remove unused import

* Set default duration in options used in tests

* Add separate devices for each sprinkler zone and update service to use config entry

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Allen Porter
2023-01-07 09:34:01 -08:00
committed by GitHub
co-authored by Martin Hjelmare
parent e3e64c103d
commit 5000c426c6
19 changed files with 1016 additions and 471 deletions
+93 -4
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable, Generator
from http import HTTPStatus
from typing import Any
from unittest.mock import patch
@@ -10,10 +11,15 @@ from pyrainbird import encryption
import pytest
from homeassistant.components.rainbird import DOMAIN
from homeassistant.components.rainbird.const import (
ATTR_DURATION,
DEFAULT_TRIGGER_TIME_MINUTES,
)
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
ComponentSetup = Callable[[], Awaitable[bool]]
@@ -21,6 +27,7 @@ ComponentSetup = Callable[[], Awaitable[bool]]
HOST = "example.com"
URL = "http://example.com/stick"
PASSWORD = "password"
SERIAL_NUMBER = 0x12635436566
#
# Response payloads below come from pyrainbird test cases.
@@ -45,14 +52,28 @@ RAIN_DELAY_OFF = "B60000"
# ACK command 0x10, Echo 0x06
ACK_ECHO = "0106"
CONFIG = {
DOMAIN: {
"host": HOST,
"password": PASSWORD,
"trigger_time": 360,
"trigger_time": {
"minutes": 6,
},
}
}
CONFIG_ENTRY_DATA = {
"host": HOST,
"password": PASSWORD,
"serial_number": SERIAL_NUMBER,
}
UNAVAILABLE_RESPONSE = AiohttpClientMockResponse(
"POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE
)
@pytest.fixture
def platforms() -> list[Platform]:
@@ -63,7 +84,37 @@ def platforms() -> list[Platform]:
@pytest.fixture
def yaml_config() -> dict[str, Any]:
"""Fixture for configuration.yaml."""
return CONFIG
return {}
@pytest.fixture
async def config_entry_data() -> dict[str, Any]:
"""Fixture for MockConfigEntry data."""
return CONFIG_ENTRY_DATA
@pytest.fixture
async def config_entry(
config_entry_data: dict[str, Any] | None
) -> MockConfigEntry | None:
"""Fixture for MockConfigEntry."""
if config_entry_data is None:
return None
return MockConfigEntry(
unique_id=SERIAL_NUMBER,
domain=DOMAIN,
data=config_entry_data,
options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES},
)
@pytest.fixture(autouse=True)
async def add_config_entry(
hass: HomeAssistant, config_entry: MockConfigEntry | None
) -> None:
"""Fixture to add the config entry."""
if config_entry:
config_entry.add_to_hass(hass)
@pytest.fixture
@@ -97,10 +148,48 @@ def mock_response(data: str) -> AiohttpClientMockResponse:
return AiohttpClientMockResponse("POST", URL, response=rainbird_response(data))
@pytest.fixture(name="stations_response")
def mock_station_response() -> str:
"""Mock response to return available stations."""
return AVAILABLE_STATIONS_RESPONSE
@pytest.fixture(name="zone_state_response")
def mock_zone_state_response() -> str:
"""Mock response to return zone states."""
return ZONE_STATE_OFF_RESPONSE
@pytest.fixture(name="rain_response")
def mock_rain_response() -> str:
"""Mock response to return rain sensor state."""
return RAIN_SENSOR_OFF
@pytest.fixture(name="rain_delay_response")
def mock_rain_delay_response() -> str:
"""Mock response to return rain delay state."""
return RAIN_DELAY_OFF
@pytest.fixture(name="api_responses")
def mock_api_responses(
stations_response: str,
zone_state_response: str,
rain_response: str,
rain_delay_response: str,
) -> list[str]:
"""Fixture to set up a list of fake API responsees for tests to extend.
These are returned in the order they are requested by the update coordinator.
"""
return [stations_response, zone_state_response, rain_response, rain_delay_response]
@pytest.fixture(name="responses")
def mock_responses() -> list[AiohttpClientMockResponse]:
def mock_responses(api_responses: list[str]) -> list[AiohttpClientMockResponse]:
"""Fixture to set up a list of fake API responsees for tests to extend."""
return [mock_response(SERIAL_RESPONSE)]
return [mock_response(api_response) for api_response in api_responses]
@pytest.fixture(autouse=True)
@@ -6,14 +6,7 @@ import pytest
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .conftest import (
RAIN_DELAY,
RAIN_DELAY_OFF,
RAIN_SENSOR_OFF,
RAIN_SENSOR_ON,
ComponentSetup,
mock_response,
)
from .conftest import RAIN_SENSOR_OFF, RAIN_SENSOR_ON, ComponentSetup
from tests.test_util.aiohttp import AiohttpClientMockResponse
@@ -25,54 +18,23 @@ def platforms() -> list[Platform]:
@pytest.mark.parametrize(
"sensor_payload,expected_state",
"rain_response,expected_state",
[(RAIN_SENSOR_OFF, "off"), (RAIN_SENSOR_ON, "on")],
)
async def test_rainsensor(
hass: HomeAssistant,
setup_integration: ComponentSetup,
responses: list[AiohttpClientMockResponse],
sensor_payload: str,
expected_state: bool,
) -> None:
"""Test rainsensor binary sensor."""
responses.extend(
[
mock_response(sensor_payload),
mock_response(RAIN_DELAY),
]
)
assert await setup_integration()
rainsensor = hass.states.get("binary_sensor.rainsensor")
assert rainsensor is not None
assert rainsensor.state == expected_state
@pytest.mark.parametrize(
"sensor_payload,expected_state",
[(RAIN_DELAY_OFF, "off"), (RAIN_DELAY, "on")],
)
async def test_raindelay(
hass: HomeAssistant,
setup_integration: ComponentSetup,
responses: list[AiohttpClientMockResponse],
sensor_payload: str,
expected_state: bool,
) -> None:
"""Test raindelay binary sensor."""
responses.extend(
[
mock_response(RAIN_SENSOR_OFF),
mock_response(sensor_payload),
]
)
assert await setup_integration()
raindelay = hass.states.get("binary_sensor.raindelay")
assert raindelay is not None
assert raindelay.state == expected_state
assert rainsensor.attributes == {
"friendly_name": "Rainsensor",
"icon": "mdi:water",
}
@@ -0,0 +1,149 @@
"""Tests for the Rain Bird config flow."""
import asyncio
from collections.abc import Generator
from http import HTTPStatus
from unittest.mock import Mock, patch
import pytest
from homeassistant import config_entries
from homeassistant.components.rainbird import DOMAIN
from homeassistant.components.rainbird.const import ATTR_DURATION
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult, FlowResultType
from .conftest import (
CONFIG_ENTRY_DATA,
HOST,
PASSWORD,
SERIAL_RESPONSE,
URL,
mock_response,
)
from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
@pytest.fixture(name="responses")
def mock_responses() -> list[AiohttpClientMockResponse]:
"""Set up fake serial number response when testing the connection."""
return [mock_response(SERIAL_RESPONSE)]
@pytest.fixture(autouse=True)
async def config_entry_data() -> None:
"""Fixture to disable config entry setup for exercising config flow."""
return None
@pytest.fixture(autouse=True)
async def mock_setup() -> Generator[Mock, None, None]:
"""Fixture for patching out integration setup."""
with patch(
"homeassistant.components.rainbird.async_setup_entry",
return_value=True,
) as mock_setup:
yield mock_setup
async def complete_flow(hass: HomeAssistant) -> FlowResult:
"""Start the config flow and enter the host and password."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result.get("type") == FlowResultType.FORM
assert result.get("step_id") == "user"
assert not result.get("errors")
assert "flow_id" in result
return await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: HOST, CONF_PASSWORD: PASSWORD},
)
async def test_controller_flow(hass: HomeAssistant, mock_setup: Mock) -> None:
"""Test the controller is setup correctly."""
result = await complete_flow(hass)
assert result.get("type") == "create_entry"
assert result.get("title") == HOST
assert "result" in result
assert result["result"].data == CONFIG_ENTRY_DATA
assert result["result"].options == {ATTR_DURATION: 6}
assert len(mock_setup.mock_calls) == 1
async def test_controller_cannot_connect(
hass: HomeAssistant,
mock_setup: Mock,
responses: list[AiohttpClientMockResponse],
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test an error talking to the controller."""
# Controller response with a failure
responses.clear()
responses.append(
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE)
)
result = await complete_flow(hass)
assert result.get("type") == FlowResultType.FORM
assert result.get("step_id") == "user"
assert result.get("errors") == {"base": "cannot_connect"}
assert not mock_setup.mock_calls
async def test_controller_timeout(
hass: HomeAssistant,
mock_setup: Mock,
) -> None:
"""Test an error talking to the controller."""
with patch(
"homeassistant.components.rainbird.config_flow.async_timeout.timeout",
side_effect=asyncio.TimeoutError,
):
result = await complete_flow(hass)
assert result.get("type") == FlowResultType.FORM
assert result.get("step_id") == "user"
assert result.get("errors") == {"base": "timeout_connect"}
assert not mock_setup.mock_calls
async def test_options_flow(hass: HomeAssistant, mock_setup: Mock) -> None:
"""Test config flow options."""
# Setup config flow
result = await complete_flow(hass)
assert result.get("type") == "create_entry"
assert result.get("title") == HOST
assert "result" in result
assert result["result"].data == CONFIG_ENTRY_DATA
assert result["result"].options == {ATTR_DURATION: 6}
# Assert single config entry is loaded
config_entry = next(iter(hass.config_entries.async_entries(DOMAIN)))
assert config_entry.state == ConfigEntryState.LOADED
# Initiate the options flow
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result.get("type") == FlowResultType.FORM
assert result.get("step_id") == "init"
# Change the default duration
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={ATTR_DURATION: 5}
)
assert result.get("type") == FlowResultType.CREATE_ENTRY
assert config_entry.options == {
ATTR_DURATION: 5,
}
+138 -17
View File
@@ -1,34 +1,155 @@
"""Tests for rainbird initialization."""
from http import HTTPStatus
from __future__ import annotations
import pytest
from homeassistant.components.rainbird import DOMAIN
from homeassistant.components.rainbird.const import ATTR_CONFIG_ENTRY_ID, ATTR_DURATION
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 .conftest import URL, ComponentSetup
from .conftest import (
ACK_ECHO,
CONFIG,
CONFIG_ENTRY_DATA,
SERIAL_NUMBER,
SERIAL_RESPONSE,
UNAVAILABLE_RESPONSE,
ComponentSetup,
mock_response,
)
from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
async def test_setup_success(
hass: HomeAssistant,
setup_integration: ComponentSetup,
) -> None:
"""Test successful setup and unload."""
assert await setup_integration()
async def test_setup_communication_failure(
@pytest.mark.parametrize(
"yaml_config,config_entry_data,initial_response",
[
({}, CONFIG_ENTRY_DATA, None),
(
CONFIG,
None,
mock_response(SERIAL_RESPONSE), # Extra import request
),
(
CONFIG,
CONFIG_ENTRY_DATA,
None,
),
],
ids=["config_entry", "yaml", "already_exists"],
)
async def test_init_success(
hass: HomeAssistant,
setup_integration: ComponentSetup,
responses: list[AiohttpClientMockResponse],
aioclient_mock: AiohttpClientMocker,
initial_response: AiohttpClientMockResponse | None,
) -> None:
"""Test successful setup and unload."""
if initial_response:
responses.insert(0, initial_response)
assert await setup_integration()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state == ConfigEntryState.LOADED
await hass.config_entries.async_unload(entries[0].entry_id)
await hass.async_block_till_done()
assert entries[0].state is ConfigEntryState.NOT_LOADED
@pytest.mark.parametrize(
"yaml_config,config_entry_data,responses,config_entry_states",
[
({}, CONFIG_ENTRY_DATA, [UNAVAILABLE_RESPONSE], [ConfigEntryState.SETUP_RETRY]),
(
CONFIG,
None,
[
UNAVAILABLE_RESPONSE, # Failure when importing yaml
],
[],
),
(
CONFIG,
None,
[
mock_response(SERIAL_RESPONSE), # Import succeeds
UNAVAILABLE_RESPONSE, # Failure on integration setup
],
[ConfigEntryState.SETUP_RETRY],
),
],
ids=["config_entry_failure", "yaml_import_failure", "yaml_init_failure"],
)
async def test_communication_failure(
hass: HomeAssistant,
setup_integration: ComponentSetup,
config_entry_states: list[ConfigEntryState],
) -> None:
"""Test unable to talk to server on startup, which permanently fails setup."""
responses.clear()
responses.append(
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE)
assert await setup_integration()
assert [
entry.state for entry in hass.config_entries.async_entries(DOMAIN)
] == config_entry_states
@pytest.mark.parametrize("platforms", [[Platform.SENSOR]])
async def test_rain_delay_service(
hass: HomeAssistant,
setup_integration: ComponentSetup,
aioclient_mock: AiohttpClientMocker,
responses: list[str],
config_entry: ConfigEntry,
) -> None:
"""Test calling the rain delay service."""
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(
DOMAIN,
"set_rain_delay",
{ATTR_CONFIG_ENTRY_ID: config_entry.entry_id, ATTR_DURATION: 3},
blocking=True,
)
assert not await setup_integration()
assert len(aioclient_mock.mock_calls) == 1
async def test_rain_delay_invalid_config_entry(
hass: HomeAssistant,
setup_integration: ComponentSetup,
aioclient_mock: AiohttpClientMocker,
config_entry: ConfigEntry,
) -> None:
"""Test calling the rain delay service."""
assert await setup_integration()
aioclient_mock.mock_calls.clear()
with pytest.raises(HomeAssistantError, match="Config entry id does not exist"):
await hass.services.async_call(
DOMAIN,
"set_rain_delay",
{ATTR_CONFIG_ENTRY_ID: "invalid", ATTR_DURATION: 3},
blocking=True,
)
assert len(aioclient_mock.mock_calls) == 0
+9 -21
View File
@@ -6,15 +6,7 @@ import pytest
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .conftest import (
RAIN_DELAY,
RAIN_SENSOR_OFF,
RAIN_SENSOR_ON,
ComponentSetup,
mock_response,
)
from tests.test_util.aiohttp import AiohttpClientMockResponse
from .conftest import RAIN_DELAY, RAIN_DELAY_OFF, ComponentSetup
@pytest.fixture
@@ -24,26 +16,22 @@ def platforms() -> list[str]:
@pytest.mark.parametrize(
"sensor_payload,expected_state",
[(RAIN_SENSOR_OFF, "False"), (RAIN_SENSOR_ON, "True")],
"rain_delay_response,expected_state",
[(RAIN_DELAY, "16"), (RAIN_DELAY_OFF, "0")],
)
async def test_sensors(
hass: HomeAssistant,
setup_integration: ComponentSetup,
responses: list[AiohttpClientMockResponse],
sensor_payload: str,
expected_state: bool,
expected_state: str,
) -> None:
"""Test sensor platform."""
responses.extend([mock_response(sensor_payload), mock_response(RAIN_DELAY)])
assert await setup_integration()
rainsensor = hass.states.get("sensor.rainsensor")
assert rainsensor is not None
assert rainsensor.state == expected_state
raindelay = hass.states.get("sensor.raindelay")
assert raindelay is not None
assert raindelay.state == "16"
assert raindelay.state == expected_state
assert raindelay.attributes == {
"friendly_name": "Raindelay",
"icon": "mdi:water-off",
}
+87 -139
View File
@@ -1,9 +1,6 @@
"""Tests for rainbird sensor platform."""
from http import HTTPStatus
import logging
import pytest
from homeassistant.components.rainbird import DOMAIN
@@ -12,11 +9,12 @@ from homeassistant.core import HomeAssistant
from .conftest import (
ACK_ECHO,
AVAILABLE_STATIONS_RESPONSE,
EMPTY_STATIONS_RESPONSE,
HOST,
PASSWORD,
URL,
RAIN_DELAY_OFF,
RAIN_SENSOR_OFF,
SERIAL_RESPONSE,
ZONE_3_ON_RESPONSE,
ZONE_5_ON_RESPONSE,
ZONE_OFF_RESPONSE,
@@ -34,20 +32,26 @@ def platforms() -> list[str]:
return [Platform.SWITCH]
@pytest.mark.parametrize(
"stations_response",
[EMPTY_STATIONS_RESPONSE],
)
async def test_no_zones(
hass: HomeAssistant,
setup_integration: ComponentSetup,
responses: list[AiohttpClientMockResponse],
) -> None:
"""Test case where listing stations returns no stations."""
responses.append(mock_response(EMPTY_STATIONS_RESPONSE))
assert await setup_integration()
zone = hass.states.get("switch.sprinkler_1")
zone = hass.states.get("switch.rain_bird_sprinkler_1")
assert zone is None
@pytest.mark.parametrize(
"zone_state_response",
[ZONE_5_ON_RESPONSE],
)
async def test_zones(
hass: HomeAssistant,
setup_integration: ComponentSetup,
@@ -55,41 +59,45 @@ async def test_zones(
) -> None:
"""Test switch platform with fake data that creates 7 zones with one enabled."""
responses.extend(
[mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_5_ON_RESPONSE)]
)
assert await setup_integration()
zone = hass.states.get("switch.sprinkler_1")
zone = hass.states.get("switch.rain_bird_sprinkler_1")
assert zone is not None
assert zone.state == "off"
assert zone.attributes == {
"friendly_name": "Rain Bird Sprinkler 1",
"zone": 1,
}
zone = hass.states.get("switch.rain_bird_sprinkler_2")
assert zone is not None
assert zone.state == "off"
assert zone.attributes == {
"friendly_name": "Rain Bird Sprinkler 2",
"zone": 2,
}
zone = hass.states.get("switch.rain_bird_sprinkler_3")
assert zone is not None
assert zone.state == "off"
zone = hass.states.get("switch.sprinkler_2")
zone = hass.states.get("switch.rain_bird_sprinkler_4")
assert zone is not None
assert zone.state == "off"
zone = hass.states.get("switch.sprinkler_3")
assert zone is not None
assert zone.state == "off"
zone = hass.states.get("switch.sprinkler_4")
assert zone is not None
assert zone.state == "off"
zone = hass.states.get("switch.sprinkler_5")
zone = hass.states.get("switch.rain_bird_sprinkler_5")
assert zone is not None
assert zone.state == "on"
zone = hass.states.get("switch.sprinkler_6")
zone = hass.states.get("switch.rain_bird_sprinkler_6")
assert zone is not None
assert zone.state == "off"
zone = hass.states.get("switch.sprinkler_7")
zone = hass.states.get("switch.rain_bird_sprinkler_7")
assert zone is not None
assert zone.state == "off"
assert not hass.states.get("switch.sprinkler_8")
assert not hass.states.get("switch.rain_bird_sprinkler_8")
async def test_switch_on(
@@ -100,14 +108,11 @@ async def test_switch_on(
) -> None:
"""Test turning on irrigation switch."""
responses.extend(
[mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_OFF_RESPONSE)]
)
assert await setup_integration()
# Initially all zones are off. Pick zone3 as an arbitrary to assert
# state, then update below as a switch.
zone = hass.states.get("switch.sprinkler_3")
zone = hass.states.get("switch.rain_bird_sprinkler_3")
assert zone is not None
assert zone.state == "off"
@@ -115,20 +120,25 @@ async def test_switch_on(
responses.extend(
[
mock_response(ACK_ECHO), # Switch on response
mock_response(ZONE_3_ON_RESPONSE), # Updated zone state
# API responses when state is refreshed
mock_response(ZONE_3_ON_RESPONSE),
mock_response(RAIN_SENSOR_OFF),
mock_response(RAIN_DELAY_OFF),
]
)
await switch_common.async_turn_on(hass, "switch.sprinkler_3")
await switch_common.async_turn_on(hass, "switch.rain_bird_sprinkler_3")
await hass.async_block_till_done()
assert len(aioclient_mock.mock_calls) == 2
aioclient_mock.mock_calls.clear()
# Verify switch state is updated
zone = hass.states.get("switch.sprinkler_3")
zone = hass.states.get("switch.rain_bird_sprinkler_3")
assert zone is not None
assert zone.state == "on"
@pytest.mark.parametrize(
"zone_state_response",
[ZONE_3_ON_RESPONSE],
)
async def test_switch_off(
hass: HomeAssistant,
setup_integration: ComponentSetup,
@@ -137,13 +147,10 @@ async def test_switch_off(
) -> None:
"""Test turning off irrigation switch."""
responses.extend(
[mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_3_ON_RESPONSE)]
)
assert await setup_integration()
# Initially the test zone is on
zone = hass.states.get("switch.sprinkler_3")
zone = hass.states.get("switch.rain_bird_sprinkler_3")
assert zone is not None
assert zone.state == "on"
@@ -152,16 +159,15 @@ async def test_switch_off(
[
mock_response(ACK_ECHO), # Switch off response
mock_response(ZONE_OFF_RESPONSE), # Updated zone state
mock_response(RAIN_SENSOR_OFF),
mock_response(RAIN_DELAY_OFF),
]
)
await switch_common.async_turn_off(hass, "switch.sprinkler_3")
await switch_common.async_turn_off(hass, "switch.rain_bird_sprinkler_3")
await hass.async_block_till_done()
# One call to change the service and one to refresh state
assert len(aioclient_mock.mock_calls) == 2
# Verify switch state is updated
zone = hass.states.get("switch.sprinkler_3")
zone = hass.states.get("switch.rain_bird_sprinkler_3")
assert zone is not None
assert zone.state == "off"
@@ -171,114 +177,60 @@ async def test_irrigation_service(
setup_integration: ComponentSetup,
aioclient_mock: AiohttpClientMocker,
responses: list[AiohttpClientMockResponse],
api_responses: list[str],
) -> None:
"""Test calling the irrigation service."""
responses.extend(
[mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_3_ON_RESPONSE)]
)
assert await setup_integration()
aioclient_mock.mock_calls.clear()
responses.extend([mock_response(ACK_ECHO), mock_response(ZONE_OFF_RESPONSE)])
await hass.services.async_call(
DOMAIN,
"start_irrigation",
{ATTR_ENTITY_ID: "switch.sprinkler_5", "duration": 30},
blocking=True,
)
# One call to change the service and one to refresh state
assert len(aioclient_mock.mock_calls) == 2
async def test_rain_delay_service(
hass: HomeAssistant,
setup_integration: ComponentSetup,
aioclient_mock: AiohttpClientMocker,
responses: list[AiohttpClientMockResponse],
) -> None:
"""Test calling the rain delay service."""
responses.extend(
[mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_3_ON_RESPONSE)]
)
assert await setup_integration()
zone = hass.states.get("switch.rain_bird_sprinkler_3")
assert zone is not None
assert zone.state == "off"
aioclient_mock.mock_calls.clear()
responses.extend(
[
mock_response(ACK_ECHO),
# API responses when state is refreshed
mock_response(ZONE_3_ON_RESPONSE),
mock_response(RAIN_SENSOR_OFF),
mock_response(RAIN_DELAY_OFF),
]
)
await hass.services.async_call(
DOMAIN, "set_rain_delay", {"duration": 30}, blocking=True
DOMAIN,
"start_irrigation",
{ATTR_ENTITY_ID: "switch.rain_bird_sprinkler_3", "duration": 30},
blocking=True,
)
assert len(aioclient_mock.mock_calls) == 1
async def test_platform_unavailable(
hass: HomeAssistant,
setup_integration: ComponentSetup,
responses: list[AiohttpClientMockResponse],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test failure while listing the stations when setting up the platform."""
responses.append(
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE)
)
with caplog.at_level(logging.WARNING):
assert await setup_integration()
assert "Failed to get stations" in caplog.text
async def test_coordinator_unavailable(
hass: HomeAssistant,
setup_integration: ComponentSetup,
responses: list[AiohttpClientMockResponse],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test failure to refresh the update coordinator."""
responses.extend(
[
mock_response(AVAILABLE_STATIONS_RESPONSE),
AiohttpClientMockResponse(
"POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE
),
],
)
with caplog.at_level(logging.WARNING):
assert await setup_integration()
assert "Failed to load zone state" in caplog.text
zone = hass.states.get("switch.rain_bird_sprinkler_3")
assert zone is not None
assert zone.state == "on"
@pytest.mark.parametrize(
"yaml_config",
"yaml_config,config_entry_data",
[
{
DOMAIN: {
"host": HOST,
"password": PASSWORD,
"trigger_time": 360,
"zones": {
1: {
"friendly_name": "Garden Sprinkler",
(
{
DOMAIN: {
"host": HOST,
"password": PASSWORD,
"trigger_time": 360,
"zones": {
1: {
"friendly_name": "Garden Sprinkler",
},
2: {
"friendly_name": "Back Yard",
},
},
2: {
"friendly_name": "Back Yard",
},
},
}
},
}
},
None,
)
],
)
async def test_yaml_config(
@@ -287,15 +239,11 @@ async def test_yaml_config(
responses: list[AiohttpClientMockResponse],
) -> None:
"""Test switch platform with fake data that creates 7 zones with one enabled."""
responses.extend(
[mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_5_ON_RESPONSE)]
)
responses.insert(0, mock_response(SERIAL_RESPONSE)) # Extra import request
assert await setup_integration()
assert hass.states.get("switch.garden_sprinkler")
assert not hass.states.get("switch.sprinkler_1")
assert not hass.states.get("switch.rain_bird_sprinkler_1")
assert hass.states.get("switch.back_yard")
assert not hass.states.get("switch.sprinkler_2")
assert hass.states.get("switch.sprinkler_3")
assert not hass.states.get("switch.rain_bird_sprinkler_2")
assert hass.states.get("switch.rain_bird_sprinkler_3")