Disclaimer
Эта статья содержит некоторое количество программного кода, написанного на языке Python. Ввиду того, что автор статьи по профессии является сисадмином, но не программистом — стиль и качество этого кода, могут вызвать проявление неконтролируемых эмоций у профессионалов. Пожалуйста, немедленно прекратите чтение если вид неаккуратного или неоптимального кода может негативно сказаться на вашем психическом состоянии.
Постановка задачи
Основной причиной реализации проекта, явилась простуда с вытекающими: избытком свободного времени и невозможностью выходить из дома. Порывшись у себя в столе я обнаружил:
- RaspberryPi 3 model B
- Вебкамера Logitech C270
- Карта памяти Kingston microSDHC 16 Гб
- Некоторое количество проводов и адаптеров
Из всего перечисленного, было решено построить систему домашнего видео-наблюдения с функционалом оповещения о вторжении. В качестве платформы был выбран телеграм-бот. Бот имеет следующие преимущества перед другими возможными реализациями (веб, мобильное приложение):
- Не требуется установки дополнительного клиентского ПО
- Серверная часть может работать с приватным IP адресом через NAT, при этом предъявляются минимальные требования к подключению (вплоть до 3G модема)
- Большая часть инфраструктуры находится на стороне сервис-провайдера, который за меня решил вопросы авторизации, безопасности итп...
С помощью беглого анализа интернет-публикаций, существующие решения обнаружены не были.
Шаг1. Операционная система
В качестве операционной системы был использован Raspbian. Для тех кто не в курсе, это такая сборка Debian, оптимизированная под работу на железе RaspberryPi. Система характеризуется стабильностью, большим количеством доступного прикладного ПО, хорошей документацией. Установка системы тривиальна и многократно описана в разных источниках. Я не буду останавливаться на этом подробно, скажу лишь что всё сводится к скачиваню образа диска и записи его на SD-карту. (Очевидно, что использовалась версия без GUI (lite)) Относительно настроек по-умолчанию, были выполнены следующие изменения:
- Настройка OpenSSH сервера
- Настройка часового пояса
- Установка пакетов python3-pip, supervisor
apt-get install python3-pip supervisor
- Устновка модуля PyTelegramBotAPI
pip3 install PyTelegramBotAPI
Шаг2. Захват изображений
Изначально я планировал использовать какое-то готовое решение для сохранения изображений с веб-камеры, а затем самостоятельно заниматься детекцией движения, однако к моему счастью был обнаружен Motion — готовый продукт который делает именно то что мне надо: захватывает изображения с веб-камеры и определяет есть ли на них изменения. Пакет входит в стандартный репозиторий и его установка не вызывает сложностей:
apt-get install motion
Файл конфигурации (/etc/motion/motion.conf) настолько обширен, что в рамках данной статьи его невозможно описать полностью, остановлюсь лишь на тех параметрах которые значимы или были изменены от стандартных:
# Наша веб-камера
videodevice /dev/video0
# Разрешение камеры (из тех. характеристик)
width 1280
height 720
#Сколько раз в секунду снимать (от 2, до 100)
#Влияет на загрузку CPU и определяет сколько сообщений вы получите в случае "вторжения"
framerate 4
#Сколько секунд после того как движение закончилось будет происходить съемка
event_gap 0
#Сохранять картинки в формате jpg со сжатием 75
output_pictures on
quality 75
picture_type jpeg
#Не сохранять видео
ffmpeg_output_movies off
#Каждые 30 секунд делать снимок (снапшот) "просто так"
snapshot_interval 30
#Обводить белым прямоугольником область в которой обнаружено движение
locate_motion_mode on
locate_motion_style box
#Путь для хранения файлов
target_dir /var/lib/motion
#Формат имени файла для снапшота (для нас здесь важно наличие слова snapshot)
snapshot_filename %v-%Y%m%d%H%M%S-snapshot
#Важный момент! Имя файлов для снимков с движением.
#Я сильно упростил оригинальный вариант и у меня имена файлов имеют вид:
#Количество секунд с 1970 года + порядковый номер снимка за эту секунду
#таким образом имя файлов это всегда целое число которое только увеличивается
#это сильно упростило парсинг, сортировку итп...
picture_filename %s%q
Motion автоматически создает symlink на последний сохраненный снимок с именем lastsnap.jpg
Шаг 3. Программирование
Неожиданно писать пришлось значительно меньше, чем я изначально планировал. Программа состоит из двух небольших скриптов и конфигурационного файла. Дополнительно в двух текстовых файлах я храню информацию о режиме работы (включен или выключен режим обнаружения вторжения) и о последнем обработанном снимке.
В конфигурационном файле config.py хранится следующая информация: телеграм-api-токен (о том как его получить, подробно написано здесь), список ID пользователей для которых разрешен доступ, имя файла с последним снимком, путь к папке со всеми снимками.
token = '4345435465:AsdfzzsdxgsYnb8DxDtn2L5KjfePsXozjv-o0'
users=['1234567890','0987654321']
lastimage='/var/lib/motion/lastsnap.jpg'
motiondir='/var/lib/motion'
Собственно сам бот. В нем реализованы следующие функции:
- Проверить, разрешен ли пользователю доступ
- Сообщить неавторизованному пользователю его ID (чтобы он мог прийти с ним ко мне за доступом)
- Показать последний сделанный снимок
- Включить/выключить режим обнаружения
- Сообщить всем пользователям бота, что режим работы изменен
import config
import telebot
from telebot import types
import logging
import datetime
logger = telebot.logger
telebot.logger.setLevel(logging.INFO) # Outputs debug messages to console.
bot = telebot.TeleBot(config.token, threaded=True)
### Функция проверки авторизации
def autor(chatid):
strid = str(chatid)
for item in config.users:
if item == strid:
return True
return False
### Функция массвой рассылки уведомлений
def sendall(text):
if len(config.users) > 0:
for user in config.users:
try:
bot.send_message(user, text)
except:
print(str(datetime.datetime.now()) + ' ' + 'Ошибка отправки сообщения ' + text + ' пользователю ' + str(
user))
### Функция проверки режима
def checkmode():
try:
mode_file = open("mode.txt", "r")
modestring = mode_file.read()
mode_file.close()
if modestring == '1':
return True
else:
return False
except:
return False
print(str(datetime.datetime.now()) + ' ' + 'Я бот, я запустился!')
sendall(str(datetime.datetime.now()) + ' ' + 'Я бот, я запустился!')
### Главное меню
@bot.message_handler(commands=['Меню', 'start', 'Обновить'])
def menu(message):
if autor(message.chat.id):
markup = types.ReplyKeyboardMarkup()
markup.row('/Обновить', '/Охрана')
if checkmode():
bot.send_message(message.chat.id, 'Режим охраны ВКЛ.', reply_markup=markup)
else:
bot.send_message(message.chat.id, 'Режим охраны ВЫКЛ.', reply_markup=markup)
try:
f = open(config.lastimage, 'rb')
bot.send_photo(message.chat.id, f)
except:
bot.send_message(message.chat.id, 'Фоток нет')
else:
markup = types.ReplyKeyboardMarkup()
markup.row('/Обновить')
bot.send_message(message.chat.id, 'Тебе сюда нельзя. Твой ID: ' + str(message.chat.id), reply_markup=markup)
### Смена режима
@bot.message_handler(commands=['Охрана'])
def toggle(message):
if autor(message.chat.id):
try:
if checkmode():
last_file = open("mode.txt", "w")
last_file.write('0')
last_file.close()
sendall('Пользователь ' + message.chat.first_name + ' выключил режим охраны')
else:
last_file = open("mode.txt", "w")
last_file.write('1')
last_file.close()
sendall('Пользователь ' + message.chat.first_name + ' включил режим охраны')
except:
bot.send_message(message.chat.id, 'Ошибка смены режима')
print(str(datetime.datetime.now()) + ' ' + "Ошибка смены режима")
menu(message)
if __name__ == '__main__':
bot.polling(none_stop=False)
Второй скрипт, запускается с некоторой периодичностью, проверяет есть ли необработанные jpg файлы без слова snapshot в имени и если включен режим обнаружения рассылает эти файлы всем пользователям бота.
import datetime
import logging
import os
import time
import telebot
import config
logger = telebot.logger
telebot.logger.setLevel(logging.INFO) # Outputs debug messages to console.
bot = telebot.TeleBot(config.token, threaded=True)
files = []
clearfiles = []
tosend = []
tosendfull = []
### Функция проверки режима
def checkmode():
try:
mode_file = open("mode.txt", "r")
modestring = mode_file.read()
mode_file.close()
if modestring == '1':
return True
else:
return False
except:
return False
## Функция массовой пассылки фотографий
def sendall(filename):
for username in config.users:
try:
f = open(filename, 'rb')
bot.send_photo(username, f)
except:
print(
str(datetime.datetime.now()) + ' ' + 'Ошибка отправки файла ' + filename + ' пользователю ' + username)
## Функция записи последнего обработтанного файла
def writeproc(filename):
try:
last_file = open("last.txt", "w")
last_file.write(filename)
last_file.close()
return last_file.close()
except:
return False
## Функция чтения последнего обработанного файла
def readproc():
try:
last_file = open("last.txt", "r")
lasstring = last_file.read()
last_file.close()
lastint = str(lasstring)
return lastint
except:
return -1
## Читаем последний обработанный файл
processed = readproc()
if processed == -1:
print(str(datetime.datetime.now()) + ' ' + 'Не Удалось прочитать последний обработанный файл. Выходим')
quit(2)
## Читаем список файлов
files = os.listdir(config.motiondir)
files = filter(lambda x: x.endswith('.jpg'), files)
## Очищаем список от снапшотов и расширений, сортируем
for file in files:
if ('snapshot' in file) or ('last' in file) or ('-' in file):
pass
else:
clearfile = file[:-4]
clearfiles.append(clearfile)
clearfiles.sort()
## Выбираем список необработанных файлов
for file in clearfiles:
if int(file) > int(processed):
tosend.append(file)
### Если есть что отправлять:
if len(tosend) > 0:
try:
if writeproc(tosend[-1]) == False:
print(str(datetime.datetime.now()) + ' ' + 'Ошибка записи последнего элемента. Выходим!')
quit(2)
else:
print(str(datetime.datetime.now()) + ' ' + 'Последний элемент записан успешно')
### Отправляем только если успешно записали последний - иначе будет бесконечная отправка
## Сначала проверяем режим
if checkmode():
## Потом формируем список фалов с полным именем
for filename in tosend:
fullname = config.motiondir + '/' + filename + '.jpg'
tosendfull.append(fullname)
## Потом отправляем неторопливо
for filename in tosendfull:
sendall(filename)
time.sleep(1)
else:
print(str(datetime.datetime.now()) + ' ' + 'Режим отправки выключен')
except:
print(str(datetime.datetime.now()) + ' ' + 'Ошибка отправки')
else:
print(str(datetime.datetime.now()) + ' ' + 'Нечего отправлять')
Шаг 4. Собираем всё в кучу
Все скрипты я разместил в каталоге /home/bigbro/bot. Для запуска, контроля и логирования использовал supervisor. Соответственно в каталоге /etc/supervisor/conf.d я создал файлы примерно такого вида:
[program:bot]
directory=/home/bigbro/bot
command=/usr/bin/python3 /home/bigbro/botbot.py
autostart=true
autorestart=true
stderr_logfile=/var/log/bot.err.log
stdout_logfile=/var/log/bot.out.log
Для периодического запуска скрипта отправки, можно было использовать cron, но из соображений единообразия я тоже запускаю его через supervisor и такой bash-скрипт:
#!/bin/bash
while true; do python3 sender.py ; sleep 30; done;
Результат
Всё работает ровно так как и было задумано:
Комментарии (39)
bcmob
04.11.2017 21:44Как раз вчера подобным образом пристроил свою Orange Pi.
Была у меня xiaomi wifi camera, китайская версия. Связь через серваки китая часто была не радовала качеством, видео о сработке были короткими и не всегда удавалось разглядеть посетителя, да и программа их не всегда сообщала о сработке.
В итоге вчера решил разобраться с этим делом. На камере активировал rtsp, на Orange Pi установил motioneye (который использует motion) и имеет приятный интерфейс с различными настройками. Настроил детектор движения, с сохранением видео и фото. Также В этой программе есть возможность отсылать по сработке сообщение на почту либо webhok. Вот через него я и организовал с помощью POST запроса отсылку себе в телеграмм фото сработки и видеоролик движения. Теперь даже если малинку украдут, фото и видео останутся в телеграмме.
Machine79
04.11.2017 22:19-1Велосипед какой то, только еще и с деревянными колесами )))) А вот motion-project.github.io мне понравилось. Использую для других целей. А видео наблюдение в картинках не интересно. Есть давно круче видео наблюдение и не надо покупать дорогущие гаджеты. Полно экранное видео в hd качестве на любом устройстве и не важно где находишься!
Sdima1357
04.11.2017 22:431 Автор ссылается на тот же проект motion
2 У большинства людей скорости интернета наверх для HD -30 FPS не хватит если картинка динамическая, а если статическая, то она не интересна.
bcmob
04.11.2017 23:05Может кто подскажет, можно ли в motion как-либо отключить сработку на движение в момент переключения камеры в ночной режим и обратно?
Insaned Автор
04.11.2017 23:27lightswitch
Type: Integer
Range / Valid values: 0 — 100
Default: 0 (disabled)
Ignore sudden massive light intensity changes given as a percentage of the picture area that changed intensity. The value defines the picture areas in percent that will trigger the lightswitch condition. When lightswitch is detected motion detection is disabled for 5 picture frames. This is to avoid false detection when light conditions change and when a camera changes sensitivity at low light.
netmaniac
05.11.2017 00:031 У motion есть режим отправки фото и видео по движению чтоб не запускать скрипт с периодичностью.
2 камера умеет захватывать и звук который тоже интересен.
Перед отправкой конвертим ffmpeg'ом видео и звук в понятные телеграмму форматы.
На самом деле отдавать целую малину под такую фигню как то жирно.
Погугли всякие "умные дома" на малине плюсом к motion
Quarc
05.11.2017 01:11Наличие дома гиперактивного котенка, увы, обесценивает идею )
Insaned Автор
05.11.2017 01:15Универсально решения конечно нет, но продукт очень гибкий — можно задать минимальное количество пикселей, которые должны измениться чтобы произошло срабатывание, можно задавать маски с мертвыми зонами или наоборот маски с зонами в которых должно произойти движение.итд итп
Quarc
05.11.2017 02:25Действительно, как-то не подумал, кот, в любом случае, значительно меньше человека.
PS: а о том что он сорвал занавески или стянул со стола скатерть, со всем что на нем находилось, удобно знать заранее, до того как приехал домой.
leonzone
05.11.2017 01:11Спасибо за статью, сейчас как раз подобное делаю для наблюдения за гаражом. В принципе у меня почти так же вышло, но всё равно интересно. Возьму себе supervisor :)
Insaned Автор
05.11.2017 01:23Ну вообще для этого правильнее sytemd использовать. supervisor это моё личное предпочтение
obe
05.11.2017 01:30Больше года назад организовал себе на Rpi3 и совместимой шлейфовой ИК-камере видеоглазок на motion с отправкой скриншотов в я-диск по движению. Rpi встроен внутрь двери. Потом добавил ИК-диод (нужна длина 900+нм чтобы его не было видно вообще никак). Теперь видно в любое время дня и ночи происходящее за дверью вне зависимости от освещения. Сделал скрипт для автозачистки при переполнении хранилища.
Из последнего заметил и добавил motioneye. Полезно например для просмотра текущего скриншота (motion позволяет смотреть только текущее видео).
dosbear
05.11.2017 08:46У меня в столе не нашлось камеры, но зато нашелся первый распберри и геркон (стоимость в магазине 50 р.). Геркон поставил на входную дверь, соединил с пинами GND и ближайшим GPIO. Далее слежу за состоянием GPIO. При изменении состояния отправляю сообщение почтой/телеграммой. Наверное последую вашему опыту и прикручу к этой связке камеру.
Chupakabra303
05.11.2017 11:13Занимался недавно похожим, правда без Motion просто с mjpg-streamer.
Платформы TL-WR902AC и Raspberry Pi 2 Model B, на которые установил LEDE (OpenWrt). Telegram bot (черновой, но работающий вариант) написал сам на LUA (+ uHTTPd). Кроме картинок с камеры бот умеет выполнять modbus команды и другие фокусы.
Insaned Автор
05.11.2017 11:21Рассказал бы про «modbus команды и другие фокусы». Многим наверное интересно.
hobbyte
05.11.2017 14:03Мне показалось удобнее писать в mp4 по движению и по оканчании файла слать в телеграм:
on_movie_end /usr/local/bin/message2telegram.sh file "#motion detected on DoorEye at %H:%M:%S %d-%m-%Y" "%f"
Плюсы: до 10Мб телеграмом расцениваются как gif. На видео лучше видно происходящее.
EBrEH
06.11.2017 11:42Доброго дня!
Большое спасибо за интересную статью.
Начал писать по вашей статье. Получается в несколько раз меньше строк. С удовольствием обсудил бы оптимизацию кода.
У меня сразу возник вопрос: как можно ботом отправлять сообщения непосредственно пользователю по его номеру а не в чат? У меня не получилось.Insaned Автор
06.11.2017 11:50И вам!
Если Вы увидели какую-то реальную проблему в коде, которая может мешать стабильности или безопасности — я буду очень вам благодарен за эту информацию.EBrEH
06.11.2017 12:03Нет, речь пока идет только про оптимизацию, я глубоко пока не анализировал.
Вот, например:
users = [123567890, 123567891] def is_known(user): # Функция проверки авторизации return user in config.users def sendall(text): # Функция массвой рассылки уведомлений for user in config.users: try: bot.send_message(user, text) except Exception as exc: print('%s Ошибка отправки сообщения %s пользователю %d: %s' % (str(datetime.datetime.now()).split('.')[0], text, user, exc))
Идеологию мое преобразование не меняет, но вод код становится приятней 'на ощупь'.
Но это мое субъективное мнение.
Я тоже сисадмин, но в свое время окончил вуз на программиста-системщика.
Пайтон изучаю самостоятельно, но полюбил его и чувствую, что это взаимно.
EBrEH
06.11.2017 11:49Вдогонку (редактировать сообщение не дает):
т.е. себе я могу отправить сообщение, т.к. бота создал я, а кому то другому не могу. Получаю ошибку:
A request to the Telegram API was unsuccessful. The server returned HTTP 403 Forbidden.
Response body: [b'{«ok»:false,«error_code»:403,«description»:«Forbidden: bot can\'t initiate conversation with a user»}']
ingumsky
07.11.2017 17:03Очень интересная статья, спасибо! Интересовался тем, чтобы сделать аналогичную штуку, но не знал с чего подступиться. Сейчас рассмотрел свои запасы. Малину у себя не нашёл (у меня её никогда не было, впрочем), зато нашёл аналогичную камеру и карточку, теперь осталось раздобыть платформу, на которой можно сделать то, что сделано у вас.
PaulZi
07.11.2017 19:51Делал аналогичное решение. У себя я дополнительно сделал анализ клиентов wi-fi, если устройств из списка нет охрана автоматом включается, если есть — бот ничего не шлёт. Правда у меня несколько комментариев проще все, ip-камера умеет сама складывать фотки на ftp при движении в зоне. Мне осталось лишь их отправить.
side2k
08.11.2017 04:30Спасибо за заметку.
У меня тут как раз Raspberry PI(правда, постарше, вторая ещё) висит, на которую заведены и видеонаблюдение, и сбор данных с нескольких датчиков температуры/влажности, раскиданных по дому, и скоро должен приехать пучок релюшек Sonoff для управления светом и прочими
Я как раз подумывал ко всему этому прикрутить телеграм-бота, а тут ваша статья.
Правда, касательно самого видеонаблюдения, у меня всё несколько сложнее, потому что частный дом, камера не одна, а уже 4(и будет больше), и все они смотрят на улицу. Через это в моем случае рациональнее оказалось ставить железный видеорегистратор, который сам умеет следить за движением и отправлять сообщения по почте.
Другое дело, что с учетом того, что камеры уличные, лог сработок на шкале времени выглядит примерно вот так:
И это ещё не самый "активный" день.
С одной стороны — все эти сработки неплохо иметь в архиве, чтобы, например, во время отпуска можно было вечером за небольшое время посмотреть "ретроспективу" за день. С другой стороны — если каждую сработку показывать в телеграме, то поток сообщений будет слишком густым и малополезным.
Поэтому я пока ещё работаю над всем этим и думаю, как сделать правильнее. Пока видятся два возможных пути — либо прикручивать второй слой анализа картинок(например, ZoneMinder), либо ставить дополнительные датчики движения, и заводит их на "тревожные" входы регистратора.
В общем, копать — не перекопать 8)
SDKiller
Супер.
+
Особенно умиляет в таких публикациях «порывшись у себя в столе я обнаружил...»
Insaned Автор
А что не так? Лично мне малинку подарили для одного стороннего проекта, который давно закончился, а камерой обзавелся когда учил английский по скайпу. Я знаю очень много людей, которые заказали себе RaspberryPi когда они только появились из любопытства, наигрались и забросили в стол.
Sdima1357
Ну вообще-то распи 3 появилась не настолько давно, чтобы успеть забыть про неё в столе. Вроде и двух лет не прошло. А впрочем направление правильное, лучше использовать, чем просто валяется
Insaned Автор
Давно. В начале 16-го.
Sdima1357
Вы лукавите :) ничего Вы не забыли. Она лежала на краю памяти, напоминая о себе время от времени( когда же ты меня к чему-то приспособишь). Впрочем это уже оффтопик
EBrEH
У меня на столе тоже не мало и Малинок и Апельсинок, а также их производны в виде ТВБоксов. Я уже давно использую аналогичное оборудование в качестве рабочих мест в офисах.
Чего и вам всем советую.