Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch from attrs to dataclasses #36

Merged
merged 11 commits into from
Jun 29, 2021
Prev Previous commit
Next Next commit
Merge remote-tracking branch 'origin/master' into use-dataclasses
  • Loading branch information
OttoWinter committed Jun 29, 2021
commit a8f1763dd2b1572118397caa7101119117e260b7
49 changes: 11 additions & 38 deletions aioesphomeapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
ListEntitiesServicesResponse,
ListEntitiesSwitchResponse,
ListEntitiesTextSensorResponse,
NumberCommandRequest,
NumberStateResponse,
SensorStateResponse,
SubscribeHomeassistantServicesRequest,
SubscribeHomeAssistantStateResponse,
Expand Down Expand Up @@ -86,6 +88,8 @@
LightInfo,
LightState,
LogLevel,
NumberInfo,
NumberState,
SensorInfo,
SensorState,
SwitchInfo,
Expand All @@ -97,9 +101,6 @@
UserServiceArgType,
)

if TYPE_CHECKING:
from aioesphomeapi.api_pb2 import LogLevel # type: ignore

_LOGGER = logging.getLogger(__name__)

ExecuteServiceDataType = Dict[
Expand Down Expand Up @@ -189,15 +190,7 @@ async def device_info(self) -> DeviceInfo:
resp = await self._connection.send_message_await_response(
DeviceInfoRequest(), DeviceInfoResponse
)
return DeviceInfo(
uses_password=resp.uses_password,
name=resp.name,
mac_address=resp.mac_address,
esphome_version=resp.esphome_version,
compilation_time=resp.compilation_time,
model=resp.model,
has_deep_sleep=resp.has_deep_sleep,
)
return DeviceInfo.from_pb(resp)

async def list_entities_services(
self,
Expand Down Expand Up @@ -231,30 +224,15 @@ def do_stop(msg: message.Message) -> bool:
services: List[UserService] = []
for msg in resp:
if isinstance(msg, ListEntitiesServicesResponse):
args = [
UserServiceArg(
name=arg.name,
type=arg.type,
)
for arg in msg.args
]
services.append(
UserService(
name=msg.name,
key=msg.key,
args=args,
)
)
services.append(UserService.from_pb(msg))
continue
cls = None
for resp_type, cls in response_types.items():
if isinstance(msg, resp_type):
break
else:
continue
cls = cast(type, cls)
kwargs = {f.name: getattr(msg, f.name) for f in dataclasses.fields(cls)}
entities.append(cls(**kwargs))
entities.append(cls.from_pb(msg))
return entities, services

async def subscribe_states(self, on_state: Callable[[Any], None]) -> None:
Expand All @@ -278,7 +256,7 @@ def on_msg(msg: message.Message) -> None:
if isinstance(msg, CameraImageResponse):
data = image_stream.pop(msg.key, bytes()) + msg.data
if msg.done:
on_state(CameraState(key=msg.key, image=data))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue people are experiencing on HA 2021.7 is because the new from_pb function only matches the attributes in the dataclass by exact name. That is image in CameraState but in CameraImageResponse it is data. Simple fix is to rename the attr to data and the references inside HA

on_state(CameraState.from_pb(msg))
else:
image_stream[msg.key] = data
return
Expand All @@ -290,8 +268,7 @@ def on_msg(msg: message.Message) -> None:
return

# pylint: disable=undefined-loop-variable
kwargs = {f.name: getattr(msg, f.name) for f in dataclasses.fields(cls)}
on_state(cls(**kwargs))
on_state(cls.from_pb(msg))

assert self._connection is not None
await self._connection.send_message_callback_response(
Expand All @@ -301,7 +278,7 @@ def on_msg(msg: message.Message) -> None:
async def subscribe_logs(
self,
on_log: Callable[[SubscribeLogsResponse], None],
log_level: Optional["LogLevel"] = None,
log_level: Optional[LogLevel] = None,
) -> None:
self._check_authenticated()

Expand All @@ -322,11 +299,7 @@ async def subscribe_service_calls(

def on_msg(msg: message.Message) -> None:
if isinstance(msg, HomeassistantServiceResponse):
kwargs = {
f.name: getattr(msg, f.name)
for f in dataclasses.fields(HomeassistantServiceCall)
}
on_service_call(HomeassistantServiceCall(**kwargs))
on_service_call(HomeassistantServiceCall.from_pb(msg))

assert self._connection is not None
await self._connection.send_message_callback_response(
Expand Down
79 changes: 46 additions & 33 deletions aioesphomeapi/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,22 +59,22 @@ def __post_init__(self) -> None:
# use this setattr to prevent FrozenInstanceError
super().__setattr__(field_.name, convert(val))

def asdict(self) -> Dict[str, Any]:
def to_dict(self) -> Dict[str, Any]:
return asdict(self)

@classmethod
def fromdict(cls: Type[_V], data: Dict[str, Any]) -> _V:
init_args = {}
for field_ in fields(cls):
name = field_.name
if name not in data:
# Use default
continue
value = data[name]
convert = field_.metadata.get("converter")
if convert is not None:
value = convert(value)
init_args[name] = value
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 fields(cls)
if f.name in data or (not ignore_missing)
}
return cls(**init_args) # type: ignore

@classmethod
def from_pb(cls: Type[_V], data: Any) -> _V:
init_args = {
f.name: getattr(data, f.name) for f in fields(cls)
}
return cls(**init_args) # type: ignore


Expand Down Expand Up @@ -325,6 +325,17 @@ class ClimateAction(APIIntEnum):
FAN = 6


class ClimatePreset(APIIntEnum):
NONE = 0
HOME = 1
AWAY = 2
BOOST = 3
COMFORT = 4
ECO = 5
SLEEP = 6
ACTIVITY = 7


@dataclass(frozen=True)
class ClimateInfo(EntityInfo):
supports_current_temperature: bool = False
Expand All @@ -335,19 +346,23 @@ class ClimateInfo(EntityInfo):
visual_min_temperature: float = 0.0
visual_max_temperature: float = 0.0
visual_temperature_step: float = 0.0
supports_away: bool = False
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 = attr.ib(type=List[str], converter=list, factory=list)
supported_presets = attr.ib(
type=List[ClimatePreset], converter=ClimatePreset.convert_list, factory=list # type: ignore
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
)
supported_custom_presets = attr.ib(type=List[str], converter=list, factory=list)

def supported_presets_compat(self, api_version: APIVersion) -> List[ClimatePreset]:
if api_version < APIVersion(1, 5):
Expand All @@ -371,20 +386,18 @@ class ClimateState(EntityState):
target_temperature: float = 0.0
target_temperature_low: float = 0.0
target_temperature_high: float = 0.0
away: bool = False
legacy_away: bool = False
fan_mode: Optional[ClimateFanMode] = converter_field(
default=ClimateFanMode.ON, converter=ClimateFanMode.convert
)
swing_mode: Optional[ClimateSwingMode] = converter_field(
default=ClimateSwingMode.OFF, converter=ClimateSwingMode.convert
)
custom_fan_mode = attr.ib(type=str, default="")
preset = attr.ib(
type=Optional[ClimatePreset],
converter=ClimatePreset.convert, # type: ignore
default=ClimatePreset.HOME,
custom_fan_mode: str = ""
preset: Optional[ClimatePreset] = converter_field(
default=ClimatePreset.HOME, converter=ClimatePreset.convert
)
custom_preset = attr.ib(type=str, default="")
custom_preset: str = ""

def preset_compat(self, api_version: APIVersion) -> Optional[ClimatePreset]:
if api_version < APIVersion(1, 5):
Expand All @@ -393,18 +406,18 @@ def preset_compat(self, api_version: APIVersion) -> Optional[ClimatePreset]:


# ==================== NUMBER ====================
@attr.s
@dataclass(frozen=True)
class NumberInfo(EntityInfo):
icon = attr.ib(type=str, default="")
min_value = attr.ib(type=float, default=0.0)
max_value = attr.ib(type=float, default=0.0)
step = attr.ib(type=float, default=0.0)
icon: str = ""
min_value: float = 0.0
max_value: float = 0.0
step: float = 0.0


@attr.s
@dataclass(frozen=True)
class NumberState(EntityState):
state = attr.ib(type=float, default=0.0)
missing_state = attr.ib(type=bool, default=False)
state: float = 0.0
missing_state: bool = False


COMPONENT_TYPE_TO_INFO = {
Expand Down
You are viewing a condensed version of this merge commit. You can view the full changes here.