Речь пойдет о сборке, настройке и запуске охранной системы с датчиками, камерой, хранением видеозаписей на удаленном домашнем NAS, встроенным UPS, web-интерфейсом и оповещением через Telegram.
Чтобы быть предельно кратким, оформил статью в стилистике hackster.io. Информация рассчитана на читателей с опытом работы с одноплатным компьютером Raspberry и пониманием принципов работы VPN.
Требования к системе
После приобретения дачного участка с небольшим деревянным домиком, встал вопрос, а что происходит в мое отсутствие на участке и все ли в порядке в самом доме. Решением было создать видеонаблюдение и охранную систему с сигнализацией и оповещением.
Основными требованиями были:
наличие охранных функций: использовать датчики в качестве пожарной сигнализации и камеру как детектор движения;
возможность хранить видео сроком семь дней и более;
высоковольтное оборудование должно уместиться в существующем электрощитке небольшого размера и не выходить за его пределы;
сигнализация должна быть автономной, работать при отключении электричества;
минимум проводов;
в дальнейшем расширять функциональные возможности;
удаленный доступ;
доступность как по деньгам, так и по оборудованию.
За основу решил взять миникомпьютер Raspberry Pi Zero W. Его легко разместить в небольшом электрощитке. Рабочий вариант выглядит так:
В итоге получилась система, которая умеет постоянно записывать видео с IP-камеры на домашний NAS через VPN, при постановке на охрану оповещать в Telegram о движении на участке и повышении температуры в доме и работать без электричества чуть более часа. Все это умещается в распределительный щит. Также можно следить за разрядом батареи, подключаться удаленно из любой точки мира не только к камере, но и зайти на страницу управления и просмотреть состояние системы, графики изменения температуры и загрузки ЦПУ.
Аппаратная часть
Набросал схему. Система получилась модульная, некоторые компоненты можно исключить.
Нам потребуется:
Raspberry Pi Zero W (wi-fi идет как точка доступа для раздачи интернета на даче);
Raspberry Pi Camera Board (используется в качестве камеры в доме);
Raspberry Pi Ethernet/USB HUB HAT (для подключения 4G-модема и IP-камеры);
UPS Lite v1.2 Power HAT (в качестве источника бесперебойного питания);
антивандальная кнопка с подсветкой и фиксацией (в качестве кнопки постановки на охрану);
модуль датчика температуры DS18B20 (плата с датчиком температуры и встроенным резистором);
IP-камера с POE и дискретными входами/выходами (камера с возможностью подачи дискретного сигнала);
POE-адаптер 220v (для подключения IP-камеры к Raspberry Pi);
4G-модем (для удаленного доступа и отправки уведомлений в интернет);
роутер с возможностью создания VPN-сервера (для удаленного доступа);
NAS с приложением для видеонаблюдения (для записи и хранения видео);
Ethernet-кабель (для подключения камеры к Raspberry);
резистор (для подключения I/O-камеры к Raspberry GPIO).
Некоторые компоненты не являются обязательными и могут быть исключены. Например, можно исключить UPS Lite, если нет проблем с электричеством. Или не использовать роутер и NAS, если не требуется хранить записи удаленно.
Программная часть
Raspberry Pi OS 32-bit;
WireGuard (VPN-сервер на роутере и VPN-клиент на raspberry);
Telegram;
Node-RED (инструмент потокового программирования):
node-red-contrib-boolean-logic (пакет для подключения логических функций);
node-red-contrib-camerapi (пакет для работы с Pi Camera);
node-red-contrib-cpu (пакет для получения нагрузки ЦПУ и температуры процессора);
node-red-contrib-easybotics-ina219-sensor (пакет для получения данных с UPS Lite);
node-red-contrib-edge-trigger (пакет с функциями триггеров);
node-red-contrib-sensor-ds18b20 (пакет для получения данных с датчика температуры);
node-red-contrib-telegrambot (пакет для отправки сообщений в telegram);
node-red-node-pi-gpio (пакет чтения/записи данных GPIO).
Сборка
Собираем «бутерброд». Подключаем Raspberry Pi Cam к Raspberry Pi Zero W. Снизу подсоединяем UPS Lite, сверху - Raspberry Pi Ethernet/USB HUB HAT. Добавляем датчик температуры DS18B20. Я использовал модуль из набора для Arduino, в нем кроме датчика температуры уже добавлен резистор и выведены контакты для быстрого подключения. Помним, что этот датчик использует 1-Wire протокол, на контроллере это пин GPIO4.
Подключаем согласно архитектурной схеме представленной выше:
минус заводим на пин GND;
плюс заводим на пин 3.3v;
сигнальный вывод на пин GPIO4.
Подключаем ввод-вывод IP-камеры к Raspberry. Я использовал камеру с наличием ввода-вывода, так как считаю этот способ более надежным и гибким в конфигурировании. Принцип следующий: как только камера обнаруживает движение или попытки взлома, формирует на выходе сигнал. В зависимости от настроек он может быть как постоянным, так и импульсным, а контакт - нормально открытым или нормально замкнутым. В моем случае у камеры на выходе будет постоянное напряжение при нормально замкнутом контакте. У Raspberry Pi Zero напряжение на пинах ввода-вывода 3.3v для логической единицы и 0v для нуля. У меня уже была камера Geovision, в описании которой сказано, что максимальный ток 30mA и на дискретном выходе 5v, следовательно мне подойдет резистор на 100 Ом. Возвращаемся к схеме и подключаем:
плюс с камеры на резистор, с резистора на пин GPIO27;
минус с камеры заводим на пин GND.
Теперь кнопка постановки на охрану. Я выбирал кнопку с LED-индикацией. Это позволяет визуально оценить, включена охрана или нет. Также индикация меняется при обнаружении движения. Удобно то, что кнопка рассчитана на питание 3-6V, что существенно облегчает монтаж к Raspberry Pi. Кнопка имеет пять контактов и позволяет подсоединить ее разными способами. Я выбрал тот, при котором подсветка горит при нажатой кнопке. Подключение сделал следующим образом:
GPIO17 подключил к кнопке к контакту С;
минус кнопки заводим на пин GND;
контакт кнопки NO подключил к GPIO22;
минус кнопки заводим на пин GND.
Дело за малым - подключить к USB-порту LTE-модем, Ethernet-кабель от Raspberry Pi Ethernet/USB HUB HAT к POE-адаптеру, а от адаптера к IP-камере. Запитать UPS Lite от стандартного блока питания для Raspberry и включить POE-адаптер в розетку. Как все это выглядит живьем, можно посмотреть на самом первом фото.
Настройка
Установка и настройка Raspberry Pi
Подготавливаем Raspberry Pi к работе. Первым делом установим Raspberry Pi OS 32-bit. Установка подробно описана на официальном сайте. После успешной установки и загрузки OS включаем поддержку протокола 1-Wire и активируем Raspberry Pi Camera Board. Делаем это через утилиту raspi-config запуском в терминале команды:
sudo raspi-config
В открывшемся окне настроек переходим в раздел Interfacing options.
Активируем P1 Camera и 1-Wire.
После сохранения настроек, скорее всего, понадобится перезагрузить Raspberry Pi.
Установка Node-RED
Далее устанавливаем инструмент потокового программирования Node-RED. На Хабре много статей о самом продукте и его установке. В моем случае никаких особенностей нет, выполняем команду:
$ bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)
После успешного выполнения скрипта, устанавливаем модули:
node-red-contrib-boolean-logic
$ npm install node-red-contrib-boolean-logic
node-red-contrib-camerapi
$ npm install node-red-contrib-camerapi
node-red-contrib-cpu
$ npm install node-red-contrib-cpu
node-red-contrib-easybotics-ina219-sensor
$ npm install node-red-contrib-easybotics-ina219-sensor
node-red-contrib-edge-trigger
$ npm install node-red-contrib-edge-trigger
node-red-contrib-sensor-ds18b20
$ npm install node-red-contrib-sensor-ds18b20
node-red-contrib-telegrambot
$ npm install node-red-contrib-telegrambot
node-red-node-pi-gpio
$ npm install node-red-node-pi-gpio
Включаем автозапуск и запускаем Node-RED.
$ sudo systemctl enable nodered.service
$ sudo systemctl start nodered.service
Открываем редактор
Как только Node-RED запустится, подключаемся через браузер по адресу: http://<ip raspberry>:1880.
Подготавливаем проект под себя
Проект представляет собой json-файл. Так как для отправки уведомлений используется модуль Telegram, в настройках которого указывается id Telegram-канала, в который отправляются оповещения, то понадобиться зарегистрировать Telegram-бота и создать чат. Для регистрации бота воспользуйтесь служебным ботом @BotFather. Узнать id-номер чата поможет бот @getmyid_bot, добавленный в созданный Telegram-чат. Чат позволит добавить пользователей, которые будут получать сообщения от системы. Номер созданного чата необходимо подставить в json-файле вместо текста CHANGE_CHAT_ID.
Содержимое json-файл проекта
[
{
"id": "35700ef4.b4cc62",
"type": "tab",
"label": "Dacha",
"disabled": false,
"info": ""
},
{
"id": "21ee0549ab80c1b3",
"type": "group",
"z": "35700ef4.b4cc62",
"name": "Temperature external",
"style": {
"fill": "#c8e7a7",
"label": true,
"stroke": "#000000",
"color": "#000000"
},
"nodes": [
"1b33f0bb.ddc0af",
"8cb14a63.63e588",
"bf42d115.5f8dc",
"f826e2b9.18e4c",
"37299a0b.442dc6",
"589f8077.70b05",
"7744d782.93a8f8",
"cd44f017.139e6",
"d9e7f0b2.9c11a"
],
"x": 34,
"y": 479,
"w": 1172,
"h": 302
},
{
"id": "3a19bea8.e27032",
"type": "group",
"z": "35700ef4.b4cc62",
"name": "Camera external motion alarm",
"style": {
"stroke": "#000000",
"fill": "#c8e7a7",
"label": true,
"color": "#000000"
},
"nodes": [
"5d05a379.500dcc",
"636c36b0.9db488",
"391ad295.c56a6e",
"14425883.8a8027",
"c14e914c.160e1",
"57894684.68d3c8",
"f7c59ddc.2f24c",
"c411c632.3dd2f8",
"48255a26.189904",
"78cfa48.1eae25c",
"ce092f2e.fb6ff",
"3b1e904c.649e9",
"48d31990.641c08",
"1d8f6830.1c7338",
"7f12a359.8f4dfc",
"6299cce8.7b20a4",
"7f956fcc.36206",
"e9b9f2a1a8421187",
"548c306e6e2c111f"
],
"x": 34,
"y": 19,
"w": 1192,
"h": 422
},
{
"id": "6e2ba88d.60c498",
"type": "group",
"z": "35700ef4.b4cc62",
"name": "CPU Load and temperature",
"style": {
"stroke": "#000000",
"fill": "#c8e7a7",
"label": true,
"color": "#000000"
},
"nodes": [
"b8c052bb.44535",
"d6fd679.6caa398",
"dab61cbf.cfa87",
"fdb3a1b6.cfb71",
"a0e62b1a.757c48"
],
"x": 34,
"y": 819,
"w": 452,
"h": 282
},
{
"id": "72f5dbe4bc2de2b5",
"type": "group",
"z": "35700ef4.b4cc62",
"name": "Capture photo",
"style": {
"fill": "#c8e7a7",
"label": true,
"stroke": "#000000",
"color": "#000000"
},
"nodes": [
"9aca7759.d95a48",
"9ca8d5ff.3857d8",
"24ee77ab.2e8738",
"ea0b75b0.94f058",
"c452f057.ca63a"
],
"x": 44,
"y": 1159,
"w": 1182,
"h": 142
},
{
"id": "8c22d8c9580d2858",
"type": "group",
"z": "35700ef4.b4cc62",
"name": "",
"style": {
"label": true,
"fill": "#c8e7a7"
},
"nodes": [
"cc5bc2c3a2c509fa",
"e291c889c1d4e186",
"0d8935ca87af5a1b",
"a067feaf7d06fabf",
"2686f0fe374cc52d",
"d6992fd1eb26d528",
"589c720b5a2be2f2",
"4ecdc496c0ecd286",
"b9d28942538208e4",
"bd842c6db6853e79",
"26d9630fb660bc9d",
"0d4f374253a9f8d4",
"65fe44dc0bb4ca1d"
],
"x": 514,
"y": 819,
"w": 692,
"h": 282
},
{
"id": "9aca7759.d95a48",
"type": "camerapi-takephoto",
"z": "35700ef4.b4cc62",
"g": "72f5dbe4bc2de2b5",
"filemode": "1",
"filename": "photo1.JPEG",
"filedefpath": "0",
"filepath": "/home/pi/Pictures/",
"fileformat": "jpeg",
"resolution": "3",
"rotation": "0",
"fliph": "0",
"flipv": "0",
"brightness": "50",
"contrast": "0",
"sharpness": "0",
"quality": "80",
"imageeffect": "none",
"exposuremode": "auto",
"iso": "0",
"agcwait": "1.0",
"led": "0",
"awb": "auto",
"name": "",
"x": 860,
"y": 1260,
"wires": [
[
"24ee77ab.2e8738",
"ea0b75b0.94f058"
]
]
},
{
"id": "9ca8d5ff.3857d8",
"type": "ui_template",
"z": "35700ef4.b4cc62",
"g": "72f5dbe4bc2de2b5",
"group": "fddd6117.b012f",
"name": "",
"order": 1,
"width": "10",
"height": "13",
"format": "<script>\nvar value = \"1\";\n// or overwrite value in your callback function ...\nthis.scope.action = function() { return value; }\n\nfunction updateF() {\n var source = '/photo1.JPEG',\n timestamp = (new Date()).getTime(),\n newUrl = source + '?_=' + timestamp;\n document.getElementById(\"photo\").src = newUrl;\n}\n</script>\n\n<md-button ng-click=\"send({payload:action()})\" onclick=\"setTimeout(updateF, 2500);\" style=\"padding:40px; margin-bottom: 40px; color: #000000; background-color: #888981;\" >\n <ui-icon icon=\"camera\"></ui-icon>\n Take a photo<br>\n</md-button>\n\n<div style=\"margin-bottom:40px;\">\n <img src=\"/photo1.JPEG\" id=\"photo\" width=\"100%\" height=\"100%\">\n</div>",
"storeOutMessages": true,
"fwdInMessages": true,
"resendOnRefresh": true,
"templateScope": "local",
"x": 130,
"y": 1260,
"wires": [
[
"9aca7759.d95a48"
]
]
},
{
"id": "24ee77ab.2e8738",
"type": "debug",
"z": "35700ef4.b4cc62",
"g": "72f5dbe4bc2de2b5",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 1110,
"y": 1260,
"wires": []
},
{
"id": "5d05a379.500dcc",
"type": "inject",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "Test sms",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 760,
"y": 340,
"wires": [
[
"e9b9f2a1a8421187"
]
]
},
{
"id": "636c36b0.9db488",
"type": "function",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "",
"func": "msg.payload = {}\nmsg.payload.chatId = CHANGE_CHAT_ID\nmsg.payload.type = \"message\"\nmsg.payload.content = \"Motion detected! [myDacha]\"\nreturn msg;\n",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 980,
"y": 320,
"wires": [
[
"f5b3043a.2a3ff8"
]
]
},
{
"id": "391ad295.c56a6e",
"type": "rpi-gpio in",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "camera grpio 7",
"pin": "13",
"intype": "down",
"debounce": "25",
"read": true,
"x": 140,
"y": 220,
"wires": [
[
"f7c59ddc.2f24c",
"78cfa48.1eae25c",
"48d31990.641c08",
"548c306e6e2c111f"
]
]
},
{
"id": "14425883.8a8027",
"type": "rising-edge",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "",
"threshold": "0",
"x": 770,
"y": 280,
"wires": [
[
"636c36b0.9db488"
]
]
},
{
"id": "c14e914c.160e1",
"type": "ui_button",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "",
"group": "371d08d2.682078",
"order": 3,
"width": "6",
"height": "1",
"passthru": false,
"label": "Test msg",
"tooltip": "",
"color": "#000000",
"bgcolor": "#888981",
"icon": "",
"payload": "1",
"payloadType": "str",
"topic": "topic",
"topicType": "msg",
"x": 760,
"y": 400,
"wires": [
[
"e9b9f2a1a8421187"
]
]
},
{
"id": "57894684.68d3c8",
"type": "ui_switch",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "",
"label": "Лампочка",
"tooltip": "",
"group": "371d08d2.682078",
"order": 1,
"width": "6",
"height": "1",
"passthru": true,
"decouple": "false",
"topic": "topic",
"topicType": "msg",
"style": "",
"onvalue": "true",
"onvalueType": "bool",
"onicon": "",
"oncolor": "",
"offvalue": "false",
"offvalueType": "bool",
"officon": "",
"offcolor": "",
"animate": false,
"x": 1100,
"y": 60,
"wires": [
[
"316ad35.6698d2c"
]
]
},
{
"id": "f7c59ddc.2f24c",
"type": "ui_text",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"group": "189e2e48.d109d2",
"order": 1,
"width": 0,
"height": 0,
"name": "",
"label": "Движение",
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 310,
"y": 300,
"wires": []
},
{
"id": "c411c632.3dd2f8",
"type": "ui_text",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"group": "189e2e48.d109d2",
"order": 3,
"width": 0,
"height": 0,
"name": "",
"label": "Охрана",
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 300,
"y": 60,
"wires": []
},
{
"id": "48255a26.189904",
"type": "rpi-gpio in",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "Security Button",
"pin": "15",
"intype": "up",
"debounce": "250",
"read": true,
"x": 140,
"y": 120,
"wires": [
[
"c411c632.3dd2f8",
"78cfa48.1eae25c",
"3b1e904c.649e9"
]
]
},
{
"id": "78cfa48.1eae25c",
"type": "BooleanLogic",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "",
"operation": "AND",
"inputCount": "2",
"topic": "result",
"x": 500,
"y": 220,
"wires": [
[
"14425883.8a8027",
"6299cce8.7b20a4"
]
]
},
{
"id": "ce092f2e.fb6ff",
"type": "inject",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "Blink 1",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "0.5",
"crontab": "",
"once": true,
"onceDelay": "0.5",
"topic": "",
"payload": "true",
"payloadType": "bool",
"x": 480,
"y": 60,
"wires": [
[
"3b1e904c.649e9"
]
]
},
{
"id": "3b1e904c.649e9",
"type": "BooleanLogic",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "",
"operation": "AND",
"inputCount": "3",
"topic": "result",
"x": 720,
"y": 120,
"wires": [
[
"7f12a359.8f4dfc"
]
]
},
{
"id": "1b33f0bb.ddc0af",
"type": "inject",
"z": "35700ef4.b4cc62",
"g": "21ee0549ab80c1b3",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "1",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 170,
"y": 640,
"wires": [
[
"8cb14a63.63e588"
]
]
},
{
"id": "8cb14a63.63e588",
"type": "sensor-ds18b20",
"z": "35700ef4.b4cc62",
"g": "21ee0549ab80c1b3",
"name": "",
"topic": "",
"sensorid": "28-011632edb7ee",
"timer": "1",
"repeat": false,
"x": 390,
"y": 640,
"wires": [
[
"bf42d115.5f8dc",
"cd44f017.139e6",
"d9e7f0b2.9c11a"
]
]
},
{
"id": "bf42d115.5f8dc",
"type": "switch",
"z": "35700ef4.b4cc62",
"g": "21ee0549ab80c1b3",
"name": "Above 40",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "gte",
"v": "40",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 600,
"y": 520,
"wires": [
[
"37299a0b.442dc6"
]
]
},
{
"id": "f826e2b9.18e4c",
"type": "function",
"z": "35700ef4.b4cc62",
"g": "21ee0549ab80c1b3",
"name": "",
"func": "msg.payload = {}\nmsg.payload.chatId = CHANGE_CHAT_ID\nmsg.payload.type = \"message\"\nmsg.payload.content = \"Temperature HI! [myDacha]\"\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1120,
"y": 520,
"wires": [
[
"f5b3043a.2a3ff8"
]
]
},
{
"id": "37299a0b.442dc6",
"type": "trigger",
"z": "35700ef4.b4cc62",
"g": "21ee0549ab80c1b3",
"name": "",
"op1": "1",
"op2": "0",
"op1type": "str",
"op2type": "str",
"duration": "5",
"extend": true,
"overrideDelay": false,
"units": "s",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 770,
"y": 520,
"wires": [
[
"7744d782.93a8f8"
]
]
},
{
"id": "589f8077.70b05",
"type": "debug",
"z": "35700ef4.b4cc62",
"g": "21ee0549ab80c1b3",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 670,
"y": 600,
"wires": []
},
{
"id": "7744d782.93a8f8",
"type": "switch",
"z": "35700ef4.b4cc62",
"g": "21ee0549ab80c1b3",
"name": "",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "1",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 950,
"y": 520,
"wires": [
[
"f826e2b9.18e4c"
]
]
},
{
"id": "cd44f017.139e6",
"type": "ui_gauge",
"z": "35700ef4.b4cc62",
"g": "21ee0549ab80c1b3",
"name": "",
"group": "189e2e48.d109d2",
"order": 5,
"width": "6",
"height": "6",
"gtype": "gage",
"title": "Temperature ext",
"label": "units",
"format": "{{value}}",
"min": "-40",
"max": "60",
"colors": [
"#00b500",
"#e6e600",
"#ca3838"
],
"seg1": "",
"seg2": "",
"x": 860,
"y": 680,
"wires": []
},
{
"id": "d9e7f0b2.9c11a",
"type": "ui_chart",
"z": "35700ef4.b4cc62",
"g": "21ee0549ab80c1b3",
"name": "",
"group": "172f5cc2.238383",
"order": 1,
"width": "18",
"height": "4",
"label": "Temperature ext",
"chartType": "line",
"legend": "false",
"xformat": "HH:mm:ss",
"interpolate": "linear",
"nodata": "",
"dot": false,
"ymin": "-40",
"ymax": "60",
"removeOlder": "1",
"removeOlderPoints": "",
"removeOlderUnit": "3600",
"cutout": 0,
"useOneColor": false,
"useUTC": false,
"colors": [
"#3f16d4",
"#aec7e8",
"#ff7f0e",
"#2ca02c",
"#98df8a",
"#d62728",
"#ff9896",
"#9467bd",
"#c5b0d5"
],
"outputs": 1,
"useDifferentColor": false,
"x": 860,
"y": 740,
"wires": [
[]
]
},
{
"id": "ea0b75b0.94f058",
"type": "function",
"z": "35700ef4.b4cc62",
"g": "72f5dbe4bc2de2b5",
"name": "",
"func": "var payload = { \nchatId: CHANGE_CHAT_ID,\n type: \"photo\",\n content: \"/home/pi/Pictures/photo1.JPEG\" };\nreturn {payload};",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1100,
"y": 1200,
"wires": [
[
"f5b3043a.2a3ff8"
]
]
},
{
"id": "c452f057.ca63a",
"type": "inject",
"z": "35700ef4.b4cc62",
"g": "72f5dbe4bc2de2b5",
"name": "Test web cam img",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "1",
"payloadType": "num",
"x": 850,
"y": 1200,
"wires": [
[
"ea0b75b0.94f058"
]
]
},
{
"id": "b8c052bb.44535",
"type": "cpu",
"z": "35700ef4.b4cc62",
"g": "6e2ba88d.60c498",
"name": "",
"msgCore": true,
"msgOverall": false,
"msgArray": false,
"msgTemp": false,
"x": 170,
"y": 860,
"wires": [
[
"d6fd679.6caa398"
]
]
},
{
"id": "d6fd679.6caa398",
"type": "ui_chart",
"z": "35700ef4.b4cc62",
"g": "6e2ba88d.60c498",
"name": "",
"group": "172f5cc2.238383",
"order": 3,
"width": "18",
"height": "4",
"label": "CPU usage",
"chartType": "line",
"legend": "true",
"xformat": "HH:mm:ss",
"interpolate": "linear",
"nodata": "",
"dot": false,
"ymin": "0",
"ymax": "100",
"removeOlder": "1",
"removeOlderPoints": "",
"removeOlderUnit": "3600",
"cutout": 0,
"useOneColor": false,
"useUTC": false,
"colors": [
"#b33c1e",
"#aec7e8",
"#ff7f0e",
"#2ca02c",
"#98df8a",
"#d62728",
"#ff9896",
"#9467bd",
"#c5b0d5"
],
"outputs": 1,
"useDifferentColor": false,
"x": 390,
"y": 880,
"wires": [
[]
]
},
{
"id": "dab61cbf.cfa87",
"type": "inject",
"z": "35700ef4.b4cc62",
"g": "6e2ba88d.60c498",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "1",
"crontab": "",
"once": true,
"onceDelay": "1",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 170,
"y": 960,
"wires": [
[
"b8c052bb.44535",
"fdb3a1b6.cfb71"
]
]
},
{
"id": "fdb3a1b6.cfb71",
"type": "cpu",
"z": "35700ef4.b4cc62",
"g": "6e2ba88d.60c498",
"name": "CPU Temp",
"msgCore": false,
"msgOverall": false,
"msgArray": false,
"msgTemp": true,
"x": 170,
"y": 1060,
"wires": [
[
"a0e62b1a.757c48"
]
]
},
{
"id": "a0e62b1a.757c48",
"type": "ui_chart",
"z": "35700ef4.b4cc62",
"g": "6e2ba88d.60c498",
"name": "",
"group": "172f5cc2.238383",
"order": 5,
"width": "18",
"height": "4",
"label": "CPU Temp",
"chartType": "line",
"legend": "false",
"xformat": "HH:mm:ss",
"interpolate": "linear",
"nodata": "",
"dot": false,
"ymin": "",
"ymax": "",
"removeOlder": "1",
"removeOlderPoints": "",
"removeOlderUnit": "3600",
"cutout": 0,
"useOneColor": false,
"useUTC": false,
"colors": [
"#30b31e",
"#aec7e8",
"#ff7f0e",
"#2ca02c",
"#98df8a",
"#d62728",
"#ff9896",
"#9467bd",
"#c5b0d5"
],
"outputs": 1,
"useDifferentColor": false,
"x": 370,
"y": 1020,
"wires": [
[]
]
},
{
"id": "f5b3043a.2a3ff8",
"type": "telegram sender",
"z": "35700ef4.b4cc62",
"name": "",
"bot": "d9de0dcc.3d2a6",
"haserroroutput": false,
"outputs": 1,
"x": 1430,
"y": 320,
"wires": [
[]
]
},
{
"id": "316ad35.6698d2c",
"type": "rpi-gpio out",
"z": "35700ef4.b4cc62",
"name": "LED",
"pin": "11",
"set": true,
"level": "0",
"freq": "",
"out": "out",
"x": 1330,
"y": 140,
"wires": []
},
{
"id": "48d31990.641c08",
"type": "Invert",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "Invert",
"x": 490,
"y": 160,
"wires": [
[
"3b1e904c.649e9"
]
]
},
{
"id": "1d8f6830.1c7338",
"type": "trigger",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "",
"op1": "",
"op2": "false",
"op1type": "pay",
"op2type": "str",
"duration": "250",
"extend": false,
"overrideDelay": false,
"units": "ms",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 1120,
"y": 120,
"wires": [
[
"316ad35.6698d2c"
]
]
},
{
"id": "7f12a359.8f4dfc",
"type": "switch",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "1",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 890,
"y": 120,
"wires": [
[
"1d8f6830.1c7338"
]
]
},
{
"id": "6299cce8.7b20a4",
"type": "switch",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "1",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 770,
"y": 200,
"wires": [
[
"7f956fcc.36206"
]
]
},
{
"id": "7f956fcc.36206",
"type": "trigger",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "",
"op1": "",
"op2": "true",
"op1type": "pay",
"op2type": "str",
"duration": "100",
"extend": false,
"overrideDelay": false,
"units": "ms",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 1020,
"y": 200,
"wires": [
[
"316ad35.6698d2c"
]
]
},
{
"id": "cc5bc2c3a2c509fa",
"type": "ina-sensor",
"z": "35700ef4.b4cc62",
"g": "8c22d8c9580d2858",
"name": "",
"handle": "93c1182fe25c4f4a",
"x": 670,
"y": 940,
"wires": [
[
"e291c889c1d4e186",
"0d4f374253a9f8d4"
],
[
"0d8935ca87af5a1b",
"26d9630fb660bc9d"
]
]
},
{
"id": "65fe44dc0bb4ca1d",
"type": "inject",
"z": "35700ef4.b4cc62",
"g": "8c22d8c9580d2858",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "1",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payloadType": "date",
"x": 650,
"y": 880,
"wires": [
[
"cc5bc2c3a2c509fa"
]
]
},
{
"id": "e291c889c1d4e186",
"type": "switch",
"z": "35700ef4.b4cc62",
"g": "8c22d8c9580d2858",
"name": "Less 3.5v",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "lt",
"v": "3.5",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 900,
"y": 900,
"wires": [
[
"d6992fd1eb26d528"
]
]
},
{
"id": "0d8935ca87af5a1b",
"type": "switch",
"z": "35700ef4.b4cc62",
"g": "8c22d8c9580d2858",
"name": "Less 30%",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "lt",
"v": "30",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 900,
"y": 1060,
"wires": [
[
"589c720b5a2be2f2"
]
]
},
{
"id": "a067feaf7d06fabf",
"type": "function",
"z": "35700ef4.b4cc62",
"g": "8c22d8c9580d2858",
"name": "",
"func": "msg.payload = {}\nmsg.payload.chatId = CHANGE_CHAT_ID\nmsg.payload.type = \"message\"\nmsg.payload.content = \"Voltage low 3.5v! [myDacha]\"\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1120,
"y": 920,
"wires": [
[
"f5b3043a.2a3ff8"
]
]
},
{
"id": "2686f0fe374cc52d",
"type": "function",
"z": "35700ef4.b4cc62",
"g": "8c22d8c9580d2858",
"name": "",
"func": "msg.payload = {}\nmsg.payload.chatId = CHANGE_CHAT_ID\nmsg.payload.type = \"message\"\nmsg.payload.content = \"Battery low 30%! [myDacha]\"\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1120,
"y": 1000,
"wires": [
[
"f5b3043a.2a3ff8"
]
]
},
{
"id": "d6992fd1eb26d528",
"type": "trigger",
"z": "35700ef4.b4cc62",
"g": "8c22d8c9580d2858",
"name": "",
"op1": "1",
"op2": "0",
"op1type": "str",
"op2type": "str",
"duration": "5",
"extend": true,
"overrideDelay": false,
"units": "s",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 910,
"y": 960,
"wires": [
[
"4ecdc496c0ecd286"
]
]
},
{
"id": "589c720b5a2be2f2",
"type": "trigger",
"z": "35700ef4.b4cc62",
"g": "8c22d8c9580d2858",
"name": "",
"op1": "1",
"op2": "0",
"op1type": "str",
"op2type": "str",
"duration": "5",
"extend": true,
"overrideDelay": false,
"units": "s",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 910,
"y": 1000,
"wires": [
[
"b9d28942538208e4"
]
]
},
{
"id": "4ecdc496c0ecd286",
"type": "switch",
"z": "35700ef4.b4cc62",
"g": "8c22d8c9580d2858",
"name": "",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "1",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 1090,
"y": 860,
"wires": [
[
"a067feaf7d06fabf"
]
]
},
{
"id": "b9d28942538208e4",
"type": "switch",
"z": "35700ef4.b4cc62",
"g": "8c22d8c9580d2858",
"name": "",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "1",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 1110,
"y": 1060,
"wires": [
[
"2686f0fe374cc52d"
]
]
},
{
"id": "bd842c6db6853e79",
"type": "debug",
"z": "35700ef4.b4cc62",
"g": "8c22d8c9580d2858",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 870,
"y": 860,
"wires": []
},
{
"id": "26d9630fb660bc9d",
"type": "ui_gauge",
"z": "35700ef4.b4cc62",
"g": "8c22d8c9580d2858",
"name": "",
"group": "189e2e48.d109d2",
"order": 3,
"width": "3",
"height": "2",
"gtype": "gage",
"title": "Battery",
"label": "units",
"format": "{{value}}",
"min": 0,
"max": "100",
"colors": [
"#ff0000",
"#e6e600",
"#008000"
],
"seg1": "30",
"seg2": "70",
"x": 720,
"y": 1060,
"wires": []
},
{
"id": "0d4f374253a9f8d4",
"type": "ui_gauge",
"z": "35700ef4.b4cc62",
"g": "8c22d8c9580d2858",
"name": "",
"group": "189e2e48.d109d2",
"order": 4,
"width": "3",
"height": "2",
"gtype": "gage",
"title": "Voltage",
"label": "units",
"format": "{{value}}",
"min": 0,
"max": "5",
"colors": [
"#ff0000",
"#e6e600",
"#008000"
],
"seg1": "",
"seg2": "",
"x": 620,
"y": 1000,
"wires": []
},
{
"id": "e9b9f2a1a8421187",
"type": "function",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "",
"func": "msg.payload = {}\nmsg.payload.chatId = CHANGE_CHAT_ID\nmsg.payload.type = \"message\"\nmsg.payload.content = \"Test sms! [myDacha]\"\nreturn msg;\n",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 990,
"y": 400,
"wires": [
[
"f5b3043a.2a3ff8"
]
]
},
{
"id": "548c306e6e2c111f",
"type": "debug",
"z": "35700ef4.b4cc62",
"g": "3a19bea8.e27032",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 340,
"y": 400,
"wires": []
},
{
"id": "fddd6117.b012f",
"type": "ui_group",
"name": "Camera",
"tab": "39cad93e.df1b16",
"order": 3,
"disp": true,
"width": "10",
"collapse": false
},
{
"id": "371d08d2.682078",
"type": "ui_group",
"name": "Проверки",
"tab": "39cad93e.df1b16",
"order": 4,
"disp": true,
"width": "6",
"collapse": false
},
{
"id": "189e2e48.d109d2",
"type": "ui_group",
"name": "Состояние",
"tab": "39cad93e.df1b16",
"order": 1,
"disp": true,
"width": "6",
"collapse": false
},
{
"id": "172f5cc2.238383",
"type": "ui_group",
"name": "Показатели",
"tab": "39cad93e.df1b16",
"order": 2,
"disp": true,
"width": "18",
"collapse": false
},
{
"id": "d9de0dcc.3d2a6",
"type": "telegram bot",
"botname": "house_bot",
"usernames": "",
"chatids": "",
"baseapiurl": "",
"updatemode": "polling",
"pollinterval": "300",
"usesocks": false,
"sockshost": "",
"socksport": "6667",
"socksusername": "anonymous",
"sockspassword": "",
"bothost": "",
"botpath": "",
"localbotport": "8443",
"publicbotport": "8443",
"privatekey": "",
"certificate": "",
"useselfsignedcertificate": false,
"sslterminated": false,
"verboselogging": false
},
{
"id": "93c1182fe25c4f4a",
"type": "ina-sensor-manager",
"address": "0x36",
"delay": "1000",
"ohms": "0.01",
"customResistor": false
},
{
"id": "39cad93e.df1b16",
"type": "ui_tab",
"name": "Dacha",
"icon": "dashboard",
"order": 2,
"disabled": false,
"hidden": false
}
]
Загружаем проект
В правом верхнем углу вызываем меню и выбираем «Импорт».
Далее можно вставить содержимое json-файла из буфера обмена или указать файл для импорта.
После успешного импорта json-файла в основном окне отобразятся все цепочки потока:
Проект разбит на пять блоков (сверху вниз, слева направо):
постановка на охрану и обработка сигналов с IP-камеры;
логика работы с датчиком температуры;
отображение системных метрик Raspberry Pi;
обработка данных с UPS Lite;
управление Raspberry Pi Camera.
Для удобства просмотра сделал все отдельными блоками. В случае, если функционал не используется, например, отсутствует UPS Lite, блок можно легко удалить.
После загрузки потоки существуют только в редакторе, осталось загрузить их на сервер нажатием кнопки «Развернуть» в правом верхнем углу Node-RED.
На этом этапе система уже может выполнять охранные функции, оповещать в Telegram и предоставлять локальное онлайн-наблюдение.
Далее будет описаны настройки, если требуется хранение видео на удаленном носителе или управление системой через интернет.
Настраиваем роутер и NAS
Для себя решил, что хранить видео с IP-камеры буду удаленно на своем NAS. Это позволит собирать записи в высоком качестве за долгий период, а также обеспечит быстрый просмотр нужного фрагмента видео (все-таки домашний интернет значительно быстрее дачного).
Так как захват видео с IP-камеры инициализируется самим NAS, а LTE-модем не имеет белого IP-адреса, нам потребуется VPN. В качестве VPN-сервера я выбрал Wireguard. Его преимуществом является быстрая работа (что важно для мобильного интернета) и простая настройка. Мой роутер от Mikrotik умеет работать с Wireguard.
Настройка роутера сводится к добавлению интерфейса Wireguard, генерации приватного и публичного ключей сервера, плюс добавлении клиентов и маршрутов. Настройка Wireguard на роутерах Mikrotik вариант Site to Site подробно описана на официальном сайте. Вариант настройки через графический интерфейс можно найти на Хабре, например тут. Мой вариант настройки, где локальная сеть имеет адрес 192.168.0.255, VPN адрес 10.10.10.1, а локальная сеть на даче - 192.168.1.255 и VPN адрес 10.10.10.2 имеет такие настройки:
/interface wireguard
add comment="WG Server" listen-port=51820 mtu=1420 name=wg_server
/interface wireguard peers
add allowed-address=10.10.10.2/32,192.168.1.0/24 comment="WG Client" interface=wg_server persistent-keepalive=25s public-key=\
"публичный ключ из файла wg-public.key"
/ip route
add disabled=no distance=2 dst-address=192.168.1.0/24 gateway=10.10.10.2 pref-src="" scope=30 target-scope=10
Для записи и хранения видео с IP-камеры NAS имеет программный модуль Surveillance Station. Это приложение, которое позволяет управлять IP-камерами из перечня поддерживаемых. Если вашей нет в списке, это не проблема, все IP-камеры имеют протокол ONVIF, который также поддерживает приложение.
Конфигурируем WireGuard на Raspberry Pi
Устанавливаем Wireguard:
$ sudo apt install wireguard
Генерируем приватный и публичный ключи клиента:
$ (umask 077 && wg genkey > wg-private.key)
$ wg pubkey < wg-private.key > wg-public.key
Создаем файл /etc/wireguard/wg0.conf с содержимым:
[Interface]
Address = 10.10.10.2/24
PrivateKey = Приватный ключ из файла wg-private.key
PostUp = /etc/wireguard/scripts/add-nat-routing.sh
PostDown = /etc/wireguard/scripts/delete-nat-routing.sh
[Peer]
PublicKey =
AllowedIPs = 10.10.10.0/24, 192.168.1.0/24б 192.168.0.0/24
Endpoint = белый IP роутера:51820
Содержимое файла wg-public.key надо вставить в настройки клиента в процессе настройки Wireguard на роутере.
В параметры PostUp и PostDown указал путь к скриптам, которые проставляют маршруты и инициализируют LTE-модем при запуске VPN соединения, а при отключении VPN - удаляют маршруты.
Скрипт add-nat-routing.sh
#!/bin/bash
IPT="/sbin/iptables"
IN_FACE="br0" # NIC connected to the internet
WG_FACE="wg0" # WG NIC
SUB_NET="10.10.10.0/24" # WG IPv4 sub/net aka CIDR
WAN_FACE="wan"
curl -H "Referer: http://192.168.99.1/index.html" "http://192.168.99.1/goform/goform_set_cmd_process?goformId=SET_CONNECTION_MODE&ConnectionMode=auto_dial"
## IPv4 ##
$IPT -t nat -A POSTROUTING -o $WG_FACE -j MASQUERADE
$IPT -t nat -A POSTROUTING -o $WAN_FACE -j MASQUERADE
$IPT -t nat -A POSTROUTING -s $SUB_NET -o $IN_FACE -j MASQUERADE
Скрипт delete-nat-routing.sh
#!/bin/bash
IPT="/sbin/iptables"
## IPv4 ##
$IPT -t nat -F
Настройки и названия интерфейсов будут описаны в разделе о конфигурировании точки доступа. Команда curl в скрипте add-nat-routing.sh выполняет инициализацию LTE-модема, команду взял из документации к моему LTE-модему.
Конфигурируем точку доступа
Так как у Raspberry Pi Zero W есть wi-fi модуль, то я решил этим воспользоваться и сделать полноценную точку доступа для раздачи интернета на даче. Процесс создания точки доступа довольно сложный, руководствовался несколькими статьями тут и тут. Первым делом устанавливаем пакеты.
Для настройки точки доступа:
sudo apt-get install hostapd
Для создания моста между wi-fi и ethernet интерфейсами:
sudo apt-get install bridge-utils
Описал интерфейсы.
Интерфейс для локальной сети lan описал в файле /etc/network/interfaces.d/lan:
iface lan inet manual
Интерфейс для беспроводной сети wifi описал в файле /etc/network/interfaces.d/wifi:
iface wifi inet manual
Интерфейс LTE-модема wan описал в файле /etc/network/interfaces.d/wan:
iface wan inet static
address 192.168.99.100
netmask 255.255.255.0
gateway 192.168.99.1
Создаем мост и включаем в него интерфейсы lan и wifi:
sudo brctl addbr br0
sudo brctl addif br0 lan
sudo brctl addif br0 wifi
Мост br0 описал в файле /etc/network/interfaces.d/br0:
auto br0
iface br0 inet static
bridge_ports wifi lan
address 192.168.1.1
broadcast 192.168.1.255
netmask 255.255.255.0
Чтобы мост заработал, исключаем из DHCP интерфейс wan, добавив в конец файла /etc/dhcpcd.conf:
denyinterfaces wan
Добавляем диапазон для раздачи адресов в файл /etc/dhcp/dhcpd.conf:
subnet 192.168.1.0 netmask 255.255.255.0 {
range 192.168.1.100 192.168.1.254;
option routers 192.168.1.1;
option subnet-mask 255.255.255.0;
}
Осталось настроить саму точку доступа. Для этого создадим файл /etc/hostapd/hostapd.conf со следующими параметрами:
## Wireless network interface ###
interface=wifi
### Driver ###
driver=nl80211
### Network name SSID ###
ssid=НАЗВАНИЕ_WIFI_СЕТИ
### Set frequency to 2.4 Ghz ###
hw_mode=g
### Channel number ###
channel=4
### Enable Wi-Fi N ###
ieee80211n=1
### Enable WMM ###
wmm_enabled=1
### Enable 40 Mhz channels ###
ht_capab=[HT40][SHORT-GI-20][DSSS_CCK-40]
### Allow all MAC Address ###
macaddr_acl=0
### Use WPA Auth ###
auth_algs=1
### Require clients to know the network name ###
ignore_broadcast_ssid=0
### Use WPA2 ###
wpa=2
### Enable Pre-Shared Key ###
wpa_key_mgmt=WPA-PSK
### Network key ###
wpa_passphrase=ПАРОЛЬ_СЕТИ_WIFI
### Use AES ###
rsn_pairwise=CCMP
bridge=br0
Обратите внимание, что там где указано НАЗВАНИЕ_СЕТИ_WIFI и ПАРОЛЬ_СЕТИ_WIFI следует придумать свои.
Указываем расположение конфигурационного файла для точки доступа в файле /etc/default/hostapd:
DAEMON_CONF="/etc/hostapd/hostapd.conf"
Осталось включить переадресацию трафика, раскомментировав в файле /etc/sysctl.conf строку:
net.ipv4.ip_forward=1
Выполняем рестарт Raspberry. После рестарта мы получим полноценный роутер с раздачей IP-адресов как по wi-fi, так и по ethernet, плюс подключенный интернет через LTE-модем и настроенный VPN-клиент.
Проверяем доступ в интернет. Проверяем сетевую связность между домашним роутером и Raspberry по локальным адресам.
В итоге получилось Site-to-Site соединение между домашней и дачной сетями. Это позволяет осуществлять доступ ко всем устройствам и сервисам на даче по внутренним адресам из домашней локальной сети через VPN, и доступ в интернет и в домашнюю сеть из локальной сети на даче.
Что дальше
В таком виде система работает уже более трех лет, сейчас появилась потребность добавить еще одну IP-камеру. Для этого достаточно подключить USB Ethernet-адаптер или добавить еще один Raspberry Pi Ethernet/USB HUB HAT к Raspberry Pi. Входов-выходов у Raspberry достаточно, чтобы добавить датчик дыма, а датчик температуры использовать только для наблюдения. Есть возможность добавить релейный модуль для включения света на участке при срабатывании движения от IP-камеры. Главное, что модернизация не потребует замены распределительного щитка и легко уместится в существующем.
Альтернатива
Raspberry Pi Zero был выбран исключительно из-за его размеров. Если размеры распределительного щитка позволяют, то проще будет использовать Raspberry Pi 3 и новее. В нем уже есть Ethernet и USB-порты.
Конечно, можно обойтись и без I/O в IP-камере и использовать камеру с wi-fi, а управление и оповещение осуществлять из приложения через облако вендора. Для себя я исключил этот вариант только потому, что считаю проводное соединение надежнее, а облака в последнее время все чаще становятся платными.
Что еще? Недавно на рынок вышел интересный продукт - Mikrotik KNOT. Это полноценный роутер с wi-fi и POE, но самое главное - у него есть GPIO, что позволит подключить не только вход-выход IP-камеры, но и датчики, в том числе аналоговые.
*Статья написана в рамках ХабраЧелленджа 0.1, который прошел в ЛАНИТ осенью 2023 года