Skip to content

Commit

Permalink
Use TypedDict to annotate kwargs for turn_on
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p committed Dec 13, 2024
1 parent 943e764 commit f3a9008
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 63 deletions.
5 changes: 3 additions & 2 deletions homeassistant/components/abode/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

from math import ceil
from typing import Any
from typing import Any, Unpack

from jaraco.abode.devices.light import Light

Expand All @@ -13,6 +13,7 @@
ATTR_HS_COLOR,
ColorMode,
LightEntity,
LightTurnOnTD,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -41,7 +42,7 @@ class AbodeLight(AbodeDevice, LightEntity):
_device: Light
_attr_name = None

def turn_on(self, **kwargs: Any) -> None:
def turn_on(self, **kwargs: Unpack[LightTurnOnTD]) -> None:
"""Turn on the light."""
if ATTR_COLOR_TEMP_KELVIN in kwargs and self._device.is_color_capable:
self._device.set_color_temp(kwargs[ATTR_COLOR_TEMP_KELVIN])
Expand Down
12 changes: 7 additions & 5 deletions homeassistant/components/demo/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import random
from typing import Any
from typing import Unpack

from homeassistant.components.light import (
ATTR_BRIGHTNESS,
Expand All @@ -16,6 +16,8 @@
ColorMode,
LightEntity,
LightEntityFeature,
LightTurnOffTD,
LightTurnOnTD,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -110,7 +112,7 @@ def __init__(
ct: int | None = None,
effect_list: list[str] | None = None,
effect: str | None = None,
hs_color: tuple[int, int] | None = None,
hs_color: tuple[float, float] | None = None,
rgbw_color: tuple[int, int, int, int] | None = None,
rgbww_color: tuple[int, int, int, int, int] | None = None,
supported_color_modes: set[ColorMode] | None = None,
Expand Down Expand Up @@ -170,7 +172,7 @@ def color_mode(self) -> str | None:
return self._color_mode

@property
def hs_color(self) -> tuple[int, int] | None:
def hs_color(self) -> tuple[float, float] | None:
"""Return the hs color value."""
return self._hs_color

Expand Down Expand Up @@ -209,7 +211,7 @@ def supported_color_modes(self) -> set[ColorMode]:
"""Flag supported color modes."""
return self._color_modes

async def async_turn_on(self, **kwargs: Any) -> None:
async def async_turn_on(self, **kwargs: Unpack[LightTurnOnTD]) -> None:
"""Turn the light on."""
self._state = True

Expand Down Expand Up @@ -243,7 +245,7 @@ async def async_turn_on(self, **kwargs: Any) -> None:
# Home Assistant about updates in our state ourselves.
self.async_write_ha_state()

async def async_turn_off(self, **kwargs: Any) -> None:
async def async_turn_off(self, **kwargs: Unpack[LightTurnOffTD]) -> None:
"""Turn the light off."""
self._state = False

Expand Down
11 changes: 8 additions & 3 deletions homeassistant/components/evil_genius_labs/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
from __future__ import annotations

import asyncio
from typing import Any, cast
from typing import Any, Unpack, cast

from homeassistant.components import light
from homeassistant.components.light import ColorMode, LightEntity, LightEntityFeature
from homeassistant.components.light import (
ColorMode,
LightEntity,
LightEntityFeature,
LightTurnOnTD,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
Expand Down Expand Up @@ -86,7 +91,7 @@ def effect(self) -> str:
@update_when_done
async def async_turn_on(
self,
**kwargs: Any,
**kwargs: Unpack[LightTurnOnTD],
) -> None:
"""Turn light on."""
if (brightness := kwargs.get(light.ATTR_BRIGHTNESS)) is not None:
Expand Down
125 changes: 86 additions & 39 deletions homeassistant/components/light/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from functools import partial
import logging
import os
from typing import Any, Final, Self, cast, final
from typing import Any, Final, Literal, Self, TypedDict, Unpack, cast, final

from propcache import cached_property
import voluptuous as vol
Expand Down Expand Up @@ -175,43 +175,43 @@ def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set[str] |


# Float that represents transition time in seconds to make change.
ATTR_TRANSITION = "transition"
ATTR_TRANSITION: Final = "transition"

# Lists holding color values
ATTR_RGB_COLOR = "rgb_color"
ATTR_RGBW_COLOR = "rgbw_color"
ATTR_RGBWW_COLOR = "rgbww_color"
ATTR_XY_COLOR = "xy_color"
ATTR_HS_COLOR = "hs_color"
ATTR_COLOR_TEMP = "color_temp" # Deprecated in HA Core 2022.11
ATTR_KELVIN = "kelvin" # Deprecated in HA Core 2022.11
ATTR_MIN_MIREDS = "min_mireds" # Deprecated in HA Core 2022.11
ATTR_MAX_MIREDS = "max_mireds" # Deprecated in HA Core 2022.11
ATTR_COLOR_TEMP_KELVIN = "color_temp_kelvin"
ATTR_MIN_COLOR_TEMP_KELVIN = "min_color_temp_kelvin"
ATTR_MAX_COLOR_TEMP_KELVIN = "max_color_temp_kelvin"
ATTR_COLOR_NAME = "color_name"
ATTR_WHITE = "white"
ATTR_RGB_COLOR: Final = "rgb_color"
ATTR_RGBW_COLOR: Final = "rgbw_color"
ATTR_RGBWW_COLOR: Final = "rgbww_color"
ATTR_XY_COLOR: Final = "xy_color"
ATTR_HS_COLOR: Final = "hs_color"
ATTR_COLOR_TEMP: Final = "color_temp" # Deprecated in HA Core 2022.11
ATTR_KELVIN: Final = "kelvin" # Deprecated in HA Core 2022.11
ATTR_MIN_MIREDS: Final = "min_mireds" # Deprecated in HA Core 2022.11
ATTR_MAX_MIREDS: Final = "max_mireds" # Deprecated in HA Core 2022.11
ATTR_COLOR_TEMP_KELVIN: Final = "color_temp_kelvin"
ATTR_MIN_COLOR_TEMP_KELVIN: Final = "min_color_temp_kelvin"
ATTR_MAX_COLOR_TEMP_KELVIN: Final = "max_color_temp_kelvin"
ATTR_COLOR_NAME: Final = "color_name"
ATTR_WHITE: Final = "white"

# Brightness of the light, 0..255 or percentage
ATTR_BRIGHTNESS = "brightness"
ATTR_BRIGHTNESS_PCT = "brightness_pct"
ATTR_BRIGHTNESS_STEP = "brightness_step"
ATTR_BRIGHTNESS_STEP_PCT = "brightness_step_pct"
ATTR_BRIGHTNESS: Final = "brightness"
ATTR_BRIGHTNESS_PCT: Final = "brightness_pct"
ATTR_BRIGHTNESS_STEP: Final = "brightness_step"
ATTR_BRIGHTNESS_STEP_PCT: Final = "brightness_step_pct"

# String representing a profile (built-in ones or external defined).
ATTR_PROFILE = "profile"
ATTR_PROFILE: Final = "profile"

# If the light should flash, can be FLASH_SHORT or FLASH_LONG.
ATTR_FLASH = "flash"
ATTR_FLASH: Final = "flash"
FLASH_SHORT = "short"
FLASH_LONG = "long"

# List of possible effects
ATTR_EFFECT_LIST = "effect_list"

# Apply an effect to the light, can be EFFECT_COLORLOOP.
ATTR_EFFECT = "effect"
ATTR_EFFECT: Final = "effect"
EFFECT_COLORLOOP = "colorloop"
EFFECT_OFF = "off"
EFFECT_RANDOM = "random"
Expand Down Expand Up @@ -274,6 +274,36 @@ def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set[str] |
}


class LightTurnOnTD(TypedDict, total=False):
"""Light turn on data."""

profile: str
transition: float
brightness: int
brightness_pct: float
brightness_step: int
brightness_step_pct: float
color_name: str
color_temp: int
color_temp_kelvin: int
kelvin: int
hs_color: tuple[float, float]
rgb_color: tuple[int, int, int]
rgbw_color: tuple[int, int, int, int]
rgbww_color: tuple[int, int, int, int, int]
xy_color: tuple[float, float]
white: int
flash: Literal["short", "long"]
effect: str


class LightTurnOffTD(TypedDict, total=False):
"""Light turn off data."""

transition: float
flash: Literal["short", "long"]


_LOGGER = logging.getLogger(__name__)


Expand All @@ -284,7 +314,7 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool:


def preprocess_turn_on_alternatives(
hass: HomeAssistant, params: dict[str, Any]
hass: HomeAssistant, params: LightTurnOnTD | dict[str, Any]
) -> None:
"""Process extra data for turn light on request.
Expand All @@ -304,6 +334,7 @@ def preprocess_turn_on_alternatives(
_LOGGER.warning("Got unknown color %s, falling back to white", color_name)
params[ATTR_RGB_COLOR] = (255, 255, 255)

kelvin: int | None
if (mired := params.pop(ATTR_COLOR_TEMP, None)) is not None:
kelvin = color_util.color_temperature_mired_to_kelvin(mired)
params[ATTR_COLOR_TEMP] = int(mired)
Expand All @@ -324,9 +355,7 @@ def preprocess_turn_on_alternatives(
params[ATTR_BRIGHTNESS] = round(255 * brightness_pct / 100)


def filter_turn_off_params(
light: LightEntity, params: dict[str, Any]
) -> dict[str, Any]:
def filter_turn_off_params(light: LightEntity, params: LightTurnOnTD) -> LightTurnOffTD:
"""Filter out params not used in turn off or not supported by the light."""
if not params:
return params
Expand All @@ -338,10 +367,13 @@ def filter_turn_off_params(
if LightEntityFeature.TRANSITION not in supported_features:
params.pop(ATTR_TRANSITION, None)

return {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)}
return cast(
LightTurnOffTD,
{k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)},
)


def filter_turn_on_params(light: LightEntity, params: dict[str, Any]) -> dict[str, Any]:
def filter_turn_on_params(light: LightEntity, params: LightTurnOnTD) -> LightTurnOnTD:
"""Filter out params not supported by the light."""
supported_features = light.supported_features

Expand Down Expand Up @@ -407,7 +439,7 @@ async def async_handle_light_on_service( # noqa: C901
If brightness is set to 0, this service will turn the light off.
"""
params: dict[str, Any] = dict(call.data["params"])
params: LightTurnOnTD = cast(LightTurnOnTD, call.data["params"])

# Only process params once we processed brightness step
if params and (
Expand Down Expand Up @@ -442,7 +474,7 @@ async def async_handle_light_on_service( # noqa: C901
):
params.pop(ATTR_COLOR_TEMP)
color_temp = params.pop(ATTR_COLOR_TEMP_KELVIN)
brightness = params.get(ATTR_BRIGHTNESS, light.brightness)
brightness = params.get(ATTR_BRIGHTNESS, light.brightness) # type: ignore[assignment]
params[ATTR_RGBWW_COLOR] = color_util.color_temperature_to_rgbww(
color_temp,
brightness,
Expand Down Expand Up @@ -471,8 +503,7 @@ async def async_handle_light_on_service( # noqa: C901
rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color)
params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
elif (rgbww_color := params.pop(ATTR_RGBWW_COLOR, None)) is not None:
# https://github.com/python/mypy/issues/13673
rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg]
rgb_color = color_util.color_rgbww_to_rgb(
*rgbww_color,
light.min_color_temp_kelvin,
light.max_color_temp_kelvin,
Expand Down Expand Up @@ -594,7 +625,7 @@ async def async_handle_light_on_service( # noqa: C901
# Add a warning in Home Assistant Core 2024.3 if the brightness is set to an
# integer.
if params.get(ATTR_WHITE) is True:
params[ATTR_WHITE] = light.brightness
params[ATTR_WHITE] = cast(int, light.brightness)

# If both white and brightness are specified, override white
if (
Expand All @@ -614,7 +645,7 @@ async def async_handle_light_off_service(
light: LightEntity, call: ServiceCall
) -> None:
"""Handle turning off a light."""
params = dict(call.data["params"])
params: LightTurnOnTD = cast(LightTurnOnTD, call.data["params"])

if ATTR_TRANSITION not in params:
profiles.apply_default(light.entity_id, True, params)
Expand Down Expand Up @@ -776,19 +807,19 @@ async def async_initialize(self) -> None:

@callback
def apply_default(
self, entity_id: str, state_on: bool | None, params: dict[str, Any]
self, entity_id: str, state_on: bool | None, params: LightTurnOnTD
) -> None:
"""Return the default profile for the given light."""
for _entity_id in (entity_id, "group.all_lights"):
name = f"{_entity_id}.default"
if name in self.data:
if not state_on or not params:
self.apply_profile(name, params)
elif self.data[name].transition is not None:
params.setdefault(ATTR_TRANSITION, self.data[name].transition)
elif (transition := self.data[name].transition) is not None:
params.setdefault(ATTR_TRANSITION, transition)

@callback
def apply_profile(self, name: str, params: dict[str, Any]) -> None:
def apply_profile(self, name: str, params: LightTurnOnTD) -> None:
"""Apply a profile."""
if (profile := self.data.get(name)) is None:
return
Expand Down Expand Up @@ -1261,6 +1292,22 @@ def __should_report_light_issue(self) -> bool:
# philips_js has known issues, we don't need users to open issues
return self.platform.platform_name not in {"philips_js"}

def turn_on(self, **kwargs: Unpack[LightTurnOnTD]) -> None:
"""Turn the entity on."""
raise NotImplementedError

async def async_turn_on(self, **kwargs: Unpack[LightTurnOnTD]) -> None:
"""Turn the entity on."""
await self.hass.async_add_executor_job(partial(self.turn_on, **kwargs))

def turn_off(self, **kwargs: Unpack[LightTurnOffTD]) -> None:
"""Turn the entity off."""
raise NotImplementedError

async def async_turn_off(self, **kwargs: Unpack[LightTurnOffTD]) -> None:
"""Turn the entity off."""
await self.hass.async_add_executor_job(partial(self.turn_off, **kwargs))


# These can be removed if no deprecated constant are in this module anymore
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
Expand Down
Loading

0 comments on commit f3a9008

Please sign in to comment.