Архив рубрики ~Лента новостей~

LLM смотрит на мой сад: как ИИ-зрение решает, звать ли меня накрыть мебель перед дождём

LLM смотрит на мой сад: как ИИ-зрение решает, звать ли меня накрыть мебель перед дождём

TL;DR

Я собрал в своём умном доме автоматизацию, которая:

  1. следит за прогнозом осадков (в том числе за краткосрочным nowcast’ом на ближайшие пару часов);

  2. когда дождь/снег на подходе — включает подсветку на уличной камере и делает снимок;

  3. отправляет снимок в мультимодальную LLM с вопросом «садовая мебель накрыта чехлами?»;

  4. если не накрыта — присылает мне в Telegram фото + анимированный радар осадков и две кнопки: «Накрыл» и «Отложить на 3 часа»;

  5. пока я не среагировал/не отложил — не спамит; на сухую погоду LLM не дёргается вовсе.

ИИ здесь на двух уровнях. Первый — LLM как слой зрения и рассуждения: принимает кадр с камеры и возвращает ответ по строгой схеме (covered / reason), без парсинга свободного текста. Второй — саму автоматизацию собрал ИИ-агент по SSH и REST/WebSocket API реального сервера (включение сущностей, config-flow, отладка граблей); человек — постановка задачи и приёмка.

Всё — на штатных механизмах платформы, без собственного компонента/интеграции: автоматизации, хелперы, встроенная LLM-интеграция.

1. Зачем это вообще

Классическая сцена: через полчаса дождь, а уличная мебель — диван, кресла, подушки — стоит без чехлов. Платформа умного дома технически «знает» и прогноз, и картинку с камеры. Но связать одно с другим и сделать вывод «пойди накрой» она сама не умеет: нет слоя, который понимает содержимое кадра и сопоставляет его с погодой. Если прозевал дождь, то будешь экстренно заносить в дом подушки и сушить их долго по углам.

Этот слой закрывает LLM: превращает кадр в вывод «мебель открыта / накрыта» с коротким обоснованием.

Само действие — надеть чехлы — делает человек: привода для этого нет, автоматизировать тут физически нечего. Задача системы не «сделать», а вовремя и по делу подсказать. Отсюда два требования:

  • Точность вместо спама. Не слать «накрой мебель» на каждый прогноз дождя. Сначала снимок и проверка через LLM — уведомление уходит, только если мебель действительно открыта.

  • Экономия вызовов. LLM дёргаем не по расписанию, а когда осадки реально близко (nowcast), и не чаще разумного (cooldown, snooze).

Контекст модели недоступен («сегодня чехлы сняли специально, потому что красили»), поэтому последнее слово — за человеком, одним тапом.

2. Архитектура

Весь конвейер — из штатных примитивов платформы:

┌─────────────────────────────────────────────────────────┐ │ Триггеры автоматизации │ │ • каждые 30 минут │ │ • nowcast осадков (2ч) поднялся выше 0 │ │ • состояние погоды сменилось на «дождь» │ └───────────────────────────┬─────────────────────────────┘ │ ┌──────────────────▼────────────────────┐ │ Условия (дёшево, без обращения к LLM):│ │ • не в режиме «отложено» (snooze) │ │ • дождь реально близко (nowcast/ │ │ прогноз/текущее состояние) │ │ • прошло > 3ч с прошлой реальной │ │ проверки (cooldown) │ └──────────────────┬────────────────────┘ │ (иначе — стоп, LLM не вызывается) ┌──────────────────▼───────────────────┐ │ 1. Включить подсветку камеры │ │ 2. Снять кадр во временный каталог │ │ 3. Выключить подсветку │ └──────────────────┬───────────────────┘ │ ┌──────────────────▼────────────────────┐ │ LLM-зрение (AI Task): │ │ вход: кадр + вопрос │ │ выход (structured): {covered, reason}│ └──────────────────┬────────────────────┘ │ covered == false? ┌──────────────────▼───────────────────┐ │ Telegram: фото + радар + кнопки │ │ [✅ Накрыл] [😴 Отложить 3ч] │ └──────────────────┬───────────────────┘ │ callback от кнопки ┌──────────────────▼───────────────────┐ │ Вторая автоматизация: │ │ выставить snooze_until, ответить │ │ на callback, отписать в чат │ └──────────────────────────────────────┘

Компоненты:

  • Хост. Старенький Raspberry Pi 3 (4 ядра ARM, ~1 ГБ RAM), платформа умного дома Home Assistant (HA) работает в Docker-контейнере. Каталог конфигурации примонтирован с хоста. Ресурсов в обрез — что позже и аукнется (см. раздел 8.7).

  • Камера. Уличная IP-камера с управляемой подсветкой/ИК. Важно: её функции (снимок, свет) проброшены в платформу как обычные сущности camera.* и light.* — поэтому автоматизация не завязана на конкретного вендора.

  • Погода. Интеграция, дающая не только прогноз, но и краткосрочный nowcast осадков (мм за ближайшие ~2 часа) и картинку-радар. Именно nowcast — лучший сигнал «дождь вот-вот».

  • LLM. Мультимодальная модель, подключённая к платформе через штатную интеграцию AI Task (об этом ниже).

  • Мессенджер. Telegram-бот в приватной группе, с inline-кнопками и обработкой callback’ов.

3. Почему без собственного компонента

Соблазн — написать свою интеграцию (и я даже начал, но потом решил, что нужно будет выложить в Open Source, а потом нести ответственность за проект — НЕТ). Но всё уже есть в платформе:

Шаг

Штатный механизм

Прогноз/nowcast

сервис получения прогноза + сущности-сенсоры осадков

Снимок

сервис camera.snapshot

Подсветка

light.turn_on / switch.turn_on (любая сущность на выбор)

LLM-зрение

AI Task — сервис ai_task.generate_data с вложениями и структурированным выводом

Уведомление

Telegram-бот: send_photo, send_animation, inline-клавиатура, answer_callback_query

Оркестрация и состояние

автоматизации + хелперы (input_datetime)

Свой код дал бы разве что более удобный UI настройки; функционально — ничего. Меньше кода — меньше поверхности для поддержки.

4. Ключевой кирпич: LLM-зрение через AI Task

Современные платформы умного дома (HA, например) умеют подключать LLM-провайдера (облачного или локального) как «AI Task»-сущность. Дальше в автоматизации доступен сервис, который принимает инструкцию + вложения (картинки) и возвращает структурированный ответ по заданной схеме.

Вызов (сокращённо, в терминах HA):

— service: ai_task.generate_data continue_on_error: true # деградируем мягко, если LLM недоступна data: entity_id: ai_task.<провайдер> task_name: garden_furniture_check instructions: >- Ты смотришь на снимок уличной террасы/сада. Скоро осадки. Определи, накрыта ли садовая мебель (диван, кресла, подушки) защитными чехлами. Если что-то открыто — covered=false. Если слишком темно или обзор закрыт — covered=false и объясни в reason. structure: # схема ответа = гарантированный формат covered: selector: boolean: {} description: true, если мебель защищена чехлами required: true reason: selector: text: {} description: одно короткое предложение с описанием required: true attachments: — media_content_id: media-source://media_source/local/weatherwatch_ai.jpg media_content_type: image/jpeg response_variable: result 5b2fc931a91271bde0a706acaada7d96

Два важных момента:

  1. Structured output. Мы не парсим свободный текст модели, а сразу получаем result.data.covered (bool) и result.data.reason (строка). Это резко упрощает логику ниже: covered != true — и всё.

  2. Вложение — только через media-source. Поле attachments принимает не путь к файлу, а идентификатор медиа-источника вида media-source://media_source/local/<файл>. А это значит, что снимок надо класть в каталог, который платформа считает медиа-источником (у меня — контейнерный /media), а не просто куда-то на диск.

Ответ на реальном кадре выглядел так:

{ «covered»: false, «reason»: «Часть мебели открыта и не накрыта чехлами.» }

5. Полная автоматизация проверки

Ниже — основная автоматизация целиком (идентификаторы сущностей обобщены, chat_id заменён плейсхолдером).

— id: weatherwatch_garden_furniture alias: WeatherWatch — проверка садовой мебели перед дождём mode: single max_exceeded: silent trigger: — platform: time_pattern # каждые 30 минут minutes: «/30» — platform: numeric_state # nowcast осадков поднялся выше 0 entity_id: sensor.precipitation_forecast_total above: 0 — platform: state # состояние стало «дождь» entity_id: weather.local to: rainy condition: # не в режиме «отложено» (Human in the Loop, см. ниже) — condition: template value_template: >- {{ as_timestamp(now()) > (state_attr(‘input_datetime.weatherwatch_snooze_until’,’timestamp’) | float(0)) }} action: — service: weather.get_forecasts continue_on_error: true target: { entity_id: weather.local } data: { type: daily } response_variable: fc — variables: bad_conditions: [rainy, pouring, snowy, snowy-rainy, hail, lightning-rainy] rain_soon: >- {{ states(‘weather.local’) in bad_conditions or (states(‘sensor.precipitation_forecast_total’) | float(0) > 0) or (fc is defined and (fc.get(‘weather.local’, {}).get(‘forecast’, [])[:2] | selectattr(‘condition’,’in’, bad_conditions) | list | count > 0)) }} # осадки действительно близко? иначе — стоп, LLM не трогаем (экономия) — condition: template value_template: «{{ rain_soon }}» # cooldown: не запускать проверку чаще раза в 3 часа — condition: template value_template: >- {{ as_timestamp(now()) — (state_attr(‘input_datetime.weatherwatch_last_check’,’timestamp’) | float(0)) > 10800 }} — service: input_datetime.set_datetime # фиксируем «реальную» проверку target: { entity_id: input_datetime.weatherwatch_last_check } data: { timestamp: «{{ as_timestamp(now()) }}» } # подсветка -> кадр -> подсветка выкл — service: light.turn_on target: { entity_id: light.garden_floodlight } — delay: «00:00:03» # дать сенсору камеры «привыкнуть» — service: camera.snapshot target: { entity_id: camera.garden } data: { filename: «/media/weatherwatch_ai.jpg» } — service: light.turn_off target: { entity_id: light.garden_floodlight } # LLM-зрение — service: ai_task.generate_data continue_on_error: true data: entity_id: ai_task.provider task_name: garden_furniture_check instructions: >- Ты смотришь на снимок сада/террасы. Скоро осадки. Накрыта ли садовая мебель защитными чехлами? Если что-то открыто — covered=false. Если темно/обзор закрыт — covered=false и объясни в reason. structure: covered: { selector: { boolean: {} }, required: true, description: «true, если мебель накрыта чехлами» } reason: { selector: { text: {} }, required: true, description: «короткое описание того, что видно» } attachments: — media_content_id: media-source://media_source/local/weatherwatch_ai.jpg media_content_type: image/jpeg response_variable: result — variables: covered: >- {{ result.data.covered if (result is defined and result.data is defined) else none }} reason: >- {{ result.data.reason if (result is defined and result.data is defined) else ‘Ожидается дождь, а проверка ИИ не сработала — проверьте вручную.’ }} # уведомляем только если НЕ накрыто — condition: template value_template: «{{ covered != true }}» — service: camera.snapshot # снимок радара во временный каталог target: { entity_id: camera.rain_radar } data: { filename: «/media/weatherwatch_map.gif» } — service: telegram_bot.send_photo data: target: !secret tg_chat_id file: «/media/weatherwatch_ai.jpg» caption: «Накройте садовую мебель — ожидается дождь. {{ reason }}» inline_keyboard: — «✅ Накрыл:/ww_covered, 😴 Отложить 3ч:/ww_snooze3h» — service: telegram_bot.send_animation data: target: !secret tg_chat_id file: «/media/weatherwatch_map.gif» caption: «Радар осадков — nowcast на 2ч: {{ states(‘sensor.precipitation_forecast_total’) }} мм»

6. Обратная связь: кнопки «Накрыл» / «Отложить»

Раз финальное решение остаётся за человеком, ему нужна максимально простая точка управления — прямо в уведомлении. Реализуется тремя вещами.

(1) Inline-кнопки на уведомлении. В сервисе отправки фото есть поле inline_keyboard, где кнопка задаётся строкой Текст:/callback_data:

inline_keyboard: — «✅ Накрыл:/ww_covered, 😴 Отложить 3ч:/ww_snooze3h»

(2) Обработчик callback’а — отдельная автоматизация. При нажатии платформа генерирует событие telegram_callback, где полезная нагрузка лежит в trigger.event.data.data:

— id: weatherwatch_snooze_callback alias: WeatherWatch — кнопки Накрыл/Отложить mode: queued max: 5 trigger: — platform: event event_type: telegram_callback condition: — condition: template value_template: «{{ trigger.event.data.data in [‘/ww_covered’, ‘/ww_snooze3h’] }}» action: — variables: is_snooze: «{{ trigger.event.data.data == ‘/ww_snooze3h’ }}» secs: «{{ 10800 if trigger.event.data.data == ‘/ww_snooze3h’ else 64800 }}» — service: input_datetime.set_datetime target: { entity_id: input_datetime.weatherwatch_snooze_until } data: { timestamp: «{{ as_timestamp(now()) + (secs | int) }}» } — service: telegram_bot.answer_callback_query # убрать «часики» на кнопке continue_on_error: true data: callback_query_id: «{{ trigger.event.data.id }}» message: >- {{ ‘Отложено на 3 часа’ if is_snooze else ‘Принято — до вечера не беспокою’ }} — service: telegram_bot.send_message continue_on_error: true data: target: !secret tg_chat_id message: >- {{ ‘😴 Отложено 3ч: ‘ if is_snooze else ‘✅ Отмечено как накрыто: ‘ }}{{ trigger.event.data.from_first }}

(3) Хелпер-состояние. Нажатие выставляет input_datetime.weatherwatch_snooze_until в «сейчас + 3ч» (отложить) или «сейчас + 18ч» (накрыл — до вечера). Основная автоматизация в первом же условии проверяет: если now < snooze_until, она вообще не запускается.

Нажатие выставляет snooze_until, основная автоматизация его учитывает. Без нажатия ничего необратимого не происходит — максимум повторное уведомление, ограниченное cooldown’ом.

7. Контроль стоимости — by design

Мультимодальные вызовы стоят денег, поэтому «дешевизна» зашита в структуру, а не в добрые намерения:

  1. Гейт по осадкам. Пока rain_soon ложно, автоматизация останавливается до снимка и LLM. В сухой день обращений к модели — ноль. Проверка каждые 30 минут — это всего лишь дешёвый рендер шаблона.

  2. Правильный cooldown. Даже во время дождя реальная проверка запускается не чаще раза в 3 часа.

  3. Snooze/ack. Ответ человека полностью выключает поток уведомлений на заданное время.

  4. Дедупликация действий. Никаких «повторить на всякий случай»: одна ситуация — одно уведомление.

8. Грабли

8.1. Вложение для LLM — только из media-source

ai_task не принимает произвольный путь к файлу: нужен media-source://…. Каталог, отдаваемый как «локальный» веб-контент, media-источником не является. Решение — писать снимок в каталог, зарегистрированный как медиа-директория, и ссылаться на него как media-source://media_source/local/<файл>.

8.2. Telegram и редиректы

Радар осадков доступен по URL, но этот URL отдаёт 301-редирект на CDN с меняющимся адресом. Загрузчик Telegram-интеграции редиректы не проходит и падает с Failed to load URL: 301. Обычный curl -L редирект проходит — отсюда и расхождение. Решение: не отдавать URL, а снять кадр камеры-радара в локальный файл и отправить его.

8.3. Анимированный GIF ≠ фото

Радар — это анимированный GIF. send_photo анимированные GIF не принимает. Нужен send_animation (или send_document) — тогда в чат уезжает «живой» радар, что даже нагляднее.

8.4. Отключённые по умолчанию сущности

Многие полезные сущности интеграции (камера-радар, сенсоры nowcast) по умолчанию выключены в реестре. Включить их пачкой без перезапуска можно через WebSocket-API реестра сущностей:

{ «type»: «config/entity_registry/update», «entity_id»: «sensor.precipitation_forecast_total», «disabled_by»: null }

После этого — «мягкий» reload соответствующей записи конфигурации, без рестарта всей платформы.

8.5. Config flow и subentries через API

Добавление интеграций и настройку «разрешённых чатов» бота можно делать целиком через REST-flow (/api/config/config_entries/flow) и flow вложенных записей (/api/config/config_entries/subentries/flow). Удобно для автоматизации «под ключ», без ручного клика по UI, Клодом.

8.6. Ошибка в cooldown, которую легко не заметить

Первая версия cooldown’а опиралась на «время последнего запуска автоматизации» (last_triggered). Проблема: этот таймстамп обновляется при каждом запуске, в том числе на «сухих» проверках, где осадков нет. В связке с проверкой каждые 30 минут это давало эффект:

  • 00:00 — сухая проверка, last_triggered = 00:00;

  • 00:30, 01:00, … — cooldown (>3ч) не прошёл, автоматизация даже не стартует;

  • если дождь появился в 01:00 — проверка заблокирована до 03:00.

То есть «сухой» запуск глушил последующую реальную проверку. Решение — вынести состояние в отдельный хелпер input_datetime.weatherwatch_last_check, который обновляется только при реальной проверке (после гейта по осадкам). Теперь сухие срабатывания cooldown не сдвигают.

Мораль: «время последнего запуска» и «время последнего значимого события» — разные вещи. Легко перепутать и получить тихо-неправильную логику.

8.7. Как я уронил сервер в своп

HA у меня крутится на старом Raspberry Pi 3 (~1 ГБ RAM) — и именно поэтому история случилась. Чтобы «безопасно» проверить конфиг, я запустил внутри контейнера штатный скрипт проверки конфигурации. Он поднимает второй экземпляр платформы в памяти. Для гигабайта это перебор: система ушла в своп, перестали отвечать и веб-интерфейс, и SSH (буквально не завершался хендшейк — хосту не хватало ресурсов даже на это). В результате просто выключил из розетки и включил заново.

Выводы:

  • на слабом железе не запускать тяжёлую проверку конфигурации «в бою» рядом с работающим инстансом;

  • перезапуск контейнера/хоста — валидный и быстрый способ выйти из свопа, если конфиг лежит на диске и переживёт рестарт;

  • reload через API валидирует изменения без поднятия второго инстанса — и в большинстве случаев его достаточно.

9. Что дальше

  • Тихие часы — не будить уведомлением, скажем, с 22:00 до 07:00 (для «ночного» дождя копить и присылать заранее вечером).

  • Больше «наблюдателей». Тот же примитив (камера + погодный триггер + вопрос на естественном языке + действие) переиспользуется: «закрыть окна», «занести бельё», «накрыть бассейн перед заморозком».

  • Обратная связь как обучение. Кнопка «на самом деле накрыто» может со временем подстраивать порог/промпт и повышать доверие.

10. Выводы

  • LLM-зрение — недостающий слой рассуждения между камерами и действиями. С нативной AI-Task-интеграцией его вплетаешь без единого своего компонента, а structured output превращает ответ модели в предсказуемые поля, а не в текст, который надо парсить.

  • ИИ-агент сегодня не только пишет код, но и разворачивает систему целиком. Эту автоматизацию он собрал сам: заходил по SSH, ходил в REST/WebSocket API, включал отключённые сущности, проходил config-flow и вычищал грабли. Роль человека сместилась к постановке задачи и приёмке результата.

  • Дешевизна и «не спамить» — это архитектура, а не намерение. Гейты по осадкам, cooldown на реальных событиях и обратная связь пользователя встроены в структуру.

  • Финальное действие оставлено за человеком осознанно, а не по недоделке. Там, где ошибка необратима или нужен контекст, ИИ берёт наблюдение и суждение, решение — за человеком.

Подписывайтесь на канал ТехДир Подсекин, ставьте лайк, вам не сложно, мне — приятно.

Источник: habr.com

Оцените материал:

Поделиться
Понравилась статья? Расскажите другим
ВКонтакте
Читайте также
Архив рубрики ~Обо всем~ Стоит ли покупать бюджетный 3D-принтер? Я протестировал десятки моделей, чтобы представить вам лучшие варианты. Новости робототехники «Дикий опыт» в Балтике: русский корвет «поиграл» с датским морским дроном Архив рубрики ~Коротко из Telegram~ Нашли очень крутой ИИ-сервис для музыкантов — Studio Moises 😇… Архив рубрики ~Коротко из Telegram~ ✅ OpenAI Academy открыла серию живых сессий Codex Bootcamp —… Архив рубрики ~Коротко из Telegram~ 🎞 Seedance 2.5 вышел глобально — ByteDance запустила модель в… Архив рубрики ~Коротко из Telegram~ 🔪 В OpenAI Showcase появился Material Lab — интерактивный инструмент… Архив рубрики ~Коротко из Telegram~ 🔉 Транскрибируем аудио в текст за секунды — и всё… Новости робототехники KUKA внедряет платформу автоматизации управления для автопроизводителей Северной Америки Новости робототехники В России продолжается масштабное тестирование беспилотных грузовых автомобилей на дорогах общего пользования Архив рубрики ~Коротко из Telegram~ Пластмассовый мир победил Конец второй мировой, помимо миллиона других проблем,… Архив рубрики ~Обо всем~ Стартапы, финансируемые венчурным капиталом, совершают больше мошеннических действий, и исследователи считают, что знают, почему. Архив рубрики ~Обо всем~ Майнинг астероидов: ключевые миссии Новости робототехники Кто выиграет и кто проиграет после того, как США запретили иностранных роботов? Новости робототехники Procore Technologies Знакомый ДронРазвертывание за 845 миллионов долларов Архив рубрики ~Обо всем~ Стоит ли покупать бюджетный 3D-принтер? Я протестировал десятки моделей, чтобы представить вам лучшие варианты. Новости робототехники «Дикий опыт» в Балтике: русский корвет «поиграл» с датским морским дроном Архив рубрики ~Коротко из Telegram~ Нашли очень крутой ИИ-сервис для музыкантов — Studio Moises 😇… Архив рубрики ~Коротко из Telegram~ ✅ OpenAI Academy открыла серию живых сессий Codex Bootcamp —… Архив рубрики ~Коротко из Telegram~ 🎞 Seedance 2.5 вышел глобально — ByteDance запустила модель в… Архив рубрики ~Коротко из Telegram~ 🔪 В OpenAI Showcase появился Material Lab — интерактивный инструмент… Архив рубрики ~Коротко из Telegram~ 🔉 Транскрибируем аудио в текст за секунды — и всё… Новости робототехники KUKA внедряет платформу автоматизации управления для автопроизводителей Северной Америки Новости робототехники В России продолжается масштабное тестирование беспилотных грузовых автомобилей на дорогах общего пользования Архив рубрики ~Коротко из Telegram~ Пластмассовый мир победил Конец второй мировой, помимо миллиона других проблем,… Архив рубрики ~Обо всем~ Стартапы, финансируемые венчурным капиталом, совершают больше мошеннических действий, и исследователи считают, что знают, почему. Архив рубрики ~Обо всем~ Майнинг астероидов: ключевые миссии Новости робототехники Кто выиграет и кто проиграет после того, как США запретили иностранных роботов? Новости робототехники Procore Technologies Знакомый ДронРазвертывание за 845 миллионов долларов

Оставить комментарий