aioesphomeapi/tests/test_model.py

658 lines
19 KiB
Python
Raw Permalink Normal View History

from __future__ import annotations
2021-07-12 20:09:17 +02:00
from dataclasses import dataclass, field
import pytest
from google.protobuf import message
2021-07-12 20:09:17 +02:00
from aioesphomeapi.api_pb2 import (
AlarmControlPanelStateResponse,
2021-07-12 20:09:17 +02:00
BinarySensorStateResponse,
BluetoothGATTCharacteristic,
BluetoothGATTDescriptor,
BluetoothGATTGetServicesResponse,
2021-07-12 20:09:17 +02:00
ClimateStateResponse,
CoverStateResponse,
DateStateResponse,
2024-04-22 05:58:16 +02:00
DateTimeStateResponse,
2021-07-12 20:09:17 +02:00
DeviceInfoResponse,
EventResponse,
2021-07-12 20:09:17 +02:00
FanStateResponse,
HomeassistantServiceMap,
HomeassistantServiceResponse,
LightStateResponse,
ListEntitiesAlarmControlPanelResponse,
2021-07-12 20:09:17 +02:00
ListEntitiesBinarySensorResponse,
2021-11-29 05:06:41 +01:00
ListEntitiesButtonResponse,
2021-07-12 20:09:17 +02:00
ListEntitiesClimateResponse,
ListEntitiesCoverResponse,
ListEntitiesDateResponse,
2024-04-22 05:58:16 +02:00
ListEntitiesDateTimeResponse,
ListEntitiesEventResponse,
2021-07-12 20:09:17 +02:00
ListEntitiesFanResponse,
ListEntitiesLightResponse,
2022-01-11 02:29:19 +01:00
ListEntitiesLockResponse,
2022-05-18 03:28:40 +02:00
ListEntitiesMediaPlayerResponse,
2021-07-12 20:09:17 +02:00
ListEntitiesNumberResponse,
2021-07-26 20:51:12 +02:00
ListEntitiesSelectResponse,
2021-07-12 20:09:17 +02:00
ListEntitiesSensorResponse,
ListEntitiesServicesArgument,
ListEntitiesServicesResponse,
ListEntitiesSwitchResponse,
ListEntitiesTextSensorResponse,
2024-03-20 02:31:00 +01:00
ListEntitiesTimeResponse,
2024-06-11 06:29:13 +02:00
ListEntitiesUpdateResponse,
ListEntitiesValveResponse,
2022-01-11 02:29:19 +01:00
LockStateResponse,
2022-05-18 03:28:40 +02:00
MediaPlayerStateResponse,
2021-07-12 20:09:17 +02:00
NumberStateResponse,
2021-07-26 20:51:12 +02:00
SelectStateResponse,
2021-07-12 20:09:17 +02:00
SensorStateResponse,
ServiceArgType,
SwitchStateResponse,
TextSensorStateResponse,
TextStateResponse,
2024-03-20 02:31:00 +01:00
TimeStateResponse,
2024-06-11 06:29:13 +02:00
UpdateStateResponse,
ValveStateResponse,
2021-07-12 20:09:17 +02:00
)
from aioesphomeapi.model import (
_TYPE_TO_NAME,
AlarmControlPanelEntityState,
AlarmControlPanelInfo,
2021-07-12 20:09:17 +02:00
APIIntEnum,
APIModelBase,
APIVersion,
BinarySensorInfo,
BinarySensorState,
)
from aioesphomeapi.model import (
BluetoothGATTCharacteristic as BluetoothGATTCharacteristicModel,
)
from aioesphomeapi.model import BluetoothGATTDescriptor as BluetoothGATTDescriptorModel
from aioesphomeapi.model import BluetoothGATTService as BluetoothGATTServiceModel
from aioesphomeapi.model import BluetoothGATTServices as BluetoothGATTServicesModel
from aioesphomeapi.model import (
BluetoothProxyFeature,
2021-11-29 05:06:41 +01:00
ButtonInfo,
CameraInfo,
2021-07-12 20:09:17 +02:00
ClimateInfo,
ClimatePreset,
ClimateState,
CoverInfo,
CoverState,
DateInfo,
DateState,
2024-04-22 05:58:16 +02:00
DateTimeInfo,
DateTimeState,
2021-07-12 20:09:17 +02:00
DeviceInfo,
Event,
EventInfo,
2021-07-12 20:09:17 +02:00
FanInfo,
FanState,
HomeassistantServiceCall,
LegacyCoverState,
LightColorCapability,
2021-07-12 20:09:17 +02:00
LightInfo,
LightState,
2022-01-11 02:29:19 +01:00
LockEntityState,
LockInfo,
2022-05-18 03:28:40 +02:00
MediaPlayerEntityState,
MediaPlayerInfo,
2021-07-12 20:09:17 +02:00
NumberInfo,
NumberState,
2021-07-26 20:51:12 +02:00
SelectInfo,
SelectState,
2021-07-12 20:09:17 +02:00
SensorInfo,
SensorState,
SirenInfo,
2021-07-12 20:09:17 +02:00
SwitchInfo,
SwitchState,
TextInfo,
2021-07-12 20:09:17 +02:00
TextSensorInfo,
TextSensorState,
TextState,
2024-03-20 02:31:00 +01:00
TimeInfo,
TimeState,
2024-06-11 06:29:13 +02:00
UpdateInfo,
UpdateState,
2021-07-12 20:09:17 +02:00
UserService,
UserServiceArg,
UserServiceArgType,
ValveInfo,
ValveState,
VoiceAssistantFeature,
build_unique_id,
2021-07-12 20:09:17 +02:00
converter_field,
)
class DummyIntEnum(APIIntEnum):
DEFAULT = 0
MY_VAL = 1
@pytest.mark.parametrize(
"input, output",
[
(0, DummyIntEnum.DEFAULT),
(1, DummyIntEnum.MY_VAL),
(2, None),
(-1, None),
(DummyIntEnum.DEFAULT, DummyIntEnum.DEFAULT),
(DummyIntEnum.MY_VAL, DummyIntEnum.MY_VAL),
],
)
def test_api_int_enum_convert(input, output):
v = DummyIntEnum.convert(input)
assert v == output
assert v is None or isinstance(v, DummyIntEnum)
@pytest.mark.parametrize(
"input, output",
[
([], []),
([1], [DummyIntEnum.MY_VAL]),
([0, 1], [DummyIntEnum.DEFAULT, DummyIntEnum.MY_VAL]),
([-1], []),
([0, -1], [DummyIntEnum.DEFAULT]),
([DummyIntEnum.DEFAULT], [DummyIntEnum.DEFAULT]),
],
)
def test_api_int_enum_convert_list(input, output):
v = DummyIntEnum.convert_list(input)
assert v == output
assert all(isinstance(x, DummyIntEnum) for x in v)
@dataclass(frozen=True)
class DummyAPIModel(APIModelBase):
val1: int = 0
val2: DummyIntEnum | None = converter_field(
2021-07-12 20:09:17 +02:00
default=DummyIntEnum.DEFAULT, converter=DummyIntEnum.convert
)
@dataclass(frozen=True)
class ListAPIModel(APIModelBase):
val: list[DummyAPIModel] = field(default_factory=list)
2021-07-12 20:09:17 +02:00
def test_api_model_base_converter():
assert DummyAPIModel().val2 == DummyIntEnum.DEFAULT
assert isinstance(DummyAPIModel().val2, DummyIntEnum)
assert DummyAPIModel(val2=0).val2 == DummyIntEnum.DEFAULT
assert isinstance(DummyAPIModel().val2, DummyIntEnum)
assert DummyAPIModel(val2=-1).val2 is None
def test_api_model_base_to_dict():
assert DummyAPIModel().to_dict() == {
"val1": 0,
"val2": 0,
}
assert DummyAPIModel(val1=-1, val2=1).to_dict() == {
"val1": -1,
"val2": 1,
}
assert ListAPIModel(val=[DummyAPIModel()]).to_dict() == {
"val": [
{
"val1": 0,
"val2": 0,
}
]
}
def test_api_model_base_from_dict():
assert DummyAPIModel.from_dict({}) == DummyAPIModel()
assert DummyAPIModel.from_dict(
{
"val1": -1,
"val2": -1,
}
) == DummyAPIModel(val1=-1, val2=None)
assert DummyAPIModel.from_dict(
{
"val1": -1,
"unknown": 100,
}
) == DummyAPIModel(val1=-1)
2021-07-12 20:09:17 +02:00
assert ListAPIModel.from_dict({}) == ListAPIModel()
assert ListAPIModel.from_dict({"val": []}) == ListAPIModel()
def test_api_model_base_from_pb():
class DummyPB:
def __init__(self, val1=0, val2=0):
self.val1 = val1
self.val2 = val2
assert DummyAPIModel.from_pb(DummyPB()) == DummyAPIModel()
assert DummyAPIModel.from_pb(DummyPB(val1=-1, val2=-1)) == DummyAPIModel(
val1=-1, val2=None
)
def test_api_version_ord():
assert APIVersion(1, 0) == APIVersion(1, 0)
assert APIVersion(1, 0) < APIVersion(1, 1)
assert APIVersion(1, 1) <= APIVersion(1, 1)
assert APIVersion(1, 0) < APIVersion(2, 0)
assert not (APIVersion(2, 1) <= APIVersion(2, 0))
assert APIVersion(2, 1) > APIVersion(2, 0)
@pytest.mark.parametrize(
"model, pb",
[
(DeviceInfo, DeviceInfoResponse),
(BinarySensorInfo, ListEntitiesBinarySensorResponse),
(BinarySensorState, BinarySensorStateResponse),
(CoverInfo, ListEntitiesCoverResponse),
(CoverState, CoverStateResponse),
(FanInfo, ListEntitiesFanResponse),
(FanState, FanStateResponse),
(LightInfo, ListEntitiesLightResponse),
(LightState, LightStateResponse),
(SensorInfo, ListEntitiesSensorResponse),
(SensorState, SensorStateResponse),
(SwitchInfo, ListEntitiesSwitchResponse),
(SwitchState, SwitchStateResponse),
(TextSensorInfo, ListEntitiesTextSensorResponse),
(TextSensorState, TextSensorStateResponse),
(ClimateInfo, ListEntitiesClimateResponse),
(ClimateState, ClimateStateResponse),
(NumberInfo, ListEntitiesNumberResponse),
(NumberState, NumberStateResponse),
(DateInfo, ListEntitiesDateResponse),
(DateState, DateStateResponse),
2021-07-26 20:51:12 +02:00
(SelectInfo, ListEntitiesSelectResponse),
(SelectState, SelectStateResponse),
2021-07-12 20:09:17 +02:00
(HomeassistantServiceCall, HomeassistantServiceResponse),
(UserServiceArg, ListEntitiesServicesArgument),
(UserService, ListEntitiesServicesResponse),
2021-11-29 05:06:41 +01:00
(ButtonInfo, ListEntitiesButtonResponse),
2022-01-11 02:29:19 +01:00
(LockInfo, ListEntitiesLockResponse),
(LockEntityState, LockStateResponse),
(ValveInfo, ListEntitiesValveResponse),
(ValveState, ValveStateResponse),
2022-05-18 03:28:40 +02:00
(MediaPlayerInfo, ListEntitiesMediaPlayerResponse),
(MediaPlayerEntityState, MediaPlayerStateResponse),
(AlarmControlPanelInfo, ListEntitiesAlarmControlPanelResponse),
(AlarmControlPanelEntityState, AlarmControlPanelStateResponse),
(TextState, TextStateResponse),
2024-03-20 02:31:00 +01:00
(TimeInfo, ListEntitiesTimeResponse),
(TimeState, TimeStateResponse),
2024-04-22 05:58:16 +02:00
(DateTimeInfo, ListEntitiesDateTimeResponse),
(DateTimeState, DateTimeStateResponse),
(EventInfo, ListEntitiesEventResponse),
(Event, EventResponse),
2024-06-11 06:29:13 +02:00
(UpdateInfo, ListEntitiesUpdateResponse),
(UpdateState, UpdateStateResponse),
2021-07-12 20:09:17 +02:00
],
)
def test_basic_pb_conversions(model, pb):
assert model.from_pb(pb()) == model()
@pytest.mark.parametrize(
"state, version, out",
[
(CoverState(legacy_state=LegacyCoverState.OPEN), (1, 0), False),
(CoverState(legacy_state=LegacyCoverState.CLOSED), (1, 0), True),
(CoverState(position=1.0), (1, 1), False),
(CoverState(position=0.5), (1, 1), False),
(CoverState(position=0.0), (1, 1), True),
],
)
def test_cover_state_legacy_state(state, version, out):
assert state.is_closed(APIVersion(*version)) is out
@pytest.mark.parametrize(
"state, version, out",
[
(ClimateInfo(legacy_supports_away=False), (1, 4), []),
(
ClimateInfo(legacy_supports_away=True),
(1, 4),
[ClimatePreset.HOME, ClimatePreset.AWAY],
),
(ClimateInfo(supported_presets=[ClimatePreset.HOME]), (1, 4), []),
(ClimateInfo(supported_presets=[], legacy_supports_away=True), (1, 5), []),
(
ClimateInfo(supported_presets=[ClimatePreset.HOME]),
(1, 5),
[ClimatePreset.HOME],
),
],
)
def test_climate_info_supported_presets_compat(state, version, out):
assert state.supported_presets_compat(APIVersion(*version)) == out
@pytest.mark.parametrize(
"state, version, out",
[
(ClimateState(legacy_away=False), (1, 4), ClimatePreset.HOME),
(ClimateState(legacy_away=True), (1, 4), ClimatePreset.AWAY),
(
ClimateState(legacy_away=True, preset=ClimatePreset.HOME),
(1, 4),
ClimatePreset.AWAY,
),
(ClimateState(preset=ClimatePreset.HOME), (1, 5), ClimatePreset.HOME),
(ClimateState(preset=ClimatePreset.BOOST), (1, 5), ClimatePreset.BOOST),
(
ClimateState(legacy_away=True, preset=ClimatePreset.BOOST),
(1, 5),
ClimatePreset.BOOST,
),
],
)
def test_climate_state_preset_compat(state, version, out):
assert state.preset_compat(APIVersion(*version)) == out
def test_homeassistant_service_map_conversion():
assert HomeassistantServiceCall.from_pb(
HomeassistantServiceResponse(
data=[HomeassistantServiceMap(key="key", value="value")]
)
) == HomeassistantServiceCall(data={"key": "value"})
assert HomeassistantServiceCall.from_dict(
{"data": {"key": "value"}}
) == HomeassistantServiceCall(data={"key": "value"})
def test_user_service_conversion():
assert UserService.from_pb(
ListEntitiesServicesResponse(
args=[
ListEntitiesServicesArgument(
name="arg", type=ServiceArgType.SERVICE_ARG_TYPE_INT
)
]
)
) == UserService(args=[UserServiceArg(name="arg", type=UserServiceArgType.INT)])
assert UserService.from_dict({"args": [{"name": "arg", "type": 1}]}) == UserService(
args=[UserServiceArg(name="arg", type=UserServiceArgType.INT)]
)
assert UserService.from_dict(
{"args": [{"name": "arg", "type_": 1}]}
) == UserService(args=[UserServiceArg(name="arg", type=UserServiceArgType.INT)])
@pytest.mark.parametrize(
"model",
[
BinarySensorInfo,
ButtonInfo,
CoverInfo,
FanInfo,
LightInfo,
NumberInfo,
DateInfo,
SelectInfo,
SensorInfo,
SirenInfo,
SwitchInfo,
TextSensorInfo,
CameraInfo,
ClimateInfo,
LockInfo,
ValveInfo,
MediaPlayerInfo,
AlarmControlPanelInfo,
TextInfo,
2024-03-20 02:31:00 +01:00
TimeInfo,
],
)
def test_build_unique_id(model):
obj = model(object_id="id")
assert build_unique_id("mac", obj) == f"mac-{_TYPE_TO_NAME[type(obj)]}-id"
@pytest.mark.parametrize(
("version", "flags"),
[
(1, BluetoothProxyFeature.PASSIVE_SCAN),
(
2,
BluetoothProxyFeature.PASSIVE_SCAN
| BluetoothProxyFeature.ACTIVE_CONNECTIONS,
),
(
3,
BluetoothProxyFeature.PASSIVE_SCAN
| BluetoothProxyFeature.ACTIVE_CONNECTIONS
| BluetoothProxyFeature.REMOTE_CACHING,
),
(
4,
BluetoothProxyFeature.PASSIVE_SCAN
| BluetoothProxyFeature.ACTIVE_CONNECTIONS
| BluetoothProxyFeature.REMOTE_CACHING
| BluetoothProxyFeature.PAIRING,
),
(
5,
BluetoothProxyFeature.PASSIVE_SCAN
| BluetoothProxyFeature.ACTIVE_CONNECTIONS
| BluetoothProxyFeature.REMOTE_CACHING
| BluetoothProxyFeature.PAIRING
| BluetoothProxyFeature.CACHE_CLEARING,
),
],
)
def test_bluetooth_backcompat_for_device_info(
version: int, flags: BluetoothProxyFeature
) -> None:
info = DeviceInfo(
legacy_bluetooth_proxy_version=version, bluetooth_proxy_feature_flags=42
)
assert info.bluetooth_proxy_feature_flags_compat(APIVersion(1, 8)) is flags
assert info.bluetooth_proxy_feature_flags_compat(APIVersion(1, 9)) == 42
# Add va compat test
@pytest.mark.parametrize(
("version", "flags"),
[
(1, VoiceAssistantFeature.VOICE_ASSISTANT),
(2, VoiceAssistantFeature.VOICE_ASSISTANT | VoiceAssistantFeature.SPEAKER),
],
)
def test_voice_assistant_backcompat_for_device_info(
version: int, flags: VoiceAssistantFeature
) -> None:
info = DeviceInfo(
legacy_voice_assistant_version=version, voice_assistant_feature_flags=42
)
assert info.voice_assistant_feature_flags_compat(APIVersion(1, 9)) is flags
assert info.voice_assistant_feature_flags_compat(APIVersion(1, 10)) == 42
@pytest.mark.parametrize(
(
"legacy_supports_brightness",
"legacy_supports_rgb",
"legacy_supports_white_value",
"legacy_supports_color_temperature",
"capability",
),
[
(False, False, False, False, [LightColorCapability.ON_OFF]),
(
True,
False,
False,
False,
[LightColorCapability.ON_OFF | LightColorCapability.BRIGHTNESS],
),
(
True,
False,
False,
True,
[
LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS
| LightColorCapability.COLOR_TEMPERATURE
],
),
(
True,
True,
False,
False,
[
LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS
| LightColorCapability.RGB
],
),
(
True,
True,
True,
False,
[
LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS
| LightColorCapability.RGB
| LightColorCapability.WHITE
],
),
(
True,
True,
False,
True,
[
LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS
| LightColorCapability.RGB
| LightColorCapability.COLOR_TEMPERATURE
],
),
(
True,
True,
True,
True,
[
LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS
| LightColorCapability.RGB
| LightColorCapability.WHITE
| LightColorCapability.COLOR_TEMPERATURE
],
),
],
)
def test_supported_color_modes_compat(
legacy_supports_brightness: bool,
legacy_supports_rgb: bool,
legacy_supports_white_value: bool,
legacy_supports_color_temperature: bool,
capability: list[LightColorCapability],
) -> None:
info = LightInfo(
legacy_supports_brightness=legacy_supports_brightness,
legacy_supports_rgb=legacy_supports_rgb,
legacy_supports_white_value=legacy_supports_white_value,
legacy_supports_color_temperature=legacy_supports_color_temperature,
supported_color_modes=[42],
)
assert info.supported_color_modes_compat(APIVersion(1, 5)) == capability
assert info.supported_color_modes_compat(APIVersion(1, 9)) == [42]
@pytest.mark.asyncio
async def test_bluetooth_gatt_services_from_dict() -> None:
"""Test bluetooth_gatt_get_services success case."""
services: message.Message = BluetoothGATTGetServicesResponse(
address=1234,
services=[
{
"uuid": [1, 1],
"handle": 1,
"characteristics": [
{
"uuid": [1, 2],
"handle": 2,
"properties": 1,
"descriptors": [
{"uuid": [1, 3], "handle": 3},
],
},
],
}
],
)
services = BluetoothGATTServicesModel.from_pb(services)
assert services.services[0] == BluetoothGATTServiceModel(
uuid=[1, 1],
handle=1,
characteristics=[
BluetoothGATTCharacteristic(
uuid=[1, 2],
handle=2,
properties=1,
descriptors=[BluetoothGATTDescriptor(uuid=[1, 3], handle=3)],
)
],
)
services == BluetoothGATTServicesModel.from_dict(
{
"services": [
{
"uuid": [1, 1],
"handle": 1,
"characteristics": [
{
"uuid": [1, 2],
"handle": 2,
"properties": 1,
"descriptors": [
{"uuid": [1, 3], "handle": 3},
],
},
],
}
]
}
)
assert services.services[0] == BluetoothGATTServiceModel(
uuid=[1, 1],
handle=1,
characteristics=[
BluetoothGATTCharacteristic(
uuid=[1, 2],
handle=2,
properties=1,
descriptors=[BluetoothGATTDescriptor(uuid=[1, 3], handle=3)],
)
],
)
assert BluetoothGATTCharacteristicModel.from_dict(
{
"uuid": [1, 2],
"handle": 2,
"properties": 1,
"descriptors": [],
}
) == BluetoothGATTCharacteristicModel(
uuid=[1, 2],
handle=2,
properties=1,
descriptors=[],
)
assert BluetoothGATTDescriptorModel.from_dict(
{"uuid": [1, 3], "handle": 3},
) == BluetoothGATTDescriptorModel(uuid=[1, 3], handle=3)