mirror of
https://github.com/home-assistant/core.git
synced 2026-09-12 02:34:48 -05:00
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:
co-authored by
Martin Hjelmare
parent
e3e64c103d
commit
5000c426c6
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user