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
View 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

View File

@ -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",
)

View File

@ -6,9 +6,16 @@ from typing import Any, Literal, Optional
from httpx import AsyncClient
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
@ -32,10 +39,17 @@ def get_url_args(url):
class URL(_URL):
def __truediv__(self, other):
return URL(str(UR(f"{self}") / other.lstrip("/")))
# Why am i doing this? good question.
try:
return URL(str(UR(f"{self}") / other.lstrip("/")))
except NameError:
return URL(str(f"{self}/{other.lstrip('/')}"))
def __floordiv__(self, other):
return URL(str(UR(f"{self}") / other.lstrip("/")))
try:
return URL(str(UR(f"{self}") / other.lstrip("/")))
except NameError:
return URL(str(f"{self}/{other.lstrip('/')}"))
def ret_cls(cls):
@ -218,7 +232,7 @@ class HughApi(SubRouter):
async def get_light(self, light_id: str):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/light/{light_id}")
async def set_light(
self,
@ -242,7 +256,7 @@ class HughApi(SubRouter):
async def get_scenes(self):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("POST", "/resource/scene")
async def create_scene(self, **kwargs):
...
@ -252,12 +266,12 @@ class HughApi(SubRouter):
async def get_scene(self, scene_id: str):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/scene/{scene_id}")
async def set_scene(self, scene_id: str, **kwargs):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("DELETE", "/resource/scene/{scene_id}")
async def delete_scene(self, scene_id: str):
...
@ -267,7 +281,7 @@ class HughApi(SubRouter):
async def get_rooms(self):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("POST", "/resource/room")
async def create_room(self, **kwargs):
...
@ -277,12 +291,12 @@ class HughApi(SubRouter):
async def get_room(self, room_id: str):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/room/{room_id}")
async def set_room(self, room_id: str, **kwargs):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("DELETE", "/resource/room/{room_id}")
async def delete_room(self, room_id: str):
...
@ -292,7 +306,7 @@ class HughApi(SubRouter):
async def get_zones(self):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("POST", "/resource/zone")
async def create_zone(self, **kwargs):
...
@ -302,12 +316,12 @@ class HughApi(SubRouter):
async def get_zone(self, zone_id: str):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/zone/{zone_id}")
async def set_zone(self, zone_id: str, **kwargs):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("DELETE", "/resource/zone/{zone_id}")
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):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/bridge_home/{bridge_home_id}")
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):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/grouped_light/{grouped_light_id}")
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):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/device/{device_id}")
async def set_device(self, device_id: str, **kwargs):
...
@ -367,7 +381,7 @@ class HughApi(SubRouter):
async def get_bridge(self, bridge_id: str):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/bridges/{bridge_id}")
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):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/device_power/{device_power_id}")
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):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/zigbee_connectivity/{zigbee_connectivity_id}")
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):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/zgb_connectivity/{zgb_connectivity_id}")
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):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/motion/{motion_id}")
async def set_motion(self, motion_id: str, **kwargs):
...
@ -442,7 +456,7 @@ class HughApi(SubRouter):
async def get_temperature(self, temperature_id: str):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/temperature/{temperature_id}")
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):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/light_level/{light_level_id}")
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):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/button/{button_id}")
async def set_button(self, button_id: str, **kwargs):
...
@ -492,7 +506,7 @@ class HughApi(SubRouter):
async def get_behavior_instances(self):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("POST", "/resource/behavior_instance")
async def create_behavior_instance(self, **kwargs):
...
@ -502,12 +516,12 @@ class HughApi(SubRouter):
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}")
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}")
async def delete_behavior_instance(self, behavior_instance_id: str):
...
@ -517,7 +531,7 @@ class HughApi(SubRouter):
async def get_geofence_clients(self):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("POST", "/resource/geofence_client")
async def create_geofence_client(self, **kwargs):
...
@ -527,12 +541,12 @@ class HughApi(SubRouter):
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}")
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}")
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):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/geolocation/{geolocation_id}")
async def set_geolocation(self, geolocation_id: str, **kwargs):
...
@ -557,7 +571,7 @@ class HughApi(SubRouter):
async def get_entertainment_configurations(self):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("POST", "/resource/entertainment_configuration")
async def create_entertainment_configuration(self, **kwargs):
...
@ -571,7 +585,7 @@ class HughApi(SubRouter):
):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route(
"PUT", "/resource/entertainment_configuration/{entertainment_configuration_id}"
)
@ -580,7 +594,7 @@ class HughApi(SubRouter):
):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route(
"DELETE",
"/resource/entertainment_configuration/{entertainment_configuration_id}",
@ -600,7 +614,7 @@ class HughApi(SubRouter):
async def get_entertainment(self, entertainment_id: str):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/entertainment/{entertainment_id}")
async def set_entertainment(self, entertainment_id: str, **kwargs):
...
@ -615,7 +629,7 @@ class HughApi(SubRouter):
async def get_homekit(self, homekit_id: str):
...
@ret_cls(models.Identifier)
@ret_cls(models._Identifier)
@route("PUT", "/resource/homekit/{homekit_id}")
async def set_homekit(self, homekit_id: str, **kwargs):
...

View File

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

View File

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

12
test.py
View File

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