Телеграм-бот для домашнего видео-наблюдения из подручных материалов

в 12:14, , рубрики: python, Raspberry Pi, telegram bots, Разработка для интернета вещей, Системы обмена сообщениями

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

Автор: Insaned

Источник

  1. ABC:

    Не понятен последний абзац перед результатом, что за скрипт куда его ложить как в супервизор записать? {{{Для периодического запуска скрипта отправки, можно было использовать cron, но из соображений единообразия я тоже запускаю его через supervisor и такой bash-скрипт}}}}

    Второй вопрос как быть со звуком – была бы крутая видеоняня…

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js