1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-17 23:53:49 +01:00

Update gardena to 2.1.0 (#165322)

This commit is contained in:
Joakim Plate
2026-03-11 12:48:24 +01:00
committed by GitHub
parent d37106a360
commit 474b683d3c
15 changed files with 184 additions and 53 deletions

View File

@@ -2,12 +2,18 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
from bleak.backends.device import BLEDevice from bleak.backends.device import BLEDevice
from gardena_bluetooth.client import CachedConnection, Client from gardena_bluetooth.client import CachedConnection, Client
from gardena_bluetooth.const import DeviceConfiguration, DeviceInformation from gardena_bluetooth.const import DeviceConfiguration, DeviceInformation
from gardena_bluetooth.exceptions import CommunicationFailure from gardena_bluetooth.exceptions import (
CharacteristicNoAccess,
CharacteristicNotFound,
CommunicationFailure,
)
from gardena_bluetooth.parse import CharacteristicTime
from homeassistant.components import bluetooth from homeassistant.components import bluetooth
from homeassistant.const import CONF_ADDRESS, Platform from homeassistant.const import CONF_ADDRESS, Platform
@@ -23,6 +29,7 @@ from .coordinator import (
GardenaBluetoothConfigEntry, GardenaBluetoothConfigEntry,
GardenaBluetoothCoordinator, GardenaBluetoothCoordinator,
) )
from .util import async_get_product_type
PLATFORMS: list[Platform] = [ PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
@@ -51,22 +58,41 @@ def get_connection(hass: HomeAssistant, address: str) -> CachedConnection:
return CachedConnection(DISCONNECT_DELAY, _device_lookup) return CachedConnection(DISCONNECT_DELAY, _device_lookup)
async def _update_timestamp(client: Client, characteristics: CharacteristicTime):
try:
await client.update_timestamp(characteristics, dt_util.now())
except CharacteristicNotFound:
pass
except CharacteristicNoAccess:
LOGGER.debug("No access to update internal time")
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: GardenaBluetoothConfigEntry hass: HomeAssistant, entry: GardenaBluetoothConfigEntry
) -> bool: ) -> bool:
"""Set up Gardena Bluetooth from a config entry.""" """Set up Gardena Bluetooth from a config entry."""
address = entry.data[CONF_ADDRESS] address = entry.data[CONF_ADDRESS]
client = Client(get_connection(hass, address))
try: try:
async with asyncio.timeout(TIMEOUT):
product_type = await async_get_product_type(hass, address)
except TimeoutError as exception:
raise ConfigEntryNotReady("Unable to find product type") from exception
client = Client(get_connection(hass, address), product_type)
try:
chars = await client.get_all_characteristics()
sw_version = await client.read_char(DeviceInformation.firmware_version, None) sw_version = await client.read_char(DeviceInformation.firmware_version, None)
manufacturer = await client.read_char(DeviceInformation.manufacturer_name, None) manufacturer = await client.read_char(DeviceInformation.manufacturer_name, None)
model = await client.read_char(DeviceInformation.model_number, None) model = await client.read_char(DeviceInformation.model_number, None)
name = await client.read_char(
DeviceConfiguration.custom_device_name, entry.title name = entry.title
) name = await client.read_char(DeviceConfiguration.custom_device_name, name)
uuids = await client.get_all_characteristics_uuid()
await client.update_timestamp(dt_util.now()) await _update_timestamp(client, DeviceConfiguration.unix_timestamp)
except (TimeoutError, CommunicationFailure, DeviceUnavailable) as exception: except (TimeoutError, CommunicationFailure, DeviceUnavailable) as exception:
await client.disconnect() await client.disconnect()
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
@@ -83,7 +109,7 @@ async def async_setup_entry(
) )
coordinator = GardenaBluetoothCoordinator( coordinator = GardenaBluetoothCoordinator(
hass, entry, LOGGER, client, uuids, device, address hass, entry, LOGGER, client, set(chars.keys()), device, address
) )
entry.runtime_data = coordinator entry.runtime_data = coordinator

View File

@@ -34,14 +34,14 @@ class GardenaBluetoothBinarySensorEntityDescription(BinarySensorEntityDescriptio
DESCRIPTIONS = ( DESCRIPTIONS = (
GardenaBluetoothBinarySensorEntityDescription( GardenaBluetoothBinarySensorEntityDescription(
key=Valve.connected_state.uuid, key=Valve.connected_state.unique_id,
translation_key="valve_connected_state", translation_key="valve_connected_state",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
char=Valve.connected_state, char=Valve.connected_state,
), ),
GardenaBluetoothBinarySensorEntityDescription( GardenaBluetoothBinarySensorEntityDescription(
key=Sensor.connected_state.uuid, key=Sensor.connected_state.unique_id,
translation_key="sensor_connected_state", translation_key="sensor_connected_state",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
@@ -60,7 +60,7 @@ async def async_setup_entry(
entities = [ entities = [
GardenaBluetoothBinarySensor(coordinator, description, description.context) GardenaBluetoothBinarySensor(coordinator, description, description.context)
for description in DESCRIPTIONS for description in DESCRIPTIONS
if description.key in coordinator.characteristics if description.char.unique_id in coordinator.characteristics
] ]
async_add_entities(entities) async_add_entities(entities)

View File

@@ -30,7 +30,7 @@ class GardenaBluetoothButtonEntityDescription(ButtonEntityDescription):
DESCRIPTIONS = ( DESCRIPTIONS = (
GardenaBluetoothButtonEntityDescription( GardenaBluetoothButtonEntityDescription(
key=Reset.factory_reset.uuid, key=Reset.factory_reset.unique_id,
translation_key="factory_reset", translation_key="factory_reset",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@@ -49,7 +49,7 @@ async def async_setup_entry(
entities = [ entities = [
GardenaBluetoothButton(coordinator, description, description.context) GardenaBluetoothButton(coordinator, description, description.context)
for description in DESCRIPTIONS for description in DESCRIPTIONS
if description.key in coordinator.characteristics if description.char.unique_id in coordinator.characteristics
] ]
async_add_entities(entities) async_add_entities(entities)

View File

@@ -15,5 +15,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"], "loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
"requirements": ["gardena-bluetooth==1.6.0"] "requirements": ["gardena-bluetooth==2.1.0"]
} }

View File

@@ -46,7 +46,7 @@ class GardenaBluetoothNumberEntityDescription(NumberEntityDescription):
DESCRIPTIONS = ( DESCRIPTIONS = (
GardenaBluetoothNumberEntityDescription( GardenaBluetoothNumberEntityDescription(
key=Valve.manual_watering_time.uuid, key=Valve.manual_watering_time.unique_id,
translation_key="manual_watering_time", translation_key="manual_watering_time",
native_unit_of_measurement=UnitOfTime.SECONDS, native_unit_of_measurement=UnitOfTime.SECONDS,
mode=NumberMode.BOX, mode=NumberMode.BOX,
@@ -58,7 +58,7 @@ DESCRIPTIONS = (
device_class=NumberDeviceClass.DURATION, device_class=NumberDeviceClass.DURATION,
), ),
GardenaBluetoothNumberEntityDescription( GardenaBluetoothNumberEntityDescription(
key=Valve.remaining_open_time.uuid, key=Valve.remaining_open_time.unique_id,
translation_key="remaining_open_time", translation_key="remaining_open_time",
native_unit_of_measurement=UnitOfTime.SECONDS, native_unit_of_measurement=UnitOfTime.SECONDS,
native_min_value=0.0, native_min_value=0.0,
@@ -69,7 +69,7 @@ DESCRIPTIONS = (
device_class=NumberDeviceClass.DURATION, device_class=NumberDeviceClass.DURATION,
), ),
GardenaBluetoothNumberEntityDescription( GardenaBluetoothNumberEntityDescription(
key=DeviceConfiguration.rain_pause.uuid, key=DeviceConfiguration.rain_pause.unique_id,
translation_key="rain_pause", translation_key="rain_pause",
native_unit_of_measurement=UnitOfTime.MINUTES, native_unit_of_measurement=UnitOfTime.MINUTES,
mode=NumberMode.BOX, mode=NumberMode.BOX,
@@ -81,7 +81,7 @@ DESCRIPTIONS = (
device_class=NumberDeviceClass.DURATION, device_class=NumberDeviceClass.DURATION,
), ),
GardenaBluetoothNumberEntityDescription( GardenaBluetoothNumberEntityDescription(
key=DeviceConfiguration.seasonal_adjust.uuid, key=DeviceConfiguration.seasonal_adjust.unique_id,
translation_key="seasonal_adjust", translation_key="seasonal_adjust",
native_unit_of_measurement=UnitOfTime.DAYS, native_unit_of_measurement=UnitOfTime.DAYS,
mode=NumberMode.BOX, mode=NumberMode.BOX,
@@ -93,7 +93,7 @@ DESCRIPTIONS = (
device_class=NumberDeviceClass.DURATION, device_class=NumberDeviceClass.DURATION,
), ),
GardenaBluetoothNumberEntityDescription( GardenaBluetoothNumberEntityDescription(
key=Sensor.threshold.uuid, key=Sensor.threshold.unique_id,
translation_key="sensor_threshold", translation_key="sensor_threshold",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
mode=NumberMode.BOX, mode=NumberMode.BOX,
@@ -117,9 +117,9 @@ async def async_setup_entry(
entities: list[NumberEntity] = [ entities: list[NumberEntity] = [
GardenaBluetoothNumber(coordinator, description, description.context) GardenaBluetoothNumber(coordinator, description, description.context)
for description in DESCRIPTIONS for description in DESCRIPTIONS
if description.key in coordinator.characteristics if description.char.unique_id in coordinator.characteristics
] ]
if Valve.remaining_open_time.uuid in coordinator.characteristics: if Valve.remaining_open_time.unique_id in coordinator.characteristics:
entities.append(GardenaBluetoothRemainingOpenSetNumber(coordinator)) entities.append(GardenaBluetoothRemainingOpenSetNumber(coordinator))
async_add_entities(entities) async_add_entities(entities)

View File

@@ -41,7 +41,7 @@ class GardenaBluetoothSensorEntityDescription(SensorEntityDescription):
DESCRIPTIONS = ( DESCRIPTIONS = (
GardenaBluetoothSensorEntityDescription( GardenaBluetoothSensorEntityDescription(
key=Valve.activation_reason.uuid, key=Valve.activation_reason.unique_id,
translation_key="activation_reason", translation_key="activation_reason",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
@@ -49,7 +49,7 @@ DESCRIPTIONS = (
char=Valve.activation_reason, char=Valve.activation_reason,
), ),
GardenaBluetoothSensorEntityDescription( GardenaBluetoothSensorEntityDescription(
key=Battery.battery_level.uuid, key=Battery.battery_level.unique_id,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
@@ -57,7 +57,7 @@ DESCRIPTIONS = (
char=Battery.battery_level, char=Battery.battery_level,
), ),
GardenaBluetoothSensorEntityDescription( GardenaBluetoothSensorEntityDescription(
key=Sensor.battery_level.uuid, key=Sensor.battery_level.unique_id,
translation_key="sensor_battery_level", translation_key="sensor_battery_level",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
@@ -67,7 +67,7 @@ DESCRIPTIONS = (
connected_state=Sensor.connected_state, connected_state=Sensor.connected_state,
), ),
GardenaBluetoothSensorEntityDescription( GardenaBluetoothSensorEntityDescription(
key=Sensor.value.uuid, key=Sensor.value.unique_id,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.MOISTURE, device_class=SensorDeviceClass.MOISTURE,
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
@@ -75,14 +75,14 @@ DESCRIPTIONS = (
connected_state=Sensor.connected_state, connected_state=Sensor.connected_state,
), ),
GardenaBluetoothSensorEntityDescription( GardenaBluetoothSensorEntityDescription(
key=Sensor.type.uuid, key=Sensor.type.unique_id,
translation_key="sensor_type", translation_key="sensor_type",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
char=Sensor.type, char=Sensor.type,
connected_state=Sensor.connected_state, connected_state=Sensor.connected_state,
), ),
GardenaBluetoothSensorEntityDescription( GardenaBluetoothSensorEntityDescription(
key=Sensor.measurement_timestamp.uuid, key=Sensor.measurement_timestamp.unique_id,
translation_key="sensor_measurement_timestamp", translation_key="sensor_measurement_timestamp",
device_class=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
@@ -102,9 +102,9 @@ async def async_setup_entry(
entities: list[GardenaBluetoothEntity] = [ entities: list[GardenaBluetoothEntity] = [
GardenaBluetoothSensor(coordinator, description, description.context) GardenaBluetoothSensor(coordinator, description, description.context)
for description in DESCRIPTIONS for description in DESCRIPTIONS
if description.key in coordinator.characteristics if description.char.unique_id in coordinator.characteristics
] ]
if Valve.remaining_open_time.uuid in coordinator.characteristics: if Valve.remaining_open_time.unique_id in coordinator.characteristics:
entities.append(GardenaBluetoothRemainSensor(coordinator)) entities.append(GardenaBluetoothRemainSensor(coordinator))
async_add_entities(entities) async_add_entities(entities)

View File

@@ -35,9 +35,9 @@ class GardenaBluetoothValveSwitch(GardenaBluetoothEntity, SwitchEntity):
"""Representation of a valve switch.""" """Representation of a valve switch."""
characteristics = { characteristics = {
Valve.state.uuid, Valve.state.unique_id,
Valve.manual_watering_time.uuid, Valve.manual_watering_time.unique_id,
Valve.remaining_open_time.uuid, Valve.remaining_open_time.unique_id,
} }
def __init__( def __init__(
@@ -48,7 +48,7 @@ class GardenaBluetoothValveSwitch(GardenaBluetoothEntity, SwitchEntity):
super().__init__( super().__init__(
coordinator, {Valve.state.uuid, Valve.manual_watering_time.uuid} coordinator, {Valve.state.uuid, Valve.manual_watering_time.uuid}
) )
self._attr_unique_id = f"{coordinator.address}-{Valve.state.uuid}" self._attr_unique_id = f"{coordinator.address}-{Valve.state.unique_id}"
self._attr_translation_key = "state" self._attr_translation_key = "state"
self._attr_is_on = None self._attr_is_on = None
self._attr_entity_registry_enabled_default = False self._attr_entity_registry_enabled_default = False

View File

@@ -0,0 +1,51 @@
"""Utility functions for Gardena Bluetooth integration."""
import asyncio
from collections.abc import AsyncIterator
from gardena_bluetooth.parse import ManufacturerData, ProductType
from homeassistant.components import bluetooth
async def _async_service_info(
hass, address
) -> AsyncIterator[bluetooth.BluetoothServiceInfoBleak]:
queue = asyncio.Queue[bluetooth.BluetoothServiceInfoBleak]()
def _callback(
service_info: bluetooth.BluetoothServiceInfoBleak,
change: bluetooth.BluetoothChange,
) -> None:
if change != bluetooth.BluetoothChange.ADVERTISEMENT:
return
queue.put_nowait(service_info)
service_info = bluetooth.async_last_service_info(hass, address, True)
if service_info:
yield service_info
cancel = bluetooth.async_register_callback(
hass,
_callback,
{bluetooth.match.ADDRESS: address},
bluetooth.BluetoothScanningMode.ACTIVE,
)
try:
while True:
yield await queue.get()
finally:
cancel()
async def async_get_product_type(hass, address: str) -> ProductType:
"""Wait for enough packets of manufacturer data to get the product type."""
data = ManufacturerData()
async for service_info in _async_service_info(hass, address):
data.update(service_info.manufacturer_data.get(ManufacturerData.company, b""))
product_type = ProductType.from_manufacturer_data(data)
if product_type is not ProductType.UNKNOWN:
return product_type
raise AssertionError("Iterator should have been infinite")

View File

@@ -44,9 +44,9 @@ class GardenaBluetoothValve(GardenaBluetoothEntity, ValveEntity):
_attr_device_class = ValveDeviceClass.WATER _attr_device_class = ValveDeviceClass.WATER
characteristics = { characteristics = {
Valve.state.uuid, Valve.state.unique_id,
Valve.manual_watering_time.uuid, Valve.manual_watering_time.unique_id,
Valve.remaining_open_time.uuid, Valve.remaining_open_time.unique_id,
} }
def __init__( def __init__(
@@ -57,7 +57,7 @@ class GardenaBluetoothValve(GardenaBluetoothEntity, ValveEntity):
super().__init__( super().__init__(
coordinator, {Valve.state.uuid, Valve.manual_watering_time.uuid} coordinator, {Valve.state.uuid, Valve.manual_watering_time.uuid}
) )
self._attr_unique_id = f"{coordinator.address}-{Valve.state.uuid}" self._attr_unique_id = f"{coordinator.address}-{Valve.state.unique_id}"
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
self._attr_is_closed = not self.coordinator.get_cached(Valve.state) self._attr_is_closed = not self.coordinator.get_cached(Valve.state)

View File

@@ -58,7 +58,7 @@ def _is_supported(discovery_info: BluetoothServiceInfo):
# Some mowers only expose the serial number in the manufacturer data # Some mowers only expose the serial number in the manufacturer data
# and not the product type, so we allow None here as well. # and not the product type, so we allow None here as well.
if product_type not in (ProductType.MOWER, None): if product_type not in (ProductType.MOWER, ProductType.UNKNOWN):
LOGGER.debug("Unsupported device: %s (%s)", manufacturer_data, discovery_info) LOGGER.debug("Unsupported device: %s (%s)", manufacturer_data, discovery_info)
return False return False

View File

@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble", "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["automower-ble==0.2.8", "gardena-bluetooth==1.6.0"] "requirements": ["automower-ble==0.2.8", "gardena-bluetooth==2.1.0"]
} }

2
requirements_all.txt generated
View File

@@ -1026,7 +1026,7 @@ gTTS==2.5.3
# homeassistant.components.gardena_bluetooth # homeassistant.components.gardena_bluetooth
# homeassistant.components.husqvarna_automower_ble # homeassistant.components.husqvarna_automower_ble
gardena-bluetooth==1.6.0 gardena-bluetooth==2.1.0
# homeassistant.components.google_assistant_sdk # homeassistant.components.google_assistant_sdk
gassist-text==0.0.14 gassist-text==0.0.14

View File

@@ -905,7 +905,7 @@ gTTS==2.5.3
# homeassistant.components.gardena_bluetooth # homeassistant.components.gardena_bluetooth
# homeassistant.components.husqvarna_automower_ble # homeassistant.components.husqvarna_automower_ble
gardena-bluetooth==1.6.0 gardena-bluetooth==2.1.0
# homeassistant.components.google_assistant_sdk # homeassistant.components.google_assistant_sdk
gassist-text==0.0.14 gassist-text==0.0.14

View File

@@ -8,7 +8,7 @@ from freezegun.api import FrozenDateTimeFactory
from gardena_bluetooth.client import Client from gardena_bluetooth.client import Client
from gardena_bluetooth.const import DeviceInformation from gardena_bluetooth.const import DeviceInformation
from gardena_bluetooth.exceptions import CharacteristicNotFound from gardena_bluetooth.exceptions import CharacteristicNotFound
from gardena_bluetooth.parse import Characteristic from gardena_bluetooth.parse import Characteristic, Service
import pytest import pytest
from homeassistant.components.gardena_bluetooth.const import DOMAIN from homeassistant.components.gardena_bluetooth.const import DOMAIN
@@ -83,7 +83,7 @@ def mock_client(
) -> Generator[Mock]: ) -> Generator[Mock]:
"""Auto mock bluetooth.""" """Auto mock bluetooth."""
client = Mock(spec_set=Client) client_class = Mock()
SENTINEL = object() SENTINEL = object()
@@ -106,19 +106,32 @@ def mock_client(
return default return default
return val return val
def _all_char(): def _all_char_uuid():
return set(mock_read_char_raw.keys()) return set(mock_read_char_raw.keys())
def _all_char():
product_type = client_class.call_args.args[1]
services = Service.services_for_product_type(product_type)
return {
char.unique_id: char
for service in services
for char in service.characteristics.values()
if char.uuid in mock_read_char_raw
}
client = Mock(spec_set=Client)
client.read_char.side_effect = _read_char client.read_char.side_effect = _read_char
client.read_char_raw.side_effect = _read_char_raw client.read_char_raw.side_effect = _read_char_raw
client.get_all_characteristics_uuid.side_effect = _all_char client.get_all_characteristics_uuid.side_effect = _all_char_uuid
client.get_all_characteristics.side_effect = _all_char
client_class.return_value = client
with ( with (
patch( patch(
"homeassistant.components.gardena_bluetooth.config_flow.Client", "homeassistant.components.gardena_bluetooth.config_flow.Client",
return_value=client, new=client_class,
), ),
patch("homeassistant.components.gardena_bluetooth.Client", return_value=client), patch("homeassistant.components.gardena_bluetooth.Client", new=client_class),
): ):
yield client yield client

View File

@@ -1,21 +1,26 @@
"""Test the Gardena Bluetooth setup.""" """Test the Gardena Bluetooth setup."""
import asyncio
from datetime import timedelta from datetime import timedelta
from unittest.mock import Mock from unittest.mock import Mock, patch
from gardena_bluetooth.const import Battery from gardena_bluetooth.const import Battery
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.components.gardena_bluetooth import DeviceUnavailable from homeassistant.components.gardena_bluetooth import DeviceUnavailable
from homeassistant.components.gardena_bluetooth.const import DOMAIN from homeassistant.components.gardena_bluetooth.const import DOMAIN
from homeassistant.components.gardena_bluetooth.util import (
async_get_product_type as original_get_product_type,
)
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.util import utcnow from homeassistant.util import utcnow
from . import WATER_TIMER_SERVICE_INFO from . import MISSING_MANUFACTURER_DATA_SERVICE_INFO, WATER_TIMER_SERVICE_INFO
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.bluetooth import inject_bluetooth_service_info
async def test_setup( async def test_setup(
@@ -28,12 +33,10 @@ async def test_setup(
"""Test setup creates expected devices.""" """Test setup creates expected devices."""
mock_read_char_raw[Battery.battery_level.uuid] = Battery.battery_level.encode(100) mock_read_char_raw[Battery.battery_level.uuid] = Battery.battery_level.encode(100)
inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO)
mock_entry.add_to_hass(hass) mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id) assert await hass.config_entries.async_setup(mock_entry.entry_id) is True
await hass.async_block_till_done()
assert mock_entry.state is ConfigEntryState.LOADED
device = device_registry.async_get_device( device = device_registry.async_get_device(
identifiers={(DOMAIN, WATER_TIMER_SERVICE_INFO.address)} identifiers={(DOMAIN, WATER_TIMER_SERVICE_INFO.address)}
@@ -41,11 +44,49 @@ async def test_setup(
assert device == snapshot assert device == snapshot
async def test_setup_delayed_product(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_entry: MockConfigEntry,
mock_read_char_raw: dict[str, bytes],
snapshot: SnapshotAssertion,
) -> None:
"""Test setup creates expected devices."""
mock_read_char_raw[Battery.battery_level.uuid] = Battery.battery_level.encode(100)
mock_entry.add_to_hass(hass)
event = asyncio.Event()
async def _get_product_type(*args, **kwargs):
event.set()
return await original_get_product_type(*args, **kwargs)
with patch(
"homeassistant.components.gardena_bluetooth.async_get_product_type",
wraps=_get_product_type,
):
async with asyncio.TaskGroup() as tg:
setup_task = tg.create_task(
hass.config_entries.async_setup(mock_entry.entry_id)
)
await event.wait()
assert mock_entry.state is ConfigEntryState.SETUP_IN_PROGRESS
inject_bluetooth_service_info(hass, MISSING_MANUFACTURER_DATA_SERVICE_INFO)
inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO)
assert await setup_task is True
async def test_setup_retry( async def test_setup_retry(
hass: HomeAssistant, mock_entry: MockConfigEntry, mock_client: Mock hass: HomeAssistant, mock_entry: MockConfigEntry, mock_client: Mock
) -> None: ) -> None:
"""Test setup creates expected devices.""" """Test setup creates expected devices."""
inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO)
original_read_char = mock_client.read_char.side_effect original_read_char = mock_client.read_char.side_effect
mock_client.read_char.side_effect = DeviceUnavailable mock_client.read_char.side_effect = DeviceUnavailable
mock_entry.add_to_hass(hass) mock_entry.add_to_hass(hass)