Por: Josue Soriano Publicado el: 2017-12-23 23:31:29, 2317
Hace poco mas de tres meses que publique como poder hacer un alimentador de mascotas utilizando Un Motor controlado por medio de una computadora Raspberry Pi y poco después publique un pequeño script de Python que permite crear chatbots pode medio de Telegram por lo que se me ocurrió unir estos dos elementos para poder hacer un script que en lugar de usar E-Mails pueda hacer una comunicación mas directa utilizando un Chatbot.
La idea es muy básica, los Chatbots en Telegram utilizan la programación multi-Hilo la cual, a diferencia de la programación lineal tradicional, el chatbot utiliza un ciclo infinito que corre en un hilo paralelo al ciclo infinito del programa principal; lo que hace especial este tipo de programación es que los hilos se pueden comunicar entre ellos para llamar funciones en común. La idea aquí es sustituir la función que revisa el correo electrónico por un hilo que revise comandos enviados al por medio del Telegram.
Los Comandos que utilizaremos serán básicamente los mismos que se utilizaban por medio de E-Mail, pero aprovechando las virtudes de un chatbot de Telegram agregue algunas funciones de monitoreo en tiempo real:
Comando | Acción realizada |
/alimentar | Inicia el proceso de alimentación siempre y cuando haya pasado el periodo de tiempo parametrizado. |
/cuando | Retorna información sobre cuando fue la ultima alimentación y una foto del plato de comida. |
/foto | Retorna una foto del plato de comida. |
/reiniciar | Reinicia el sistema, establece la variable de ultima alimentación a 0 para poder hacer una alimentación de inmediato. |
/estado | Informa sobre el estado de conexión a Internet: SSID del Wireless conectado y la dirección IP que tiene el sistema dentro de la red. |
Voy a reestructurar el Tutorial anterior para poder hacer un Alimentador de Mascotas Ahora Automatizado por medio de Telegram junto con toda la configuración necesaria a realizar en una Raspberry PI:
Como lo comente en el post anterior esta no es una Idea original Mia, lo encontre en Instructables asi que los creditos realmente son del autor de la misma.
Se necesita armar un pequeño circuito que activa un Motor de Alto torque, y como mis conocimientos de electronica son muy pero muy escasos me tuve que pasar un par de dias aprendiendo las cosas basicas, asi que les recomiendo que vean los canales de Youtube de TerrazoCultor y sobre todo vean los videos de electornica basica de Charly Labs
El Script de automatizacion original esta escrito en Python y utiliza conexion a GMail para consultar los comandos, nunca habia usado este lenguaje de programacion pero la verdad no es tan diferente de los demas, lo he modificado un poco para que se adapte mejor a las nuevas librerias de Python y el proceso de automatizacion junto con las configuraciones del sistema operativo permiten parametrizar lo siguiente:
Materiales Utilizados
Todos los materiales anteriores a excepcion de los ultimos dos no se pueden conseguir localmente (o por lo menos no sabia donde conseguirlos), sin embargo la varilla y el acoplador probablemente se encuntren en algun taller de soldadura pero al ser la primera vez que hago algo asi, no tenia idea de como debia ser para que se acoplara al motor asi que lo pedi a la pagina de los links descritos anteriomente; acontinuacion los materiales que pude comprar en tiendas locales en San Pedro Sula:
Alguinos materiales micelaneos que se pueden conseguir en ferreterias o posiblemente ya tengamos:
Armando la estructura
La idea principal es sustituir la manija dispensadora que viene en el ZEVRO por la varilla D que luego se conectara al Motor por medio del Acoplador. El dispensador se sujetara de la caja de madera y la caja de madera a la Pared. Como no se mucho de electrónica no utilicé ninguna placa de baquelita para montar el circuito así que use la protoboard para poder poner todos los componentes así que en el fondo de la caja va sujeta con tornillos la Rapberry Pi y la protoboard la cual ya tenía un adhesivo en la parte de atrás así que solo fue de pegarla. La caja debe de tener tres botones los cuales tendrán la función de Reiniciar el temporizador, activar el alimentador y el último será un paso directo para activar el motor sin pasar por el circuito. El motor se sujetara dentro de la caja por medio del braket por lo que, de la caja, solo sobresaldrá la varilla D que se conecta al dispensador, en la parte de abajo de la caja fije y ajuste la webcam para que en cada correo de consulta y confirmacion me envie una foto de como esta el plato, esto para no sobre alimentar en caso de que no se hayan terminado la ultima tanda de comida.
De la parte inferior del dispensador se colocara el tubo que bajara hasta el plato y en la base se colocara el codo de PVC, yo coloque un poco de Tape Industrial en la salida para minimizar la velocidad del flujo de comida e hice una base dispensadora para evitar que la comida salga disparada por todos lados. Para fijar el tubo a la pared utilice abrazaderas metálicas que fije a la pared con los tornillos para taco S8.
En la puerta de la caja sujete la placa de la pantalla LCD y utilice los pares trenzados del cable UTP para llevarlos directamente a la Raspberry, en el otro extremo de los cables empalme las puntas hembras de los jumpers para hacer más fácil la conexión en los puertos GPIO de la Raspberry. Este sería el diagrama de circuitos. Voy a tratar de explicarlo con lo poco lo que logre captar de Electrónica.
El motor se conecta directamente al polo positivo del transformador de 12 voltios pero para que la corriente fluya debe pasar por el circuito en el polo neutro del motor, para ello se utiliza el transistor N2222. Los transistores tienen normalmente 3 patitas las cuales corresponden a un colector, una base y un emisor, dependiendo del modelo del transistor la ubicación de estas patitas pueden variar; este transistor hace la función de Switch aquí es donde conectamos el polo neutro del motor al colector del transistor, el pin #19 de la rapberry se conecta a la base por medio de una resistencia de 270Ω y el emisor se conecta a la terminal neutra del transformador de 12V junto con uno de los polos Tierra de la raspberry; el transistor dejara fluir la corriente entre colector y emisor siempre y cuando la base se estimule con suficiente voltaje; mas adelante programaremos las rapberry para que según ciertas ordenes el puerto 19 emitirá 3.3 voltios, lo suficiente para que el circuito continúe y active el motor.
Para la pantalla LCD se utiliza otra parte de la protobard en un circuito diferente donde conecte el potenciómetro de 10KΩ el cual recula el contraste del texto que aparece en la pantalla LCD, así que si no aparece nada en la pantalla es porque probablemente el potenciómetro este totalmente cerrado; en mi caso yo lo deje totalmente abierto para que se visualice mejor el texto. Al final las conexiones en la protoboard quedarían de la siguiente manera.
Configuración de la Raspberry
Para configurar la rapberry utilice la versión Lite del Rapbian (no necesitamos la carcasa grafica) y aplique la configuración básica que muestra el raspbian una vez que se instala o utilizando el comando raspi-config: expandi el espacio al 100% y habilite el SSH pero sobre todo lo mas importante es cambiar la contraseña y el nombre de usuario al usuario pi que viene por defecto (De lo contrario, como me paso a mi, pueden sufrir un ataque por el puerto 22 si tienen una IP publica direccionada a la raspberry). Aparte de eso la configuracion para que se conecte automaticamente a mi red WiFi (Suponiendo que mi red se llama "BlogSoriano" y mi password es "$eguridad123!") generamos y guardamos la clave en el archivo de conexiones Wireless con el siguiente comando:
$ sudo wpa_passphrase "BlogSoriano" "$eguridad123!" | sudo tee -a /etc/wpa_supplicant/wpa_supplicant.conf > /dev/null
lo siguiente es actualizar e instalar el sistema de instalacion de python "pip" para ello vamos a loguearnos como root, actualizar e instalar los paquetes necesarios:
$ sudo -i
$ apt-get update
$ apt-get install build-essential python-dev python-smbus python-pip
Con esto ya tenemos acceso a la libreria de clases de python, para el script que vamos a usar se necesita instala lo siguiente:
$ pip install RPi.GPIO Adafruit-CharLCD httplib2 html2text netifaces wireless telepot cv2
Una vez terminada la instalacion de dependencias vamos a escribir el siguiente script, yo lo guarde en la ruta /opt/petfeeder.py el archivo lo pueden crear mediante el comando nano /opt/petfeeder.py y dentro de este archivo pegamos el siguiente codigo, aclaro que este codigo no lo escribi yo ya que nunca habia programado en Python, unicamene lo modifique para que funcione con las nuevas librerias y con la pantalla LCD de 20x4; tambien inclui una funcion para leer tweets random desde algunas cuentas que yo sigo desde una API que programe en PHP la cual compartire mas tarde:
# coding=utf8
import md5
import time
import os
import sys
import RPi.GPIO as GPIO
from Adafruit_CharLCD import Adafruit_CharLCD
import httplib2
import json
import html2text
import cv2
import netifaces as ni
from wireless import Wireless
import telepot
from telepot.loop import MessageLoop
MOTORON = True
SYSTEMRESET = False #Inicia el proceso de Reseteo para forzar la alimentacion
CHUCKNORRIS = False #activa para que adjunte un chiste de Chuck Norris en ingles en los correos
NUMBERTRIVIA = False #active para que mande dentro de los correos una trivia de un numero aleatorio.
#Sistema de seguridad, para evitar que cualquier persona active los comandos
SYSPASSWORD = "B7F93DGE2G5CB3C0F89A41A1A354F386" #password para poder enviar comandos al sistema Encriptado en MD5
CHATSFILE = os.getcwd() + "/authchats.txt" #Archivo donde se guardan los chats que se han autorizados a enviar comandos.
AUTHCHATS = [] #Arreglo que guarda los chats que se han autorizado
TRYAUTH = [] #Arreglo de los chats que estan intentando autizarse
# Archivos para guardar estados de Ultima Alimentacion
LOGFILE = os.getcwd() + "/petfeeder.log"
PHOTOFILE = '/tmp/foodstatus.jpg'
RECORDAR = True
# Variables Para conectar al Chatbot
BOTKEY = '123456789:cdufiwe23rqnlkajscdu5alnlkajsc'
bot = telepot.Bot(BOTKEY)
# GPIO pins para el control de alimentacion
MOTORCONTROLPIN = 19
FEEDBUTTONPIN = 6
RESETBUTTONPIN = 13
# GPIO pins para 20x4 HD44780
lcd_rs = 27
lcd_en = 22
lcd_d4 = 25
lcd_d5 = 24
lcd_d6 = 23
lcd_d7 = 18
lcd_bl = 4
lcd_cols = 20
lcd_rows = 4
#Variables para evaluacion de la red Inalambrica
CURRENTIP = "0.0.0.0"
CURRENTSSID = "None"
# Variables para la informacion de la alimentacion
feedInterval = 28800 # 8 Horas en segundos
FEEDFILE = os.getcwd() + "/lastfeed.log"
cupsToFeed = 1.2
motorTime = cupsToFeed * 27 # Toma 27 Segundos al motor para poder dar 1 Taza de comida.
# Function that gets Chuck Norris jokes from the internet. It uses an HTTP GET and then a JSON parser
def getChuckNorrisQuote():
# The database where the jokes are stored
ICNDB="http://api.icndb.com/jokes/random"
# Doing a HTTP request to get the response (resp) and content (content)
resp, content = httplib2.Http().request(ICNDB)
# The content is in the following JSON format and needs to be parsed
# {u'type': u'success', u'value' : {u'joke': 'Text of the joke', u'id': 238, u'categories': []}}
parsed_content = json.loads(content)
joke = "\n\n** Chiste Random de Chuck Norris **:\n" + html2text.html2text(parsed_content['value']['joke'])
return str(joke)
# Function that gets a number trivia from the internet. It uses an HTTP GET and then a JSON parser
def getNumberTrivia():
# The database where the trivia are stored
NUMDB="http://numbersapi.com/random/trivia?json"
# Doing a HTTP request to get the response (resp) and content (content)
resp, content = httplib2.Http().request(NUMDB)
# The content is in the following JSON format and needs to be parsed
# {u'text': u'Text of trivia', u'type' : u'trivia, u'number': , u'found': True}
parsed_content = json.loads(content)
trivia = "\n\n** Curiosidad del Numero " + str(parsed_content['number']) + " **\n"
trivia = trivia + parsed_content['text']
return str(trivia)
def handle(msg):
global SYSTEMRESET
content_type, chat_type, chat_id = telepot.glance(msg)
#print(content_type, chat_type, chat_id)
if content_type == 'text':
MENSAJE = msg['text']
UNICODE = MENSAJE.encode('unicode-escape').decode('ascii')
if str(chat_id) in AUTHCHATS:
if MENSAJE == "Cuando" or UNICODE == "\u2753":
msgBody = "La ultima alimentación se realizo el " + time.strftime("%b %d at %I:%M %P", time.localtime(lastFeed))
if (time.time() - lastFeed) > feedInterval:
msgBody = msgBody + "\nListo para alimentar ahorita!"
else:
msgBody = msgBody + "\nLa siguiente alimentación comenzara el " + time.strftime("%b %d at %I:%M %P", time.localtime(lastFeed + feedInterval))
if NUMBERTRIVIA:
msgBody = msgBody + getNumberTrivia()
if CHUCKNORRIS:
msgBody = msgBody + getChuckNorrisQuote()
getPhoto()
bot.sendPhoto(chat_id, open(PHOTOFILE, 'rb'))
bot.sendMessage(chat_id, msgBody)
elif MENSAJE == "Alimentar" or UNICODE == "\U0001f37d":
msgBody = "La ultima alimentación se realizo el " + time.strftime("%b %d at %I:%M %P", time.localtime(lastFeed))
proceder = False
if (time.time() - lastFeed) > feedInterval:
msgBody = msgBody + "\nLa alimentación comenzara en breves momentos"
proceder = True
else:
msgBody = msgBody + "\nLa próxima alimentación se realizara el " + time.strftime("%b %d at %I:%M %P", time.localtime(lastFeed + feedInterval))
if NUMBERTRIVIA:
msgBody = msgBody + getNumberTrivia()
if CHUCKNORRIS:
msgBody = msgBody + getChuckNorrisQuote()
bot.sendMessage(chat_id, msgBody)
if proceder:
feednow()
proceder=False
elif MENSAJE == "Foto" or UNICODE == "\U0001f4f7":
getPhoto()
bot.sendPhoto(chat_id, open(PHOTOFILE, 'rb'))
elif MENSAJE == "Estado" or UNICODE == "\U0001f4a1":
IPDIR = getIP()
SSID = getSSID()
msgBody = "Sistema Activo"
msgBody = msgBody + "\nWireless: " + SSID
msgBody = msgBody + "\nDirección IP Local: " + IPDIR
bot.sendMessage(chat_id, msgBody)
elif MENSAJE == "Reiniciar" or UNICODE == "\U0001f504":
SYSTEMRESET = True
else:
bot.sendMessage(chat_id, "Comando no definido")
else:
if chat_id in TRYAUTH:
TRYAUTH.remove(chat_id)
m = md5.new()
m.update(MENSAJE)
CRYPTED = m.hexdigest().upper()
if CRYPTED == SYSPASSWORD:
with open(CHATSFILE, "a") as myfile:
myfile.write(str(chat_id)+"\n")
myfile.close()
AUTHCHATS.append(str(chat_id))
bot.sendMessage(chat_id, "Autorización correcta, proceda a enviar comandos")
else:
bot.sendMessage(chat_id, "Contraseña Incorrecta")
else:
if MENSAJE == "Autorizar":
bot.sendMessage(chat_id, "Envié la contraseña de autorización")
TRYAUTH.append(chat_id)
else:
msgbody = "Este Chat no esta autorizado para poder enviar comandos"
msgbody = msgbody + "\npara autorizar envié Autorizar y siga las instrucciones"
bot.sendMessage(chat_id, msgbody)
def buttonpressed(PIN):
# Check if the button is pressed
global GPIO
# Cheap (sleep) way of controlling bounces / rapid presses
time.sleep(0.2)
button_state = GPIO.input(PIN)
if button_state == False:
return True
else:
return False
def printlcd(row, col, LCDmesg):
# Set the row and column for the LCD and print the message
global logFile
global lcd
lcd.set_cursor(row, col)
lcd.message(LCDmesg)
lcd.set_cursor(0,2)
lcd.message("Wifi: " + CURRENTSSID)
lcd.set_cursor(0,3)
lcd.message("IP: " + CURRENTIP)
def feednow():
# Run the motor for motorTime, messages in the LCD during the feeeding
global GPIO
global MOTORCONTROLPIN
global motorTime
global lastFeed
global AUTHCHATS
global bot
lcd.clear()
printlcd(0,0,"Alimentando.....")
#print "Alimentando...."
if MOTORON:
GPIO.output(MOTORCONTROLPIN, True)
time.sleep(motorTime)
GPIO.output(MOTORCONTROLPIN, False)
lastFeed = time.time()
saveLastFeed()
printlcd(0,1, "Terminado!")
msgBody = "Alimentación terminada Exitosamente!"
getPhoto()
for CHAT in AUTHCHATS:
bot.sendPhoto(CHAT, open(PHOTOFILE, 'rb'), msgBody)
time.sleep(2)
return time.time()
def saveLastFeed():
global FEEDFILE
global lastFeed
with open(FEEDFILE, 'w') as feedFile:
feedFile.write(str(lastFeed))
feedFile.close()
def getPhoto():
global PHOTOFILE
camera_port = 0
camera = cv2.VideoCapture(camera_port)
time.sleep(0.1)
return_value, image = camera.read()
cv2.imwrite(PHOTOFILE, image)
del(camera)
def getIP():
global ni
try:
ni.ifaddresses('wlan0')
ip = ni.ifaddresses('wlan0')[ni.AF_INET][0]['addr']
except:
ip = "0.0.0.0"
return str(ip)
def getSSID():
try:
wireless = Wireless()
ssid = wireless.current()
except:
ssid = "Ninguno"
return str(ssid)
# This is is the main program, essentially runs in a continuous loop looking for button press
try:
#print "Iniciando Servicio de Alimentacion"
#### Begin initializations #########################
####################################################
# Initialize the logfile
logFile = open(LOGFILE, 'a')
# Initialize the LCD
lcd = Adafruit_CharLCD(lcd_rs, lcd_en, lcd_d4, lcd_d5, lcd_d6, lcd_d7, lcd_cols, lcd_rows, lcd_bl)
# lcd.begin(16,2)
lcd.clear()
lcd.show_cursor(False)
# Initialize the GPIO system
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
# Initialize the pin for the motor control
GPIO.setup(MOTORCONTROLPIN, GPIO.OUT)
GPIO.output(MOTORCONTROLPIN, False)
# Initialize the pin for the feed and reset buttons
GPIO.setup(FEEDBUTTONPIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(RESETBUTTONPIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
#obtenemos los datos de conexion a la red inalambrica
CURRENTIP = getIP()
CURRENTSSID = getSSID()
# Initialize lastFeed
if os.path.isfile(FEEDFILE):
with open(FEEDFILE) as feedFile:
lastFeed = float(feedFile.read())
feedFile.close()
else:
lastFeed = time.time()
saveLastFeed()
# Inicializando Chats Autorizados
if os.path.isfile(CHATSFILE):
with open(CHATSFILE) as f:
AUTHCHATS = f.read().splitlines()
f.close()
#### End of initializations ########################
####################################################
#Inicia Hilo escucha del chatbot
MessageLoop(bot, handle).run_as_thread()
#### The main loop ####
while True:
try:
if CURRENTIP != getIP() or CURRENTIP == "0.0.0.0":
CURRENTIP = getIP()
CURRETSSID = getSSID()
if CURRENTIP == "0.0.0.0":
#Reactivamos la interfaz de red
os.system("ifup wlan0")
os.system("wpa_cli reconfigure")
time.sleep(5)
CURRENTIP = getIP()
CURRENTSSID = getSSID()
lcd.clear()
#### If reset button pressed, then reset the counter
if buttonpressed(RESETBUTTONPIN) or SYSTEMRESET:
SYSTEMRESET = False
lcd.clear()
printlcd(0,0, "Reiniciando... ")
time.sleep(2)
lastFeed = time.time() - feedInterval + 5
saveLastFeed()
for CHAT in AUTHCHATS:
bot.sendMessage(CHAT, "El sistema se reinicio Correctamente a las " + time.strftime("%m/%d %I:%M:%S%P", time.localtime(time.time())))
#### Check if we are ready to feed
if (time.time() - lastFeed) > feedInterval:
printlcd(0,0, time.strftime("%m/%d %I:%M:%S%P", time.localtime(time.time())))
printlcd(0,1, "Listo para Alimentar")
if RECORDAR:
RECORDAR = False
for CHAT in AUTHCHATS:
getPhoto()
bot.sendPhoto(CHAT, open(PHOTOFILE, 'rb'), "La ultima alimentación se realizo a las " + time.strftime("%m/%d %I:%M:%S%P", time.localtime(lastFeed)) + ", la alimentación ya esta activa.")
if buttonpressed(FEEDBUTTONPIN):
feednow()
#### Since it is not time to feed yet, keep the countdown going
else:
RECORDAR = True
timeToFeed = (lastFeed + feedInterval) - time.time()
printlcd(0,0, time.strftime("%m/%d %I:%M:%S%P", time.localtime(time.time())))
printlcd(0,1, 'Proxima: ' + time.strftime("%Hh %Mm %Ss", time.gmtime(timeToFeed)))
if buttonpressed(FEEDBUTTONPIN):
lcd.clear()
printlcd(0,0, "Ahora no, intenta el")
printlcd(0,1, time.strftime("%b/%d %H:%M", time.localtime(lastFeed + feedInterval)))
time.sleep(2)
time.sleep(.6)
except Exception as e:
logError = time.strftime("%m/%d %I:%M:%S%P", time.localtime(time.time())) + " Error inesperado: " + str(e) + "\n"
for CHAT in AUTHCHATS:
bot.sendMessage(CHAT, logError)
logFile.write(logError)
#### Cleaning up at the end
except KeyboardInterrupt:
logFile.close()
lcd.clear()
GPIO.cleanup()
except SystemExit:
logFile.close()
lcd.clear()
GPIO.cleanup()
Una vez que tengamos el Script hay que cambiar los valores de las variables con la llave para el bot; para ello es necesario poder crear un nuevo Bot de Telegram, con eso solo quedaria guardar el archivo y salir (con Ctrl+o se guarda y ctrl+x se sale del editor nano), solo nos hace falta que sea a prueba de reinicios; el script por si ya guarda la ultima vez que se activo el sistema de alimentacion, por lo que solo debemos hacer que el script se ejecute cada vez que se inicie el sistema operativo, para ello encontre una solucion muy particular con un programa llamado supervisor, el cual se instala desde los repositorios del Debian:
$ apt-get install supervisor
y una vez instalado solamente necesitamos crear un archivo de configuracion en /etc/supervisor/conf.d/petfeeder.conf, al igual que el caso anterior lo podemos crear y guardar con nano, este archivo tendra lo siguiente:
[program:petfeederd]
directory=/opt
command=python petfeeder.py
autostart=true
autorestart=true
Una vez guardado el archivo podemos usar el comando supervisorctl [start|stop|restart] petfeederd, como en este caso el servicio no se ha iniciado lo ejecutamos con start:
$ supervisorctl start petfeederd
Y con esto ya deberia funcionar el sistema, la pantalla LCD deberia mostrar informacion de cuando deberia ser la siguiente alimentacion o si ya esta listo para alimentar deberia decirlo. Tambien debe mostrar el nombre de la Red inalambrica a la que se esta conectado y la direccion IP que se esta utilizando por si necesitamos conectarnos por medio de SSH, comparto un pequeño video que hice, me disculpo por la calidad del mismo, no soy muy bueno es este tema de los vlogs.