This commit is contained in:
Victor Alexandrovich Tsyrenschikov
2026-03-30 20:25:42 +05:00
parent 139f9f1bd2
commit 373ed28445
2449 changed files with 53602 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
"""Yandex Smart Home API schemas."""
# ruff: noqa
from .callback import *
from .capability import *
from .capability_color import *
from .capability_mode import *
from .capability_onoff import *
from .capability_range import *
from .capability_toggle import *
from .capability_video import *
from .device import *
from .property import *
from .property_event import *
from .property_float import *
from .response import *

View File

@@ -0,0 +1,46 @@
"""Base class for API response schemas."""
from __future__ import annotations
from typing import Any, Generic, TypeVar
from pydantic import BaseModel, ConfigDict, model_serializer
T = TypeVar("T")
class APIModel(BaseModel):
"""Base API response model."""
model_config = ConfigDict(
extra="forbid",
populate_by_name=True,
arbitrary_types_allowed=True,
validate_assignment=True,
# strict=False, # можно включить, если нужно более строгое приведение типов
)
@model_serializer(mode="wrap")
def serialize(self, handler, info):
"""Serialize model excluding None values."""
dumped = handler(self)
if info.mode == "json":
# для json() — исключаем None и используем ensure_ascii=False
return {k: v for k, v in dumped.items() if v is not None}
return dumped
def as_dict(self) -> dict[str, Any]:
"""Generate a dictionary representation of the model (exclude None)."""
return self.model_dump(exclude_none=True, by_alias=False)
def as_json(self) -> str:
"""Generate a JSON representation of the model (exclude None, ensure_ascii=False)."""
return self.model_dump_json(exclude_none=True, ensure_ascii=False)
class GenericAPIModel(APIModel, Generic[T]):
"""Base generic API response model."""
# В pydantic v2 GenericModel больше не нужен — достаточно Generic + BaseModel
# Если нужно что-то специфическое — добавляй поля/валидаторы здесь
pass

View File

@@ -0,0 +1,59 @@
"""Schema for event notification service.
https://yandex.ru/dev/dialogs/smart-home/doc/reference-alerts/resources-alerts.html
"""
from __future__ import annotations
from enum import StrEnum
import time
from typing import Union
from pydantic import BaseModel, ConfigDict, Field
from .base import APIModel
from .device import DeviceState
class CallbackStatesRequestPayload(APIModel):
"""Payload of request body for notification about device state change."""
user_id: str
devices: list[DeviceState]
class CallbackStatesRequest(APIModel):
"""Request body for notification about device state change."""
ts: float = Field(default_factory=time.time)
payload: CallbackStatesRequestPayload
class CallbackDiscoveryRequestPayload(APIModel):
"""Payload of request body for notification about change of devices' parameters."""
user_id: str
class CallbackDiscoveryRequest(APIModel):
"""Request body for notification about change of devices' parameters."""
ts: float = Field(default_factory=time.time)
payload: CallbackDiscoveryRequestPayload
class CallbackResponseStatus(StrEnum):
"""Status of a callback request."""
OK = "ok"
ERROR = "error"
class CallbackResponse(APIModel):
"""Response on a callback request."""
status: CallbackResponseStatus
error_code: str | None = None
error_message: str | None = None
CallbackRequest: type = Union[CallbackDiscoveryRequest, CallbackStatesRequest]

View File

@@ -0,0 +1,139 @@
"""Schema for device capabilities."""
from __future__ import annotations
from enum import StrEnum
from typing import Annotated, Any, Literal, TypeVar, Union
from pydantic import BaseModel, ConfigDict, Field
from .base import APIModel
from .capability_color import (
ColorSettingCapabilityInstance,
ColorSettingCapabilityInstanceActionState,
ColorSettingCapabilityParameters,
RGBInstanceActionState,
SceneInstanceActionState,
TemperatureKInstanceActionState,
)
from .capability_mode import ModeCapabilityInstance, ModeCapabilityInstanceActionState, ModeCapabilityParameters
from .capability_onoff import OnOffCapabilityInstance, OnOffCapabilityInstanceActionState, OnOffCapabilityParameters
from .capability_range import RangeCapabilityInstance, RangeCapabilityInstanceActionState, RangeCapabilityParameters
from .capability_toggle import ToggleCapabilityInstance, ToggleCapabilityInstanceActionState, ToggleCapabilityParameters
from .capability_video import (
GetStreamInstanceActionResultValue,
GetStreamInstanceActionState,
VideoStreamCapabilityInstance,
VideoStreamCapabilityParameters,
)
class CapabilityType(StrEnum):
ON_OFF = "devices.capabilities.on_off"
COLOR_SETTING = "devices.capabilities.color_setting"
MODE = "devices.capabilities.mode"
RANGE = "devices.capabilities.range"
TOGGLE = "devices.capabilities.toggle"
VIDEO_STREAM = "devices.capabilities.video_stream"
@property
def short(self) -> str:
return str(self).replace("devices.capabilities.", "")
CapabilityParameters = Union[
OnOffCapabilityParameters,
ColorSettingCapabilityParameters,
ModeCapabilityParameters,
RangeCapabilityParameters,
ToggleCapabilityParameters,
VideoStreamCapabilityParameters,
]
CapabilityInstance = Union[
OnOffCapabilityInstance,
ColorSettingCapabilityInstance,
ModeCapabilityInstance,
RangeCapabilityInstance,
ToggleCapabilityInstance,
VideoStreamCapabilityInstance,
]
class CapabilityDescription(APIModel):
type: CapabilityType
retrievable: bool
reportable: bool
parameters: CapabilityParameters | None = None
class CapabilityInstanceStateValue(APIModel):
instance: CapabilityInstance
value: Any
class CapabilityInstanceState(APIModel):
type: CapabilityType
state: CapabilityInstanceStateValue
class OnOffCapabilityInstanceAction(APIModel):
type: Literal[CapabilityType.ON_OFF] = CapabilityType.ON_OFF
state: OnOffCapabilityInstanceActionState
class ColorSettingCapabilityInstanceAction(APIModel):
type: Literal[CapabilityType.COLOR_SETTING] = CapabilityType.COLOR_SETTING
state: ColorSettingCapabilityInstanceActionState
class ModeCapabilityInstanceAction(APIModel):
type: Literal[CapabilityType.MODE] = CapabilityType.MODE
state: ModeCapabilityInstanceActionState
class RangeCapabilityInstanceAction(APIModel):
type: Literal[CapabilityType.RANGE] = CapabilityType.RANGE
state: RangeCapabilityInstanceActionState
class ToggleCapabilityInstanceAction(APIModel):
type: Literal[CapabilityType.TOGGLE] = CapabilityType.TOGGLE
state: ToggleCapabilityInstanceActionState
class VideoStreamCapabilityInstanceAction(APIModel):
type: Literal[CapabilityType.VIDEO_STREAM] = CapabilityType.VIDEO_STREAM
state: GetStreamInstanceActionState
CapabilityInstanceAction = Annotated[
Union[
OnOffCapabilityInstanceAction,
ColorSettingCapabilityInstanceAction,
ModeCapabilityInstanceAction,
RangeCapabilityInstanceAction,
ToggleCapabilityInstanceAction,
VideoStreamCapabilityInstanceAction,
],
Field(discriminator="type"),
]
CapabilityInstanceActionState = TypeVar(
"CapabilityInstanceActionState",
OnOffCapabilityInstanceActionState,
ColorSettingCapabilityInstanceActionState,
RGBInstanceActionState,
TemperatureKInstanceActionState,
SceneInstanceActionState,
ModeCapabilityInstanceActionState,
RangeCapabilityInstanceActionState,
ToggleCapabilityInstanceActionState,
GetStreamInstanceActionState,
contravariant=True,
)
CapabilityInstanceActionResultValue = GetStreamInstanceActionResultValue | None

View File

@@ -0,0 +1,116 @@
"""Schema for color_setting capability.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/color_setting.html
"""
from __future__ import annotations
from enum import StrEnum
from typing import Annotated, Any, Literal, Optional, Self, Union
from pydantic import BaseModel, ConfigDict, Field, model_validator
from .base import APIModel
class ColorSettingCapabilityInstance(StrEnum):
"""Instance of a color_setting capability."""
BASE = "base"
RGB = "rgb"
HSV = "hsv"
TEMPERATURE_K = "temperature_k"
SCENE = "scene"
class ColorScene(StrEnum):
"""Color scene."""
ALARM = "alarm"
ALICE = "alice"
CANDLE = "candle"
DINNER = "dinner"
FANTASY = "fantasy"
GARLAND = "garland"
JUNGLE = "jungle"
MOVIE = "movie"
NEON = "neon"
NIGHT = "night"
OCEAN = "ocean"
PARTY = "party"
READING = "reading"
REST = "rest"
ROMANCE = "romance"
SIREN = "siren"
SUNRISE = "sunrise"
SUNSET = "sunset"
class CapabilityParameterColorModel(StrEnum):
"""Color model."""
RGB = "rgb"
HSV = "hsv"
class CapabilityParameterTemperatureK(APIModel):
"""Color temperature range."""
min: int
max: int
class CapabilityParameterColorScene(APIModel):
"""Parameter of a scene instance."""
scenes: list[dict[Literal["id"], ColorScene]]
@classmethod
def from_list(cls, scenes: list[ColorScene]) -> Self:
return cls(scenes=[{"id": s} for s in scenes])
class ColorSettingCapabilityParameters(APIModel):
"""Parameters of a color_setting capability."""
color_model: Optional[CapabilityParameterColorModel] = None
temperature_k: Optional[CapabilityParameterTemperatureK] = None
color_scene: Optional[CapabilityParameterColorScene] = None
@model_validator(mode='after')
def any_of(self) -> Self:
"""Проверяем, что хотя бы одно поле заполнено (после полной валидации)."""
if not any([
self.color_model is not None,
self.temperature_k is not None,
self.color_scene is not None,
]):
raise ValueError("one of color_model, temperature_k or color_scene must have a value")
return self
class RGBInstanceActionState(APIModel):
"""New value for a rgb instance."""
instance: Literal[ColorSettingCapabilityInstance.RGB] = ColorSettingCapabilityInstance.RGB
value: int
class TemperatureKInstanceActionState(APIModel):
"""New value for a temperature_k instance."""
instance: Literal[ColorSettingCapabilityInstance.TEMPERATURE_K] = ColorSettingCapabilityInstance.TEMPERATURE_K
value: int
class SceneInstanceActionState(APIModel):
"""New value for a scene instance."""
instance: Literal[ColorSettingCapabilityInstance.SCENE] = ColorSettingCapabilityInstance.SCENE
value: ColorScene
ColorSettingCapabilityInstanceActionState = Annotated[
Union[RGBInstanceActionState, TemperatureKInstanceActionState, SceneInstanceActionState],
Field(discriminator="instance"),
]
"""New value for an instance of color_setting capability."""

View File

@@ -0,0 +1,133 @@
"""Schema for mode capability.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/mode.html
"""
from enum import StrEnum
from typing import Literal, Self
from .base import APIModel
class ModeCapabilityInstance(StrEnum):
"""Instance of a mode capability.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/mode-instance.html
"""
CLEANUP_MODE = "cleanup_mode"
COFFEE_MODE = "coffee_mode"
DISHWASHING = "dishwashing"
FAN_SPEED = "fan_speed"
HEAT = "heat"
INPUT_SOURCE = "input_source"
PROGRAM = "program"
SWING = "swing"
TEA_MODE = "tea_mode"
THERMOSTAT = "thermostat"
VENTILATION_MODE = "ventilation_mode"
WORK_SPEED = "work_speed"
class ModeCapabilityMode(StrEnum):
"""Mode value of a mode capability.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/mode-instance-modes.html
"""
WET_CLEANING = "wet_cleaning"
DRY_CLEANING = "dry_cleaning"
MIXED_CLEANING = "mixed_cleaning"
AUTO = "auto"
ECO = "eco"
SMART = "smart"
TURBO = "turbo"
COOL = "cool"
DRY = "dry"
FAN_ONLY = "fan_only"
HEAT = "heat"
PREHEAT = "preheat"
HIGH = "high"
LOW = "low"
MEDIUM = "medium"
MAX = "max"
MIN = "min"
FAST = "fast"
SLOW = "slow"
EXPRESS = "express"
NORMAL = "normal"
QUIET = "quiet"
HORIZONTAL = "horizontal"
STATIONARY = "stationary"
VERTICAL = "vertical"
SUPPLY_AIR = "supply_air"
EXTRACTION_AIR = "extraction_air"
ONE = "one"
TWO = "two"
THREE = "three"
FOUR = "four"
FIVE = "five"
SIX = "six"
SEVEN = "seven"
EIGHT = "eight"
NINE = "nine"
TEN = "ten"
AMERICANO = "americano"
CAPPUCCINO = "cappuccino"
DOUBLE = "double"
ESPRESSO = "espresso"
DOUBLE_ESPRESSO = "double_espresso"
LATTE = "latte"
BLACK_TEA = "black_tea"
FLOWER_TEA = "flower_tea"
GREEN_TEA = "green_tea"
HERBAL_TEA = "herbal_tea"
OOLONG_TEA = "oolong_tea"
PUERH_TEA = "puerh_tea"
RED_TEA = "red_tea"
WHITE_TEA = "white_tea"
GLASS = "glass"
INTENSIVE = "intensive"
PRE_RINSE = "pre_rinse"
ASPIC = "aspic"
BABY_FOOD = "baby_food"
BAKING = "baking"
BREAD = "bread"
BOILING = "boiling"
CEREALS = "cereals"
CHEESECAKE = "cheesecake"
DEEP_FRYER = "deep_fryer"
DESSERT = "dessert"
FOWL = "fowl"
FRYING = "frying"
MACARONI = "macaroni"
MILK_PORRIDGE = "milk_porridge"
MULTICOOKER = "multicooker"
PASTA = "pasta"
PILAF = "pilaf"
PIZZA = "pizza"
SAUCE = "sauce"
SLOW_COOK = "slow_cook"
SOUP = "soup"
STEAM = "steam"
STEWING = "stewing"
VACUUM = "vacuum"
YOGURT = "yogurt"
class ModeCapabilityParameters(APIModel):
"""Parameters of a mode capability."""
instance: ModeCapabilityInstance
modes: list[dict[Literal["value"], ModeCapabilityMode]]
@classmethod
def from_list(cls, instance: ModeCapabilityInstance, modes: list[ModeCapabilityMode]) -> Self:
return cls(instance=instance, modes=[{"value": m} for m in modes])
class ModeCapabilityInstanceActionState(APIModel):
"""New value for a mode capability."""
instance: ModeCapabilityInstance
value: ModeCapabilityMode

View File

@@ -0,0 +1,32 @@
"""Schema for on_off capability.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/on_off.html
"""
from __future__ import annotations
from enum import StrEnum
from typing import Literal
from pydantic import BaseModel
from .base import APIModel
from .response import SuccessActionResult
class OnOffCapabilityInstance(StrEnum):
"""Instance of an on_off capability."""
ON = "on"
class OnOffCapabilityParameters(APIModel):
"""Parameters of a on_off capability."""
split: bool = False
class OnOffCapabilityInstanceActionState(APIModel):
"""New value for an on_off capability."""
instance: Literal[OnOffCapabilityInstance.ON] = OnOffCapabilityInstance.ON
value: bool
class OnOffCapabilityInstanceActionResultValue(BaseModel):
"""ActionResult value for on_off capability."""
on: bool

View File

@@ -0,0 +1,106 @@
"""Schema for range capability.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/range.html
"""
from __future__ import annotations
from enum import StrEnum
from typing import Any, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator, computed_field
from .base import APIModel
class RangeCapabilityUnit(StrEnum):
"""Unit used in a range capability."""
PERCENT = "unit.percent"
TEMPERATURE_CELSIUS = "unit.temperature.celsius"
class RangeCapabilityInstance(StrEnum):
"""Instance of a range capability.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/range-instance.html
"""
BRIGHTNESS = "brightness"
CHANNEL = "channel"
HUMIDITY = "humidity"
OPEN = "open"
TEMPERATURE = "temperature"
VOLUME = "volume"
class RangeCapabilityRange(APIModel):
"""Value range of a range capability."""
min: float
max: float
precision: float
def __str__(self) -> str:
return f"[{self.min}, {self.max}]"
class RangeCapabilityParameters(APIModel):
"""Parameters of a range capability."""
instance: RangeCapabilityInstance
unit: Optional[RangeCapabilityUnit] = Field(default=None, exclude=True) # исключаем из сериализации, если нужно
random_access: bool
range: Optional[RangeCapabilityRange] = Field(default=None)
@computed_field
@property
def computed_unit(self) -> RangeCapabilityUnit:
"""Вычисляемый unit на основе instance."""
match self.instance:
case RangeCapabilityInstance.BRIGHTNESS:
return RangeCapabilityUnit.PERCENT
case RangeCapabilityInstance.HUMIDITY:
return RangeCapabilityUnit.PERCENT
case RangeCapabilityInstance.OPEN:
return RangeCapabilityUnit.PERCENT
case RangeCapabilityInstance.TEMPERATURE:
return RangeCapabilityUnit.TEMPERATURE_CELSIUS
case _:
return self.unit or RangeCapabilityUnit.PERCENT # fallback
@model_validator(mode='after')
def validate_range(self) -> Self:
"""Force range boundaries for a capability instance."""
r = self.range
if r:
match self.instance:
case RangeCapabilityInstance.HUMIDITY | RangeCapabilityInstance.OPEN:
r.min = max(0.0, r.min)
r.max = min(100.0, r.max)
case RangeCapabilityInstance.BRIGHTNESS:
r.min = max(0.0, min(1.0, r.min))
r.max = 100.0
r.precision = 1.0
else:
if self.instance in (
RangeCapabilityInstance.BRIGHTNESS,
RangeCapabilityInstance.HUMIDITY,
RangeCapabilityInstance.OPEN,
RangeCapabilityInstance.TEMPERATURE,
):
raise ValueError(f"range field required for {self.instance}")
return self
class RangeCapabilityInstanceActionState(APIModel):
"""New value for a range capability."""
instance: RangeCapabilityInstance
value: float
relative: bool = False
@field_validator("relative", mode="before")
@classmethod
def set_relative(cls, v: Any) -> bool:
"""Update relative value."""
return False if v is None else v

View File

@@ -0,0 +1,36 @@
"""Schema for toggle capability.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/toggle.html
"""
from enum import StrEnum
from .base import APIModel
class ToggleCapabilityInstance(StrEnum):
"""Instance of a toggle capability.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/toggle-instance.html
"""
BACKLIGHT = "backlight"
CONTROLS_LOCKED = "controls_locked"
IONIZATION = "ionization"
KEEP_WARM = "keep_warm"
MUTE = "mute"
OSCILLATION = "oscillation"
PAUSE = "pause"
class ToggleCapabilityParameters(APIModel):
"""Parameters of a toggle capability."""
instance: ToggleCapabilityInstance
class ToggleCapabilityInstanceActionState(APIModel):
"""New value for a toggle capability."""
instance: ToggleCapabilityInstance
value: bool

View File

@@ -0,0 +1,43 @@
"""Schema for video_stream capability.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/video_stream.html
"""
from enum import StrEnum
from typing import Literal, List
from .base import APIModel
StreamProtocols = List[Literal["hls"]]
class VideoStreamCapabilityInstance(StrEnum):
"""Instance of a video_stream capability."""
GET_STREAM = "get_stream"
class VideoStreamCapabilityParameters(APIModel):
"""Parameters of a video_stream capability."""
protocols: StreamProtocols
class GetStreamInstanceActionStateValue(APIModel):
"""New state value for a get_stream instance."""
protocols: StreamProtocols
class GetStreamInstanceActionState(APIModel):
"""New value for a get_stream instance."""
instance: Literal[VideoStreamCapabilityInstance.GET_STREAM]
value: GetStreamInstanceActionStateValue
class GetStreamInstanceActionResultValue(APIModel):
"""New value after a get_stream instance state changed."""
stream_url: str
protocol: Literal["hls"]

View File

@@ -0,0 +1,199 @@
"""Schema for an user device.
https://yandex.ru/dev/dialogs/smart-home/doc/reference/get-devices.html
https://yandex.ru/dev/dialogs/smart-home/doc/reference/post-devices-query.html
https://yandex.ru/dev/dialogs/smart-home/doc/reference/post-action.html
"""
from __future__ import annotations
from enum import StrEnum
from typing import Any, List, Literal, Optional, Union
from pydantic import BaseModel, ConfigDict, Field
from .base import APIModel
from .capability import (
CapabilityDescription,
CapabilityInstance,
CapabilityInstanceAction,
CapabilityInstanceActionResultValue,
CapabilityInstanceState,
CapabilityType,
)
from .property import PropertyDescription, PropertyInstanceState
from .response import ResponseCode, ResponsePayload
class DeviceType(StrEnum):
"""User device type.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/device-types.html
"""
LIGHT = "devices.types.light"
LIGHT_STRIP = "devices.types.light.strip"
LIGHT_CEILING = "devices.types.light.ceiling"
LIGHT_LAMP = "devices.types.light.lamp"
LIGHT_GARLAND = "devices.types.light.garland"
SOCKET = "devices.types.socket"
SWITCH = "devices.types.switch"
SWITCH_RELAY = "devices.types.switch.relay"
THERMOSTAT = "devices.types.thermostat"
THERMOSTAT_AC = "devices.types.thermostat.ac"
MEDIA_DEVICE = "devices.types.media_device"
MEDIA_DEVICE_TV = "devices.types.media_device.tv"
MEDIA_DEVICE_TV_BOX = "devices.types.media_device.tv_box"
MEDIA_DEVICE_RECIEVER = "devices.types.media_device.receiver"
CAMERA = "devices.types.camera"
COOKING = "devices.types.cooking"
COFFEE_MAKER = "devices.types.cooking.coffee_maker"
KETTLE = "devices.types.cooking.kettle"
MULTICOOKER = "devices.types.cooking.multicooker"
OPENABLE = "devices.types.openable"
OPENABLE_CURTAIN = "devices.types.openable.curtain"
OPENABLE_VALVE = "devices.types.openable.valve"
HUMIDIFIER = "devices.types.humidifier"
PURIFIER = "devices.types.purifier"
VACUUM_CLEANER = "devices.types.vacuum_cleaner"
WASHING_MACHINE = "devices.types.washing_machine"
DISHWASHER = "devices.types.dishwasher"
IRON = "devices.types.iron"
SENSOR = "devices.types.sensor"
SENSOR_MOTION = "devices.types.sensor.motion"
SENSOR_VIBRATION = "devices.types.sensor.vibration"
SENSOR_ILLUMINATION = "devices.types.sensor.illumination"
SENSOR_OPEN = "devices.types.sensor.open"
SENSOR_CLIMATE = "devices.types.sensor.climate"
SENSOR_WATER_LEAK = "devices.types.sensor.water_leak"
SENSOR_BUTTON = "devices.types.sensor.button"
SENSOR_GAS = "devices.types.sensor.gas"
SENSOR_SMOKE = "devices.types.sensor.smoke"
SMART_METER = "devices.types.smart_meter"
SMART_METER_COLD_WATER = "devices.types.smart_meter.cold_water"
SMART_METER_ELECTRICITY = "devices.types.smart_meter.electricity"
SMART_METER_GAS = "devices.types.smart_meter.gas"
SMART_METER_HEAT = "devices.types.smart_meter.heat"
SMART_METER_HOT_WATER = "devices.types.smart_meter.hot_water"
PET_DRINKING_FOUNTAIN = "devices.types.pet_drinking_fountain"
PET_FEEDER = "devices.types.pet_feeder"
VENTILATION = "devices.types.ventilation"
VENTILATION_FAN = "devices.types.ventilation.fan"
OTHER = "devices.types.other"
class DeviceInfo(APIModel):
"""Extended device info."""
manufacturer: Optional[str] = Field(default=None)
model: Optional[str] = Field(default=None)
hw_version: Optional[str] = Field(default=None)
sw_version: Optional[str] = Field(default=None)
class DeviceDescription(APIModel):
"""Device description for a device list request."""
id: str
name: str
description: Optional[str] = None
room: Optional[str] = None
type: DeviceType
capabilities: Optional[List[CapabilityDescription]] = None
properties: Optional[List[PropertyDescription]] = None
device_info: Optional[DeviceInfo] = None
class DeviceState(APIModel):
"""Device state for a state query request."""
id: str
capabilities: Optional[List[CapabilityInstanceState]] = None
properties: Optional[List[PropertyInstanceState]] = None
error_code: Optional[ResponseCode] = None
error_message: Optional[str] = None
class DeviceList(ResponsePayload):
"""Response payload for a device list request."""
user_id: str
devices: List[DeviceDescription]
class DeviceStates(ResponsePayload):
"""Response payload for a state query request."""
devices: List[DeviceState]
class StatesRequestDevice(APIModel):
"""Device for a state query request."""
id: str
custom_data: Optional[dict[str, Any]] = None
class StatesRequest(APIModel):
"""Request body for a state query request."""
devices: List[StatesRequestDevice]
class ActionRequestDevice(APIModel):
"""Device for a state change request."""
id: str
capabilities: List[CapabilityInstanceAction]
class ActionRequestPayload(APIModel):
"""Request payload for state change request."""
action_id: Optional[str] = Field(default=None) # ← добавлено для совместимости с Яндексом
devices: List[ActionRequestDevice]
class ActionRequest(APIModel):
"""Request body for a state change request."""
payload: ActionRequestPayload
class SuccessActionResult(APIModel):
"""Success device action result."""
status: Literal["DONE"] = "DONE"
class FailedActionResult(APIModel):
"""Failed device action result."""
status: Literal["ERROR"] = "ERROR"
error_code: ResponseCode
class ActionResultCapabilityState(APIModel):
"""Result of capability instance state change."""
instance: CapabilityInstance
value: Optional[CapabilityInstanceActionResultValue] = None
action_result: Union[SuccessActionResult, FailedActionResult]
class ActionResultCapability(APIModel):
"""Result of capability state change."""
type: CapabilityType
state: ActionResultCapabilityState
class ActionResultDevice(APIModel):
"""Device for a state change response."""
id: str
capabilities: Optional[List[ActionResultCapability]] = None
action_result: Optional[Union[FailedActionResult, SuccessActionResult]] = None
class ActionResult(ResponsePayload):
"""Response for a device state change."""
devices: List[ActionResultDevice]

View File

@@ -0,0 +1,65 @@
"""Schema for device property.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/properties-types.html
"""
from enum import StrEnum
from typing import Any, Literal
from .base import APIModel
from .property_event import EventPropertyInstance, EventPropertyParameters
from .property_float import FloatPropertyInstance, FloatPropertyParameters
class PropertyType(StrEnum):
"""Property type."""
FLOAT = "devices.properties.float"
EVENT = "devices.properties.event"
@property
def short(self) -> str:
"""Return short version of the property type."""
return str(self).replace("devices.properties.", "")
class FloatPropertyDescription(APIModel):
"""Description of a float property for a device list request."""
type: Literal[PropertyType.FLOAT] = PropertyType.FLOAT
retrievable: bool
reportable: bool
parameters: FloatPropertyParameters
class EventPropertyDescription(APIModel):
"""Description of an event property for a device list request."""
type: Literal[PropertyType.EVENT] = PropertyType.EVENT
retrievable: bool
reportable: bool
parameters: EventPropertyParameters[Any]
PropertyDescription = FloatPropertyDescription | EventPropertyDescription
"""Description of a property for a device list request."""
PropertyParameters = FloatPropertyParameters | EventPropertyParameters[Any]
"""Parameters of a property for a device list request."""
PropertyInstance = FloatPropertyInstance | EventPropertyInstance
"""All property instances."""
class PropertyInstanceStateValue(APIModel):
"""Property instance value."""
instance: PropertyInstance
value: Any
class PropertyInstanceState(APIModel):
"""Property state for state query and callback requests."""
type: PropertyType
state: PropertyInstanceStateValue

View File

@@ -0,0 +1,195 @@
"""Schema for event property.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/event.html
"""
from __future__ import annotations
from enum import StrEnum
from typing import Any, Generic, Literal, TypeVar
from pydantic import BaseModel, ConfigDict, field_validator
from .base import APIModel
class EventPropertyInstance(StrEnum):
"""Instance of an event property.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/event-instance.html
"""
VIBRATION = "vibration"
OPEN = "open"
BUTTON = "button"
MOTION = "motion"
SMOKE = "smoke"
GAS = "gas"
BATTERY_LEVEL = "battery_level"
FOOD_LEVEL = "food_level"
WATER_LEVEL = "water_level"
WATER_LEAK = "water_leak"
class EventInstanceEvent(StrEnum):
"""Base class for an instance event."""
...
class VibrationInstanceEvent(EventInstanceEvent):
"""Event of a vibration instance."""
TILT = "tilt"
FALL = "fall"
VIBRATION = "vibration"
class OpenInstanceEvent(EventInstanceEvent):
"""Event of a open instance."""
OPENED = "opened"
CLOSED = "closed"
class ButtonInstanceEvent(EventInstanceEvent):
"""Event of a button instance."""
CLICK = "click"
DOUBLE_CLICK = "double_click"
LONG_PRESS = "long_press"
class MotionInstanceEvent(EventInstanceEvent):
"""Event of a motion instance."""
DETECTED = "detected"
NOT_DETECTED = "not_detected"
class SmokeInstanceEvent(EventInstanceEvent):
"""Event of a smoke instance."""
DETECTED = "detected"
NOT_DETECTED = "not_detected"
HIGH = "high"
class GasInstanceEvent(EventInstanceEvent):
"""Event of a gas instance."""
DETECTED = "detected"
NOT_DETECTED = "not_detected"
HIGH = "high"
class BatteryLevelInstanceEvent(EventInstanceEvent):
"""Event of a battery_level instance."""
LOW = "low"
NORMAL = "normal"
HIGH = "high"
class FoodLevelInstanceEvent(EventInstanceEvent):
"""Event of a food_level instance."""
EMPTY = "empty"
LOW = "low"
NORMAL = "normal"
class WaterLevelInstanceEvent(EventInstanceEvent):
"""Event of a water_level instance."""
EMPTY = "empty"
LOW = "low"
NORMAL = "normal"
class WaterLeakInstanceEvent(EventInstanceEvent):
"""Event of a water_leak instance."""
DRY = "dry"
LEAK = "leak"
EventInstanceEventT = TypeVar("EventInstanceEventT", bound=EventInstanceEvent)
def get_event_class_for_instance(instance: EventPropertyInstance) -> type[EventInstanceEvent]:
"""Return EventInstanceEvent enum for event property instance."""
return {
EventPropertyInstance.VIBRATION: VibrationInstanceEvent,
EventPropertyInstance.OPEN: OpenInstanceEvent,
EventPropertyInstance.BUTTON: ButtonInstanceEvent,
EventPropertyInstance.MOTION: MotionInstanceEvent,
EventPropertyInstance.SMOKE: SmokeInstanceEvent,
EventPropertyInstance.GAS: GasInstanceEvent,
EventPropertyInstance.BATTERY_LEVEL: BatteryLevelInstanceEvent,
EventPropertyInstance.FOOD_LEVEL: FoodLevelInstanceEvent,
EventPropertyInstance.WATER_LEVEL: WaterLevelInstanceEvent,
EventPropertyInstance.WATER_LEAK: WaterLeakInstanceEvent,
}[instance]
def get_supported_events_for_instance(instance: EventPropertyInstance) -> list[EventInstanceEvent]:
"""Return list of supported events for event property instance."""
return list(get_event_class_for_instance(instance).__members__.values())
class EventPropertyParameters(APIModel, Generic[EventInstanceEventT]):
"""Parameters of an event property."""
instance: EventPropertyInstance
events: list[dict[Literal["value"], EventInstanceEventT]] = []
@field_validator("events", mode="before")
@classmethod
def set_events(cls, v: Any) -> list[dict[Literal["value"], Any]]:
"""Update events list value."""
if not v:
# Получаем тип события из generic параметра
# В v2 это cls.model_fields["events"].annotation.__args__[1]
event_type = cls.model_fields["events"].annotation.__args__[1]
return [{"value": m} for m in event_type.__members__.values()]
return v
class VibrationEventPropertyParameters(EventPropertyParameters[VibrationInstanceEvent]):
instance: Literal[EventPropertyInstance.VIBRATION] = EventPropertyInstance.VIBRATION
class OpenEventPropertyParameters(EventPropertyParameters[OpenInstanceEvent]):
instance: Literal[EventPropertyInstance.OPEN] = EventPropertyInstance.OPEN
class ButtonEventPropertyParameters(EventPropertyParameters[ButtonInstanceEvent]):
instance: Literal[EventPropertyInstance.BUTTON] = EventPropertyInstance.BUTTON
class MotionEventPropertyParameters(EventPropertyParameters[MotionInstanceEvent]):
instance: Literal[EventPropertyInstance.MOTION] = EventPropertyInstance.MOTION
class SmokeEventPropertyParameters(EventPropertyParameters[SmokeInstanceEvent]):
instance: Literal[EventPropertyInstance.SMOKE] = EventPropertyInstance.SMOKE
class GasEventPropertyParameters(EventPropertyParameters[GasInstanceEvent]):
instance: Literal[EventPropertyInstance.GAS] = EventPropertyInstance.GAS
class BatteryLevelEventPropertyParameters(EventPropertyParameters[BatteryLevelInstanceEvent]):
instance: Literal[EventPropertyInstance.BATTERY_LEVEL] = EventPropertyInstance.BATTERY_LEVEL
class FoodLevelEventPropertyParameters(EventPropertyParameters[FoodLevelInstanceEvent]):
instance: Literal[EventPropertyInstance.FOOD_LEVEL] = EventPropertyInstance.FOOD_LEVEL
class WaterLevelEventPropertyParameters(EventPropertyParameters[WaterLevelInstanceEvent]):
instance: Literal[EventPropertyInstance.WATER_LEVEL] = EventPropertyInstance.WATER_LEVEL
class WaterLeakEventPropertyParameters(EventPropertyParameters[WaterLeakInstanceEvent]):
instance: Literal[EventPropertyInstance.WATER_LEAK] = EventPropertyInstance.WATER_LEAK

View File

@@ -0,0 +1,195 @@
"""Schema for float property.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/float.html
"""
from __future__ import annotations
from enum import StrEnum
from typing import Literal, Optional
from pydantic import BaseModel, Field
from .base import APIModel
class FloatPropertyInstance(StrEnum):
"""Instance of a float property.
https://yandex.ru/dev/dialogs/smart-home/doc/concepts/float-instance.html
"""
AMPERAGE = "amperage"
BATTERY_LEVEL = "battery_level"
CO2_LEVEL = "co2_level"
ELECTRICITY_METER = "electricity_meter"
FOOD_LEVEL = "food_level"
GAS_METER = "gas_meter"
HEAT_METER = "heat_meter"
HUMIDITY = "humidity"
ILLUMINATION = "illumination"
METER = "meter"
PM10_DENSITY = "pm10_density"
PM1_DENSITY = "pm1_density"
PM2_5_DENSITY = "pm2.5_density"
POWER = "power"
PRESSURE = "pressure"
TEMPERATURE = "temperature"
TVOC = "tvoc"
VOLTAGE = "voltage"
WATER_LEVEL = "water_level"
WATER_METER = "water_meter"
class FloatUnit(StrEnum):
"""Unit used in a float property."""
AMPERE = "unit.ampere"
CUBIC_METER = "unit.cubic_meter"
GIGACALORIE = "unit.gigacalorie"
KILOWATT_HOUR = "unit.kilowatt_hour"
LUX = "unit.illumination.lux"
MCG_M3 = "unit.density.mcg_m3"
PERCENT = "unit.percent"
PPM = "unit.ppm"
VOLT = "unit.volt"
WATT = "unit.watt"
class PressureUnit(StrEnum):
"""Pressure unit."""
PASCAL = "unit.pressure.pascal"
MMHG = "unit.pressure.mmhg"
ATM = "unit.pressure.atm"
BAR = "unit.pressure.bar"
class TemperatureUnit(StrEnum):
"""Temperature unit."""
CELSIUS = "unit.temperature.celsius"
KELVIN = "unit.temperature.kelvin"
class FloatPropertyParameters(APIModel):
"""Parameters of a float property."""
instance: FloatPropertyInstance
unit: FloatUnit | PressureUnit | TemperatureUnit | None = None
@property
def range(self) -> tuple[int | None, int | None]:
"""Return value range."""
return None, None
class FloatPropertyAboveZeroMixin:
"""Mixin for a property that has value only above zero."""
@property
def range(self) -> tuple[int | None, int | None]:
"""Return value range."""
return 0, None
class PercentFloatPropertyParameters(FloatPropertyParameters):
unit: Literal[FloatUnit.PERCENT] = FloatUnit.PERCENT
@property
def range(self) -> tuple[int | None, int | None]:
"""Return value range."""
return 0, 100
class DensityFloatPropertyParameters(FloatPropertyAboveZeroMixin, FloatPropertyParameters):
unit: Literal[FloatUnit.MCG_M3] = FloatUnit.MCG_M3
class AmperageFloatPropertyParameters(FloatPropertyAboveZeroMixin, FloatPropertyParameters):
instance: Literal[FloatPropertyInstance.AMPERAGE] = FloatPropertyInstance.AMPERAGE
unit: Literal[FloatUnit.AMPERE] = FloatUnit.AMPERE
class BatteryLevelFloatPropertyParameters(PercentFloatPropertyParameters):
instance: Literal[FloatPropertyInstance.BATTERY_LEVEL] = FloatPropertyInstance.BATTERY_LEVEL
class CO2LevelFloatPropertyParameters(FloatPropertyAboveZeroMixin, FloatPropertyParameters):
instance: Literal[FloatPropertyInstance.CO2_LEVEL] = FloatPropertyInstance.CO2_LEVEL
unit: Literal[FloatUnit.PPM] = FloatUnit.PPM
class ElectricityMeterFloatPropertyParameters(FloatPropertyAboveZeroMixin, FloatPropertyParameters):
instance: Literal[FloatPropertyInstance.ELECTRICITY_METER] = FloatPropertyInstance.ELECTRICITY_METER
unit: Literal[FloatUnit.KILOWATT_HOUR] = FloatUnit.KILOWATT_HOUR
class FoodLevelFloatPropertyParameters(PercentFloatPropertyParameters):
instance: Literal[FloatPropertyInstance.FOOD_LEVEL] = FloatPropertyInstance.FOOD_LEVEL
class GasMeterFloatPropertyParameters(FloatPropertyAboveZeroMixin, FloatPropertyParameters):
instance: Literal[FloatPropertyInstance.GAS_METER] = FloatPropertyInstance.GAS_METER
unit: Literal[FloatUnit.CUBIC_METER] = FloatUnit.CUBIC_METER
class HeatMeterFloatPropertyParameters(FloatPropertyAboveZeroMixin, FloatPropertyParameters):
instance: Literal[FloatPropertyInstance.HEAT_METER] = FloatPropertyInstance.HEAT_METER
unit: Literal[FloatUnit.GIGACALORIE] = FloatUnit.GIGACALORIE
class HumidityFloatPropertyParameters(PercentFloatPropertyParameters):
instance: Literal[FloatPropertyInstance.HUMIDITY] = FloatPropertyInstance.HUMIDITY
class IlluminationFloatPropertyParameters(FloatPropertyAboveZeroMixin, FloatPropertyParameters):
instance: Literal[FloatPropertyInstance.ILLUMINATION] = FloatPropertyInstance.ILLUMINATION
unit: Literal[FloatUnit.LUX] = FloatUnit.LUX
class MeterFloatPropertyParameters(FloatPropertyAboveZeroMixin, FloatPropertyParameters):
instance: Literal[FloatPropertyInstance.METER] = FloatPropertyInstance.METER
unit: None = Field(default=None)
class PM1DensityFloatPropertyParameters(DensityFloatPropertyParameters):
instance: Literal[FloatPropertyInstance.PM1_DENSITY] = FloatPropertyInstance.PM1_DENSITY
class PM25DensityFloatPropertyParameters(DensityFloatPropertyParameters):
instance: Literal[FloatPropertyInstance.PM2_5_DENSITY] = FloatPropertyInstance.PM2_5_DENSITY
class PM10DensityFloatPropertyParameters(DensityFloatPropertyParameters):
instance: Literal[FloatPropertyInstance.PM10_DENSITY] = FloatPropertyInstance.PM10_DENSITY
class PowerFloatPropertyParameters(FloatPropertyAboveZeroMixin, FloatPropertyParameters):
instance: Literal[FloatPropertyInstance.POWER] = FloatPropertyInstance.POWER
unit: Literal[FloatUnit.WATT] = FloatUnit.WATT
class PressureFloatPropertyParameters(FloatPropertyAboveZeroMixin, FloatPropertyParameters):
instance: Literal[FloatPropertyInstance.PRESSURE] = FloatPropertyInstance.PRESSURE
unit: PressureUnit
class TemperatureFloatPropertyParameters(FloatPropertyParameters):
instance: Literal[FloatPropertyInstance.TEMPERATURE] = FloatPropertyInstance.TEMPERATURE
unit: TemperatureUnit
class TVOCFloatPropertyParameters(DensityFloatPropertyParameters):
instance: Literal[FloatPropertyInstance.TVOC] = FloatPropertyInstance.TVOC
class VoltageFloatPropertyParameters(FloatPropertyAboveZeroMixin, FloatPropertyParameters):
instance: Literal[FloatPropertyInstance.VOLTAGE] = FloatPropertyInstance.VOLTAGE
unit: Literal[FloatUnit.VOLT] = FloatUnit.VOLT
class WaterLevelFloatPropertyParameters(PercentFloatPropertyParameters):
instance: Literal[FloatPropertyInstance.WATER_LEVEL] = FloatPropertyInstance.WATER_LEVEL
class WaterMeterFloatPropertyParameters(FloatPropertyAboveZeroMixin, FloatPropertyParameters):
instance: Literal[FloatPropertyInstance.WATER_METER] = FloatPropertyInstance.WATER_METER
unit: Literal[FloatUnit.CUBIC_METER] = FloatUnit.CUBIC_METER

View File

@@ -0,0 +1,59 @@
"""Schema for an API response for Yandex Smart Home."""
from __future__ import annotations
from enum import StrEnum
from typing import Optional, Any, Dict, Literal
from .base import APIModel
class ResponseCode(StrEnum):
"""Response code."""
DOOR_OPEN = "DOOR_OPEN"
LID_OPEN = "LID_OPEN"
REMOTE_CONTROL_DISABLED = "REMOTE_CONTROL_DISABLED"
NOT_ENOUGH_WATER = "NOT_ENOUGH_WATER"
LOW_CHARGE_LEVEL = "LOW_CHARGE_LEVEL"
CONTAINER_FULL = "CONTAINER_FULL"
CONTAINER_EMPTY = "CONTAINER_EMPTY"
DRIP_TRAY_FULL = "DRIP_TRAY_FULL"
DEVICE_STUCK = "DEVICE_STUCK"
DEVICE_OFF = "DEVICE_OFF"
FIRMWARE_OUT_OF_DATE = "FIRMWARE_OUT_OF_DATE"
NOT_ENOUGH_DETERGENT = "NOT_ENOUGH_DETERGENT"
HUMAN_INVOLVEMENT_NEEDED = "HUMAN_INVOLVEMENT_NEEDED"
DEVICE_UNREACHABLE = "DEVICE_UNREACHABLE"
DEVICE_BUSY = "DEVICE_BUSY"
INTERNAL_ERROR = "INTERNAL_ERROR"
INVALID_ACTION = "INVALID_ACTION"
INVALID_VALUE = "INVALID_VALUE"
NOT_SUPPORTED_IN_CURRENT_MODE = "NOT_SUPPORTED_IN_CURRENT_MODE"
ACCOUNT_LINKING_ERROR = "ACCOUNT_LINKING_ERROR"
DEVICE_NOT_FOUND = "DEVICE_NOT_FOUND"
class ResponsePayload(APIModel):
"""Base class for an API response payload."""
class Error(ResponsePayload):
"""Error payload."""
error_code: ResponseCode
error_message: Optional[str] = None
class SuccessActionResult(APIModel):
"""Represents a successful action result."""
status: Literal["DONE"] = "DONE"
class FailedActionResult(APIModel):
"""Represents a failed action result."""
status: Literal["ERROR"] = "ERROR"
error_code: ResponseCode
class Response(APIModel):
"""Base API response."""
request_id: Optional[str] = None
payload: Optional[Dict[str, Any]] = None