Введение

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

Дмитрий Попов

Привет, это я! Данную статью я выпускаю как частное лицо.

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

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

Сама статья - это результат моего исследования в рамках расширения своих профессиональных навыков. 

Изначально я видел эту статью, как некую историю про то “как не программист программу писал”, но в текущем виде она скорее представляет из себя декомпозицию той работы, которую я проделал при изучении базовых навыков python и содержит конкретный пример функционала, который можно реализовать с настоящим железом из области СКУД.

Что будет в статье?

  1. Как написать рабочий, но не идеальный код, чтобы через телеграм бота открыть дверь в СКУД Sigur;

  2. Пояснение за библиотеки, модули и функции, и откуда они берутся в коде этой программы. Почему именно Python?

  3. Как все это великолепие установить и запустить в Docker.


Среда разработки и язык программирования

Язык программирования Python

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

Среда разработки PyCharm

Здесь все просто - PyCharm от компании JetBrains это самая популярная интегрированная среда разработки (IDE), которая специально создана для написания и отладки кода на Python. 

По моим наблюдениям, все остальные IDE для питона являются либо менее востребованными, либо узкоспециализированными и соответственно менее функциональными для широкого класса задач, например Spyder - среда адаптированная для разработчиков в области data science. 


Пишем код

Сознательно пропустим процесс установки питона и среды разработки на ПК и перейдем сразу к используемым библиотекам и самому коду.

Структура приложения

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

  1. main.py - основной кусок кода, где мы будем обращаться к боту и вызывать различные команды по API системы контроля и управления доступом Sigur;

  2. config.yml - файл конфигурации, где мы будем прописывать реквизиты для подключения к боту, к API и указывать различные кастомизированные параметры в программе. К данному файлу мы будем обращаться из основного файла кода;

  3. requirements.txt - это файл, который содержит все зависимости приложения, там будут указаны версии используемых библиотек в коде программы. Этот файл пригодится нам при развертывании приложения в docker, а также может быть полезен при переносе вашего кода на другую машину или развертывании в другом окружении.

  4. Dockerfile - файл, который содержит некоторую инструкцию о том, как собрать докер контейнер. В нем мы будем указывать параметры создания образа, окружения, установки библиотек и другие сложные с ходу для понимания в докере вещи.

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

Библиотеки

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

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

  1. aiogram - фреймворк на python для работы с API Telegram. Достаточно быстро обновляется под актуальные версии API телеграма, при его использовании я меньше сталкивался с проблемами, в отличии от других известных библиотек для телеграм. 

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

  1. yaml - позволяет работать с yml файлами. Используем для подгрузки данных из файла конфигурации;

  2. PIL - позволяет работать с изображениями. Используем ее вместе с библиотекой cv2;

  3. cv2 - библиотека алгоритмов для работы с машинным зрением, изображениями и численными алгоритмами. Для нас она интересна возможностью вытащить изображение из rtsp потока камеры видеонаблюдения; (в этой части так и не будет использован);

  4. socket - это модуль для работы с сетевым соединением в python. Мы его будем использовать для подключения к интеграционному интерфейсу по tcp.

  5. другие библиотеки, которые я забыл здесь отметить, sorry.

Подключение к телеграм

Заполним часть нашего конфигурационного файла

#BOT, где token_api - токен бота телеграм, users - список пользователей, которым разрешен доступ к боту.
token_api: '1364382023:FAHsqdfVlTfDAX23sYJxG0fIbdfDFSDfs'
users: [12345678, 12345679]

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

Узнать свой user ID в telegram можно в боте @getmyid_bot

Импортируем нужные нам библиотеки для работы

from aiogram import executor, Bot, Dispatcher, types
import yaml

Из aiogram импортируем нужные классы и модуль, где:

  • executor отвечает за запуск бота и выполнение функций;

  • Bot определяет с какими командами обращается пользователь и как на них отвечать

  • Dispatcher отслеживает обновления команд

  • types - модуль, который обрабатывает текстовые сообщения от пользователя

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

pip install aiogram==2.20
pip install PyYAML~=6.0

Я указал конкретные версии библиотек, с которыми производил тестирование работоспособности программы.

Вызываем конфигурационный файл и забираем из него нужные данные для подключения

with open(r'config.yml') as file:
  token_api = configdata['token_api']
  users = configdata['users']

Инициализируем работу функций Bot и Dispatcher, а также назначаем переменной список пользователей из файла конфигурации.

bot = Bot(token = token_api)
dp = Dispatcher(bot=bot)
users = users

Напишем первый обработчик команды /start и реализуем проверку по списку разрешенных пользователей. Данную проверку нам потребуется реализовать на каждую вызываемую команду или функцию.

@dp.message_handler(commands=['start'])
async def cmd_start(message: types.Message) -> None:
   if message.chat.id not in users:
       await bot.send_message(chat_id=message.from_user.id, text='У вас нет доступа. Обратитесь к администратору')
   else:
       await message.answer('Добро пожаловать')

В конце кода нашей программы мы указываем метод start_polling, который опрашивает сервер и проверяет обновления.

if __name__ == '__main__':
   executor.start_polling(dp,
                          skip_updates=True)

Так выглядит попытка обратиться к боту с user ID, которого нет в списке.

Обработка команды /start от пользователя, которого нет в списке
Обработка команды /start от пользователя, которого нет в списке

Попытка вызова другой команды, которую мы добавили для теста в код

@dp.message_handler(commands=['1'])
async def cmd_1(message: types.Message) -> None:
   await bot.send_message(chat_id=message.from_user.id, text='Вы получили доступ к команде')
Обработка команды /1 от пользователя, которого нет в списке, но и проверка на пользователя отсутствует в обработчике
Обработка команды /1 от пользователя, которого нет в списке, но и проверка на пользователя отсутствует в обработчике

Работаем с API Sigur

Для реализации нашей задачи по открыванию двери и смены состояния точек доступа в СКУД подходит API, который работает на основе текстовых команд по TCP сокету. В данном случае наша программа выступает клиентом в рамках взаимодействия с системой.

Выдержка из документации

Интерфейс реализован посредством создания на стороне сервера СКУД TCP-сервера. TCP-сервер ожидает соединений от клиентов на порту, определенном параметром «OIF1_Port» в файле конфигурации серверного процесса «sphinxd.cfg». По умолчанию используется порт 3312. После установления TCP-соединения происходит информационный обмен по этому соединению согласно протоколу, описанному ниже. Протокол является текстовым и ориентирован на передачу однострочных сообщений. Каждая передаваемая строка заканчивается символами "\r\n" (0x0D, 0x0A). Сервер передает что-либо клиенту только в ответ на запрос, переданный со стороны клиента. 

Этот документ просто так не лежит в интернете, но вы можете запросить его у производителя и он заботливо вам его предоставит

Каждый раз при подсоединении к серверу перед отправкой команд требуется совершить запрос LOGIN, где указывается версия интерфейса взаимодействия и логин и пароль пользователя, которого требуется предварительно создать и выдать ему соответствующие права на работу с интеграционным протоколом.

Создадим пользователя в приложении Клиент Sigur и выполним несколько действий:

  • перейдем на вкладку оператор, выберем Использовать, а также укажем логин и пароль, с которыми будем подключаться по интерфейсу

    ПО Клиент (Sigur)
    ПО Клиент (Sigur)
  • ниже в списке разрешений активируем чекбокс - Доступ по протоколу OIF (интеграция) и применим изменения.

    ПО Клиент (Sigur)
    ПО Клиент (Sigur)

Вернемся к коду и дополним наш конфигурационный файл данными подключения к серверу и к интеграционному интерфейсу

#ACS Sigur OIF connection params
server_ip: 127.0.0.1
server_port: 3312
login: 'login'
password: 'password'

А также не забудем их вызвать в основном коде

with open(r'config.yml') as file:
   configdata = yaml.load(file, Loader=yaml.FullLoader)
   token_api = configdata['token_api']
   acsserver_ip = configdata['server_ip']
   acsserver_port = configdata['server_port']
   acslogin = configdata['login']
   acspassword = configdata['password']
   users = configdata['users']

Открываем дверь через API Sigur

Для реализации тестового запроса создадим метод с командой /openDoor1, на которую будет реагировать наш бот. Обязательно не забываем про проверку разрешенного юзера, СКУД все-таки предназначен для ограничения доступа и мы не хотим пустить злоумышленника через нашего бота.

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

LOGIN 1.8 login password
ALLOWPASS 9 1447 UNKNOWN
расшифровка параметров:
  • 1.8 - версия интеграционного интерфейса;

  • login и password реквизиты ранее созданного оператора;

  • ALLOWPASS - команда разрешения однократного прохода;

  • параметр 9 - id точки доступа в СКУД Sigur; 

  • 1447 - id пользователя, от лица которого мы хотим произвести проход;

  • UNKNOWN (IN или OUT) - направление прохода на точке доступа. Указание направления может быть важным в том случае, если вы организуете учет рабочего времени и должны понимать, когда пользователь начинает рабочий день, а когда заканчивает.

Теперь напишем код декоратора, который будет по соответствующей команде отправлять запрос на открытие двери в СКУД

@dp.message_handler(commands=['openDoor1'])
async def cmd_start(message: types.Message) -> None:
   if message.chat.id not in users:
       await bot.send_message(chat_id=message.from_user.id, text='У вас нет доступа к этой двери. Обратитесь к администратору')
   else:
       # Импортируем модуль socket в рамках хендлера
       import socket
	  
       def send_tcp_message():
           message = f"LOGIN 1.8 {acslogin} {acspassword}\r\nALLOWPASS 9 5 UNKNOWN\r\n"
           try:
               # Создаем сокет
               s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
               # Подключаемся по указанному ip адресу и порту сервера СКУД
               s.connect((acsserver_ip, acsserver_port))
               # Отправляем сообщение, при этом кодируем его в битовое представление
               s.send(message.encode('utf-8'))
               # Закрываем сокет
               s.close()
           except Exception as e:
               # Обрабатываем ошибки
               return str(e)
       send_tcp_message()


       # Отправляем сообщение пользователю о том, что у него есть доступ к команде и она отправлена
       await message.answer('Команда отправлена')

 Откроем ПО Sigur и проверим, как работает наш код:

Telegram, диалог с ботом
Telegram, диалог с ботом
ПО Клиент (Sigur)
ПО Клиент (Sigur)
Получение оповещения в Telegram встроенными в ПО Sigur средствами

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

В рамках данной статьи я решил не использовать этот функционал и пошел простым путем - использовал нативный инструмент ПО Sigur, который позволяет получать оповещение по любым событиям в системе.

ПО Клиент (Sigur). Настройки реакции на событие
ПО Клиент (Sigur). Настройки реакции на событие
Оповещение в Telegram.
Оповещение в Telegram.

Делаем кнопку открытия двери

Открывать дверь вписывая команду не самый удобный способ. 

Библиотека aiogram позволяет создавать кнопки управления в чате с ботом.

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

from aiogram.types import ReplyKeyboardMarkup, KeyboardButton
from aiogram.dispatcher.filters import Text

Определим функцию, которая будет отвечать за состав и параметры кнопок. Создадим кнопку “Открыть дверь”, именно по нажатию на нее будет отправляться сообщение, которое бот будет интерпретировать как команду открытия двери. 

def get_keyboard() -> ReplyKeyboardMarkup:
   kb = ReplyKeyboardMarkup(resize_keyboard=True)
   kb.add(KeyboardButton('Открыть дверь'))
   return kb

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

Заменим параметр commands на text, текст команды 'openDoor1' на 'Открыть дверь' 

Осталось отобразить кнопку для пользователя. Кнопка будет появляться при начале работы с ботом после команды /start. Для этого к ответному сообщению в обработчике команды start укажем параметр reply_markup.

@dp.message_handler(commands=['start'])
async def cmd_start(message: types.Message) -> None:
  if message.chat.id not in users:
      await bot.send_message(chat_id=message.from_user.id, text='У вас нет доступа. Обратитесь к администратору')
  else:
      await message.answer('Добро пожаловать',
                           reply_markup=get_keyboard())

Запустим основной файл программы и проверим работу:

Telegram, диалог с ботом
Telegram, диалог с ботом

Развертывание Python приложения и хостинг

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

Хостинг

Большая часть статей на тему создания телеграм бота и его развертывания рекомендует воспользоваться онлайн хостингом. Такой вариант я сразу исключил по причине того, что СКУД это в первую очередь система безопасности и выбрасывать наружу интерфейс, по которому можно открыть дверь является не самым лучшим решением.

Docker

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

Что мне потребовалось для запуска решения:

  • виртуальная машина на OS Ubuntu Jammy 22.04.2 (LTS). У вас это может быть почти любой мини пк.

  • Docker (version 24.0.2)

  • много кофе и времени

Было больно, было и осталось многим непонятным, но я нашел некий рецепт для себя, чтобы запускать свои python программы в докере и далее делюсь уже конкретным сценарием, как это сделать.

Подготовка файлов программы для развертывания в docker

  1. файл requirements.txt

подготовить его можно сразу в pycharm. Для создания списка библиотек, их версий и сохранения всего этого в файл в терминале потребуется ввести команду

pip freeze > requirements.txt

  1. Dockerfile можно создать через пкм в файлах проекта, он так и называется New -> Dockerfile

# В качестве среды исполнения используем Python runtime
FROM python:3.9-slim
# Указываем рабочую директорию для контейнера
WORKDIR /app
# Копируем файл с библиотеками в контейнер
COPY requirements.txt .
# Устанавливаем зависимости для python
RUN pip install --no-cache-dir -r requirements.txt
RUN apt-get update
# Копируем все файлы программы в контейнер
COPY . .
# Запускаем основной файл программы при запуске контейнера
CMD ["python", "main.py"]
  1. Переносим все файлы проекта в директорию на виртуальную машину

Создание образа и запуск контейнера

  1. Открываем терминал и переходим в директорию проекта

    Terminal
    Terminal
  2. Создаем docker образ на основе наших файлов. Для этого в директории проекта вводим команду, где oifbotimage - наименование образа

docker build -t oifbotimage .

Начинается процесс сборки образа, и, если вы все сделали правильно или у вас здесь и сейчас есть шаманский бубен, все завершается успешно

Результат выполнения команды в terminal
Результат выполнения команды в terminal

Список созданных образов можно посмотреть с помощью командыdocker images

Результат выполнения команды в Terminal
Результат выполнения команды в Terminal

Осталось запустить контейнер на основе созданного образа. Для этого воспользуемся соответствующей командой. Вместе с наименованием образа указываем его тег. При запуске контейнера видим сообщение от модуля диспетчера. Проверяем бота - работает!

Результат в Terminal
Результат в Terminal
Результат в Terminal
Результат в Terminal

Котик и ссылка на программный код в Github

Этот котик сделан руками, а не скачан из интернета. Пусть останется здесь.
Этот котик сделан руками, а не скачан из интернета. Пусть останется здесь.

Github

Что дальше?

Здесь я хочу отметить список возможных тем для следующих публикаций сопряженных с этой темой:

  1. Как при разрешении прохода сделать снепшот с rtsp потока камеры видеонаблюдения средствами opencv;

  2. Добавляем в функционал бота открытия двери управление состоянием замков. Делаем бот безопаснее;

  3. Используем REST API для заявок посетителей в telegram web application.

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


  1. TheGreatDanton
    13.12.2023 17:12

    Огонь, давно такое нужно в махровых СКУДах! Еще бы неплохо с умными домами подружиться по типу home assistant/


  1. r_anisimov
    13.12.2023 17:12

    С этой функциональности у sigur я начинал писать универсальную систему родительского контроля для различных скуд. Забавно, но в итоге сама эта возможность перестала иметь практический смысл. Хотя впервой было удобно, плюс коллегам уведомления приходили о том, что я пришёл/ушёл


    1. iampopovdmitriy Автор
      13.12.2023 17:12

      А почему сейчас эта возможность потеряла практический смысл?


      1. r_anisimov
        13.12.2023 17:12

        Никому оказалась не нужна эта функция


  1. GarryPiter
    13.12.2023 17:12

    API от Sigura стоит как крыло от боинга, где вы его заимели ?


    1. r_anisimov
      13.12.2023 17:12

      Стандартное общение через tcp – базовая штука, документацию можно у них попросить. Если модуль уведомлений хотите – можно попросить у них пробный период. Мне на месяц давали.


  1. pr0l
    13.12.2023 17:12

    Те теперь умение использовать api производителя это достижение?

    Могу выложить кусок кода для работы с perco-web, так же открывает и закрывает нужные двери, а еще берет и присылает фотку с ip камеры перед открытием двери и после закрытия. На целую статью хватит) если еще расписывать как инлайн кнопки делать и обрабатывать.

    Автору - зачем городить огород для работы с камерой, если многие камеры умеют отдавать mjpeg простым запросом. Регистраторы тоже умеют.


    1. r_anisimov
      13.12.2023 17:12

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


    1. iampopovdmitriy Автор
      13.12.2023 17:12

      Спасибо за обратную связь. Не позиционирую данную статью и код, как достижение. Прекрасно осознаю, что настолько базовая фича реализуется любым разработчиком начального уровня.

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

      Хорошее замечание про камеры. При желании можно было бы и ONVIF поддержать, если подходить по-взрослому.

      По поводу кода для работы с perco-web, напишите, возможно кому-то из области окажется это полезным.


  1. sYB-Tyumen
    13.12.2023 17:12

    Немного сумбурно местами.

    • часть модулей не используется

    • в начале, похоже, пропущен шаг парсинга конфигурационного файла (во второй раз уже есть)

    • почему бы не сделать в сообщении об отсутствии доступа вывод ещё и id пользователя, чтобы он сразу мог сообщить его администратору? Чтобы пользователь помучился, узнавая свой id?

    • кажется, решение с упаковкой токенов и паролей в контейнер не очень хорошее. Может стоит мапить yaml-файл c хоста или передавать в параметрах запуска контейнера? Или шифровать обратимым шифрованием, а пароль для расшифровки передавать при запуске контейнера? Будет интересно в продолжении увидеть немного ИБ


    1. iampopovdmitriy Автор
      13.12.2023 17:12

      Спасибо за комментарий

      • парсинг конфиг файла вроде на месте, проверил;

      • про id юзера и правда стоит так сделать, спасибо. Думаю ответ вообще должен содержать не только id юзера, но и контакты условного админа, к которому стоит обратиться по этому вопросу;

      • про ИБ хорошее замечание, спасибо. Думал о том, как лучше это организовать и решил в первой итерации этого не делать. Буду изучать и скорее всего дополню решение в плане безопасности;