Как известно, у каждого свой собственный груз ответственности. Так и у меня с котом свой контракт — я его гуляю и кормлю, а он остается великолепным. Честный размен, но моя умная автокормушка Petkit Fresh Element Mini (модель P530) начала очень странно работать — то терять связь со своим китайским отечеством, то перекармливать ложными срабатываниями. В общем, всячески подставлять меня под расторжение договора.

А что это значит?

Правильно — самое время привнести в неё непоправимые улучшения, все как мы любим :)

Первое знакомство

Признаюсь, я не проводил предоперационной подготовки — решил сохранить некое чувство новизны. И был сполна вознагражден, ведь отщелкнув защелки и открутив болты, я начал расплываться в улыбке:

Да-да, сложно представить подарок лучше, чем увидеть ESP8266. Теперь можно не копать плату в поисках плавающих глюков, вслепую тыкая мультиметром и осциллографом, а прошить туда ESPHome и нормально раздебажить и/или закостылить появившиеся бажульки. Заодно и отучить кормилку от облаков — это всегда правильно, это я одобряю.

Более подробно периферия (с точки зрения ESP8266) на материнской плате выглядит так:

ESP8266 общается с ISD91230 по UART (как оказалось позже), а также к нему подключен RTC PCF8563 по I2C: SDA на GPIO5, SCL на GPIO14. Для визуалов:

ISD91230 отвечает за работу с датчиками, лампочками, мотором и прочей механикой. ESP8266 нужен для высокоуровневой логики — расписание, интернеты, кнопка ручной кормежки и все в таком духе.

Подготовка

Дальше стало очевидно, что пора начинать гуглить. К сожалению, ничего “коробочного” я не нашел, зато наткнулся на отличную репу: https://github.com/earlynerd/petkit-serial-bus — где автор больмень разреверсил протокол и утверждал, что ESP8266 с ISD91230 общаются по UART.

Но, прежде чем погрузиться в это великолепие, я достал паяльник, взял немного МГТФ, припаялся к важным для прошивки пинам и подключил USB-TTL (не забыв про питание от лабораторника, но это уже за кадром).

GPIO15 замкнул на землю, чтобы перевести ESP8266 в режим прошивки по UART (см. Boot Mode Selection). Очень удобное решение инженеров Petkit — совместить GPIO15 ESP8266 с Reset пином ISD91230, чтобы последний не мешался.

Почему сразу USB-TTL? Потому что мне так больше нравится: сначала sanity-check прошиваемости, потом дамп прошивки, и только затем можно тратить время на игры с протоколом. К счастью, никаких опций безопасности не включено, ничего глитчить не нужно, и прошивка спокойно дампится (вместе с кредами в plaintext, офк).

% esptool.py -p /dev/serial/by-id/usb-1a86_USB_Single_Serial_5AE6065766-if00 read_flash 0 ALL esp8266.bin
esptool.py v4.8.1
Serial port /dev/serial/by-id/usb-1a86_USB_Single_Serial_5AE6065766-if00
Connecting...
Detecting chip type... ESP8266
Chip is ESP8266EX
Features: WiFi
Crystal is 26MHz
MAC: c8:c9:a3:03:d6:a3
Stub is already running. No upload is necessary.
Configuring flash size...
Detected flash size: 2MB
2097152 (100 %)
Read 2097152 bytes at 0x00000000 in 190.2 seconds (88.2 kbit/s)...
Hard resetting via RTS pin...

Про протокол

Пришла пора узнать, действительно ли ESP8266 общается с ISD91230 по UART и совпадает ли реальность с petkit-serial-bus. Автор описывает протокол так:

  • есть пакет запроса от ESP8266 AA AA 07 01 01 59 9B, который состоит из:
    • AA AA — заголовок
    • 07 — длина пакета вместе с заголовком
    • 01 — тип пакета, в данном случае это запрос состояния
    • 01 — порядковый номер (будет использоваться в ответе)
    • опциональный payload, которого тут нет
    • 59 9B — CRC-16 с сидом 0xFFFF всего пакета
  • на запрос ISD91230 всегда отвечает ACK пакетом вида AA AA 08 01 01 01 94 13, где:
    • AA AA — заголовок, как и в предыдущем случае
    • 08 — длина пакета, как и в предыдущем случае
    • 01 — тип пакета для которого предразначен ACK
    • 01 — порядковый номер пакета запроса
    • 01 — всегда таков, видимо значит OK
    • 94 13 — CRC-16, как и в предыдущем случае
  • и репортами AA AA 12 02 FF 00 00 01 08 EC 02 3F 08 71 02 20 24 C5, где:
    • AA AA — заголовок, как и в предыдущем случае
    • 07 — длина пакета, как и в предыдущем случае
    • 02 — тип пакета, в данном случае статус
    • FF — порядковый номер пакета, для периодических (e.g. статуса, отправляемого раз в пару секунд) всегда равен 0xFF, в остальных случаях — порядковый номер пакета запроса
    • опциональный payload, в данном случае:
      • 00 — статус еды (опрашивает оптический сенсор у бака): 0x00 — пуст, 0x01 — полон
      • 00 — статус дверцы (опрашивает оптический сенсор дверцы): 0x00 — закрыта, 0x01 — открыта
      • дальше хз что за байтики
    • 24 C5 — CRC-16, как и в предыдущем случае

Довольно стройный и логичный формат для бинарного протокола — тут тебе и magic, и размер, и контрольная сумма, все как положено. Похожее можно встретить то тут, то там, потому что это база. Еще бы добавить версию протокола, но при общении внутри одной железки не так критично. Хотя возможно одна из AA — это именно она, кто знает.

Но теория теорией, а для практики я подключаю логический анализатор DSLogic Plus к TX/RX + GND:

Запускаю DSView, добавляю декодер UART (115200 baud 8n1) на оба канала, и всё выглядит правдоподобно:

Если призумить, виден первый пакет от ISD91230:

Затем — статус (seq 0xFF, потому что это не ответ на запрос):

Дальше просыпается ESP8266, который отправляет свое стандартное приветствие (внимание: он это делает на 74880 бод, а уже потом переключится на 115200):

и он же запрашивает статус:

На который ISD91230 отвечает ACK:

и возвращает статус (тут seq не 0xFF, потому что это ответ на запрос с seq 0x01):

Пока всё сходится. Но это только транспорт: команды и их значения могут отличаться чуть более, чем полностью. Вдруг разные ревизии, версии прошивки или вообще модель (хотя у автора тоже Petkit Fresh Element Mini), ну вы знаете как оно бывает. А потому нужно собрать пакетики именно из моей кормилочки.

Для сбора пакетиков можно было бы продолжать использовать логический анализатор. Или подключить двухканальный USB-TTL. Или, как мне, захотеть еще немного запаха припоя и спаять сниффер на RP2040-Zero:

В котором соединить GPIO0 с GPIO5 (ESP8266 TX), GPIO1 с GPIO4 (ISD91230 TX). Залить прошивку petkitBusMonitor. И чуть подсобрать все воедино (оптические сенсоры наличия корма, очевидно, можно не подключать):

Подаем питание на кормилочку и вуаля — запуск:

% tio /dev/serial/by-id/usb-Raspberry_Pi_Pico_E6617C93E3301B2C-if00
[15:28:28.395] tio 62083fd
[15:28:28.395] Press ctrl-t q to quit
[15:28:28.396] Connected to /dev/serial/by-id/usb-Raspberry_Pi_Pico_E6617C93E3301B2C-if00
ISD91230: packet type: 0, length: 7, seq: 0, crc: 7A8B [Valid!], 
ISD91230: packet type: 2, length: 18, seq: 255, crc: CCBC [Valid!],  Data: 0x01010008F902420CCC0339
ESP8266: packet type: 1, length: 7, seq: 1, crc: 599B [Valid!], 
ISD91230: packet type: 1, length: 8, seq: 1, crc: 9413 [Valid!],  Data: 0x01
ISD91230: packet type: 2, length: 18, seq: 1, crc: 0ACF [Valid!],  Data: 0x01010008D2023908420214
ESP8266: packet type: 1, length: 7, seq: 2, crc: 69F8 [Valid!], 
ESP8266: packet type: 19, length: 9, seq: 1, crc: DF3E [Valid!],  Data: 0x057E
ISD91230: packet type: 1, length: 8, seq: 2, crc: C140 [Valid!],  Data: 0x01
ISD91230: packet type: 2, length: 18, seq: 2, crc: AE75 [Valid!],  Data: 0x01010008D4023908420214
ESP8266: packet type: 3, length: 11, seq: 1, crc: 6CFA [Valid!],  Data: 0x00050005
ISD91230: packet type: 19, length: 8, seq: 1, crc: B910 [Valid!],  Data: 0x01
ISD91230: packet type: 20, length: 9, seq: 1, crc: AE3B [Valid!],  Data: 0x0004
ISD91230: packet type: 3, length: 8, seq: 1, crc: FA73 [Valid!],  Data: 0x01
ESP8266: packet type: 5, length: 9, seq: 1, crc: D309 [Valid!],  Data: 0x0005
ISD91230: packet type: 5, length: 8, seq: 1, crc: 48D3 [Valid!],  Data: 0x01
ESP8266: packet type: 1, length: 7, seq: 3, crc: 79D9 [Valid!], 
ISD91230: packet type: 1, length: 8, seq: 3, crc: F271 [Valid!],  Data: 0x01
ISD91230: packet type: 2, length: 18, seq: 3, crc: B444 [Valid!],  Data: 0x01010008D3023908420214
ESP8266: packet type: 4, length: 11, seq: 1, crc: CE7D [Valid!],  Data: 0x00FF00FF
ISD91230: packet type: 4, length: 8, seq: 1, crc: 7FE3 [Valid!],  Data: 0x01
ESP8266: packet type: 6, length: 9, seq: 1, crc: 057F [Valid!],  Data: 0xFFFF
ISD91230: packet type: 6, length: 8, seq: 1, crc: 1183 [Valid!],  Data: 0x01
ESP8266: packet type: 13, length: 19, seq: 1, crc: 62BA [Valid!],  Data: 0x003C0190F01222201F4F01
ESP8266: packet type: 1, length: 7, seq: 4, crc: 093E [Valid!], 
ISD91230: packet type: 13, length: 8, seq: 1, crc: E172 [Valid!],  Data: 0x01
ESP8266: packet type: 1, length: 7, seq: 5, crc: 191F [Valid!], 
ISD91230: packet type: 1, length: 8, seq: 4, crc: 6BE6 [Valid!],  Data: 0x01
ISD91230: packet type: 2, length: 18, seq: 4, crc: BC0F [Valid!],  Data: 0x01010008D3023908420214
ISD91230: packet type: 1, length: 8, seq: 5, crc: 58D7 [Valid!],  Data: 0x01
ISD91230: packet type: 2, length: 18, seq: 5, crc: A63E [Valid!],  Data: 0x01010008D4023908420214
ESP8266: packet type: 14, length: 14, seq: 1, crc: 0767 [Valid!],  Data: 0x0300C800C80001
ISD91230: packet type: 14, length: 8, seq: 1, crc: B822 [Valid!],  Data: 0x01
ESP8266: packet type: 1, length: 7, seq: 6, crc: 297C [Valid!], 
ISD91230: packet type: 1, length: 8, seq: 6, crc: 0D84 [Valid!],  Data: 0x01
ISD91230: packet type: 2, length: 18, seq: 6, crc: 5BD4 [Valid!],  Data: 0x01010008D2023908410214
ESP8266: packet type: 13, length: 19, seq: 2, crc: AD1F [Valid!],  Data: 0x003C0190F01222201F4F01
ESP8266: packet type: 1, length: 7, seq: 7, crc: 395D [Valid!], 
ISD91230: packet type: 13, length: 8, seq: 2, crc: B421 [Valid!],  Data: 0x01
ISD91230: packet type: 1, length: 8, seq: 7, crc: 3EB5 [Valid!],  Data: 0x01
ISD91230: packet type: 2, length: 18, seq: 7, crc: B990 [Valid!],  Data: 0x01010008D3023908420214
ESP8266: packet type: 1, length: 7, seq: 8, crc: C8B2 [Valid!], 
ISD91230: packet type: 1, length: 8, seq: 8, crc: 2E8B [Valid!],  Data: 0x01
ISD91230: packet type: 2, length: 18, seq: 8, crc: 2C47 [Valid!],  Data: 0x01010008D5023908430215
ESP8266: packet type: 1, length: 7, seq: 9, crc: D893 [Valid!], 
ISD91230: packet type: 1, length: 8, seq: 9, crc: 1DBA [Valid!],  Data: 0x01
ISD91230: packet type: 2, length: 18, seq: 9, crc: F056 [Valid!],  Data: 0x01010008D3023908410214
ISD91230: packet type: 1, length: 8, seq: 10, crc: 48E9 [Valid!],  Data: 0x01
ESP8266: packet type: 1, length: 7, seq: 10, crc: E8F0 [Valid!], 
ISD91230: packet type: 2, length: 18, seq: 10, crc: B5DD [Valid!],  Data: 0x01010008D4023908420214
ISD91230: packet type: 14, length: 8, seq: 2, crc: ED71 [Valid!],  Data: 0x01
ESP8266: packet type: 14, length: 14, seq: 2, crc: 7991 [Valid!],  Data: 0x0103E803E8FFFF
ESP8266: packet type: 14, length: 14, seq: 3, crc: E6C0 [Valid!],  Data: 0x0203E803E8FFFF
ISD91230: packet type: 14, length: 8, seq: 3, crc: DE40 [Valid!],  Data: 0x01
ISD91230: packet type: 2, length: 18, seq: 255, crc: 956D [Valid!],  Data: 0x01010008D4023908420214

Очередность запросов/ответов порой немного плавает, но это не сильно мешает. Вот важные запросы инициализации (моргалки и пищалки не считаем важными):

type: 0x13 payload: 05 7E (настройки двигателя?)
type: 0x03 payload: 00 05 00 05
type: 0x05 payload: 00 05
type: 0x04 payload: 00 FF 00 FF
type: 0x06 payload: FF FF
type: 0x0D payload: 00 3C 01 90 0F 01 22 22 01 F4 0F 01

Затем, тыкаю во все, что тыкается, и составляю список нужных мне команд:

  • статус:
    • запрос: type: 0x01 payload: <none>
    • ответ: type: 0x02 payload: 00 01 00 XX XX XX XX XX XX XX:
      • (0) — состояние дверцы: 0x00 — закрыта, 0x01 — открыта
      • (1) — наличие еды: 0x00 — пусто, 0x01 — есть, что покушать
      • (2) — ready (?): сначала он 0x00, затем меняется на 0x01
      • (3-9) — я хз
  • открыть дверцу:
    • запрос: type: 0x07 payload: 1E
    • ответ: type: 0x08 payload: 02 0E 14 00 22:
      • (0) — успешность: 0x02 — успех, 0x00 — ошибка
      • (1-4) — я хз
  • закрыть дверцу:
    • запрос: type: 0x09 payload: 1E
    • ответ: type: 0x0A payload: 02 08 14 00 30:
      • (0) — успешность: 0x02 — успех, 0x00 — ошибка
      • (1-4) — я хз
  • насыпать еды (запуск двигателя):
    • запрос: type: 0x0B payload: 01 01 01 50:
      • (0) — количество порций (одна порция около 5 грамм)
      • (1-3) — я хз
    • ответ: type: 0x0C payload: 02 01 01 01 42, где по байтам:
      • (0) — количество отсыпанных порций
      • (1) — хз
      • (2) — хз, но при нескольких запросах только в финале будет не 0x00
      • (3-4) — хз
    • иногда следом идет запрос type: 0x0B payload: 00 02 01 50. Смысл не осознал
  • управление лампочками и пищалкой:
    • запрос — type: 0x0E payload: 03 00 C8 00 C8 00 01:
      • (0) — цель: 0x01 — верхний LED, 0x02 — нижний LED, 0x03 — бипер
      • (1-2) — “on” state ms, 0xFFFF значит всегда
      • (3-4) — “off” state ms, 0xFFFF значит всегда
      • (5-6) — количество, 0xFFFF значит всегда

Это база, без которой кота не накормить :) Если же сматчить с petkit-serial-bus, то совпадение не 100% (например, перепутаны местами статус еды и дверцы), но довольно хорошее. Как я и говорил ранее, так бывает, потому и проверял, потому и прикопал сэмплов со своего девайса ;)

ESPHome компонент pkt_p530

Разумеется, в качестве прошивки я решил взять ESPHome, к которому написал компонент pkt_p530 для общения с ISD91230. Долго думал, как бы сохранить баланс между программированием на ямлах (буэ) и возможностью гибко кастомизировать процесс кормления и/или реакцию на разные непредвиденные ситуации. В итоге, пришел к простой конструкции:

  • есть P530Component, который:
    • занят транспортом: отправить пакетик, сгенерить seq no, вычитать ответы
    • минимально парсит пакеты, прилетающие от ISD91230
    • позволяет подписаться на пакет нужного типа, с нужный seq no и таймаутом
    • обрабатывает всякую мелочевку, в духе работы с пользовательскими сенсорами
  • есть набор PktAction, каждый из которых:
    • имеет опциональные on_error/on_complete ветки
    • для них кастомный control flow, т.к. в норме Action в ESPHome это блокирущие fire-and-forget действия :/
    • отравляет нужный набор пакетов, вешает коллбэки на получение ACK и ответов
    • по коллбэку решает, что делать дальше: передать управление, выполнить on_error/on_complete ветку и т.д.

Таким образом, получилась прямая конструкция с асинхронным компонентом и набором действий к нему. Например, код действия выдачи еды:

template<typename... Ts> class DispenseAction : public PktAction<Ts...> {
 public:
  TEMPLATABLE_VALUE(uint8_t, portions)

 protected:
  ErrorCode do_action_(const Ts &...x) override {
    if (!this->parent_->has_food()) {
      return ErrorCode::NO_FOOD;
    }

    uint8_t portions = this->portions_.value(x...);
    uint8_t payload[] = {portions, 0x01, 0x01, 0x50};

    // 1 portion is about 1.65s
    uint32_t timeout = portions * 3000;
    return this->send_cmd_(ReqType::DISPENSE, payload, sizeof(payload), ReportType::DISPENSE_DONE, timeout);
  }

  ErrorCode handle_report_(const std::span<const uint8_t> payload) override {
    if (payload.size() < 3 || payload[2] == 0x00) {
      return ErrorCode::NOT_IMPLEMENTED;  // not done yet
    }

    return ErrorCode::OK;
  }
};

Его использование в скрипте ESPHome:

script:
  - id: feed
    mode: single
    parameters:
      portions: int
    then:
	  - pkt_p530.dispense:
		  portions: 2
		  on_error:
		    - logger.log: "Shit happens!"
		    - pkt_p530.door_close:
		    - pkt_p530.beep: "short"
		    - script.stop: feed
	  - logger.log: "Done"

Под катопом:

  • запустится DispenseAction и проверит наличие еды
  • если еда есть, то отправит запрос DISPENSE (0x0B) с нужным количеством порций
  • подпишется на ACK для этого запроса и отпустит выполнение
  • P530Component при вычитывании ответов сматчит нужный ACK и дернет коллбэк DispenseAction
  • тот подпишется на репорт DISPENSE_DONE (0x0C) и отпустит выполнение
  • P530Component при вычитывании ответов обнаружит нужный репорт и дернет коллбэк DispenseAction
  • тот проверит, что пища достигла миски, отпишется от репорта DISPENSE_DONE и выполнит ветку on_complete или передаст выполнение следующему действию

Для визуалов:

По мне выглядит неплохо. Никакой мешанины из wait_until/if + condition в скрипте ESPHome, минимум блокировок main loop, с таймаутами и прочей гигиеной, в духе возможности обработать ошибки. Из понятных улучшений — можно было бы не блокировать loop у P530Component при вызове коллбэков, но тогда все становится неприятно и путано.

Прошивка

Фуф, все запчасти готовы! Самое время опять подкинуть USB-TTL, прошить ESP8266 в ESPHome и заняться написанием конфига.

Сначала просим ESPHome запустить скрипт инициализации при запуске (рестарты не важны - его можно запускать сколь угодно много раз, я проверял), подключить нужные компоненты и не забывать отключить UART логгер:

esphome:
  name: petkitp530
  friendly_name: PetkitP530
  on_boot:
    then:
      - script.execute: bootup

esp8266:
  board: esp01_1m

external_components:
  - source: github://buglloc/esphome-components
    components: [pkt_p530]
    refresh: 1h

logger:
  hardware_uart: UART1
  baud_rate: 0 # disable UART logger

uart:
  id: isd91230
  tx_pin: GPIO1
  rx_pin: GPIO3
  baud_rate: 115200

i2c:
  - id: pcf8563_i2c
    scl: GPIO14
    sda: GPIO5

pkt_p530:
  id: feeder
  uart_id: isd91230

script:
  - id: bootup
    mode: single
    then:
      - pkt_p530.init:
          on_error:
          - script.execute: "boot_failed"
      - pkt_p530.beep:
          preset: "short"
          count: 1
      - pkt_p530.door_open:
      - delay: 2s
      - pkt_p530.door_close:
          on_error:
          - script.execute: "boot_failed"
      - pkt_p530.led_upper: "steady_on"

  - id: boot_failed
    mode: restart
    then:
      - logger.log:
          format: "Ooops, init failed!"
          level: "INFO"
      - pkt_p530.beep: "short"
      - pkt_p530.door_close:
      - pkt_p530.led_upper: "blink_fast"
      - script.stop: "bootup"

Тут мы:

  • запускаем действие инициализации
  • затем короткий пик
  • открываем дверцу
  • ждем чутка
  • закрываем дверцу
  • включаем верхнюю лампочку

Открывание дверцы добавил, чтобы вернуть кормилку в ожидаемое состояние, мало ли чего. В случае ошибки реагируем большим количеством пиков, закрытием дверцы и начинаем быстро моргать верхней лампочкой.

Затем добавляем Binary Sensor для реакции на кнопки и скрипт кормления:

binary_sensor:
  - platform: gpio
    pin: 
      number: GPIO13
      inverted: true
      mode:
        input: true
        pullup: true
    name: "Manual feed"
    filters:
      - delayed_on_off: 500ms
    on_press: 
      then:
        - script.execute:
            id: feed
            portions: 5

  - platform: pkt_p530
    id: feeder
    door_opened:
      name: "Door opened"
    food_low_issue:
      name: "Low food issue"
    door_open_issue:
      name: "Door issue (open)"
    door_close_issue:
      name: "Door issue (close)"

script:
  - id: feed
    mode: single
    parameters:
      portions: int
    then:
      - logger.log:
          format: "Feeding..."
          level: "INFO"
      - if:
          condition:
            pkt_p530.has_food
          then:
            - pkt_p530.led_lower: "blink"

            - pkt_p530.door_open:
                on_error:
                  - logger.log:
                      format: "Can't open door: abort feeding"
                      level: "ERROR"
                  - pkt_p530.door_close:
                  - pkt_p530.beep: "short"
                  - script.stop: feed

            - pkt_p530.dispense:
                portions: !lambda return portions;
                on_error:
                  - logger.log:
                      format: "Dispensing failed"
                      level: "ERROR"
                  - pkt_p530.door_close:
                  - pkt_p530.beep: "short"
                  - script.stop: feed

            - pkt_p530.door_close:
                on_error:
                  - logger.log:
                      format: "Door close door: abort feeding"
                      level: "ERROR"
                  - pkt_p530.beep: "short"
                  - script.stop: feed

            - pkt_p530.led_lower: "steady_off"

            - logger.log:
                format: "Feeding complete, meow"
                level: "INFO"

          else:
            - logger.log:
                format: "NO FOOD!"
                level: "ERROR"
            - pkt_p530.beep: "long"

Тут мы при нажатии на кнопку ручного кормления запускаем скрипт feed с нужным количеством порций, который:

  • проверит наличие еды, не обязательно, но чем раньше, тем проще
  • начнет моргать нижней лампочкой
  • откроет дверцу
  • насыпет нужное количество порций еды
  • закроет дверцу
  • выключит нижнюю лампочку

В случае каких-то проблем — реагирует закрытием дверцы, пиком и морганием нижнего диода (продолжает моргать со старта). Отдельно binary sensor door_open_issue/door_close_issue сообщит о проблеме, на что можно отреагировать уже из Home Assistant (нотификашки, сообщенька в tg чатик и тд).

Кормушка почти готова, осталось завести кронячку кормления (не круто оставлять кота голодным при недоступности Home Assistant, потому все локально):

time:
  - platform: pcf8563
    id: pcf8563_time
    address: 0x51
  - platform: homeassistant
    id: ha_time
    on_time_sync:
      then:
        pcf8563.write_time
    on_time:
      - cron: '00 00 09 * * *'
        then:
          - script.execute:
              id: feed
              portions: 8
      - cron: '00 00 21 * * *'
        then:
          - script.execute:
              id: feed
              portions: 4

Вот и все, дальше остается только тюнить и настраивать разные свистелки :) Полную версию конфига можно поштырить на github.

Результат

Самое время опробовать вживую:

На видео я:

  • включил эту чудо-машину
  • “накормил” котана из Home Assistant и хардварной кнопки
  • напоследнок проверил, что не пропущу проблемы с дверцой

Имхо, в текущем виде его нельзя назвать production-ready, и какие-то мелочи нужно еще потюнить, но базовая функциональность работает, и это уже отлично :) Надеюсь, не оставлю кота голодным :D

А на сегодня у меня всё. Всем (づ˶•༝•˶)づ♡