Любой домостроитель знает: лучшее облако — локальное “облако”. А потому будем отучать увлажнитель воздуха Tuvio TAP01DE от чужих серверов.
Intro
Какое-то время назад я прикупил себе увлажнитель/очиститель воздуха Tuvio TAP01DE — вот такой:
Это ни в коем случае не рекомендация, просто факт. Даже не знаю, почему взял именно его — возможно, захотелось побыть рискованным парнем :)
Для моей небольшой студии его мощности вполне хватает, спать не мешает, но бесят две вещи:
- очень часто приходится заправлять бак (буквально раз в полтора дня);
 - управляется через облако.
 
И если с первым я ничего поделать не могу (кроме как собрать свой, офк), то от облака его вполне можно отучить. Ведь Tuvio — это обычный white-label на платформе Tuya. А значит, Tuya Local или Local Tuya должен зарешать.
Зачем? Ну… как обычно:
- любое вендорское облако — злющее зло;
 - зачем мне иметь по приложению на каждое устройство, если есть Home Assistant;
 - само приложение Tuvio — это какой-то стыд с лужей мочи в центре (не открывайте).
 
Dive into
Чтобы локально подключиться к Tuya-устройству, сначала нужно раздобыть его device id и local key. Есть разные способы их заполучить, но самый простой — через портал разработчика Tuya. Поэтому, прежде чем открывать Home Assistant:
- ставим мобильное приложение SmartLife и регистрируемся (я выбрал регион RU);
 - добавляем наш очиститель, зажав иконку регулировки скорости вентилятора;
 - регистрируемся в Tuya Developer Platform;
 - создаём новое облако в ДЦ “Central Europe Data Center” (к нему привязан RU регион);
 - идём в 
Cloud→Project Management→Open Projectи запоминаем креды:
 - не уходя со страницы проекта, идём в 
Devices→Link App Accountи жмёмAdd App Account→Tuya App Account Authorization:
 - сканируем QR-код в приложении 
SmartLife, авторизуемся, и в портал подтягиваются наши устройства. Запоминаем егоdevice id:
 
Дальше есть два пути:
- надеяться на 
Tuya Local/Local Tuya, отдав их мастеру свой аккаунт разработчика; - воспользоваться tinytuya.
 
Мне ближе второй вариант — тем более, он пригодится и для отладки:
# Ставим tinytuya
% pip install tinytuya
# Запускаем визард и вводим ранее прикопанные Access ID, Access Secret и Device ID
% tinytuya wizard
TinyTuya Setup Wizard [1.17.4]
    Enter API Key from tuya.com: c44ufc3q9um7k7wae44d
    Enter API Secret from tuya.com: a3c4d402056e490ca96db802382cb2d2
    Enter any Device ID currently registered in Tuya App (used to pull full list) or 'scan' to scan for one: bf9f4125ac798e6a383jaj
      Region List
        cn	China Data Center (alias: AY)
        us	US - Western America Data Center (alias: AZ)
        us-e	US - Eastern America Data Center (alias: UE)
        eu	Central Europe Data Center
        eu-w	Western Europe Data Center (alias: WE)
        in	India Data Center
        sg	Singapore Data Center
    Enter Your Region (Options: cn, us, us-e, eu, eu-w, in, or sg): eu
>> Configuration Data Saved to tinytuya.json
{
    "apiKey": "c44ufc3q9um7k7wae44d",
    "apiSecret": "a3c4d402056e490ca96db802382cb2d2",
    "apiRegion": "eu",
    "apiDeviceID": "bf9f4125ac798e6a383jaj"
}
Download DP Name mappings? (Y/n): Y
Device Listing
[
    {
        "name": "Tuvio TAP01DE",
        "id": "bf9XXXXXXXX",
        "key": "XXXXXXXX",
        "mac": "80:64:7c:0c:77:aa",
        "uuid": "XXXXXXXX",
        "sn": "XXXXXXXX",
        "category": "jsq",
        "product_name": "Tuvio TAP01DE",
        "product_id": "cdtata05klpwbyu9",
        "biz_type": 18,
        "model": "TAP01DE",
        "sub": false,
        "icon": "https://images.tuyaeu.com/smart/icon/bay1695300390722FkGj/2c7da8627d8684e73531929f4cdd0ed3.png",
        "mapping": {}
    }
]
>> Saving list to devices.json
    1 registered devices saved
>> Saving raw TuyaPlatform response to tuya-raw.json
Poll local devices? (Y/n): Y
Scanning local network for Tuya devices...
    1 local devices discovered                         
Polling local devices...
    [Tuvio TAP01DE            ] 10.11.0.146        - [On]  - DPS: {'1': True, '5': True, '13': 50, '14': 42, '16': False, '19': 'cancel', '20': 0, '22': 0, '33': 1693, '101': '1', '102': True, '103': True, '104': False, '105': False, '106': 60, '108': False, '109': False}
>> Saving device snapshot data to snapshot.json
>> Saving IP addresses to devices.json
    1 device IP addresses found
Done.
Нас больше всего интересует две вещи:
- локальный ключ увлажнителя (не забудем прикопать);
 - отсутствие официального маппинга DP-кодов (data points).
 
Разумеется, без маппинга железкой никак не поуправлять, ибо хз как с ней говорить. А потому придётся или искать в интернетах, или собирать самому. Собрать самому несложно — открываем питоний, подключаемся к устройству и запрашиваем текущее состояние:
In [1]: import tinytuya
In [2]: d = tinytuya.Device('<device_id>', '<device_ip>', '<local_key>', version=3.4)
In [3]: d.status()
Out[3]: 
{'dps': {'1': True,
  '5': True,
  '13': 50,
  '14': 44,
  '16': False,
  '19': 'cancel',
  '20': 0,
  '22': 0,
  '33': 1693,
  '101': '1',
  '102': True,
  '103': True,
  '104': False,
  '105': False,
  '106': 60,
  '108': False,
  '109': False}}
Дальше идём в SmartLife, крутим доступные крутилки, и составляем список кодов (что такое 22 — я так и не понял, вероятно, какая-то диагностика):
1: bool - power switch
5: bool - light
13: from 40 to 70 - target humidity
14: from 0 to 100 - current humidity
16: bool - sleep
19: cancel/1H/2H/3H/4H/5H/6H/7H/8H/9H - countdown (cancel mean off)
20: 1/2/3/4/5/6/7/8/9 - left time in countdown mode
33: integer - filter life (h)
101: A/1/2/3/4 - fan level (A mean auto)
102: bool - vertical switch (swing??)
103: bool - uv
104: bool - wet
105: bool - drying
106: from 1 to 100 - night light bright
108: bool - humidity switch
109: bool - beeper switch
Итак, как-будто всё готово. Можно добавлять устройство в Home Assistant!
Tuya Local
Но сперва решить, что лучше Tuya Local или Local Tuya? Если честно — я не знаю :)
Я выбрал Tuya Local, потому что:
- у него есть отдельный хелпер для увлажнителей;
 - устройства описываются ямличками, с которыми ChatGPT почти справляется :)
 
В интернетах пишут разное, у кого первый стабильнее работает, у кого второй. Для меня пока единственный минус — нельзя держать описания устройств out-of-tree, поэтому репу пришлось форкнуть. А дальше всё просто:
- скармливаем ChatGPT (было лениво иконки выбирать) всю нужную инфу:
- маппинг DP-кодов;
 - информацию об устройстве;
 - примеры описаний других увлажнителей;
 
 - слегка правим шероховатости (особенно 
humidifier), поглядывая в humidifier.py; - и вуаля — вот описание нашего увлажнителя:
 
name: Tuvio TAP01DE Air Washer
products:
  - id: cdtata05klpwbyu9
    manufacturer: Tuvio
    model: TAP01DE
entities:
  - entity: humidifier
    class: humidifier
    icon: mdi:air-humidifier
    dps:
      - id: 1
        name: switch
        type: boolean
      - id: 13
        name: humidity
        type: integer
        range:
          min: 40
          max: 70
        unit: "%"
        mapping:
          - constraint: mode
            conditions:
              - dps_val: true
                invalid: false
              - dps_val: false
                invalid: true
      - id: 14
        name: current_humidity
        type: integer
        unit: "%"
      - id: 104
        type: boolean
        name: action
        mapping:
          - dps_val: true
            value: "work"
          - dps_val: false
            value: "idle"
      - id: 108
        type: boolean
        name: mode
        mapping:
          - dps_val: false
            value: "auto"
          - dps_val: true
            value: "normal"
  - entity: switch
    name: Sleep
    icon: mdi:sleep
    dps:
      - id: 16
        name: switch
        type: boolean
  - entity: select
    name: Timer
    icon: mdi:timer-outline
    dps:
      - id: 19
        name: option
        type: string
        mapping:
          - dps_val: "cancel"
            value: "Off"
          - dps_val: "1H"
            value: "1 Hour"
          - dps_val: "2H"
            value: "2 Hours"
          - dps_val: "3H"
            value: "3 Hours"
          - dps_val: "4H"
            value: "4 Hours"
          - dps_val: "5H"
            value: "5 Hours"
          - dps_val: "6H"
            value: "6 Hours"
          - dps_val: "7H"
            value: "7 Hours"
          - dps_val: "8H"
            value: "8 Hours"
          - dps_val: "9H"
            value: "9 Hours"
  - entity: sensor
    name: Time remaining
    icon: mdi:timer-sand
    class: duration
    dps:
      - id: 20
        name: sensor
        type: integer
        unit: "h"
  - entity: sensor
    name: Filter life
    icon: mdi:air-filter
    class: duration
    category: diagnostic
    dps:
      - id: 33
        type: integer
        name: sensor
        unit: "h"
  - entity: select
    name: Fan speed
    icon: mdi:fan
    dps:
      - id: 101
        name: option
        type: string
        mapping:
          - dps_val: "A"
            value: "Auto"
          - dps_val: "1"
            value: "Low"
          - dps_val: "2"
            value: "Medium"
          - dps_val: "3"
            value: "High"
          - dps_val: "4"
            value: "Turbo"
  - entity: switch
    name: Vertical swing
    icon: mdi:pan-vertical
    dps:
      - id: 102
        name: switch
        type: boolean
  - entity: switch
    name: UV
    icon: mdi:radiation
    dps:
      - id: 103
        name: switch
        type: boolean
  - entity: switch
    name: Wet
    icon: mdi:water-outline
    dps:
      - id: 104
        type: boolean
        name: switch
  - entity: switch
    name: Drying
    icon: mdi:hair-dryer
    dps:
      - id: 105
        name: switch
        type: boolean
  - entity: light
    name: Night Light
    category: config
    dps:
      - id: 5
        type: boolean
        name: switch
      - id: 106
        type: integer
        name: brightness
        range:
          min: 1
          max: 100
        unit: "%"
  - entity: switch
    name: Beeper
    icon: mdi:volume-high
    category: config
    dps:
      - id: 109
        name: switch
        type: boolean
Финишная прямая, я обещаю.
Home Assistant
Когда все запчасти собраны, остаётся только чуть мышкой поелозить:
- ставим HACS;
 - добавляем новую репу 
buglloc/tuya-local:
 - ставим 
Tuya Localи перезагружаем Home Assistant; - добавляем устройство 
Tuya Local:
 - выбираем ручное подключение (тут по вкусу, но раз уж всё у нас на руках…):

 - заполняем данные для подключения:

 - если всё хорошо, 
Tuya Localнайдёт подходящее описание:
 
Клик-клик. Щёлк-щёлк. Па-ба-ам:

Ну не красота ли? :)
Upd
Ахаха, дописав пост, решил отправить PR в Tuya Local и внезапно нашёл похожий:
feat: add Tuvio Air Washer device support.
Вак всегда, я сильно быстрее умных мыслей ;)
Заключение
Как ни странно, но это моё чуть ли не первое устройство на Tuya. Не скажу, что я в восторге, но чего не отнять — так это его популярности. А популярность конвертируется в тулинг, благодаря чему на добавление увлажнителя ушло всего пару часов вместе с чтением манов.
А пока у меня всё. Всем кота! (づ˶•༝•˶)づ♡