diff --git a/aioesphomeapi/client.py b/aioesphomeapi/client.py index b331071..d9197d2 100644 --- a/aioesphomeapi/client.py +++ b/aioesphomeapi/client.py @@ -427,7 +427,7 @@ class APIClient: # Return CameraState with the merged data image_data = bytes().join(data_parts) del image_stream[msg_key] - on_state(CameraState(key=msg.key, data=image_data)) + on_state(CameraState(key=msg.key, data=image_data)) # type: ignore[call-arg] assert self._connection is not None self._connection.send_message_callback_response( @@ -847,7 +847,7 @@ class APIClient: services.extend(BluetoothGATTServices.from_pb(msg).services) - return ESPHomeBluetoothGATTServices(address=address, services=services) + return ESPHomeBluetoothGATTServices(address=address, services=services) # type: ignore[call-arg] async def bluetooth_gatt_read( self, diff --git a/aioesphomeapi/model.py b/aioesphomeapi/model.py index f2e576e..405e30e 100644 --- a/aioesphomeapi/model.py +++ b/aioesphomeapi/model.py @@ -1,7 +1,7 @@ import enum import sys from dataclasses import asdict, dataclass, field, fields -from functools import cache, lru_cache +from functools import cache, lru_cache, partial from typing import ( TYPE_CHECKING, Any, @@ -20,11 +20,12 @@ from uuid import UUID from .util import fix_float_single_double_conversion if sys.version_info[:2] < (3, 10): - _dataclass_decorator = dataclass() + _dataclass_decorator = dataclass + _frozen_dataclass_decorator = partial(dataclass, frozen=True) else: - _dataclass_decorator = dataclass( # pylint: disable=unexpected-keyword-arg - slots=True - ) + _dataclass_decorator = partial(dataclass, slots=True) + _frozen_dataclass_decorator = partial(dataclass, frozen=True, slots=True) + if TYPE_CHECKING: from .api_pb2 import ( # type: ignore @@ -69,7 +70,7 @@ class APIIntEnum(enum.IntEnum): cached_fields = cache(fields) -@dataclass(frozen=True) +@_frozen_dataclass_decorator class APIModelBase: def __post_init__(self) -> None: for field_ in cached_fields(type(self)): # type: ignore[arg-type] @@ -78,10 +79,10 @@ class APIModelBase: continue val = getattr(self, field_.name) # use this setattr to prevent FrozenInstanceError - super().__setattr__(field_.name, convert(val)) + object.__setattr__(self, field_.name, convert(val)) def to_dict(self) -> Dict[str, Any]: - return asdict(self) + return asdict(self) # type: ignore[no-any-return, call-overload] @classmethod def from_dict( @@ -124,7 +125,7 @@ class BluetoothProxySubscriptionFlag(enum.IntFlag): RAW_ADVERTISEMENTS = 1 << 0 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class DeviceInfo(APIModelBase): uses_password: bool = False name: str = "" @@ -165,7 +166,7 @@ class EntityCategory(APIIntEnum): DIAGNOSTIC = 2 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class EntityInfo(APIModelBase): object_id: str = "" key: int = 0 @@ -178,26 +179,26 @@ class EntityInfo(APIModelBase): ) -@dataclass(frozen=True) +@_frozen_dataclass_decorator class EntityState(APIModelBase): key: int = 0 # ==================== BINARY SENSOR ==================== -@dataclass(frozen=True) +@_frozen_dataclass_decorator class BinarySensorInfo(EntityInfo): device_class: str = "" is_status_binary_sensor: bool = False -@dataclass(frozen=True) +@_frozen_dataclass_decorator class BinarySensorState(EntityState): state: bool = False missing_state: bool = False # ==================== COVER ==================== -@dataclass(frozen=True) +@_frozen_dataclass_decorator class CoverInfo(EntityInfo): assumed_state: bool = False supports_stop: bool = False @@ -223,7 +224,7 @@ class CoverOperation(APIIntEnum): IS_CLOSING = 2 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class CoverState(EntityState): legacy_state: Optional[LegacyCoverState] = converter_field( default=LegacyCoverState.OPEN, converter=LegacyCoverState.convert @@ -245,7 +246,7 @@ class CoverState(EntityState): # ==================== FAN ==================== -@dataclass(frozen=True) +@_frozen_dataclass_decorator class FanInfo(EntityInfo): supports_oscillation: bool = False supports_speed: bool = False @@ -264,7 +265,7 @@ class FanDirection(APIIntEnum): REVERSE = 1 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class FanState(EntityState): state: bool = False oscillating: bool = False @@ -287,7 +288,7 @@ class LightColorCapability(enum.IntFlag): RGB = 1 << 5 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class LightInfo(EntityInfo): supported_color_modes: List[int] = converter_field( default_factory=list, converter=list @@ -357,7 +358,7 @@ class LightInfo(EntityInfo): return self.supported_color_modes -@dataclass(frozen=True) +@_frozen_dataclass_decorator class LightState(EntityState): state: bool = False brightness: float = converter_field( @@ -405,7 +406,7 @@ class LastResetType(APIIntEnum): AUTO = 2 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class SensorInfo(EntityInfo): device_class: str = "" unit_of_measurement: str = "" @@ -419,43 +420,43 @@ class SensorInfo(EntityInfo): ) -@dataclass(frozen=True) +@_frozen_dataclass_decorator class SensorState(EntityState): state: float = 0.0 missing_state: bool = False # ==================== SWITCH ==================== -@dataclass(frozen=True) +@_frozen_dataclass_decorator class SwitchInfo(EntityInfo): assumed_state: bool = False device_class: str = "" -@dataclass(frozen=True) +@_frozen_dataclass_decorator class SwitchState(EntityState): state: bool = False # ==================== TEXT SENSOR ==================== -@dataclass(frozen=True) +@_frozen_dataclass_decorator class TextSensorInfo(EntityInfo): pass -@dataclass(frozen=True) +@_frozen_dataclass_decorator class TextSensorState(EntityState): state: str = "" missing_state: bool = False # ==================== CAMERA ==================== -@dataclass(frozen=True) +@_frozen_dataclass_decorator class CameraInfo(EntityInfo): pass -@dataclass(frozen=True) +@_frozen_dataclass_decorator class CameraState(EntityState): data: bytes = field(default_factory=bytes) @@ -511,7 +512,7 @@ class ClimatePreset(APIIntEnum): ACTIVITY = 7 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class ClimateInfo(EntityInfo): supports_current_temperature: bool = False supports_two_point_target_temperature: bool = False @@ -558,7 +559,7 @@ class ClimateInfo(EntityInfo): return self.supported_presets -@dataclass(frozen=True) +@_frozen_dataclass_decorator class ClimateState(EntityState): mode: Optional[ClimateMode] = converter_field( default=ClimateMode.OFF, converter=ClimateMode.convert @@ -604,7 +605,7 @@ class NumberMode(APIIntEnum): SLIDER = 2 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class NumberInfo(EntityInfo): min_value: float = converter_field( default=0.0, converter=fix_float_single_double_conversion @@ -622,7 +623,7 @@ class NumberInfo(EntityInfo): device_class: str = "" -@dataclass(frozen=True) +@_frozen_dataclass_decorator class NumberState(EntityState): state: float = converter_field( default=0.0, converter=fix_float_single_double_conversion @@ -631,32 +632,32 @@ class NumberState(EntityState): # ==================== SELECT ==================== -@dataclass(frozen=True) +@_frozen_dataclass_decorator class SelectInfo(EntityInfo): options: List[str] = converter_field(default_factory=list, converter=list) -@dataclass(frozen=True) +@_frozen_dataclass_decorator class SelectState(EntityState): state: str = "" missing_state: bool = False # ==================== SIREN ==================== -@dataclass(frozen=True) +@_frozen_dataclass_decorator class SirenInfo(EntityInfo): tones: List[str] = converter_field(default_factory=list, converter=list) supports_volume: bool = False supports_duration: bool = False -@dataclass(frozen=True) +@_frozen_dataclass_decorator class SirenState(EntityState): state: bool = False # ==================== BUTTON ==================== -@dataclass(frozen=True) +@_frozen_dataclass_decorator class ButtonInfo(EntityInfo): device_class: str = "" @@ -677,7 +678,7 @@ class LockCommand(APIIntEnum): OPEN = 2 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class LockInfo(EntityInfo): supports_open: bool = False assumed_state: bool = False @@ -686,7 +687,7 @@ class LockInfo(EntityInfo): code_format: str = "" -@dataclass(frozen=True) +@_frozen_dataclass_decorator class LockEntityState(EntityState): state: Optional[LockState] = converter_field( default=LockState.NONE, converter=LockState.convert @@ -709,12 +710,12 @@ class MediaPlayerCommand(APIIntEnum): UNMUTE = 4 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class MediaPlayerInfo(EntityInfo): supports_pause: bool = False -@dataclass(frozen=True) +@_frozen_dataclass_decorator class MediaPlayerEntityState(EntityState): state: Optional[MediaPlayerState] = converter_field( default=MediaPlayerState.NONE, converter=MediaPlayerState.convert @@ -749,14 +750,14 @@ class AlarmControlPanelCommand(APIIntEnum): TRIGGER = 6 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class AlarmControlPanelInfo(EntityInfo): supported_features: int = 0 requires_code: bool = False requires_code_to_arm: bool = False -@dataclass(frozen=True) +@_frozen_dataclass_decorator class AlarmControlPanelEntityState(EntityState): state: Optional[AlarmControlPanelState] = converter_field( default=AlarmControlPanelState.DISARMED, @@ -796,7 +797,7 @@ def _convert_homeassistant_service_map( return {v.key: v.value for v in value} # type: ignore -@dataclass(frozen=True) +@_frozen_dataclass_decorator class HomeassistantServiceCall(APIModelBase): service: str = "" is_event: bool = False @@ -822,7 +823,7 @@ class UserServiceArgType(APIIntEnum): STRING_ARRAY = 7 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class UserServiceArg(APIModelBase): name: str = "" type: Optional[UserServiceArgType] = converter_field( @@ -842,7 +843,7 @@ class UserServiceArg(APIModelBase): return ret -@dataclass(frozen=True) +@_frozen_dataclass_decorator class UserService(APIModelBase): name: str = "" key: int = 0 @@ -955,7 +956,7 @@ def make_ble_raw_advertisement_processor( return _on_ble_raw_advertisement_response -@dataclass(frozen=True) +@_frozen_dataclass_decorator class BluetoothDeviceConnection(APIModelBase): address: int = 0 connected: bool = False @@ -963,28 +964,28 @@ class BluetoothDeviceConnection(APIModelBase): error: int = 0 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class BluetoothDevicePairing(APIModelBase): address: int = 0 paired: bool = False error: int = 0 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class BluetoothDeviceUnpairing(APIModelBase): address: int = 0 success: bool = False error: int = 0 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class BluetoothDeviceClearCache(APIModelBase): address: int = 0 success: bool = False error: int = 0 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class BluetoothGATTRead(APIModelBase): address: int = 0 handle: int = 0 @@ -992,7 +993,7 @@ class BluetoothGATTRead(APIModelBase): data: bytes = field(default_factory=bytes) -@dataclass(frozen=True) +@_frozen_dataclass_decorator class BluetoothGATTDescriptor(APIModelBase): uuid: str = converter_field(default="", converter=_join_split_uuid) handle: int = 0 @@ -1008,7 +1009,7 @@ class BluetoothGATTDescriptor(APIModelBase): return ret -@dataclass(frozen=True) +@_frozen_dataclass_decorator class BluetoothGATTCharacteristic(APIModelBase): uuid: str = converter_field(default="", converter=_join_split_uuid) handle: int = 0 @@ -1029,7 +1030,7 @@ class BluetoothGATTCharacteristic(APIModelBase): return ret -@dataclass(frozen=True) +@_frozen_dataclass_decorator class BluetoothGATTService(APIModelBase): uuid: str = converter_field(default="", converter=_join_split_uuid) handle: int = 0 @@ -1048,7 +1049,7 @@ class BluetoothGATTService(APIModelBase): return ret -@dataclass(frozen=True) +@_frozen_dataclass_decorator class BluetoothGATTServices(APIModelBase): address: int = 0 services: List[BluetoothGATTService] = converter_field( @@ -1056,19 +1057,19 @@ class BluetoothGATTServices(APIModelBase): ) -@dataclass(frozen=True) +@_frozen_dataclass_decorator class ESPHomeBluetoothGATTServices: address: int = 0 services: List[BluetoothGATTService] = field(default_factory=list) -@dataclass(frozen=True) +@_frozen_dataclass_decorator class BluetoothConnectionsFree(APIModelBase): free: int = 0 limit: int = 0 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class BluetoothGATTError(APIModelBase): address: int = 0 handle: int = 0 @@ -1085,7 +1086,7 @@ class BluetoothDeviceRequestType(APIIntEnum): CLEAR_CACHE = 6 -@dataclass(frozen=True) +@_frozen_dataclass_decorator class VoiceAssistantCommand(APIModelBase): start: bool = False conversation_id: str = ""