Add support for aqua contour/precise line of gardena products (#165326)

This commit is contained in:
Joakim Plate
2026-03-17 08:32:17 +01:00
committed by GitHub
parent 2d273a86ba
commit 120d3ee85a
15 changed files with 1421 additions and 85 deletions
@@ -7,7 +7,7 @@ import logging
from bleak.backends.device import BLEDevice
from gardena_bluetooth.client import CachedConnection, Client
from gardena_bluetooth.const import DeviceConfiguration, DeviceInformation
from gardena_bluetooth.const import AquaContour, DeviceConfiguration, DeviceInformation
from gardena_bluetooth.exceptions import (
CharacteristicNoAccess,
CharacteristicNotFound,
@@ -35,6 +35,7 @@ PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.VALVE,
@@ -90,8 +91,10 @@ async def async_setup_entry(
name = entry.title
name = await client.read_char(DeviceConfiguration.custom_device_name, name)
name = await client.read_char(AquaContour.custom_device_name, name)
await _update_timestamp(client, DeviceConfiguration.unix_timestamp)
await _update_timestamp(client, AquaContour.unix_timestamp)
except (TimeoutError, CommunicationFailure, DeviceUnavailable) as exception:
await client.disconnect()
@@ -4,7 +4,7 @@ from __future__ import annotations
from dataclasses import dataclass, field
from gardena_bluetooth.const import Sensor, Valve
from gardena_bluetooth.const import AquaContour, Sensor, Valve
from gardena_bluetooth.parse import CharacteristicBool
from homeassistant.components.binary_sensor import (
@@ -47,6 +47,13 @@ DESCRIPTIONS = (
entity_category=EntityCategory.DIAGNOSTIC,
char=Sensor.connected_state,
),
GardenaBluetoothBinarySensorEntityDescription(
key=AquaContour.frost_warning.unique_id,
translation_key="frost_warning",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
char=AquaContour.frost_warning,
),
)
@@ -43,6 +43,7 @@ def _is_supported(discovery_info: BluetoothServiceInfo):
ProductType.WATER_COMPUTER,
ProductType.AUTOMATS,
ProductType.PRESSURE_TANKS,
ProductType.AQUA_CONTOURS,
):
_LOGGER.debug("Unsupported device: %s", manufacturer_data)
return False
@@ -70,6 +71,7 @@ class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_read_data(self):
"""Try to connect to device and extract information."""
assert self.address
client = Client(get_connection(self.hass, self.address))
try:
model = await client.read_char(DeviceInformation.model_number)
@@ -4,7 +4,7 @@ from __future__ import annotations
from dataclasses import dataclass, field
from gardena_bluetooth.const import DeviceConfiguration, Sensor, Valve
from gardena_bluetooth.const import DeviceConfiguration, Sensor, Spray, Valve
from gardena_bluetooth.parse import (
Characteristic,
CharacteristicInt,
@@ -18,7 +18,7 @@ from homeassistant.components.number import (
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
from homeassistant.const import DEGREE, PERCENTAGE, EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -34,6 +34,7 @@ class GardenaBluetoothNumberEntityDescription(NumberEntityDescription):
default_factory=lambda: CharacteristicInt("")
)
connected_state: Characteristic | None = None
scale: float = 1.0
@property
def context(self) -> set[str]:
@@ -104,6 +105,27 @@ DESCRIPTIONS = (
char=Sensor.threshold,
connected_state=Sensor.connected_state,
),
GardenaBluetoothNumberEntityDescription(
key="spray_sector",
translation_key="spray_sector",
native_unit_of_measurement=DEGREE,
mode=NumberMode.BOX,
native_min_value=0.0,
native_max_value=359.0,
native_step=1.0,
char=Spray.sector,
),
GardenaBluetoothNumberEntityDescription(
key="spray_distance",
translation_key="spray_distance",
native_unit_of_measurement=PERCENTAGE,
mode=NumberMode.SLIDER,
native_min_value=0.0,
native_max_value=100.0,
native_step=0.1,
char=Spray.distance,
scale=10.0,
),
)
@@ -134,7 +156,7 @@ class GardenaBluetoothNumber(GardenaBluetoothDescriptorEntity, NumberEntity):
if data is None:
self._attr_native_value = None
else:
self._attr_native_value = float(data)
self._attr_native_value = float(data) / self.entity_description.scale
if char := self.entity_description.connected_state:
self._attr_available = bool(self.coordinator.get_cached(char))
@@ -145,7 +167,9 @@ class GardenaBluetoothNumber(GardenaBluetoothDescriptorEntity, NumberEntity):
async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
await self.coordinator.write(self.entity_description.char, int(value))
await self.coordinator.write(
self.entity_description.char, int(value * self.entity_description.scale)
)
self.async_write_ha_state()
@@ -0,0 +1,113 @@
"""Support for select entities."""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import IntEnum
from gardena_bluetooth.const import (
AquaContour,
AquaContourPosition,
AquaContourWatering,
)
from gardena_bluetooth.parse import CharacteristicInt
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import GardenaBluetoothConfigEntry
from .entity import GardenaBluetoothDescriptorEntity
def _enum_to_int(enum: type[IntEnum]) -> dict[str, int]:
return {member.name.lower(): member.value for member in enum}
def _reverse_dict(value: dict[str, int]) -> dict[int, str]:
return {value: key for key, value in value.items()}
@dataclass(frozen=True, kw_only=True)
class GardenaBluetoothSelectEntityDescription(SelectEntityDescription):
"""Description of entity."""
key: str = field(init=False)
char: CharacteristicInt
option_to_number: dict[str, int]
number_to_option: dict[int, str] = field(init=False)
def __post_init__(self):
"""Initialize calculated fields."""
object.__setattr__(self, "key", self.char.unique_id)
object.__setattr__(self, "options", list(self.option_to_number.keys()))
object.__setattr__(
self, "number_to_option", _reverse_dict(self.option_to_number)
)
@property
def context(self) -> set[str]:
"""Context needed for update coordinator."""
return {self.char.uuid}
DESCRIPTIONS = (
GardenaBluetoothSelectEntityDescription(
translation_key="watering_active",
char=AquaContourWatering.watering_active,
option_to_number=_enum_to_int(AquaContourWatering.watering_active.enum),
),
GardenaBluetoothSelectEntityDescription(
translation_key="operation_mode",
char=AquaContour.operation_mode,
option_to_number=_enum_to_int(AquaContour.operation_mode.enum),
),
GardenaBluetoothSelectEntityDescription(
translation_key="active_position",
char=AquaContourPosition.active_position,
option_to_number={
"position_1": 1,
"position_2": 2,
"position_3": 3,
"position_4": 4,
"position_5": 5,
},
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: GardenaBluetoothConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up select based on a config entry."""
coordinator = entry.runtime_data
entities = [
GardenaBluetoothSelectEntity(coordinator, description, description.context)
for description in DESCRIPTIONS
if description.char.unique_id in coordinator.characteristics
]
async_add_entities(entities)
class GardenaBluetoothSelectEntity(GardenaBluetoothDescriptorEntity, SelectEntity):
"""Representation of a select entity."""
entity_description: GardenaBluetoothSelectEntityDescription
@property
def current_option(self) -> str | None:
"""Return the selected entity option to represent the entity state."""
char = self.entity_description.char
value = self.coordinator.get_cached(char)
if value is None:
return None
return self.entity_description.number_to_option.get(value)
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
char = self.entity_description.char
value = self.entity_description.option_to_number[option]
await self.coordinator.write(char, value)
self.async_write_ha_state()
@@ -2,10 +2,19 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from gardena_bluetooth.const import Battery, Sensor, Valve
from gardena_bluetooth.const import (
AquaContourBattery,
Battery,
EventHistory,
FlowStatistics,
Sensor,
Spray,
Valve,
)
from gardena_bluetooth.parse import Characteristic
from homeassistant.components.sensor import (
@@ -13,8 +22,15 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
SensorStateClass,
StateType,
)
from homeassistant.const import (
DEGREE,
PERCENTAGE,
EntityCategory,
UnitOfVolume,
UnitOfVolumeFlowRate,
)
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
@@ -22,13 +38,28 @@ from homeassistant.util import dt as dt_util
from .coordinator import GardenaBluetoothConfigEntry, GardenaBluetoothCoordinator
from .entity import GardenaBluetoothDescriptorEntity, GardenaBluetoothEntity
type SensorRawType = StateType | datetime
def _get_timestamp(value: datetime | None):
if value is None:
return None
return value.replace(tzinfo=dt_util.get_default_time_zone())
def _get_distance_ratio(value: int | None):
if value is None:
return None
return value / 1000
@dataclass(frozen=True)
class GardenaBluetoothSensorEntityDescription(SensorEntityDescription):
class GardenaBluetoothSensorEntityDescription[T](SensorEntityDescription):
"""Description of entity."""
char: Characteristic = field(default_factory=lambda: Characteristic(""))
char: Characteristic[T] = field(default_factory=lambda: Characteristic(""))
connected_state: Characteristic | None = None
get: Callable[[T | None], SensorRawType] = lambda x: x # type: ignore[assignment, return-value]
@property
def context(self) -> set[str]:
@@ -56,6 +87,14 @@ DESCRIPTIONS = (
native_unit_of_measurement=PERCENTAGE,
char=Battery.battery_level,
),
GardenaBluetoothSensorEntityDescription(
key=AquaContourBattery.battery_level.unique_id,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE,
char=AquaContourBattery.battery_level,
),
GardenaBluetoothSensorEntityDescription(
key=Sensor.battery_level.unique_id,
translation_key="sensor_battery_level",
@@ -88,6 +127,78 @@ DESCRIPTIONS = (
entity_category=EntityCategory.DIAGNOSTIC,
char=Sensor.measurement_timestamp,
connected_state=Sensor.connected_state,
get=_get_timestamp,
),
GardenaBluetoothSensorEntityDescription(
key=FlowStatistics.overall.unique_id,
translation_key="flow_statistics_overall",
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.VOLUME,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfVolume.LITERS,
char=FlowStatistics.overall,
),
GardenaBluetoothSensorEntityDescription(
key=FlowStatistics.current.unique_id,
translation_key="flow_statistics_current",
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE,
char=FlowStatistics.current,
),
GardenaBluetoothSensorEntityDescription(
key=FlowStatistics.resettable.unique_id,
translation_key="flow_statistics_resettable",
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.VOLUME,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfVolume.LITERS,
char=FlowStatistics.resettable,
),
GardenaBluetoothSensorEntityDescription(
key=FlowStatistics.last_reset.unique_id,
translation_key="flow_statistics_reset_timestamp",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
char=FlowStatistics.last_reset,
get=_get_timestamp,
),
GardenaBluetoothSensorEntityDescription(
key=Spray.current_distance.unique_id,
translation_key="spray_current_distance",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE,
char=Spray.current_distance,
get=_get_distance_ratio,
),
GardenaBluetoothSensorEntityDescription(
key=Spray.current_sector.unique_id,
translation_key="spray_current_sector",
state_class=SensorStateClass.MEASUREMENT_ANGLE,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=DEGREE,
char=Spray.current_sector,
),
GardenaBluetoothSensorEntityDescription(
key="aqua_contour_error",
translation_key="aqua_contour_error",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
char=EventHistory.error,
get=lambda x: (
x.error_code.name.lower()
if x and isinstance(x.error_code, EventHistory.error.enum)
else None
),
options=[member.name.lower() for member in EventHistory.error.enum],
),
GardenaBluetoothSensorEntityDescription(
key="aqua_contour_error_timestamp",
translation_key="error_timestamp",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.TIMESTAMP,
char=EventHistory.error,
get=lambda x: _get_timestamp(x.time_stamp) if x else None,
),
)
@@ -116,8 +227,7 @@ class GardenaBluetoothSensor(GardenaBluetoothDescriptorEntity, SensorEntity):
def _handle_coordinator_update(self) -> None:
value = self.coordinator.get_cached(self.entity_description.char)
if isinstance(value, datetime):
value = value.replace(tzinfo=dt_util.get_default_time_zone())
value = self.entity_description.get(value)
self._attr_native_value = value
if char := self.entity_description.connected_state:
@@ -22,6 +22,9 @@
},
"entity": {
"binary_sensor": {
"frost_warning": {
"name": "Frost"
},
"sensor_connected_state": {
"name": "Sensor connection"
},
@@ -52,12 +55,79 @@
},
"sensor_threshold": {
"name": "Sensor threshold"
},
"spray_distance": {
"name": "Distance"
},
"spray_sector": {
"name": "Sector"
}
},
"select": {
"active_position": {
"name": "Active position",
"state": {
"position_1": "Position 1",
"position_2": "Position 2",
"position_3": "Position 3",
"position_4": "Position 4",
"position_5": "Position 5"
}
},
"operation_mode": {
"name": "Operation mode",
"state": {
"active": "Active",
"deep_sleep": "Deep sleep",
"manual_mode": "Manual",
"pre_winter": "Winter preparation"
}
},
"watering_active": {
"name": "Watering",
"state": {
"contour_1": "Contour 1",
"contour_2": "Contour 2",
"contour_3": "Contour 3",
"contour_4": "Contour 4",
"contour_5": "Contour 5",
"preview": "Preview",
"rest": "Idle",
"setup_mode": "Setup"
}
}
},
"sensor": {
"activation_reason": {
"name": "Activation reason"
},
"aqua_contour_error": {
"name": "Error",
"state": {
"charger_error": "Charger error",
"flash_error": "Flash error",
"no_error": "No error detected",
"no_water": "Not enough water",
"rotation_sensor_error": "Rotation sensor error",
"sprinkler_motor_error": "Sprinkler motor error",
"valve_motor_error": "Valve motor error"
}
},
"error_timestamp": {
"name": "Error timestamp"
},
"flow_statistics_current": {
"name": "Current flow"
},
"flow_statistics_overall": {
"name": "Overall flow"
},
"flow_statistics_reset_timestamp": {
"name": "Flow reset timestamp"
},
"flow_statistics_resettable": {
"name": "Flow since reset"
},
"remaining_open_timestamp": {
"name": "Valve closing"
},
@@ -69,6 +139,12 @@
},
"sensor_type": {
"name": "Sensor type"
},
"spray_current_distance": {
"name": "Current distance"
},
"spray_current_sector": {
"name": "Current sector"
}
},
"switch": {
+35 -4
View File
@@ -2,7 +2,8 @@
from unittest.mock import patch
from homeassistant.const import Platform
from homeassistant.components.gardena_bluetooth.const import DOMAIN
from homeassistant.const import CONF_ADDRESS, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
@@ -33,6 +34,16 @@ WATER_TIMER_UNNAMED_SERVICE_INFO = BluetoothServiceInfo(
source="local",
)
AQUA_CONTOUR_SERVICE_INFO = BluetoothServiceInfo(
name="Aqua Contour",
address="00000000-0000-0000-0000-000000000003",
rssi=-63,
service_data={},
manufacturer_data={1062: b"\x02\x05\x00\x04\x06\x12\x10\x01"},
service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"],
source="local",
)
MISSING_SERVICE_SERVICE_INFO = BluetoothServiceInfo(
name="Missing Service Info",
address="00000000-0000-0000-0001-000000000000",
@@ -68,14 +79,34 @@ UNSUPPORTED_GROUP_SERVICE_INFO = BluetoothServiceInfo(
)
def get_config_entry(service_info: BluetoothServiceInfo) -> MockConfigEntry:
"""Construct a config entry for a given discovery."""
return MockConfigEntry(
domain=DOMAIN,
data={CONF_ADDRESS: service_info.address},
unique_id=service_info.address,
)
async def setup_entry(
hass: HomeAssistant, mock_entry: MockConfigEntry, platforms: list[Platform]
) -> None:
hass: HomeAssistant,
mock_entry: MockConfigEntry | None = None,
platforms: list[Platform] | None = None,
service_info: BluetoothServiceInfo = WATER_TIMER_SERVICE_INFO,
) -> MockConfigEntry:
"""Make sure the device is available."""
inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO)
inject_bluetooth_service_info(hass, service_info)
if platforms is None:
platforms = []
with patch("homeassistant.components.gardena_bluetooth.PLATFORMS", platforms):
if mock_entry is None:
mock_entry = get_config_entry(service_info)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
return mock_entry
@@ -11,24 +11,18 @@ from gardena_bluetooth.exceptions import CharacteristicNotFound
from gardena_bluetooth.parse import Characteristic, Service
import pytest
from homeassistant.components.gardena_bluetooth.const import DOMAIN
from homeassistant.components.gardena_bluetooth.coordinator import SCAN_INTERVAL
from homeassistant.const import CONF_ADDRESS
from homeassistant.core import HomeAssistant
from . import WATER_TIMER_SERVICE_INFO
from . import WATER_TIMER_SERVICE_INFO, get_config_entry
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.common import async_fire_time_changed
@pytest.fixture
def mock_entry():
"""Create hass config fixture."""
return MockConfigEntry(
domain=DOMAIN,
data={CONF_ADDRESS: WATER_TIMER_SERVICE_INFO.address},
unique_id=WATER_TIMER_SERVICE_INFO.address,
)
return get_config_entry(WATER_TIMER_SERVICE_INFO)
@pytest.fixture(scope="module")
@@ -1,5 +1,40 @@
# serializer version: 1
# name: test_setup
# name: test_setup[Aqua Contour]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'bluetooth',
'00000000-0000-0000-0000-000000000003',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'gardena_bluetooth',
'00000000-0000-0000-0000-000000000003',
),
}),
'labels': set({
}),
'manufacturer': None,
'model': 'Aqua Contour',
'model_id': None,
'name': 'My contour',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': '2.0.0',
'via_device_id': None,
})
# ---
# name: test_setup[Timer]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
@@ -24,9 +59,9 @@
'labels': set({
}),
'manufacturer': None,
'model': 'Mock Model',
'model': 'Model Number TBD',
'model_id': None,
'name': 'Mock Title',
'name': 'My timer',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
@@ -0,0 +1,197 @@
# serializer version: 1
# name: test_setup[aqua_contour][select.mock_title_active_position-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'position_1',
'position_2',
'position_3',
'position_4',
'position_5',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.mock_title_active_position',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Active position',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Active position',
'platform': 'gardena_bluetooth',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'active_position',
'unique_id': '00000000-0000-0000-0000-000000000003-98bd0132-0b0e-421a-84e5-ddbf75dc6de4',
'unit_of_measurement': None,
})
# ---
# name: test_setup[aqua_contour][select.mock_title_active_position-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title Active position',
'options': list([
'position_1',
'position_2',
'position_3',
'position_4',
'position_5',
]),
}),
'context': <ANY>,
'entity_id': 'select.mock_title_active_position',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_setup[aqua_contour][select.mock_title_operation_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'active',
'manual_mode',
'pre_winter',
'deep_sleep',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.mock_title_operation_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Operation mode',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Operation mode',
'platform': 'gardena_bluetooth',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'operation_mode',
'unique_id': '00000000-0000-0000-0000-000000000003-98bd0a17-0b0e-421a-84e5-ddbf75dc6de4',
'unit_of_measurement': None,
})
# ---
# name: test_setup[aqua_contour][select.mock_title_operation_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title Operation mode',
'options': list([
'active',
'manual_mode',
'pre_winter',
'deep_sleep',
]),
}),
'context': <ANY>,
'entity_id': 'select.mock_title_operation_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_setup[aqua_contour][select.mock_title_watering-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'preview',
'setup_mode',
'rest',
'contour_1',
'contour_2',
'contour_3',
'contour_4',
'contour_5',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.mock_title_watering',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Watering',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Watering',
'platform': 'gardena_bluetooth',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'watering_active',
'unique_id': '00000000-0000-0000-0000-000000000003-98bd0d11-0b0e-421a-84e5-ddbf75dc6de4:1',
'unit_of_measurement': None,
})
# ---
# name: test_setup[aqua_contour][select.mock_title_watering-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title Watering',
'options': list([
'preview',
'setup_mode',
'rest',
'contour_1',
'contour_2',
'contour_3',
'contour_4',
'contour_5',
]),
}),
'context': <ANY>,
'entity_id': 'select.mock_title_watering',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'rest',
})
# ---
@@ -31,49 +31,45 @@
'state': '45',
})
# ---
# 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',
# name: test_sensors[aqua_contour][sensor.mock_title_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'context': <ANY>,
'entity_id': 'sensor.mock_title_valve_closing',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2023-01-01T01:01:40+00:00',
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_title_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Battery',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'gardena_bluetooth',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000-0000-0000-0000-000000000003-00002a19-0000-1000-8000-00805f9b34fb',
'unit_of_measurement': '%',
})
# ---
# 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_reported': <ANY>,
'last_updated': <ANY>,
'state': '2023-01-01T01:01:10+00:00',
})
# ---
# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-sensor.mock_title_valve_closing].2
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_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
})
# ---
# name: test_setup[98bd2a19-0b0e-421a-84e5-ddbf75dc6de4-raw0-sensor.mock_title_battery]
# name: test_sensors[aqua_contour][sensor.mock_title_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
@@ -89,7 +85,462 @@
'state': '100',
})
# ---
# name: test_setup[98bd2a19-0b0e-421a-84e5-ddbf75dc6de4-raw0-sensor.mock_title_battery].1
# name: test_sensors[aqua_contour][sensor.mock_title_current_distance-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_title_current_distance',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Current distance',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Current distance',
'platform': 'gardena_bluetooth',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'spray_current_distance',
'unique_id': '00000000-0000-0000-0000-000000000003-98bd0113-0b0e-421a-84e5-ddbf75dc6de4:1',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[aqua_contour][sensor.mock_title_current_distance-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title Current distance',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_title_current_distance',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.333',
})
# ---
# name: test_sensors[aqua_contour][sensor.mock_title_current_flow-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_title_current_flow',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Current flow',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.VOLUME_FLOW_RATE: 'volume_flow_rate'>,
'original_icon': None,
'original_name': 'Current flow',
'platform': 'gardena_bluetooth',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'flow_statistics_current',
'unique_id': '00000000-0000-0000-0000-000000000003-98bd0e19-0b0e-421a-84e5-ddbf75dc6de4',
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,
})
# ---
# name: test_sensors[aqua_contour][sensor.mock_title_current_flow-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'volume_flow_rate',
'friendly_name': 'Mock Title Current flow',
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.mock_title_current_flow',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '222',
})
# ---
# name: test_sensors[aqua_contour][sensor.mock_title_current_sector-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_title_current_sector',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Current sector',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Current sector',
'platform': 'gardena_bluetooth',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'spray_current_sector',
'unique_id': '00000000-0000-0000-0000-000000000003-98bd0114-0b0e-421a-84e5-ddbf75dc6de4:1',
'unit_of_measurement': '°',
})
# ---
# name: test_sensors[aqua_contour][sensor.mock_title_current_sector-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Title Current sector',
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
'unit_of_measurement': '°',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_title_current_sector',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2',
})
# ---
# name: test_sensors[aqua_contour][sensor.mock_title_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'no_error',
'no_water',
'not_enough_water',
'charger_error',
'sprinkler_motor_error',
'valve_motor_error',
'rotation_sensor_error',
'flash_error',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_title_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Error',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Error',
'platform': 'gardena_bluetooth',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'aqua_contour_error',
'unique_id': '00000000-0000-0000-0000-000000000003-aqua_contour_error',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[aqua_contour][sensor.mock_title_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Mock Title Error',
'options': list([
'no_error',
'no_water',
'not_enough_water',
'charger_error',
'sprinkler_motor_error',
'valve_motor_error',
'rotation_sensor_error',
'flash_error',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.mock_title_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'flash_error',
})
# ---
# name: test_sensors[aqua_contour][sensor.mock_title_error_timestamp-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_title_error_timestamp',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Error timestamp',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Error timestamp',
'platform': 'gardena_bluetooth',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'error_timestamp',
'unique_id': '00000000-0000-0000-0000-000000000003-aqua_contour_error_timestamp',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[aqua_contour][sensor.mock_title_error_timestamp-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'Mock Title Error timestamp',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_title_error_timestamp',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2000-01-01T08:00:00+00:00',
})
# ---
# name: test_sensors[aqua_contour][sensor.mock_title_overall_flow-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_title_overall_flow',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Overall flow',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.VOLUME: 'volume'>,
'original_icon': None,
'original_name': 'Overall flow',
'platform': 'gardena_bluetooth',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'flow_statistics_overall',
'unique_id': '00000000-0000-0000-0000-000000000003-98bd0e16-0b0e-421a-84e5-ddbf75dc6de4',
'unit_of_measurement': <UnitOfVolume.LITERS: 'L'>,
})
# ---
# name: test_sensors[aqua_contour][sensor.mock_title_overall_flow-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'volume',
'friendly_name': 'Mock Title Overall flow',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfVolume.LITERS: 'L'>,
}),
'context': <ANY>,
'entity_id': 'sensor.mock_title_overall_flow',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '111',
})
# ---
# name: test_sensors[timer][sensor.mock_title_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_title_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Battery',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'gardena_bluetooth',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000-0000-0000-0000-000000000001-98bd2a19-0b0e-421a-84e5-ddbf75dc6de4',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[timer][sensor.mock_title_battery-state]
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_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_sensors[timer][sensor.mock_title_valve_closing-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mock_title_valve_closing',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Valve closing',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Valve closing',
'platform': 'gardena_bluetooth',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'remaining_open_timestamp',
'unique_id': '00000000-0000-0000-0000-000000000001-remaining_open_timestamp',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[timer][sensor.mock_title_valve_closing-state]
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_reported': <ANY>,
'last_updated': <ANY>,
'state': '2023-01-01T01:00:10+00:00',
})
# ---
# name: test_setup[standard_sensor]
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_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_setup[standard_sensor].1
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
@@ -105,3 +556,45 @@
'state': '10',
})
# ---
# name: test_setup[valve_sensor]
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_reported': <ANY>,
'last_updated': <ANY>,
'state': '2023-01-01T01:01:40+00:00',
})
# ---
# name: test_setup[valve_sensor].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_reported': <ANY>,
'last_updated': <ANY>,
'state': '2023-01-01T01:01:10+00:00',
})
# ---
# name: test_setup[valve_sensor].2
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_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
})
# ---
@@ -4,7 +4,15 @@ import asyncio
from datetime import timedelta
from unittest.mock import Mock, patch
from gardena_bluetooth.const import Battery
from gardena_bluetooth.const import (
AquaContour,
AquaContourBattery,
Battery,
DeviceConfiguration,
DeviceInformation,
)
from habluetooth import BluetoothServiceInfo
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.gardena_bluetooth import DeviceUnavailable
@@ -17,29 +25,74 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.util import utcnow
from . import MISSING_MANUFACTURER_DATA_SERVICE_INFO, WATER_TIMER_SERVICE_INFO
from . import (
AQUA_CONTOUR_SERVICE_INFO,
MISSING_MANUFACTURER_DATA_SERVICE_INFO,
WATER_TIMER_SERVICE_INFO,
get_config_entry,
)
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.bluetooth import inject_bluetooth_service_info
@pytest.mark.parametrize(
("service_info", "char_values"),
[
pytest.param(
WATER_TIMER_SERVICE_INFO,
{
Battery.battery_level.uuid: Battery.battery_level.encode(100),
DeviceInformation.model_number.uuid: DeviceInformation.model_number.encode(
"Model Number TBD"
),
DeviceInformation.firmware_version.uuid: DeviceInformation.firmware_version.encode(
"1.2.3"
),
DeviceConfiguration.custom_device_name.uuid: DeviceConfiguration.custom_device_name.encode(
"My timer"
),
},
id=WATER_TIMER_SERVICE_INFO.name,
),
pytest.param(
AQUA_CONTOUR_SERVICE_INFO,
{
AquaContourBattery.battery_level.uuid: AquaContourBattery.battery_level.encode(
100
),
DeviceInformation.model_number.uuid: DeviceInformation.model_number.encode(
"Aqua Contour"
),
DeviceInformation.firmware_version.uuid: DeviceInformation.firmware_version.encode(
"2.0.0"
),
AquaContour.custom_device_name.uuid: AquaContour.custom_device_name.encode(
"My contour"
),
},
id=AQUA_CONTOUR_SERVICE_INFO.name,
),
],
)
async def test_setup(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_entry: MockConfigEntry,
mock_read_char_raw: dict[str, bytes],
service_info: BluetoothServiceInfo,
char_values: dict[str, bytes],
snapshot: SnapshotAssertion,
) -> None:
"""Test setup creates expected devices."""
mock_read_char_raw[Battery.battery_level.uuid] = Battery.battery_level.encode(100)
inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO)
mock_entry = get_config_entry(service_info)
mock_read_char_raw.update(char_values)
inject_bluetooth_service_info(hass, service_info)
mock_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_entry.entry_id) is True
device = device_registry.async_get_device(
identifiers={(DOMAIN, WATER_TIMER_SERVICE_INFO.address)}
identifiers={(DOMAIN, service_info.address)}
)
assert device == snapshot
@@ -0,0 +1,134 @@
"""Test Gardena Bluetooth sensor."""
from collections.abc import Awaitable, Callable
from unittest.mock import Mock, call
from gardena_bluetooth.const import (
AquaContour,
AquaContourPosition,
AquaContourWatering,
AquaContourWateringMode,
)
from habluetooth import BluetoothServiceInfo
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.select import (
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import AQUA_CONTOUR_SERVICE_INFO, setup_entry
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture
def mock_chars(mock_read_char_raw):
"""Mock data on device."""
mock_read_char_raw[AquaContourWatering.watering_active.uuid] = b"\x00"
return mock_read_char_raw
@pytest.mark.parametrize(
("service_info", "raw"),
[
pytest.param(
AQUA_CONTOUR_SERVICE_INFO,
{
AquaContourWatering.watering_active.uuid: AquaContourWatering.watering_active.encode(
0
),
AquaContour.operation_mode.uuid: AquaContour.operation_mode.encode(0),
AquaContourPosition.active_position.uuid: AquaContourPosition.active_position.encode(
0
),
},
id="aqua_contour",
),
],
)
async def test_setup(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_read_char_raw: dict[str, bytes],
service_info: BluetoothServiceInfo,
raw: dict[str, bytes],
entity_registry: er.EntityRegistry,
) -> None:
"""Test setup creates expected entities."""
mock_read_char_raw.update(raw)
mock_entry = await setup_entry(
hass, platforms=[Platform.SELECT], service_info=service_info
)
await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id)
async def test_state_change(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_read_char_raw: dict[str, bytes],
scan_step: Callable[[], Awaitable[None]],
) -> None:
"""Test setup creates expected entities."""
entity_id = "select.mock_title_watering"
mock_read_char_raw[AquaContourWatering.watering_active.uuid] = (
AquaContourWatering.watering_active.encode(AquaContourWateringMode.REST)
)
await setup_entry(
hass, platforms=[Platform.SELECT], service_info=AQUA_CONTOUR_SERVICE_INFO
)
state = hass.states.get(entity_id)
assert state
assert state.state == "rest"
mock_read_char_raw[AquaContourWatering.watering_active.uuid] = (
AquaContourWatering.watering_active.encode(AquaContourWateringMode.CONTOUR_1)
)
await scan_step()
state = hass.states.get(entity_id)
assert state
assert state.state == "contour_1"
async def test_select(
hass: HomeAssistant,
mock_entry: MockConfigEntry,
mock_client: Mock,
mock_read_char_raw: dict[str, bytes],
) -> None:
"""Test switching makes correct calls."""
mock_read_char_raw[AquaContourWatering.watering_active.uuid] = b"\x00"
entity_id = "select.mock_title_watering"
await setup_entry(
hass, platforms=[Platform.SELECT], service_info=AQUA_CONTOUR_SERVICE_INFO
)
assert hass.states.get(entity_id)
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "rest"},
blocking=True,
)
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "contour_3"},
blocking=True,
)
assert mock_client.write_char.mock_calls == [
call(AquaContourWatering.watering_active, 0),
call(AquaContourWatering.watering_active, 3),
]
@@ -1,28 +1,44 @@
"""Test Gardena Bluetooth sensor."""
from collections.abc import Awaitable, Callable
from datetime import datetime
from gardena_bluetooth.const import Battery, Sensor, Valve
from gardena_bluetooth.const import (
AquaContourBattery,
AquaContourErrorCode,
Battery,
EventHistory,
FlowStatistics,
Sensor,
Spray,
Valve,
)
from gardena_bluetooth.parse import ErrorData
from habluetooth import BluetoothServiceInfo
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_entry
from . import AQUA_CONTOUR_SERVICE_INFO, WATER_TIMER_SERVICE_INFO, setup_entry
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.parametrize(
("uuid", "raw", "entity_id"),
("service_info", "uuid", "raw", "entity_id"),
[
(
pytest.param(
WATER_TIMER_SERVICE_INFO,
Battery.battery_level.uuid,
[Battery.battery_level.encode(100), Battery.battery_level.encode(10)],
"sensor.mock_title_battery",
id="standard_sensor",
),
(
pytest.param(
WATER_TIMER_SERVICE_INFO,
Valve.remaining_open_time.uuid,
[
Valve.remaining_open_time.encode(100),
@@ -30,23 +46,23 @@ from tests.common import MockConfigEntry
Valve.remaining_open_time.encode(0),
],
"sensor.mock_title_valve_closing",
id="valve_sensor",
),
],
)
async def test_setup(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_entry: MockConfigEntry,
mock_read_char_raw: dict[str, bytes],
scan_step: Callable[[], Awaitable[None]],
service_info: BluetoothServiceInfo,
uuid: str,
raw: list[bytes],
entity_id: str,
) -> None:
"""Test setup creates expected entities."""
mock_read_char_raw[uuid] = raw[0]
await setup_entry(hass, mock_entry, [Platform.SENSOR])
await setup_entry(hass, platforms=[Platform.SENSOR], service_info=service_info)
assert hass.states.get(entity_id) == snapshot
for char_raw in raw[1:]:
@@ -55,6 +71,54 @@ async def test_setup(
assert hass.states.get(entity_id) == snapshot
@pytest.mark.parametrize(
("service_info", "raw"),
[
pytest.param(
WATER_TIMER_SERVICE_INFO,
{
Battery.battery_level.uuid: Battery.battery_level.encode(100),
Valve.remaining_open_time.uuid: Valve.remaining_open_time.encode(10),
},
id="timer",
),
pytest.param(
AQUA_CONTOUR_SERVICE_INFO,
{
AquaContourBattery.battery_level.uuid: AquaContourBattery.battery_level.encode(
100
),
FlowStatistics.overall.uuid: FlowStatistics.overall.encode(111),
FlowStatistics.current.uuid: FlowStatistics.overall.encode(222),
Spray.current_distance.uuid: Spray.current_distance.encode(333),
Spray.current_sector.uuid: Spray.current_sector.encode(2),
EventHistory.error.uuid: EventHistory.error.encode(
ErrorData(
1, 1, datetime(2000, 1, 1), AquaContourErrorCode.FLASH_ERROR
)
),
},
id="aqua_contour",
),
],
)
async def test_sensors(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_read_char_raw: dict[str, bytes],
service_info: BluetoothServiceInfo,
raw: dict[str, bytes],
) -> None:
"""Test setup creates expected entities."""
mock_read_char_raw.update(raw)
mock_entry = await setup_entry(
hass, platforms=[Platform.SENSOR], service_info=service_info
)
await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id)
async def test_connected_state(
hass: HomeAssistant,
snapshot: SnapshotAssertion,