Как сделать бота в Telegram, который открывает дверь с электромагнитным замком, используя Go lang, Raspberry Pi и паяльник +14



image

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

Принцип работы очень прост:

  • Человек отправляет сообщение боту в Telegram
  • Rasberry Pi получает новые сообщение через Telegram API
  • RPi открывает входную дверь

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

  • n-p-n транзистор кт315 (другого не было)
  • Резистор 10КОм
  • Пару проводов

Схема выглядит примерно так:

Схема ключа

Как собиралось

Как собиралось

Потом надо было написать код:

Код
package main
import (
  // Системные пакеты
  "log"
  "io/ioutil"
  "path/filepath"
  "strings"
  "time"

  // Парсер yaml файлов
  "gopkg.in/yaml.v2"

  // Библиетка для работы с rpi
  "github.com/stianeikeland/go-rpio"

  // Библитека для работы с telegram API
  "github.com/Syfaro/telegram-bot-api"
)

type Config struct {
  // Токен телеграм бота
  Token             string `yaml:"token"`
  // Разрешенные айдишники чатов
  AllowedChatIds    []int `yaml:"allowed_chat_ids"`
  // Ключевые слова для открывания двери
  OpenDoorPhrases   []string `yaml:"open_door_phrases"`
  TurnLedOnPhrases  []string `yaml:"turn_led_on_phrases"`
  TurnLedOffPhrases []string `yaml:"turn_led_off_phrases"`
}

var bot *tgbotapi.BotAPI
var config *Config
var OpenDoorPhrases []string
var TurnLedOnPhrases []string
var TurnLedOffPhrases []string
var AllowedChatIds []int
var doorOpened chan *tgbotapi.Message
var ledTurnedOn chan *tgbotapi.Message
var ledTurnedOff chan *tgbotapi.Message
var doorPin = rpio.Pin(10)
var ledPin = rpio.Pin(9)

func readConfig() (*Config, error) {
  var yamlFile []byte
  var err error
  filename, _ := filepath.Abs("./config.yml")
  yamlFile, err = ioutil.ReadFile(filename)
  if err != nil {
    return nil, err
  }
  var conf Config
  if err := yaml.Unmarshal(yamlFile, &conf); err != nil {
    return nil, err
  }
  return &conf, err
}

func main() {
  var err error
  // Читаем конфиг
  if config, err = readConfig(); err != nil {
    panic(err)
  }

  // Инициализируем бота
  bot, err = tgbotapi.NewBotAPI(config.Token)
  if err != nil {
    log.Panic(err)
  }

  // Для работы с gpio в rpi
  if err = rpio.Open(); err != nil {
    log.Panic(err)
  }
  defer rpio.Close()
  // Устанавливаем пины на output
  ledPin.Output()
  doorPin.Output()

  // Инициализируем все остальные переменные 
  doorOpened = make(chan *tgbotapi.Message)
  ledTurnedOn = make(chan *tgbotapi.Message)
  ledTurnedOff = make(chan *tgbotapi.Message)
  AllowedChatIds = config.AllowedChatIds
  OpenDoorPhrases = config.OpenDoorPhrases
  TurnLedOnPhrases = config.TurnLedOnPhrases
  TurnLedOffPhrases = config.TurnLedOffPhrases
  log.Printf("Authorized on account %s", bot.Self.UserName)

  var ucfg tgbotapi.UpdateConfig = tgbotapi.NewUpdate(0)
  ucfg.Timeout = 60
  err = bot.UpdatesChan(ucfg)

  // Слушаем события
  go Listen()
  ListenUpdates()
}

func OpenDoor() chan<- *tgbotapi.Message {
  // Открываем дверь
  go launchDoor()
  return (chan<-*tgbotapi.Message)(doorOpened)
} 

func TurnLedOn() chan<- *tgbotapi.Message {
  // Включаем светодиод
  ledPin.High()
  return (chan<-*tgbotapi.Message)(ledTurnedOn)
}

func TurnLedOff() chan<- *tgbotapi.Message {
  // Выключаем светодиод
  ledPin.Low()
  return (chan<-*tgbotapi.Message)(ledTurnedOff)
}

// Открывание двери
func launchDoor() {
  log.Println("door is beeing opened")
  doorPin.High()
  ledPin.High()
  time.Sleep(100*time.Millisecond)
  doorPin.Low()
  ledPin.Low()
}

// Проверяет, является ли указанное сообщение ключевым
func tryToDo(text string, phrases []string) bool {
  for i:=0; i<len(phrases); i++ {
    if strings.ToLower(text) == phrases[i] {
      return true
    }
  }
  return false
}

// Проверяет, является ли указанный чат разрешенным
func auth(chatId int) bool {
  for i:=0; i<len(AllowedChatIds); i++ {
    if chatId == AllowedChatIds[i] {
      return true
    }
  }
  return false
}

// Отправка сообщения
func send(chatId int, msg string) {
  log.Println(msg)
  bot_msg := tgbotapi.NewMessage(chatId, msg)
  bot.SendMessage(bot_msg)
}


func Listen() {
  for {
    select {
      case msg := <- doorOpened:
        reply := msg.From.FirstName + " открыл(а) дверь"
        send(msg.Chat.ID, reply)
      case msg := <- ledTurnedOn:
        reply := msg.From.FirstName + " включил(а) светодиод"
        send(msg.Chat.ID, reply)
      case msg := <- ledTurnedOff:
        reply := msg.From.FirstName + " выключил(а) светодиод"
        send(msg.Chat.ID, reply)
    }
  }
}

func ListenUpdates() {
  for {
    select {
    case update := <-bot.Updates:
      userName := update.Message.From.UserName
      chatID := update.Message.Chat.ID
      text := update.Message.Text
      // Проверяем является ли этот чат разрешенным
      if !auth(chatID) {
        reply := "Вам нельзя это делать"
        log.Println(reply)
        bot_msg := tgbotapi.NewMessage(chatID, reply)
        bot.SendMessage(bot_msg)
        continue
      }

      log.Printf("[%s] %d %s", userName, chatID, text)
      // По очереди пытаемся открыть дверь, включить/выключить светодиод
      if tryToDo(text, OpenDoorPhrases) {
        OpenDoor() <- &update.Message
      }
      if tryToDo(text, TurnLedOnPhrases) {
        TurnLedOn() <- &update.Message
      }
      if tryToDo(text, TurnLedOffPhrases) {
        TurnLedOff() <- &update.Message
      }
    }
  }
}


Что касается config.yml; Это просто конфиг:

token: <secret token>
allowed_chat_ids: 
  - <id1>
  - <id2>
open_door_phrases:
  - open
  - open the door
  - open door
  - door open
  - дверь откройся
  - открыть дверь
  - открыть
  - открыть
  - откройся
  - открой
  - сим-сим, откройся
  - abrete sesamo
  - -o
  - o
  - отк
  - /open
turn_led_on_phrases:
  - led on
  - test on
turn_led_off_phrases:
  - led off
  - test off

Оставалось только загрузить скомпилированный файл на RPi и подключить все провода.

Так выглядит готовый вариант устройства
Так выглядит готовый вариант устройства

Кнопка, к которой я подключил систему управления
Кнопка, к которой я подключил систему управления

Сама дверь
Сам замок

Чат с ботом

Вы можете помочь и перевести немного средств на развитие сайта



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

  1. ibrin
    /#8843586

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

  2. mcleod095
    /#8843622

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

    • kaikash
      /#8843798

      Нет, там все это асинхронно запускается(разные горутины), получится, что дверь будет открыта на несколько секунд дольше
      К тому же, я не планирую малину оставлять там только для работы с дверью; в планах есть еще несколько интересных проектов, так что малина еще пригодится :)

      • mcleod095
        /#8843808

        То что в горутине я вижу.
        Но устройство то одно.
        И вот у нас с разницей в 100 мс приходят два запроса на открытие, первый открывает дверь и ждет пока тикает время, тут приходит второй и ставит выход на ноге малинки в отрытое состояние, через время первый меняет состояние порта на малинке и замок переходит в режим закрытия. Второй запрос еще ждет своего времени на закрытие. Да получается что дверь не останется открыта на неопределенное время, но вот то что мы начинаем менять состояние на GPIO просто потому что есть еще один запрос не совсем нормально. Лучше сделать одну горутину в которую будут приходит запросы и она уже будет смотреть состояние замка, если он открыт то незачем дергать GPIO еще раз, лучше просто дальше тикать.

        • kaikash
          /#8843832

          Не стоит забывать то, что в офисе работает ±40 человек, и 100 запросов одновременно никто делать не будет. А если кто-то решит злоупотреблять, его просто кикнут из беседы telegram, и все :)

  3. IvanT
    /#8843648 / +2

    Статья интересная, но вот решение странное. Достать брелок, приложить, положить в карман, войти — неудобно; достать телефон, разблокировать, найти иконку телеграма, запустить, найти чат замка, открыть, найти поле ввода, нажать, набрать open, отправить, заблокировать телефон, положить в карман — звучит куда удобнее :) Особенно удобно наверное открывать зимой когда руки в перчатках, а также тогда когда руки чем-то заняты. А если заняты зимой и в перчатках… :)

    • ibrin
      /#8843686

      А вдруг там налево-направо-по коридору прямо-вниз по лестнице-снова по коридору и дверь? И тут домофон звонит. -Wassuuuup! -Aaaaaaa -Открывай, чувак! И он такой не вставая хлабысь команду в чат и все открыл!

    • alexpp
      /#8843772

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

      • IvanT
        /#8843780

        Лучший интерфейс это отсутствие интерфейса. К примеру мы разработали и используем подобную систему для входа / выхода в нашей компании (правда она не только за это отвечает, а имеет и множество других полезных функций) и для открывания двери у нас в основном используются именно брелки, но дополнительно есть возможность войти по отпечатку пальца. Сейчас же тестируем технологию, как во многих авто, просто открыть дверь с ключом в кармане. Если ключ есть — дверь откроется и запомнит кто вошел, если нет — не откроется. Вот это и есть идеальный вариант. Подошел к двери, потянул за ручку, вошел. Всё.

  4. zorgrhrd
    /#8843728 / +1

    Не судите строго, это моя первая статья!

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

    • egor_masalitin
      /#8843902

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