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:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
51
homeassistant/components/gardena_bluetooth/util.py
Normal file
51
homeassistant/components/gardena_bluetooth/util.py
Normal 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")
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
2
requirements_all.txt
generated
@@ -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
|
||||||
|
|||||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user