Skip to main content

rmind.yandex.smart.home

Библиотека для реализации умного дома с Алисой в Yandex Cloud Function. Покрыта вся часть 1 и 2 spec.md: шесть задокументированных умений со всеми инстансами, свойства float/event и реестр типов устройств.

v0.2.0 — релиз с поломкой совместимости. Ниже раздел «Миграция с 0.1.9».

pip install rmind-yandex-smart-home

Необходимая облачная инфраструктура

Managed Service for YDB

хранение состояния устройств

Переменные окружения Значение По умолчанию Обязательный
YDB_ENDPOINT grpcs grpcs://ydb.serverless.yandexcloud.net:2135 Нет
YDB_DATABASE адресс БД - Да

IoT Core/Брокер

общение с устройствами пользователя через MQTT протокол

Переменные окружения Значение По умолчанию Обязательный
MQTT_URL адрес брокера mqtt.cloud.yandex.net Нет
MQTT_PORT порт брокера 8883 Нет
MQTT_USER пользователь - Да
MQTT_PASS пароль - Да

YDB подключается лениво — при первом обращении к состоянию, а не на import. Пока навык не трогает хранилище, без YDB_DATABASE библиотека импортируется.

Быстрый старт

from rmind_yandex_smart_home import (Engine, FloatProperty, OnOff, Range,
                                     YandexIoTDevice)

class Strip(YandexIoTDevice):
  def __init__(self, id):
    super().__init__(id, "Ледовая лента", "devices.types.light.strip")

  @OnOff()
  def on_off(self, params):
    return 'on:1' if params['value'] else 'on:0'

  @Range('brightness')
  def brightness(self, params):
    return f"bright:{params['value']}"

  @FloatProperty('power')
  def power(self):
    return 12.5          # собственное значение свойства

engine = Engine()
engine.register_device(Strip("my-device-unique-id"))

def handler(event, context):
  return engine.handle(event, context)

Метод умения возвращает текст сообщения устройству, которое Engine отправит в MQTT-топик /yandex-iot-core/{device_id}/commands. Возврат None — команда не нужна, действие всё равно считается выполненным (например, состояние изменено только на стороне функции). Словарь или список сериализуются в JSON.

Метод свойства возвращает текущее значение либо None.

Capabilities (умения)

Все умения — классы-декораторы. Общие параметры каждого: retrievable (по умолчанию True), reportable (по умолчанию False), default (значение по умолчанию для query). retrievable=False исключает умение из ответа query — провайдер не может знать состояние.

OnOff — devices.capabilities.on_off

@OnOff(split=False)                 # split=True: разные команды на вкл и выкл
def on_off(self, params):            # params: {"instance": "on", "value": True}
  return 'on:1' if params['value'] else 'on:0'

ColorSetting — devices.capabilities.color_setting

Нужен хотя бы один из color_model, temperature_k, scenes. Инстанс (hsv / rgb / temperature_k / scene) приходит в запросе и не объявляется отдельно — одно умение покрывает все объявленные представления цвета.

@ColorSetting(color_model='rgb', temperature_k=[2700, 6500], scenes=['party', 'garland'])
def color(self, params):             # {"instance": "rgb", "value": 14210514}
  return get_rgb_message(params['value'])
Аргумент Значение
color_model 'rgb' (по умолчанию) или 'hsv'; None — цвет не объявлен
temperature_k пара [min, max] в кельвинах
scenes список id из catalog.COLOR_SCENES (16 сцен)

rgb — целое [0; 16777215], hsv — {h: 0..360, s: 0..100, v: 0..100}. Состояние цвета хранится в одной строке: rgb, температура и сцена — это одно и то же «какой сейчас свет», а не независимые параметры.

def get_rgb_message(value):
  h = hex(value)[2:]
  if len(h) == 5:
    h = '0' + h
  r, g, b = tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
  return f"{r};{g};{b}"

Mode — devices.capabilities.mode

@Mode('thermostat')                                  # значения из каталога
def thermostat(self, params):                        # {"instance": "thermostat", "value": "cool"}
  return f"mode:{params['value']}"

@Mode('program', modes=['express', 'auto', 'one'])   # свой порядок и подмножество
def program(self, params):
  ...

instance обязателен. Доступные инстансы (полные списки значений — catalog.MODELS, рекомендации по типам — catalog.PROGRAM_MODES): cleanup_mode, coffee_mode, dishwashing, fan_speed, heat, input_source, program, swing, tea_mode, thermostat, ventilation_mode, work_speed.

Значение default при пропуске — первый объявленный режим.

Range — devices.capabilities.range

@Range('brightness')                                          # 0..100, unit.percent
@Range('temperature', minimum=40, maximum=100, precision=5)    # unit.temperature.celsius
@Range('open', precision=10)                                   # шторы: 0..100, шаг 10
@Range('volume', unit=None)                                    # единицы не объявлены
Инстанс unit по умолчанию Диапазон по умолчанию
brightness unit.percent 0–100, шаг 1
channel — 0–999, шаг 1
humidity unit.percent 0–100, шаг 1
open unit.percent 0–100, шаг 1
temperature unit.temperature.celsius 18–40, шаг 1
volume unit.percent 0–100, шаг 1

minimum, maximum, precision, unit, random_access переопределяются в декораторе. Относительные команды (relative: true) Engine разворачивает в абсолютное значение по сохранённому состоянию и приводит к шагу и границам — обработчик всегда получает готовое число.

Toggle — devices.capabilities.toggle

@Toggle('ionization')
def ionization(self, params):        # {"instance": "ionization", "value": True}
  return 'ion:1' if params['value'] else 'ion:0'

Инстансы: backlight, controls_locked, ionization, keep_warm, mute, oscillation, pause.

Properties (свойства)

Свойства только читаются: Алиса запрашивает их в query. Источник значения — по порядку приоритета:

  1. геттер — метод под декоратором, если вернул не None;
  2. хранилище — строка YDB, куда записал action или handle_device_report;
  3. default= — если объявлен явно.

Если ничего не дало значение, свойство просто отсутствует в ответе. Для событийных свойств это принципиально: выдуманное motion: detected заставило бы Алису докладывать о движении, которого не было.

class Humidifier(YandexIoTDevice):
  @FloatProperty('humidity')          # unit.percent подставится из каталога
  def humidity(self):
    return None                       # своё значение неизвестно — бери из хранилища

  @FloatProperty('temperature', unit='unit.temperature.kelvin')
  def temperature(self):
    return read_sensor()              # собственный источник значения

  @EventProperty('water_level')
  def water(self):
    return None

Событийное свойство забирается из хранилища один раз: после query строка удаляется, чтобы следующее показание не было повтором того же события.

devices.properties.float

Инстанс Единица по умолчанию
amperage unit.ampere
battery_level unit.percent
co2_level unit.ppm
electricity_meter unit.kilowatt_hour
food_level unit.percent
gas_meter unit.cubic_meter
heat_meter unit.gigacalorie
humidity unit.percent
illumination unit.illumination.lux
meter — (единицы не требуются)
pm1_density, pm2.5_density, pm10_density, tvoc unit.density.mcg_m3
power unit.watt
pressure unit.pressure.mmhg (допустимы также atm, pascal, bar)
temperature unit.temperature.celsius (допустим kelvin)
voltage unit.volt
water_level unit.percent
water_meter unit.cubic_meter

devices.properties.event

Инстанс Значения
battery_level low, normal
button click, double_click, long_press
food_level empty, low, normal
gas detected, not_detected, high
motion detected, not_detected
open opened, closed
smoke detected, not_detected, high
vibration tilt, fall, vibration
water_leak dry, leak
water_level empty, low, normal

В spec.md значения smoke и gas не детализированы — список выше взят из документации Yandex. subset объявляется аргументом events=['click'].

Типы устройств

device_types — таблица «устройство → рекомендованные умения/свойства» из части 3 spec.md. При register_device библиотека сверяет декларации с рекомендациями и печатает предупреждения; отказывать в регистрации она не правомочна — платформа эти списки не навязывает.

from rmind_yandex_smart_home import device_types

device_types.get('devices.types.thermostat.ac').allows_capability('mode', 'swing')  # True
device_types.get('devices.types.light').allows_property('float', 'temperature')      # False

Неизвестный тип устройства (devices.types.unknown) тоже регистрируется, но с предупреждением: для реальных устройств используйте конкретный тип, иначе навык могут отклонить на модерации.

Ошибки: DeviceError и error_code

Ответ action при ERROR обязан нести код из фиксированного списка платформы.

from rmind_yandex_smart_home import DeviceError

@OnOff()
def on_off(self, params):
  if params['value'] and self.water_level < 10:
    raise DeviceError('NOT_ENOUGH_WATER', 'Долейте воду')
  return 'kettle:on'
Что произошло Что вернёт Engine
raise DeviceError(code, message) ERROR, ваш code и message
любое другое исключение в обработчике ERROR, INTERNAL_ERROR
команда не доставлена в MQTT ERROR, DEVICE_UNREACHABLE
умение/инстанс не объявлен на устройстве ERROR, INVALID_ACTION
значение вне диапазона или не того типа ERROR, INVALID_VALUE
устройство не зарегистрировано ERROR, DEVICE_NOT_FOUND

Коды: DOOR_OPEN, LID_OPEN, REMOTE_CONTROL_DISABLED, NOT_ENOUGH_WATER, LOW_CHARGE_LEVEL, CONTAINER_FULL, CONTAINER_EMPTY, DRIP_TRAY_FULL, DEVICE_STUCK, DEVICE_OFF, FIRMWARE_OUT_OF_DATE, NOT_ENOUGH_DETERGENT, HUMAN_INVOLVEMENT_NEEDED, DEVICE_UNREACHABLE, DEVICE_BUSY, INTERNAL_ERROR, INVALID_ACTION, INVALID_VALUE, NOT_SUPPORTED_IN_CURRENT_MODE, ACCOUNT_LINKING_ERROR, DEVICE_NOT_FOUND (catalog.ERROR_CODES).

Приём показаний с устройства: handle_device_report

Значения свойств приходят с самого устройства, а не от Алисы. Устройство публикует JSON в топик device.get_state_topic() (/yandex-iot-core/{id}/states), триггер Cloud Function вызывает:

def states_handler(event, context):
  return engine.handle_device_report(event)
{
  "device_id": "sensor-01",
  "user_id": 12345,
  "properties": [
    {"type": "devices.properties.float", "instance": "temperature", "value": 23.5},
    {"type": "devices.properties.event", "instance": "motion", "value": "detected"}
  ],
  "capabilities": [
    {"type": "devices.capabilities.range", "instance": "brightness", "value": 40}
  ]
}

Принимаются только объявленные на устройстве умения и свойства с корректными значениями: устройство не может записать то, чего у него нет. Ответ содержит reported и rejected с причинами.

user_id в отчёте обязателен на практике: состояние ищется по паре (user_id, key). Отчёт устройства не содержит токен Алисы, поэтому id берётся из user_id отчёта либо из Engine(state_user_id=...). Без него значение пишется в бакет 0 и query его не увидит — библиотека печатает предупреждение.

Пользовательское умение

Своё умение — наследник Capability; механизм тот же, что у встроенных.

from rmind_yandex_smart_home import Capability

class WorkSpeed(Capability):
  type = 'devices.capabilities.mode'
  keyed_by_instance = True          # состояние различается по инстансу

  def __init__(self, **kwargs):
    super().__init__('work_speed', **kwargs)
    self.parameters['instance'] = 'work_speed'
    self.parameters['modes'] = [{'value': x} for x in ('min', 'turbo')]

  def default_state(self, instance=None):
    return {'instance': 'work_speed', 'value': 'min'}

  def validate(self, state):
    ok, code, message = super().validate(state)
    if not ok:
      return ok, code, message
    if state.get('value') not in ('min', 'turbo'):
      return False, 'INVALID_VALUE', 'speed: min или turbo'
    return True, None, None
@WorkSpeed()
def work_speed(self, params):
  return f'speed:{params["value"]}'

Хранение состояния

Таблица YDB device_capability_state с прошлых версий не меняется — user_id, capability_key, device_id, capability_state. Изменился только формат ключа, потому что прежний (cp_{device}_{тип}) давал двум range-умениям одного устройства писать в одну строку:

Что Ключ
умение с инстансами (range, mode, toggle) cp_{device_id}_{тип}_{instance}
умение с одним состоянием (on_off, color_setting) cp_{device_id}_{тип}
свойство pr_{device_id}_{float|event}_{instance}

Старые записи под прежними ключами становятся нечитаемыми и будут перезаписаны при первых командах. Если терять текущее состояние нельзя — перепишите ключи отдельным скриптом перед выкаткой.

user_id для ключей — реальный id пользователя из токена запроса (login.yandex.ru/info). В 0.1.9 discovery отдавал захардкоженный id, из-за чего все пользователи делили одно состояние.

Миграция с 0.1.9

0.1.9 0.2.0
@on_off() @OnOff()
@color_setting(temperature_k=[2700, 5100], scenes=[...]) @ColorSetting(...) — аргументы те же
@brightness() @Range('brightness')
@mode(modes=[...]) (всегда program) @Mode('program', modes=[...]) — instance обязателен
— @Toggle(instance), @FloatProperty(instance), @EventProperty(instance)
Engine() Engine(state_user_id=<id владельца>) — если есть отчёты устройств
обработчик вернул None → ERROR обработчик вернул None → DONE без команды
action_result без error_code всегда валидный error_code
device.result(params, success) удалён: состояние ответа собирает Engine
состояние в YDB поднималось на import лениво, при первом обращении

rgb остаётся моделью цвета по умолчанию, так что существующие световые устройства после переименования декораторов работают как раньше.

Что не реализовано

  • devices.capabilities.video_stream — требует stream_url в ответе action и заголовков отдачи HLS.
  • reportable-умения: параметр принимается и уходит в discovery, но push в https://api.dialogs.yandex.ru/api/devices/state не отправляется — для него нужен OAuth-токен навыка, которого в библиотеке нет. Состояние читается по query.
  • custom — в spec.md помечен как недокументированный (404), не реализован.

Разработка

python -m unittest discover -s tests -t .

Тесты не требуют YDB, брокера и доступа к Яндексу: внешние модули подменяются, хранилище и MQTT-клиент инжектятся в Engine. Публика пакета — workflow .gitverse/workflows/artifactory.yaml по тегу v*.

Metadata

Release files for rmind-yandex-smart-home 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rmind-yandex-smart-home 0.2.0
File Size Uploaded
rmind_yandex_smart_home-0.2.0.tar.gz 63.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for rmind-yandex-smart-home 0.2.0
File Interpreter ABI Platform
rmind_yandex_smart_home-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 109.2 kB

Release files / rmind_yandex_smart_home-0.2.0.tar.gz

Download URL rmind_yandex_smart_home-0.2.0.tar.gz
Size 63.0 kB
Tags Source
SHA-256 checksum
How to use checksums
498c728d8129f88e3d6ec70f2678b896226487fdfff80d4a0b3e16763621a928
BLAKE2b-256 checksum
How to use checksums
dd4e5adb9585382b0a49aa9aafd852bff629c51ef11ca6b41e94ff7446068e64
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / rmind_yandex_smart_home-0.2.0-py3-none-any.whl

Download URL rmind_yandex_smart_home-0.2.0-py3-none-any.whl
Size 46.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4530034f90f2e38ef48bafb0c8f76cc842aec7f66e220a6431338fc2a3737d91
BLAKE2b-256 checksum
How to use checksums
fd0c6783eba16fd91091be209166324575f79f939f1d98be263772a6b263d9a5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release history Release notifications | RSS feed

0.2.1

2 release files

This release

0.2.0 This release

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page