Disclaimer


Эта статья содержит некоторое количество программного кода, написанного на языке Python. Ввиду того, что автор статьи по профессии является сисадмином, но не программистом — стиль и качество этого кода, могут вызвать проявление неконтролируемых эмоций у профессионалов. Пожалуйста, немедленно прекратите чтение если вид неаккуратного или неоптимального кода может негативно сказаться на вашем психическом состоянии.


Постановка задачи


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



Из всего перечисленного, было решено построить систему домашнего видео-наблюдения с функционалом оповещения о вторжении. В качестве платформы был выбран телеграм-бот. Бот имеет следующие преимущества перед другими возможными реализациями (веб, мобильное приложение):


  • Не требуется установки дополнительного клиентского ПО
  • Серверная часть может работать с приватным 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) настолько обширен, что в рамках данной статьи его невозможно описать полностью, остановлюсь лишь на тех параметрах которые значимы или были изменены от стандартных:


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 пользователей для которых разрешен доступ, имя файла с последним снимком, путь к папке со всеми снимками.


config.py
token = '4345435465:AsdfzzsdxgsYnb8DxDtn2L5KjfePsXozjv-o0'
users=['1234567890','0987654321']
lastimage='/var/lib/motion/lastsnap.jpg'
motiondir='/var/lib/motion'

Собственно сам бот. В нем реализованы следующие функции:


  • Проверить, разрешен ли пользователю доступ
  • Сообщить неавторизованному пользователю его ID (чтобы он мог прийти с ним ко мне за доступом)
  • Показать последний сделанный снимок
  • Включить/выключить режим обнаружения
  • Сообщить всем пользователям бота, что режим работы изменен

bot.py
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 в имени и если включен режим обнаружения рассылает эти файлы всем пользователям бота.


sender.py
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;

Результат
Всё работает ровно так как и было задумано:


image

Комментарии (39)


  1. SDKiller
    04.11.2017 17:14
    +1

    Супер.
    +

    Особенно умиляет в таких публикациях «порывшись у себя в столе я обнаружил...»


    1. Insaned Автор
      04.11.2017 17:18

      А что не так? Лично мне малинку подарили для одного стороннего проекта, который давно закончился, а камерой обзавелся когда учил английский по скайпу. Я знаю очень много людей, которые заказали себе RaspberryPi когда они только появились из любопытства, наигрались и забросили в стол.


      1. Sdima1357
        04.11.2017 17:26
        +1

        Ну вообще-то распи 3 появилась не настолько давно, чтобы успеть забыть про неё в столе. Вроде и двух лет не прошло. А впрочем направление правильное, лучше использовать, чем просто валяется


        1. Insaned Автор
          04.11.2017 17:29

          Давно. В начале 16-го.


          1. Sdima1357
            04.11.2017 17:39

            Вы лукавите :) ничего Вы не забыли. Она лежала на краю памяти, напоминая о себе время от времени( когда же ты меня к чему-то приспособишь). Впрочем это уже оффтопик


    1. EBrEH
      06.11.2017 12:11

      У меня на столе тоже не мало и Малинок и Апельсинок, а также их производны в виде ТВБоксов. Я уже давно использую аналогичное оборудование в качестве рабочих мест в офисах.
      Чего и вам всем советую.


  1. PavelNN
    04.11.2017 18:16

    Классное решение!
    Постараюсь повторить.
    Жаль нужно заказывать — в столе рыться бессмысленно)


    1. Insaned Автор
      04.11.2017 18:57

      Будут вопросы — обращайтесь.


  1. apterion7
    04.11.2017 18:56

    В disclaimer так написано, как будто в статье использованы кадры из тяжелого гей-порно.))
    Спасибо, коллега, повторю ваш опыт.


    1. Apcel
      04.11.2017 20:51

      Примерно так её и воспринимают знающие про генераторы, ContextManager'ы и прочий синтаксический сахар люди, да.


  1. tommy24
    04.11.2017 21:15

    Если токен, который вы указали настоящий, то лучше затереть его. Любой может нарушить стабильность работы бота


    1. Insaned Автор
      04.11.2017 21:17

      Нет конечно. Равно как и ID пользователей. Но всё равно спасибо за предостережение.


  1. bcmob
    04.11.2017 21:44

    Как раз вчера подобным образом пристроил свою Orange Pi.
    Была у меня xiaomi wifi camera, китайская версия. Связь через серваки китая часто была не радовала качеством, видео о сработке были короткими и не всегда удавалось разглядеть посетителя, да и программа их не всегда сообщала о сработке.
    В итоге вчера решил разобраться с этим делом. На камере активировал rtsp, на Orange Pi установил motioneye (который использует motion) и имеет приятный интерфейс с различными настройками. Настроил детектор движения, с сохранением видео и фото. Также В этой программе есть возможность отсылать по сработке сообщение на почту либо webhok. Вот через него я и организовал с помощью POST запроса отсылку себе в телеграмм фото сработки и видеоролик движения. Теперь даже если малинку украдут, фото и видео останутся в телеграмме.


  1. Machine79
    04.11.2017 22:19
    -1

    Велосипед какой то, только еще и с деревянными колесами )))) А вот motion-project.github.io мне понравилось. Использую для других целей. А видео наблюдение в картинках не интересно. Есть давно круче видео наблюдение и не надо покупать дорогущие гаджеты. Полно экранное видео в hd качестве на любом устройстве и не важно где находишься!


    1. Sdima1357
      04.11.2017 22:43

      1 Автор ссылается на тот же проект motion
      2 У большинства людей скорости интернета наверх для HD -30 FPS не хватит если картинка динамическая, а если статическая, то она не интересна.


  1. bcmob
    04.11.2017 23:05

    Может кто подскажет, можно ли в motion как-либо отключить сработку на движение в момент переключения камеры в ночной режим и обратно?


    1. Insaned Автор
      04.11.2017 23:27

      lightswitch


      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.


  1. netmaniac
    05.11.2017 00:03

    1 У motion есть режим отправки фото и видео по движению чтоб не запускать скрипт с периодичностью.
    2 камера умеет захватывать и звук который тоже интересен.
    Перед отправкой конвертим ffmpeg'ом видео и звук в понятные телеграмму форматы.
    На самом деле отдавать целую малину под такую фигню как то жирно.
    Погугли всякие "умные дома" на малине плюсом к motion


  1. Quarc
    05.11.2017 01:11

    Наличие дома гиперактивного котенка, увы, обесценивает идею )


    1. Insaned Автор
      05.11.2017 01:15

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


      1. Quarc
        05.11.2017 02:25

        Действительно, как-то не подумал, кот, в любом случае, значительно меньше человека.
        PS: а о том что он сорвал занавески или стянул со стола скатерть, со всем что на нем находилось, удобно знать заранее, до того как приехал домой.


  1. leonzone
    05.11.2017 01:11

    Спасибо за статью, сейчас как раз подобное делаю для наблюдения за гаражом. В принципе у меня почти так же вышло, но всё равно интересно. Возьму себе supervisor :)


    1. Insaned Автор
      05.11.2017 01:23

      Ну вообще для этого правильнее sytemd использовать. supervisor это моё личное предпочтение


  1. obe
    05.11.2017 01:30

    Больше года назад организовал себе на Rpi3 и совместимой шлейфовой ИК-камере видеоглазок на motion с отправкой скриншотов в я-диск по движению. Rpi встроен внутрь двери. Потом добавил ИК-диод (нужна длина 900+нм чтобы его не было видно вообще никак). Теперь видно в любое время дня и ночи происходящее за дверью вне зависимости от освещения. Сделал скрипт для автозачистки при переполнении хранилища.
    Из последнего заметил и добавил motioneye. Полезно например для просмотра текущего скриншота (motion позволяет смотреть только текущее видео).


  1. dosbear
    05.11.2017 08:46

    У меня в столе не нашлось камеры, но зато нашелся первый распберри и геркон (стоимость в магазине 50 р.). Геркон поставил на входную дверь, соединил с пинами GND и ближайшим GPIO. Далее слежу за состоянием GPIO. При изменении состояния отправляю сообщение почтой/телеграммой. Наверное последую вашему опыту и прикручу к этой связке камеру.


  1. Chupakabra303
    05.11.2017 11:13

    Занимался недавно похожим, правда без Motion просто с mjpg-streamer.
    Платформы TL-WR902AC и Raspberry Pi 2 Model B, на которые установил LEDE (OpenWrt). Telegram bot (черновой, но работающий вариант) написал сам на LUA (+ uHTTPd). Кроме картинок с камеры бот умеет выполнять modbus команды и другие фокусы.


    1. Insaned Автор
      05.11.2017 11:21

      Рассказал бы про «modbus команды и другие фокусы». Многим наверное интересно.


    1. Bookvarenko
      05.11.2017 17:31

      Да, было бы интересно узнать поподробнее об этом в формате статьи.


  1. 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. На видео лучше видно происходящее.


  1. EBrEH
    06.11.2017 11:42

    Доброго дня!
    Большое спасибо за интересную статью.
    Начал писать по вашей статье. Получается в несколько раз меньше строк. С удовольствием обсудил бы оптимизацию кода.
    У меня сразу возник вопрос: как можно ботом отправлять сообщения непосредственно пользователю по его номеру а не в чат? У меня не получилось.


    1. Insaned Автор
      06.11.2017 11:50

      И вам!
      Если Вы увидели какую-то реальную проблему в коде, которая может мешать стабильности или безопасности — я буду очень вам благодарен за эту информацию.


      1. 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))

        Идеологию мое преобразование не меняет, но вод код становится приятней 'на ощупь'.
        Но это мое субъективное мнение.
        Я тоже сисадмин, но в свое время окончил вуз на программиста-системщика.
        Пайтон изучаю самостоятельно, но полюбил его и чувствую, что это взаимно.


  1. 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»}']


    1. Insaned Автор
      06.11.2017 11:52

      Всё так. Дело не в том что вы его создали а в том что сами инициировали с ним диалог. Человек которому вы хотите написать — должен так же сначала сам пообщаться с ботом.


      1. EBrEH
        06.11.2017 13:32

        Спасибо.
        Понятно.


  1. ingumsky
    07.11.2017 17:03

    Очень интересная статья, спасибо! Интересовался тем, чтобы сделать аналогичную штуку, но не знал с чего подступиться. Сейчас рассмотрел свои запасы. Малину у себя не нашёл (у меня её никогда не было, впрочем), зато нашёл аналогичную камеру и карточку, теперь осталось раздобыть платформу, на которой можно сделать то, что сделано у вас.


  1. PaulZi
    07.11.2017 19:51

    Делал аналогичное решение. У себя я дополнительно сделал анализ клиентов wi-fi, если устройств из списка нет охрана автоматом включается, если есть — бот ничего не шлёт. Правда у меня несколько комментариев проще все, ip-камера умеет сама складывать фотки на ftp при движении в зоне. Мне осталось лишь их отправить.


    1. Insaned Автор
      08.11.2017 08:36

      Отличная идея!


  1. side2k
    08.11.2017 04:30

    Спасибо за заметку.
    У меня тут как раз Raspberry PI(правда, постарше, вторая ещё) висит, на которую заведены и видеонаблюдение, и сбор данных с нескольких датчиков температуры/влажности, раскиданных по дому, и скоро должен приехать пучок релюшек Sonoff для управления светом и прочими
    Я как раз подумывал ко всему этому прикрутить телеграм-бота, а тут ваша статья.


    Правда, касательно самого видеонаблюдения, у меня всё несколько сложнее, потому что частный дом, камера не одна, а уже 4(и будет больше), и все они смотрят на улицу. Через это в моем случае рациональнее оказалось ставить железный видеорегистратор, который сам умеет следить за движением и отправлять сообщения по почте.
    Другое дело, что с учетом того, что камеры уличные, лог сработок на шкале времени выглядит примерно вот так:


    множество сработок за день


    И это ещё не самый "активный" день.
    С одной стороны — все эти сработки неплохо иметь в архиве, чтобы, например, во время отпуска можно было вечером за небольшое время посмотреть "ретроспективу" за день. С другой стороны — если каждую сработку показывать в телеграме, то поток сообщений будет слишком густым и малополезным.


    Поэтому я пока ещё работаю над всем этим и думаю, как сделать правильнее. Пока видятся два возможных пути — либо прикручивать второй слой анализа картинок(например, ZoneMinder), либо ставить дополнительные датчики движения, и заводит их на "тревожные" входы регистратора.


    В общем, копать — не перекопать 8)