Я сделал тачпанель управления умным домом чтобы вам тоже пришлось

от автора

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

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

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

Зачем

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

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

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

Железо

Подробно расписывать, что такое умный дом, и как он устроен конкретно у меня, смысла не вижу, статей об этом полно. Мой основан на 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). Если вы потом будете вносить изменения в план (а вы будете), малейший сдвиг — и придется перерендерить все варианты плана. Поэтому перед каждым рендерингом восстанавливаем сохраненный вид.

Когда все готово, рендерим тестовую картинку. Только так вы сможете увидеть финальный вид вашего плана, с освещением и тенями. Вам точно придется делать это несколько раз, чтобы пофиксить все косяки и подобрать подходящий размер картинки в пикселях. Я в итоге остановился на 1458×1010, что для 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, учитывая разницу в системах координат, ориентации и перспективное искажение плана. У вас есть набор калибровочных точек, связывающих координаты робота и пиксельные координаты на плане.

Ваша идея с разделением плана на сетку 5×5 и расчетом локальных коэффициентов для каждого квадрата — это правильный подход для учета нелинейных искажений, таких как перспектива. Однако реализация такого сложного алгоритма интерполяции (например, билинейной или полиномиальной для каждого квадрата сетки) непосредственно в шаблоне сенсора 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 %}  

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

Итог

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/919522/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *