diff --git a/tests/components/gardena_bluetooth/__init__.py b/tests/components/gardena_bluetooth/__init__.py index 6a064409e9e..a5ea94088fd 100644 --- a/tests/components/gardena_bluetooth/__init__.py +++ b/tests/components/gardena_bluetooth/__init__.py @@ -1,7 +1,18 @@ """Tests for the Gardena Bluetooth integration.""" +from unittest.mock import patch + +from homeassistant.components.gardena_bluetooth.const import DOMAIN +from homeassistant.components.gardena_bluetooth.coordinator import Coordinator +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from tests.common import MockConfigEntry +from tests.components.bluetooth import ( + inject_bluetooth_service_info, +) + WATER_TIMER_SERVICE_INFO = BluetoothServiceInfo( name="Timer", address="00000000-0000-0000-0000-000000000001", @@ -59,3 +70,18 @@ UNSUPPORTED_GROUP_SERVICE_INFO = BluetoothServiceInfo( service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"], source="local", ) + + +async def setup_entry( + hass: HomeAssistant, mock_entry: MockConfigEntry, platforms: list[Platform] +) -> Coordinator: + """Make sure the device is available.""" + + inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO) + + with patch("homeassistant.components.gardena_bluetooth.PLATFORMS", platforms): + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + return hass.data[DOMAIN][mock_entry.entry_id] diff --git a/tests/components/gardena_bluetooth/conftest.py b/tests/components/gardena_bluetooth/conftest.py index f09a274742f..a4d7170e945 100644 --- a/tests/components/gardena_bluetooth/conftest.py +++ b/tests/components/gardena_bluetooth/conftest.py @@ -1,10 +1,30 @@ """Common fixtures for the Gardena Bluetooth tests.""" from collections.abc import Generator +from typing import Any from unittest.mock import AsyncMock, Mock, patch +from freezegun import freeze_time from gardena_bluetooth.client import Client +from gardena_bluetooth.const import DeviceInformation +from gardena_bluetooth.exceptions import CharacteristicNotFound +from gardena_bluetooth.parse import Characteristic import pytest +from homeassistant.components.gardena_bluetooth.const import DOMAIN +from homeassistant.const import CONF_ADDRESS + +from . import WATER_TIMER_SERVICE_INFO + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_entry(): + """Create hass config fixture.""" + return MockConfigEntry( + domain=DOMAIN, data={CONF_ADDRESS: WATER_TIMER_SERVICE_INFO.address} + ) + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock, None, None]: @@ -16,15 +36,52 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: yield mock_setup_entry +@pytest.fixture +def mock_read_char_raw(): + """Mock data on device.""" + return { + DeviceInformation.firmware_version.uuid: b"1.2.3", + DeviceInformation.model_number.uuid: b"Mock Model", + } + + @pytest.fixture(autouse=True) -def mock_client(enable_bluetooth): +def mock_client(enable_bluetooth: None, mock_read_char_raw: dict[str, Any]) -> None: """Auto mock bluetooth.""" client = Mock(spec_set=Client) - client.get_all_characteristics_uuid.return_value = set() + + SENTINEL = object() + + def _read_char(char: Characteristic, default: Any = SENTINEL): + try: + return char.decode(mock_read_char_raw[char.uuid]) + except KeyError: + if default is SENTINEL: + raise CharacteristicNotFound from KeyError + return default + + def _read_char_raw(uuid: str, default: Any = SENTINEL): + try: + return mock_read_char_raw[uuid] + except KeyError: + if default is SENTINEL: + raise CharacteristicNotFound from KeyError + return default + + def _all_char(): + return set(mock_read_char_raw.keys()) + + client.read_char.side_effect = _read_char + client.read_char_raw.side_effect = _read_char_raw + client.get_all_characteristics_uuid.side_effect = _all_char with patch( "homeassistant.components.gardena_bluetooth.config_flow.Client", return_value=client, + ), patch( + "homeassistant.components.gardena_bluetooth.Client", return_value=client + ), freeze_time( + "2023-01-01", tz_offset=1 ): yield client diff --git a/tests/components/gardena_bluetooth/snapshots/test_init.ambr b/tests/components/gardena_bluetooth/snapshots/test_init.ambr new file mode 100644 index 00000000000..a3ecff80a46 --- /dev/null +++ b/tests/components/gardena_bluetooth/snapshots/test_init.ambr @@ -0,0 +1,28 @@ +# serializer version: 1 +# name: test_setup + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'gardena_bluetooth', + '00000000-0000-0000-0000-000000000001', + ), + }), + 'is_new': False, + 'manufacturer': None, + 'model': 'Mock Model', + 'name': 'Mock Title', + 'name_by_user': None, + 'suggested_area': None, + 'sw_version': '1.2.3', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/gardena_bluetooth/snapshots/test_number.ambr b/tests/components/gardena_bluetooth/snapshots/test_number.ambr new file mode 100644 index 00000000000..a12cce06019 --- /dev/null +++ b/tests/components/gardena_bluetooth/snapshots/test_number.ambr @@ -0,0 +1,86 @@ +# serializer version: 1 +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-number.mock_title_remaining_open_time] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Remaining open time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_remaining_open_time', + 'last_changed': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-number.mock_title_remaining_open_time].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Remaining open time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_remaining_open_time', + 'last_changed': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw2-number.mock_title_open_for] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Open for', + 'max': 1440, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'number.mock_title_open_for', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[98bd0f14-0b0e-421a-84e5-ddbf75dc6de4-raw0-number.mock_title_manual_watering_time] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Manual watering time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_manual_watering_time', + 'last_changed': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_setup[98bd0f14-0b0e-421a-84e5-ddbf75dc6de4-raw0-number.mock_title_manual_watering_time].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Manual watering time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_manual_watering_time', + 'last_changed': , + 'last_updated': , + 'state': '10.0', + }) +# --- diff --git a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..883f377c3a5 --- /dev/null +++ b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr @@ -0,0 +1,57 @@ +# serializer version: 1 +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-sensor.mock_title_valve_closing] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Mock Title Valve closing', + }), + 'context': , + 'entity_id': 'sensor.mock_title_valve_closing', + 'last_changed': , + 'last_updated': , + 'state': '2023-01-01T01:01:40+00:00', + }) +# --- +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-sensor.mock_title_valve_closing].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Mock Title Valve closing', + }), + 'context': , + 'entity_id': 'sensor.mock_title_valve_closing', + 'last_changed': , + 'last_updated': , + 'state': '2023-01-01T01:00:10+00:00', + }) +# --- +# name: test_setup[98bd2a19-0b0e-421a-84e5-ddbf75dc6de4-raw0-sensor.mock_title_battery] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Mock Title Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.mock_title_battery', + 'last_changed': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_setup[98bd2a19-0b0e-421a-84e5-ddbf75dc6de4-raw0-sensor.mock_title_battery].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Mock Title Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.mock_title_battery', + 'last_changed': , + 'last_updated': , + 'state': '10', + }) +# --- diff --git a/tests/components/gardena_bluetooth/test_init.py b/tests/components/gardena_bluetooth/test_init.py new file mode 100644 index 00000000000..3ad7e6dce61 --- /dev/null +++ b/tests/components/gardena_bluetooth/test_init.py @@ -0,0 +1,58 @@ +"""Test the Gardena Bluetooth setup.""" + +from datetime import timedelta +from unittest.mock import Mock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.gardena_bluetooth import DeviceUnavailable +from homeassistant.components.gardena_bluetooth.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.util import utcnow + +from . import WATER_TIMER_SERVICE_INFO + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_setup( + hass: HomeAssistant, + mock_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test setup creates expected devices.""" + + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.state is ConfigEntryState.LOADED + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device( + identifiers={(DOMAIN, WATER_TIMER_SERVICE_INFO.address)} + ) + assert device == snapshot + + +async def test_setup_retry( + hass: HomeAssistant, mock_entry: MockConfigEntry, mock_client: Mock +) -> None: + """Test setup creates expected devices.""" + + original_read_char = mock_client.read_char.side_effect + mock_client.read_char.side_effect = DeviceUnavailable + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.state is ConfigEntryState.SETUP_RETRY + + mock_client.read_char.side_effect = original_read_char + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + + assert mock_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/gardena_bluetooth/test_number.py b/tests/components/gardena_bluetooth/test_number.py new file mode 100644 index 00000000000..f1955905cce --- /dev/null +++ b/tests/components/gardena_bluetooth/test_number.py @@ -0,0 +1,60 @@ +"""Test Gardena Bluetooth sensor.""" + + +from gardena_bluetooth.const import Valve +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from . import setup_entry + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("uuid", "raw", "entity_id"), + [ + ( + Valve.manual_watering_time.uuid, + [ + Valve.manual_watering_time.encode(100), + Valve.manual_watering_time.encode(10), + ], + "number.mock_title_manual_watering_time", + ), + ( + Valve.remaining_open_time.uuid, + [ + Valve.remaining_open_time.encode(100), + Valve.remaining_open_time.encode(10), + ], + "number.mock_title_remaining_open_time", + ), + ( + Valve.remaining_open_time.uuid, + [Valve.remaining_open_time.encode(100)], + "number.mock_title_open_for", + ), + ], +) +async def test_setup( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_read_char_raw: dict[str, bytes], + uuid: str, + raw: list[bytes], + entity_id: str, +) -> None: + """Test setup creates expected entities.""" + + mock_read_char_raw[uuid] = raw[0] + coordinator = await setup_entry(hass, mock_entry, [Platform.NUMBER]) + assert hass.states.get(entity_id) == snapshot + + for char_raw in raw[1:]: + mock_read_char_raw[uuid] = char_raw + await coordinator.async_refresh() + assert hass.states.get(entity_id) == snapshot diff --git a/tests/components/gardena_bluetooth/test_sensor.py b/tests/components/gardena_bluetooth/test_sensor.py new file mode 100644 index 00000000000..d7cdc205f50 --- /dev/null +++ b/tests/components/gardena_bluetooth/test_sensor.py @@ -0,0 +1,52 @@ +"""Test Gardena Bluetooth sensor.""" + + +from gardena_bluetooth.const import Battery, Valve +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from . import setup_entry + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("uuid", "raw", "entity_id"), + [ + ( + Battery.battery_level.uuid, + [Battery.battery_level.encode(100), Battery.battery_level.encode(10)], + "sensor.mock_title_battery", + ), + ( + Valve.remaining_open_time.uuid, + [ + Valve.remaining_open_time.encode(100), + Valve.remaining_open_time.encode(10), + ], + "sensor.mock_title_valve_closing", + ), + ], +) +async def test_setup( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_read_char_raw: dict[str, bytes], + uuid: str, + raw: list[bytes], + entity_id: str, +) -> None: + """Test setup creates expected entities.""" + + mock_read_char_raw[uuid] = raw[0] + coordinator = await setup_entry(hass, mock_entry, [Platform.SENSOR]) + assert hass.states.get(entity_id) == snapshot + + for char_raw in raw[1:]: + mock_read_char_raw[uuid] = char_raw + await coordinator.async_refresh() + assert hass.states.get(entity_id) == snapshot