phlyght/phlyght/models.py

618 lines
16 KiB
Python

from typing import Any, Literal, Optional, TypeAlias, TypeVar
from uuid import UUID
from enum import Enum, auto
from pydantic import BaseModel, Field
try:
from ujson import loads, dumps # type: ignore
except ImportError:
from orjson import loads, dumps # type: ignore
except ImportError: # type: ignore
from json import loads, dumps # noqa
__all__ = (
"Archetype",
"RoomType",
"Room",
"Light",
"Scene",
"Zone",
"BridgeHome",
"GroupedLight",
"Device",
"Bridge",
"DevicePower",
"ZigbeeConnectivity",
"ZGPConnectivity",
"Motion",
"Temperature",
"LightLevel",
"Button",
"BehaviorScript",
"BehaviorInstance",
"GeofenceClient",
"Geolocation",
"EntertainmentConfiguration",
"Entertainment",
"Resource",
"Homekit",
)
_T_M: TypeAlias = "RoomType | Archetype | Room | Light | Scene | Zone | BridgeHome | GroupedLight | Device | Bridge | DevicePower | ZigbeeConnectivity | ZGPConnectivity | Motion | Temperature | LightLevel | Button | BehaviorScript | BehaviorInstance | GeofenceClient | Geolocation | EntertainmentConfiguration | Entertainment | Resource | Homekit"
_T = TypeVar("_T")
class Entity(BaseModel):
__module__ = "phlyght"
__cache__: dict[str, type] = {}
id: UUID = Field(description="The unique identifier of the entity.")
class Config:
__root__: Optional["Entity"]
json_loads = loads
json_dumps = dumps
@classmethod
def get_entities(cls) -> dict[str, type]:
return cls.__cache__
@classmethod
def __prepare__(cls, name, bases, **kwds):
return super().__prepare__(name, bases, **kwds)
def __init_subclass__(cls, **kwargs):
super().__init_subclass__()
Entity.__cache__[
_.get_default() if (_ := cls.__fields__.get("type")) else "unknown"
] = cls
def __hash__(self) -> int:
return hash(self.id)
class RoomType(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values):
return name.lower()
LIVING_ROOM = auto()
KITCHEN = auto()
DINING = auto()
BEDROOM = auto()
KIDS_BEDROOM = auto()
BATHROOM = auto()
NURSERY = auto()
RECREATION = auto()
OFFICE = auto()
GYM = auto()
HALLWAY = auto()
TOILET = auto()
FRONT_DOOR = auto()
GARAGE = auto()
TERRACE = auto()
GARDEN = auto()
DRIVEWAY = auto()
CARPORT = auto()
HOME = auto()
DOWNSTAIRS = auto()
UPSTAIRS = auto()
TOP_FLOOR = auto()
ATTIC = auto()
GUEST_ROOM = auto()
STAIRCASE = auto()
LOUNGE = auto()
MAN_CAVE = auto()
COMPUTER = auto()
STUDIO = auto()
MUSIC = auto()
TV = auto()
READING = auto()
CLOSET = auto()
STORAGE = auto()
LAUNDRY_ROOM = auto()
BALCONY = auto()
PORCH = auto()
BARBECUE = auto()
POOL = auto()
OTHER = auto()
class Archetype(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values):
return name.lower()
BRIDGE_V2 = auto()
UNKNOWN_ARCHETYPE = auto()
CLASSIC_BULB = auto()
SULTAN_BULB = auto()
FLOOD_BULB = auto()
SPOT_BULB = auto()
CANDLE_BULB = auto()
LUSTER_BULB = auto()
PENDANT_ROUND = auto()
PENDANT_LONG = auto()
CEILING_ROUND = auto()
CEILING_SQUARE = auto()
FLOOR_SHADE = auto()
FLOOR_LANTERN = auto()
TABLE_SHADE = auto()
RECESSED_CEILING = auto()
RECESSED_FLOOR = auto()
SINGLE_SPOT = auto()
DOUBLE_SPOT = auto()
TABLE_WASH = auto()
WALL_LANTERN = auto()
WALL_SHADE = auto()
FLEXIBLE_LAMP = auto()
GROUND_SPOT = auto()
WALL_SPOT = auto()
PLUG = auto()
HUE_GO = auto()
HUE_LIGHTSTRIP = auto()
HUE_IRIS = auto()
HUE_BLOOM = auto()
BOLLARD = auto()
WALL_WASHER = auto()
HUE_PLAY = auto()
VINTAGE_BULB = auto()
CHRISTMAS_TREE = auto()
HUE_CENTRIS = auto()
HUE_LIGHTSTRIP_TV = auto()
HUE_TUBE = auto()
HUE_SIGNE = auto()
class Dimming(BaseModel):
class Config:
frozen = True
allow_mutation = False
validate_assignment = True
brightness: Optional[float]
min_dim_level: Optional[float] = Field(0, repr=False)
class XY(BaseModel):
class Config:
frozen = True
allow_mutation = False
validate_assignment = True
x: Optional[float]
y: Optional[float]
class On(BaseModel):
class Config:
frozen = True
allow_mutation = False
validate_assignment = True
on: bool = Field(..., alias="on")
class ColorPoint(BaseModel):
xy: Optional[XY]
class Identifier(BaseModel):
class Config:
frozen = True
allow_mutation = False
validate_assignment = True
rid: Optional[str]
rtype: Optional[str]
class Metadata(BaseModel):
class Config:
frozen = True
allow_mutation = False
validate_assignment = True
name: Optional[str]
archetype: Optional[Archetype | RoomType] = Archetype.UNKNOWN_ARCHETYPE
image: Optional[Identifier] = Field(None, repr=False)
class ColorTemperature(BaseModel):
mirek: Optional[int]
mirek_valid: Optional[bool]
mirek_schema: Optional[dict[str, float]]
class Gamut(BaseModel):
red: Optional[XY]
green: Optional[XY]
blue: Optional[XY]
class Color(BaseModel):
xy: Optional[XY]
gamut: Optional["Gamut"]
gamut_type: Optional[Literal["A", "B", "C"]]
class Dynamics(BaseModel):
status: Optional[str]
status_values: Optional[list[str]]
speed: Optional[float]
speed_valid: Optional[bool]
class Gradient(BaseModel):
points: Optional[list[ColorPoint]]
points_capable: Optional[int]
class Effects(BaseModel):
effect: Optional[list[str]]
status_values: Optional[list[str]]
status: Optional[str]
effect_values: Optional[list[str]]
class TimedEffects(BaseModel):
effect: Optional[str]
duration: Optional[int]
status_values: Optional[list[str]]
status: Optional[str]
effect_values: Optional[list[str]]
class Action(BaseModel):
on: Optional[On]
dimming: Optional[Dimming]
color: Optional[ColorPoint]
color_temperature: Optional[dict[str, float]]
gradient: Optional[dict[str, list[ColorPoint]]]
effects: Optional[dict[str, str]]
dynamics: Optional[dict[str, float]]
class Actions(BaseModel):
target: Optional[Identifier]
action: Optional[Action]
dimming: Optional[Dimming]
color: Optional[ColorPoint]
class PaletteColor(BaseModel):
color: Optional[ColorPoint]
dimming: Optional[Dimming]
class PaletteTemperature(BaseModel):
color_temperature: dict[str, float]
dimming: Optional[Dimming]
class Palette(BaseModel):
color: Optional[list[PaletteColor]]
dimming: Optional[list[Dimming]]
color_temperature: Optional[list[PaletteTemperature]]
class ProductData(BaseModel):
model_id: Optional[str]
manufacturer_name: Optional[str]
product_name: Optional[str]
product_archetype: Optional[Archetype]
certified: Optional[bool]
software_version: Optional[str]
hardware_platform_type: Optional[str]
class PowerState(BaseModel):
battery_state: Literal["normal", "low", "critical"]
battery_level: float = Field(le=100.0, ge=0.0)
class Temp(BaseModel):
temperature: float = Field(lt=100.0, gt=-100.0)
temperature_valid: Optional[bool]
class LightLevelValue(BaseModel):
light_level: Optional[int]
light_level_valid: Optional[bool]
class StreamProxy(BaseModel):
mode: Literal["auto", "manual"]
node: Optional[Identifier]
class XYZ(BaseModel):
x: float = Field(ge=-1.0, le=1.0)
y: float = Field(ge=-1.0, le=1.0)
z: float = Field(ge=-1.0, le=1.0)
class SegmentRef(BaseModel):
service: Optional[Identifier]
index: Optional[int]
class EntertainmentChannel(BaseModel):
channel_id: int = Field(ge=0, le=255)
position: Optional[XYZ]
members: Optional[list[SegmentRef]]
class ServiceLocation(BaseModel):
service: Optional[Identifier]
position: Optional[XYZ]
positions: list[XYZ] = Field(max_items=2, min_items=1)
class EntertainmentLocation(BaseModel):
service_location: Optional[list[ServiceLocation]]
class Segment(BaseModel):
start: int = Field(..., ge=0)
length: int = Field(..., ge=1)
class SegmentManager(BaseModel):
configurable: Optional[bool]
max_segments: int = Field(..., ge=1)
segments: Optional[list[Segment]]
class Dependee(BaseModel):
type: Optional[str]
target: Optional[Identifier]
level: Optional[str]
class BehaviorInstance(Entity):
type: Literal["behavior_instance"] = "behavior_instance"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
script_id: Optional[str]
enabled: Optional[bool]
state: Optional[dict[str, Any]]
configuration: dict[str, Any]
dependees: Optional[list[Dependee]]
status: Literal["initializing", "running", "disabled", "errored"]
last_error: Optional[str]
metadata: dict[Literal["name"], str]
migrated_from: Optional[str] = None
class BehaviorScript(Entity):
type: Literal["behavior_script"] = "behavior_script"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
description: Optional[str]
configuration_schema: dict[str, Any]
trigger_schema: dict[str, Any]
state_schema: dict[str, Any]
version: Optional[str]
metadata: dict[str, str]
class Bridge(Entity):
type: Literal["bridge"] = "bridge"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
bridge_id: Optional[str]
time_zone: dict[str, str]
class BridgeHome(Entity):
type: Literal["bridge_home"] = "bridge_home"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
services: Optional[list[Identifier]]
children: Optional[list[Identifier]]
class Button(Entity):
type: Literal["button"] = "button"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Optional[Identifier]
metadata: dict[Literal["control_id"], int]
button: Optional[
dict[
Literal["last_event"],
Literal[
"initial_press",
"repeat",
"short_release",
"long_release",
"double_short_release",
],
]
]
class Device(Entity):
type: Literal["device"] = "device"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
services: Optional[list[Identifier]]
metadata: Optional[Metadata]
product_data: Optional[ProductData]
class DevicePower(Entity):
type: Literal["device_power"] = "device_power"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Optional[Identifier]
power_state: Optional[PowerState]
class Entertainment(Entity):
type: Literal["entertainment"] = "entertainment"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Optional[Identifier]
renderer: Optional[bool]
proxy: Optional[bool]
max_streams: Optional[int] = Field(1, ge=1)
segments: Optional[SegmentManager] = None
class EntertainmentConfiguration(Entity):
type: Literal["entertainment_configuration"] = "entertainment_configuration"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
metadata: dict[Literal["name"], str]
name: Optional[str] = ""
configuration_type: Literal["screen", "monitor", "music", "3dspace", "other"]
status: Literal["active", "inactive"]
active_streamer: Optional[Identifier] = None
stream_proxy: Optional[StreamProxy]
channels: Optional[list[EntertainmentChannel]]
locations: Optional[EntertainmentLocation] = None
light_services: Optional[list[Identifier]]
class GeofenceClient(Entity):
type: Literal["geofence_client"] = "geofence_client"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
name: Optional[str]
class Geolocation(Entity):
type: Literal["geolocation"] = "geolocation"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
is_configured: Optional[bool] = False
class GroupedLight(Entity):
type: Literal["grouped_light"] = "grouped_light"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
on: On = Field(repr=False)
alert: dict[str, list[str]]
class Homekit(Entity):
id: UUID
type: str = "resource"
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
status: Optional[Literal["paired", "pairing", "unpaired"]] = "unpaired"
class Light(Entity):
id: UUID
id_v1: Optional[str] = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Optional[Identifier]
metadata: Optional[Metadata]
on: Optional[On]
dimming: Optional[Dimming]
dimming_delta: Optional[dict]
color_temperature: Optional[ColorTemperature]
color_temperature_delta: Optional[dict]
color: Optional[Color]
gradient: Optional[Gradient]
dynamics: Optional[Dynamics]
alert: Optional[dict[str, list[str]]]
signaling: Optional[dict]
mode: Optional[str]
effects: Optional[Effects]
type: Literal["light"] = "light"
class LightLevel(Entity):
type: Literal["light_level"] = "light_level"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Optional[Identifier]
enabled: Optional[bool]
light: Optional[LightLevelValue]
class Motion(Entity):
type: Literal["motion"] = "motion"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Optional[Identifier]
enabled: Optional[bool]
motion: dict[str, bool]
class Resource(Entity):
id: UUID
type: str = "device"
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
class Room(Entity):
type: Literal["room"] = "room"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
services: Optional[list[Identifier]]
metadata: Optional[Metadata]
children: Optional[list[Identifier]]
class Scene(Entity):
id: UUID
id_v1: Optional[str] = Field(..., regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
metadata: Optional[Metadata]
group: Optional[Identifier]
actions: Optional[list[Actions]]
palette: Optional[Palette]
speed: Optional[float]
auto_dynamic: Optional[bool]
type: Literal["scene"] = "scene"
class Temperature(Entity):
type: Literal["temperature"] = "temperature"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Optional[Identifier]
enabled: Optional[bool]
temperature: Optional[Temp]
class ZGPConnectivity(Entity):
type: Literal["zgp_connectivity"] = "zgp_connectivity"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Optional[Identifier]
status: Optional[
Literal[
"connected", "disconnected", "connectivity_issue", "unidirectional_incoming"
]
]
source_id: Optional[str]
class ZigbeeConnectivity(Entity):
type: Literal["zigbee_connectivity"] = "zigbee_connectivity"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
owner: Optional[Identifier]
status: Optional[
Literal[
"connected", "disconnected", "connectivity_issue", "unidirectional_incoming"
]
]
mac_address: Optional[str]
class Zone(Entity):
type: Literal["zone"] = "zone"
id: UUID
id_v1: Optional[str] = Field("", regex=r"^(\/[a-z]{4,32}\/[0-9a-zA-Z-]{1,32})?$")
services: Optional[list[Identifier]]
metadata: Optional[Metadata]
children: Optional[list[Identifier]]