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

Improve esphome state property decorator typing #77152

Merged
merged 2 commits into from
Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions homeassistant/components/esphome/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
DOMAIN = "esphome"
CONF_NOISE_PSK = "noise_psk"
_LOGGER = logging.getLogger(__name__)
_R = TypeVar("_R")
_DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData")

STORAGE_VERSION = 1
Expand Down Expand Up @@ -595,20 +596,18 @@ def async_list_entities(infos: list[EntityInfo]) -> None:
)


_PropT = TypeVar("_PropT", bound=Callable[..., Any])


def esphome_state_property(func: _PropT) -> _PropT:
def esphome_state_property(
func: Callable[[_EntityT], _R]
) -> Callable[[_EntityT], _R | None]:
"""Wrap a state property of an esphome entity.

This checks if the state object in the entity is set, and
prevents writing NAN values to the Home Assistant state machine.
"""

@property # type: ignore[misc]
@functools.wraps(func)
def _wrapper(self): # type: ignore[no-untyped-def]
# pylint: disable=protected-access
def _wrapper(self: _EntityT) -> _R | None:
# pylint: disable-next=protected-access
if not self._has_state:
return None
val = func(self)
Expand All @@ -618,7 +617,7 @@ def _wrapper(self): # type: ignore[no-untyped-def]
return None
return val

return cast(_PropT, _wrapper)
return _wrapper


_EnumT = TypeVar("_EnumT", bound=APIIntEnum)
Expand Down
13 changes: 9 additions & 4 deletions homeassistant/components/esphome/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,6 @@ async def async_setup_entry(
)


# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
# pylint: disable=invalid-overridden-method


class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEntity):
"""A climate implementation for ESPHome."""

Expand Down Expand Up @@ -219,11 +215,13 @@ def supported_features(self) -> int:
features |= ClimateEntityFeature.SWING_MODE
return features

@property # type: ignore[misc]
@esphome_state_property
def hvac_mode(self) -> str | None:
"""Return current operation ie. heat, cool, idle."""
return _CLIMATE_MODES.from_esphome(self._state.mode)

@property # type: ignore[misc]
@esphome_state_property
def hvac_action(self) -> str | None:
"""Return current action."""
Expand All @@ -232,40 +230,47 @@ def hvac_action(self) -> str | None:
return None
return _CLIMATE_ACTIONS.from_esphome(self._state.action)

@property # type: ignore[misc]
@esphome_state_property
def fan_mode(self) -> str | None:
"""Return current fan setting."""
return self._state.custom_fan_mode or _FAN_MODES.from_esphome(
self._state.fan_mode
)

@property # type: ignore[misc]
@esphome_state_property
def preset_mode(self) -> str | None:
"""Return current preset mode."""
return self._state.custom_preset or _PRESETS.from_esphome(
self._state.preset_compat(self._api_version)
)

@property # type: ignore[misc]
@esphome_state_property
def swing_mode(self) -> str | None:
"""Return current swing mode."""
return _SWING_MODES.from_esphome(self._state.swing_mode)

@property # type: ignore[misc]
@esphome_state_property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._state.current_temperature

@property # type: ignore[misc]
@esphome_state_property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return self._state.target_temperature

@property # type: ignore[misc]
@esphome_state_property
def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach."""
return self._state.target_temperature_low

@property # type: ignore[misc]
@esphome_state_property
def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach."""
Expand Down
9 changes: 5 additions & 4 deletions homeassistant/components/esphome/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ async def async_setup_entry(
)


# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
# pylint: disable=invalid-overridden-method


class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity):
"""A cover implementation for ESPHome."""

Expand Down Expand Up @@ -69,29 +65,34 @@ def assumed_state(self) -> bool:
"""Return true if we do optimistic updates."""
return self._static_info.assumed_state

@property # type: ignore[misc]
@esphome_state_property
def is_closed(self) -> bool | None:
"""Return if the cover is closed or not."""
# Check closed state with api version due to a protocol change
return self._state.is_closed(self._api_version)

@property # type: ignore[misc]
@esphome_state_property
def is_opening(self) -> bool:
"""Return if the cover is opening or not."""
return self._state.current_operation == CoverOperation.IS_OPENING

@property # type: ignore[misc]
@esphome_state_property
def is_closing(self) -> bool:
"""Return if the cover is closing or not."""
return self._state.current_operation == CoverOperation.IS_CLOSING

@property # type: ignore[misc]
@esphome_state_property
def current_cover_position(self) -> int | None:
"""Return current position of cover. 0 is closed, 100 is open."""
if not self._static_info.supports_position:
return None
return round(self._state.position * 100.0)

@property # type: ignore[misc]
@esphome_state_property
def current_cover_tilt_position(self) -> int | None:
"""Return current position of cover tilt. 0 is closed, 100 is open."""
Expand Down
8 changes: 4 additions & 4 deletions homeassistant/components/esphome/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,6 @@ async def async_setup_entry(
)


# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
# pylint: disable=invalid-overridden-method


class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
"""A fan implementation for ESPHome."""

Expand Down Expand Up @@ -116,11 +112,13 @@ async def async_set_direction(self, direction: str) -> None:
key=self._static_info.key, direction=_FAN_DIRECTIONS.from_hass(direction)
)

@property # type: ignore[misc]
@esphome_state_property
def is_on(self) -> bool | None:
"""Return true if the entity is on."""
return self._state.state

@property # type: ignore[misc]
@esphome_state_property
def percentage(self) -> int | None:
"""Return the current speed percentage."""
Expand All @@ -143,13 +141,15 @@ def speed_count(self) -> int:
return len(ORDERED_NAMED_FAN_SPEEDS)
return self._static_info.supported_speed_levels

@property # type: ignore[misc]
@esphome_state_property
def oscillating(self) -> bool | None:
"""Return the oscillation state."""
if not self._static_info.supports_oscillation:
return None
return self._state.oscillating

@property # type: ignore[misc]
@esphome_state_property
def current_direction(self) -> str | None:
"""Return the current fan direction."""
Expand Down
12 changes: 8 additions & 4 deletions homeassistant/components/esphome/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,6 @@ def _filter_color_modes(
return [mode for mode in supported if mode & features]


# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
# pylint: disable=invalid-overridden-method


class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
"""A light implementation for ESPHome."""

Expand All @@ -134,6 +130,7 @@ def _supports_color_mode(self) -> bool:
"""Return whether the client supports the new color mode system natively."""
return self._api_version >= APIVersion(1, 6)

@property # type: ignore[misc]
@esphome_state_property
def is_on(self) -> bool | None:
"""Return true if the light is on."""
Expand Down Expand Up @@ -263,11 +260,13 @@ async def async_turn_off(self, **kwargs: Any) -> None:
data["transition_length"] = kwargs[ATTR_TRANSITION]
await self._client.light_command(**data)

@property # type: ignore[misc]
@esphome_state_property
def brightness(self) -> int | None:
"""Return the brightness of this light between 0..255."""
return round(self._state.brightness * 255)

@property # type: ignore[misc]
@esphome_state_property
def color_mode(self) -> str | None:
"""Return the color mode of the light."""
Expand All @@ -278,6 +277,7 @@ def color_mode(self) -> str | None:

return _color_mode_to_ha(self._state.color_mode)

@property # type: ignore[misc]
@esphome_state_property
def rgb_color(self) -> tuple[int, int, int] | None:
"""Return the rgb color value [int, int, int]."""
Expand All @@ -294,13 +294,15 @@ def rgb_color(self) -> tuple[int, int, int] | None:
round(self._state.blue * self._state.color_brightness * 255),
)

@property # type: ignore[misc]
@esphome_state_property
def rgbw_color(self) -> tuple[int, int, int, int] | None:
"""Return the rgbw color value [int, int, int, int]."""
white = round(self._state.white * 255)
rgb = cast("tuple[int, int, int]", self.rgb_color)
return (*rgb, white)

@property # type: ignore[misc]
@esphome_state_property
def rgbww_color(self) -> tuple[int, int, int, int, int] | None:
"""Return the rgbww color value [int, int, int, int, int]."""
Expand Down Expand Up @@ -328,11 +330,13 @@ def rgbww_color(self) -> tuple[int, int, int, int, int] | None:
round(self._state.warm_white * 255),
)

@property # type: ignore[misc]
@esphome_state_property
def color_temp(self) -> float | None: # type: ignore[override]
"""Return the CT color value in mireds."""
return self._state.color_temperature

@property # type: ignore[misc]
@esphome_state_property
def effect(self) -> str | None:
"""Return the current effect."""
Expand Down
8 changes: 4 additions & 4 deletions homeassistant/components/esphome/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ async def async_setup_entry(
)


# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
# pylint: disable=invalid-overridden-method


class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity):
"""A lock implementation for ESPHome."""

Expand All @@ -53,21 +49,25 @@ def code_format(self) -> str | None:
return self._static_info.code_format
return None

@property # type: ignore[misc]
@esphome_state_property
def is_locked(self) -> bool | None:
"""Return true if the lock is locked."""
return self._state.state == LockState.LOCKED

@property # type: ignore[misc]
@esphome_state_property
def is_locking(self) -> bool | None:
"""Return true if the lock is locking."""
return self._state.state == LockState.LOCKING

@property # type: ignore[misc]
@esphome_state_property
def is_unlocking(self) -> bool | None:
"""Return true if the lock is unlocking."""
return self._state.state == LockState.UNLOCKING

@property # type: ignore[misc]
@esphome_state_property
def is_jammed(self) -> bool | None:
"""Return true if the lock is jammed (incomplete locking)."""
Expand Down
7 changes: 3 additions & 4 deletions homeassistant/components/esphome/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,27 +59,26 @@ async def async_setup_entry(
)


# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
# pylint: disable=invalid-overridden-method


class EsphomeMediaPlayer(
EsphomeEntity[MediaPlayerInfo, MediaPlayerEntityState], MediaPlayerEntity
):
"""A media player implementation for esphome."""

_attr_device_class = MediaPlayerDeviceClass.SPEAKER

@property # type: ignore[misc]
@esphome_state_property
def state(self) -> str | None:
"""Return current state."""
return _STATES.from_esphome(self._state.state)

@property # type: ignore[misc]
@esphome_state_property
def is_volume_muted(self) -> bool:
"""Return true if volume is muted."""
return self._state.muted

@property # type: ignore[misc]
@esphome_state_property
def volume_level(self) -> float | None:
"""Volume level of the media player (0..1)."""
Expand Down
5 changes: 1 addition & 4 deletions homeassistant/components/esphome/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ async def async_setup_entry(
)


# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
# pylint: disable=invalid-overridden-method


class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity):
"""A number implementation for esphome."""

Expand Down Expand Up @@ -78,6 +74,7 @@ def mode(self) -> NumberMode:
return NUMBER_MODES.from_esphome(self._static_info.mode)
return NumberMode.AUTO

@property # type: ignore[misc]
@esphome_state_property
def native_value(self) -> float | None:
"""Return the state of the entity."""
Expand Down
5 changes: 1 addition & 4 deletions homeassistant/components/esphome/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@ async def async_setup_entry(
)


# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
# pylint: disable=invalid-overridden-method


class EsphomeSelect(EsphomeEntity[SelectInfo, SelectState], SelectEntity):
"""A select implementation for esphome."""

Expand All @@ -40,6 +36,7 @@ def options(self) -> list[str]:
"""Return a set of selectable options."""
return self._static_info.options

@property # type: ignore[misc]
@esphome_state_property
def current_option(self) -> str | None:
"""Return the state of the entity."""
Expand Down
Loading