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

Add type hints for Group class #447

Merged
merged 19 commits into from
Mar 8, 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
11 changes: 11 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true

[mypy-pytradfri.group]
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.mood]
check_untyped_defs = true
disallow_incomplete_defs = true
Expand Down
122 changes: 86 additions & 36 deletions pytradfri/group.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
"""Group handling."""
from __future__ import annotations

from typing import TYPE_CHECKING, Dict, List, Optional

from pydantic import Field

from .color import COLORS
from .command import Command
from .const import (
ATTR_DEVICE_STATE,
ATTR_GROUP_ID,
Expand All @@ -23,86 +30,115 @@
RANGE_Y,
ROOT_GROUPS,
)
from .device import Device
from .error import ColorError
from .resource import ApiResource
from .mood import Mood
from .resource import ApiResource, ApiResourceResponse, TypeRaw

if TYPE_CHECKING:
from .gateway import Gateway


class GroupResponse(ApiResourceResponse):
"""Represent API response for a blind."""

color_hex: Optional[str] = Field(alias=ATTR_LIGHT_COLOR_HEX)
dimmer: int = Field(alias=ATTR_LIGHT_DIMMER)
group_members: Dict[str, Dict[str, List[int]]] = Field(alias=ATTR_GROUP_MEMBERS)
mood_id: str = Field(alias=ATTR_MOOD)
state: int = Field(alias=ATTR_DEVICE_STATE)


class Group(ApiResource):
"""Represent a group."""

def __init__(self, gateway, raw):
_model_class: type[GroupResponse] = GroupResponse
raw: GroupResponse

def __init__(self, gateway: Gateway, raw: TypeRaw) -> None:
"""Create object of class."""
super().__init__(raw)
self._gateway = gateway

@property
def path(self):
def path(self) -> list[str]:
"""Path."""
return [ROOT_GROUPS, str(self.id)]

@property
def state(self):
def state(self) -> bool:
"""Boolean representing the light state of the group."""
return self.raw.get(ATTR_DEVICE_STATE) == 1
return self.raw.state == 1

@property
def dimmer(self):
def dimmer(self) -> int:
"""Dimmer value of the group."""
return self.raw.get(ATTR_LIGHT_DIMMER)
return self.raw.dimmer

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

return None

@property
def member_ids(self):
"""Members of this group."""
info = self.raw.get(ATTR_GROUP_MEMBERS, {})
def member_ids(self) -> list[int]:
"""
Members of this group.

if not info or ATTR_HS_LINK not in info:
return []
A group with devices will look like this:
{"15002": {"9003": [65536, 65537]}}

return info[ATTR_HS_LINK].get(ATTR_ID, [])
An empty group will look like this:
{"15002": {"9003": []}}

If a group is created in the app and no devices are added
to it, it will immediately be deleted.
"""
return self.raw.group_members[ATTR_HS_LINK][ATTR_ID]

@property
def mood_id(self):
def mood_id(self) -> str:
"""Active mood."""
return self.raw.get(ATTR_MOOD)
return self.raw.mood_id

def members(self):
def members(self) -> list[Command[Device]]:
"""Return device objects of members of this group."""
return [self._gateway.get_device(dev) for dev in self.member_ids]
return [self._gateway.get_device(str(dev)) for dev in self.member_ids]

def add_member(self, memberid):
def add_member(self, memberid: str) -> Command[None]:
"""Add a member to this group."""
return self._gateway.add_group_member(
{ATTR_GROUP_ID: self.id, ATTR_ID: [memberid]}
)

def remove_member(self, memberid):
def remove_member(self, memberid: str) -> Command[None]:
"""Remove a member from this group."""
return self._gateway.remove_group_member(
{ATTR_GROUP_ID: self.id, ATTR_ID: [memberid]}
)

def moods(self):
def moods(self) -> Command[list[Command[Mood]]]:
"""Return mood objects of moods in this group."""
return self._gateway.get_moods(self.id)

def mood(self):
def mood(self) -> Command[Mood]:
"""Active mood."""
return self._gateway.get_mood(self.mood_id, mood_parent=self.id)

def activate_mood(self, mood_id):
def activate_mood(self, mood_id: str) -> Command[None]:
"""Activate a mood."""
return self.set_values({ATTR_MOOD: mood_id, ATTR_DEVICE_STATE: int(self.state)})

def set_state(self, state):
def set_state(self, state: bool) -> Command[None]:
"""Set state of a group."""
return self.set_values({ATTR_DEVICE_STATE: int(state)})

def set_dimmer(self, dimmer, transition_time=None):
def set_dimmer(
self, dimmer: int, transition_time: int | None = None
) -> Command[None]:
"""Set dimmer value of a group.

dimmer: Integer between 0..255
Expand All @@ -115,7 +151,9 @@ def set_dimmer(self, dimmer, transition_time=None):
values[ATTR_TRANSITION_TIME] = transition_time
return self.set_values(values)

def set_color_temp(self, color_temp, *, index=0, transition_time=None):
def set_color_temp(
self, color_temp: int, *, index: int = 0, transition_time: int | None = None
) -> Command[None]:
"""Set color temp a light."""
self._value_validate(color_temp, RANGE_MIREDS, "Color temperature")

Expand All @@ -126,18 +164,26 @@ def set_color_temp(self, color_temp, *, index=0, transition_time=None):

return self.set_values(values)

def set_hex_color(self, color, transition_time=None):
def set_hex_color(
self, color: str, transition_time: int | None = None
) -> Command[None]:
"""Set hex color of a group."""
values = {
values: dict[str, int | str] = {
ATTR_LIGHT_COLOR_HEX: color,
}
if transition_time is not None:
values[ATTR_TRANSITION_TIME] = transition_time
return self.set_values(values)

def set_hsb(
self, hue, saturation, brightness=None, *, index=0, transition_time=None
):
self,
hue: int,
saturation: int,
brightness: int | None = None,
*,
index: int = 0,
transition_time: int | None = None,
) -> Command[None]:
"""Set HSB color settings of the light."""
self._value_validate(hue, RANGE_HUE, "Hue")
self._value_validate(saturation, RANGE_SATURATION, "Saturation")
Expand All @@ -153,7 +199,9 @@ def set_hsb(

return self.set_values(values)

def set_xy_color(self, color_x, color_y, transition_time=None):
def set_xy_color(
self, color_x: int, color_y: int, transition_time: int | None = None
) -> Command[None]:
"""Set xy color of a group."""
self._value_validate(color_x, RANGE_X, "X color")
self._value_validate(color_y, RANGE_Y, "Y color")
Expand All @@ -165,7 +213,9 @@ def set_xy_color(self, color_x, color_y, transition_time=None):

return self.set_values(values)

def set_predefined_color(self, colorname, transition_time=None):
def set_predefined_color(
self, colorname: str, transition_time: int | None = None
) -> Command[None]:
"""Set predefined color for group."""
try:
color = COLORS[colorname.lower().replace(" ", "_")]
Expand All @@ -174,15 +224,15 @@ def set_predefined_color(self, colorname, transition_time=None):
raise ColorError(f"Invalid color specified: {colorname}") from exc

def _value_validate(
self, value, rnge, identifier="Given"
): # pylint: disable=no-self-use
self, value: int, rnge: tuple[int, int], identifier: str = "Given"
) -> None: # pylint: disable=no-self-use
"""Make sure a value is within a given range."""
if value is not None and (value < rnge[0] or value > rnge[1]):
raise ValueError(
f"{identifier} value must be between {rnge[0]} and {rnge[1]}."
)

def __repr__(self):
def __repr__(self) -> str:
"""Return representation of class object."""
state = "on" if self.state else "off"
return f"<Group {self.name} - {state}>"
2 changes: 1 addition & 1 deletion tests/test_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def test_setters(group):
assert cmd.data == {ATTR_GROUP_ID: GROUP[ATTR_ID], ATTR_ID: [65547]}


def test_moods(group):
def test_moods(group: Group) -> None:
"""Test moods."""
cmd = group.moods()
assert cmd.path == [ROOT_MOODS, str(group.id)]