aioesphomeapi/aioesphomeapi/model.py

1102 lines
30 KiB
Python

from __future__ import annotations
import enum
import sys
from collections.abc import Iterable
from dataclasses import asdict, dataclass, field, fields
from functools import cache, lru_cache, partial
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
from uuid import UUID
from .api_pb2 import BluetoothLERawAdvertisement # type: ignore[attr-defined]
from .util import fix_float_single_double_conversion
if sys.version_info[:2] < (3, 10):
_dataclass_decorator = dataclass
_frozen_dataclass_decorator = partial(dataclass, frozen=True)
else:
_dataclass_decorator = partial(dataclass, slots=True)
_frozen_dataclass_decorator = partial(dataclass, frozen=True, slots=True)
if TYPE_CHECKING:
from .api_pb2 import ( # type: ignore
BluetoothLEAdvertisementResponse,
BluetoothLERawAdvertisementsResponse,
HomeassistantServiceMap,
)
# All fields in here should have defaults set
# Home Assistant depends on these fields being constructible
# with args from a previous version of Home Assistant.
# The default value should *always* be the Protobuf default value
# for a field (False, 0, empty string, enum with value 0, ...)
_T = TypeVar("_T", bound="APIIntEnum")
_V = TypeVar("_V")
class APIIntEnum(enum.IntEnum):
"""Base class for int enum values in API model."""
@classmethod
def convert(cls: type[_T], value: int) -> _T | None:
try:
return cls(value)
except ValueError:
return None
@classmethod
def convert_list(cls: type[_T], value: list[int]) -> list[_T]:
ret = []
for x in value:
try:
ret.append(cls(x))
except ValueError:
pass
return ret
# Fields do not change so we can cache the result
# of calling fields() on the dataclass
cached_fields = cache(fields)
@_frozen_dataclass_decorator
class APIModelBase:
def __post_init__(self) -> None:
for field_ in cached_fields(type(self)): # type: ignore[arg-type]
convert = field_.metadata.get("converter")
if convert is None:
continue
val = getattr(self, field_.name)
# use this setattr to prevent FrozenInstanceError
object.__setattr__(self, field_.name, convert(val))
def to_dict(self) -> dict[str, Any]:
return asdict(self) # type: ignore[no-any-return, call-overload]
@classmethod
def from_dict(
cls: type[_V], data: dict[str, Any], *, ignore_missing: bool = True
) -> _V:
init_args = {
f.name: data[f.name]
for f in cached_fields(cls) # type: ignore[arg-type]
if f.name in data or (not ignore_missing)
}
return cls(**init_args)
@classmethod
def from_pb(cls: type[_V], data: Any) -> _V:
return cls(**{f.name: getattr(data, f.name) for f in cached_fields(cls)}) # type: ignore[arg-type]
def converter_field(*, converter: Callable[[Any], _V], **kwargs: Any) -> _V:
metadata = kwargs.pop("metadata", {})
metadata["converter"] = converter
return cast(_V, field(metadata=metadata, **kwargs))
@dataclass(frozen=True, order=True)
class APIVersion(APIModelBase):
major: int = 0
minor: int = 0
class BluetoothProxyFeature(enum.IntFlag):
PASSIVE_SCAN = 1 << 0
ACTIVE_CONNECTIONS = 1 << 1
REMOTE_CACHING = 1 << 2
PAIRING = 1 << 3
CACHE_CLEARING = 1 << 4
RAW_ADVERTISEMENTS = 1 << 5
class BluetoothProxySubscriptionFlag(enum.IntFlag):
RAW_ADVERTISEMENTS = 1 << 0
@_frozen_dataclass_decorator
class DeviceInfo(APIModelBase):
uses_password: bool = False
name: str = ""
friendly_name: str = ""
mac_address: str = ""
compilation_time: str = ""
model: str = ""
manufacturer: str = ""
has_deep_sleep: bool = False
esphome_version: str = ""
project_name: str = ""
project_version: str = ""
webserver_port: int = 0
voice_assistant_version: int = 0
legacy_bluetooth_proxy_version: int = 0
bluetooth_proxy_feature_flags: int = 0
def bluetooth_proxy_feature_flags_compat(self, api_version: APIVersion) -> int:
if api_version < APIVersion(1, 9):
flags: int = 0
if self.legacy_bluetooth_proxy_version >= 1:
flags |= BluetoothProxyFeature.PASSIVE_SCAN
if self.legacy_bluetooth_proxy_version >= 2:
flags |= BluetoothProxyFeature.ACTIVE_CONNECTIONS
if self.legacy_bluetooth_proxy_version >= 3:
flags |= BluetoothProxyFeature.REMOTE_CACHING
if self.legacy_bluetooth_proxy_version >= 4:
flags |= BluetoothProxyFeature.PAIRING
if self.legacy_bluetooth_proxy_version >= 5:
flags |= BluetoothProxyFeature.CACHE_CLEARING
return flags
return self.bluetooth_proxy_feature_flags
class EntityCategory(APIIntEnum):
NONE = 0
CONFIG = 1
DIAGNOSTIC = 2
@_frozen_dataclass_decorator
class EntityInfo(APIModelBase):
object_id: str = ""
key: int = 0
name: str = ""
unique_id: str = ""
disabled_by_default: bool = False
icon: str = ""
entity_category: EntityCategory | None = converter_field(
default=EntityCategory.NONE, converter=EntityCategory.convert
)
@_frozen_dataclass_decorator
class EntityState(APIModelBase):
key: int = 0
# ==================== BINARY SENSOR ====================
@_frozen_dataclass_decorator
class BinarySensorInfo(EntityInfo):
device_class: str = ""
is_status_binary_sensor: bool = False
@_frozen_dataclass_decorator
class BinarySensorState(EntityState):
state: bool = False
missing_state: bool = False
# ==================== COVER ====================
@_frozen_dataclass_decorator
class CoverInfo(EntityInfo):
assumed_state: bool = False
supports_stop: bool = False
supports_position: bool = False
supports_tilt: bool = False
device_class: str = ""
class LegacyCoverState(APIIntEnum):
OPEN = 0
CLOSED = 1
class LegacyCoverCommand(APIIntEnum):
OPEN = 0
CLOSE = 1
STOP = 2
class CoverOperation(APIIntEnum):
IDLE = 0
IS_OPENING = 1
IS_CLOSING = 2
@_frozen_dataclass_decorator
class CoverState(EntityState):
legacy_state: LegacyCoverState | None = converter_field(
default=LegacyCoverState.OPEN, converter=LegacyCoverState.convert
)
position: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
tilt: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
current_operation: CoverOperation | None = converter_field(
default=CoverOperation.IDLE, converter=CoverOperation.convert
)
def is_closed(self, api_version: APIVersion) -> bool:
if api_version < APIVersion(1, 1):
return self.legacy_state == LegacyCoverState.CLOSED
return self.position == 0.0
# ==================== FAN ====================
@_frozen_dataclass_decorator
class FanInfo(EntityInfo):
supports_oscillation: bool = False
supports_speed: bool = False
supports_direction: bool = False
supported_speed_levels: int = 0
class FanSpeed(APIIntEnum):
LOW = 0
MEDIUM = 1
HIGH = 2
class FanDirection(APIIntEnum):
FORWARD = 0
REVERSE = 1
@_frozen_dataclass_decorator
class FanState(EntityState):
state: bool = False
oscillating: bool = False
speed: FanSpeed | None = converter_field(
default=FanSpeed.LOW, converter=FanSpeed.convert
)
speed_level: int = 0
direction: FanDirection | None = converter_field(
default=FanDirection.FORWARD, converter=FanDirection.convert
)
# ==================== LIGHT ====================
class LightColorCapability(enum.IntFlag):
ON_OFF = 1 << 0
BRIGHTNESS = 1 << 1
WHITE = 1 << 2
COLOR_TEMPERATURE = 1 << 3
COLD_WARM_WHITE = 1 << 4
RGB = 1 << 5
@_frozen_dataclass_decorator
class LightInfo(EntityInfo):
supported_color_modes: list[int] = converter_field(
default_factory=list, converter=list
)
min_mireds: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
max_mireds: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
effects: list[str] = converter_field(default_factory=list, converter=list)
# deprecated, do not use
legacy_supports_brightness: bool = False
legacy_supports_rgb: bool = False
legacy_supports_white_value: bool = False
legacy_supports_color_temperature: bool = False
def supported_color_modes_compat(self, api_version: APIVersion) -> list[int]:
if api_version < APIVersion(1, 6):
key = (
self.legacy_supports_brightness,
self.legacy_supports_rgb,
self.legacy_supports_white_value,
self.legacy_supports_color_temperature,
)
# map legacy flags to color modes,
# key: (brightness, rgb, white, color_temp)
modes_map = {
(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
],
}
return cast(list[int], modes_map[key]) if key in modes_map else []
return self.supported_color_modes
@_frozen_dataclass_decorator
class LightState(EntityState):
state: bool = False
brightness: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
color_mode: int = 0
color_brightness: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
red: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
green: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
blue: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
white: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
color_temperature: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
cold_white: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
warm_white: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
effect: str = ""
# ==================== SENSOR ====================
class SensorStateClass(APIIntEnum):
NONE = 0
MEASUREMENT = 1
TOTAL_INCREASING = 2
TOTAL = 3
class LastResetType(APIIntEnum):
NONE = 0
NEVER = 1
AUTO = 2
@_frozen_dataclass_decorator
class SensorInfo(EntityInfo):
device_class: str = ""
unit_of_measurement: str = ""
accuracy_decimals: int = 0
force_update: bool = False
state_class: SensorStateClass | None = converter_field(
default=SensorStateClass.NONE, converter=SensorStateClass.convert
)
last_reset_type: LastResetType | None = converter_field(
default=LastResetType.NONE, converter=LastResetType.convert
)
@_frozen_dataclass_decorator
class SensorState(EntityState):
state: float = 0.0
missing_state: bool = False
# ==================== SWITCH ====================
@_frozen_dataclass_decorator
class SwitchInfo(EntityInfo):
assumed_state: bool = False
device_class: str = ""
@_frozen_dataclass_decorator
class SwitchState(EntityState):
state: bool = False
# ==================== TEXT SENSOR ====================
@_frozen_dataclass_decorator
class TextSensorInfo(EntityInfo):
pass
@_frozen_dataclass_decorator
class TextSensorState(EntityState):
state: str = ""
missing_state: bool = False
# ==================== CAMERA ====================
@_frozen_dataclass_decorator
class CameraInfo(EntityInfo):
pass
@_frozen_dataclass_decorator
class CameraState(EntityState):
data: bytes = field(default_factory=bytes)
# ==================== CLIMATE ====================
class ClimateMode(APIIntEnum):
OFF = 0
HEAT_COOL = 1
COOL = 2
HEAT = 3
FAN_ONLY = 4
DRY = 5
AUTO = 6
class ClimateFanMode(APIIntEnum):
ON = 0
OFF = 1
AUTO = 2
LOW = 3
MEDIUM = 4
HIGH = 5
MIDDLE = 6
FOCUS = 7
DIFFUSE = 8
QUIET = 9
class ClimateSwingMode(APIIntEnum):
OFF = 0
BOTH = 1
VERTICAL = 2
HORIZONTAL = 3
class ClimateAction(APIIntEnum):
OFF = 0
COOLING = 2
HEATING = 3
IDLE = 4
DRYING = 5
FAN = 6
class ClimatePreset(APIIntEnum):
NONE = 0
HOME = 1
AWAY = 2
BOOST = 3
COMFORT = 4
ECO = 5
SLEEP = 6
ACTIVITY = 7
@_frozen_dataclass_decorator
class ClimateInfo(EntityInfo):
supports_current_temperature: bool = False
supports_two_point_target_temperature: bool = False
supported_modes: list[ClimateMode] = converter_field(
default_factory=list, converter=ClimateMode.convert_list
)
visual_min_temperature: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
visual_max_temperature: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
visual_target_temperature_step: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
visual_current_temperature_step: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
legacy_supports_away: bool = False
supports_action: bool = False
supported_fan_modes: list[ClimateFanMode] = converter_field(
default_factory=list, converter=ClimateFanMode.convert_list
)
supported_swing_modes: list[ClimateSwingMode] = converter_field(
default_factory=list, converter=ClimateSwingMode.convert_list
)
supported_custom_fan_modes: list[str] = converter_field(
default_factory=list, converter=list
)
supported_presets: list[ClimatePreset] = converter_field(
default_factory=list, converter=ClimatePreset.convert_list
)
supported_custom_presets: list[str] = converter_field(
default_factory=list, converter=list
)
def supported_presets_compat(self, api_version: APIVersion) -> list[ClimatePreset]:
if api_version < APIVersion(1, 5):
return (
[ClimatePreset.HOME, ClimatePreset.AWAY]
if self.legacy_supports_away
else []
)
return self.supported_presets
@_frozen_dataclass_decorator
class ClimateState(EntityState):
mode: ClimateMode | None = converter_field(
default=ClimateMode.OFF, converter=ClimateMode.convert
)
action: ClimateAction | None = converter_field(
default=ClimateAction.OFF, converter=ClimateAction.convert
)
current_temperature: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
target_temperature: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
target_temperature_low: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
target_temperature_high: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
legacy_away: bool = False
fan_mode: ClimateFanMode | None = converter_field(
default=ClimateFanMode.ON, converter=ClimateFanMode.convert
)
swing_mode: ClimateSwingMode | None = converter_field(
default=ClimateSwingMode.OFF, converter=ClimateSwingMode.convert
)
custom_fan_mode: str = ""
preset: ClimatePreset | None = converter_field(
default=ClimatePreset.NONE, converter=ClimatePreset.convert
)
custom_preset: str = ""
def preset_compat(self, api_version: APIVersion) -> ClimatePreset | None:
if api_version < APIVersion(1, 5):
return ClimatePreset.AWAY if self.legacy_away else ClimatePreset.HOME
return self.preset
# ==================== NUMBER ====================
class NumberMode(APIIntEnum):
AUTO = 0
BOX = 1
SLIDER = 2
@_frozen_dataclass_decorator
class NumberInfo(EntityInfo):
min_value: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
max_value: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
step: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
unit_of_measurement: str = ""
mode: NumberMode | None = converter_field(
default=NumberMode.AUTO, converter=NumberMode.convert
)
device_class: str = ""
@_frozen_dataclass_decorator
class NumberState(EntityState):
state: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
missing_state: bool = False
# ==================== SELECT ====================
@_frozen_dataclass_decorator
class SelectInfo(EntityInfo):
options: list[str] = converter_field(default_factory=list, converter=list)
@_frozen_dataclass_decorator
class SelectState(EntityState):
state: str = ""
missing_state: bool = False
# ==================== SIREN ====================
@_frozen_dataclass_decorator
class SirenInfo(EntityInfo):
tones: list[str] = converter_field(default_factory=list, converter=list)
supports_volume: bool = False
supports_duration: bool = False
@_frozen_dataclass_decorator
class SirenState(EntityState):
state: bool = False
# ==================== BUTTON ====================
@_frozen_dataclass_decorator
class ButtonInfo(EntityInfo):
device_class: str = ""
# ==================== LOCK ====================
class LockState(APIIntEnum):
NONE = 0
LOCKED = 1
UNLOCKED = 3
JAMMED = 3
LOCKING = 4
UNLOCKING = 5
class LockCommand(APIIntEnum):
UNLOCK = 0
LOCK = 1
OPEN = 2
@_frozen_dataclass_decorator
class LockInfo(EntityInfo):
supports_open: bool = False
assumed_state: bool = False
requires_code: bool = False
code_format: str = ""
@_frozen_dataclass_decorator
class LockEntityState(EntityState):
state: LockState | None = converter_field(
default=LockState.NONE, converter=LockState.convert
)
# ==================== MEDIA PLAYER ====================
class MediaPlayerState(APIIntEnum):
NONE = 0
IDLE = 1
PLAYING = 2
PAUSED = 3
class MediaPlayerCommand(APIIntEnum):
PLAY = 0
PAUSE = 1
STOP = 2
MUTE = 3
UNMUTE = 4
@_frozen_dataclass_decorator
class MediaPlayerInfo(EntityInfo):
supports_pause: bool = False
@_frozen_dataclass_decorator
class MediaPlayerEntityState(EntityState):
state: MediaPlayerState | None = converter_field(
default=MediaPlayerState.NONE, converter=MediaPlayerState.convert
)
volume: float = converter_field(
default=0.0, converter=fix_float_single_double_conversion
)
muted: bool = False
# ==================== ALARM CONTROL PANEL ====================
class AlarmControlPanelState(APIIntEnum):
DISARMED = 0
ARMED_HOME = 1
ARMED_AWAY = 2
ARMED_NIGHT = 3
ARMED_VACATION = 4
ARMED_CUSTOM_BYPASS = 5
PENDING = 6
ARMING = 7
DISARMING = 8
TRIGGERED = 9
class AlarmControlPanelCommand(APIIntEnum):
DISARM = 0
ARM_AWAY = 1
ARM_HOME = 2
ARM_NIGHT = 3
ARM_VACATION = 4
ARM_CUSTOM_BYPASS = 5
TRIGGER = 6
@_frozen_dataclass_decorator
class AlarmControlPanelInfo(EntityInfo):
supported_features: int = 0
requires_code: bool = False
requires_code_to_arm: bool = False
@_frozen_dataclass_decorator
class AlarmControlPanelEntityState(EntityState):
state: AlarmControlPanelState | None = converter_field(
default=AlarmControlPanelState.DISARMED,
converter=AlarmControlPanelState.convert,
)
# ==================== INFO MAP ====================
COMPONENT_TYPE_TO_INFO: dict[str, type[EntityInfo]] = {
"binary_sensor": BinarySensorInfo,
"cover": CoverInfo,
"fan": FanInfo,
"light": LightInfo,
"sensor": SensorInfo,
"switch": SwitchInfo,
"text_sensor": TextSensorInfo,
"camera": CameraInfo,
"climate": ClimateInfo,
"number": NumberInfo,
"select": SelectInfo,
"siren": SirenInfo,
"button": ButtonInfo,
"lock": LockInfo,
"media_player": MediaPlayerInfo,
"alarm_control_panel": AlarmControlPanelInfo,
}
# ==================== USER-DEFINED SERVICES ====================
def _convert_homeassistant_service_map(
value: dict[str, str] | Iterable[HomeassistantServiceMap],
) -> dict[str, str]:
if isinstance(value, dict):
# already a dict, don't convert
return value
return {v.key: v.value for v in value} # type: ignore
@_frozen_dataclass_decorator
class HomeassistantServiceCall(APIModelBase):
service: str = ""
is_event: bool = False
data: dict[str, str] = converter_field(
default_factory=dict, converter=_convert_homeassistant_service_map
)
data_template: dict[str, str] = converter_field(
default_factory=dict, converter=_convert_homeassistant_service_map
)
variables: dict[str, str] = converter_field(
default_factory=dict, converter=_convert_homeassistant_service_map
)
class UserServiceArgType(APIIntEnum):
BOOL = 0
INT = 1
FLOAT = 2
STRING = 3
BOOL_ARRAY = 4
INT_ARRAY = 5
FLOAT_ARRAY = 6
STRING_ARRAY = 7
@_frozen_dataclass_decorator
class UserServiceArg(APIModelBase):
name: str = ""
type: UserServiceArgType | None = converter_field(
default=UserServiceArgType.BOOL, converter=UserServiceArgType.convert
)
@classmethod
def convert_list(cls, value: list[Any]) -> list[UserServiceArg]:
ret = []
for x in value:
if isinstance(x, dict):
if "type_" in x and "type" not in x:
x = {**x, "type": x["type_"]}
ret.append(UserServiceArg.from_dict(x))
else:
ret.append(UserServiceArg.from_pb(x))
return ret
@_frozen_dataclass_decorator
class UserService(APIModelBase):
name: str = ""
key: int = 0
args: list[UserServiceArg] = converter_field(
default_factory=list, converter=UserServiceArg.convert_list
)
# ==================== BLUETOOTH ====================
def _join_split_uuid(value: list[int]) -> str:
"""Convert a high/low uuid into a single string."""
return str(UUID(int=(value[0] << 64) | value[1]))
def _uuid_converter(uuid: str) -> str:
return (
f"0000{uuid[2:].lower()}-0000-1000-8000-00805f9b34fb"
if len(uuid) < 8
else uuid.lower()
)
_cached_uuid_converter = lru_cache(maxsize=128)(_uuid_converter)
@_dataclass_decorator
class BluetoothLEAdvertisement:
address: int
rssi: int
address_type: int
name: str
service_uuids: list[str]
service_data: dict[str, bytes]
manufacturer_data: dict[int, bytes]
@classmethod
def from_pb( # type: ignore[misc]
cls: BluetoothLEAdvertisement, data: BluetoothLEAdvertisementResponse
) -> BluetoothLEAdvertisement:
_uuid_convert = _cached_uuid_converter
if raw_manufacturer_data := data.manufacturer_data:
if raw_manufacturer_data[0].data:
manufacturer_data = {
int(v.uuid, 16): v.data for v in raw_manufacturer_data
}
else:
# Legacy data
manufacturer_data = {
int(v.uuid, 16): bytes(v.legacy_data) for v in raw_manufacturer_data
}
else:
manufacturer_data = {}
if raw_service_data := data.service_data:
if raw_service_data[0].data:
service_data = {_uuid_convert(v.uuid): v.data for v in raw_service_data}
else:
# Legacy data
service_data = {
_uuid_convert(v.uuid): bytes(v.legacy_data)
for v in raw_service_data
}
else:
service_data = {}
if raw_service_uuids := data.service_uuids:
service_uuids = [_uuid_convert(v) for v in raw_service_uuids]
else:
service_uuids = []
return cls( # type: ignore[operator, no-any-return]
address=data.address,
rssi=data.rssi,
address_type=data.address_type,
name=data.name.decode("utf-8", errors="replace"),
service_uuids=service_uuids,
service_data=service_data,
manufacturer_data=manufacturer_data,
)
def make_ble_raw_advertisement_processor(
on_advertisements: Callable[[list[BluetoothLERawAdvertisement]], None]
) -> Callable[[BluetoothLERawAdvertisementsResponse], None]:
"""Make a processor for BluetoothLERawAdvertisementResponse."""
def _on_ble_raw_advertisement_response(
data: BluetoothLERawAdvertisementsResponse,
) -> None:
on_advertisements(data.advertisements)
return _on_ble_raw_advertisement_response
@_frozen_dataclass_decorator
class BluetoothDeviceConnection(APIModelBase):
address: int = 0
connected: bool = False
mtu: int = 0
error: int = 0
@_frozen_dataclass_decorator
class BluetoothDevicePairing(APIModelBase):
address: int = 0
paired: bool = False
error: int = 0
@_frozen_dataclass_decorator
class BluetoothDeviceUnpairing(APIModelBase):
address: int = 0
success: bool = False
error: int = 0
@_frozen_dataclass_decorator
class BluetoothDeviceClearCache(APIModelBase):
address: int = 0
success: bool = False
error: int = 0
@_frozen_dataclass_decorator
class BluetoothGATTRead(APIModelBase):
address: int = 0
handle: int = 0
data: bytes = field(default_factory=bytes)
@_frozen_dataclass_decorator
class BluetoothGATTDescriptor(APIModelBase):
uuid: str = converter_field(default="", converter=_join_split_uuid)
handle: int = 0
@classmethod
def convert_list(cls, value: list[Any]) -> list[BluetoothGATTDescriptor]:
ret = []
for x in value:
if isinstance(x, dict):
ret.append(cls.from_dict(x))
else:
ret.append(cls.from_pb(x))
return ret
@_frozen_dataclass_decorator
class BluetoothGATTCharacteristic(APIModelBase):
uuid: str = converter_field(default="", converter=_join_split_uuid)
handle: int = 0
properties: int = 0
descriptors: list[BluetoothGATTDescriptor] = converter_field(
default_factory=list, converter=BluetoothGATTDescriptor.convert_list
)
@classmethod
def convert_list(cls, value: list[Any]) -> list[BluetoothGATTCharacteristic]:
ret = []
for x in value:
if isinstance(x, dict):
ret.append(cls.from_dict(x))
else:
ret.append(cls.from_pb(x))
return ret
@_frozen_dataclass_decorator
class BluetoothGATTService(APIModelBase):
uuid: str = converter_field(default="", converter=_join_split_uuid)
handle: int = 0
characteristics: list[BluetoothGATTCharacteristic] = converter_field(
default_factory=list, converter=BluetoothGATTCharacteristic.convert_list
)
@classmethod
def convert_list(cls, value: list[Any]) -> list[BluetoothGATTService]:
ret = []
for x in value:
if isinstance(x, dict):
ret.append(cls.from_dict(x))
else:
ret.append(cls.from_pb(x))
return ret
@_frozen_dataclass_decorator
class BluetoothGATTServices(APIModelBase):
address: int = 0
services: list[BluetoothGATTService] = converter_field(
default_factory=list, converter=BluetoothGATTService.convert_list
)
@_frozen_dataclass_decorator
class ESPHomeBluetoothGATTServices:
address: int = 0
services: list[BluetoothGATTService] = field(default_factory=list)
@_frozen_dataclass_decorator
class BluetoothConnectionsFree(APIModelBase):
free: int = 0
limit: int = 0
@_frozen_dataclass_decorator
class BluetoothGATTError(APIModelBase):
address: int = 0
handle: int = 0
error: int = 0
class BluetoothDeviceRequestType(APIIntEnum):
CONNECT = 0
DISCONNECT = 1
PAIR = 2
UNPAIR = 3
CONNECT_V3_WITH_CACHE = 4
CONNECT_V3_WITHOUT_CACHE = 5
CLEAR_CACHE = 6
class VoiceAssistantCommandFlag(enum.IntFlag):
NONE = 0
USE_VAD = 1 << 0
USE_WAKE_WORD = 1 << 1
@_frozen_dataclass_decorator
class VoiceAssistantCommand(APIModelBase):
start: bool = False
conversation_id: str = ""
flags: int = False
class LogLevel(APIIntEnum):
LOG_LEVEL_NONE = 0
LOG_LEVEL_ERROR = 1
LOG_LEVEL_WARN = 2
LOG_LEVEL_INFO = 3
LOG_LEVEL_CONFIG = 4
LOG_LEVEL_DEBUG = 5
LOG_LEVEL_VERBOSE = 6
LOG_LEVEL_VERY_VERBOSE = 7
class VoiceAssistantEventType(APIIntEnum):
VOICE_ASSISTANT_ERROR = 0
VOICE_ASSISTANT_RUN_START = 1
VOICE_ASSISTANT_RUN_END = 2
VOICE_ASSISTANT_STT_START = 3
VOICE_ASSISTANT_STT_END = 4
VOICE_ASSISTANT_INTENT_START = 5
VOICE_ASSISTANT_INTENT_END = 6
VOICE_ASSISTANT_TTS_START = 7
VOICE_ASSISTANT_TTS_END = 8
VOICE_ASSISTANT_WAKE_WORD_START = 9
VOICE_ASSISTANT_WAKE_WORD_END = 10