mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Add support for aqua contour/precise line of gardena products (#165326)
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user