"""Test fixtures for rainbird.""" from __future__ import annotations from collections.abc import Generator from http import HTTPStatus import json from typing import Any from unittest.mock import patch 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 EVENT_HOMEASSISTANT_CLOSE, Platform from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse HOST = "example.com" URL = "http://example.com/stick" PASSWORD = "password" SERIAL_NUMBER = 0x12635436566 MAC_ADDRESS = "4C:A1:61:00:11:22" MAC_ADDRESS_UNIQUE_ID = "4c:a1:61:00:11:22" # # Response payloads below come from pyrainbird test cases. # # Get serial number Command 0x85. Serial is 0x12635436566 SERIAL_RESPONSE = "850000012635436566" ZERO_SERIAL_RESPONSE = "850000000000000000" # Model and version command 0x82 MODEL_AND_VERSION_RESPONSE = "820005090C" # ESP-TM2 # Get available stations command 0x83 AVAILABLE_STATIONS_RESPONSE = "83017F000000" # Mask for 7 zones EMPTY_STATIONS_RESPONSE = "830000000000" # Get zone state command 0xBF. ZONE_3_ON_RESPONSE = "BF0004000000" # Zone 3 is on ZONE_5_ON_RESPONSE = "BF0010000000" # Zone 5 is on ZONE_OFF_RESPONSE = "BF0000000000" # All zones off ZONE_STATE_OFF_RESPONSE = "BF0000000000" # Get rain sensor state command 0XBE RAIN_SENSOR_OFF = "BE00" RAIN_SENSOR_ON = "BE01" # Get rain delay command 0xB6 RAIN_DELAY = "B60010" # 0x10 is 16 RAIN_DELAY_OFF = "B60000" # ACK command 0x10, Echo 0x06 ACK_ECHO = "0106" WIFI_PARAMS_RESPONSE = { "macAddress": MAC_ADDRESS, "localIpAddress": "1.1.1.38", "localNetmask": "255.255.255.0", "localGateway": "1.1.1.1", "rssi": -61, "wifiSsid": "wifi-ssid-name", "wifiPassword": "wifi-password-name", "wifiSecurity": "wpa2-aes", "apTimeoutNoLan": 20, "apTimeoutIdle": 20, "apSecurity": "unknown", "stickVersion": "Rain Bird Stick Rev C/1.63", } CONFIG = { DOMAIN: { "host": HOST, "password": PASSWORD, "trigger_time": { "minutes": 6, }, } } CONFIG_ENTRY_DATA_OLD_FORMAT = { "host": HOST, "password": PASSWORD, "serial_number": SERIAL_NUMBER, } CONFIG_ENTRY_DATA = { "host": HOST, "password": PASSWORD, "serial_number": SERIAL_NUMBER, "mac": MAC_ADDRESS, } @pytest.fixture def platforms() -> list[Platform]: """Fixture to specify platforms to test.""" return [] @pytest.fixture async def config_entry_unique_id() -> str: """Fixture for config entry unique id.""" return MAC_ADDRESS_UNIQUE_ID @pytest.fixture async def serial_number() -> int: """Fixture for serial number used in the config entry data.""" return SERIAL_NUMBER @pytest.fixture async def config_entry_data(serial_number: int) -> dict[str, Any]: """Fixture for MockConfigEntry data.""" return { **CONFIG_ENTRY_DATA, "serial_number": serial_number, } @pytest.fixture async def config_entry( config_entry_data: dict[str, Any] | None, config_entry_unique_id: str | None, ) -> MockConfigEntry | None: """Fixture for MockConfigEntry.""" if config_entry_data is None: return None return MockConfigEntry( unique_id=config_entry_unique_id, 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(autouse=True) def setup_platforms( hass: HomeAssistant, platforms: list[str], ) -> None: """Fixture for setting up the default platforms.""" with patch(f"homeassistant.components.{DOMAIN}.PLATFORMS", platforms): yield @pytest.fixture(autouse=True) def aioclient_mock(hass: HomeAssistant) -> Generator[AiohttpClientMocker, None, None]: """Context manager to mock aiohttp client.""" mocker = AiohttpClientMocker() def create_session(): session = mocker.create_session(hass.loop) async def close_session(event): """Close session.""" await session.close() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, close_session) return session with ( patch( "homeassistant.components.rainbird.async_create_clientsession", side_effect=create_session, ), patch( "homeassistant.components.rainbird.config_flow.async_create_clientsession", side_effect=create_session, ), ): yield mocker def rainbird_json_response(result: dict[str, str]) -> bytes: """Create a fake API response.""" return encryption.encrypt( f'{{"jsonrpc": "2.0", "result": {json.dumps(result)}, "id": 1}} ', PASSWORD, ) def mock_json_response(result: dict[str, str]) -> AiohttpClientMockResponse: """Create a fake AiohttpClientMockResponse.""" return AiohttpClientMockResponse( "POST", URL, response=rainbird_json_response(result) ) def mock_response(data: str) -> AiohttpClientMockResponse: """Create a fake AiohttpClientMockResponse.""" return mock_json_response({"data": data}) def mock_response_error( status: HTTPStatus = HTTPStatus.SERVICE_UNAVAILABLE, ) -> AiohttpClientMockResponse: """Create a fake AiohttpClientMockResponse.""" return AiohttpClientMockResponse("POST", URL, status=status) @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="model_and_version_response") def mock_model_and_version_response() -> str: """Mock response to return rain delay state.""" return MODEL_AND_VERSION_RESPONSE @pytest.fixture(name="api_responses") def mock_api_responses( model_and_version_response: str, 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 [ model_and_version_response, stations_response, zone_state_response, rain_response, rain_delay_response, ] @pytest.fixture(name="responses") 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(api_response) for api_response in api_responses] @pytest.fixture(autouse=True) def handle_responses( aioclient_mock: AiohttpClientMocker, responses: list[AiohttpClientMockResponse], ) -> None: """Fixture for command mocking for fake responses to the API url.""" async def handle(method, url, data) -> AiohttpClientMockResponse: return responses.pop(0) aioclient_mock.post(URL, side_effect=handle)