Add some basic tests for gardena (#96777)

This commit is contained in:
Joakim Plate 2023-07-17 21:13:13 +02:00 committed by GitHub
parent d80b7d0145
commit d02bf837a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 426 additions and 2 deletions

View file

@ -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]

View file

@ -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

View file

@ -0,0 +1,28 @@
# serializer version: 1
# name: test_setup
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'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,
})
# ---

View file

@ -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': <NumberMode.AUTO: 'auto'>,
'step': 60.0,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.mock_title_remaining_open_time',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <NumberMode.AUTO: 'auto'>,
'step': 60.0,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.mock_title_remaining_open_time',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <NumberMode.BOX: 'box'>,
'step': 1.0,
'unit_of_measurement': 'min',
}),
'context': <ANY>,
'entity_id': 'number.mock_title_open_for',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <NumberMode.BOX: 'box'>,
'step': 60,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.mock_title_manual_watering_time',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <NumberMode.BOX: 'box'>,
'step': 60,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.mock_title_manual_watering_time',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '10.0',
})
# ---

View file

@ -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': <ANY>,
'entity_id': 'sensor.mock_title_valve_closing',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'entity_id': 'sensor.mock_title_valve_closing',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_title_battery',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_title_battery',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '10',
})
# ---

View file

@ -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

View file

@ -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

View file

@ -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