Tech
October 28, 2020

Raspberry Pi CCTV Discord bot

Raspberry Pi 3B+

Sup


Сегодня мы будем строить систему видеонаблюдения, которая будет присылать в дискорд смешные видосы (иногда с нашим участием) при обнаружении движения в охраняемой области.

Я использовал Raspberry Pi 3B+ и веб-камеру Logitech C270. Дополнительно на камере закреплена небольшая широкоугольная линза.

Motion


Для обнаружения движения на изображении с камеры используется пакет motion.

$ yay -Sy motion
$ sudo systemctl enable motion.service
By the way I am using Archlinux. И обертку yay для pacman'а. В вашем любимом дистре команды установки пакетов могут быть другими. И я предполагаю, что первоначальная настройка RPi уже выполнена - вы установили и обновили ОС и подключились к сети.

Приведу полностью свой /etc/motion/motion.conf :

# Motion будет запускаться как сервис systemd, 
# поэтому родная функциональность демона у него отключена
daemon off
setup_mode off
log_level 6

# Директория, куда будут сохраняться видео
target_dir /opt/CAM1

# Настройки камеры
videodevice /dev/video0
v4l2_palette 8
width 1280
height 720
# Частота кадров снижена из соображений производительности
framerate 2

text_left CAMERA1
text_right %Y-%m-%d\n%T-%q
text_changes on
emulate_motion off

# Порог срабатывания в количестве изменившихся пикселей
threshold 3500
noise_level 32
despeckle_filter EedDl
minimum_motion_frames 1

# Минимальный интервал между срабатываниями обнаружения
event_gap 30
pre_capture 2
post_capture 2

# Вывод картинок отключен
picture_output off
picture_output_motion off
picture_filename %Y%m%d%H%M%S-%q

# Настройки видео
movie_output on
movie_max_time 60
movie_quality 55
movie_codec mp4
movie_filename %t-%v-%Y%m%d%H%M%S
movie_passthrough off

# Веб-интерфейс отключен
webcontrol_port 8080
webcontrol_localhost on
webcontrol_parms 0
webcontrol_html_output off

# Вывод видеопотока в реальном времени отключен
stream_port 8081
stream_localhost on
stream_motion off
stream_maxrate 15

# Предотвращение срабатывания при резком включении света
lightswitch_percent 65
lightswitch_frames 1

# Место, где обнаружено движение будет обведено красным прямоугольником
locate_motion_mode on
locate_motion_style redbox

# По завершении создания видео запустить этот скрипт
on_movie_end /opt/motion_scripts/mvfin.sh %f

Описание опций можно посмотреть в документации к motion.

Так как видеофайл создается сразу же когда обнаружится движение, то прежде чем отправлять его, нам нужно дождаться, пока он будет создан полностью.
Для этого motion вызывает простой скрипт, перемещающий готовый файл в другую директорию, которую периодически проверяет бот.

## /opt/motion_scripts/mvfin.sh

#!/usr/bin/sh
if [ -f $1 ]; then
	mv $1 /opt/FIN1/
fi

Чтобы всё работало, motion должен иметь права на запись в директории /opt/CAM1 и /opt/FIN1. А так же иметь права выполнять скрипт.

$ cd /opt && sudo mkdir CAM1 FIN1
$ sudo chown -R motion:video CAM1 FIN1 motion_scripts
$ sudo chmod -R o-rwx CAM1 FIN1 motion_scripts
$ sudo chmod g+w FIN1
$ sudo usermod -a video YourUserName

$ ls -l
drwxr-x--- 2 motion video 3488 Oct 27 09:29 CAM1
drwxrwx--- 2 motion video 3488 Oct 27 09:29 FIN1
drwxr-x--- 2 motion video 3488 Oct 26 11:55 motion_scripts

$ ls -l motion_scripts/mvfin.sh 
-rwxr-x--- 1 motion video 61 Oct 26 11:55 motion_scripts/mvfin.sh

Бот, который будет запущен от обычного пользователя должен иметь право удалять файлы из директории /opt/FIN1, поэтому её права установлены в 770, а мой пользователь добавлен в группу video.

Теперь motion можно запускать и проверить, что в /opt/FIN1 появляются файлы.
$ sudo systemctl start motion.service

Python


Для работы бота потребуется установить пакет discord.py
$ yay -S python-discord

Возможно, вы предпочтёте создать venv и ставить пакеты pip'ом.

Создаем директорию ~/cctv, и в ней два файла:

## config.py

# Токен бота
TOKEN = 'Qwertyuiop'

# Идентификатор канала для уведомлений
CHANNEL = 123123123123

# Директория из которой будут отправляться файлы
CAMDIR = '/opt/FIN1'

# Интервал проверки новых файлов
INTERVAL = 120

## bot.py

import os
import time
import discord
import asyncio
import config

class CCTV(discord.Client):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.bg_task = self.loop.create_task(self.check_vid_task())

    async def on_ready(self):
        print('Bot login: {0.user}'.format(client))

    async def on_message(self, message):
        if message.author == client.user:
            return

        if message.content.startswith('&ping'):
            await message.channel.send(f'pong {round (client.latency * 1000)}ms ')

    async def check_vid_task(self):
        await self.wait_until_ready()
        chan = self.get_channel(config.CHANNEL)
        while not self.is_closed():
            for newfile in os.listdir(config.CAMDIR):
                await chan.send(content=time.ctime(), 
                                file=discord.File(config.CAMDIR + '/' + newfile))
                os.remove(config.CAMDIR + '/' + newfile)
            await asyncio.sleep(config.INTERVAL)

client = CCTV()

client.run(config.TOKEN)

Следуя инструкции, создаем бота, и пишем его токен в конфиг.

Создаем в дискорде сервер, в котором не будет никаких лишних участников. И добавляем текстовый канал для уведомлений от бота. ID этого канала тоже пишем в конфиг.

ID копируется через контекстное меню канала

Добавляем бота на созданный сервер, по той же инструкции.

Стартуем бота
$ python ~/cctv/bot.py

Если всё сделано правильно, бот напишет свой логин в терминал и начнет отправлять в канал сообщения с видео, которые успели накопиться с запуска motion'a.

Автозапуск


Чтобы бот стартовал сам после загрузки RPi создадим пользовательский юнит systemd.

## ~/.config/systemd/user/cctvbot.service

[Unit]
Description=CCTV Discord Bot

[Service]
ExecStart=python /home/YourUserName/cctv/bot.py

[Install]
WantedBy=default.target

Включаем его:

$ systemctl --user --now enable cctvbot.service
$ sudo loginctl enable-linger YourUserName
Еще для автозапуска можно использовать supervisor

Примеры работы


Пример сообщений от бота.
Коварный тип гражданской наружности.

Заключение


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

Есть три варианта:

  1. Физический переключатель, подключенный к GPIO. А в коде скрипта считывать его состояние с помощью python-periphery.
  2. Подписать бот на специальный topic в моей MQTT сети и управлять видеонаблюдением со смартфона.
  3. Включать бота просто по расписанию. Например даже cron'ом.

UPD-0

Отправка оповещений теперь включается если мой смартфон не подключен к местной беспроводной сети.
А проверяется это банальным пингом:

...
if subprocess.call(['ping', '-c', '1', '-w', '1', '192.168.0.10']) == 1:
    await chan.send(...)
...

Конечно, нужно еще импортировать модуль subprocess в коде бота и подкрутить настройки DHCP на роутере, чтобы смартфон каждый раз получал один и тот же IP адрес.