This commit is contained in:
rooba 2022-11-28 00:54:10 -08:00
parent 5c930d7742
commit 353adfddd2
6 changed files with 210 additions and 120 deletions

9
README Normal file

@ -0,0 +1,9 @@
# Phlyght
This is an async implementation of the v2 Philips Hue API.
For a brief example of how to use, see [here](test.py)
## Depends
pydantic, httpx, yarl

@ -1,3 +1,49 @@
__all__ = ("Router", "route", "RouterMeta", "SubRouter", "HughApi") from .api import Router
from .models import (
Archetype,
Room,
Light,
Scene,
Zone,
BridgeHome,
GroupedLight,
Device,
Bridge,
DevicePower,
ZigbeeConnectivity,
ZGPConnectivity,
Motion,
Temperature,
LightLevel,
Button,
BehaviorScript,
BehaviorInstance,
GeofenceClient,
Geolocation,
EntertainmentConfiguration,
)
from .api import Router, route, RouterMeta, SubRouter, HughApi __all__ = (
"Router",
"Archetype",
"Room",
"Light",
"Scene",
"Zone",
"BridgeHome",
"GroupedLight",
"Device",
"Bridge",
"DevicePower",
"ZigbeeConnectivity",
"ZGPConnectivity",
"Motion",
"Temperature",
"LightLevel",
"Button",
"BehaviorScript",
"BehaviorInstance",
"GeofenceClient",
"Geolocation",
"EntertainmentConfiguration",
)

@ -6,9 +6,16 @@ from typing import Any, Literal, Optional
from httpx import AsyncClient from httpx import AsyncClient
from httpx._urls import URL as _URL from httpx._urls import URL as _URL
from yarl import URL as UR
# from rich import print try:
from yarl import URL as UR
except ImportError:
...
try:
from rich import print # noqa
except ImportError:
...
from . import models from . import models
@ -32,10 +39,17 @@ def get_url_args(url):
class URL(_URL): class URL(_URL):
def __truediv__(self, other): def __truediv__(self, other):
# Why am i doing this? good question.
try:
return URL(str(UR(f"{self}") / other.lstrip("/"))) return URL(str(UR(f"{self}") / other.lstrip("/")))
except NameError:
return URL(str(f"{self}/{other.lstrip('/')}"))
def __floordiv__(self, other): def __floordiv__(self, other):
try:
return URL(str(UR(f"{self}") / other.lstrip("/"))) return URL(str(UR(f"{self}") / other.lstrip("/")))
except NameError:
return URL(str(f"{self}/{other.lstrip('/')}"))
def ret_cls(cls): def ret_cls(cls):
@ -218,7 +232,7 @@ class HughApi(SubRouter):
async def get_light(self, light_id: str): async def get_light(self, light_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/light/{light_id}") @route("PUT", "/resource/light/{light_id}")
async def set_light( async def set_light(
self, self,
@ -242,7 +256,7 @@ class HughApi(SubRouter):
async def get_scenes(self): async def get_scenes(self):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("POST", "/resource/scene") @route("POST", "/resource/scene")
async def create_scene(self, **kwargs): async def create_scene(self, **kwargs):
... ...
@ -252,12 +266,12 @@ class HughApi(SubRouter):
async def get_scene(self, scene_id: str): async def get_scene(self, scene_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/scene/{scene_id}") @route("PUT", "/resource/scene/{scene_id}")
async def set_scene(self, scene_id: str, **kwargs): async def set_scene(self, scene_id: str, **kwargs):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("DELETE", "/resource/scene/{scene_id}") @route("DELETE", "/resource/scene/{scene_id}")
async def delete_scene(self, scene_id: str): async def delete_scene(self, scene_id: str):
... ...
@ -267,7 +281,7 @@ class HughApi(SubRouter):
async def get_rooms(self): async def get_rooms(self):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("POST", "/resource/room") @route("POST", "/resource/room")
async def create_room(self, **kwargs): async def create_room(self, **kwargs):
... ...
@ -277,12 +291,12 @@ class HughApi(SubRouter):
async def get_room(self, room_id: str): async def get_room(self, room_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/room/{room_id}") @route("PUT", "/resource/room/{room_id}")
async def set_room(self, room_id: str, **kwargs): async def set_room(self, room_id: str, **kwargs):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("DELETE", "/resource/room/{room_id}") @route("DELETE", "/resource/room/{room_id}")
async def delete_room(self, room_id: str): async def delete_room(self, room_id: str):
... ...
@ -292,7 +306,7 @@ class HughApi(SubRouter):
async def get_zones(self): async def get_zones(self):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("POST", "/resource/zone") @route("POST", "/resource/zone")
async def create_zone(self, **kwargs): async def create_zone(self, **kwargs):
... ...
@ -302,12 +316,12 @@ class HughApi(SubRouter):
async def get_zone(self, zone_id: str): async def get_zone(self, zone_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/zone/{zone_id}") @route("PUT", "/resource/zone/{zone_id}")
async def set_zone(self, zone_id: str, **kwargs): async def set_zone(self, zone_id: str, **kwargs):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("DELETE", "/resource/zone/{zone_id}") @route("DELETE", "/resource/zone/{zone_id}")
async def delete_zone(self, zone_id: str): async def delete_zone(self, zone_id: str):
... ...
@ -322,7 +336,7 @@ class HughApi(SubRouter):
async def get_bridge_home(self, bridge_home_id: str): async def get_bridge_home(self, bridge_home_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/bridge_home/{bridge_home_id}") @route("PUT", "/resource/bridge_home/{bridge_home_id}")
async def set_bridge_home(self, bridge_home_id: str, **kwargs): async def set_bridge_home(self, bridge_home_id: str, **kwargs):
... ...
@ -337,7 +351,7 @@ class HughApi(SubRouter):
async def get_grouped_light(self, grouped_light_id: str): async def get_grouped_light(self, grouped_light_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/grouped_light/{grouped_light_id}") @route("PUT", "/resource/grouped_light/{grouped_light_id}")
async def set_grouped_light(self, grouped_light_id: str, **kwargs): async def set_grouped_light(self, grouped_light_id: str, **kwargs):
... ...
@ -352,7 +366,7 @@ class HughApi(SubRouter):
async def get_device(self, device_id: str): async def get_device(self, device_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/device/{device_id}") @route("PUT", "/resource/device/{device_id}")
async def set_device(self, device_id: str, **kwargs): async def set_device(self, device_id: str, **kwargs):
... ...
@ -367,7 +381,7 @@ class HughApi(SubRouter):
async def get_bridge(self, bridge_id: str): async def get_bridge(self, bridge_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/bridges/{bridge_id}") @route("PUT", "/resource/bridges/{bridge_id}")
async def set_bridge(self, bridge_id: str, **kwargs): async def set_bridge(self, bridge_id: str, **kwargs):
... ...
@ -382,7 +396,7 @@ class HughApi(SubRouter):
async def get_device_power(self, device_power_id: str): async def get_device_power(self, device_power_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/device_power/{device_power_id}") @route("PUT", "/resource/device_power/{device_power_id}")
async def set_device_power(self, device_power_id: str, **kwargs): async def set_device_power(self, device_power_id: str, **kwargs):
... ...
@ -397,7 +411,7 @@ class HughApi(SubRouter):
async def get_zigbee_connectivity(self, zigbee_connectivity_id: str): async def get_zigbee_connectivity(self, zigbee_connectivity_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/zigbee_connectivity/{zigbee_connectivity_id}") @route("PUT", "/resource/zigbee_connectivity/{zigbee_connectivity_id}")
async def set_zigbee_connectivity(self, zigbee_connectivity_id: str, **kwargs): async def set_zigbee_connectivity(self, zigbee_connectivity_id: str, **kwargs):
... ...
@ -412,7 +426,7 @@ class HughApi(SubRouter):
async def get_zgb_connectivity(self, zgb_connectivity_id: str): async def get_zgb_connectivity(self, zgb_connectivity_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/zgb_connectivity/{zgb_connectivity_id}") @route("PUT", "/resource/zgb_connectivity/{zgb_connectivity_id}")
async def set_zgb_connectivity(self, zgb_connectivity_id: str, **kwargs): async def set_zgb_connectivity(self, zgb_connectivity_id: str, **kwargs):
... ...
@ -427,7 +441,7 @@ class HughApi(SubRouter):
async def get_motion(self, motion_id: str): async def get_motion(self, motion_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/motion/{motion_id}") @route("PUT", "/resource/motion/{motion_id}")
async def set_motion(self, motion_id: str, **kwargs): async def set_motion(self, motion_id: str, **kwargs):
... ...
@ -442,7 +456,7 @@ class HughApi(SubRouter):
async def get_temperature(self, temperature_id: str): async def get_temperature(self, temperature_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/temperature/{temperature_id}") @route("PUT", "/resource/temperature/{temperature_id}")
async def set_temperature(self, temperature_id: str, **kwargs): async def set_temperature(self, temperature_id: str, **kwargs):
... ...
@ -457,7 +471,7 @@ class HughApi(SubRouter):
async def get_light_level(self, light_level_id: str): async def get_light_level(self, light_level_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/light_level/{light_level_id}") @route("PUT", "/resource/light_level/{light_level_id}")
async def set_light_level(self, light_level_id: str, **kwargs): async def set_light_level(self, light_level_id: str, **kwargs):
... ...
@ -472,7 +486,7 @@ class HughApi(SubRouter):
async def get_button(self, button_id: str): async def get_button(self, button_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/button/{button_id}") @route("PUT", "/resource/button/{button_id}")
async def set_button(self, button_id: str, **kwargs): async def set_button(self, button_id: str, **kwargs):
... ...
@ -492,7 +506,7 @@ class HughApi(SubRouter):
async def get_behavior_instances(self): async def get_behavior_instances(self):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("POST", "/resource/behavior_instance") @route("POST", "/resource/behavior_instance")
async def create_behavior_instance(self, **kwargs): async def create_behavior_instance(self, **kwargs):
... ...
@ -502,12 +516,12 @@ class HughApi(SubRouter):
async def get_behavior_instance(self, behavior_instance_id: str): async def get_behavior_instance(self, behavior_instance_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/behavior_instance/{behavior_instance_id}") @route("PUT", "/resource/behavior_instance/{behavior_instance_id}")
async def set_behavior_instance(self, behavior_instance_id: str, **kwargs): async def set_behavior_instance(self, behavior_instance_id: str, **kwargs):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("DELETE", "/resource/behavior_instance/{behavior_instance_id}") @route("DELETE", "/resource/behavior_instance/{behavior_instance_id}")
async def delete_behavior_instance(self, behavior_instance_id: str): async def delete_behavior_instance(self, behavior_instance_id: str):
... ...
@ -517,7 +531,7 @@ class HughApi(SubRouter):
async def get_geofence_clients(self): async def get_geofence_clients(self):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("POST", "/resource/geofence_client") @route("POST", "/resource/geofence_client")
async def create_geofence_client(self, **kwargs): async def create_geofence_client(self, **kwargs):
... ...
@ -527,12 +541,12 @@ class HughApi(SubRouter):
async def get_geofence_client(self, geofence_client_id: str): async def get_geofence_client(self, geofence_client_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/geofence_client/{geofence_client_id}") @route("PUT", "/resource/geofence_client/{geofence_client_id}")
async def set_geofence_client(self, geofence_client_id: str, **kwargs): async def set_geofence_client(self, geofence_client_id: str, **kwargs):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("DELETE", "/resource/geofence_client/{geofence_client_id}") @route("DELETE", "/resource/geofence_client/{geofence_client_id}")
async def delete_geofence_client(self, geofence_client_id: str): async def delete_geofence_client(self, geofence_client_id: str):
... ...
@ -547,7 +561,7 @@ class HughApi(SubRouter):
async def get_geolocation(self, geolocation_id: str): async def get_geolocation(self, geolocation_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/geolocation/{geolocation_id}") @route("PUT", "/resource/geolocation/{geolocation_id}")
async def set_geolocation(self, geolocation_id: str, **kwargs): async def set_geolocation(self, geolocation_id: str, **kwargs):
... ...
@ -557,7 +571,7 @@ class HughApi(SubRouter):
async def get_entertainment_configurations(self): async def get_entertainment_configurations(self):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("POST", "/resource/entertainment_configuration") @route("POST", "/resource/entertainment_configuration")
async def create_entertainment_configuration(self, **kwargs): async def create_entertainment_configuration(self, **kwargs):
... ...
@ -571,7 +585,7 @@ class HughApi(SubRouter):
): ):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route( @route(
"PUT", "/resource/entertainment_configuration/{entertainment_configuration_id}" "PUT", "/resource/entertainment_configuration/{entertainment_configuration_id}"
) )
@ -580,7 +594,7 @@ class HughApi(SubRouter):
): ):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route( @route(
"DELETE", "DELETE",
"/resource/entertainment_configuration/{entertainment_configuration_id}", "/resource/entertainment_configuration/{entertainment_configuration_id}",
@ -600,7 +614,7 @@ class HughApi(SubRouter):
async def get_entertainment(self, entertainment_id: str): async def get_entertainment(self, entertainment_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/entertainment/{entertainment_id}") @route("PUT", "/resource/entertainment/{entertainment_id}")
async def set_entertainment(self, entertainment_id: str, **kwargs): async def set_entertainment(self, entertainment_id: str, **kwargs):
... ...
@ -615,7 +629,7 @@ class HughApi(SubRouter):
async def get_homekit(self, homekit_id: str): async def get_homekit(self, homekit_id: str):
... ...
@ret_cls(models.Identifier) @ret_cls(models._Identifier)
@route("PUT", "/resource/homekit/{homekit_id}") @route("PUT", "/resource/homekit/{homekit_id}")
async def set_homekit(self, homekit_id: str, **kwargs): async def set_homekit(self, homekit_id: str, **kwargs):
... ...

@ -1,11 +1,34 @@
__all__ = ("Room", "Light", "Scene")
from typing import Any, Literal, Optional, Generic, TypeVar from typing import Any, Literal, Optional, Generic, TypeVar
from uuid import UUID from uuid import UUID
from pydantic import BaseModel, Field
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, auto from enum import Enum, auto
from pydantic import BaseModel, Field
__all__ = (
"Archetype",
"Room",
"Light",
"Scene",
"Zone",
"BridgeHome",
"GroupedLight",
"Device",
"Bridge",
"DevicePower",
"ZigbeeConnectivity",
"ZGPConnectivity",
"Motion",
"Temperature",
"LightLevel",
"Button",
"BehaviorScript",
"BehaviorInstance",
"GeofenceClient",
"Geolocation",
"EntertainmentConfiguration",
)
_T = TypeVar("_T") _T = TypeVar("_T")
@ -55,70 +78,65 @@ class Archetype(Enum):
HUE_SIGNE = auto() HUE_SIGNE = auto()
class _id_v1(str):
def __get__(self, instance, owner):
return Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$)")
@dataclass @dataclass
class Dimming: class _Dimming:
brightness: float brightness: float
min_dim_level: Optional[float] = Field(0, repr=False) min_dim_level: Optional[float] = Field(0, repr=False)
@dataclass @dataclass
class XY: class _XY:
x: float x: float
y: float y: float
@dataclass @dataclass
class On: class _On:
on: bool = Field(..., alias="on") on: bool = Field(..., alias="on")
@dataclass @dataclass
class ColorPoint: class _ColorPoint:
xy: XY xy: _XY
@dataclass(frozen=True) @dataclass(frozen=True)
class Identifier: class _Identifier:
rid: str rid: str
rtype: str rtype: str
@dataclass(frozen=True) @dataclass(frozen=True)
class Metadata: class _Metadata:
name: str name: str
archetype: Optional[Archetype] = Archetype.UNKNOWN_ARCHETYPE archetype: Optional[Archetype] = Archetype.UNKNOWN_ARCHETYPE
image: Optional[Identifier] = Field(None, repr=False) image: Optional[_Identifier] = Field(None, repr=False)
class HueGroupedMeta(type): class _HueGroupedMeta(type):
def update(cls): def update(cls):
for v in cls.__dict__.values(): for v in cls.__dict__.values():
if hasattr(v, "update_forward_refs"): if hasattr(v, "update_forward_refs"):
v.update_forward_refs() v.update_forward_refs()
class HueGrouped(metaclass=HueGroupedMeta): class _HueGrouped(metaclass=_HueGroupedMeta):
... ...
class _Lights(HueGrouped): class _Lights(_HueGrouped):
class ColorTemperature(BaseModel): class ColorTemperature(BaseModel):
mirek: Optional[int] mirek: Optional[int]
mirek_valid: bool mirek_valid: bool
mirek_schema: dict[str, float] mirek_schema: dict[str, float]
class Gamut(BaseModel): class Gamut(BaseModel):
red: XY red: _XY
green: XY green: _XY
blue: XY blue: _XY
class Color(BaseModel): class Color(BaseModel):
xy: XY xy: _XY
gamut: "_Lights.Gamut" gamut: "_Lights.Gamut"
gamut_type: Literal["A"] | Literal["B"] | Literal["C"] gamut_type: Literal["A"] | Literal["B"] | Literal["C"]
@ -129,7 +147,7 @@ class _Lights(HueGrouped):
speed_valid: bool speed_valid: bool
class Gradient(BaseModel): class Gradient(BaseModel):
points: list[ColorPoint] points: list[_ColorPoint]
points_capable: int points_capable: int
class Effects(BaseModel): class Effects(BaseModel):
@ -148,10 +166,10 @@ class _Lights(HueGrouped):
class Light(Generic[_T], BaseModel): class Light(Generic[_T], BaseModel):
id: UUID id: UUID
id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$") id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Identifier owner: _Identifier
metadata: Metadata metadata: _Metadata
on: On = Field(repr=False) on: _On = Field(repr=False)
dimming: Dimming dimming: _Dimming
dimming_delta: dict dimming_delta: dict
color_temperature: Optional["_Lights.ColorTemperature"] color_temperature: Optional["_Lights.ColorTemperature"]
color_temperature_delta: Optional[dict] color_temperature_delta: Optional[dict]
@ -168,40 +186,40 @@ class _Lights(HueGrouped):
_Lights.update() _Lights.update()
class _Scenes(HueGrouped): class _Scenes(_HueGrouped):
class Action(BaseModel): class Action(BaseModel):
on: Optional[On] on: Optional[_On]
dimming: Optional[Dimming] dimming: Optional[_Dimming]
color: Optional[ColorPoint] color: Optional[_ColorPoint]
color_temperature: Optional[dict[str, float]] color_temperature: Optional[dict[str, float]]
gradient: Optional[dict[str, list[ColorPoint]]] gradient: Optional[dict[str, list[_ColorPoint]]]
effects: Optional[dict[str, str]] effects: Optional[dict[str, str]]
dynamics: Optional[dict[str, float]] dynamics: Optional[dict[str, float]]
class Actions(BaseModel): class Actions(BaseModel):
target: Identifier target: _Identifier
action: "_Scenes.Action" = Field(repr=False) action: "_Scenes.Action" = Field(repr=False)
dimming: Optional[Dimming] dimming: Optional[_Dimming]
color: Optional[ColorPoint] color: Optional[_ColorPoint]
class PaletteColor(BaseModel): class PaletteColor(BaseModel):
color: ColorPoint color: _ColorPoint
dimming: Dimming dimming: _Dimming
class PaletteTemperature(BaseModel): class PaletteTemperature(BaseModel):
color_temperature: dict[str, float] color_temperature: dict[str, float]
dimming: Dimming dimming: _Dimming
class Palette(BaseModel): class Palette(BaseModel):
color: list["_Scenes.PaletteColor"] color: list["_Scenes.PaletteColor"]
dimming: Optional[list[Dimming]] dimming: Optional[list[_Dimming]]
color_temperature: list["_Scenes.PaletteTemperature"] color_temperature: list["_Scenes.PaletteTemperature"]
class Scene(BaseModel): class Scene(BaseModel):
id: UUID id: UUID
id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$") id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
metadata: Metadata metadata: _Metadata
group: Identifier group: _Identifier
actions: list["_Scenes.Actions"] actions: list["_Scenes.Actions"]
palette: "_Scenes.Palette" palette: "_Scenes.Palette"
speed: float speed: float
@ -215,38 +233,38 @@ _Scenes.update()
class Room(BaseModel): class Room(BaseModel):
type: Literal["room"] type: Literal["room"]
id: UUID id: UUID
id_v1: _id_v1 id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
services: list[Identifier] services: list[_Identifier]
metadata: Metadata metadata: _Metadata
children: list[Identifier] children: list[_Identifier]
class Zone(BaseModel): class Zone(BaseModel):
type: Literal["zone"] type: Literal["zone"]
id: UUID id: UUID
id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$") id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
services: list[Identifier] services: list[_Identifier]
metadata: Metadata metadata: _Metadata
children: list[Identifier] children: list[_Identifier]
class BridgeHome(BaseModel): class BridgeHome(BaseModel):
type: Literal["bridge_home"] type: Literal["bridge_home"]
id: UUID id: UUID
id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$") id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
services: list[Identifier] services: list[_Identifier]
children: list[Identifier] children: list[_Identifier]
class GroupedLight(BaseModel): class GroupedLight(BaseModel):
type: Literal["grouped_light"] type: Literal["grouped_light"]
id: UUID id: UUID
id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$") id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
on: On = Field(repr=False) on: _On = Field(repr=False)
alert: list[str] alert: list[str]
class ProductData(BaseModel): class _ProductData(BaseModel):
model_id: str model_id: str
manufacturer_name: str manufacturer_name: str
product_name: str product_name: str
@ -260,9 +278,9 @@ class Device(BaseModel):
type: Literal["device"] type: Literal["device"]
id: UUID id: UUID
id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$") id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
services: list[Identifier] services: list[_Identifier]
metadata: Metadata metadata: _Metadata
product_data: ProductData product_data: _ProductData
class Bridge(BaseModel): class Bridge(BaseModel):
@ -273,7 +291,7 @@ class Bridge(BaseModel):
time_zone: dict[str, str] time_zone: dict[str, str]
class PowerState(BaseModel): class _PowerState(BaseModel):
battery_state: Literal["normal", "low", "critical"] battery_state: Literal["normal", "low", "critical"]
battery_level: float = Field(lt=100.0, gt=0.0) battery_level: float = Field(lt=100.0, gt=0.0)
@ -282,15 +300,15 @@ class DevicePower(BaseModel):
type: Literal["device_power"] type: Literal["device_power"]
id: UUID id: UUID
id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$") id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Identifier owner: _Identifier
power_state: PowerState power_state: _PowerState
class ZigbeeConnectivity(BaseModel): class ZigbeeConnectivity(BaseModel):
type: Literal["zigbee_connectivity"] type: Literal["zigbee_connectivity"]
id: UUID id: UUID
id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$") id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Identifier owner: _Identifier
status: Literal[ status: Literal[
"connected", "disconnected", "connectivity_issue", "unidirectional_incoming" "connected", "disconnected", "connectivity_issue", "unidirectional_incoming"
] ]
@ -301,7 +319,7 @@ class ZGPConnectivity(BaseModel):
type: Literal["zgp_connectivity"] type: Literal["zgp_connectivity"]
id: UUID id: UUID
id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$") id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Identifier owner: _Identifier
status: Literal[ status: Literal[
"connected", "disconnected", "connectivity_issue", "unidirectional_incoming" "connected", "disconnected", "connectivity_issue", "unidirectional_incoming"
] ]
@ -312,7 +330,7 @@ class Motion(BaseModel):
type: Literal["motion"] type: Literal["motion"]
id: UUID id: UUID
id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$") id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Identifier owner: _Identifier
enabled: bool enabled: bool
motion: dict[str, bool] motion: dict[str, bool]
@ -326,7 +344,7 @@ class Temperature(BaseModel):
type: Literal["temperature"] type: Literal["temperature"]
id: UUID id: UUID
id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$") id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Identifier owner: _Identifier
enabled: bool enabled: bool
temperature: _Temp temperature: _Temp
@ -340,7 +358,7 @@ class LightLevel(BaseModel):
type: Literal["light_level"] type: Literal["light_level"]
id: UUID id: UUID
id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$") id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Identifier owner: _Identifier
enabled: bool enabled: bool
light: _Light light: _Light
@ -349,7 +367,7 @@ class Button(BaseModel):
type: Literal["button"] type: Literal["button"]
id: UUID id: UUID
id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$") id_v1: str = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Identifier owner: _Identifier
metadata: dict[Literal["control_id"], int] metadata: dict[Literal["control_id"], int]
button: dict[ button: dict[
Literal["last_event"], Literal["last_event"],
@ -375,9 +393,9 @@ class BehaviorScript(BaseModel):
metadata: dict[str, str] metadata: dict[str, str]
class Dependee(BaseModel): class _Dependee(BaseModel):
type: str type: str
target: Identifier target: _Identifier
level: str level: str
@ -389,7 +407,7 @@ class BehaviorInstance(BaseModel):
enabled: bool enabled: bool
state: Optional[dict[str, Any]] state: Optional[dict[str, Any]]
configuration: dict[str, Any] configuration: dict[str, Any]
dependees: list[Dependee] dependees: list[_Dependee]
status: Literal["initializing", "running", "disabled", "errored"] status: Literal["initializing", "running", "disabled", "errored"]
last_error: str last_error: str
metadata: dict[Literal["name"], str] metadata: dict[Literal["name"], str]
@ -410,9 +428,9 @@ class Geolocation(BaseModel):
is_configured: bool = False is_configured: bool = False
class StreamProxy(BaseModel): class _StreamProxy(BaseModel):
mode: Literal["auto", "manual"] mode: Literal["auto", "manual"]
node: Identifier node: _Identifier
class EntertainmentConfiguration(BaseModel): class EntertainmentConfiguration(BaseModel):
@ -423,8 +441,8 @@ class EntertainmentConfiguration(BaseModel):
name: Optional[str] = "" name: Optional[str] = ""
configuration_type: Literal["screen", "monitor", "music", "3dspace", "other"] configuration_type: Literal["screen", "monitor", "music", "3dspace", "other"]
status: Literal["active", "inactive"] status: Literal["active", "inactive"]
active_streamer: Identifier active_streamer: _Identifier
stream_proxy: StreamProxy stream_proxy: _StreamProxy
... # TODO: finish the last 4 objects ... # TODO: finish the last 4 objects

@ -6,14 +6,13 @@ authors = ["Ra <ra@tcp.direct>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.11,<4.0.0" python = ">=3.11,<4.0.0"
rich = ">=12.6.0" httpx = ">=0.23.1"
aioredis = ">=2.0.1" pydantic = ">=1.10.2"
httpx = "^0.23.1" yarl = ">=1.8.1"
yarl = "^1.8.1"
pydantic = "^1.10.2"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
black = ">=22.10.0" black = ">=22.10.0"
rich = ">=12.6.0"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]

12
test.py

@ -1,16 +1,20 @@
from asyncio import run from asyncio import run
from rich import print
from phlyght.api import Router from phlyght.api import Router
try:
from rich import print # noqa
except ImportError:
...
async def main(): async def main():
router = Router("Your user key with the hue bridge") router = Router("user api key")
lights = await router.get_lights() lights = await router.get_lights()
for light in lights: for light in lights:
detailed_light = await router.get_light(light_id=str(light.id)) detailed_light = await router.get_light(light_id=str(light.id)) # noqa
print(detailed_light) print(light, detailed_light)
scenes = await router.get_scenes() scenes = await router.get_scenes()
for scene in scenes: for scene in scenes: