🤖 Удалённый пульт для умного дома: Telegram-бот + Node-RED
В предыдущей статье «Telegram + Node-RED: создаём чат-бота» мы рассмотрели создание простейшего бота и подключение токена к библиотеке node-red-contrib-telegrambot. Пришло время сделать реально удобное и практичное управление системой умного дома вне локальной сети.
В данной статье будут рассмотрены следующие автоматизации удаленного управления:
- 📋 Создание стартового окна — с приветствием и вашими данными Telegram, а также окно меню и помощи с существующими командами
- 💡 Управление освещением — просмотр состояния источников освещения и управление RGB-лампами (включение/выключение, яркость, цвет)
- 🐳 Управление Docker-контейнерами — просмотр состояния контейнеров Portainer, запуск, остановка, перезагрузка
- ✅ Автоматизация с отслеживанием задач Todoist — начата, будет доработана (ограничения API v1 и v2)
- 🎂 Хранение дней рождения — внутри Node-RED без HA, создание, изменение, добавление, удаление
- 🧘 Вызов сценария медитации — из другого потока (link call)
- 🌤️ Погода с рекомендациями — из встроенной интеграции HA, советы что взять/надеть на улицу
- 🧹 Управление роботом-пылесосом — запуск/отправка на базу, отслеживание состояния, хранение данных запуска и времени работы
- 📊 Мониторинг датчиков и сенсоров — все датчики дома в одном месте
Данная статья будет со временем дополняться, так как есть желание переделать задачник, добавить календарики Google и Apple как минимум.
Содержание статьи
- 1Стартовое окно (команды /start, /help, /menu)
- 2Обработка inline-кнопок (callback_query)
- 3Управление освещением (RGB, яркость, цвет)
- 4Управление Docker-контейнерами (Portainer)
- 5Todoist задачи через календари HA
- 6Дни рождения (хранение в файле Node-RED)
- 7Вызов сценария медитации
- 8Погода с рекомендациями
- 9Мониторинг датчиков и сенсоров
- 10Управление роботом-пылесосом
1. Стартовое окно (команды /start, /help, /menu)
Реализован блок приветствия по команде /start, который возвращает информацию об аккаунте (имя, username, id) и кнопку перехода в главное меню. Команда /help выводит справочную информацию, а /menu генерирует основную inline-клавиатуру для доступа ко всем функциям бота.



1.1 Пошаговая настройка
Общая архитектура
Поток обрабатывает команды /start, /help, /menu. Получает сообщение → маршрутизирует → запрашивает данные пользователя из API Telegram → формирует ответ → отправляет.
1. Приём сообщений (telegram receiver)
- Тип узла:
telegram receiver - Привязан к боту (ID конфигурации
[ID_BOT_CONFIG]) - Режим: polling
- Выходные поля:
msg.payload.content— текст,msg.payload.chatId— ID чата,msg.payload.userId— ID пользователя
2. Маршрутизатор команд (switch)
- Тип узла:
switch - Свойство:
msg.payload.content - Правила:
/start→ выход 1,/help→ выход 2,/menu→ выход 3
3. Обработка /start — запрос данных пользователя
Узел «Запрос к API» (function):
const chatId = msg.payload.chatId;
const botToken = "[ВАШ_ТОКЕН]";
msg.url = `https://api.telegram.org/bot${botToken}/getChat?chat_id=${chatId}`;
msg.method = "GET";
msg.originalChatId = chatId;
return msg;
Узел «HTTP-запрос» (http request):
- Метод: GET
- Возврат: объект (ret: "obj")
Узел «Обработчик /start» (function) — основная логика:
if (msg.payload && msg.payload.ok === true) {
const chatData = msg.payload.result;
const username = chatData.username || "без username";
const firstName = chatData.first_name || "";
const lastName = chatData.last_name || "";
const chatId = chatData.id;
let welcomeMessage = `🏠 *Добро пожаловать!*\n\n`;
welcomeMessage += `👤 *Информация о вас:*\n`;
welcomeMessage += `• ID: ${chatId}\n`;
welcomeMessage += `• Username: @${username}\n`;
welcomeMessage += `• Имя: ${firstName} ${lastName}\n\n`;
welcomeMessage += `🤖 Используйте /menu для доступа ко всем функциям\n`;
welcomeMessage += `или /help для списка команд.`;
const keyboard = {
reply_markup: {
inline_keyboard: [
[{ text: "📋 Перейти в меню", callback_data: "back_main" }]
]
}
};
msg.payload = {
chatId: chatId,
type: "message",
content: welcomeMessage,
options: { parse_mode: "Markdown", reply_markup: keyboard.reply_markup }
};
return msg;
} else {
const chatId = msg.originalChatId;
let welcomeMessage = `🏠 *Добро пожаловать!*\n\n👤 Ваш ID: ${chatId}\n\n🤖 Используйте /menu для доступа ко всем функциям.`;
const keyboard = {
reply_markup: {
inline_keyboard: [
[{ text: "📋 Перейти в меню", callback_data: "back_main" }]
]
}
};
msg.payload = {
chatId: chatId,
type: "message",
content: welcomeMessage,
options: { parse_mode: "Markdown", reply_markup: keyboard.reply_markup }
};
return msg;
}
4. Обработка /help (function)
const helpText = `❓ *Помощь по командам бота*
*Основные команды:*
/start — Приветствие
/menu — Главное меню
/help — Эта справка
*Управление домом:*
/lights — Управление освещением
/status — Статус датчиков
*Продуктивность:*
/tasks — Список задач
/meditation — Медитация
*IT и другое:*
/docker — Docker контейнеры
/birthdays — Дни рождения
/vacuum — Пылесос
💡 Все функции доступны через кнопки в меню.`;
msg.payload = {
chatId: msg.payload.chatId,
type: "message",
content: helpText,
options: { parse_mode: "Markdown" }
};
return msg;
5. Обработка /menu (function)
6. Отправка сообщения (telegram sender)
- Тип узла:
telegram sender - Привязан к тому же боту
1.2 Полный код
JSON для импорта в Node-RED (Меню → Import). Замените [ВАШ_ТОКЕН], [ВАШ_БОТ], [ВАШ_USERNAME], [ID_BOT_CONFIG], [IP_СЕРВЕРА] на свои значения.
[
{
"id": "29d04a2fe2721f63",
"type": "group",
"z": "d91b643aa7cd4cef",
"name": "Стартовая часть",
"style": {
"stroke": "#e3f3d3",
"fill": "#ffC000",
"label": true,
"label-position": "sw",
"color": "#000000"
},
"nodes": [
"13444dc9a2835902",
"f887ffa453bb5b92",
"3f91011494f21d58",
"79cb76a2fd7818a2",
"4734286741b412c8",
"04b0138e50a7fffe",
"dc20b8f969aa8c88",
"519f350edb45da7e",
"3fb84020d2e07c3e"
],
"x": 34,
"y": 39,
"w": 1272,
"h": 230
},
{
"id": "13444dc9a2835902",
"type": "switch",
"z": "d91b643aa7cd4cef",
"g": "29d04a2fe2721f63",
"name": "Маршрутизатор команд",
"property": "payload.content",
"propertyType": "msg",
"rules": [
{"t": "eq", "v": "/start", "vt": "str"},
{"t": "eq", "v": "/help", "vt": "str"},
{"t": "eq", "v": "/menu", "vt": "str"}
],
"checkall": "true",
"repair": false,
"outputs": 3,
"x": 370,
"y": 200,
"wires": [
["04b0138e50a7fffe"],
["3f91011494f21d58"],
["79cb76a2fd7818a2"]
]
},
{
"id": "f887ffa453bb5b92",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "29d04a2fe2721f63",
"name": "Обработчик /start",
"func": "const chatId = msg.payload.chatId;\nlet welcomeMessage = `🏠 *Добро пожаловать домой!*\\n\\n`;\nwelcomeMessage += `👤 *Информация о вас:*\\n`;\nwelcomeMessage += `• ID: ${chatId}\\n`;\nwelcomeMessage += `• Username: @пользователь\\n`;\nwelcomeMessage += `• Имя: (будет после запроса)\\n\\n`;\nwelcomeMessage += `🤖 Я ваш персональный ассистент.\\n`;\nwelcomeMessage += `Используйте /menu для доступа ко всем функциям\\n`;\nwelcomeMessage += `или /help для списка команд.`;\nconst keyboard = { reply_markup: { inline_keyboard: [[{ text: \"📋 Перейти в меню\", callback_data: \"back_main\" }]] } };\nmsg.payload = { chatId: chatId, type: \"message\", content: welcomeMessage, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 550,
"y": 80,
"wires": [[]]
},
{
"id": "3f91011494f21d58",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "29d04a2fe2721f63",
"name": "Обработчик /help",
"func": "const helpText = `❓ *Помощь по командам бота*\n\n*Основные команды:*\n/start — Приветствие и информация о вас\n/menu — Главное меню\n/help — Эта справка\n\n*Управление домом (в разработке):*\n/lights — Управление освещением\n/status — Статус всех датчиков\n/windows — Состояние окон\n\n*Продуктивность (в разработке):*\n/tasks — Список задач\n/alarm — Будильник\n/pomodoro — Таймер Pomodoro\n/meditation — Медитация\n\n*IT и другое (в разработке):*\n/docker — Статус Docker контейнеров\n/calendar — Календарь событий\n/birthdays — Дни рождения\n/vacuum — Управление пылесосом\n\n💡 *Совет:* Все функции также доступны через кнопки в меню.`;\nmsg.payload = { chatId: msg.payload.chatId, type: \"message\", content: helpText, options: { parse_mode: \"Markdown\" } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 750,
"y": 180,
"wires": [["4734286741b412c8"]]
},
{
"id": "79cb76a2fd7818a2",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "29d04a2fe2721f63",
"name": "Обработчик /menu",
"func": "const mainKeyboard = { reply_markup: { inline_keyboard: [ [{ text: \"💡 Управление светом\", callback_data: \"menu_lights\" }], [{ text: \"📊 Мониторинг\", callback_data: \"menu_monitoring\" }], [{ text: \"🌤️ Погода\", callback_data: \"menu_weather\" }], [{ text: \"✅ Задачи\", callback_data: \"menu_tasks\" }], [{ text: \"🍅 Помидорка\", callback_data: \"menu_pomodoro\" }, { text: \"🧘 Медитация\", callback_data: \"menu_meditation\" }], [{ text: \"🧹 Кракен\", callback_data: \"menu_vacuum\" }, { text: \"🐳 Docker\", callback_data: \"menu_docker\" }], [{ text: \"📅 Календарь\", callback_data: \"menu_calendar\" }, { text: \"🎂 Дни рождения\", callback_data: \"menu_birthdays\" }], [{ text: \"❓ Помощь\", callback_data: \"menu_help\" }] ] } };\nmsg.payload = { chatId: msg.payload.chatId || msg.payload.userId, type: \"message\", content: \"🏠 *Главное меню*\\nВыберите действие:\", options: { parse_mode: \"Markdown\", reply_markup: mainKeyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 760,
"y": 220,
"wires": [["4734286741b412c8"]]
},
{
"id": "4734286741b412c8",
"type": "telegram sender",
"z": "d91b643aa7cd4cef",
"g": "29d04a2fe2721f63",
"name": "Отправка сообщения",
"bot": "[ID_BOT_CONFIG]",
"haserroroutput": false,
"outputs": 1,
"x": 1180,
"y": 220,
"wires": [[]]
},
{
"id": "04b0138e50a7fffe",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "29d04a2fe2721f63",
"name": "Запрос к API",
"func": "const chatId = msg.payload.chatId;\nconst botToken = \"[ВАШ_ТОКЕН]\";\nmsg.url = `https://api.telegram.org/bot${botToken}/getChat?chat_id=${chatId}`;\nmsg.method = \"GET\";\nmsg.originalChatId = chatId;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 600,
"y": 140,
"wires": [["dc20b8f969aa8c88"]]
},
{
"id": "dc20b8f969aa8c88",
"type": "http request",
"z": "d91b643aa7cd4cef",
"g": "29d04a2fe2721f63",
"name": "Выполняет запрос данных",
"method": "GET",
"ret": "obj",
"x": 820,
"y": 140,
"wires": [["519f350edb45da7e"]]
},
{
"id": "519f350edb45da7e",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "29d04a2fe2721f63",
"name": "Обработчик /start",
"func": "if (msg.payload && msg.payload.ok === true) {\n const chatData = msg.payload.result;\n const username = chatData.username || \"без username\";\n const firstName = chatData.first_name || \"\";\n const lastName = chatData.last_name || \"\";\n const chatId = chatData.id;\n let welcomeMessage = `🏠 *Добро пожаловать домой!*\\n\\n`;\n welcomeMessage += `👤 *Информация о вас:*\\n`;\n welcomeMessage += `• ID: ${chatId}\\n`;\n welcomeMessage += `• Username: @${username}\\n`;\n welcomeMessage += `• Имя: ${firstName} ${lastName}\\n\\n`;\n welcomeMessage += `🤖 Я ваш персональный ассистент.\\n`;\n welcomeMessage += `Используйте /menu для доступа ко всем функциям\\n`;\n welcomeMessage += `или /help для списка команд.`;\n const keyboard = { reply_markup: { inline_keyboard: [[{ text: \"📋 Перейти в меню\", callback_data: \"back_main\" }]] } };\n msg.payload = { chatId: chatId, type: \"message\", content: welcomeMessage, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\n return msg;\n} else {\n const chatId = msg.originalChatId;\n let welcomeMessage = `🏠 *Добро пожаловать домой!*\\n\\n`;\n welcomeMessage += `👤 *Информация о вас:*\\n`;\n welcomeMessage += `• ID: ${chatId}\\n`;\n welcomeMessage += `• Username: (не удалось получить)\\n`;\n welcomeMessage += `• Имя: (не удалось получить)\\n\\n`;\n welcomeMessage += `🤖 Я ваш персональный ассистент.\\n`;\n welcomeMessage += `Используйте /menu для доступа ко всем функциям\\n`;\n welcomeMessage += `или /help для списка команд.`;\n const keyboard = { reply_markup: { inline_keyboard: [[{ text: \"📋 Перейти в меню\", callback_data: \"back_main\" }]] } };\n msg.payload = { chatId: chatId, type: \"message\", content: welcomeMessage, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\n return msg;\n}",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1050,
"y": 140,
"wires": [["4734286741b412c8"]]
},
{
"id": "3fb84020d2e07c3e",
"type": "telegram receiver",
"z": "d91b643aa7cd4cef",
"g": "29d04a2fe2721f63",
"name": "Прием сообщений",
"bot": "[ID_BOT_CONFIG]",
"x": 150,
"y": 140,
"wires": [["13444dc9a2835902"], []]
},
{
"id": "[ID_BOT_CONFIG]",
"type": "telegram bot",
"botname": "[ВАШ_БОТ]",
"usernames": "[ВАШ_USERNAME]",
"updatemode": "polling",
"pollinterval": "300",
"localbothost": "[IP_СЕРВЕРА]",
"localbotport": 8443,
"publicbotport": 8443
},
{
"id": "bd8debbf6258bb88",
"type": "global-config",
"env": [],
"modules": {
"node-red-contrib-telegrambot": "17.4.12"
}
}
]
2. Обработка inline-кнопок (callback_query)
Поток перехватывает callback-запросы от нажатий на inline-кнопки в сообщениях бота, маршрутизирует их по типу действия и возвращает соответствующий ответ. Для ещё не реализованных функций отправляется сообщение-заглушка с кнопкой «Назад в меню».
2.1 Пошаговая настройка
1. Перехват нажатий кнопок (telegram event)
- Тип узла:
telegram event - Событие:
callback_query autoanswer: false— не отправляет автоматическое уведомление Telegram- Выходные поля:
msg.payload.content— callback_data,msg.payload.chatId— ID чата,msg.payload.messageId— ID исходного сообщения
2. Маршрутизатор меню (switch)
- Тип узла:
switch - Свойство:
msg.payload.content - Правила:
back_menu→ выход 1 (вернуться в меню)menu_help→ выход 2 (показать помощь)menu_calendar→ выход 3 (заглушка)menu_pomodoro→ выход 4 (заглушка)back_main→ выход 5 (вернуться в главное меню)
3. Заглушка для нереализованных функций (function)
const keyboard = {
reply_markup: {
inline_keyboard: [
[{ text: "◀️ Назад в меню", callback_data: "back_main" }]
]
}
};
msg.payload = {
chatId: msg.payload.chatId,
type: "message",
content: "🚧 *Этот раздел в разработке* 🚧\n\nСкоро здесь появится полезный функционал!",
options: { parse_mode: "Markdown", reply_markup: keyboard.reply_markup }
};
return msg;
4. Отправка сообщения (telegram sender)
- Тип узла:
telegram sender - Привязан к боту
2.2 Полный код
JSON для импорта в Node-RED (Меню → Import). Замените [ID_BOT_CONFIG], [ВАШ_БОТ], [ВАШ_USERNAME], [IP_СЕРВЕРА] на свои значения.
[
{
"id": "51a74554495a8ec2",
"type": "group",
"z": "d91b643aa7cd4cef",
"name": "Главное меню",
"style": {
"stroke": "#ffffbf",
"fill": "#ffff00",
"label": true,
"label-position": "sw",
"color": "#000000"
},
"nodes": [
"b56b312ad5310f68",
"9ce493c2a53da075",
"dd0e4b8080e0804d",
"18a7c3468af60966"
],
"x": 34,
"y": 276.5,
"w": 1172,
"h": 135
},
{
"id": "b56b312ad5310f68",
"type": "telegram event",
"z": "d91b643aa7cd4cef",
"g": "51a74554495a8ec2",
"name": "Обработчик нажатия кнопок",
"bot": "[ID_BOT_CONFIG]",
"event": "callback_query",
"autoanswer": false,
"x": 190,
"y": 340,
"wires": [["9ce493c2a53da075"]]
},
{
"id": "9ce493c2a53da075",
"type": "switch",
"z": "d91b643aa7cd4cef",
"g": "51a74554495a8ec2",
"name": "маршрутизатор menu",
"property": "payload.content",
"propertyType": "msg",
"rules": [
{"t": "eq", "v": "back_menu", "vt": "str"},
{"t": "eq", "v": "menu_help", "vt": "str"},
{"t": "eq", "v": "menu_calendar", "vt": "str"},
{"t": "eq", "v": "menu_pomodoro", "vt": "str"},
{"t": "eq", "v": "back_main", "vt": "str"}
],
"checkall": "true",
"repair": false,
"outputs": 5,
"x": 450,
"y": 340,
"wires": [
["79cb76a2fd7818a2"],
["3f91011494f21d58"],
["dd0e4b8080e0804d"],
["dd0e4b8080e0804d"],
["79cb76a2fd7818a2"]
]
},
{
"id": "dd0e4b8080e0804d",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "51a74554495a8ec2",
"name": "Пока-что заглушка",
"func": "const keyboard = { reply_markup: { inline_keyboard: [[{ text: \"◀️ Назад в меню\", callback_data: \"back_main\" }]] } };\nmsg.payload = { chatId: msg.payload.chatId, type: \"message\", content: \"🚧 *Этот раздел в разработке* 🚧\\n\\nСкоро здесь появится полезный функционал!\", options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 720,
"y": 360,
"wires": [["18a7c3468af60966"]]
},
{
"id": "18a7c3468af60966",
"type": "telegram sender",
"z": "d91b643aa7cd4cef",
"g": "51a74554495a8ec2",
"name": "Отправка сообщения",
"bot": "[ID_BOT_CONFIG]",
"haserroroutput": false,
"outputs": 1,
"x": 1080,
"y": 360,
"wires": [[]]
},
{
"id": "[ID_BOT_CONFIG]",
"type": "telegram bot",
"botname": "[ВАШ_БОТ]",
"usernames": "[ВАШ_USERNAME]",
"updatemode": "polling",
"pollinterval": "300",
"localbothost": "[IP_СЕРВЕРА]",
"localbotport": 8443,
"publicbotport": 8443
},
{
"id": "cf4d3d1e0ea8594d",
"type": "global-config",
"env": [],
"modules": {
"node-red-contrib-telegrambot": "17.4.12"
}
}
]
3. Управление освещением (RGB, яркость, цвет)
Управление освещением строится на возможности отслеживания состояния всех лампочек одним сообщением, а дальше уже выбор конкретной и управление точечно ей, такими как включение и выключение, управление яркостью и смена цвета — всё это для RGB-лампочек.




3.1 Пошаговая настройка
Общая архитектура
Этот поток реализует полноценное управление умным освещением через Telegram-бота: вывод списка ламп с их текущим статусом, индивидуальное управление каждой лампой (вкл/выкл, яркость, цвет), интеграция с Home Assistant через WebSocket API.
Часть 1. Вход в меню освещения
1.1. Перехват callback (telegram event)
- Тип узла:
telegram event - Событие:
callback_query - Привязан к боту (
[ID_BOT_CONFIG])
1.2. Маршрутизатор освещения (switch)
- Тип узла:
switch - Свойство:
msg.payload.content - Правила (регулярные выражения):
light_control_.*→ панель управления лампойlight_color_.*→ палитра цветовlight_on_.*→ включитьlight_off_.*→ выключитьlight_brightness_.*→ изменить яркостьlight_color_set_.*→ установить цветmenu_lights→ список ламп
Часть 2. Отображение списка ламп (menu_lights)
Узел «Lights Menu» (function) — сохраняет chatId в глобальную переменную.
Узлы «Лампа 1» и «Лампа 2» (api-current-state) — запрашивают состояние каждой лампы из Home Assistant.
Узлы нормализации (function) — приводят данные к единому формату: msg.topic = entity_id, msg.payload = "on"/"off".
Узел «Объединение ламп» (join) — собирает данные от обеих ламп в один объект: {"light.test_product_2": "on", "light.kolco2": "off"}.
Узел «Format Lights List» (function) — форматирует сообщение со статусами и создаёт кнопки управления для каждой лампы.
Часть 3. Панель управления лампой
Узел «Light Control Panel» (function) — создаёт панель управления для конкретной лампы с кнопками: Включить, Выключить, Яркость +10%, Яркость -10%, Цвет, Назад к списку.
let lightId = msg.payload.content.replace("light_control_", "");
const lightNames = {
"[ВАШ_ENTITY_ID_1]": "💡 [ВАШЕ_НАЗВАНИЕ_ЛАМПЫ_1]",
"[ВАШ_ENTITY_ID_2]": "💡 [ВАШЕ_НАЗВАНИЕ_ЛАМПЫ_2]"
};
const controlKeyboard = {
reply_markup: {
inline_keyboard: [
[{ text: "🔆 Включить", callback_data: `light_on_${lightId}` },
{ text: "❌ Выключить", callback_data: `light_off_${lightId}` }],
[{ text: "📈 Яркость +10%", callback_data: `light_brightness_up_${lightId}` }],
[{ text: "📉 Яркость -10%", callback_data: `light_brightness_down_${lightId}` }],
[{ text: "🎨 Цвет", callback_data: `light_color_${lightId}` }],
[{ text: "◀️ Назад к списку", callback_data: "menu_lights" }]
]
}
};
msg.payload = { chatId: chatId, type: "message", content: `🎮 *Управление лампой*\n${lightName}\n\nВыберите действие:`, options: { parse_mode: "Markdown", reply_markup: controlKeyboard.reply_markup } };
return msg;
Узел «Light Color Picker» (function) — показывает 7 основных цветов (красный, зелёный, синий, жёлтый, фиолетовый, оранжевый, белый) с защитой от двойного открытия (3 секунды).
Часть 4. Выполнение команд освещения
Узел «Route Light Command» (function) — разбирает callback_data и направляет команду на соответствующий API-узел.
if (data.startsWith("light_on_")) {
const lightId = data.replace("light_on_", "");
return [msg, null, null, null, null];
} else if (data.startsWith("light_off_")) {
// ...
} else if (data.startsWith("light_brightness_up_")) {
// ...
} else if (data.startsWith("light_brightness_down_")) {
// ...
} else if (data.startsWith("light_color_set_")) {
const withoutPrefix = data.replace("light_color_set_", "");
const lastUnderscoreIndex = withoutPrefix.lastIndexOf("_");
const lightId = withoutPrefix.substring(0, lastUnderscoreIndex);
const colorName = withoutPrefix.substring(lastUnderscoreIndex + 1);
return [null, null, null, null, msg];
}
API-узлы Home Assistant:
Light On—light.turn_onLight Off—light.turn_offBrightness Up—light.turn_onс{"brightness_step_pct": 10}Brightness Down—light.turn_onс{"brightness_step_pct": -10}Set Color—light.turn_onс{"color_name": "{{payload.color_name}}"}
Узел «Light Command Response» (function) — формирует сообщение-подтверждение с кнопками «Назад к управлению» и «В главное меню».
3.2 Полный код
JSON для импорта в Node-RED (Меню → Import). Замените все плейсхолдеры [ВАШ_...] на свои значения.
[
{
"id": "0f92fc930c9c0e37",
"type": "group",
"z": "d91b643aa7cd4cef",
"name": "Освещение",
"style": {
"stroke": "#92d04f",
"fill": "#92d04f",
"label": true,
"label-position": "sw",
"color": "#000000"
},
"nodes": [
"53548f8c35190c24",
"3265e6c3a79c0230",
"ba884f1df3c6c325",
"22f7d792ee1ef7b7",
"7b97ff2c7db9964d",
"59593b4bd087ab1c",
"26247f431a91ecef",
"cced4a9f2b1d1397",
"3a1a0bc2d173af2f",
"59b989087a64d873",
"323e4bb05385f518",
"c429d3b3ab6c8090",
"f198a68addc257e3",
"cafae120e9e6d2a1",
"d09dad056bd19a1a",
"32cf6caf208f25c3",
"954ad439bf9804bc",
"95e2ef959d1a010f",
"b23a5d629687e907",
"dab4f66d2392f1eb"
],
"x": 54,
"y": 939,
"w": 2092,
"h": 310
},
{
"id": "53548f8c35190c24",
"type": "telegram event",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Обработчик нажатия кнопок",
"bot": "[ID_BOT_CONFIG]",
"event": "callback_query",
"autoanswer": false,
"x": 210,
"y": 1020,
"wires": [["3265e6c3a79c0230"]]
},
{
"id": "3265e6c3a79c0230",
"type": "switch",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "маршрутизатор light",
"property": "payload.content",
"propertyType": "msg",
"rules": [
{"t": "regex", "v": "light_control_.*", "vt": "str", "case": false},
{"t": "regex", "v": "light_color_.*", "vt": "str", "case": false},
{"t": "regex", "v": "light_on_.*", "vt": "str", "case": false},
{"t": "regex", "v": "light_off_.*", "vt": "str", "case": false},
{"t": "regex", "v": "light_brightness_.*", "vt": "str", "case": false},
{"t": "regex", "v": "light_color_set_.*", "vt": "str", "case": false},
{"t": "eq", "v": "menu_lights", "vt": "str"}
],
"checkall": "true",
"repair": false,
"outputs": 7,
"x": 460,
"y": 1020,
"wires": [
["ba884f1df3c6c325"],
["22f7d792ee1ef7b7"],
["3a1a0bc2d173af2f"],
["3a1a0bc2d173af2f"],
["3a1a0bc2d173af2f"],
["3a1a0bc2d173af2f"],
["f198a68addc257e3"]
]
},
{
"id": "ba884f1df3c6c325",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Light Control Panel",
"func": "let lightId = msg.payload.content.replace(\"light_control_\", \"\");\nconst chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nconst lightNames = {\n \"[ВАШ_ENTITY_ID_1]\": \"💡 [ВАШЕ_НАЗВАНИЕ_ЛАМПЫ_1]\",\n \"[ВАШ_ENTITY_ID_2]\": \"💡 [ВАШЕ_НАЗВАНИЕ_ЛАМПЫ_2]\"\n};\nconst lightName = lightNames[lightId] || lightId;\nconst controlKeyboard = {\n reply_markup: {\n inline_keyboard: [\n [{ text: \"🔆 Включить\", callback_data: `light_on_${lightId}` }, { text: \"❌ Выключить\", callback_data: `light_off_${lightId}` }],\n [{ text: \"📈 Яркость +10%\", callback_data: `light_brightness_up_${lightId}` }],\n [{ text: \"📉 Яркость -10%\", callback_data: `light_brightness_down_${lightId}` }],\n [{ text: \"🎨 Цвет\", callback_data: `light_color_${lightId}` }],\n [{ text: \"◀️ Назад к списку\", callback_data: \"menu_lights\" }]\n ]\n }\n};\nmsg.payload = { chatId: chatId, type: \"message\", content: `🎮 *Управление лампой*\\n${lightName}\\n\\nВыберите действие:`, options: { parse_mode: \"Markdown\", reply_markup: controlKeyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 730,
"y": 980,
"wires": [["323e4bb05385f518"]]
},
{
"id": "22f7d792ee1ef7b7",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Light Color Picker",
"func": "if (msg.payload.content && msg.payload.content.startsWith(\"light_color_set_\")) return null;\nlet lightId = msg.payload.content.replace(\"light_color_\", \"\");\nconst chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nconst paletteKey = `palette_${lightId}`;\nconst lastPaletteTime = global.get(paletteKey);\nconst now = Date.now();\nif (lastPaletteTime && (now - lastPaletteTime) < 3000) return null;\nglobal.set(paletteKey, now);\nconst colorKeyboard = {\n reply_markup: {\n inline_keyboard: [\n [{ text: \"🔴 Красный\", callback_data: `light_color_set_${lightId}_red` }],\n [{ text: \"🟢 Зеленый\", callback_data: `light_color_set_${lightId}_green` }],\n [{ text: \"🔵 Синий\", callback_data: `light_color_set_${lightId}_blue` }],\n [{ text: \"🟡 Желтый\", callback_data: `light_color_set_${lightId}_yellow` }],\n [{ text: \"💜 Фиолетовый\", callback_data: `light_color_set_${lightId}_purple` }],\n [{ text: \"🟠 Оранжевый\", callback_data: `light_color_set_${lightId}_orange` }],\n [{ text: \"⚪ Белый\", callback_data: `light_color_set_${lightId}_white` }],\n [{ text: \"◀️ Назад\", callback_data: `light_control_${lightId}` }]\n ]\n }\n};\nmsg.payload = { chatId: chatId, type: \"message\", content: \"🎨 *Выберите цвет:*\", options: { parse_mode: \"Markdown\", reply_markup: colorKeyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 730,
"y": 1020,
"wires": [["323e4bb05385f518"]]
},
{
"id": "7b97ff2c7db9964d",
"type": "api-call-service",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Light Off",
"server": "[ID_HA_SERVER]",
"action": "light.turn_off",
"entityId": ["{{payload.lightId}}"],
"x": 980,
"y": 1080,
"wires": [["59b989087a64d873"]]
},
{
"id": "59593b4bd087ab1c",
"type": "api-call-service",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Brightness Up",
"server": "[ID_HA_SERVER]",
"action": "light.turn_on",
"entityId": ["{{payload.lightId}}"],
"data": "{\"brightness_step_pct\": 10}",
"x": 1000,
"y": 1120,
"wires": [["59b989087a64d873"]]
},
{
"id": "26247f431a91ecef",
"type": "api-call-service",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Brightness Down",
"server": "[ID_HA_SERVER]",
"action": "light.turn_on",
"entityId": ["{{payload.lightId}}"],
"data": "{\"brightness_step_pct\": -10}",
"x": 1010,
"y": 1160,
"wires": [["59b989087a64d873"]]
},
{
"id": "cced4a9f2b1d1397",
"type": "api-call-service",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Set Color",
"server": "[ID_HA_SERVER]",
"action": "light.turn_on",
"entityId": ["{{payload.lightId}}"],
"data": "{\"color_name\": \"{{payload.color_name}}\"}",
"x": 980,
"y": 1200,
"wires": [["59b989087a64d873"]]
},
{
"id": "3a1a0bc2d173af2f",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Route Light Command",
"func": "if (!msg.payload) return null;\nlet data = msg.payload.content || msg.payload.data;\nif (!data) return null;\nconst chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nif (data.startsWith(\"light_on_\")) {\n const lightId = data.replace(\"light_on_\", \"\");\n return [{ payload: { chatId: chatId, lightId: lightId }, topic: \"on\" }, null, null, null, null];\n} else if (data.startsWith(\"light_off_\")) {\n const lightId = data.replace(\"light_off_\", \"\");\n return [null, { payload: { chatId: chatId, lightId: lightId }, topic: \"off\" }, null, null, null];\n} else if (data.startsWith(\"light_brightness_up_\")) {\n const lightId = data.replace(\"light_brightness_up_\", \"\");\n return [null, null, { payload: { chatId: chatId, lightId: lightId }, topic: \"brightness_up\" }, null, null];\n} else if (data.startsWith(\"light_brightness_down_\")) {\n const lightId = data.replace(\"light_brightness_down_\", \"\");\n return [null, null, null, { payload: { chatId: chatId, lightId: lightId }, topic: \"brightness_down\" }, null];\n} else if (data.startsWith(\"light_color_set_\")) {\n const withoutPrefix = data.replace(\"light_color_set_\", \"\");\n const lastUnderscoreIndex = withoutPrefix.lastIndexOf(\"_\");\n const lightId = withoutPrefix.substring(0, lastUnderscoreIndex);\n const colorName = withoutPrefix.substring(lastUnderscoreIndex + 1);\n return [null, null, null, null, { payload: { chatId: chatId, lightId: lightId, color_name: colorName }, topic: \"color\" }];\n}\nreturn null;",
"outputs": 5,
"timeout": 0,
"noerr": 0,
"x": 710,
"y": 1120,
"wires": [
["c429d3b3ab6c8090"],
["7b97ff2c7db9964d"],
["59593b4bd087ab1c"],
["26247f431a91ecef"],
["cced4a9f2b1d1397"]
]
},
{
"id": "59b989087a64d873",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Light Command Response",
"func": "const chatId = msg.payload?.chatId || msg.originalMessage?.chat?.id || msg.chatId;\nconst lightId = msg.lightId || msg.payload?.lightId;\nconst topic = msg.topic;\nlet resultMessage = \"\";\nif (topic === \"on\") resultMessage = \"🔆 Свет включен\";\nelse if (topic === \"off\") resultMessage = \"❌ Свет выключен\";\nelse if (topic === \"brightness_up\") resultMessage = \"📈 Яркость увеличена\";\nelse if (topic === \"brightness_down\") resultMessage = \"📉 Яркость уменьшена\";\nelse if (topic === \"color\") {\n const colorDisplay = { red: \"🔴 красный\", green: \"🟢 зеленый\", blue: \"🔵 синий\", yellow: \"🟡 желтый\", purple: \"💜 фиолетовый\", orange: \"🟠 оранжевый\", white: \"⚪ белый\" };\n const colorName = msg.payload?.color_name || \"\";\n resultMessage = `🎨 Цвет изменен на ${colorDisplay[colorName] || colorName}`;\n} else return null;\nconst keyboard = { reply_markup: { inline_keyboard: [[{ text: \"◀️ Назад к управлению\", callback_data: `light_control_${lightId}` }, { text: \"🏠 В главное меню\", callback_data: \"back_main\" }]] } };\nmsg.payload = { chatId: chatId, type: \"message\", content: `✅ ${resultMessage}`, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1280,
"y": 1140,
"wires": [["323e4bb05385f518"]]
},
{
"id": "323e4bb05385f518",
"type": "telegram sender",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Отправка сообщения",
"bot": "[ID_BOT_CONFIG]",
"x": 1240,
"y": 1020,
"wires": [[]]
},
{
"id": "c429d3b3ab6c8090",
"type": "api-call-service",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Light On",
"server": "[ID_HA_SERVER]",
"action": "light.turn_on",
"entityId": ["{{payload.lightId}}"],
"x": 980,
"y": 1040,
"wires": [["59b989087a64d873"]]
},
{
"id": "f198a68addc257e3",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Lights Menu",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"lights_chat_id\", chatId);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1390,
"y": 1080,
"wires": [["d09dad056bd19a1a", "cafae120e9e6d2a1"]]
},
{
"id": "cafae120e9e6d2a1",
"type": "api-current-state",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Лампа 2",
"server": "[ID_HA_SERVER]",
"entity_id": "[ВАШ_ENTITY_ID_2]",
"x": 1560,
"y": 1100,
"wires": [["954ad439bf9804bc"]]
},
{
"id": "d09dad056bd19a1a",
"type": "api-current-state",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Лампа 1",
"server": "[ID_HA_SERVER]",
"entity_id": "[ВАШ_ENTITY_ID_1]",
"x": 1560,
"y": 1060,
"wires": [["32cf6caf208f25c3"]]
},
{
"id": "32cf6caf208f25c3",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Нормализация Лампа 1",
"func": "let lightName = msg.data?.entity_id || msg.payload?.entity_id || msg.topic || \"unknown\";\nlet lightState = (typeof msg.payload === \"object\") ? (msg.payload.state === \"on\" ? \"on\" : \"off\") : msg.payload;\nmsg.topic = lightName;\nmsg.payload = lightState;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1770,
"y": 1060,
"wires": [["95e2ef959d1a010f"]]
},
{
"id": "954ad439bf9804bc",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Нормализация Лампа 2",
"func": "let lightName = msg.data?.entity_id || msg.payload?.entity_id || msg.topic || \"unknown\";\nlet lightState = (typeof msg.payload === \"object\") ? (msg.payload.state === \"on\" ? \"on\" : \"off\") : msg.payload;\nmsg.topic = lightName;\nmsg.payload = lightState;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1770,
"y": 1100,
"wires": [["95e2ef959d1a010f"]]
},
{
"id": "95e2ef959d1a010f",
"type": "join",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Объединение ламп",
"mode": "custom",
"build": "object",
"property": "payload",
"propertyType": "msg",
"key": "topic",
"count": "2",
"timeout": "5",
"x": 1650,
"y": 1180,
"wires": [["b23a5d629687e907"]]
},
{
"id": "b23a5d629687e907",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Format Lights List",
"func": "const lightsState = msg.payload;\nlet chatId = global.get(\"lights_chat_id\");\nconst lastSent = global.get(\"last_lights_message\");\nconst now = Date.now();\nif (lastSent && (now - lastSent) < 3000) return null;\nglobal.set(\"last_lights_message\", now);\nif (!chatId) chatId = msg.originalMessage?.chat?.id || msg.chatId;\nconst lightNames = {\n \"[ВАШ_ENTITY_ID_1]\": \"💡 [ВАШЕ_НАЗВАНИЕ_ЛАМПЫ_1]\",\n \"[ВАШ_ENTITY_ID_2]\": \"💡 [ВАШЕ_НАЗВАНИЕ_ЛАМПЫ_2]\"\n};\nlet message = \"💡 *Управление светом*\\n\\n\";\nconst keyboard = [];\nfor (const [entityId, state] of Object.entries(lightsState)) {\n const name = lightNames[entityId] || entityId;\n const status = state === \"on\" ? \"🟢 Включена\" : \"⚫ Выключена\";\n message += `${name}: ${status}\\n`;\n keyboard.push([{ text: name, callback_data: `light_control_${entityId}` }]);\n}\nkeyboard.push([{ text: \"◀️ Назад в меню\", callback_data: \"back_main\" }]);\nglobal.set(\"lights_list\", Object.keys(lightsState));\nmsg.payload = { chatId: chatId, type: \"message\", content: message, options: { parse_mode: \"Markdown\", reply_markup: { inline_keyboard: keyboard } } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1870,
"y": 1180,
"wires": [["dab4f66d2392f1eb"]]
},
{
"id": "dab4f66d2392f1eb",
"type": "telegram sender",
"z": "d91b643aa7cd4cef",
"g": "0f92fc930c9c0e37",
"name": "Отправка сообщения",
"bot": "[ID_BOT_CONFIG]",
"x": 2020,
"y": 1020,
"wires": [[]]
},
{
"id": "[ID_BOT_CONFIG]",
"type": "telegram bot",
"botname": "[ВАШ_БОТ]",
"usernames": "[ВАШ_USERNAME]",
"updatemode": "polling",
"pollinterval": "300",
"localbothost": "[IP_СЕРВЕРА]",
"localbotport": 8443
},
{
"id": "[ID_HA_SERVER]",
"type": "server",
"name": "Home Assistant",
"version": 6,
"addon": false
},
{
"id": "5c71334ad782912a",
"type": "global-config",
"env": [],
"modules": {
"node-red-contrib-telegrambot": "17.4.12",
"node-red-contrib-home-assistant-websocket": "0.80.3"
}
}
]
4. Управление Docker-контейнерами (Portainer)
Ранее к Home Assistant был подключён Portainer, поэтому добавим возможность отслеживания состояния контейнеров с возможностью их перезагрузки, запуска и остановки. Также можно вытянуть различные данные о процессоре и т.д.


4.1 Пошаговая настройка
Общая архитектура
Этот поток позволяет просматривать список Docker-контейнеров и их статус (запущен/остановлен), управлять контейнерами (запуск, остановка, перезапуск), получать обратную связь о выполнении команд. Интеграция происходит через Home Assistant, который через Portainer управляет Docker-контейнерами.
Часть 1. Вход в меню Docker
1.1. Перехват callback (telegram event)
- Тип узла:
telegram event - Событие:
callback_query - Привязан к боту (
[ID_BOT_CONFIG])
1.2. Маршрутизатор Docker (switch)
- Тип узла:
switch - Свойство:
msg.payload.content - Правила:
menu_docker→ показать список контейнеровdocker_control_.*→ открыть панель управленияdocker_start_.*→ запустить контейнерdocker_stop_.*→ остановить контейнерdocker_restart_.*→ перезапустить контейнер
Часть 2. Отображение списка контейнеров
Узел «Docker Menu» (function) — сохраняет chatId в глобальную переменную и формирует HTTP-запрос к Home Assistant API для получения состояния всех сущностей.
Узел «Get HA Entities» (http request) — выполняет GET-запрос к Home Assistant API, получает JSON-массив всех сущностей.
Узел «Format Docker List» (function) — фильтрует сущности состояния Docker-контейнеров (оканчивающиеся на _state), преобразует технические имена в понятные, формирует список с иконками статуса (🟢/🔴) и кнопками управления.
const stateEntities = entities.filter(e =>
e.entity_id.endsWith("_state") &&
(e.entity_id.includes("mynodered") || e.entity_id.includes("homeassistant") ||
e.entity_id.includes("esphome") || e.entity_id.includes("mosquitto") ||
e.entity_id.includes("portainer") || e.entity_id.includes("netalertx"))
);
// Маппинг имён
if (containerName === "mynodered") containerName = "Node-RED";
if (containerName === "homeassistant") containerName = "Home Assistant";
if (containerName === "esphome") containerName = "ESPHome";
if (containerName === "mosquitto") containerName = "Mosquitto";
if (containerName === "portainer") containerName = "Portainer";
if (containerName === "netalertx") containerName = "NetAlertX";
// Формирование сообщения с иконками статуса
const icon = isRunning ? "🟢" : "🔴";
message += `${icon} *${name}*: ${statusText}\n`;
keyboard.push([{ text: `${icon} ${name}`, callback_data: `docker_control_${name}` }]);
Часть 3. Панель управления контейнером
Узел «Docker Control Panel» (function) — создаёт панель управления для выбранного контейнера с кнопками: «Запустить», «Остановить», «Перезапустить», «Назад к списку».
let containerName = msg.payload.content.replace("docker_control_", "");
const controlKeyboard = {
reply_markup: {
inline_keyboard: [
[{ text: "▶️ Запустить", callback_data: `docker_start_${containerName}` }],
[{ text: "⏹️ Остановить", callback_data: `docker_stop_${containerName}` }],
[{ text: "🔄 Перезапустить", callback_data: `docker_restart_${containerName}` }],
[{ text: "◀️ Назад к списку", callback_data: "menu_docker" }]
]
}
};
Часть 4. Выполнение команд Docker
Узел «Handle Docker Command» (function) — разбирает callback_data и направляет команду на соответствующий API-узел.
Маппинг entity_id для управления контейнерами:
- Запуск/остановка →
switch.xxx_container(сервисыturn_on/turn_off) - Перезапуск →
button.xxx_restart_container(сервисpress)
API-узлы Home Assistant:
Docker Start—switch.turn_onDocker Stop—switch.turn_offDocker Restart—button.press
Узел «Docker Command Response» (function) — формирует сообщение-подтверждение с кнопками «Назад к списку» и «В главное меню».
4.2 Полный код
JSON для импорта в Node-RED (Меню → Import). Замените все плейсхолдеры [ВАШ_...] на свои значения.
[
{
"id": "67d65c2359b258e5",
"type": "group",
"z": "d91b643aa7cd4cef",
"name": "Docker",
"style": {
"stroke": "#92d04f",
"fill": "#92d04f",
"label": true,
"label-position": "sw",
"color": "#000000"
},
"nodes": [
"eecc47fd380f012f",
"132a5adf4cadda86",
"6813233649ebb36f",
"0aead6032a740aba",
"f90f8202233ac6fc",
"4b3080710211e8f6",
"558574256ae327c0",
"634f0fb22e895654",
"221a78429049bf81",
"57ef3b14cfc18f56",
"16125cc85da93d5e",
"28051a79b5c3df37"
],
"x": 54,
"y": 1739,
"w": 1412,
"h": 270
},
{
"id": "eecc47fd380f012f",
"type": "api-call-service",
"z": "d91b643aa7cd4cef",
"g": "67d65c2359b258e5",
"name": "Docker Start",
"server": "[ID_HA_SERVER]",
"action": "switch.turn_on",
"entityId": ["{{entityId}}"],
"x": 1010,
"y": 1880,
"wires": [["57ef3b14cfc18f56"]]
},
{
"id": "132a5adf4cadda86",
"type": "telegram event",
"z": "d91b643aa7cd4cef",
"g": "67d65c2359b258e5",
"name": "Обработчик нажатия кнопок",
"bot": "[ID_BOT_CONFIG]",
"event": "callback_query",
"autoanswer": false,
"x": 210,
"y": 1840,
"wires": [["6813233649ebb36f"]]
},
{
"id": "6813233649ebb36f",
"type": "switch",
"z": "d91b643aa7cd4cef",
"g": "67d65c2359b258e5",
"name": "маршрутизатор docker",
"property": "payload.content",
"propertyType": "msg",
"rules": [
{"t": "eq", "v": "menu_docker", "vt": "str"},
{"t": "regex", "v": "docker_control_.*", "vt": "str", "case": false},
{"t": "regex", "v": "docker_start_.*", "vt": "str", "case": false},
{"t": "regex", "v": "docker_stop_.*", "vt": "str", "case": false},
{"t": "regex", "v": "docker_restart_.*", "vt": "str", "case": false}
],
"checkall": "true",
"repair": false,
"outputs": 5,
"x": 480,
"y": 1840,
"wires": [
["0aead6032a740aba"],
["634f0fb22e895654"],
["221a78429049bf81"],
["221a78429049bf81"],
["221a78429049bf81"]
]
},
{
"id": "0aead6032a740aba",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "67d65c2359b258e5",
"name": "Docker Menu",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"docker_chat_id\", chatId);\nmsg.url = \"[ВАШ_IP_HA]:8123/api/states\";\nmsg.method = \"GET\";\nmsg.headers = {\n \"Authorization\": \"Bearer [ВАШ_ДОЛГОСРОЧНЫЙ_ТОКЕН_HA]\",\n \"Content-Type\": \"application/json\"\n};\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 740,
"y": 1780,
"wires": [["f90f8202233ac6fc"]]
},
{
"id": "f90f8202233ac6fc",
"type": "http request",
"z": "d91b643aa7cd4cef",
"g": "67d65c2359b258e5",
"name": "Get HA Entities",
"method": "GET",
"ret": "obj",
"x": 920,
"y": 1780,
"wires": [["4b3080710211e8f6"]]
},
{
"id": "4b3080710211e8f6",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "67d65c2359b258e5",
"name": "Format Docker List",
"func": "const chatId = global.get(\"docker_chat_id\");\nconst entities = msg.payload;\nif (!entities || !Array.isArray(entities)) return null;\nconst stateEntities = entities.filter(e =>\n e.entity_id.endsWith(\"_state\") &&\n (e.entity_id.includes(\"mynodered\") || e.entity_id.includes(\"homeassistant\") ||\n e.entity_id.includes(\"esphome\") || e.entity_id.includes(\"mosquitto\") ||\n e.entity_id.includes(\"portainer\") || e.entity_id.includes(\"netalertx\"))\n);\nconst containerMap = new Map();\nstateEntities.forEach(entity => {\n let containerName = entity.entity_id.replace(\"sensor.\", \"\").replace(\"_state\", \"\").replace(\"_status\", \"\");\n if (containerName === \"mynodered\") containerName = \"Node-RED\";\n if (containerName === \"homeassistant\") containerName = \"Home Assistant\";\n if (containerName === \"esphome\") containerName = \"ESPHome\";\n if (containerName === \"mosquitto\") containerName = \"Mosquitto\";\n if (containerName === \"portainer\") containerName = \"Portainer\";\n if (containerName === \"netalertx\") containerName = \"NetAlertX\";\n const isRunning = entity.state === \"running\" || entity.state === \"on\";\n if (!containerMap.has(containerName)) {\n containerMap.set(containerName, { name: containerName, status: isRunning ? \"running\" : \"stopped\" });\n }\n});\nlet message = \"🐳 *Docker контейнеры*\\n\\n\";\nconst keyboard = [];\nfor (const [name, container] of containerMap) {\n const isRunning = container.status === \"running\";\n const icon = isRunning ? \"🟢\" : \"🔴\";\n const statusText = isRunning ? \"Работает\" : \"Остановлен\";\n message += `${icon} *${name}*: ${statusText}\\n`;\n keyboard.push([{ text: `${icon} ${name}`, callback_data: `docker_control_${name}` }]);\n}\nkeyboard.push([{ text: \"🔄 Обновить список\", callback_data: \"menu_docker\" }]);\nkeyboard.push([{ text: \"◀️ Назад в меню\", callback_data: \"back_main\" }]);\nmsg.payload = { chatId: chatId, type: \"message\", content: message, options: { parse_mode: \"Markdown\", reply_markup: { inline_keyboard: keyboard } } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1110,
"y": 1780,
"wires": [["558574256ae327c0"]]
},
{
"id": "558574256ae327c0",
"type": "telegram sender",
"z": "d91b643aa7cd4cef",
"g": "67d65c2359b258e5",
"name": "Отправка сообщения",
"bot": "[ID_BOT_CONFIG]",
"x": 1340,
"y": 1820,
"wires": [[]]
},
{
"id": "634f0fb22e895654",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "67d65c2359b258e5",
"name": "Docker Control Panel",
"func": "let containerName = msg.payload.content.replace(\"docker_control_\", \"\");\nconst chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"docker_chat_id\", chatId);\nconst controlKeyboard = {\n reply_markup: {\n inline_keyboard: [\n [{ text: \"▶️ Запустить\", callback_data: `docker_start_${containerName}` }],\n [{ text: \"⏹️ Остановить\", callback_data: `docker_stop_${containerName}` }],\n [{ text: \"🔄 Перезапустить\", callback_data: `docker_restart_${containerName}` }],\n [{ text: \"◀️ Назад к списку\", callback_data: \"menu_docker\" }]\n ]\n }\n};\nmsg.payload = { chatId: chatId, type: \"message\", content: `🎮 *Управление контейнером*\\n📦 ${containerName}\\n\\nВыберите действие:`, options: { parse_mode: \"Markdown\", reply_markup: controlKeyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 760,
"y": 1820,
"wires": [["558574256ae327c0"]]
},
{
"id": "221a78429049bf81",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "67d65c2359b258e5",
"name": "Handle Docker Command",
"func": "const chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nconst data = msg.payload.content;\nlet action = \"\", containerName = \"\";\nif (data.startsWith(\"docker_start_\")) { action = \"start\"; containerName = data.replace(\"docker_start_\", \"\"); }\nelse if (data.startsWith(\"docker_stop_\")) { action = \"stop\"; containerName = data.replace(\"docker_stop_\", \"\"); }\nelse if (data.startsWith(\"docker_restart_\")) { action = \"restart\"; containerName = data.replace(\"docker_restart_\", \"\"); }\nif (!containerName || !action) return null;\nglobal.set(\"docker_chat_id\", chatId);\nglobal.set(\"docker_action\", action);\nglobal.set(\"docker_container\", containerName);\nconst startStopMap = {\n \"Node-RED\": \"switch.mynodered_container\",\n \"Home Assistant\": \"switch.homeassistant_container\",\n \"ESPHome\": \"switch.esphome_container\",\n \"Mosquitto\": \"switch.mosquitto_container\",\n \"Portainer\": \"switch.portainer_container\",\n \"NetAlertX\": \"switch.netalertx_container\"\n};\nconst restartMap = {\n \"Node-RED\": \"button.mynodered_restart_container\",\n \"Home Assistant\": \"button.homeassistant_restart_container\",\n \"ESPHome\": \"button.esphome_restart_container\",\n \"Mosquitto\": \"button.mosquitto_restart_container\",\n \"Portainer\": \"button.portainer_restart_container\",\n \"NetAlertX\": \"button.netalertx_restart_container\"\n};\nlet entityId = \"\", haAction = \"\";\nif (action === \"start\") { entityId = startStopMap[containerName]; haAction = \"switch.turn_on\"; }\nelse if (action === \"stop\") { entityId = startStopMap[containerName]; haAction = \"switch.turn_off\"; }\nelse if (action === \"restart\") { entityId = restartMap[containerName]; haAction = \"button.press\"; }\nif (!entityId) return null;\nconst statusMsg = { payload: { chatId: chatId, type: \"message\", content: `🔄 Выполняется ${action} контейнера ${containerName}...`, options: { parse_mode: \"Markdown\" } } };\nnode.send(statusMsg);\nmsg.entityId = entityId;\nmsg.haAction = haAction;\nmsg.containerName = containerName;\nif (action === \"start\") return [msg, null, null];\nelse if (action === \"stop\") return [null, msg, null];\nelse if (action === \"restart\") return [null, null, msg];\nreturn null;",
"outputs": 3,
"timeout": 0,
"noerr": 0,
"x": 780,
"y": 1920,
"wires": [
["eecc47fd380f012f"],
["16125cc85da93d5e"],
["28051a79b5c3df37"]
]
},
{
"id": "57ef3b14cfc18f56",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "67d65c2359b258e5",
"name": "Docker Command Response",
"func": "const chatId = global.get(\"docker_chat_id\");\nconst action = global.get(\"docker_action\");\nconst containerName = global.get(\"docker_container\");\nif (!chatId) return null;\nlet resultMessage = \"\";\nif (action === \"start\") resultMessage = `▶️ Контейнер ${containerName} запущен`;\nelse if (action === \"stop\") resultMessage = `⏹️ Контейнер ${containerName} остановлен`;\nelse if (action === \"restart\") resultMessage = `🔄 Контейнер ${containerName} перезапущен`;\nconst keyboard = { reply_markup: { inline_keyboard: [[{ text: \"◀️ Назад к списку\", callback_data: \"menu_docker\" }, { text: \"🏠 В главное меню\", callback_data: \"back_main\" }]] } };\nmsg.payload = { chatId: chatId, type: \"message\", content: `✅ ${resultMessage}`, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nglobal.set(\"docker_action\", null);\nglobal.set(\"docker_container\", null);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1260,
"y": 1920,
"wires": [["558574256ae327c0"]]
},
{
"id": "16125cc85da93d5e",
"type": "api-call-service",
"z": "d91b643aa7cd4cef",
"g": "67d65c2359b258e5",
"name": "Docker Stop",
"server": "[ID_HA_SERVER]",
"action": "switch.turn_off",
"entityId": ["{{entityId}}"],
"x": 1010,
"y": 1920,
"wires": [["57ef3b14cfc18f56"]]
},
{
"id": "28051a79b5c3df37",
"type": "api-call-service",
"z": "d91b643aa7cd4cef",
"g": "67d65c2359b258e5",
"name": "Docker Restart",
"server": "[ID_HA_SERVER]",
"action": "button.press",
"entityId": ["{{entityId}}"],
"x": 1020,
"y": 1960,
"wires": [["57ef3b14cfc18f56"]]
},
{
"id": "[ID_BOT_CONFIG]",
"type": "telegram bot",
"botname": "[ВАШ_БОТ]",
"usernames": "[ВАШ_USERNAME]",
"updatemode": "polling",
"pollinterval": "300",
"localbothost": "[IP_СЕРВЕРА]",
"localbotport": 8443
},
{
"id": "[ID_HA_SERVER]",
"type": "server",
"name": "Home Assistant",
"version": 6,
"addon": false
},
{
"id": "74b4366b57c8eefd",
"type": "global-config",
"env": [],
"modules": {
"node-red-contrib-home-assistant-websocket": "0.80.3",
"node-red-contrib-telegrambot": "17.4.12"
}
}
]
5. Todoist задачи через календари HA
Первоначальной задачей было реализовать функционал, который может добавлять/редактировать/удалять и выполнять задачу. В процессе разработки модуля управления задачами было проанализировано несколько технических подходов. Основными критериями выбора являлись: возможность создания, редактирования и удаления задач, поддержка дедлайнов, синхронизация с мобильными устройствами, простота интеграции с Home Assistant и Node-RED, а также стабильность работы.
📋 Анализ вариантов интеграции задач (развернуть)
Вариант 1. Встроенный локальный список задач Home Assistant (Local To-do)
Описание: Home Assistant имеет встроенную интеграцию «Список задач (Local To-do)», которая не требует подключения внешних сервисов и полностью работает локально. Интеграция создаёт сущности типа todo.* (например, todo.moi_zadachi), с которыми можно взаимодействовать через сервисы todo.add_item, todo.update_item, todo.get_items и todo.remove_item.
Результат: ✅ Технически реализуемо, но требует доработки.
Достоинства:
- Не требует регистрации во внешних сервисах
- Работает локально, быстро, без задержек
- Полностью совместим со всеми сервисами Home Assistant
- Данные хранятся в конфигурации HA
Недостатки:
- Нет синхронизации с мобильными устройствами
- Нет встроенных push-уведомлений
- Требуется реализация собственного интерфейса для добавления дедлайнов
Вердикт: Отложен в пользу облачного решения с синхронизацией.
Вариант 2. Todoist (облачный сервис задач)
Описание: Todoist — один из самых популярных облачных сервисов для управления задачами. Интеграция с Home Assistant осуществляется через официальную интеграцию, которая подключается по API-токену. После подключения в Home Assistant появляются сущности:
calendar.*– календари для каждого проекта (показывают задачи с датами)todo.*– списки задач (должны показывать все задачи, но в текущей версии не заполняются)
Рассмотренные способы подключения:
- Прямой вызов Todoist REST API v2 — ❌ API отключён (410 Gone)
- Прямой вызов Todoist Sync API v9 — ❌ API отключён (410 Gone)
- Палитра
@foxleigh81/node-red-contrib-todoist-api— ❌ Использует устаревшие эндпоинты - HA сущности
todo.*— ❌ Не синхронизируют задачи - HA сущности
calendar.*— ✅ Работает, но только задачи с датами
Достоинства:
- Отличное мобильное приложение с push-уведомлениями
- Синхронизация между устройствами
- Поддержка подзадач, меток, приоритетов
- Бесплатный тариф покрывает базовые потребности
Недостатки:
- Старые версии API полностью отключены Todoist
- Требуется аккаунт в сервисе
- Данные хранятся в облаке (не локально)
Итоговое решение в рамках ВКР: Использование календарей Todoist в Home Assistant (calendar.get_events). Ограничение – отображаются только задачи с установленной датой. Этого достаточно для реализации просмотра задач по проектам.
Вариант 3. Google Tasks
Описание: Интеграция с Google Tasks позволяет синхронизировать задачи из аккаунта Google. Подключается через Google API и требует настройки OAuth-авторизации. В Home Assistant создаются сущности типа todo.* для каждого списка задач.
Результат: ✅ Технически реализуемо, но имеет ограничения.
Достоинства:
- Интеграция с экосистемой Google
- Доступно на всех устройствах через Google
Недостатки:
- Google Tasks API принимает только дату для дедлайна (время игнорируется)
- Сложная настройка OAuth-авторизации
- Меньший функционал по сравнению с Todoist
Вариант 4. Vikunja (самостоятельный хостинг)
Описание: Vikunja — это open-source сервер для управления задачами, который можно развернуть на собственном сервере. Интеграция с Home Assistant осуществляется через REST API. Требует установки и настройки отдельного сервера Vikunja.
Результат: ⚠️ Рассматривался, но не внедрялся.
Достоинства:
- Полный контроль над данными (self-hosted)
- Открытый исходный код
- Поддержка дедлайнов, приоритетов, проектов
Недостатки:
- Требуется установка и обслуживание отдельного сервера
- Нет готовой интеграции в Home Assistant (только через REST API)
- Нет мобильного приложения (только веб-интерфейс)
Вердикт: Отложен из-за сложности развёртывания и отсутствия нативной интеграции с HA.


5.1 Пошаговая настройка
Общая архитектура
Этот поток позволяет просматривать задачи из Todoist, синхронизированные с календарями Home Assistant. Реализована навигация: выбор проекта → просмотр задач с дедлайнами, отображение статуса выполнения (выполнено/не выполнено), подсветка просроченных задач.
Важно: Реализован только просмотр, без создания/редактирования задач.
Схема потока (укрупнённо)
- Нажатие кнопки → Маршрутизация → Три направления:
menu_tasks→ Главное меню задач → Выбор действияtasks_by_project→ Меню выбора проекта → Список проектовtasks_by_project_.*→ Запрос к HA Calendar API → Форматирование списка задач → Отправка
Часть 1. Вход в меню задач
1.1. Перехват callback (telegram event)
- Тип узла:
telegram event - Событие:
callback_query - Привязан к боту (
[ID_BOT_CONFIG])
1.2. Маршрутизатор задач (switch)
- Тип узла:
switch - Свойство:
msg.payload.content - Правила:
menu_tasks→ главное меню задачtasks_by_project→ меню выбора проектаtasks_by_project_.*(regex) → просмотр задач проекта
Часть 2. Главное меню задач
Узел «Tasks Main Menu» (function) — сохраняет chatId в глобальную переменную и показывает главное меню задач с кнопками: «По проектам», «Назад в меню».
let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;
global.set("tasks_chat_id", chatId);
const tasksKeyboard = {
reply_markup: {
inline_keyboard: [
[{ text: "🎯 По проектам", callback_data: "tasks_by_project" }],
[{ text: "◀️ Назад в меню", callback_data: "back_main" }]
]
}
};
msg.payload = { chatId: chatId, type: "message", content: "✅ *Todoist задачи*\n\nПросмотр задач доступен через проекты:", options: { parse_mode: "Markdown", reply_markup: tasksKeyboard.reply_markup } };
return msg;
Часть 3. Меню выбора проекта
Узел «Tasks By Projects Menu» (function) — показывает список проектов с эмодзи, каждый — кнопка для просмотра задач.
const projects = [
{ name: "[НАЗВАНИЕ_ПРОЕКТА_1]", emoji: "[ЭМОДЗИ_1]", entity: "[CALENDAR_ENTITY_ID_1]" },
{ name: "[НАЗВАНИЕ_ПРОЕКТА_2]", emoji: "[ЭМОДЗИ_2]", entity: "[CALENDAR_ENTITY_ID_2]" }
];
const keyboard = [];
projects.forEach(project => {
keyboard.push([{ text: `${project.emoji} ${project.name}`, callback_data: `tasks_by_project_${project.entity}` }]);
});
keyboard.push([{ text: "◀️ Назад к задачам", callback_data: "menu_tasks" }]);
msg.payload = { chatId: chatId, type: "message", content: "🎯 *Выберите проект:*", options: { parse_mode: "Markdown", reply_markup: { inline_keyboard: keyboard } } };
return msg;
Часть 4. Получение задач из календаря
Узел «Get Tasks from Calendar» (function) — извлекает entity_id календаря из callback_data, формирует URL для запроса к Home Assistant Calendar API (период 2000-2099 годы, чтобы получить все задачи).
const chatId = global.get("tasks_chat_id");
const data = msg.payload.content;
const entityId = data.split("_").pop();
const haUrl = "[ВАШ_IP_HA]:8123";
const haToken = "[ВАШ_ДОЛГОСРОЧНЫЙ_ТОКЕН_HA]";
global.set("tasks_current_entity", entityId);
const startDate = new Date();
startDate.setFullYear(2000, 0, 1);
const endDate = new Date();
endDate.setFullYear(2099, 11, 31);
msg.url = `${haUrl}/api/calendars/${entityId}?start=${startDate.toISOString()}&end=${endDate.toISOString()}`;
msg.method = "GET";
msg.headers = { "Authorization": `Bearer ${haToken}`, "Content-Type": "application/json" };
return msg;
Узел «HTTP Request HA» (http request) — выполняет GET-запрос к Home Assistant API.
Узел «Format Calendar Response» (function) — обрабатывает ответ, форматирует даты в русском стиле, отмечает выполненные задачи (✅) и невыполненные (◻️), подсвечивает просроченные задачи (🔴 и ⚠️), добавляет кнопки обновления и навигации.
function formatDateToRussian(dateString) {
const months = ["января", "февраля", "марта", "апреля", "мая", "июня",
"июля", "августа", "сентября", "октября", "ноября", "декабря"];
const date = new Date(dateString);
return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`;
}
// Определение просрочки (МСК)
const nowMsk = new Date(new Date().getTime() + 3 * 60 * 60 * 1000);
const todayMskStr = nowMsk.toISOString().split('T')[0];
if (dueDate < todayMskStr && !isCompleted) {
isOverdue = true;
dueText = `🔴 (просрочено до ${formatDateToRussian(dueDate)})`;
}
// Иконка выполнения
const checkBox = isCompleted ? "✅" : "◻️";
5.2 Полный код
JSON для импорта в Node-RED (Меню → Import). Замените все плейсхолдеры [ВАШ_...] на свои значения.
[
{
"id": "e19002771a4f43f9",
"type": "group",
"z": "d91b643aa7cd4cef",
"name": "Задачи (не фулл)",
"style": {
"stroke": "#ffff00",
"fill": "#ffff00",
"label": true,
"label-position": "sw",
"color": "#000000"
},
"nodes": [
"tasks_callback_router",
"telegram_event_tasks",
"tasks_main_menu",
"tasks_by_project_menu",
"tasks_get_calendar_events",
"tasks_http_request",
"tasks_format_calendar_response",
"f7f6dc1e350e7d88"
],
"x": 14,
"y": 2059,
"w": 1482,
"h": 250
},
{
"id": "tasks_callback_router",
"type": "switch",
"z": "d91b643aa7cd4cef",
"g": "e19002771a4f43f9",
"name": "маршрутизатор задач",
"property": "payload.content",
"propertyType": "msg",
"rules": [
{"t": "eq", "v": "menu_tasks", "vt": "str"},
{"t": "eq", "v": "tasks_by_project", "vt": "str"},
{"t": "regex", "v": "tasks_by_project_.*", "vt": "str", "case": false}
],
"checkall": "true",
"repair": false,
"outputs": 3,
"x": 370,
"y": 2120,
"wires": [
["tasks_main_menu"],
["tasks_by_project_menu"],
["tasks_get_calendar_events"]
]
},
{
"id": "telegram_event_tasks",
"type": "telegram event",
"z": "d91b643aa7cd4cef",
"g": "e19002771a4f43f9",
"name": "Обработчик задач",
"bot": "[ID_BOT_CONFIG]",
"event": "callback_query",
"autoanswer": false,
"x": 130,
"y": 2160,
"wires": [["tasks_callback_router"]]
},
{
"id": "tasks_main_menu",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "e19002771a4f43f9",
"name": "Tasks Main Menu",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"tasks_chat_id\", chatId);\nconst tasksKeyboard = { reply_markup: { inline_keyboard: [[{ text: \"🎯 По проектам\", callback_data: \"tasks_by_project\" }], [{ text: \"◀️ Назад в меню\", callback_data: \"back_main\" }]] } };\nmsg.payload = { chatId: chatId, type: \"message\", content: \"✅ *Todoist задачи*\\n\\nПросмотр задач доступен через проекти:\", options: { parse_mode: \"Markdown\", reply_markup: tasksKeyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 670,
"y": 2100,
"wires": [["f7f6dc1e350e7d88"]]
},
{
"id": "tasks_by_project_menu",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "e19002771a4f43f9",
"name": "Tasks By Projects Menu",
"func": "let chatId = global.get(\"tasks_chat_id\");\nconst projects = [\n { name: \"[НАЗВАНИЕ_ПРОЕКТА_1]\", emoji: \"[ЭМОДЗИ_1]\", entity: \"[CALENDAR_ENTITY_ID_1]\" },\n { name: \"[НАЗВАНИЕ_ПРОЕКТА_2]\", emoji: \"[ЭМОДЗИ_2]\", entity: \"[CALENDAR_ENTITY_ID_2]\" }\n];\nconst keyboard = [];\nprojects.forEach(project => { keyboard.push([{ text: `${project.emoji} ${project.name}`, callback_data: `tasks_by_project_${project.entity}` }]); });\nkeyboard.push([{ text: \"◀️ Назад к задачам\", callback_data: \"menu_tasks\" }]);\nmsg.payload = { chatId: chatId, type: \"message\", content: \"🎯 *Выберите проект:*\", options: { parse_mode: \"Markdown\", reply_markup: { inline_keyboard: keyboard } } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 750,
"y": 2140,
"wires": [["f7f6dc1e350e7d88"]]
},
{
"id": "tasks_get_calendar_events",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "e19002771a4f43f9",
"name": "Get Tasks from Calendar",
"func": "const chatId = global.get(\"tasks_chat_id\");\nconst data = msg.payload.content;\nconst entityId = data.split(\"_\").pop();\nconst haUrl = \"[ВАШ_IP_HA]:8123\";\nconst haToken = \"[ВАШ_ДОЛГОСРОЧНЫЙ_ТОКЕН_HA]\";\nglobal.set(\"tasks_current_entity\", entityId);\nglobal.set(\"tasks_chat_id\", chatId);\nconst startDate = new Date();\nstartDate.setFullYear(2000, 0, 1);\nconst endDate = new Date();\nendDate.setFullYear(2099, 11, 31);\nmsg.url = `${haUrl}/api/calendars/${entityId}?start=${startDate.toISOString()}&end=${endDate.toISOString()}`;\nmsg.method = \"GET\";\nmsg.headers = { \"Authorization\": `Bearer ${haToken}`, \"Content-Type\": \"application/json\" };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 770,
"y": 2220,
"wires": [["tasks_http_request"]]
},
{
"id": "tasks_http_request",
"type": "http request",
"z": "d91b643aa7cd4cef",
"g": "e19002771a4f43f9",
"name": "HTTP Request HA",
"method": "GET",
"ret": "obj",
"x": 990,
"y": 2260,
"wires": [["tasks_format_calendar_response"]]
},
{
"id": "tasks_format_calendar_response",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "e19002771a4f43f9",
"name": "Format Calendar Response",
"func": "const chatId = global.get(\"tasks_chat_id\");\nlet events = msg.payload || [];\nif (events.events && Array.isArray(events.events)) events = events.events;\nif (!Array.isArray(events)) events = [];\nif (events.length === 0) {\n const emptyKeyboard = { reply_markup: { inline_keyboard: [[{ text: \"◀️ Назад к проектам\", callback_data: \"tasks_by_project\" }], [{ text: \"🏠 В главное меню\", callback_data: \"back_main\" }]] } };\n msg.payload = { chatId: chatId, type: \"message\", content: \"✅ Нет задач в этом проекте!\", options: { parse_mode: \"Markdown\", reply_markup: emptyKeyboard.reply_markup } };\n return msg;\n}\nfunction formatDateToRussian(dateString) {\n const months = [\"января\", \"февраля\", \"марта\", \"апреля\", \"мая\", \"июня\", \"июля\", \"августа\", \"сентября\", \"октября\", \"ноября\", \"декабря\"];\n const date = new Date(dateString);\n return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`;\n}\nconst nowMsk = new Date(new Date().getTime() + 3 * 60 * 60 * 1000);\nconst todayMskStr = nowMsk.toISOString().split('T')[0];\nlet message = `📋 *Проект*\\n\\n`;\nlet taskNumber = 1;\nevents.forEach(event => {\n const content = event.summary || \"Без названия\";\n const isCompleted = (event.status === \"completed\" || event.completed === true);\n const checkBox = isCompleted ? \"✅\" : \"◻️\";\n let dueDate = null;\n if (event.start) {\n if (typeof event.start === \"string\") dueDate = event.start.split('T')[0];\n else if (event.start.date) dueDate = event.start.date;\n }\n let dueText = \"\", isOverdue = false;\n if (dueDate && !isCompleted) {\n if (dueDate < todayMskStr) {\n isOverdue = true;\n dueText = `🔴 (просрочено до ${formatDateToRussian(dueDate)})`;\n } else if (dueDate === todayMskStr) {\n dueText = `📅 СЕГОДНЯ (до ${formatDateToRussian(dueDate)})`;\n } else {\n dueText = `📅 до ${formatDateToRussian(dueDate)}`;\n }\n }\n message += `${taskNumber}. ${checkBox} ${content}\\n`;\n if (dueText) message += `${dueText}\\n`;\n if (isOverdue) message += `⚠️ Просрочено!\\n`;\n message += `\\n`;\n taskNumber++;\n});\nconst keyboard = [[{ text: \"🔄 Обновить список\", callback_data: `tasks_by_project_${global.get(\"tasks_current_entity\")}` }], [{ text: \"◀️ Назад к проектам\", callback_data: \"tasks_by_project\" }], [{ text: \"🏠 В главное меню\", callback_data: \"back_main\" }]];\nmsg.payload = { chatId: chatId, type: \"message\", content: message, options: { parse_mode: \"Markdown\", reply_markup: { inline_keyboard: keyboard } } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1220,
"y": 2220,
"wires": [["f7f6dc1e350e7d88"]]
},
{
"id": "f7f6dc1e350e7d88",
"type": "telegram sender",
"z": "d91b643aa7cd4cef",
"g": "e19002771a4f43f9",
"name": "Отправка сообщения",
"bot": "[ID_BOT_CONFIG]",
"x": 1370,
"y": 2120,
"wires": [[]]
}
]
6. Дни рождения (хранение в файле Node-RED)
Достаточно простенькая ветка по вносу/редактированию и удалению дат дней рождения, вносимых вручную через бота. Всё работает только через Node-RED, сохранение данных при перезагрузке контейнера хранится в файле birthdays.json, в который пишутся все данные и вытягиваются после перезапуска.



6.1 Пошаговая настройка
Общая архитектура
Этот поток реализует просмотр дней рождения по месяцам, добавление новых записей, редактирование существующих, удаление записей, автоматические ежедневные напоминания (в 09:00). Данные хранятся в JSON-файле /data/birthdays.json в файловой системе Node-RED.
Часть 1. Вход в меню дней рождения
1.1. Перехват callback (telegram event)
- Тип узла:
telegram event - Событие:
callback_query - Привязан к боту (
[ID_BOT_CONFIG])
1.2. Маршрутизатор ДР (switch)
- Тип узла:
switch - Свойство:
msg.payload.content - Правила:
menu_birthdays→ показать меню месяцевbirthdays_month_.*→ показать именинников месяцаbirthday_add→ начать добавлениеbirthday_delete_.*→ удалить записьbirthday_edit_.*→ начать редактирование
Часть 2. Меню месяцев
Узел «Birthdays Menu» (function) — загружает данные из файла (при первом запуске), показывает список месяцев с количеством именинников, добавляет кнопку добавления и возврата.
const fs = global.get("fs");
const filePath = '/data/birthdays.json';
let birthdays = global.get("birthdays");
if (!birthdays || Object.keys(birthdays).length === 0) {
try {
if (fs.existsSync(filePath)) {
const data = fs.readFileSync(filePath, 'utf8');
birthdays = JSON.parse(data);
global.set("birthdays", birthdays);
}
} catch (err) {
node.warn("Ошибка загрузки: " + err.message);
}
}
// Подсчёт записей по месяцам
const monthCounts = new Array(12).fill(0);
for (const [name, data] of Object.entries(birthdays)) {
if (data && data.date) {
const month = parseInt(data.date.split(".")[1]) - 1;
if (month >= 0 && month < 12) monthCounts[month]++;
}
}
// Формирование меню с кнопками месяцев
Часть 3. Список именинников месяца
Узел «Get Birthdays by Month» (function) — фильтрует записи по выбранному месяцу, сортирует по дням, показывает возраст (если указан год), добавляет кнопки ✏️ Редактировать и 🗑️ Удалить для каждой записи.
function calculateAge(dateStr, year) {
if (!year) return null;
const today = new Date();
const birthDate = new Date(year, parseInt(dateStr.split(".")[1]) - 1, parseInt(dateStr.split(".")[0]));
let age = today.getFullYear() - birthDate.getFullYear();
const m = today.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) age--;
return age;
}
// Сортировка по дню месяца
filtered.sort((a, b) => {
const dayA = parseInt(a.date.split(".")[0]);
const dayB = parseInt(b.date.split(".")[0]);
return dayA - dayB;
});
Часть 4. Добавление дня рождения
Узел «Add Birthday Dialog» (function) — устанавливает флаг ожидания ввода, показывает инструкцию по формату, добавляет кнопку отмены.
Формат ввода: Фамилия Имя Отчество | ДД.ММ | ГГГГ (год можно не указывать). Примеры: Иванова Анна Сергеевна | 10.03 | 1999, Петров Сергей | 15.03.
Часть 5. Обработка текстового ввода
Узел «Text Input Handler» (function) — центральный обработчик добавления и редактирования. Проверяет формат ввода, сохраняет/обновляет запись, сохраняет данные в JSON-файл, показывает успешный результат с действиями.
function saveBirthdays(data) {
try {
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
node.warn("Данные сохранены в " + filePath);
} catch (err) {
node.warn("Ошибка сохранения: " + err.message);
}
}
// Валидация формата
if (!text.includes("|")) { /* ошибка */ }
const parts = text.split("|").map(p => p.trim());
const fullName = parts[0];
const date = parts[1];
const year = parts[2] ? parseInt(parts[2]) : null;
const dateRegex = /^\d{2}\.\d{2}$/;
if (!dateRegex.test(date)) { /* ошибка */ }
Часть 6. Редактирование и удаление
Узел «Edit Birthday Dialog» (function) — устанавливает флаг ожидания редактирования, сохраняет старое имя для удаления при обновлении, добавляет кнопки «Удалить» и «Отмена».
Узел «Delete Birthday» (function) — удаляет запись из глобальной переменной, сохраняет обновлённые данные в файл, подтверждает удаление.
Часть 7. Автоматические напоминания
Cron-триггер (inject) — настройки: crontab: "00 09 * * *" (каждый день в 09:00).
Узел формирования напоминания (function) — находит все ДР в ближайшие 7 дней (с учётом перехода года), формирует красивое напоминание с указанием «СЕГОДНЯ!», «ЗАВТРА!» или «через N дней».
var today = new Date();
today.setHours(0, 0, 0, 0);
var todayTime = today.getTime();
var upcoming = [];
for (var name in birthdays) {
var parts = data.date.split(".");
var day = parseInt(parts[0]);
var month = parseInt(parts[1]) - 1;
var birthdayThisYear = new Date(today.getFullYear(), month, day);
var birthdayTime = birthdayThisYear.getTime();
if (birthdayTime < todayTime) {
birthdayThisYear = new Date(today.getFullYear() + 1, month, day);
birthdayTime = birthdayThisYear.getTime();
}
var diffDays = Math.ceil((birthdayTime - todayTime) / (1000 * 60 * 60 * 24));
if (diffDays >= 0 && diffDays <= 7) {
upcoming.push({ name: name, date: data.date, days: diffDays, year: data.year });
}
}
functionGlobalContext: {
fs: require('fs')
}
6.2 Полный код
JSON для импорта в Node-RED (Меню → Import). Замените [ID_BOT_CONFIG], [ВАШ_БОТ], [ВАШ_USERNAME], [IP_СЕРВЕРА] на свои значения.
[
{
"id": "f811330c0e1eb8a5",
"type": "group",
"z": "d91b643aa7cd4cef",
"name": "Дни рождения",
"style": {
"stroke": "#92d04f",
"fill": "#92d04f",
"label": true,
"label-position": "sw",
"color": "#000000"
},
"nodes": [
"f5041138e2385cc7",
"2c32ea40a69be537",
"8d5ddc8b131a576f",
"c2d56fa3506ee2ee",
"3af710e543dc0c0d",
"82877cab97683bf3",
"3e5f23b5391c9235",
"b04acdd5308acdc2",
"1e91e0afa5b55e23",
"9cc4ac57a3e0a3a1",
"cron_notify",
"notify_function"
],
"x": 74,
"y": 2359,
"w": 1092,
"h": 310
},
{
"id": "f5041138e2385cc7",
"type": "telegram event",
"z": "d91b643aa7cd4cef",
"g": "f811330c0e1eb8a5",
"name": "Обработчик ДР",
"bot": "[ID_BOT_CONFIG]",
"event": "callback_query",
"autoanswer": false,
"x": 190,
"y": 2480,
"wires": [["2c32ea40a69be537"]]
},
{
"id": "2c32ea40a69be537",
"type": "switch",
"z": "d91b643aa7cd4cef",
"g": "f811330c0e1eb8a5",
"name": "маршрутизатор ДР",
"property": "payload.content",
"propertyType": "msg",
"rules": [
{"t": "eq", "v": "menu_birthdays", "vt": "str"},
{"t": "regex", "v": "birthdays_month_.*", "vt": "str", "case": false},
{"t": "eq", "v": "birthday_add", "vt": "str"},
{"t": "regex", "v": "birthday_delete_.*", "vt": "str", "case": false},
{"t": "regex", "v": "birthday_edit_.*", "vt": "str", "case": false}
],
"checkall": "true",
"repair": false,
"outputs": 5,
"x": 430,
"y": 2480,
"wires": [
["8d5ddc8b131a576f"],
["3af710e543dc0c0d"],
["82877cab97683bf3"],
["b04acdd5308acdc2"],
["1e91e0afa5b55e23"]
]
},
{
"id": "8d5ddc8b131a576f",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "f811330c0e1eb8a5",
"name": "Birthdays Menu",
"func": "const fs = global.get(\"fs\");\nconst filePath = '/data/birthdays.json';\nlet birthdays = global.get(\"birthdays\");\nif (!birthdays || Object.keys(birthdays).length === 0) {\n try {\n if (fs.existsSync(filePath)) {\n const data = fs.readFileSync(filePath, 'utf8');\n birthdays = JSON.parse(data);\n global.set(\"birthdays\", birthdays);\n }\n } catch (err) {}\n}\nbirthdays = global.get(\"birthdays\") || {};\nlet chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"birthdays_chat_id\", chatId);\nconst monthNames = [\"Январь\",\"Февраль\",\"Март\",\"Апрель\",\"Май\",\"Июнь\",\"Июль\",\"Август\",\"Сентябрь\",\"Октябрь\",\"Ноябрь\",\"Декабрь\"];\nconst monthCounts = new Array(12).fill(0);\nfor (const [name, data] of Object.entries(birthdays)) {\n if (data && data.date) {\n const month = parseInt(data.date.split(\".\")[1]) - 1;\n if (month >= 0 && month < 12) monthCounts[month]++;\n }\n}\nlet message = \"🎂 *Дни рождения*\\n\\n\";\nconst keyboard = [];\nfor (let i = 0; i < monthNames.length; i++) {\n const count = monthCounts[i];\n const displayCount = count > 0 ? ` (${count})` : \"\";\n message += `${i + 1}. ${monthNames[i]}${displayCount}\\n`;\n keyboard.push([{ text: `${monthNames[i]}${displayCount}`, callback_data: `birthdays_month_${i + 1}` }]);\n}\nkeyboard.push([{ text: \"➕ Добавить день рождения\", callback_data: \"birthday_add\" }]);\nkeyboard.push([{ text: \"◀️ Назад в меню\", callback_data: \"back_main\" }]);\nmsg.payload = { chatId: chatId, type: \"message\", content: message, options: { parse_mode: \"Markdown\", reply_markup: { inline_keyboard: keyboard } } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 720,
"y": 2400,
"wires": [["c2d56fa3506ee2ee"]]
},
{
"id": "c2d56fa3506ee2ee",
"type": "telegram sender",
"z": "d91b643aa7cd4cef",
"g": "f811330c0e1eb8a5",
"name": "Отправка сообщения",
"bot": "[ID_BOT_CONFIG]",
"x": 1040,
"y": 2480,
"wires": [[]]
},
{
"id": "3af710e543dc0c0d",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "f811330c0e1eb8a5",
"name": "Get Birthdays by Month",
"func": "let chatId = global.get(\"birthdays_chat_id\");\nlet data = msg.payload.content;\nlet month = parseInt(data.split(\"_\").pop());\nlet birthdays = global.get(\"birthdays\") || {};\nlet filtered = [];\nfor (const [name, info] of Object.entries(birthdays)) {\n const infoMonth = parseInt(info.date.split(\".\")[1]);\n if (infoMonth === month) {\n filtered.push({ name: name, date: info.date, year: info.year || null });\n }\n}\nfiltered.sort((a, b) => {\n const dayA = parseInt(a.date.split(\".\")[0]);\n const dayB = parseInt(b.date.split(\".\")[0]);\n return dayA - dayB;\n});\nconst monthNames = [\"Январь\",\"Февраль\",\"Март\",\"Апрель\",\"Май\",\"Июнь\",\"Июль\",\"Август\",\"Сентябрь\",\"Октябрь\",\"Ноябрь\",\"Декабрь\"];\nfunction calculateAge(dateStr, year) {\n if (!year) return null;\n const today = new Date();\n const birthDate = new Date(year, parseInt(dateStr.split(\".\")[1]) - 1, parseInt(dateStr.split(\".\")[0]));\n let age = today.getFullYear() - birthDate.getFullYear();\n const m = today.getMonth() - birthDate.getMonth();\n if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) age--;\n return age;\n}\nif (filtered.length === 0) {\n const emptyKeyboard = { reply_markup: { inline_keyboard: [[{ text: \"➕ Добавить\", callback_data: \"birthday_add\" }], [{ text: \"◀️ Назад к месяцам\", callback_data: \"menu_birthdays\" }]] } };\n msg.payload = { chatId: chatId, type: \"message\", content: `📅 *${monthNames[month - 1]}*\\n\\n✅ Нет дней рождений в этом месяце.`, options: { parse_mode: \"Markdown\", reply_markup: emptyKeyboard.reply_markup } };\n return msg;\n}\nlet message = `📅 *${monthNames[month - 1]}*\\n\\n`;\nconst keyboard = [];\nfiltered.forEach(person => {\n const age = calculateAge(person.date, person.year);\n message += `🎂 ${person.date} — *${person.name}*`;\n if (age) message += ` (${age} лет)`;\n message += `\\n`;\n keyboard.push([\n { text: `✏️ ${person.name.substring(0, 20)}`, callback_data: `birthday_edit_${person.name}` },\n { text: `🗑️ Удалить`, callback_data: `birthday_delete_${person.name}` }\n ]);\n});\nmessage += `\\nВыберите действие:`;\nkeyboard.push([{ text: \"➕ Добавить\", callback_data: \"birthday_add\" }]);\nkeyboard.push([{ text: \"◀️ Назад к месяцам\", callback_data: \"menu_birthdays\" }]);\nmsg.payload = { chatId: chatId, type: \"message\", content: message, options: { parse_mode: \"Markdown\", reply_markup: { inline_keyboard: keyboard } } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 730,
"y": 2440,
"wires": [["c2d56fa3506ee2ee"]]
},
{
"id": "82877cab97683bf3",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "f811330c0e1eb8a5",
"name": "Add Birthday Dialog",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"birthdays_chat_id\", chatId);\nglobal.set(\"birthdays_waiting_for\", \"birthday_data\");\nconst keyboard = { reply_markup: { inline_keyboard: [[{ text: \"◀️ Отмена\", callback_data: \"menu_birthdays\" }]] } };\nmsg.payload = { chatId: chatId, type: \"message\", content: \"✏️ *Добавление дня рождения*\\n\\nВведите данные в формате:\\n\\n`Фамилия Имя Отчество | ДД.ММ | ГГГГ`\\n\\n📌 *Примеры:*\\n`Иванова Анна Сергеевна | 10.03 | 1999`\\n`Петров Сергей | 15.03` (без отчества и года)\", options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 740,
"y": 2480,
"wires": [["c2d56fa3506ee2ee"]]
},
{
"id": "3e5f23b5391c9235",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "f811330c0e1eb8a5",
"name": "Text Input Handler",
"func": "const fs = global.get(\"fs\");\nconst filePath = '/data/birthdays.json';\nfunction saveBirthdays(data) { try { fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); } catch (err) { node.warn(\"Ошибка сохранения: \" + err.message); } }\nlet birthdays = global.get(\"birthdays\") || {};\nif (global.get(\"birthdays_waiting_for\") === \"birthday_data\" && msg.payload.chatId === global.get(\"birthdays_chat_id\")) {\n const text = msg.payload.content;\n global.set(\"birthdays_waiting_for\", null);\n if (!text.includes(\"|\")) { /* ошибка */ return; }\n const parts = text.split(\"|\").map(p => p.trim());\n if (parts.length < 2 || !parts[0] || !parts[1]) { /* ошибка */ return; }\n let fullName = parts[0];\n const date = parts[1];\n const year = parts[2] ? parseInt(parts[2]) : null;\n const dateRegex = /^\\d{2}\\.\\d{2}$/;\n if (!dateRegex.test(date)) { /* ошибка */ return; }\n birthdays[fullName] = { date: date, year: year };\n global.set(\"birthdays\", birthdays);\n saveBirthdays(birthdays);\n const month = parseInt(date.split(\".\")[1]);\n const keyboard = { reply_markup: { inline_keyboard: [[{ text: \"📅 Посмотреть в этом месяце\", callback_data: `birthdays_month_${month}` }], [{ text: \"➕ Добавить еще\", callback_data: \"birthday_add\" }], [{ text: \"◀️ Назад к месяцам\", callback_data: \"menu_birthdays\" }]] } };\n const successMsg = { payload: { chatId: msg.payload.chatId, type: \"message\", content: `✅ День рождения *${fullName}* (${date}) добавлен!`, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } } };\n node.send(successMsg);\n return;\n}\nif (global.get(\"birthdays_waiting_for\") === \"birthday_edit\" && msg.payload.chatId === global.get(\"birthdays_chat_id\")) {\n const text = msg.payload.content;\n const oldName = global.get(\"birthdays_edit_name\");\n global.set(\"birthdays_waiting_for\", null);\n if (!text.includes(\"|\")) { /* ошибка */ return; }\n const parts = text.split(\"|\").map(p => p.trim());\n if (parts.length < 2 || !parts[0] || !parts[1]) { /* ошибка */ return; }\n let fullName = parts[0];\n const date = parts[1];\n const year = parts[2] ? parseInt(parts[2]) : null;\n const dateRegex = /^\\d{2}\\.\\d{2}$/;\n if (!dateRegex.test(date)) { /* ошибка */ return; }\n delete birthdays[oldName];\n birthdays[fullName] = { date: date, year: year };\n global.set(\"birthdays\", birthdays);\n saveBirthdays(birthdays);\n const month = parseInt(date.split(\".\")[1]);\n const keyboard = { reply_markup: { inline_keyboard: [[{ text: \"📅 Посмотреть в этом месяце\", callback_data: `birthdays_month_${month}` }], [{ text: \"✏️ Редактировать другого\", callback_data: \"menu_birthdays\" }], [{ text: \"◀️ Назад к месяцам\", callback_data: \"menu_birthdays\" }]] } };\n const successMsg = { payload: { chatId: msg.payload.chatId, type: \"message\", content: `✅ День рождения *${fullName}* (${date}) обновлён!`, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } } };\n node.send(successMsg);\n return;\n}\nreturn null;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 450,
"y": 2560,
"wires": [["c2d56fa3506ee2ee"]]
},
{
"id": "b04acdd5308acdc2",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "f811330c0e1eb8a5",
"name": "Delete Birthday",
"func": "const fs = global.get(\"fs\");\nconst filePath = '/data/birthdays.json';\nfunction saveBirthdays(data) { try { fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); } catch (err) {} }\nlet chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nlet name = msg.payload.content.replace(\"birthday_delete_\", \"\");\nlet birthdays = global.get(\"birthdays\") || {};\nif (birthdays[name]) {\n delete birthdays[name];\n global.set(\"birthdays\", birthdays);\n saveBirthdays(birthdays);\n msg.payload = { chatId: chatId, type: \"message\", content: `✅ День рождения *${name}* удален!`, options: { parse_mode: \"Markdown\" } };\n} else {\n msg.payload = { chatId: chatId, type: \"message\", content: `❌ Запись для *${name}* не найдена.`, options: { parse_mode: \"Markdown\" } };\n}\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 740,
"y": 2540,
"wires": [["c2d56fa3506ee2ee"]]
},
{
"id": "1e91e0afa5b55e23",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "f811330c0e1eb8a5",
"name": "Edit Birthday Dialog",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nlet name = msg.payload.content.replace(\"birthday_edit_\", \"\");\nglobal.set(\"birthdays_chat_id\", chatId);\nglobal.set(\"birthdays_waiting_for\", \"birthday_edit\");\nglobal.set(\"birthdays_edit_name\", name);\nconst keyboard = { reply_markup: { inline_keyboard: [[{ text: \"🗑️ Удалить\", callback_data: `birthday_delete_${name}` }], [{ text: \"◀️ Отмена\", callback_data: \"menu_birthdays\" }]] } };\nmsg.payload = { chatId: chatId, type: \"message\", content: `✏️ *Редактирование дня рождения*\\n\\nТекущие данные для *${name}*.\\n\\nВведите новые данные в формате:\\n\\n\\`Фамилия Имя Отчество | ДД.ММ | ГГГГ\\``, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 700,
"y": 2580,
"wires": [["c2d56fa3506ee2ee"]]
},
{
"id": "9cc4ac57a3e0a3a1",
"type": "telegram event",
"z": "d91b643aa7cd4cef",
"g": "f811330c0e1eb8a5",
"name": "Обработчик сообщений",
"bot": "[ID_BOT_CONFIG]",
"event": "message",
"autoanswer": false,
"x": 210,
"y": 2560,
"wires": [["3e5f23b5391c9235"]]
},
{
"id": "cron_notify",
"type": "inject",
"z": "d91b643aa7cd4cef",
"g": "f811330c0e1eb8a5",
"name": "Напоминания",
"crontab": "00 09 * * *",
"once": false,
"x": 210,
"y": 2620,
"wires": [["notify_function"]]
},
{
"id": "notify_function",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "f811330c0e1eb8a5",
"name": "Формирование напоминания",
"func": "var birthdays = global.get(\"birthdays\") || {};\nvar chatId = global.get(\"birthdays_chat_id\");\nif (!chatId) return null;\nvar today = new Date();\ntoday.setHours(0, 0, 0, 0);\nvar todayTime = today.getTime();\nvar upcoming = [];\nfor (var name in birthdays) {\n var data = birthdays[name];\n if (!data || !data.date) continue;\n var parts = data.date.split(\".\");\n var day = parseInt(parts[0]);\n var month = parseInt(parts[1]) - 1;\n var birthdayThisYear = new Date(today.getFullYear(), month, day);\n var birthdayTime = birthdayThisYear.getTime();\n if (birthdayTime < todayTime) {\n birthdayThisYear = new Date(today.getFullYear() + 1, month, day);\n birthdayTime = birthdayThisYear.getTime();\n }\n var diffDays = Math.ceil((birthdayTime - todayTime) / (1000 * 60 * 60 * 24));\n if (diffDays >= 0 && diffDays <= 7) {\n upcoming.push({ name: name, date: data.date, days: diffDays, year: data.year });\n }\n}\nif (upcoming.length > 0) {\n upcoming.sort(function(a, b) { return a.days - b.days; });\n var message = \"🎉 *Напоминание о днях рождения!*\\n\\n\";\n for (var i = 0; i < upcoming.length; i++) {\n var person = upcoming[i];\n var daysText = person.days === 0 ? \"🎉 СЕГОДНЯ! 🎉\" : (person.days === 1 ? \"📅 ЗАВТРА!\" : \"📅 через \" + person.days + \" дня\");\n var ageText = person.year ? \" — \" + (new Date().getFullYear() - person.year) + \" лет\" : \"\";\n message += \"🎂 \" + person.date + \" — *\" + person.name + \"*\" + ageText + \"\\n \" + daysText + \"\\n\\n\";\n }\n msg.payload = { chatId: chatId, type: \"message\", content: message, options: { parse_mode: \"Markdown\" } };\n return msg;\n}\nreturn null;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 360,
"y": 2620,
"wires": [["c2d56fa3506ee2ee"]]
},
{
"id": "[ID_BOT_CONFIG]",
"type": "telegram bot",
"botname": "[ВАШ_БОТ]",
"usernames": "[ВАШ_USERNAME]",
"updatemode": "polling",
"pollinterval": "300",
"localbothost": "[IP_СЕРВЕРА]",
"localbotport": 8443
},
{
"id": "ef2ca67c0771fa6d",
"type": "global-config",
"env": [],
"modules": {
"node-red-contrib-telegrambot": "17.4.12"
}
}
]
7. Вызов сценария медитации
Ранее был реализован на другом потоке вызов сценария медитации. Здесь сделана простенькая ветка вызова с телефона без обращения к Алисе.

7.1 Пошаговая настройка
Общая архитектура
Этот поток позволяет открыть меню медитации, запустить сценарий релаксации (приглушение света, включение музыки), получить подтверждение о начале медитации через 1 минуту.
Ключевая особенность: Использует link call для запуска автоматизации в Home Assistant (отправляет команду в другой поток).
Часть 1. Вход в меню медитации
1.1. Перехват callback (telegram event)
- Тип узла:
telegram event - Событие:
callback_query - Привязан к боту (
[ID_BOT_CONFIG]) autoanswer: false
1.2. Маршрутизатор (switch)
- Тип узла:
switch - Свойство:
msg.payload.content - Правила:
menu_meditation→ показать меню медитацииmeditation_start→ запустить сценарий
Часть 2. Меню медитации
Узел «Meditation Menu» (function) — показывает меню с кнопкой «Начать медитацию» и даёт инструкции пользователю.
Часть 3. Запуск медитации
Узел «Start Meditation» (function) — сохраняет chatId в глобальную переменную, формирует команду { command: "start_meditation" } для передачи в другой поток.
let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;
global.set("meditation_chat_id", chatId);
msg.payload = { command: "start_meditation" };
return msg;
Узел «Call Meditation Flow» (link call) — отправляет команду в другой поток Node-RED. Настройки: links: ["[ID_LINK_IN]"], timeout: "30" секунд.
link in с ID [ID_LINK_IN] должен существовать в другом месте конфигурации (автоматизация Home Assistant).
Узел «Задержка 1 минута» (delay) — ожидает 1 минуту перед отправкой подтверждения пользователю. Это даёт время на выполнение автоматизации в Home Assistant.
Узел «Meditation Response» (function) — через 1 минуту после нажатия кнопки отправляет подтверждение, что свет приглушён и музыка играет, с кнопкой возврата в главное меню.
const chatId = global.get("meditation_chat_id");
if (!chatId) return null;
const keyboard = { reply_markup: { inline_keyboard: [[{ text: "◀️ Назад в меню", callback_data: "back_main" }]] } };
msg.payload = {
chatId: chatId,
type: "message",
content: "🧘 *Медитация началась!*\n\nСвет приглушен, музыка играет.\nНаслаждайтесь моментом покоя.",
options: { parse_mode: "Markdown", reply_markup: keyboard.reply_markup }
};
return msg;
7.2 Полный код
JSON для импорта в Node-RED (Меню → Import). Замените [ID_BOT_CONFIG], [ID_LINK_IN], [ВАШ_БОТ], [ВАШ_USERNAME], [IP_СЕРВЕРА] на свои значения.
[
{
"id": "1e184013dbafdd3b",
"type": "group",
"z": "d91b643aa7cd4cef",
"name": "Медитация",
"style": {
"stroke": "#92d04f",
"fill": "#92d04f",
"label": true,
"label-position": "sw",
"color": "#001f60"
},
"nodes": [
"8fccb91f87903b54",
"37acdb85f753c54d",
"dc849ead1bb224e1",
"78370be4dc08e199",
"ec8ca1c0ac2be3c8",
"80a3e6e0984c2deb",
"2ac8f632d0448c1f",
"f04bb2ec130f0035"
],
"x": 54,
"y": 1539,
"w": 1212,
"h": 170
},
{
"id": "8fccb91f87903b54",
"type": "telegram event",
"z": "d91b643aa7cd4cef",
"g": "1e184013dbafdd3b",
"name": "Обработчик нажатия кнопок",
"bot": "[ID_BOT_CONFIG]",
"event": "callback_query",
"autoanswer": false,
"x": 210,
"y": 1580,
"wires": [["37acdb85f753c54d"]]
},
{
"id": "37acdb85f753c54d",
"type": "switch",
"z": "d91b643aa7cd4cef",
"g": "1e184013dbafdd3b",
"name": "маршрутизатор menu",
"property": "payload.content",
"propertyType": "msg",
"rules": [
{"t": "eq", "v": "menu_meditation", "vt": "str"},
{"t": "eq", "v": "meditation_start", "vt": "str"}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 470,
"y": 1580,
"wires": [
["dc849ead1bb224e1"],
["78370be4dc08e199"]
]
},
{
"id": "dc849ead1bb224e1",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "1e184013dbafdd3b",
"name": "Meditation Menu",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nconst meditationKeyboard = { reply_markup: { inline_keyboard: [[{ text: \"🧘 Начать медитацию\", callback_data: \"meditation_start\" }], [{ text: \"◀️ Назад в меню\", callback_data: \"back_main\" }]] } };\nmsg.payload = { chatId: chatId, type: \"message\", content: \"🧘 *Медитация*\\n\\nНастройте свет, включите спокойную музыку и начните практику.\\n\\nНажмите кнопку ниже, чтобы запустить сценарий отдыха:\", options: { parse_mode: \"Markdown\", reply_markup: meditationKeyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 710,
"y": 1580,
"wires": [["80a3e6e0984c2deb"]]
},
{
"id": "78370be4dc08e199",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "1e184013dbafdd3b",
"name": "Start Meditation",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"meditation_chat_id\", chatId);\nmsg.payload = { command: \"start_meditation\" };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 700,
"y": 1620,
"wires": [
["ec8ca1c0ac2be3c8"],
["f04bb2ec130f0035"]
]
},
{
"id": "ec8ca1c0ac2be3c8",
"type": "link call",
"z": "d91b643aa7cd4cef",
"g": "1e184013dbafdd3b",
"name": "Call Meditation Flow",
"links": ["[ID_LINK_IN]"],
"linkType": "static",
"timeout": "30",
"x": 900,
"y": 1620,
"wires": [[]]
},
{
"id": "80a3e6e0984c2deb",
"type": "telegram sender",
"z": "d91b643aa7cd4cef",
"g": "1e184013dbafdd3b",
"name": "Отправка сообщения",
"bot": "[ID_BOT_CONFIG]",
"x": 1140,
"y": 1580,
"wires": [[]]
},
{
"id": "2ac8f632d0448c1f",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "1e184013dbafdd3b",
"name": "Meditation Response",
"func": "const chatId = global.get(\"meditation_chat_id\");\nif (!chatId) return null;\nconst keyboard = { reply_markup: { inline_keyboard: [[{ text: \"◀️ Назад в меню\", callback_data: \"back_main\" }]] } };\nmsg.payload = { chatId: chatId, type: \"message\", content: \"🧘 *Медитация началась!*\\n\\nСвет приглушен, музыка играет.\\nНаслаждайтесь моментом покоя.\", options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1120,
"y": 1660,
"wires": [["80a3e6e0984c2deb"]]
},
{
"id": "f04bb2ec130f0035",
"type": "delay",
"z": "d91b643aa7cd4cef",
"g": "1e184013dbafdd3b",
"name": "Задержка 1 минута",
"pauseType": "delay",
"timeout": "1",
"timeoutUnits": "minutes",
"x": 900,
"y": 1660,
"wires": [["2ac8f632d0448c1f"]]
},
{
"id": "[ID_BOT_CONFIG]",
"type": "telegram bot",
"botname": "[ВАШ_БОТ]",
"usernames": "[ВАШ_USERNAME]",
"updatemode": "polling",
"pollinterval": "300",
"localbothost": "[IP_СЕРВЕРА]",
"localbotport": 8443
},
{
"id": "f9ec15a2562cf4e4",
"type": "global-config",
"env": [],
"modules": {
"node-red-contrib-telegrambot": "17.4.12"
}
}
]
8. Погода с рекомендациями
Максимально простой сценарий, который выводит погоду на улице и даёт рекомендации: например, взять зонтик или SPF-крем. Погода берётся из встроенной интеграции Home Assistant, но можно сделать интеграцию с Яндекс/Гисметео и т.д.

8.1 Пошаговая настройка
Общая архитектура
Этот поток позволяет получить текущую погоду из Home Assistant, отобразить температуру, влажность, ветер, давление, облачность, УФ-индекс, получить персонализированные рекомендации на основе погодных условий и конвертировать давление из гПа в мм рт. ст.
Часть 1. Вход в меню погоды
1.1. Перехват callback (telegram event)
- Тип узла:
telegram event - Событие:
callback_query - Привязан к боту (
[ID_BOT_CONFIG]) autoanswer: false
1.2. Маршрутизатор (switch)
- Тип узла:
switch - Свойство:
msg.payload.content - Правило:
menu_weather→ показать погоду
Часть 2. Инициализация
Узел «Init погода» (function) — сохраняет chatId в глобальную переменную для последующей отправки ответа.
let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;
global.set("weather_chat_id", chatId);
return msg;
Часть 3. Получение данных погоды
Узел «Get Погода» (api-current-state) — запрашивает текущее состояние погоды из Home Assistant.
- Entity ID:
weather.forecast_home_assistant(ваш погодный сервис) - Выходные поля:
msg.payload— состояние (sunny, rainy и т.д.),msg.data.attributes— детальные параметры
Пример ответа от HA:
{
"payload": "sunny",
"data": {
"attributes": {
"temperature": 22.5,
"humidity": 65,
"wind_speed": 12,
"pressure": 1013,
"cloud_coverage": 20,
"uv_index": 5
}
}
}
Часть 4. Анализ и форматирование
Узел «Send погода» (function) — выполняет извлечение данных, конвертацию давления из гПа в мм рт. ст., генерацию рекомендаций и формирование сообщения.
const temp = msg.data.attributes.temperature;
const humidity = msg.data.attributes.humidity;
const wind = msg.data.attributes.wind_speed;
const pressure_hpa = msg.data.attributes.pressure;
const cloud = msg.data.attributes.cloud_coverage;
const uv = msg.data.attributes.uv_index;
const condition = msg.payload;
// Конвертация давления: гПа → мм рт. ст.
const pressure_mm = (pressure_hpa * 0.750062).toFixed(1);
let recommendations = [];
if (condition === "rainy") recommendations.push("☔ Возьмите зонт — на улице дождь!");
if (temp > 25) recommendations.push("🧴 Жарко! Используйте солнцезащитный крем и пейте воду.");
if (temp < 10) recommendations.push("🧥 Холодно! Наденьте куртку.");
if (temp < 0) recommendations.push("🧣 Мороз! Шапка и перчатки обязательны.");
if (humidity > 70) recommendations.push("💧 Высокая влажность — может быть душно.");
if (wind > 15) recommendations.push("💨 Сильный ветер! Будьте осторожны.");
if (pressure_hpa < 1000) recommendations.push("📉 Низкое давление — возможна головная боль.");
if (pressure_hpa > 1030) recommendations.push("📈 Высокое давление — метеозависимым быть внимательнее.");
if (uv > 5) recommendations.push("🧴 Высокий УФ-индекс! Обязательно используйте солнцезащитный крем.");
if (cloud > 80 && condition !== "rainy") recommendations.push("☁️ Пасмурно, солнца не будет.");
if (temp >= 18 && temp <= 25 && condition !== "rainy" && condition !== "cloudy")
recommendations.push("🌿 Отличная погода для прогулки!");
if (recommendations.length === 0) recommendations.push("✅ Погода спокойная. Хорошего дня!");
Полный список рекомендаций
- ☔ Дождь → «Возьмите зонт»
- ❄️ Снег → «Идет снег, оденьтесь теплее»
- 🕶️ Солнечно → «Не забудьте очки и крем»
- 🧴 Температура > 25°C → «Пейте воду, используйте крем»
- 🧥 Температура < 10°C → «Наденьте куртку»
- 🧣 Температура < 0°C → «Шапка и перчатки обязательны»
- 💧 Влажность > 70% → «Может быть душно»
- 🏜️ Влажность < 30% → «Увлажните воздух дома»
- 💨 Ветер > 15 км/ч → «Будьте осторожны»
- ⚠️ Ветер > 30 км/ч → «Лучше оставаться дома»
- 📉 Давление < 1000 гПа → «Возможна головная боль»
- 📈 Давление > 1030 гПа → «Метеозависимым быть внимательнее»
- 🧴 УФ-индекс > 5 → «Обязательно используйте крем»
- ☁️ Облачность > 80% → «Солнца не будет»
- 🌿 Температура 18-25°C, без дождя и облачности → «Отличная погода для прогулки»
8.2 Полный код
JSON для импорта в Node-RED (Меню → Import). Замените [ID_BOT_CONFIG], [ID_HA_SERVER], [ВАШ_БОТ], [ВАШ_USERNAME], [IP_СЕРВЕРА] на свои значения.
[
{
"id": "9840988744a3239e",
"type": "group",
"z": "d91b643aa7cd4cef",
"name": "Погода",
"style": {
"stroke": "#92d04f",
"fill": "#92d04f",
"label": true,
"label-position": "sw",
"color": "#000000"
},
"nodes": [
"f357a12ea8af31ce",
"06e0c7e3fba12102",
"198e4eb80d4e4577",
"cd0c09ad47743b6f",
"afb611c40f5d9824",
"ce786ab16fe372d6"
],
"x": 134,
"y": 459,
"w": 1272,
"h": 90
},
{
"id": "f357a12ea8af31ce",
"type": "switch",
"z": "d91b643aa7cd4cef",
"g": "9840988744a3239e",
"name": "маршрутизатор погода",
"property": "payload.content",
"propertyType": "msg",
"rules": [
{"t": "eq", "v": "menu_weather", "vt": "str"}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 550,
"y": 500,
"wires": [["198e4eb80d4e4577"]]
},
{
"id": "06e0c7e3fba12102",
"type": "telegram event",
"z": "d91b643aa7cd4cef",
"g": "9840988744a3239e",
"name": "Обработчик нажатия кнопок",
"bot": "[ID_BOT_CONFIG]",
"event": "callback_query",
"autoanswer": false,
"x": 290,
"y": 500,
"wires": [["f357a12ea8af31ce"]]
},
{
"id": "198e4eb80d4e4577",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "9840988744a3239e",
"name": "Init погода",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"weather_chat_id\", chatId);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 750,
"y": 500,
"wires": [["cd0c09ad47743b6f"]]
},
{
"id": "cd0c09ad47743b6f",
"type": "api-current-state",
"z": "d91b643aa7cd4cef",
"g": "9840988744a3239e",
"name": "Get Погода",
"server": "[ID_HA_SERVER]",
"entity_id": "weather.forecast_home_assistant",
"outputProperties": [
{
"property": "payload",
"propertyType": "msg",
"value": "string",
"valueType": "entityState"
},
{
"property": "data",
"propertyType": "msg",
"value": "",
"valueType": "entity"
}
],
"x": 910,
"y": 500,
"wires": [["afb611c40f5d9824"]]
},
{
"id": "afb611c40f5d9824",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "9840988744a3239e",
"name": "Send погода",
"func": "let chatId = global.get(\"weather_chat_id\");\nif (!chatId) return null;\nconst temp = msg.data.attributes.temperature;\nconst humidity = msg.data.attributes.humidity;\nconst wind = msg.data.attributes.wind_speed;\nconst pressure_hpa = msg.data.attributes.pressure;\nconst cloud = msg.data.attributes.cloud_coverage;\nconst uv = msg.data.attributes.uv_index;\nconst condition = msg.payload;\nconst pressure_mm = (pressure_hpa * 0.750062).toFixed(1);\nlet rain = \"no\";\nif (condition === \"rainy\" || condition === \"pouring\" || condition === \"rain\") rain = \"rain\";\nelse if (condition === \"snowy\" || condition === \"snow\") rain = \"snow\";\nlet weatherIcon = \"🌤️\";\nif (condition === \"sunny\" || condition === \"clear\") weatherIcon = \"☀️\";\nelse if (condition === \"rainy\") weatherIcon = \"☔\";\nelse if (condition === \"snowy\") weatherIcon = \"❄️\";\nelse if (condition === \"cloudy\") weatherIcon = \"☁️\";\nelse if (condition === \"foggy\") weatherIcon = \"🌫️\";\nlet recommendations = [];\nif (rain === \"rain\") recommendations.push(\"☔ Возьмите зонт — на улице дождь!\");\nelse if (rain === \"snow\") recommendations.push(\"❄️ Идет снег, оденьтесь теплее!\");\nif (condition === \"sunny\" || condition === \"clear\") recommendations.push(\"🕶️ Солнечно! Не забудьте очки и крем от загара.\");\nif (temp > 25) recommendations.push(\"🧴 Жарко! Используйте солнцезащитный крем и пейте воду.\");\nif (temp < 10) recommendations.push(\"🧥 Холодно! Наденьте куртку.\");\nif (temp < 0) recommendations.push(\"🧣 Мороз! Шапка и перчатки обязательны.\");\nif (humidity > 70) recommendations.push(\"💧 Высокая влажность — может быть душно.\");\nelse if (humidity < 30) recommendations.push(\"🏜️ Низкая влажность — увлажните воздух дома.\");\nif (wind > 15) recommendations.push(\"💨 Сильный ветер! Будьте осторожны.\");\nif (wind > 30) recommendations.push(\"⚠️ Очень сильный ветер — лучше оставаться дома.\");\nif (pressure_hpa < 1000) recommendations.push(\"📉 Низкое давление — возможна головная боль.\");\nelse if (pressure_hpa > 1030) recommendations.push(\"📈 Высокое давление — метеозависимым быть внимательнее.\");\nif (uv > 5) recommendations.push(\"🧴 Высокий УФ-индекс! Обязательно используйте солнцезащитный крем.\");\nif (cloud > 80 && rain !== \"rain\") recommendations.push(\"☁️ Пасмурно, солнца не будет.\");\nif (temp >= 18 && temp <= 25 && rain !== \"rain\" && condition !== \"cloudy\") recommendations.push(\"🌿 Отличная погода для прогулки!\");\nif (recommendations.length === 0) recommendations.push(\"✅ Погода спокойная. Хорошего дня!\");\nlet message = `${weatherIcon} *Погода на улице*\\n\\n`;\nmessage += `🌡️ Температура: ${temp}°C\\n`;\nmessage += `💧 Влажность: ${humidity}%\\n`;\nmessage += `💨 Ветер: ${wind} км/ч\\n`;\nmessage += `📊 Давление: ${pressure_mm} мм рт. ст.\\n`;\nmessage += `☁️ Облачность: ${cloud}%\\n`;\nif (uv > 0) message += `☀️ УФ-индекс: ${uv}\\n`;\nmessage += `🌧️ Осадки: ${rain === \"rain\" ? \"Дождь\" : (rain === \"snow\" ? \"Снег\" : \"Нет\")}\\n\\n`;\nmessage += `*📋 Рекомендации:*\\n`;\nmessage += recommendations.join(\"\\n\");\nconst keyboard = { reply_markup: { inline_keyboard: [[{ text: \"◀️ Назад в меню\", callback_data: \"back_main\" }]] } };\nglobal.set(\"weather_chat_id\", null);\nmsg.payload = { chatId: chatId, type: \"message\", content: message, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1070,
"y": 500,
"wires": [["ce786ab16fe372d6"]]
},
{
"id": "ce786ab16fe372d6",
"type": "telegram sender",
"z": "d91b643aa7cd4cef",
"g": "9840988744a3239e",
"name": "Отправка сообщения",
"bot": "[ID_BOT_CONFIG]",
"x": 1280,
"y": 500,
"wires": [[]]
},
{
"id": "[ID_BOT_CONFIG]",
"type": "telegram bot",
"botname": "[ВАШ_БОТ]",
"usernames": "[ВАШ_USERNAME]",
"updatemode": "polling",
"pollinterval": "300",
"localbothost": "[IP_СЕРВЕРА]",
"localbotport": 8443
},
{
"id": "[ID_HA_SERVER]",
"type": "server",
"name": "Home Assistant",
"version": 6,
"addon": false
},
{
"id": "0f9f74fa04aeb272",
"type": "global-config",
"env": [],
"modules": {
"node-red-contrib-telegrambot": "17.4.12",
"node-red-contrib-home-assistant-websocket": "0.80.3"
}
}
]
9. Мониторинг датчиков и сенсоров
Ветка для мониторинга показаний всех датчиков и сенсоров, которые есть в доме. В данном случае это окна, лампочки, датчик BME680 (температура, влажность, давление, качество воздуха), а также PIR-датчик движения.


9.1 Пошаговая настройка
Схема потока (укрупнённо)
- Главное меню → Мониторинг → Три основных направления:
- Телеметрия (monitor_telemetry) → меню выбора показателя → запрос конкретного датчика → ответ
- PIR датчик (monitor_pir) → сохранение chatId → запрос состояния → ответ
- Окно (monitor_window) → сохранение chatId → запрос состояния → ответ
Часть 1. Вход в меню мониторинга
1.1. Перехват callback (telegram event)
- Тип узла:
telegram event - Событие:
callback_query - Привязан к боту (
[ID_BOT_CONFIG])
1.2. Маршрутизатор мониторинга (switch)
- Тип узла:
switch - Свойство:
msg.payload.content - Правила:
monitor_telemetry→ меню телеметрииmonitor_pir→ PIR датчик движенияmonitor_window→ состояние окнаsensor_temperature→ температураsensor_humidity→ влажностьsensor_pressure→ давлениеsensor_iaq→ качество воздуха (IAQ)menu_monitoring→ главное меню мониторинга
Часть 2. Главное меню мониторинга
Узел «Monitoring Menu» (function) — показывает главное меню мониторинга с тремя категориями: Телеметрия, PIR датчик, Окно.
Часть 3. Телеметрия (датчики окружающей среды)
Узел «Telemetry Menu» (function) — показывает меню выбора конкретного датчика телеметрии: температура, влажность, давление, качество воздуха (IAQ).
Запрос температуры:
- Init Temperature → сохраняет chatId
- Get Temperature (
api-current-state) → entity_id:sensor.test_product_temperatura - Send температура → форматирует ответ с кнопкой «Назад в телеметрию»
Запрос влажности: аналогично, entity_id: sensor.test_product_vlazhnost
Запрос давления: аналогично, entity_id: sensor.test_product_davlenie
Запрос качества воздуха (IAQ):
- Init IAQ → сохраняет chatId
- Get IAQ (
api-current-state) → entity_id:sensor.test_product_kachestvo_vozdukha - Send IAQ → расшифровывает значения и показывает цветовую индикацию
let iaqText = "";
const iaqValue = String(msg.payload).toLowerCase();
if (iaqValue === "good" || iaqValue === "excellent") iaqText = "🟢 Отличное";
else if (iaqValue === "moderate" || iaqValue === "fair") iaqText = "🟡 Умеренное";
else if (iaqValue === "poor" || iaqValue === "unhealthy") iaqText = "🟠 Плохое";
else if (iaqValue === "very poor" || iaqValue === "bad" || iaqValue === "very unhealthy") iaqText = "🔴 Очень плохое";
else iaqText = msg.payload;
Часть 4. PIR датчик движения
Init PIR (function) — сохраняет chatId в глобальную переменную.
Get PIR (api-current-state) — entity_id: binary_sensor.test_product_zaniatost
Send PIR (function) — формирует ответ с иконкой 🚶♂️/💤 и статусом «Детектировано движение» / «Нет движения».
let state = msg.payload;
let status = state === "on" ? "🔴 Детектировано движение" : "⚫ Нет движения";
let icon = state === "on" ? "🚶♂️" : "💤";
const keyboard = { reply_markup: { inline_keyboard: [[{ text: "◀️ Назад в мониторинг", callback_data: "menu_monitoring" }]] } };
msg.payload = { chatId: chatId, type: "message", content: `${icon} *PIR Датчик движения:*\n${status}`, options: { parse_mode: "Markdown", reply_markup: keyboard.reply_markup } };
global.set("pir_chat_id", null);
Часть 5. Датчик окна
Init Window (function) — сохраняет chatId.
Get Window (api-current-state) — entity_id: binary_sensor.test_product_dver_1
Send Window (function) — формирует ответ с иконкой ⚠️/✅ и статусом «Открыто» / «Закрыто».
9.2 Полный код
JSON для импорта в Node-RED (Меню → Import). Замените [ID_BOT_CONFIG], [ID_HA_SERVER], [ВАШ_БОТ], [ВАШ_USERNAME], [IP_СЕРВЕРА] на свои значения.
[
{
"id": "40709ee4bf606d70",
"type": "group",
"z": "d91b643aa7cd4cef",
"name": "Мониторинг",
"style": {
"stroke": "#92d04f",
"fill": "#92d04f",
"label": true,
"label-position": "sw",
"color": "#000000"
},
"nodes": [
"c02b1bb4ad8d2e09",
"4e9afbc910a02ab8",
"ab02b6f1fea816e8",
"db3db795046abbc9",
"0d4eb56076d0399c",
"cd4dbf063a566602",
"abe9f8b03d91c29b",
"3c7b031394b504b4",
"934b95c49a6d60c4",
"395a5bc4df7a7ea3",
"01881c0fe29e38ed",
"6257721be80c11d0",
"51f07c88e8c27a53",
"20eb7c78346b83f9",
"45ce9898c0c48b11",
"cfde414470594a31",
"d261af2ad13a9a31",
"2358f1f5037610f2",
"14283ab4cc1ed28b",
"c7a764c75a822b04",
"318a4dee4563cb3c",
"0c9e1265af100a13",
"f1827f9626cf94c1"
],
"x": 34,
"y": 579,
"w": 1552,
"h": 330
},
{
"id": "c02b1bb4ad8d2e09",
"type": "api-current-state",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Get Temperature",
"server": "[ID_HA_SERVER]",
"entity_id": "sensor.test_product_temperatura",
"x": 930,
"y": 660,
"wires": [["20eb7c78346b83f9"]]
},
{
"id": "4e9afbc910a02ab8",
"type": "api-current-state",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Get Humidity",
"server": "[ID_HA_SERVER]",
"entity_id": "sensor.test_product_vlazhnost",
"x": 910,
"y": 700,
"wires": [["45ce9898c0c48b11"]]
},
{
"id": "ab02b6f1fea816e8",
"type": "api-current-state",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Get Pressure",
"server": "[ID_HA_SERVER]",
"entity_id": "sensor.test_product_davlenie",
"x": 910,
"y": 740,
"wires": [["cfde414470594a31"]]
},
{
"id": "db3db795046abbc9",
"type": "api-current-state",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Get IAQ",
"server": "[ID_HA_SERVER]",
"entity_id": "sensor.test_product_kachestvo_vozdukha",
"x": 900,
"y": 780,
"wires": [["d261af2ad13a9a31"]]
},
{
"id": "0d4eb56076d0399c",
"type": "api-current-state",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Get PIR",
"server": "[ID_HA_SERVER]",
"entity_id": "binary_sensor.test_product_zaniatost",
"x": 1020,
"y": 820,
"wires": [["abe9f8b03d91c29b"]]
},
{
"id": "cd4dbf063a566602",
"type": "api-current-state",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Get Window",
"server": "[ID_HA_SERVER]",
"entity_id": "binary_sensor.test_product_dver_1",
"x": 1030,
"y": 860,
"wires": [["3c7b031394b504b4"]]
},
{
"id": "abe9f8b03d91c29b",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Send PIR",
"func": "let chatId = global.get(\"pir_chat_id\");\nif (!chatId) { chatId = msg.originalMessage?.chat?.id || msg.chatId; }\nif (!chatId) return null;\nlet state = msg.payload;\nlet status = state === \"on\" ? \"🔴 Детектировано движение\" : \"⚫ Нет движения\";\nlet icon = state === \"on\" ? \"🚶♂️\" : \"💤\";\nconst keyboard = { reply_markup: { inline_keyboard: [[{ text: \"◀️ Назад в мониторинг\", callback_data: \"menu_monitoring\" }]] } };\nmsg.payload = { chatId: chatId, type: \"message\", content: `${icon} *PIR Датчик движения:*\\n${status}`, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nglobal.set(\"pir_chat_id\", null);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1160,
"y": 820,
"wires": [["01881c0fe29e38ed"]]
},
{
"id": "3c7b031394b504b4",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Send Window",
"func": "let chatId = global.get(\"window_chat_id\");\nif (!chatId) { chatId = msg.originalMessage?.chat?.id || msg.chatId; }\nif (!chatId) return null;\nlet state = msg.payload;\nlet status = state === \"on\" ? \"🪟 Открыто\" : \"🔒 Закрыто\";\nlet icon = state === \"on\" ? \"⚠️\" : \"✅\";\nconst keyboard = { reply_markup: { inline_keyboard: [[{ text: \"◀️ Назад в мониторинг\", callback_data: \"menu_monitoring\" }]] } };\nmsg.payload = { chatId: chatId, type: \"message\", content: `${icon} *Окно:* ${status}`, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nglobal.set(\"window_chat_id\", null);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1200,
"y": 860,
"wires": [["01881c0fe29e38ed"]]
},
{
"id": "934b95c49a6d60c4",
"type": "telegram event",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Обработчик нажатия кнопок",
"bot": "[ID_BOT_CONFIG]",
"event": "callback_query",
"autoanswer": false,
"x": 190,
"y": 680,
"wires": [["395a5bc4df7a7ea3"]]
},
{
"id": "395a5bc4df7a7ea3",
"type": "switch",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "маршрутизатор monitoring",
"property": "payload.content",
"propertyType": "msg",
"rules": [
{"t": "eq", "v": "monitor_telemetry", "vt": "str"},
{"t": "eq", "v": "monitor_pir", "vt": "str"},
{"t": "eq", "v": "monitor_window", "vt": "str"},
{"t": "eq", "v": "sensor_temperature", "vt": "str"},
{"t": "eq", "v": "sensor_humidity", "vt": "str"},
{"t": "eq", "v": "sensor_pressure", "vt": "str"},
{"t": "eq", "v": "sensor_iaq", "vt": "str"},
{"t": "eq", "v": "menu_monitoring", "vt": "str"}
],
"checkall": "true",
"repair": false,
"outputs": 8,
"x": 460,
"y": 680,
"wires": [
["2358f1f5037610f2"],
["6257721be80c11d0"],
["51f07c88e8c27a53"],
["14283ab4cc1ed28b"],
["c7a764c75a822b04"],
["318a4dee4563cb3c"],
["0c9e1265af100a13"],
["f1827f9626cf94c1"]
]
},
{
"id": "01881c0fe29e38ed",
"type": "telegram sender",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Отправка сообщения",
"bot": "[ID_BOT_CONFIG]",
"x": 1460,
"y": 640,
"wires": [[]]
},
{
"id": "6257721be80c11d0",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Init PIR",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"pir_chat_id\", chatId);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 880,
"y": 820,
"wires": [["0d4eb56076d0399c"]]
},
{
"id": "51f07c88e8c27a53",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Init Window",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"window_chat_id\", chatId);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 870,
"y": 860,
"wires": [["cd4dbf063a566602"]]
},
{
"id": "20eb7c78346b83f9",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Send температура",
"func": "let chatId = global.get(\"telemetry_chat_id\");\nif (!chatId) return null;\nconst keyboard = { reply_markup: { inline_keyboard: [[{ text: \"◀️ Назад в телеметрию\", callback_data: \"monitor_telemetry\" }]] } };\nmsg.payload = { chatId: chatId, type: \"message\", content: `🌡️ Температура: ${msg.payload}°C`, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1130,
"y": 660,
"wires": [["01881c0fe29e38ed"]]
},
{
"id": "45ce9898c0c48b11",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Send влажность",
"func": "let chatId = global.get(\"telemetry_chat_id\");\nif (!chatId) return null;\nconst keyboard = { reply_markup: { inline_keyboard: [[{ text: \"◀️ Назад в телеметрию\", callback_data: \"monitor_telemetry\" }]] } };\nmsg.payload = { chatId: chatId, type: \"message\", content: `💧 Влажность: ${msg.payload}%`, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1130,
"y": 700,
"wires": [["01881c0fe29e38ed"]]
},
{
"id": "cfde414470594a31",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Send давление",
"func": "let chatId = global.get(\"telemetry_chat_id\");\nif (!chatId) return null;\nconst keyboard = { reply_markup: { inline_keyboard: [[{ text: \"◀️ Назад в телеметрию\", callback_data: \"monitor_telemetry\" }]] } };\nmsg.payload = { chatId: chatId, type: \"message\", content: `📊 Давление: ${msg.payload} мм.рт`, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1120,
"y": 740,
"wires": [["01881c0fe29e38ed"]]
},
{
"id": "d261af2ad13a9a31",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Send IAQ",
"func": "let chatId = global.get(\"telemetry_chat_id\");\nif (!chatId) return null;\nconst keyboard = { reply_markup: { inline_keyboard: [[{ text: \"◀️ Назад в телеметрию\", callback_data: \"monitor_telemetry\" }]] } };\nlet iaqText = \"\";\nconst iaqValue = String(msg.payload).toLowerCase();\nif (iaqValue === \"good\" || iaqValue === \"excellent\") iaqText = \"🟢 Отличное\";\nelse if (iaqValue === \"moderate\" || iaqValue === \"fair\") iaqText = \"🟡 Умеренное\";\nelse if (iaqValue === \"poor\" || iaqValue === \"unhealthy\") iaqText = \"🟠 Плохое\";\nelse if (iaqValue === \"very poor\" || iaqValue === \"bad\" || iaqValue === \"very unhealthy\") iaqText = \"🔴 Очень плохое\";\nelse iaqText = msg.payload;\nmsg.payload = { chatId: chatId, type: \"message\", content: `🏭 Качество воздуха (IAQ): ${iaqText}`, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1100,
"y": 780,
"wires": [["01881c0fe29e38ed"]]
},
{
"id": "2358f1f5037610f2",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Telemetry Menu",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nconst telemetryKeyboard = {\n reply_markup: {\n inline_keyboard: [\n [{ text: \"🌡️ Температура\", callback_data: \"sensor_temperature\" }],\n [{ text: \"💧 Влажность\", callback_data: \"sensor_humidity\" }],\n [{ text: \"📊 Давление\", callback_data: \"sensor_pressure\" }],\n [{ text: \"🏭 Качество воздуха (IAQ)\", callback_data: \"sensor_iaq\" }],\n [{ text: \"◀️ Назад к мониторингу\", callback_data: \"menu_monitoring\" }]\n ]\n }\n};\nmsg.payload = { chatId: chatId, type: \"message\", content: \"🌡️ *Телеметрия*\\nВыберите показатель:\", options: { parse_mode: \"Markdown\", reply_markup: telemetryKeyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 800,
"y": 620,
"wires": [["01881c0fe29e38ed"]]
},
{
"id": "14283ab4cc1ed28b",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Init",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"telemetry_chat_id\", chatId);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 770,
"y": 660,
"wires": [["c02b1bb4ad8d2e09"]]
},
{
"id": "c7a764c75a822b04",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Init",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"telemetry_chat_id\", chatId);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 770,
"y": 700,
"wires": [["4e9afbc910a02ab8"]]
},
{
"id": "318a4dee4563cb3c",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Init",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"telemetry_chat_id\", chatId);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 770,
"y": 740,
"wires": [["ab02b6f1fea816e8"]]
},
{
"id": "0c9e1265af100a13",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Init",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nglobal.set(\"telemetry_chat_id\", chatId);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 770,
"y": 780,
"wires": [["db3db795046abbc9"]]
},
{
"id": "f1827f9626cf94c1",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "40709ee4bf606d70",
"name": "Monitoring Menu",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nconst monitoringKeyboard = {\n reply_markup: {\n inline_keyboard: [\n [{ text: \"🌡️ Телеметрия\", callback_data: \"monitor_telemetry\" }],\n [{ text: \"🚶 PIR (Датчик движения)\", callback_data: \"monitor_pir\" }],\n [{ text: \"🪟 Окно\", callback_data: \"monitor_window\" }],\n [{ text: \"◀️ Назад в меню\", callback_data: \"back_main\" }]\n ]\n }\n};\nmsg.payload = { chatId: chatId, type: \"message\", content: \"📊 *Мониторинг*\\nВыберите категорию:\", options: { parse_mode: \"Markdown\", reply_markup: monitoringKeyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 570,
"y": 840,
"wires": [["01881c0fe29e38ed"]]
}
]
10. Управление роботом-пылесосом
Ветка для отслеживания состояния робота-пылесоса, а также его управления (запуск/отправка на базу). Помимо этого, сохраняются все запуски за день и их время работы.
Важно: Первоначально робот был подключен через Yandex Station, но он некорректно передаёт статус уборки (только «уборка» и «ожидает», и то «уборка» у него, когда он стоит на базе). Поэтому была подключена интеграция Tuya, так как робот от них. Теперь все данные верны, а также можно уровень заряда и т.д. вытащить в статистику.

10.1 Пошаговая настройка
Часть 1. Вход в меню пылесоса
1.1. Перехват callback (telegram event)
- Тип узла:
telegram event - Событие:
callback_query - Привязан к боту (
[ID_BOT_CONFIG])
1.2. Маршрутизатор кракен (switch)
- Тип узла:
switch - Свойство:
msg.payload.content - Правила:
vacuum_start→ запустить уборкуvacuum_return→ отправить на базуmenu_vacuum→ показать статус и меню
Часть 2. Получение статуса и меню
Узел «Init Vacuum» (function) — извлекает chatId из разных вариантов входного сообщения и сохраняет в глобальную переменную.
let chatId = msg.payload.chatId || msg.payload.message?.chat?.id || msg.originalMessage?.chat?.id || msg.chatId;
if (!chatId) return null;
global.set("vacuum_chat_id", chatId);
return msg;
Узел «Get Vacuum State» (api-current-state) — entity_id: select.kraken_mode (ваш пылесос).
Узел «Vacuum Menu» (function) — расшифровывает статусы, показывает уровень заряда батареи, историю уборок за сегодня, формирует меню управления.
// Расшифровка статусов
if (vacuumValue === "smart") stateText = "🧠 Интеллектуальная уборка";
else if (vacuumValue === "standby") stateText = "⚫ На базе (ожидание)";
else if (vacuumValue === "chargego") stateText = "🏠 Возвращается на базу";
else if (vacuumValue === "Интеллектуальный") { modeText = "🎮 Режим: 🧠 Интеллектуальный"; stateText = "🧹 Уборка"; }
// ... другие режимы
// Уровень заряда батареи
let batteryIcon = batteryLevel > 50 ? "🔋" : (batteryLevel > 20 ? "🪫" : "⚠️");
Часть 3. Запуск уборки
Узел «Handle Vacuum Start» (function) — сохраняет время начала уборки в историю (с учётом МСК).
const nowMSK = new Date(new Date().getTime() + 3 * 60 * 60 * 1000);
const today = nowMSK.toISOString().split('T')[0];
const currentTime = nowMSK.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
history[today].push({ startTime: currentTime, startTimestamp: nowMSK.getTime() });
Узел «Запуск кракена» (api-call-service) — domain: vacuum, service: start, entityId: vacuum.kraken.
Часть 4. Возврат на базу
Узел «Кракен домой» (api-call-service) — service: return_to_base.
Часть 5. Подтверждение команд
Узел «Vacuum Command Response» (function) — отправляет пользователю подтверждение выполнения команды.
Часть 6. Автоматическое обновление истории
Узел poll-state — каждые 60 секунд проверяет состояние пылесоса (entityId: select.kraken_mode, updateInterval: 60 секунд, outputOnChanged: true).
Узел «Vacuum History Updater» (function) — отслеживает переход из режима уборки в режим ожидания, вычисляет длительность уборки в минутах, обновляет запись в истории.
const wasCleaning = previousState === "smart" || previousState === "Интеллектуальный" || ...;
const isNowStandby = (vacuumValue === "standby" || vacuumValue === "docked" || vacuumValue === "chargego");
if (wasCleaning && isNowStandby) {
// Вычисляем длительность уборки в минутах
const durationMinutes = Math.round((nowMSK.getTime() - lastRun.startTimestamp) / 60000);
lastRun.duration = durationMinutes;
}
Часть 7. Голосовое управление через Алису
Узел «Слушаем Алису» (server-events) — eventType: yandex_intent.
Узел «Проверка команды» (switch) — правила: "кракен" → запуск, "кракен домой" → возврат на базу.
10.2 Полный код
JSON для импорта в Node-RED (Меню → Import). Замените [ID_BOT_CONFIG], [ID_HA_SERVER], [ВАШ_БОТ], [ВАШ_USERNAME], [IP_СЕРВЕРА] на свои значения.
[
{
"id": "ae80c63cc7a187a8",
"type": "group",
"z": "d91b643aa7cd4cef",
"name": "Кракен",
"style": {
"stroke": "#92d04f",
"fill": "#92d04f",
"label": true,
"label-position": "sw",
"color": "#000000"
},
"nodes": [
"a871849add4eba6b",
"3c226d0e2c1d350a",
"710c480369b6d296",
"cf3ef468bea33966",
"94e2d8668499aa57",
"7957df743ee8ed55",
"0f497795b39b0f72",
"d17e197ff82db659",
"b329518f06007f67",
"805e72f66ea5268e",
"f4e8904d29e5d12a",
"d9ce82172a7b3e6f",
"562c0d610a9da509",
"f59df362018dd210",
"daa0a2e1a4c1f103"
],
"x": 54,
"y": 1279,
"w": 1292,
"h": 230
},
{
"id": "a871849add4eba6b",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Init Vacuum",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id || msg.originalMessage?.chat?.id || msg.chatId;\nif (!chatId) return null;\nglobal.set(\"vacuum_chat_id\", chatId);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 270,
"y": 1420,
"wires": [["3c226d0e2c1d350a"]]
},
{
"id": "3c226d0e2c1d350a",
"type": "api-current-state",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Get Vacuum State",
"server": "[ID_HA_SERVER]",
"entity_id": "select.kraken_mode",
"outputProperties": [
{
"property": "payload",
"propertyType": "msg",
"value": "string",
"valueType": "entityState"
},
{
"property": "data",
"propertyType": "msg",
"value": "",
"valueType": "entity"
}
],
"x": 450,
"y": 1420,
"wires": [["710c480369b6d296"]]
},
{
"id": "710c480369b6d296",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Vacuum Menu",
"func": "let chatId = global.get(\"vacuum_chat_id\");\nif (!chatId) return null;\nlet vacuumValue = msg.payload;\nif (typeof vacuumValue === \"object\") vacuumValue = vacuumValue.state || vacuumValue.payload || \"unknown\";\nlet stateText = \"\", stateIcon = \"\", modeText = \"\";\nif (vacuumValue === \"smart\") { stateText = \"🧠 Интеллектуальная уборка\"; stateIcon = \"🧹\"; }\nelse if (vacuumValue === \"standby\") { stateText = \"⚫ На базе (ожидание)\"; stateIcon = \"🔋\"; }\nelse if (vacuumValue === \"chargego\") { stateText = \"🏠 Возвращается на базу\"; stateIcon = \"🏠\"; }\nelse if (vacuumValue === \"Интеллектуальный\") { modeText = \"🎮 Режим: 🧠 Интеллектуальный\"; stateText = \"🧹 Уборка\"; stateIcon = \"🧹\"; }\nelse if (vacuumValue === \"Следовать за стеной\") { modeText = \"🎮 Режим: 🧱 Следовать за стеной\"; stateText = \"🧹 Уборка\"; stateIcon = \"🧹\"; }\nelse if (vacuumValue === \"Швабра\") { modeText = \"🎮 Режим: 💧 Швабра\"; stateText = \"🧹 Уборка\"; stateIcon = \"🧹\"; }\nelse if (vacuumValue === \"Спираль\") { modeText = \"🎮 Режим: 🌀 Спираль\"; stateText = \"🧹 Уборка\"; stateIcon = \"🧹\"; }\nelse { stateText = `Статус: ${vacuumValue}`; stateIcon = \"❓\"; }\nlet batteryLevel = msg.data?.attributes?.battery_level || null;\nlet batteryText = \"\";\nif (batteryLevel !== null) {\n const batteryIcon = batteryLevel > 50 ? \"🔋\" : (batteryLevel > 20 ? \"🪫\" : \"⚠️\");\n batteryText = `\\n🔋 Батарея: ${batteryIcon} ${batteryLevel}%`;\n}\nconst nowMSK = new Date(new Date().getTime() + 3 * 60 * 60 * 1000);\nconst today = nowMSK.toISOString().split('T')[0];\nconst history = global.get(\"vacuum_history\") || {};\nconst todayRuns = history[today] || [];\nlet historyText = \"\", totalDuration = 0;\nif (todayRuns.length === 0) historyText = \"❌ Сегодня не запускался\";\nelse {\n historyText = \"📅 *Сегодняшние запуски:*\\n\";\n todayRuns.forEach((run, index) => {\n const durationText = run.duration ? `${run.duration} мин` : \"в процессе\";\n historyText += `${index + 1}. ${run.startTime} → ${durationText}\\n`;\n if (run.duration) totalDuration += run.duration;\n });\n if (totalDuration > 0) {\n const hours = Math.floor(totalDuration / 60);\n const minutes = totalDuration % 60;\n historyText += `\\n⏱️ *Общее время уборки:* ${hours > 0 ? `${hours} ч ` : \"\"}${minutes} мин`;\n }\n}\nlet message = `${stateIcon} *Кракен (робот-пылесос)*\\n\\n📊 Состояние: ${stateText}`;\nif (modeText) message += `\\n${modeText}`;\nmessage += batteryText + `\\n\\n${historyText}\\n\\nВыберите действие:`;\nconst keyboard = {\n reply_markup: {\n inline_keyboard: [\n [{ text: \"🧹 Запустить уборку\", callback_data: \"vacuum_start\" }],\n [{ text: \"🏠 Отправить на базу\", callback_data: \"vacuum_return\" }],\n [{ text: \"🔄 Обновить статус\", callback_data: \"menu_vacuum\" }],\n [{ text: \"◀️ Назад в меню\", callback_data: \"back_main\" }]\n ]\n }\n};\nmsg.payload = { chatId: chatId, type: \"message\", content: message, options: { parse_mode: \"Markdown\", reply_markup: keyboard.reply_markup } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 640,
"y": 1420,
"wires": [["d9ce82172a7b3e6f"]]
},
{
"id": "cf3ef468bea33966",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Handle Vacuum Start",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nif (!chatId) chatId = global.get(\"vacuum_chat_id\");\nif (!chatId) return null;\nglobal.set(\"vacuum_chat_id\", chatId);\nconst nowMSK = new Date(new Date().getTime() + 3 * 60 * 60 * 1000);\nconst today = nowMSK.toISOString().split('T')[0];\nconst currentTime = nowMSK.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });\nlet history = global.get(\"vacuum_history\") || {};\nif (!history[today]) history[today] = [];\nhistory[today].push({ startTime: currentTime, startTimestamp: nowMSK.getTime() });\nif (history[today].length > 10) history[today] = history[today].slice(-10);\nglobal.set(\"vacuum_history\", history);\nmsg.vacuumCommand = \"start\";\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 720,
"y": 1320,
"wires": [["94e2d8668499aa57"]]
},
{
"id": "94e2d8668499aa57",
"type": "api-call-service",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Запуск кракена",
"server": "[ID_HA_SERVER]",
"action": "vacuum.start",
"entityId": ["vacuum.kraken"],
"x": 930,
"y": 1320,
"wires": [["d17e197ff82db659"]]
},
{
"id": "7957df743ee8ed55",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Handle Vacuum Return",
"func": "let chatId = msg.payload.chatId || msg.payload.message?.chat?.id;\nif (!chatId) chatId = global.get(\"vacuum_chat_id\");\nif (!chatId) return null;\nglobal.set(\"vacuum_chat_id\", chatId);\nmsg.vacuumCommand = \"return\";\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 730,
"y": 1360,
"wires": [["0f497795b39b0f72"]]
},
{
"id": "0f497795b39b0f72",
"type": "api-call-service",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Кракен домой",
"server": "[ID_HA_SERVER]",
"action": "vacuum.return_to_base",
"entityId": ["vacuum.kraken"],
"x": 940,
"y": 1360,
"wires": [["d17e197ff82db659"]]
},
{
"id": "d17e197ff82db659",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Vacuum Command Response",
"func": "const chatId = global.get(\"vacuum_chat_id\");\nif (!chatId) return null;\nconst command = msg.vacuumCommand;\nlet resultMessage = command === \"start\" ? \"🧹 Кракен начал уборку!\" : (command === \"return\" ? \"🏠 Кракен возвращается на базу\" : \"✅ Команда выполнена\");\nmsg.payload = { chatId: chatId, type: \"message\", content: `✅ ${resultMessage}`, options: { parse_mode: \"Markdown\" } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 1190,
"y": 1340,
"wires": [[]]
},
{
"id": "b329518f06007f67",
"type": "function",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Vacuum History Updater",
"func": "let vacuumValue = msg.payload;\nif (typeof vacuumValue === \"object\") vacuumValue = vacuumValue.state || vacuumValue.payload || \"unknown\";\nlet previousState = global.get(\"vacuum_previous_state\") || \"unknown\";\nglobal.set(\"vacuum_previous_state\", vacuumValue);\nconst wasCleaning = previousState === \"smart\" || previousState === \"Интеллектуальный\" || previousState === \"Следовать за стеной\" || previousState === \"Швабра\" || previousState === \"Спираль\";\nconst isNowStandby = (vacuumValue === \"standby\" || vacuumValue === \"docked\" || vacuumValue === \"chargego\");\nif (wasCleaning && isNowStandby) {\n const nowMSK = new Date(new Date().getTime() + 3 * 60 * 60 * 1000);\n const today = nowMSK.toISOString().split('T')[0];\n let history = global.get(\"vacuum_history\") || {};\n if (history[today] && history[today].length > 0) {\n for (let i = history[today].length - 1; i >= 0; i--) {\n if (!history[today][i].endTime) {\n history[today][i].endTime = nowMSK.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });\n const durationMinutes = Math.round((nowMSK.getTime() - history[today][i].startTimestamp) / 60000);\n history[today][i].duration = durationMinutes;\n break;\n }\n }\n global.set(\"vacuum_history\", history);\n }\n}\nreturn null;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"x": 670,
"y": 1460,
"wires": [[]]
},
{
"id": "805e72f66ea5268e",
"type": "switch",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "маршрутизатор кракен",
"property": "payload.content",
"propertyType": "msg",
"rules": [
{"t": "eq", "v": "vacuum_start", "vt": "str"},
{"t": "eq", "v": "vacuum_return", "vt": "str"},
{"t": "eq", "v": "menu_vacuum", "vt": "str"}
],
"checkall": "true",
"repair": false,
"outputs": 3,
"x": 470,
"y": 1360,
"wires": [
["cf3ef468bea33966"],
["7957df743ee8ed55"],
["a871849add4eba6b"]
]
},
{
"id": "f4e8904d29e5d12a",
"type": "telegram event",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Обработчик нажатия кнопок",
"bot": "[ID_BOT_CONFIG]",
"event": "callback_query",
"autoanswer": false,
"x": 210,
"y": 1360,
"wires": [["805e72f66ea5268e"]]
},
{
"id": "d9ce82172a7b3e6f",
"type": "telegram sender",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Отправка сообщения",
"bot": "[ID_BOT_CONFIG]",
"x": 840,
"y": 1420,
"wires": [[]]
},
{
"id": "562c0d610a9da509",
"type": "poll-state",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Poll State",
"server": "[ID_HA_SERVER]",
"updateInterval": "60",
"updateIntervalUnits": "seconds",
"outputOnChanged": true,
"entityId": "select.kraken_mode",
"x": 410,
"y": 1460,
"wires": [["b329518f06007f67"]]
},
{
"id": "f59df362018dd210",
"type": "server-events",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Слушаем Алису",
"server": "[ID_HA_SERVER]",
"eventType": "yandex_intent",
"x": 240,
"y": 1320,
"wires": [["daa0a2e1a4c1f103"]]
},
{
"id": "daa0a2e1a4c1f103",
"type": "switch",
"z": "d91b643aa7cd4cef",
"g": "ae80c63cc7a187a8",
"name": "Проверка команды",
"property": "payload.event.text",
"propertyType": "msg",
"rules": [
{"t": "eq", "v": "кракен", "vt": "str"},
{"t": "eq", "v": "кракен домой", "vt": "str"}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 460,
"y": 1320,
"wires": [
["cf3ef468bea33966"],
["7957df743ee8ed55"]
]
},
{
"id": "[ID_BOT_CONFIG]",
"type": "telegram bot",
"botname": "[ВАШ_БОТ]",
"usernames": "[ВАШ_USERNAME]",
"updatemode": "polling",
"pollinterval": "300",
"localbothost": "[IP_СЕРВЕРА]",
"localbotport": 8443
},
{
"id": "[ID_HA_SERVER]",
"type": "server",
"name": "Home Assistant",
"version": 6,
"addon": false
},
{
"id": "cb623d923d2ab315",
"type": "global-config",
"env": [],
"modules": {
"node-red-contrib-home-assistant-websocket": "0.80.3",
"node-red-contrib-telegrambot": "17.4.12"
}
}
]