Bedienung der Rollladensteuerung über einen Telegram-Bot mit Raspberry Pi


Vorwort

Ziel dieses Teilprojektes war es, meine Rollladensteuerung über ein Smartphone bedienen zu können. Die Entwicklung einer eigenen Smartphone-APP sowie der dazugehörigen API für den Steuercomputer erschien für den gewünschten Funktionsumfang zu aufwendig. So kam mir die Idee, einen bekannten Messenger-Dienst als Smartfone-App zu nutzen. Der Steuercomputer braucht so, nur die gesendeten Nachrichten auszuwerten und in die Befehle für die Rollladensteuerung umzusetzen. Der Messenger "Telegram" ist für diese Aufgabe gerade zu prädestiniert. Die Entwickler erlauben explizit eine "nicht menschliche" Nutzung.


Vorbereitungen auf dem Smartphone

Zuerst muss natürlich die Messenger-App installiert werden. Dazu einfach im jeweiligen App-Store nach "Telegram" suchen und die App installieren. Danach muss nur noch der Bot erzeugt werden. Lustigerweise geschieht dies über einen anderen (von Telegram betriebenen) Bot.
Nach dem Starten der Telegram-App sind folgende Schritte notwendig:

Suchfunktion öffnen

Schritt 1
1. "BotFather" suchen
2. und hinzufügen
Schritt 2
BotFather neu starten

Schritt 3


mit dem Befehl "/newbot"
einen eigenen Bot erzeugen
Schritt 4
Bot-Namen vergeben

Schritt 5
User-Namen vergeben

Schritt 6


1. Token merken
2. Bot hinzufügen
Schritt 7
Bot öffnen

Schritt 8
Bot starten

Schritt 9

Ab jetzt ist der eigene Bot im Prinzip betriebsbereit. Wichtig hierbei ist, das vergebene Token (Bild7). Dieses wird später im Script des Raspberrys benötigt. Sollte das Token einmal vergessen worden sein - keine Panik. Man kann es sich über den "Botfather" immer wieder anzeigen lassen. Dazu einfach den Befehl "/mybots" an den Botfather senden. Nach der Auswahl des eigenen Bots ist das Token abrufbar. In diesem Menü sind auch diverse andere Einstellungen für den eigenen Bot möglich.
z.B.
  • Profilbild hochladen
  • Bot-Beschreibung eingeben
  • Bot-Namen ändern
  • diverse Privacy Einstellungen
  • usw.


  • Vorbereitung der Hardware

    Hier kommt ein "Raspberry Pi 3 Model B" zum Einsatz. Aber auch die älteren Modelle sollten hier noch gut funktionieren.
    An den Raspberry ist folgende Peripherie angeschlossen:
    • Spannungsversorgung an Pin2(+5V) und Pin3(GND)
    • LED über Vorwiderstand an Pin5 der 40-poligen Stiftleiste - (GPIO9)
    • Taster über Schutzwiderstand gegen Masse sowie Pullupwiderstand an Pin7 - (GPIO7)
    • TTL UART zu RS232-Adapter an Pin8(TX) und Pin10(RX) - (GPIO15 und GPIO16)
    Peripherie

    Erfolgt die Spannungsversorgung direkt über die 40-polige Stiftleiste, wird dabei eine auf der Raspberry-Plaine aufgelötete Schmelzsicherung überbrückt. Wer das mit seinem Gewissen nicht vereinbaren kann, sollte eine externe Sicherung in die Verbindung zu Pin2 der Stiftleiste vorsehen. Ich sah dazu jedoch keine Veranlassung. Der TTL UART zu RS232 Adapter ist identisch mit dem in der Rollladensteuerung beschriebenen Adapter. D.h. aber auch, dass für die Verbindung zwischen Raspberry Pi und der Rollladensteuerung ein Nullmodemkabel benötigt wird.

    fertige Hardware


    Vorbereitung des Raspberry Pi

    Der Raspberry Pi wird in der Regel ohne SD-Karte ausgeliefert. D.h. Das Betriebssystem muss selbst installiert werden. Die folgende Anleitung beschreibt die Installation und Konfiguration des Betriebssystems. Die Anleitung ist für das Betriebssystem Rasbian in der Version "Stretch Lite" vom 07.09.2017 geschrieben (aktuelle Version zum Zeitpunkt der Anleitungserstellung).
  • Image des Betriebssystems aus dem Internet herunterladen (die lite Version - also ohne graphische Oberfläche)
  • Dieses Image z.B. mit "win32 disk imager" auf die SD-Karte speichern.
  • Programm win32 Disk Imager
  • So vorbereitete SD-Karte in den Raspberry stecken.

  • Damit ist der Raspberry im Prinzip schon betriebsbereit. Für die weiteren Schritte sind ein Monitor, eine Tastatur und eine Internetverbindung notwendig. Nach dem ersten Login (user: "pi" und password: "raspberry" (ggf. "raspberrz" wegen per default englischer Tastatur)) sollten ein paar Grundeinstellungen am Raspberry vorgenommen werden.

    Dazu folgenden Befehl in der Komandozeile ausführen:
    sudo raspi-config
    Es öffnet sich ein Tool zum Konfigurieren des Raspberrys in dem ich folgende Einstellungen getätigt habe:
    - update this tool to the latest version (Tool startet nach einer Weile von selbst neu.)
    - Localisation Options -> Change Keyboard Layout -> Generic 105-key (Intl) PC -> Other -> German -> German -> Right Alt (AltGr) -> No compose key
    - Change User Password
    - Network Options -> Hostname
    - Localisation Options -> Change Locale -> de_DE.UTF-8 UTF-8
    - Localisation Options -> Change Timezone -> Europe -> Berlin
    - Localisation Options -> Change Wifi Country -> DE Germany
    - Interfacing Options -> Camera -> enabled? -> No
    - Interfacing Options -> SSH -> enabled? -> Yes
    - Interfacing Options -> VNC -> enabled? -> No
    - Interfacing Options -> SPI -> enabled? -> No
    - Interfacing Options -> I2C -> enabled? -> No
    - Interfacing Options -> Serial -> shell? -> No -> hardware enabled? -> Yes
    - Interfacing Options -> 1-Wire -> enabled? -> No
    - Interfacing Options -> Remote GPIO -> over network? -> No
    - Advanced Options -> Expand Filesystem
    - Finish


    Sollte der Raspberry nach dem Beenden des Tools nicht schon von selbst neu starten, folgenden Befehl ausführen:
    sudo reboot


    Nach dem Neustart wird der Raspberry mit folgenden Befehlen auf den neuesten Stand gebracht:
    sudo apt-get update
    sudo apt-get -d upgrade
    sudo apt-get -y upgrade


    Nochmals neu starten mit:
    sudo reboot


    Firmware updaten und danach neu starten mit:
    sudo rpi-update
    sudo reboot



    nur beim (ab?) Raspberry Pi 3

    Der Raspberry Pi 3 Model B verfügt über ein Bluetooth-Modul, welches an die Hardware-UART-Schnittstelle (ttyAMA0) des Prozessors angeschlossen ist. (Bei den älteren Modellen war die Hardware-UART-Schnittstelle direkt mit der Stiftleiste verbunden.) Standardmäßig ist beim Raspberry 3 Model B jetzt eine Software-UART-Schnittstelle (ttyS0) mit der Stiftleiste verbunden. Die Baudraten werden damit nicht befriedigend eingehalten. Für dieses Problem gibt es mehrere im Netz beschriebene Methoden, ich habe mich für folgende entschieden:
    Dienst für das Bluetooth-Modul deaktivieren mit:
    sudo systemctl disable hciuart.service


    Die Datei "config.txt" mit einem Editor öffnen z.B.:
    sudo nano /boot/config.txt
    In der letzten Zeile den Text "dtoverlay=pi3-disable-bt"ergänzen.
    Mit [STRG]+[X] den Editor beenden und die Änderungen mit [j] [ENTER]speichern.
    Dabei wird die ttyAMA0 automatisch wieder auf die Stiftleiste gemappt.



    Ab jetzt ist der Raspberry auf dem neuesten Stand und die Hardware-UART-Schnittstelle (ttyAMA0) für unsere Bedürfnisse angepasst. Nacheinander diverse, für die Scripte benötigte, Pakete installieren (die letzten beiden sollten schon drauf sein, aber eine Kontrolle ist besser):
    sudo apt-get install python-pip
    sudo pip install python-telegram-bot
    sudo pip install pyserial
    sudo apt-get install wiringpi
    sudo apt-get install python-dev
    sudo apt-get install python-rpi.gpio


    Jetzt kommen die Python-Scripte ins Spiel. Als erstes wird das Script für die LED und den Taster beschrieben. Die LED dient zur Betriebsanzeige und der Taster zum definierten Herunterfahren des Raspberrys. Wird der Taster länger als 3 Sekunden betätigt, führt dies zum Shutdown. Im Normalbetrieb blitzt die LED alle 5 Sekunden kurz auf. Dazu eine Datei "shutdown.py" erzeugen, diese ausführbar machen und anschließend im Editor öffnen:
    mkdir /home/pi/shutdown
    touch /home/pi/shutdown/shutdown.py
    chmod +x /home/pi/shutdown/shutdown.py
    nano /home/pi/shutdown/shutdown.py
    Folgenden Inhalt in die Datei schreiben:
    #!/usr/bin/python
    # -*- coding: utf-8 -*-
     
    import time
    import RPi.GPIO as GPIO
    import os
    import sys
     
    GPIO.setmode(GPIO.BOARD)			#Pinheadernummern als Referenz benutzen
    GPIO.setup(7, GPIO.IN)				#PIN7 als Eingang dekalrieren (Taster gegen Masse mit 10k Pullup gegen 3,3V und 330R Widerstand in Serie)
    GPIO.setup(5, GPIO.OUT, initial=GPIO.LOW)	#PIN5 als Ausgang deklarieren (LED über 330R an Masse und LED abschalten)
     
    def poweroff(channel):
    	time.sleep(0.1)
    	GPIO.output(5, GPIO.HIGH)
    	time.sleep(3)
    	GPIO.output(5, GPIO.LOW)
    	if GPIO.input(7) == GPIO.LOW:
    		for i in range(19):
    			GPIO.output(5, GPIO.HIGH)
    			time.sleep(0.1)
    			GPIO.output(5, GPIO.LOW)
    			time.sleep(0.1)
     
    		os.system("wall das System wird herunter gefahren")
    		os.system("sudo shutdown -h now")
     
    GPIO.add_event_detect(7, GPIO.FALLING, callback = poweroff, bouncetime = 50)
     
    try:
    	while True:
    		time.sleep(5)
    		GPIO.output(5, GPIO.HIGH)
    		time.sleep(0.05)
    		GPIO.output(5, GPIO.LOW)
     
    except KeyboardInterrupt:
    	GPIO.cleanup()
    	sys.stdout.write("\nProgramm wurde manuell beendet!")
    Mit [STRG]+[X] den Editor beenden und die Änderungen mit [j] [ENTER]speichern.


    Für Das Script des Telegram-Bots ist wieder eine Datei zu erzeugen, ausführbar zu machen und im Editor zu öffnen:
    mkdir /home/pi/rollladensteuerung
    touch /home/pi/rollladensteuerung/telegram_bot.py
    chmod +x /home/pi/rollladensteuerung/telegram_bot.py
    nano /home/pi/rollladensteuerung/telegram_bot.py
    Folgenden Inhalt in die Datei schreiben:
    (Die richtigen IDs der berechtigten Nutzer werden später ergänzt)
    #!/usr/bin/python
    # -*- coding: utf-8 -*-
     
    from telegram import (ReplyKeyboardMarkup, ReplyKeyboardRemove, ParseMode)
    from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters, RegexHandler, ConversationHandler)
    import time
    import serial
    import sys
     
    try:
    	my_tty = serial.Serial(port='/dev/ttyAMA0', baudrate = 9600, parity =serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=0.1)
    	sys.stdout.write(my_tty.portstr + " geöffnet\n\n")
    	my_tty.close()
    	my_tty.open()
    	my_tty.reset_input_buffer()
    	my_tty.reset_output_buffer()
     
    except Exception, e:
    	sys.stdout.write("serieller Port konnte nicht geöffnet werden:\n" + str(e) + "\n\n")
    	exit()
     
    FENSTERWAHL, POSITION = range(2)
     
    Soll_Fenster = ''
     
    LIST_OF_ADMINS = [153535527, 440173254]
     
    keyboard1 =	[['Wohnen', 'Erker', 'Terrasse'],
    		 ['Lars', 'Paula', 'Schlafen'],
    		 ['Kochen','Dusche', 'Bad'],
    		 ['NORD', 'OST', 'SUED', 'WEST', 'ALLE'],
    		 ['Positionsabfrage', 'Abbrechen']
    		]
    markup1 = ReplyKeyboardMarkup(keyboard1)
     
    keyboard2 =	[['100%', '90%', '80%'],
    		 ['70%', '60%', '50%'],
    		 ['40%', '30%', '20%'],
    		 ['10%', '0%', 'Abbrechen']
    		]
    markup2 = ReplyKeyboardMarkup(keyboard2)			
     
    def start(bot, update):
    	try:
    		user_id = update.message.from_user.id
    	except (NameError, AttributeError):
    		try:
    			user_id = update.inline_query.from_user.id
    		except (NameError, AttributeError):
    			try:
    				user_id = update.chosen_inline_result.from_user.id
    			except (NameError, AttributeError):
    				try:
    					user_id = update.callback_query.from_user.id
    				except (NameError, AttributeError):
    					return ConversationHandler.END
    	if user_id not in LIST_OF_ADMINS:
    		update.message.reply_text('Hello %s %s.This is a private Bot.Your ChatID: "%s" has been blocked.' % (update.message.from_user.first_name, update.message.from_user.last_name, update.message.chat_id))
    		return ConversationHandler.END
    	else:
    		update.message.reply_text('Fensterauswahl:', reply_markup=markup1)
    		return FENSTERWAHL
     
     
    def fensterauswahl(bot, update):
    	global Soll_Fenster	
    	Soll_Fenster = update.message.text
    	update.message.reply_text('Position:', reply_markup=markup2)
    	return POSITION
     
     
    def position(bot, update):
    	Soll_Position = update.message.text	
    	sys.stdout.write("*** \"%s\" soll auf %s gefahren werden" % ((Soll_Fenster), Soll_Position))
    	if Soll_Fenster == 'Wohnen':
    		my_tty.write(('M8,' + Soll_Position[:-1]).encode() + '\n')
    	elif Soll_Fenster == 'Erker':
    		my_tty.write(('M9,' + Soll_Position[:-1]).encode() + '\n')
    		time.sleep(0.2)
    		my_tty.write(('M10,' + Soll_Position[:-1]).encode() + '\n')
    		time.sleep(0.2)
    		my_tty.write(('M11,' + Soll_Position[:-1]).encode() + '\n')
    	elif Soll_Fenster == 'Terrasse':
    		my_tty.write(('M1,' + Soll_Position[:-1]).encode() + '\n')
    	elif Soll_Fenster == 'Lars':
    		my_tty.write(('M3,' + Soll_Position[:-1]).encode() + '\n')
    	elif Soll_Fenster == 'Paula':
    		my_tty.write(('M6,' + Soll_Position[:-1]).encode() + '\n')
    	elif Soll_Fenster == 'Schlafen':
    		my_tty.write(('M7,' + Soll_Position[:-1]).encode() + '\n')
    	elif Soll_Fenster == 'Kochen':
    		my_tty.write(('M2,' + Soll_Position[:-1]).encode() + '\n')
    	elif Soll_Fenster == 'Dusche':
    		my_tty.write(('M4,' + Soll_Position[:-1]).encode() + '\n')
    	elif Soll_Fenster == 'Bad':
    		my_tty.write(('M5,' + Soll_Position[:-1]).encode() + '\n')
    	elif Soll_Fenster == 'NORD':
    		my_tty.write(('G1,' + Soll_Position[:-1]).encode() + '\n')
    	elif Soll_Fenster == 'OST':
    		my_tty.write(('G2,' + Soll_Position[:-1]).encode() + '\n')
    	elif Soll_Fenster == 'SUED':
    		my_tty.write(('G3,' + Soll_Position[:-1]).encode() + '\n')
    	elif Soll_Fenster == 'WEST':
    		my_tty.write(('G4,' + Soll_Position[:-1]).encode() + '\n')
    	elif Soll_Fenster == 'ALLE':
    		my_tty.write(('S,' + Soll_Position[:-1]).encode() + '\n')
     
    	update.message.reply_text('Fensterauswahl:', reply_markup=markup1)
    	return FENSTERWAHL
     
     
    def posabfrage(bot, update):
    	mystring=''
    	update.message.reply_text('Positionen werden abgefragt...', reply_markup=ReplyKeyboardRemove())
    	my_tty.reset_input_buffer()
    	my_tty.reset_output_buffer()
    	time.sleep(0.2)
    	my_tty.write('P,0\n')
    	time.sleep(0.5)
    	while my_tty.in_waiting > 0:
    		mystring += my_tty.read()
     
    	my_text = '<code>Wohnen  : ' + mystring.split(',')[7].rjust(3) + '%\nErker1  : ' + mystring.split(',')[8].rjust(3) + '%\nErker2  : ' + mystring.split(',')[9].rjust(3) + '%\nErker3  : ' + mystring.split(',')[10].rjust(3) + '%\nTerrasse: ' + mystring.split(',')[0].rjust(3) + '%\nLars    : ' + mystring.split(',')[2].rjust(3) + '%\nPaula   : ' + mystring.split(',')[5].rjust(3) + '%\nSchlafen: ' + mystring.split(',')[6].rjust(3) + '%\nKochen  : ' + mystring.split(',')[1].rjust(3) + '%\nDusche  : ' + mystring.split(',')[3].rjust(3) + '%\nBad     : ' + mystring.split(',')[4].rjust(3) + '%</code>'
    	bot.send_message(chat_id=update.message.chat_id, text=my_text , parse_mode=ParseMode.HTML)
     
    	time.sleep(2)
    	update.message.reply_text('Fensterauswahl:', reply_markup=markup1)
    	return FENSTERWAHL
     
     
    def stop(bot, update):
    	update.message.reply_text('Bis bald mit /start', reply_markup=ReplyKeyboardRemove())
    	return ConversationHandler.END
     
     
    def error(bot, update, error):
    	return ConversationHandler.END
     
     
    def main():
        updater = Updater("329942791:AAHNwMraT90u0Nn_3kkbDQ4xLsb3x0zSzjA")
     
        dp = updater.dispatcher
     
        conv_handler = ConversationHandler(
            entry_points=[CommandHandler('start', start)],
     
            states={
    		FENSTERWAHL:	[RegexHandler('^(Wohnen|Erker|Terrasse|Lars|Paula|Schlafen|Kochen|Dusche|Bad|NORD|OST|SUED|WEST|ALLE)$', fensterauswahl),
    				 RegexHandler('^Positionsabfrage$', posabfrage),
    				 RegexHandler('^Abbrechen$', stop)],
     
    		POSITION:	[RegexHandler('^(100%|90%|80%|70%|60%|50%|40%|30%|20%|10%|0%)$', position),
    				 RegexHandler('^Abbrechen$', stop)],
            },
     
            fallbacks=[CommandHandler('/stop', stop)]
        )
     
        dp.add_handler(conv_handler)
     
        dp.add_error_handler(error)
     
        updater.start_polling()
     
        updater.idle()
     
    if __name__ == '__main__':
        main()
     
    Mit [STRG]+[X] den Editor beenden und die Änderungen mit [j] [ENTER]speichern.


    Jetzt müssen die Scripte noch als Dienste beschrieben und gestartet werden.
    Datei für den Shutdown-Dienst erzeugen und im Editor öffnen:
    sudo nano /etc/systemd/system/gpio_daemon.service
    Folgenden Inhalt in die Datei schreiben:
    [Unit]
    	Description=Shutdown ueber Taster
    	After=multi-user.target
     
    [Service]
    	Type=idle
    	ExecStart=/usr/bin/python /home/pi/shutdown/shutdown.py
    	Restart=on-failure
    	RestartSec=1m
     
    [Install]
    	WantedBy=multi-user.target
     
    Mit [STRG]+[X] den Editor beenden und die Änderungen mit [j] [ENTER]speichern.


    Das Gleiche für den Telegram-Bot-Dienst:
    sudo nano /etc/systemd/system/rollladen_daemon.service
    Folgenden Inhalt in die Datei schreiben:
    [Unit]
    	Description=Rollladen_Steuerung_ueber_Telegram
    	After=multi-user.target
     
    [Service]
    	Type=idle
    	ExecStart=/usr/bin/python /home/pi/rollladensteuerung/telegram_bot.py
    	Restart=on-failure
    	RestartSec=1m
     
    [Install]
    	WantedBy=multi-user.target
     


    Beide Dienste aktivieren und den Raspberry Pi neu starten:
    sudo systemctl daemon-reload
    sudo systemctl enable gpio_daemon.service
    sudo systemctl enable rollladen_daemon.service
    sudo reboot


    Prüfen ob die Dienste laufen:
    sudo systemctl status gpio_daemon.service
    sudo systemctl status rollladen_daemon.service


    Wenn die Dienste ordungsgemäß laufen, kann auf dem Smartphone der erste Befehl an den eigenen Bot gesendet werden:
    Auf den Befehl "/start" wird die API mit großer Warscheinlichkeit etwa so antworten:

    Hello "Dein Telegram-Nickname". This is a private Bot. Your ChatID: "12345678" has been blocked.

    Aus dieser Nachricht ist die ChatID des eigenen Telegram-Accounts zu entnehmen, und in die Datei "telegram_bot.py" an entsprechender Stelle einzutragen.
    Bei "LIST_OF_ADMINS = [123456789, 445566778]" - es können mehrere Nutzer IDs (durch Kommata getrennt) angegeben werden.
    nano /home/pi/rollladensteuerung/telegram_bot.py
    Dies dient einfach dazu, dass nicht jeder Mensch der Welt den Bot nutzen kann/darf. Damit die Änderungen in der Datei wirksam werden, ist es am einfachsten den Raspberry neu zu starten.
    sudo reboot


    Funktionstest


    "/start" klicken

    Funktionstest 1
    Fenster per Schaltflüche
    auswählen
    Funktionstest 2
    Sollposition auswählen

    Funktionstest 3


    anschließend gelangt man
    zurück zur Fensterauswahl
    Funktionstest 4
    zum Beenden, die
    Schaltflächen scrollen
    Funktionstest 5
    ggf. verborgene Schaltflächen
    wieder anzeigen.
    Funktionstest 5


    geplante Erweiterungen

  • Abfrage der Istpositionen --> done 19.06.2018
  • Timeout bei Nichtbenutzung und offener Schaltflächen -> Auto-Abbrechen