Mittwoch, 7. November 2012

Ruby: Der Unwetter-Bot mit Twitter- und Arduino-Anbindung

tldr; (Abstract)

In diesem Blogpost zeige ich, wie man mit einem Ruby-Script über Regular Expressions statische Webseiten automatisiert auswertet und abhängig vom Ergebnis über das Twitter Gem eine Direct Message verschickt.


Die Warnseite des Deutschen Wetterdienstes hatte bei mir schon immer einen festen Platz auf der Bookmarkliste. Wer will schon mit offenen Fenstern vom Sturm überrascht werden und als Bahnfahrer ist man in der Regel eh "unwettersensitiv".

Das Problem: Man muss die Informationen selber einholen. Besser wäre es natürlich, wenn man die Informationen automatisch als Direct Message bekommt und am allerbesten wenn diese Infos für weitere Automatisierungen herhalten könnten. Genau das ermöglicht das folgende Script.

Vorab aber zwei Warnhinweise:

  1. Die Wetterdaten des DWD unterliegen dem Urheberrecht. Sich selber eine DM oder Notification auf Basis frei verfügbarer Informationen zu senden oder für weitere Heim-Automatisierung zu verwenden ist eine Sache, die via Script gewonnen Daten anderweitig zu veröffentlichen eine (wohl kaum durch die DWD-AGB gedeckte) andere Sache. Und das mit Recht, denn:
  2. Unwetterschutz ist eine ernste Sache. Das Script ist mehr ein Proof-of-Concept und sollte nicht in Bereichen eingesetzt werden, in dem Leib und Leben von einer 100% korrekten Funktion abhängt. Dazu später auch noch mehr... Wenn ihr eine "ernsthafte" Anwendung plant könnt ihr euch direkt an den DWD wenden, die bieten ein breites Produktportfolio. Ich habe mir deren Warnseiten hier aufgrund ihres recht statischen Charakters exemplarisch rausgepickt.

Nun aber zur Funktion des Scripts. Zuvor sollte man wissen, dass der Deutsche Wetterdienst seine Warninformationen individuell für Kreise auf eigenen statischen Webseiten ohne jQuery-Schischi anbietet, soweit ich das sehen kann decken sich diese Kreise mit den Landkreisen, ggf. noch einmal wetterrevelant unterteilt, und werden vom DWD mit 3-Buchstaben-Kürzeln versehen. Beispiele:

Stadt Bottrop: http://www.dwd.de/dyn/app/ws/html/reports/BOT_warning_de.html

Cuxhaven Binnenland http://www.dwd.de/dyn/app/ws/html/reports/CUI_warning_de.html

Das Script macht sich nun zunutze, dass der DWD die Warnlage auf diesen Landkreisseiten immer gleich aufbaut. Wenn keine Warnung vorliegt findet sich der entsprechende String "keine Warnungen" im Seitenquelltext, ansonsten werden Warnmeldungen mit "Amtliche Warnmeldung" begonnen.

Diesen relativ statischen Aufbau macht sich das Script zunutze und steuert nicht nur seinen eigenen Prozesslauf, sondern extrahiert auch die folgenden relevanten Informationen über Regular Expressions aus dem Seitenquelltext:

  • Name des Landkreises
  • Titel der Warnmeldung
  • Zeitlicher Beginn der Warnlage
  • Zeitliches Ende der Warnlage
  • Veröffentlichungszeitpunkt

Insbesondere die Extraktion des Veröffentlichungszeitpunkt ist für den Prozesslauf von besonderer Bedeutung:

  • Zunächst prüft das Script, ob überhaupt eine Warnmeldung vorliegt.
  • Ist das der Fall werden die o.g. Daten extrahiert und in eigene Variablen weggeschrieben
  • Aus dem Veröffentlichungszeitpunkt wird unter der Annahme, das pro Landkreis eine individuelle Warnmeldung über den minutengenauen Veröffentlichungszeitpunkt determiniert werden kann, eine ID gebildet.
  • Diese ID wird mit den IDs aus einer CSV-Datei verglichen. Diese enthält alle bereits durch das Script für diesen Landkreis registrierten Warnmeldungen.
  • Ist die ID bereits in der Liste enthalten endet das Script hier.
  • Ist die ID noch nicht enthalten wird sie zusammen mit den anderen extrahierten Informationen der CSV-Datei hinzugefügt.
  • Als weitere Aktionen wird mir zum einen der Titel der Warnmeldung als Direct Message via Twitter zugestellt.
  • Zudem prüft das Script optional, ob laut Titel die Warnmeldung vor einem Regenereignis warnt. Wenn ja wird über den seriellen Monitor eine "1" geschrieben. (Diese Information könnte wiederum von einem Arduino genutzt werden, um im Rahmen der Heimautomation ein Rückschlagventil im Keller zu schließen. Zum Thema Ruby<>Arduino Kommunikation siehe u.a. meinen älteren Blogpost Arduino, das Restclient Gem und bidirektionale Kommunikation).

Um korrekt zu funktionieren benötigt das Script ein paar Konfigurationsparameter, die am Anfang definiert werden:

  • Den DWD-Citycode, der über die Bundeswarnkarte via Durchklicken ermittelt werden kann.
  • Den Dateinamen der CSV-Datei zum loggen der Warnmeldungen
  • Den Intervall für die erneute Prüfung der Warnseite in Sekunden.
  • Die API-Keys für die Twitter-API, näheres siehe dazu auf der Seite des Twitter-Gems und auf dem Developerportal von Twitter
  • Konfigurationsparameter für das Schreiben auf dem seriellen Port.

Soweit so gut hat das Script doch einen kleinen Schönheitsfehler. Es wertet immer nur die die erste Warnmeldung auf der Warnseite auf. Der DWD fügt die neueste Warnmeldung immer oben auf der Seite hinzu, wenn eine alte Warnmeldung noch Bestand hat rutscht diese weiter herunter. Insofern ist die Einstellung für den Aktualisierungsintervall mit Bedacht zu wählen, auf der einen Seite wollen wir die DWD-Seite nicht fluten, auf der anderen Seite keine Warnmeldung verpassen. Eine Einstellung von 5 Minuten (300 Sekunden) sollte dabei einen guten Mittelweg darstellen. Die technisch saubere Lösung würde daraus bestehen alle Warnmeldungen auf der Seite wie oben beschrieben zu parsen und nicht nur die erste Meldung. Aber zum einen würde das über das Proof-of-concept-Ziel "Abhängig von einer Meldung auf einer Webseite führen wie eine Aktion aus" hinausgehen und zum anderen wäre es bedeutet einfacher, dafür den eMail-Newsletter des DWD zu analysieren. Wie das geht werden wir uns in einem späteren Beitrag zu Gemüte führen, sobald ich mich mit den entsprechenden email-Ruby-Gems beschäftigt habe ;-)

Zuletzt noch ein Dank an den Deutscher Wetterdienst für seine Arbeit. Besucht mal deren Webseite, für Wetter-Nerds ist das eine wahre Fundgrube :-)

require 'rubygems'
require 'rest-client'
require 'fastercsv'
require 'twitter'
require 'serialport'

### Konfiguration ###

# DWD Citycode  
city = "BOT"

# Logging CSV
csvfilename = "logging.csv"

# Sleep-Intervall in Sekunden
intervall = 300

# Twitter
Twitter.configure do |config|
  config.consumer_key = ''
  config.consumer_secret = ''
  config.oauth_token = ''
  config.oauth_token_secret = ''
end

# Arduino Serial Monitor
serial_support = false

if serial_support == true then
  serial_port = "/dev/tty.usbmodem411"
  serial_baud_rate = 9600
  serial_data_bits = 8
  serial_stop_bits = 1
  serial_parity = SerialPort::NONE
  # init serial port
  sp = SerialPort.new(serial_port, serial_baud_rate, serial_data_bits, serial_stop_bits, serial_parity)
end

#########################################


loop do
 begin

    response = RestClient.get "http://www.dwd.de/dyn/app/ws/html/reports/#{city}_warning_de.html"
  
    if response =~ /keine Warnungen/ then
      puts "DWD: Keine Warnung in #{city}"
    
    elsif response =~ /Amtliche WARNUNG/ then
      if response =~ /(Amtliche WARNUNG vor [\D]+)(<\/.*>)$/ then
        message = $1.gsub(/\n/," ").gsub(/(<.*>)/,"").gsub("  "," ").strip
      end

      # Startdatum ($3) und Uhrzeit ($5)
      if response =~ /(gültig von: )(\w*,\s)(\d*\.\d*\.\d*)(.)(\d\d:\d\d)/ then
        startdate = $3
        starttime = $5
      end
    
      # Enddatum ($3) und Uhrzeit ($5)
      if response =~ /(bis: )(\w*,\s)(\d*\.\d*\.\d*)(.)(\d\d:\d\d)/ then
        enddate = $3
        endtime = $5   
      end
    
      # Ausgabedatum und Zeit
      if response =~ /(am: )(\w*,\s)(\d*\.\d*\.\d*)(.)(\d\d:\d\d)/ then
        announcedate = $3
        announcetime = $5   
      end
      
      # Name der Stadt
      if response =~ /<title>(.*)-(.*)<\/title>/ then
        cityname = $2.strip
      end
    
      # Aus Ausgabedaten notification_id generieren, Muster: ddmmyyyyhhmm
      notification_id = (announcedate+announcetime).gsub(".","").gsub(":","")
    
      linenumber = 0
      notification_id_logging = 0
      notification_id_already_logged = false
    
      # Logging-Datei parsen
      FasterCSV.foreach(csvfilename, :quote_char => '"', :col_sep =>';', :row_sep =>:auto) do |row|
        # Erste Zeile erhält nur Header
        unless linenumber == 0 then
          # notification_id_logging aus erster Zelle auslesen 
          # und mit aktueller notification_id vergleichen
          notification_id_logging = row[0].to_i
          if notification_id_logging == notification_id.to_i then
            notification_id_already_logged = true
          end
        end
        linenumber += 1
      end
    
      # Falls Warnung noch nicht im Log...
      if notification_id_already_logged == false then
        # Warnung der CSV-Datei hinzufügen
        open(csvfilename, 'a') do |fileop|
          fileop.puts "#{notification_id};#{announcedate};#{announcetime};#{message};#{startdate};#{starttime};#{enddate};#{endtime}"
        end
        Twitter.direct_message_create("petschbot", "DWD-Warnung für ##{cityname}: #{message}")
        puts "DWD-Warnung für ##{cityname}: #{message}"
        if serial_support == true then
          if message =~ /regen/i then
            sp.write "1"
          end
        end
      end
  
    else
      puts "Warnstatus für #{city} nicht auslesbar"
    end

  rescue => error
    puts "Error (Falscher Stadtcode?)"
  end

sleep(intervall)

end

Keine Kommentare:

Kommentar veröffentlichen