Четвертый год строю умный дом. Кажется, понял, что в нем самое главное. Не какое решение выбрать в качестве сервера, не какие технологии использовать, не каких производителей девайсы закупать — это все детали.

Главное — вовремя остановиться. И у меня это не получилось, судя по тому, что я потратил прорву времени на это.

Давно хотел сделать такую панель, но нормального гайда не нашел, пришлось придумывать собственный велосипед.

Зачем

Умный дом прекрасен и без тачпанели. Большая часть магии все равно под капотом и обходится без интерфейсов, чисто на автоматизациях. А такие команды как «включить свет» и «сделать теплее», удобно отдавать голосом через умную колонку. Или через телеграм-бот.

В общем, меня все устраивало, пока я не поставил датчики влажности почвы в цветочные горшки. Сообщение в телеграм «Полейте белый цветок на подоконнике!» пару раз ставило в тупик, так как и белых цветков и подоконников в доме немало. Я задумался, и тут все завертелось…

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

Железо

Подробно расписывать, что такое умный дом, и как он устроен конкретно у меня, смысла не вижу, статей об этом полно. Мой основан на Home Assistant, просто потому, что в него можно интегрировать вообще все. У меня, например, 52 Zigbee- и 38 WiFi-устройств, 2 Modbus-шлюза для управления климатической техникой, IP-домофон, и шлюз LoRa для связи с GPS-трекером в машине. Все это от разных производителей, почти у каждого своя экосистема и свои заморочки. Для возведения таких вавилонов Home Assistant вне конкуренции.

Железо для тачпанели выбирал довольно долго. Поначалу рассматривал два очевидных варианта, от которых в итоге отказался: Android-планшет, и промышленный компьютер с тачскрином.

Казалось бы, планшет — идеальная железка для панели, вешай на стену и наслаждайся. Многие так и делают, а через полгода выясняется, что зря. От постоянного подключения к адаптеру питания аккумулятор планшета накрывается (а то и вздувается), и все это приходится нести в ремонт.

Кто-то пытается наколхозить обходное решение: например, ставит умную розетку, которая включает питание только когда аккумулятор разрядится до 10%. Или же вообще извлекает из планшета аккумулятор и подпаивает питание напрямую.

В любом случае этот путь мне не годился, потому что даже если все получится, диагональ панели будет очень маленькой.

У промышленных компов с Aliexpress другие проблемы: либо дорого (40+ тыс. руб.), либо непонятная надежность и никакая производительность. А то и все сразу.

В итоге я приобрел на Авито старый сенсорный монитор iiyama T2252MSC за 16 000 руб. (верный выбор, подсмотрел в одном телеграм-канале про умный дом) и микро-ПК Findarling T5B за 8 000 руб. (ошибка, тормозит, пришлось заменить на более современный). 

Findarling T5B. Зря купил
Findarling T5B. Зря купил

Следующий вопрос — как и куда вешать эту панель. Когда я 8 лет назад начал строить дом, перерасход бюджета вынудил исключить из проекта не самые необходимые инженерные системы — в частности, под нож пошел проводной умный дом. В итоге никакой инфраструктуры под умный дом не заложено, приходится импровизировать.

Провода три года старательно намекали, что им нужно найти применение
Провода три года старательно намекали, что им нужно найти применение

Но мне повезло — в холле когда-то планировалось повесить монитор домофона, поэтому туда были выведены витая пара и питание. Оставалось только выдолбить перфоратором нишу для оборудования, и закрепить кронштейн для монитора.

Сперва добейся, потом критикуй. Ну или сразу критикуй
Сперва добейся, потом критикуй. Ну или сразу критикуй

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

Софт

На подключенном к тачпанели компе установлен Windows 10 с Chrome в режиме киоска и больше ничего. И все равно производительности древнего Intel Atom не хватило, слишком тяжелый фронтенд я наворотил. Выражается это в периодических фризах картинки с камер и в общей недостаточной отзывчивости интерфейса. В итоге поменял на чуть более громоздкий комп с Intel N97, с некоторым трудом удалось разместить его в той же нише.

GMKteс G5. Дельный аппарат.
GMKteс G5. Дельный аппарат.

Chrome с этого компа в режиме киоска ломится на сервер HA под специальным бесправным пользователем, и показывает доступные ему панели.

Дневной вид панели управления
Дневной вид панели управления

Самое трудоемкое — сконфигурировать панель поэтажного плана в Home Assistant. Код этой панели у меня занимает 20 тысяч строк. К счастью, не весь код пишется вручную, основную часть можно сгенерить специальным плагином.

Чтобы нарисовать поэтажный план, я поставил Sweet Home 3D, и наборы моделей к нему. Затем потратил несколько дней, чтобы научиться им пользоваться, и чтобы получилось что-то похожее на реальный дом.

Совет: чем больше на плане деталей, тем он лучше смотрится. Поэтому у меня, например, на плане второго этажа размещена модель стола для пинг-понга, хотя этот стол никак с умным домом не взаимодействует. Но он там есть и хорошо заполняет пустое пространство.

Больше деталей!
Больше деталей!

Когда план более-менее готов, ставим плагин home-assistant-floor-plan. Он нужен, чтобы сгенерить код панели Home Assistant. Экономит очень много времени (напоминаю, 20 тысяч строк!).

Теперь размещаем источники света. Те, которые должны быть интерактивными, называем по идентификаторам их сущностей в Home Assistant. Если на плане должно что-то исчезать и появляться — называем этот объект по соответствующему бинарному датчику или переключателю. Например, фигурка человека на унитазе привязана к датчику присутствия binary_sensor.prisutstvie_v_sanuzle_presence. А машина на парковке привязана к вспомогательной сущности input_boolean.car_home, значение которой ставится true в случае, если расстояние машины до дома меньше 20 метров.

Название интерактивной сущности должно быть таким же, как ее идентификатор в Home Assistant
Название интерактивной сущности должно быть таким же, как ее идентификатор в Home Assistant

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

Совет: выберите и сохраните вид для окна предпросмотра (CTRL+ALT+R). Если вы потом будете вносить изменения в план (а вы будете), малейший сдвиг — и придется перерендерить все варианты плана. Поэтому перед каждым рендерингом восстанавливаем сохраненный вид.

Когда все готово, рендерим тестовую картинку. Только так вы сможете увидеть финальный вид вашего плана, с освещением и тенями. Вам точно придется делать это несколько раз, чтобы пофиксить все косяки и подобрать подходящий размер картинки в пикселях. Я в итоге остановился на 1458x1010, что для FHD-монитора хорошо подошло.

Если все ок, жмем Tools → Home Assistant Floor Plan и настраиваем плагин — конфигуратор панели.

Конфигурация панели поэтажного плана
Конфигурация панели поэтажного плана

Прокликиваем каждую сущность в списке Other enitities и настраиваем.

  • Display type: оставляем Icon

  • Display condition: условие отображения иконки сущности. Если нам нужна иконка для управления объектом, то Always, если не нужна, то Never. Ну либо When on/When Off, чтобы иконка была видна только когда соответствующая ей сущность была true либо false. Например, для ворот у меня иконка включена, так как через нее я их открываю и закрываю, а для присутствия в санузле — выключена, так как управлять там нечем. Если пофантазировать, можно на эту иконку привязать TTS через Яндекс.Станцию, чтобы она на 10й громкости предлагала освободить туалет, но сортирные шутки быстро надоедают.

  • Tap action: действие по нажатию. Toggle переключает сущность, More info показывает значение. More info актуально для датчиков и объектов с поп‑апами управления, а вот выключателям света, приводам ворот и т. д. ставим Toggle.

  • Double tap action и Hold action позволяют настроить реакцию на двойное и продолжительное нажатие. Пока не нашел этому применения.

  • Display furniture: отображение объекта на плане. Always если всегда, State is.. только когда его сущность в home assistant имеет указанное значение, State isn't — только когда сущность имеет любое другое значение. Например, у меня машина на плане отображается только когда значение input_boolean.car_home равняется true, аналогично с воротами и присутствием в санузле.

Ставим рендерер какой нравится (я разницы между ними не вижу), а качество High. Sensitivity мало на что влияет, я оставляю дефолтную 10.

В Render Times можно добавить для какого времени суток будут рендериться изображения. Дело в том, что плагин позволяет настроить так, чтобы картинка менялась с течением времени. Например, в 10:00 вы увидите на плане свой дом в утреннем освещении, в 15:00 в дневном, а в полночь — в темноте. Звучит круто, по факту же изменения малозаметны. Ну, тени движутся, это да. Но цена этого значительна: на каждое указанное время будет рендериться полный пакет изображений, а это долго.

Чтобы потом в вашей панели рендеры плана переключались по времени, надо создать в HA вспомогательный датчик sensor.time_as_number_utc, с шаблоном {{states("sensor.time_utc").split(":") | join | int}}. Это позволяет установить смену картинки плана хоть каждые полчаса. Я поигрался этим, и в итоге сделал проще. После восхода у меня дневной вид, после заката — ночной.

Каждый источник освещения и каждый исчезающий объект удвоит вам количество рендеров. Если у вас только одна интерактивная лампа, и вы указали одно время дня, то плагин сделает две картинки: с включенной лампой и выключенной. Если две интерактивные лампы, картинки будет уже четыре, и т.д. У меня 448 рендеров первого этажа и 24 второго. Это при том, что я в итоге остановился всего на двух вариантах плана, на дневном и ночном. Вариант с почасовой сменой плана потребует в 12 раз больше рендеров.

Проблема тут в том, что Sweet Home 3D рендерит эти картинки очень долго. Использовать GPU он не умеет, и на моем i7-14700 эти 448 картинок рендерятся около 15 часов со 100% загрузкой CPU.

Все настроили, проходимся по чеклисту:

  • Все ли интерактивные источники света указаны в панели Detected Lights?

  • Есть ли ошибки в именах сущностей источников света?

  • Все ли исчезающие объекты указаны в панели Other entities?

  • Все ли исчезающие объекты сконфигурированы правильно?

  • Установлены ли размеры изображения?

Если все ок, нажимаем Start и идем спать.

У меня Sweet Home 3D при рендеринге иногда крашится, но, к счастью, после перезапуска продолжает с того места, где прервался (если стоит галочка Use existing renders).

Конфигурация панели поэтажного плана

Для корневого макета панели берем Sidebar, чтобы не занимать всю ширину монитора планом дома. На боковую панель выносим самые полезные данные, не имеющие привязки к конкретной локации в доме — в моем случае это напряжения и потребляемая мощность на фазах, управление вентиляцией и кондиционером, почасовой прогноз погоды и т.д.

Основную, левую часть макета занимает карточка Vertical Stack. Так как в доме два этажа, то в этом макете две карточки Conditional, содержимое которых показывается попеременно, в зависимости от значения переменной input_select.floorplan_floor. Внутри каждой Conditional-карточки сидит карточка Picture-Elements, которая, собственно, и содержит план дома и все элементы, которые на него накладываются.

Структура панели
Структура панели

Базовый код Picture-Elements генерит для нас упомянутый плагин. Все картинки с планами всех этажей, которые мы нарендерили в Sweet Home, мы складываем в \\homeassistant\config\www\floorplan, а полученный код плана этажа из файла floorplan.yaml (который генерит плагин) копипастим в текстовый редактор панелей Home Assistant, в карточку Picture-Elements. Так мы получим интерактивный план этажа дома с управлением светом и исчезающими элементами.

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

Совет: редактируйте код панели в стороннем текстовом редакторе, и затем копипастите в редактор панели Home Assistant. Дело в том, что редактор панели перерисовывает превью панели при изменении любого символа, из-за этого все люто тормозит при попытке редактировать сложные панели.

К сожалению, все дополнительные элементы приходится выравнивать методом приближений — чуть поменяли расположение, сохранили панель, посмотрели, в каком месте теперь элемент, снова подправляем.

Распишу, какие элементы есть в моей панели.

Переключение этажа. Создаем вспомогательную сущность input_select.floorplan_floor с возможными значениями «Первый этаж», «Второй этаж» и т.д., прописываем отображение панели Conditional в зависимости от значения input_select.floorplan_floor.

На каждый этаж добавляем элемент переключения.

Скрытый текст
type: icon
icon: mdi:floor-plan
style:
  left: 3%
  top: 3%
tap_action:
  action: perform-action
  perform_action: input_select.select_next
  target:
    entity_id: input_select.floorplan_floor
  data:
    cycle: true

Показания датчиков. Самое простое — это элемент типа state-label, привязанный к объекту, чье значение вы хотите показать на плане.

Скрытый текст
type: state-label
entity: sensor.datchik_v_detskoi_temperature
style:
  left: 52%
  top: 16%
  color: black
  font-weight: bold
  font-size: 16px
  background: null

По нажатию можно посмотреть, как показания менялись во времени.

Если датчик питается от батареи, полезно рядом с его показаниями разместить индикатор низкого заряда, это элемент типа conditional, отображаемый при низком значении объекта батареи датчика.

Скрытый текст
type: conditional
conditions:
  - condition: numeric_state
    entity: sensor.datchik_v_detskoi_battery
    below: 5
elements:
  - type: icon
    icon: mdi:battery-alert-variant-outline
    style:
      left: 54%
      top: 15%
      color: "#ff0000"
      transform: translate(-50%, -50%)
      scale: 60%
    entity: sensor.datchik_v_detskoi_battery

Управление шторами. Простой элемент state-icon без изысков, так как его поп-ап достаточно функционален.

Скрытый текст
type: state-icon
entity: cover.shtora_v_kabinete_3
tap_action:
  action: more-info
style:
  left: 17%
  top: 53%
  border-radius: 50%
  text-align: center
  background: "#D8BFD8"
  opacity: 80%

Тыкаем в иконку — всплывает поп-ап, где можно управлять шторой.

Иконка полива цветка. Три иконки (элемента) на самом деле — красная, желтая и зеленая, отображаются на одном и том же месте в зависимости от показаний датчика влажности почвы.

Скрытый текст
type: conditional
conditions:
  - condition: numeric_state
    entity: sensor.vlazhnost_tsvetka_na_bufete_soil_moisture
    above: 49
elements:
  - type: icon
    icon: mdi:flower
    entity: sensor.vlazhnost_tsvetka_na_bufete_soil_moisture
    style:
      top: 38%
      left: 78%
      color: green
      background: "#D8BFD8"
      border-radius: 5px
title: цветок на буфете полит
type: conditional
conditions:
  - condition: numeric_state
    entity: sensor.vlazhnost_tsvetka_na_bufete_soil_moisture
    below: 50
  - condition: numeric_state
    entity: sensor.vlazhnost_tsvetka_na_bufete_soil_moisture
    above: 16
elements:
  - type: icon
    icon: mdi:flower
    entity: sensor.vlazhnost_tsvetka_na_bufete_soil_moisture
    style:
      top: 38%
      left: 78%
      color: yellow
      background: "#D8BFD8"
      border-radius: 5px
title: цветок на буфете надо полить
type: conditional
conditions:
  - condition: numeric_state
    entity: sensor.vlazhnost_tsvetka_na_bufete_soil_moisture
    below: 16
elements:
  - type: icon
    icon: mdi:flower
    entity: sensor.vlazhnost_tsvetka_na_bufete_soil_moisture
    style:
      top: 38%
      left: 78%
      color: red
      background: "#D8BFD8"
      border-radius: 5px
title: цветок на буфете сохнет

Термостаты. Вот тут можно поизвращаться. Дело в том, что у панели типа Picture-Elements набор разрешенных элементов очень убог, а обычную интерфейсную карточку для Home Assistant разместить на этой панели нельзя. При этом можно разместить любую кастомную карточку для Home Assistant — вроде тех, что устанавливаются через HACS. Идиотизм, но приходится действовать так: находим более-менее подходящую кастомную карточку, устанавливаем через HACS, и пожалуйста, добавляем через элемент типа custom. Например, мне пришлось устанавливать карточку Better Thermostat, несмотря на то что меня вполне устроила бы и стандартная карточка термостата.

Тут нужно два элемента: иконка для вызова карточки термостата (чтобы не загромождать план дома, если термостатов несколько, а показывать по нажатию), и сама карточка термостата типа custom, вложенная в conditional.

Скрытый текст
type: state-icon
style:
  left: 22%
  top: 63%
  border-radius: 50%
  border: 2px solid red
  text-align: center
  background-color: rgba(255, 255, 255, 0.3)
  opacity: 100%
entity: input_boolean.koshkin_dom
state_color: true
tap_action:
  action: toggle

 Создаем вспомогательную сущность input_boolean.koshkin_dom, и если она true, то показываем элемент с карточкой термостата.

Скрытый текст
type: conditional
conditions:
  - condition: state
    entity: input_boolean.koshkin_dom
    state: "on"
elements:
  - type: custom:better-thermostat-ui-card
    entity: climate.koshkin_dom
    disable_window: false
    disable_off: false
    name: Кошкин дом
    style:
      left: 30%
      top: 40%
      width: 300px
      border-radius: 10px
      border: 2px solid black
      text-align: center
      opacity: 95%

Но можно так не извращаться и обойтись довольно убогим информационным поп-апом термостата.

Скрытый текст
type: state-icon
icon: mdi:cat
style:
  left: 22%
  top: 63%
  border-radius: 50%
  border: 2px solid red
  text-align: center
  background-color: rgba(255, 255, 255, 0.3)
  opacity: 100%
entity: climate.koshkin_dom
state_color: true
tap_action:
  action: more-info

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

Внимание! Обогревать кошек — это важно. Не пренебрегайте этой обязанностью.

Вот он батонится. Внутри домика обогреватель на 300 Вт
Вот он батонится. Внутри домика обогреватель на 300 Вт

Картинка с камеры. Просто элемент типа image.

Скрытый текст
type: image
camera_view: live
camera_image: camera.camera2
style:
  left: 62%
  top: 87%
  width: 300px
  border: 2px solid black
  border-radius: 10px
  text-align: center
  background-color: rgba(255, 255, 255, 0.3)
  opacity: 100%

Не всегда работает стабильно, картинка иногда фризится, почему — пока не разобрался. Но в целом удобно.

Тапнув по картике, получим поп-ап с картинкой побольше.

Картинка с отслеживаемым объектом. В моем случае это последнее появление кота на камере с отметкой, когда он был зафиксирован. Мне это нужно, чтобы понимать, откуда — с крыльца или веранды — звать котов, когда они изволят вернуться с гулянки.

Определением объектов в кадре занимается сервер видеонаблюдения Frigate, с ним интегрирован Home Assistant. Интеграция Frigate создает в HA сущности с изображениями последнего обнаруженного объекта каждого опознаваемого типа, меня интересует объект типа cat.

Скрытый текст
type: image
entity: image.camera2_cat
camera_view: auto
style:
  left: 75%
  top: 85%
  width: 120px
  border-radius: 50%
  border: 2px solid black
  text-align: center
  background-color: rgba(255, 255, 255, 0.3)
  opacity: 100%

Сгенерить подпись с количеством времени, прошедшего с момента появления объекта в кадре, оказалось не очень просто. Пришлось в configuration.yaml прописать специальные датчики (тут прибегнул к вайбкодингу, собственных знаний не хватило).

Скрытый текст
      time_since_camera2_cat_change:
        friendly_name: 'Время с последнего изменения camera2_cat'
        value_template: >-
          {% set snapshot_time_str = states('image.camera2_cat') %}
          {% if snapshot_time_str is none or snapshot_time_str in ['unavailable', 'unknown', ''] %}
            Неизвестно
          {% else %}
            {% set snapshot_ts = as_timestamp(snapshot_time_str, 0) %}
            {% set current_ts = now().timestamp() %}
            {% if snapshot_ts == 0 %}
              Ошибка формата
            {% else %}
              {% set diff_secs = current_ts - snapshot_ts %}
              {% if diff_secs < 0 %}
                Ошибка времени
              {% elif diff_secs >= 86400 %}
                Давно
              {% else %}
                {% set hours = (diff_secs // 3600) | int %}
                {% set minutes = ((diff_secs % 3600) // 60) | int %}
                {% if hours >= 1 %}
                  {{ hours }} ч {{ minutes }} мин назад
                {% elif minutes >= 1 %}
                  {{ minutes }} мин назад
                {% else %}
                  Меньше минуты назад
                {% endif %}
              {% endif %}
            {% endif %}
          {% endif %}
        icon_template: mdi:clock

Ну и сама подпись, элемент типа state-label.

Скрытый текст
type: state-label
entity: sensor.time_since_camera2_cat_change
style:
  left: 74.8%
  top: 92%
  color: black
  font-weight: bold
  font-size: 12px
  background: white
  border-radius: 10px
  opacity: 40%

Управление роботом-пылесосом. Это то, на что я потратил больше всего времени. Сначала вешаем иконку в место расположения док-станции.

Скрытый текст
type: state-icon
entity: vacuum.roborock_m1s_6ec6_robot_cleaner
title: null
style:
  top: 53.32%
  left: 44.64%
  border-radius: 50%
  text-align: center
  background-color: rgba(255, 255, 255, 0.3)
  opacity: 100%
tap_action:
  action: more-info

Получаем по клику простенькую панель управления, где нет возможности указать, где убираться.

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

Для этого сначала отдельно настраиваем кастомную карточку пылесоса. У меня пылесос Dreame, к нему подошла интеграция Dreame vacuum от Tasshack.

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

После того, как интеграция сгенерировала конфиг карточки, а мы его поправили как нам надо, можно переносить этот код на наш план дома в элемент типа conditional, внутри которого сидит элемент типа custom с нашей карточкой. Код получается огромный — зато можно все настроить.

Скрытый текст
type: conditional
conditions:
  - condition: state
    entity: input_boolean.vacuum_map_show
    state: "on"
elements:
  - type: custom:xiaomi-vacuum-map-card
    map_source:
      camera: camera.x40_ultra_complete_map
    calibration_source:
      camera: true
    entity: vacuum.x40_ultra_complete
    vacuum_platform: Tasshack/dreame-vacuum
    icons:
      - icon: mdi:play
        conditions:
          - entity: vacuum.x40_ultra_complete
            value_not: cleaning
          - entity: vacuum.x40_ultra_complete
            value_not: error
          - entity: vacuum.x40_ultra_complete
            value_not: returning
        tooltip: Start
        tap_action:
          action: call-service
          service: vacuum.start
          service_data:
            entity_id: vacuum.x40_ultra_complete
      - icon: mdi:pause
        conditions:
          - entity: vacuum.x40_ultra_complete
            value_not: docked
          - entity: vacuum.x40_ultra_complete
            value_not: idle
          - entity: vacuum.x40_ultra_complete
            value_not: error
          - entity: vacuum.x40_ultra_complete
            value_not: paused
        tooltip: Pause
        tap_action:
          action: call-service
          service: vacuum.pause
          service_data:
            entity_id: vacuum.x40_ultra_complete
      - icon: mdi:stop
        conditions:
          - entity: vacuum.x40_ultra_complete
            value_not: docked
          - entity: vacuum.x40_ultra_complete
            value_not: idle
          - entity: vacuum.x40_ultra_complete
            value_not: error
          - entity: vacuum.x40_ultra_complete
            value_not: paused
        tooltip: Stop
        tap_action:
          action: call-service
          service: vacuum.stop
          service_data:
            entity_id: vacuum.x40_ultra_complete
      - icon: mdi:home-map-marker
        conditions:
          - entity: vacuum.x40_ultra_complete
            value_not: docked
          - entity: vacuum.x40_ultra_complete
            value_not: returning
        tooltip: Return to base
        tap_action:
          action: call-service
          service: vacuum.return_to_base
          service_data:
            entity_id: vacuum.x40_ultra_complete
      - icon: mdi:map-marker
        tooltip: Locate
        tap_action:
          action: call-service
          service: vacuum.locate
          service_data:
            entity_id: vacuum.x40_ultra_complete
      - menu_id: cleaning_mode
        icon: mdi:broom
        tooltip: Cleaning mode
        label: Сухая
        conditions:
          - entity: vacuum.x40_ultra_complete
            attribute: cleaning_mode
            value: Sweeping
        entity: select.x40_ultra_complete_cleaning_mode
        available_values_attribute: options
        icon_mapping:
          sweeping: mdi:broom
          mopping: mdi:water-opacity
          sweeping_and_mopping: mdi:hydro-power
          mopping_after_sweeping: mdi:water-polo
        tap_action:
          action: call-service
          service: select.select_option
          service_data:
            option: sweeping
            entity_id: select.x40_ultra_complete_cleaning_mode
      - menu_id: cleaning_mode
        icon: mdi:water-opacity
        tooltip: Cleaning mode
        label: Влажная
        conditions:
          - entity: vacuum.x40_ultra_complete
            attribute: cleaning_mode
            value: Mopping
        entity: select.x40_ultra_complete_cleaning_mode
        available_values_attribute: options
        icon_mapping:
          sweeping: mdi:broom
          mopping: mdi:water-opacity
          sweeping_and_mopping: mdi:hydro-power
          mopping_after_sweeping: mdi:water-polo
        tap_action:
          action: call-service
          service: select.select_option
          service_data:
            option: mopping
            entity_id: select.x40_ultra_complete_cleaning_mode
      - menu_id: cleaning_mode
        icon: mdi:hydro-power
        tooltip: Cleaning mode
        label: Сухая и влажная
        conditions:
          - entity: vacuum.x40_ultra_complete
            attribute: cleaning_mode
            value: Sweeping and mopping
        entity: select.x40_ultra_complete_cleaning_mode
        available_values_attribute: options
        icon_mapping:
          sweeping: mdi:broom
          mopping: mdi:water-opacity
          sweeping_and_mopping: mdi:hydro-power
          mopping_after_sweeping: mdi:water-polo
        tap_action:
          action: call-service
          service: select.select_option
          service_data:
            option: sweeping_and_mopping
            entity_id: select.x40_ultra_complete_cleaning_mode
      - menu_id: cleaning_mode
        icon: mdi:water-polo
        tooltip: Cleaning mode
        label: Влажная после сухой
        conditions:
          - entity: vacuum.x40_ultra_complete
            attribute: cleaning_mode
            value: Mopping after sweeping
        entity: select.x40_ultra_complete_cleaning_mode
        available_values_attribute: options
        icon_mapping:
          sweeping: mdi:broom
          mopping: mdi:water-opacity
          sweeping_and_mopping: mdi:hydro-power
          mopping_after_sweeping: mdi:water-polo
        tap_action:
          action: call-service
          service: select.select_option
          service_data:
            option: mopping_after_sweeping
            entity_id: select.x40_ultra_complete_cleaning_mode
      - menu_id: fan_speed
        icon: mdi:fan-remove
        label: Тихая
        conditions:
          - entity: vacuum.x40_ultra_complete
            attribute: fan_speed
            value: Silent
        tooltip: Change fan speed
        tap_action:
          action: call-service
          service: vacuum.set_fan_speed
          service_data:
            entity_id: vacuum.x40_ultra_complete
            fan_speed: Silent
      - menu_id: fan_speed
        icon: mdi:fan-speed-1
        label: Обычная
        conditions:
          - entity: vacuum.x40_ultra_complete
            attribute: fan_speed
            value: Standard
        tooltip: Change fan speed
        tap_action:
          action: call-service
          service: vacuum.set_fan_speed
          service_data:
            entity_id: vacuum.x40_ultra_complete
            fan_speed: Standard
      - menu_id: fan_speed
        icon: mdi:fan-speed-2
        label: Мощная
        conditions:
          - entity: vacuum.x40_ultra_complete
            attribute: fan_speed
            value: Strong
        tooltip: Change fan speed
        tap_action:
          action: call-service
          service: vacuum.set_fan_speed
          service_data:
            entity_id: vacuum.x40_ultra_complete
            fan_speed: Strong
      - menu_id: fan_speed
        icon: mdi:fan-speed-3
        label: Турбо
        conditions:
          - entity: vacuum.x40_ultra_complete
            attribute: fan_speed
            value: Turbo
        tooltip: Change fan speed
        tap_action:
          action: call-service
          service: vacuum.set_fan_speed
          service_data:
            entity_id: vacuum.x40_ultra_complete
            fan_speed: Turbo
      - icon: mdi:fan-alert
        conditions:
          - entity: vacuum.x40_ultra_complete
            attribute: fan_speed
            value_not: Silent
          - entity: vacuum.x40_ultra_complete
            attribute: fan_speed
            value_not: Standard
          - entity: vacuum.x40_ultra_complete
            attribute: fan_speed
            value_not: Strong
          - entity: vacuum.x40_ultra_complete
            attribute: fan_speed
            value_not: Turbo
        tooltip: Change fan speed
        tap_action:
          action: call-service
          service: vacuum.set_fan_speed
          service_data:
            entity_id: vacuum.x40_ultra_complete
            fan_speed: Silent
    tiles:
      - tile_id: status
        entity: vacuum.x40_ultra_complete
        label: Чем занят
        attribute: status
        icon: mdi:robot-vacuum
        translations:
          sleeping: Спит
          starting: Поехал
          charger disconnected: Нет питания
          idle: Ждет
          remote control active: Удаленное управление
          cleaning: Убирается
          returning home: Возвращается
          manual mode: Ручной режим
          charging: Заряжается
          charging problem: Проблема с зарядкой
          paused: На паузе
          spot cleaning: Чистит точку
          error: Ошибка
          shutting down: Выключается
          updating: Обновляет прошивку
          docking: Заходит в базу
          going to target: Идет на цель
          zoned cleaning: Зональная уборка
          segment cleaning: Уборка сегмента
          emptying the bin: Выгружает пыль
          charging complete: Зарядка завершена
          device offline: Не в сети
      - tile_id: battery_level
        entity: vacuum.x40_ultra_complete
        label: Заряд
        attribute: battery_level
        icon_source: vacuum.x40_ultra_complete.attributes.battery_icon
        unit: "%"
      - tile_id: fan_speed
        entity: vacuum.x40_ultra_complete
        label: Мощность
        attribute: fan_speed
        icon: mdi:fan
        translations:
          silent: Тихая
          standard: Обычная
          medium: Средняя
          strong: Мощная
          turbo: Турбо
          auto: Auto
          gentle: Нежная
      - tile_id: mop_pad_humidity
        attribute: mop_pad_humidity
        label: Швабры
        icon: mdi:water-percent
        entity: vacuum.x40_ultra_complete
        precision: 0
        translations:
          wet: Мокрые
          dry: Сухие
    map_modes:
      - name: Зональная уборка
        icon: mdi:select-drag
        run_immediately: false
        coordinates_rounding: true
        coordinates_to_meters_divider: 1000
        selection_type: MANUAL_RECTANGLE
        max_selections: 20
        repeats_type: EXTERNAL
        max_repeats: 3
        service_call_schema:
          service: dreame_vacuum.vacuum_clean_zone
          service_data:
            zone: "[[selection]]"
            repeats: "[[repeats]]"
            entity_id: "[[entity_id]]"
        predefined_selections: []
        variables: {}
      - name: Уборка на точке
        icon: mdi:map-marker-plus
        run_immediately: false
        coordinates_rounding: true
        coordinates_to_meters_divider: 1000
        selection_type: MANUAL_POINT
        max_selections: 999
        repeats_type: EXTERNAL
        max_repeats: 3
        service_call_schema:
          service: dreame_vacuum.vacuum_clean_spot
          service_data:
            points: "[[selection]]"
            repeats: "[[repeats]]"
            entity_id: "[[entity_id]]"
        predefined_selections: []
        variables: {}
      - name: Передвинуть робота
        icon: mdi:map-marker-radius
        run_immediately: false
        coordinates_rounding: true
        coordinates_to_meters_divider: 1000
        selection_type: MANUAL_POINT
        max_selections: 1
        repeats_type: NONE
        max_repeats: 1
        service_call_schema:
          service: dreame_vacuum.vacuum_goto
          service_data:
            x: "[[point_x]]"
            "y": "[[point_y]]"
            entity_id: "[[entity_id]]"
        predefined_selections: []
        variables: {}
      - template: vacuum_clean_segment
        predefined_selections:
          - id: "1"
            icon:
              name: mdi:bookshelf
              x: -150
              "y": 8350
            label:
              text: Кабинет
              x: -150
              "y": 8350
              offset_y: 35
            outline:
              - - -1750
                - 6850
              - - 1750
                - 6850
              - - 1750
                - 9850
              - - -1750
                - 9850
          - id: "2"
            icon:
              name: mdi:bed-king-outline
              x: 3150
              "y": 4600
            label:
              text: Спальня
              x: 3150
              "y": 4600
              offset_y: 35
            outline:
              - - 1700
                - 2400
              - - 4700
                - 2400
              - - 4700
                - 6600
              - - 1700
                - 6600
          - id: "3"
            icon:
              name: mdi:toilet
              x: 2950
              "y": 1750
            label:
              text: Малый санузел
              x: 2950
              "y": 1750
              offset_y: 35
            outline:
              - - 1750
                - 1100
              - - 4650
                - 1100
              - - 4650
                - 2400
              - - 1750
                - 2400
          - id: "4"
            icon:
              name: mdi:foot-print
              x: -150
              "y": 4400
            label:
              text: Холл
              x: -150
              "y": 4400
              offset_y: 35
            outline:
              - - -2900
                - 1050
              - - 1700
                - 1050
              - - 1700
                - 6850
              - - -2900
                - 6850
          - id: "5"
            icon:
              name: mdi:foot-print
              x: -3950
              "y": 2150
            label:
              text: Прихожая
              x: -3950
              "y": 2150
              offset_y: 35
            outline:
              - - -4750
                - 150
              - - -2900
                - 150
              - - -2900
                - 4050
              - - -4750
                - 4050
          - id: "6"
            icon:
              name: mdi:sofa-outline
              x: -1350
              "y": -1700
            label:
              text: Гостиная
              x: -1350
              "y": -1700
              offset_y: 35
            outline:
              - - -2700
                - -4200
              - - 900
                - -4200
              - - 900
                - 1100
              - - -2700
                - 1100
          - id: "7"
            icon:
              name: mdi:toilet
              x: -1500
              "y": 5200
            label:
              text: Санузел
              x: -1500
              "y": 5200
              offset_y: 35
            outline:
              - - -2750
                - 3800
              - - -600
                - 3800
              - - -600
                - 6550
              - - -2750
                - 6550
          - id: "8"
            icon:
              name: mdi:archive-outline
              x: 950
              "y": 2900
            label:
              text: Кладовка
              x: 950
              "y": 2900
              offset_y: 35
            outline:
              - - 600
                - 1700
              - - 1350
                - 1700
              - - 1350
                - 4150
              - - 600
                - 4150
          - id: "9"
            icon:
              name: mdi:chef-hat
              x: 2750
              "y": -1900
            label:
              text: Кухня
              x: 2750
              "y": -1900
              offset_y: 35
            outline:
              - - 900
                - -4600
              - - 4750
                - -4600
              - - 4750
                - 750
              - - 900
                - 750
    additional_presets: []
    style:
      left: 36%
      top: 48%
      width: 450px
      border-radius: 10px
      border: 2px solid black
      text-align: center
      opacity: 95%

Теперь следующий этап: хочется, чтобы иконка робота не просто сидела на месте док-станции, а перемещалась в соответствии с местоположением самого робота.

Иконку с переменными координатами панель Picture-Elements сделать так просто мне не дала, пришлось снова обратиться к нейросети, которая посоветовала использовать card-mod. Это кастомная UI-интеграция, которая позволяет использовать CSS-стили внутри карточек Home Assistant.

Скрытый текст
type: state-icon
entity: input_boolean.vacuum_map_show
tap_action:
  action: toggle
icon: mdi:robot-vacuum
card_mod:
  style: >

    {% set vac = state_attr('camera.x40_ultra_complete_map','vacuum_position')
    %}

    {% set raw_angle = vac.a | float(0) %}

    {% set offset = 180 %}

    {% set angle = ((offset - raw_angle) % 360) %}

    :host {
      position: absolute;
      left: {{ states('sensor.vacuum_left_px_new') }}px;
      top: {{ states('sensor.vacuum_top_px_new') }}px;

      width: 25px;
      height: 25px;
      padding: 3px;
      box-sizing: border-box;

      background-color: rgba(255,255,255,0.9);
      border: 1px solid #e0e0e0;
      border-radius: 50%;
      box-shadow: 0 1px 4px rgba(0,0,0,0.25);

      display: flex;
      align-items: center;
      justify-content: center;
      transform: rotate({{ angle }}deg) !important;
      transform-origin: center center !important;
      z-index: initial !important;
    }

    ha-icon {
      color: #000;
      width: 20px;
      height: 20px;
      margin: 0;
      transform: rotate({{ angle }}deg) !important;
      transform-origin: center center !important;

Основная сложность в преобразовании координат робота на плоскости его карты в координаты на плане дома, который не плоский, а имеет перспективу. Эту задачу я пока решил не до конца — в некоторых комнатах иконка пока еще заметно смещается от реального местоположения робота. Я разбил карту на 25 квадратов, вручную погонял робота по этажу, и составил таблицу из 40 калибровочных точек, где записал соответствие координат робота на его карте и координат на моем плане. Затем скормил эту таблицу Gemini 2.5, и попросил просчитать коэффициенты формул для билинейной интерполяции координат.

Скрытый текст

gemini-2.5-pro-exp

17.04.2025 at 21:52

Здравствуйте! Понимаю вашу задачу: нужно отобразить положение робота-пылесоса (vacuum_position из camera.x40_ultra_complete_map) на плане этажа в Home Assistant, учитывая разницу в системах координат, ориентации и перспективное искажение плана. У вас есть набор калибровочных точек, связывающих координаты робота и пиксельные координаты на плане.

Ваша идея с разделением плана на сетку 5x5 и расчетом локальных коэффициентов для каждого квадрата — это правильный подход для учета нелинейных искажений, таких как перспектива. Однако реализация такого сложного алгоритма интерполяции (например, билинейной или полиномиальной для каждого квадрата сетки) непосредственно в шаблоне сенсора Jinja2 (configuration.yaml) будет чрезвычайно сложной или даже невозможной.

Сложно, очень сложно. Пришлось поуговаривать, чтобы получить код для датчиков, преобразующих координаты (вставляется в configuration.yaml).

Скрытый текст

Не, ну должно же как-то проще решаться!

template:
  - sensor:
      - name: "Vacuum Left Px New"
        unit_of_measurement: "px"
        state: >
          {% set vac = state_attr('camera.x40_ultra_complete_map','vacuum_position') %}
          {% if vac and vac.x is number and vac.y is number %}
            {% set x = vac.x %}
            {% set y = vac.y %}

            {# Параметры сетки #}
            {% set x_min = -4579.0 %}
            {% set y_min = -3992.0 %}
            {% set dx = 1809.2 %}
            {% set dy = 2721.0 %}

            {# Коэффициенты преобразования для каждой ячейки (i,j) #}
            {% set T = {
              '0,0': {'a11': -0.009983, 'a12': -0.066817, 'b1': 832.657804, 'a21': -0.078867, 'a22': -0.007117, 'b2': 412.183573},
              '0,1': {'a11': 0.019068,  'a12': -0.140582, 'b1': 990.284377, 'a21': -0.060169, 'a22': -0.054593, 'b2': 513.633816},
              '0,2': {'a11': -0.002175, 'a12': -0.065921, 'b1': 874.062874, 'a21': -0.073109, 'a22': 0.006211,  'b2': 413.315245},
              '0,3': {'a11': 0.002615,  'a12': -0.060233, 'b1': 870.560191, 'a21': -0.052611, 'a22': -0.000162, 'b2': 500.394952},
              '0,4': {'a11': 0.012255, 'a12': -0.059423, 'b1': 884.735305, 'a21': -0.073910, 'a22': -0.001304, 'b2': 464.928882},
              '1,0': {'a11': -0.003491, 'a12': -0.056227, 'b1': 890.937886, 'a21': -0.050846, 'a22': -0.000977, 'b2': 505.763572},
              '1,1': {'a11': 0.015411,  'a12': -0.088180, 'b1': 952.763568, 'a21': -0.048956, 'a22': -0.005007, 'b2': 512.407395},
              '1,2': {'a11': 0.000446,  'a12': -0.057291, 'b1': 854.350294, 'a21': -0.046248, 'a22': -0.002631, 'b2': 522.396612},
              '1,3': {'a11': -0.009347, 'a12': -0.061078, 'b1': 851.945874, 'a21': -0.081280, 'a22': -0.002187, 'b2': 455.781485},
              '1,4': {'a11': 0.012255,  'a12': -0.059423, 'b1': 884.735305, 'a21': -0.073910, 'a22': -0.001304, 'b2': 464.928882},
              '2,0': {'a11': -0.005939, 'a12': -0.061828, 'b1': 871.041469, 'a21': -0.054300, 'a22': 0.005926,  'b2': 536.218771},
              '2,1': {'a11': 0.006405,  'a12': -0.013656, 'b1': 865.722704, 'a21': -0.038146, 'a22': 0.004478,  'b2': 523.389948},
              '2,2': {'a11': -0.027127, 'a12': -0.049086, 'b1': 798.194359, 'a21': -0.071610, 'a22': 0.001844,  'b2': 479.471436},
              '2,3': {'a11': 0.000527,  'a12': -0.051492, 'b1': 820.432526, 'a21': -0.078333, 'a22': -0.003569, 'b2': 510.680259},
              '2,4': {'a11': 0.017637, 'a12': -0.063504, 'b1': 894.109592, 'a21': -0.049961, 'a22': -0.003430, 'b2': 495.854427},
              '3,0': {'a11': -0.002444, 'a12': -0.069982, 'b1': 835.602179, 'a21': -0.065399, 'a22': 0.031824,  'b2': 648.769444},
              '3,1': {'a11': -0.010379, 'a12': -0.038164, 'b1': 876.685108, 'a21': -0.063546, 'a22': -0.032611, 'b2': 539.979253},
              '3,2': {'a11': -0.028113, 'a12': -0.117443, 'b1': 1088.703, 'a21': -0.043728, 'a22': -0.018741, 'b2': 429.021},
              '3,3': {'a11': 0.008957,  'a12': -0.031518, 'b1': 680.888,  'a21': -0.064406, 'a22': 0.005359,  'b2': 457.586},
              '3,4': {'a11': -0.038366, 'a12': -0.073320, 'b1':1032.926937,'a21': -0.078527,'a22': -0.008437,'b2':566.662052},
              '4,0': {'a11': 0.000319,  'a12': -0.060475, 'b1': 860.744277, 'a21': -0.074903, 'a22': -0.000886,'b2':562.266867},
              '4,1': {'a11': -0.000857, 'a12': -0.049575,'b1':865.376888,'a21': -0.044549,'a22': -0.003059,'b2':442.610229},
              '4,2': {'a11': 0.016497,  'a12': -0.004794, 'b1': 1008.000, 'a21': -0.068834, 'a22': 0.020000,  'b2': 617.500},
              '4,3': {'a11': 0.004780,  'a12': -0.032272, 'b1': 687.222,  'a21': -0.051716, 'a22': 0.017072,  'b2': 351.838},
              '4,4': {'a11': 0.004780,  'a12': -0.032272,'b1':687.222253,'a21': -0.051716,'a22': 0.017072,'b2':351.838141}
            } %}

            {# Вычисляем индексы ячеек и веса #}
            {% set i0 = ((x - x_min) / dx) | int %}
            {% if i0 < 0 %}{% set i0 = 0 %}{% endif %}
            {% if i0 > 4 %}{% set i0 = 4 %}{% endif %}
            {% set i1 = i0 + 1 if i0 < 4 else 4 %}
            {% set j0 = ((y - y_min) / dy) | int %}
            {% if j0 < 0 %}{% set j0 = 0 %}{% endif %}
            {% if j0 > 4 %}{% set j0 = 4 %}{% endif %}
            {% set j1 = j0 + 1 if j0 < 4 else 4 %}

            {% set x0 = x_min + dx * i0 %}
            {% set x1 = x_min + dx * i1 %}
            {% set y0 = y_min + dy * j0 %}
            {% set y1 = y_min + dy * j1 %}
            {% set alpha = 0 if x1 == x0 else ((x - x0) / (x1 - x0)) %}
            {% set beta = 0 if y1 == y0 else ((y - y0) / (y1 - y0)) %}

            {# Применяем 4 аффинных преобразования #}
            {% set k00 = i0 ~ ',' ~ j0 %}
            {% set k10 = i1 ~ ',' ~ j0 %}
            {% set k01 = i0 ~ ',' ~ j1 %}
            {% set k11 = i1 ~ ',' ~ j1 %}
            {% set t00 = T[k00] %}{% set t10 = T[k10] %}
            {% set t01 = T[k01] %}{% set t11 = T[k11] %}
            {% set px00 = t00.a11 * x + t00.a12 * y + t00.b1 %}
            {% set px10 = t10.a11 * x + t10.a12 * y + t10.b1 %}
            {% set px01 = t01.a11 * x + t01.a12 * y + t01.b1 %}
            {% set px11 = t11.a11 * x + t11.a12 * y + t11.b1 %}

            {# Билинейная интерполяция #}
            {% set pred_x = (1 - alpha) * (1 - beta) * px00 + alpha * (1 - beta) * px10 + (1 - alpha) * beta * px01 + alpha * beta * px11 %}
            {{ pred_x | round(1) }}
          {% else %}
            0
          {% endif %}
  
  - sensor:
      - name: "Vacuum Top Px New"
        unit_of_measurement: "px"
        state: >
          {% set vac = state_attr('camera.x40_ultra_complete_map','vacuum_position') %}
          {% if vac and vac.x is number and vac.y is number %}
            {% set x = vac.x %}
            {% set y = vac.y %}
            {% set x_min = -4579.0 %}
            {% set y_min = -3992.0 %}
            {% set dx = 1809.2 %}
            {% set dy = 2721.0 %}
            {% set T = {
              '0,0': {'a11': -0.009983, 'a12': -0.066817, 'b1': 832.657804, 'a21': -0.078867, 'a22': -0.007117, 'b2': 412.183573},
              '0,1': {'a11': 0.019068,  'a12': -0.140582, 'b1': 990.284377, 'a21': -0.060169, 'a22': -0.054593, 'b2': 513.633816},
              '0,2': {'a11': -0.002175, 'a12': -0.065921, 'b1': 874.062874, 'a21': -0.073109, 'a22': 0.006211,  'b2': 413.315245},
              '0,3': {'a11': 0.002615,  'a12': -0.060233, 'b1': 870.560191, 'a21': -0.052611, 'a22': -0.000162, 'b2': 500.394952},
              '0,4': {'a11': 0.012255, 'a12': -0.059423, 'b1': 884.735305, 'a21': -0.073910, 'a22': -0.001304, 'b2': 464.928882},
              '1,0': {'a11': -0.003491, 'a12': -0.056227, 'b1': 890.937886, 'a21': -0.050846, 'a22': -0.000977, 'b2': 505.763572},
              '1,1': {'a11': 0.015411,  'a12': -0.088180, 'b1': 952.763568, 'a21': -0.048956, 'a22': -0.005007, 'b2': 512.407395},
              '1,2': {'a11': 0.000446,  'a12': -0.057291, 'b1': 854.350294, 'a21': -0.046248, 'a22': -0.002631, 'b2': 522.396612},
              '1,3': {'a11': -0.009347, 'a12': -0.061078, 'b1': 851.945874, 'a21': -0.081280, 'a22': -0.002187, 'b2': 455.781485},
              '1,4': {'a11': 0.012255,  'a12': -0.059423, 'b1': 884.735305, 'a21': -0.073910, 'a22': -0.001304, 'b2': 464.928882},
              '2,0': {'a11': -0.005939, 'a12': -0.061828, 'b1': 871.041469, 'a21': -0.054300, 'a22': 0.005926,  'b2': 536.218771},
              '2,1': {'a11': 0.006405,  'a12': -0.013656, 'b1': 865.722704, 'a21': -0.038146, 'a22': 0.004478,  'b2': 523.389948},
              '2,2': {'a11': -0.027127, 'a12': -0.049086, 'b1': 798.194359, 'a21': -0.071610, 'a22': 0.001844,  'b2': 479.471436},
              '2,3': {'a11': 0.000527,  'a12': -0.051492, 'b1': 820.432526, 'a21': -0.078333, 'a22': -0.003569, 'b2': 510.680259},
              '2,4': {'a11': 0.017637, 'a12': -0.063504, 'b1': 894.109592, 'a21': -0.049961, 'a22': -0.003430, 'b2': 495.854427},
              '3,0': {'a11': -0.002444, 'a12': -0.069982, 'b1': 835.602179, 'a21': -0.065399, 'a22': 0.031824,  'b2': 648.769444},
              '3,1': {'a11': -0.010379, 'a12': -0.038164, 'b1': 876.685108, 'a21': -0.063546, 'a22': -0.032611, 'b2': 539.979253},
              '3,2': {'a11': -0.028113, 'a12': -0.117443, 'b1': 1088.703, 'a21': -0.043728, 'a22': -0.018741, 'b2': 429.021},
              '3,3': {'a11': 0.008957,  'a12': -0.031518, 'b1': 680.888,  'a21': -0.064406, 'a22': 0.005359,  'b2': 457.586},
              '3,4': {'a11': -0.038366, 'a12': -0.073320, 'b1':1032.926937,'a21': -0.078527,'a22': -0.008437,'b2':566.662052},
              '4,0': {'a11': 0.000319,  'a12': -0.060475, 'b1': 860.744277, 'a21': -0.074903, 'a22': -0.000886,'b2':562.266867},
              '4,1': {'a11': -0.000857, 'a12': -0.049575,'b1':865.376888,'a21': -0.044549,'a22': -0.003059,'b2':442.610229},
              '4,2': {'a11': 0.016497,  'a12': -0.004794, 'b1': 1008.000, 'a21': -0.068834, 'a22': 0.020000,  'b2': 617.500},
              '4,3': {'a11': 0.004780,  'a12': -0.032272, 'b1': 687.222,  'a21': -0.051716, 'a22': 0.017072,  'b2': 351.838},
              '4,4': {'a11': 0.004780,  'a12': -0.032272,'b1':687.222253,'a21': -0.051716,'a22': 0.017072,'b2':351.838141}
            } %}
            {# Расчёт индексов #}
            {% set raw_i = (x - x_min) / dx %}
            {% set i0 = 0 if raw_i < 0 else (4 if raw_i > 4 else raw_i|int) %}
            {% set raw_j = (y - y_min) / dy %}
            {% set j0 = 0 if raw_j < 0 else (4 if raw_j > 4 else raw_j|int) %}
            {% set i1 = i0 + 1 if i0 < 4 else 4 %}
            {% set j1 = j0 + 1 if j0 < 4 else 4 %}
            {% set alpha = (x - (x_min + i0*dx)) / dx %}
            {% set beta  = (y - (y_min + j0*dy)) / dy %}
            {% set k00 = i0|string + ',' + j0|string %}
            {% set k10 = i1|string + ',' + j0|string %}
            {% set k01 = i0|string + ',' + j1|string %}
            {% set k11 = i1|string + ',' + j1|string %}
            {% set P00y = (T[k00]['a21'] * x) + (T[k00]['a22'] * y) + T[k00]['b2'] %}
            {% set P10y = (T[k10]['a21'] * x) + (T[k10]['a22'] * y) + T[k10]['b2'] %}
            {% set P01y = (T[k01]['a21'] * x) + (T[k01]['a22'] * y) + T[k01]['b2'] %}
            {% set P11y = (T[k11]['a21'] * x) + (T[k11]['a22'] * y) + T[k11]['b2'] %}
            {% set pred_y = (1 - alpha)*(1 - beta)*P00y + alpha*(1 - beta)*P10y + (1 - alpha)*beta*P01y + alpha*beta*P11y %}
            {{ pred_y | round(1) }}
          {% else %}
            0
          {% endif %}

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

Итог

Ну, работает. Удобно. Каждый день используем. Ваять все это было интересно, хотя меня не отпускало чувство, что я изобретаю велосипед, и где-то есть подробные гайды, по которым все это можно сделать быстро и просто. Ткните меня в нее носом, если есть. А если не было, то теперь есть мой.

Очень рассчитываю на советы, идеи, а также на пинки за костыльные и неоптимальные решения.

Комментарии (11)


  1. Zara6502
    18.06.2025 12:02

    я сейчас на свидетелей умного дома смотрю так же как на меня в 1995 году родственники смотрели сидящего днями и ночами за ПК, самые популярные слова "главное что не наркоманит" и "главное что по улице не шляется".

    в этом во всем есть один момент - когда у тебя этого нет, то невозможно понять зачем оно всё нужно, а когда оно у тебя есть, ты не понимаешь как ты мог раньше жить без этого.

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


    1. Lynx-eyed Автор
      18.06.2025 12:02

      Как хобби не хуже любого другого, я считаю.


      1. Zara6502
        18.06.2025 12:02

        а в ряде случаев полезнее для окружающих


    1. xSVPx
      18.06.2025 12:02

      Зачем в коридоре монитор действительно понятно не очень. В целом умный дом должен работать "сам". Т.е. не требовать бдить и управлять, тогда это удобно.

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

      Т.е. сценарии, где прям можно удобнее сделать есть. Но да - требует некоторых усилий.


      1. Lynx-eyed Автор
        18.06.2025 12:02

        Дом и работает сам, панель управления лишь дополняет автоматизации и другие каналы управления (голос, телеграм, кнопки).


      1. vbifkol
        18.06.2025 12:02

        Зачем в коридоре монитор действительно понятно не очень. В целом умный дом должен работать "сам". Т.е. не требовать бдить и управлять, тогда это удобно.

        В статье же есть об этом - возможность получить визуально быстро нужную инфу. На примере цветка. Мне вот еще может быть важно посмотреть текущее электропотребление и где включен свет перед выходом.


        1. xSVPx
          18.06.2025 12:02

          Зачем на свет смотреть ?

          Ну т.е. у себя я планирую когда-нибудь сделать сценарную кнопку "выход" по которой будет проводиться проверка всего что надо, выключаться свет итд итп. Но вешать монитор даже в голову не приходило. В телефон приложение поставить еще ладно...


      1. Zara6502
        18.06.2025 12:02

        я на озоне купил светильник за 150 руб с датчиком движения, подключил к нему старый повербанк который лежил без дела и теперь у меня в коридоре умнейший дом )))) когда я иду ночью в туалет он мне подсвечивает и обратно. Чтобы не вставать к светильнику общему я у изголовья сделал лампу и выключатель = 300 руб. Просто я читал много про умные дома и имхо я бы таким деньгам нашел лучшее применение. Просто люди которые никогда в жизни не испытывали проблем со здоровьем наивно полагают что у них всё будет круто, стабильно и доходно еще очень много лет, но как правило звиздец подкрадывается незаметно, рушатся семьи, страдают дети. Я сгущаю конечно, но не думать о таком наверное тоже преступно в каком-то смысле.


        1. xSVPx
          18.06.2025 12:02

          Причем тут здоровье то ?

          На дистанции умный дом выгоднее кучи автономных светильников. И сложную логику можно сделать только на нём. А кое-где эта логика в целом не лишняя. К примеру, у меня автомат включает на 2 минуты свет, а кнопка на 10 минут, чтобы руками не махать если что-то надо в коридоре сделать. Сможете так с своей лампой :)? А еще у меня есть кнопка включающая свет в коридоре из спальни, чтобы в темеоте не красться...

          Закроются ли ваши краны холодной и горячей воды, если открыть воду на полчасика ? Получите ли вы уведомление хотя бы об этом ? А если намочить датчики протечки (они ведь у вас есть)?

          У меня в ванной осушитель, и в автоматическом режиме он работает не слишком хорошо, потому как датчик должен быть не в осушителе :). Какую такую "лампу" мне купить для решения этой проблемы :)? Итд итп.

          Да даже свет. Вы врубаете ночью "на полную"? Зачем ? Можно ведь в щависимости от времени суток по ночам включать на чуть-чуть, чтобы он не мешал остальным домашним.

          Итд итп.

          Но в одном вы правы - это всё, как и любое удобство в целом лакшери. Т.е. не необходимо.


  1. vbifkol
    18.06.2025 12:02

    А что за лора шлюз в гпс-трекер?


    1. Lynx-eyed Автор
      18.06.2025 12:02

      Мне нужно было, чтобы ворота открывались заранее, когда я приезжаю домой, и закрывались, когда уезжаю. При этом не хотелось платить опсосу за связь. Решение - в машине GPS-трекер с модемом LoRa, а в доме на чердаке стоит шлюз LoRa-WiFi. Связь на 800 МГц, бьет где-то до километра, более чем хватает. Дом раз в 30 секунд получает MQTT-сообщение с координатами, рассчитывает расстояние до машины и решает, открывать ворота, закрывать, или ничего не делать.