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. Источник значения —
по порядку приоритета:
- геттер — метод под декоратором, если вернул не
None; - хранилище — строка YDB, куда записал
actionилиhandle_device_report; 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.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rmind_yandex_smart_home-0.2.1.tar.gz | 61.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rmind_yandex_smart_home-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 105.9 kB
Release files / rmind_yandex_smart_home-0.2.1.tar.gz
| Download URL | rmind_yandex_smart_home-0.2.1.tar.gz |
|---|---|
| Size | 61.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bcd8d68514d667214ef9d5a02352c0fd79aed5e6ebbb8ad7af0c6d78e2388756
|
|
BLAKE2b-256 checksum How to use checksums |
6ae5815cf33aafd47050c39a372a21941897cd335bf2ebabe15bbc7fb532fe41
|
| 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.1-py3-none-any.whl
| Download URL | rmind_yandex_smart_home-0.2.1-py3-none-any.whl |
|---|---|
| Size | 44.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cb5767e955442e56e1ab1708a2f7943fec52ee62f0c128edc975e5d244e2c1fc
|
|
BLAKE2b-256 checksum How to use checksums |
287caf5bfc610c5cc9b1538a618c506018a35cece683191d189494e404e87c13
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.15
|