Skip to content

Commit

Permalink
Implement pydantic for lights (#436)
Browse files Browse the repository at this point in the history
* Implement pydantic for lights

* Save changes

* Fix typing

* Sort attributes

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update pytradfri/device/light_control.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update pytradfri/device/light_control.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Type hint values

* Use Mapping

* Fix

* Check for None

* Fix tests

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Remove deepcopy

* Black

* Black tests

* Add Black target version settings

* Save

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
  • Loading branch information
ggravlingen and MartinHjelmare authored Feb 27, 2022
1 parent 8507581 commit 1a6597f
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 92 deletions.
22 changes: 22 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,25 @@ disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true

[mypy-pytradfri.device.light]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true

[mypy-pytradfri.device.light_control]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[tool.black]
target-version = ["py38", "py39", "py310"]

[tool.isort]
# https://github.com/PyCQA/isort/wiki/isort-Settings
profile = "black"
Expand Down
32 changes: 15 additions & 17 deletions pytradfri/color.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
"""Test Color."""
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from .device.light import LightResponse

from .const import (
ATTR_LIGHT_COLOR_HEX,
ATTR_LIGHT_COLOR_HUE,
ATTR_LIGHT_COLOR_SATURATION,
ATTR_LIGHT_COLOR_X as X,
ATTR_LIGHT_COLOR_Y as Y,
ATTR_LIGHT_DIMMER,
ATTR_LIGHT_MIREDS,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP,
SUPPORT_HEX_COLOR,
Expand Down Expand Up @@ -44,28 +42,28 @@
COLORS = {name.lower().replace(" ", "_"): hex for hex, name in COLOR_NAMES.items()}


def supported_features(data: dict[str, str | int]) -> int:
def supported_features(data: LightResponse) -> int:
"""Return supported features."""
supported_color_features = 0

if ATTR_LIGHT_DIMMER in data:
if data.dimmer:
supported_color_features = supported_color_features + SUPPORT_BRIGHTNESS

if ATTR_LIGHT_COLOR_HEX in data:
if data.color_hex:
supported_color_features = supported_color_features + SUPPORT_HEX_COLOR

if ATTR_LIGHT_MIREDS in data:
if data.color_mireds:
supported_color_features = supported_color_features + SUPPORT_COLOR_TEMP

if X in data and Y in data:
if data.color_xy_x and data.color_xy_y:
supported_color_features = supported_color_features + SUPPORT_XY_COLOR

if (
ATTR_LIGHT_MIREDS not in data
and X in data
and Y in data
and ATTR_LIGHT_COLOR_SATURATION in data
and ATTR_LIGHT_COLOR_HUE in data
data.color_mireds is None
and data.color_xy_x is not None
and data.color_xy_y is not None
and data.color_saturation is not None
and data.color_hue is not None
):
supported_color_features = supported_color_features + SUPPORT_RGB_COLOR

Expand Down
3 changes: 2 additions & 1 deletion pytradfri/device/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
ROOT_DEVICES,
ROOT_SIGNAL_REPEATER,
)
from ..device.light import LightResponse
from ..resource import ApiResource, ApiResourceResponse
from .air_purifier_control import AirPurifierControl, AirPurifierResponse
from .blind_control import BlindControl, BlindResponse
Expand Down Expand Up @@ -56,7 +57,7 @@ class DeviceResponse(ApiResourceResponse):
blind_control: Optional[List[BlindResponse]] = Field(alias=ATTR_START_BLINDS)
device_info: DeviceInfoResponse = Field(alias=ATTR_DEVICE_INFO)
last_seen: Optional[int] = Field(alias=ATTR_LAST_SEEN)
light_control: Optional[List[Dict[str, Any]]] = Field(alias=ATTR_LIGHT_CONTROL)
light_control: Optional[List[LightResponse]] = Field(alias=ATTR_LIGHT_CONTROL)
reachable: int = Field(alias=ATTR_REACHABLE_STATE)
signal_repeater_control: Optional[List[Dict[str, Any]]] = Field(
alias=ROOT_SIGNAL_REPEATER
Expand Down
87 changes: 61 additions & 26 deletions pytradfri/device/light.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""Represent a light."""
from __future__ import annotations

from typing import Any
from typing import TYPE_CHECKING, Optional

from pydantic import BaseModel, Field

from ..color import supported_features
from ..const import (
ATTR_DEVICE_STATE,
ATTR_ID,
ATTR_LIGHT_COLOR_HEX,
ATTR_LIGHT_COLOR_HUE,
ATTR_LIGHT_COLOR_SATURATION,
Expand All @@ -19,6 +22,24 @@
SUPPORT_XY_COLOR,
)

if TYPE_CHECKING:
# avoid cyclic import at runtime.
from . import Device


class LightResponse(BaseModel):
"""Represent API response for a blind."""

color_mireds: Optional[int] = Field(alias=ATTR_LIGHT_MIREDS)
color_hex: Optional[str] = Field(alias=ATTR_LIGHT_COLOR_HEX)
color_xy_x: Optional[int] = Field(alias=ATTR_LIGHT_COLOR_X)
color_xy_y: Optional[int] = Field(alias=ATTR_LIGHT_COLOR_Y)
color_hue: Optional[int] = Field(alias=ATTR_LIGHT_COLOR_HUE)
color_saturation: Optional[int] = Field(alias=ATTR_LIGHT_COLOR_SATURATION)
dimmer: int = Field(alias=ATTR_LIGHT_DIMMER)
id: int = Field(alias=ATTR_ID)
state: int = Field(alias=ATTR_DEVICE_STATE)


class Light:
"""Represent a light.
Expand All @@ -27,69 +48,83 @@ class Light:
pdf
"""

def __init__(self, device, index):
def __init__(self, device: Device, index: int):
"""Create object of class."""
self.device = device
self.index = index

@property
def supported_features(self):
def supported_features(self) -> int:
"""Return supported features."""
return supported_features(self.raw)

@property
def state(self):
def state(self) -> bool:
"""Return device state."""
return self.raw.get(ATTR_DEVICE_STATE) == 1
return self.raw.state == 1

@property
def dimmer(self):
def dimmer(self) -> int | None:
"""Return dimmer if present."""
if self.supported_features & SUPPORT_BRIGHTNESS:
return self.raw.get(ATTR_LIGHT_DIMMER)
return self.raw.dimmer
return None

@property
def color_temp(self):
def color_temp(self) -> int | None:
"""Return color temperature."""
if self.supported_features & SUPPORT_COLOR_TEMP:
if self.raw.get(ATTR_LIGHT_MIREDS) != 0:
return self.raw.get(ATTR_LIGHT_MIREDS)
if self.supported_features & SUPPORT_COLOR_TEMP and self.raw.color_mireds:
return self.raw.color_mireds
return None

@property
def hex_color(self):
def hex_color(self) -> str | None:
"""Return hex color."""
if self.supported_features & SUPPORT_HEX_COLOR:
return self.raw.get(ATTR_LIGHT_COLOR_HEX)
return self.raw.color_hex
return None

@property
def xy_color(self):
def xy_color(self) -> tuple[int, int] | None:
"""Return xy color."""
if self.supported_features & SUPPORT_XY_COLOR:
return (self.raw.get(ATTR_LIGHT_COLOR_X), self.raw.get(ATTR_LIGHT_COLOR_Y))
if (
self.supported_features & SUPPORT_XY_COLOR
and self.raw.color_xy_x is not None
and self.raw.color_xy_y is not None
):
return (self.raw.color_xy_x, self.raw.color_xy_y)
return None

@property
def hsb_xy_color(self):
def hsb_xy_color(
self,
) -> tuple[int, int, int, int, int] | None:
"""Return hsb xy color."""
return (
self.raw.get(ATTR_LIGHT_COLOR_HUE),
self.raw.get(ATTR_LIGHT_COLOR_SATURATION),
self.raw.get(ATTR_LIGHT_DIMMER),
self.raw.get(ATTR_LIGHT_COLOR_X),
self.raw.get(ATTR_LIGHT_COLOR_Y),
)
if (
self.raw.color_hue is not None
and self.raw.color_saturation is not None
and self.raw.dimmer is not None
and self.raw.color_xy_x is not None
and self.raw.color_xy_y is not None
):
return (
self.raw.color_hue,
self.raw.color_saturation,
self.raw.dimmer,
self.raw.color_xy_x,
self.raw.color_xy_y,
)

return None

@property
def raw(self) -> dict[str, Any]:
def raw(self) -> LightResponse:
"""Return raw data that it represents."""
light_control_response = self.device.raw.light_control
assert light_control_response is not None
return light_control_response[self.index]

def __repr__(self):
def __repr__(self) -> str:
"""Return representation of class object."""
state = "on" if self.state else "off"
return (
Expand Down
Loading

0 comments on commit 1a6597f

Please sign in to comment.