[Bottle] HTML automatisch aktualisieren

Django, Flask, Bottle, WSGI, CGI…
Antworten
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

Hallo

In meiner Bottle-App habe ich eine Funktion eingebaut, welche mir einen Bewegungssensor ausliest.

Code: Alles auswählen

return template('info', username=username, alarm=alarm, motion=motion)

Code: Alles auswählen

<p class="bg-primary">Bewegnung Nr. <b><test_tag>{{motion}}</test_tag></b> erkannt</p>
Welche Möglichkeit habe ich, dass zB ''{{motion}}'' im Template automatisch aktualisiert wird ohne dass ein User die Seite neu laden muss?
Mit jQuery kenne ich mich noch nicht gut aus, darum wäre ich für ein paar Tipps dankbar, falls dies nur mit JS möglich ist.
BlackJack

@lackschuh: Im Template kannst Du nichts mehr nachträglich aktualisieren. Daraus wird ja HTML erstellt und an den Browser gesendet. Damit ist das erledigt.

Du müsstest eine eigene URL zur Verfügung stellen über die man den Wert des Sensors abfragen kann. Entweder als HTML-Fragment oder als JSON. Und dann fragst Du auf der Clientsseite per JavaScript regelmässig diesen Wert ab und baust ihn an der passenden Stelle in die angezeigte Webseite ein.

<test_tag> ist übrigens kein gültiges HTML.
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

Danke für die Info. Ich werde mich diesbezüglich schlau machen und mich wieder melden.
<test_tag> ist übrigens kein gültiges HTML.
Ich hab da mit jQuery experimentiert und diesbezüglich ist es soweit ich das bis jetzt verstanden habe Wurst, wie die Tags benannt werden. In der 'test.js' bekomme ich den Inhalt mit '$('test_tag').html()' aber das ist/war der falsche Weg.

mfg
BlackJack

@lackschuh: Das jQuery das Wurst ist, ist trotzdem kein Grund kaputtes HTML zu erzeugen. Wenn man Elemente eindeutig indentifizieren möchte, dann verpasst man denen ein `id`-Attribut. Und wenn man an der Stelle nicht ein ”natürliches” Element hat, dann kennt HTML <span> und <div> für Inline- oder Blockelemente die man verwenden kann statt kaputtes HTML zu erzeugen.
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

Hallo

Beim Versuch der Umsetzung deines ersten Vorschlags (URL zur Verfügung stellen) bin ich auf ein Problem gestoßen, welches mich seit gestern Abend beschäftigt. Und zwar zähle ich die Anzahl an erkannten Bewegungen. Um 'global' zu vermeiden, zähle ich mittels Decorator die Anzahl der Aufrufe der Funktion. Nun aber ist es so, dass zB der User die URL x-beliebig aufrufen bzw die Seite neu laden kann und so werden die Werte verfälscht.

Leider fällt mich nichts sinnvolles ein, wie ich dies lösen könnte. Ich bräuchte die Daten nur temporär oder soll ich die Daten trotzdem in ein JSON File speichern?

PS:
Die Fake-Tag habe ich durch eine 'id' ersetze.

Code: Alles auswählen

@app.route('/info',  method='POST')
def info_post():
    GPIO.setup(PIR_PIN, GPIO.IN)
    alarm =  int(request.forms.get('checkbox'))
    
    if alarm == 1:
        # http://sourceforge.net/p/raspberry-gpio-python/wiki/Inputs/
        GPIO.add_event_detect(PIR_PIN, GPIO.RISING, callback=bewegnung)
        while alarm == 1:
            time.sleep(1)

    elif alarm == 0:
        print("Quit")
        GPIO.cleanup()
    

@app.route('/bewegung')
def get_motion_data():
    motion_data = bewegnung()
    return motion_data


@counter
def bewegnung():
    return  'Bewegung erkannt Nr. {0} am: {1}'.format(bewegnung.count, time.strftime("%d-%B-%Y %H:%M:%S"))       
Sirius3
User
Beiträge: 18255
Registriert: Sonntag 21. Oktober 2012, 17:20

@lackschuh: verstehe nicht, was eine Bewegung mit der Anzahl der Aufrufe einer Funktion zu tun haben soll :K
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

Moin,

Wenn der PIR-Sensor eine Bewegung erkennt, dann wird die Funktion bewegnung() (''callback=bewegnung'') aufgerufen. Nun möchte ich die Anzahl an Bewegungen zählen. Auf der Konsole funktioniert das auch ganz gut, weil da niemand eingreifen kann. Mir fehlt aber jetzt die Logik, wie ich das in Bottle umsetze. Am ehesten schreibe ich eine Funktion, wo die Daten, also ''Bewegung Nr. {0} am: {1}'.format(count, time.strftime("%d-%B-%Y %H:%M:%S"))' in eine JSON Datei geschreiben werden und eine zweite Funktion, welche das JSON mittels route/URL dem jQuery zur Verfügung stellt.

Analog dazu:

Code: Alles auswählen

count = 0

def bewegnung():
    global count
    count = count + 1
    return  'Bewegung Nr. {0} am: {1}'.format(count, time.strftime("%d-%B-%Y %H:%M:%S"))    
Konsolenprogramm:

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf-8

from __future__ import print_function
from functools import wraps
import RPi.GPIO as GPIO
import time


GPIO.setmode(GPIO.BCM)
PIR_PIN = 8
GPIO.setup(PIR_PIN, GPIO.IN)


def counter(func):
    @wraps(func)
    def tmp(*args, **kwargs):
        tmp.count += 1
        return func(*args, **kwargs)
    tmp.count = 0
    return tmp


@counter
def bewegnung(PIR_PIN):
    print('Bewegung erkannt {0}'.format(bewegnung.count))

    
time.sleep(2)


def main():
    try:
        GPIO.add_event_detect(PIR_PIN, GPIO.RISING, callback=bewegnung)
        while True:
            time.sleep(1)

    except KeyboardInterrupt:
        print("Quit")
        GPIO.cleanup()


if __name__ == '__main__':
    main()
Sirius3
User
Beiträge: 18255
Registriert: Sonntag 21. Oktober 2012, 17:20

@lackschuh: Du brauchst das nicht in eine Datei zu schreiben, eine Variable reicht auch schon.
BlackJack

@lackschuh: Das so einzubauen sieht mir ein wenig gefährlich aus. Der Benutzer, oder sogar mehrere Benutzer können die `/info`-URL ja durchaus mehr als einmal aufrufen. Bist Du sicher, dass es okay ist `add_event_detect()` mehr als einmal aufzurufen? Und ist das alles threadsafe? Oder garantierst Du dass das immer nur single-threaded laufen wird‽

Das nächste ist die ``while alarm == 1``-Schleife. Da hättest Du auch gleich ``while True`` schreiben können. Wie sollte `alarm` denn dort jemals den Wert ändern wenn alles was *in* der Schleife passiert ein `sleep()` ist? Damit hättest Du einen single-threaded Webserver an der Stelle auch erfolgreich lahm gelegt und einem multi-threaded einen Thread geklaut, weil der fortan in dieser Schleife hängt.

Wir haben hier ein klassisches Beispiel von der Vermischung von Programmlogik und Benutzerinteraktion. Ich würde erst einmal die Programmlogik ohne Webanwendung schreiben. Und da wir hier Zustand haben der über Funktionsaufrufe hinweg existiert, bietet sich da irgendwie eine Klasse an.

Ob der Code am Ende ausschliesslich single-threaded laufen darf, oder auch mit multi-threaded klar kommt, und auf keinen Fall in mehreren Prozessen laufen darf, solltest Du übrigens gut dokumentieren. Würde ich in den „Requirements”-Abschnitt und auch zu Anmerkungen zur Installation schreiben. Und die Webanwendung selber sollte vielleicht die entsprechenden WSGI-Umgebungsvariablen prüfen um gegebenfalls zu meckern wenn die Laufzeitumgebung nicht unterstützt wird.

Mal zwei Absätze aus der mod_wsgi-Dokumentation zum Thema:
„A WSGI application which is not written to take into consideration the different combinations of process and threading models may not be portable and potentially may not be robust when deployed to an alternate hosting platform or configuration.

Although you may not need an application or application component to work under all possible combinations for these values initially, it is highly recommended that any application component still be designed to work under any of the different operating modes. If for some reason this cannot be done due to the very nature of what functionality the component provides, the component should validate if it is being run within a compatible configuration and return a HTTP 500 internal server error response if it isn't.”
Bei Gelegenheit könntest Du alle Vorkommen von `bewegnung` durch `bewegung` ersetzen. :-)
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

@Sirius3
Aber wie ohne 'global'?
Mit 'global' geht es schon:

Code: Alles auswählen

counter = 0   
def bewegung(PIR_PIN):
    global counter
    counter = counter + 1
    print 'Bewegung %d' % counter


@app.route('/bewegung')
def get_motion_data():
    global counter
    return  'Bewegung erkannt Nr. {0}'.format(counter) 

EDIT:
Hab BlackJack's Beitrag noch nicht gesehen.
BlackJack

@lackschuh: Mit dem Dekorator hast Du doch ein Attribut auf dem Funktionsobjekt. Warum willst Du das durch ein ``global`` ersetzen?
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

BlackJack hat geschrieben:Der Benutzer, oder sogar mehrere Benutzer können die `/info`-URL ja durchaus mehr als einmal aufrufen.
Ich hab ein Login mittels 'bottle.ext.sqlite' gebaut, so dass nur berechtigte User den Sensor an- oder abschalten können.
BlackJack hat geschrieben:Bist Du sicher, dass es okay ist `add_event_detect()` mehr als einmal aufzurufen?
Die Methode wird nur einmal aufgerufen, wenn der Schalter auf 'on' geht also wenn alarm = 1 ist, oder hab ich da was übersehen?
BlackJack hat geschrieben:Mit dem Dekorator hast Du doch ein Attribut auf dem Funktionsobjekt
Ja funktioniert auch gut. Ich werde mir im Laufe der Woche deinen Rat zu Gemüte führen und das Programm nochmals auseinander nehmen (Programmlogik und Benutzerinteraktion) und auch der Namensgebung mehr Beachtung schenken.

Bis jetzt habe ich es mittels jQuery gelöst, dies gleicht aber einer DDOS-Attacke, denn so wird permanent die URL '/bewegung' geladen.

Code: Alles auswählen

@counter
def get_motion(PIR_PIN):
    #print 'Bewegung %d' % get_motion.count
    print  'Bewegung erkannt Nr. {0} am: {1}'.format(get_motion.count, time.strftime("%d-%B-%Y %H:%M:%S")) 


@app.route('/bewegung')
def get_motion_data():
    return  '{0}'.format(get_motion.count) 


@app.route('/info')
def info(db):
    global alarm
    username = is_logged_in(db)
    ip = request.environ.get('REMOTE_ADDR')
    if not username:
        return template('main', username=False,  ip=ip)
    else:
        return template('info', username=username, alarm=alarm)
    
    
@app.route('/info',  method='POST')
def info_post():
    global alarm
    GPIO.setup(PIR_PIN, GPIO.IN)
    alarm =  int(request.forms.get('checkbox'))
    
    if alarm == 1:
        GPIO.add_event_detect(PIR_PIN, GPIO.RISING, callback=get_motion)
        while True:
            time.sleep(1)

    elif alarm == 0:
        print 'Exit'
        GPIO.cleanup()

Code: Alles auswählen

% rebase('layout', title='Seiten-Titel', username=username)
<div class="container">
  <h2>Bla Bla 1</h2>
  <p class="page-header">Bewegungsmelder ein oder ausschalten</p>
  <h4>Alarmanlage:  <strong>{{alarm}}</strong></h4>
  <input type="checkbox" name="my-checkbox">
  <hr>
  <p class="bg-primary">Bewegnung Nr. <strong></strong> erkannt</p>
  </div>

Code: Alles auswählen

    $("p.bg-primary").find( "strong" ).load( "bewegung" );
       var refreshId = setInterval(function() {
          $("p.bg-primary").load('bewegung');
        }, 1000);
PS:
1x 'global' hab ich noch in Verwendung :oops:
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

lackschuh hat geschrieben:
BlackJack hat geschrieben:Bist Du sicher, dass es okay ist `add_event_detect()` mehr als einmal aufzurufen?
Die Methode wird nur einmal aufgerufen, wenn der Schalter auf 'on' geht also wenn alarm = 1 ist, oder hab ich da was übersehen?
Vielleicht kommt ja jemand auf die Idee und ruft die Seite einfach mehrfach auf ;-)
Das Leben ist wie ein Tennisball.
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

Wenn die Seite von zwei Usern gleichzeitig aufgerufen wird, dann passiert noch nichts. Sollte dann aber beide den Schalter betätigen zum Einschalten des Sensors, dann bekommt der langsamere folgende Meldung bzw auf der Konsole wird folgendes ausgegeben:

Code: Alles auswählen

Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/bottle.py", line 862, in _handle
    return route.call(**args)
  File "/usr/local/lib/python2.7/dist-packages/bottle.py", line 1729, in wrapper
    rv = callback(*a, **ka)
  File "app.py", line 283, in info_post
    GPIO.add_event_detect(PIR_PIN, GPIO.RISING, callback=get_motion)
RuntimeError: Conflicting edge detection already enabled for this GPIO channel
BlackJack

@lackschuh: Diese Ausnahme sollte man ja eigentlich verhindern.

Vielleicht habe ich mich mit dem ``while True:`` missverständlich ausgedrück: Das ist totaler Unsinn. Du blockierst damit einfach nur einen Thread in einem multi-threaded Webserver, oder den gesamten Webserver bei einem single-threaded Webserer. Ein multi-process Server funktioniert mit der Anwendung gar nicht, weil da weder der Counter einmalig ist, noch der Code der auf die Hardware zugreift.

Das mit der ständigen Abfrage ist der Weg wie man das (noch) löst. Zumindest bis sich Websockets durchgesetzt haben.

Das HTML- und JavaScript-Fragment verwendet jetzt doch gar keine ID um das Element zu identifizieren‽

Wenn man sowieso schon regelmässig die Anzahl der Bewegungen abfragt, könnte man auch gleich den Zustand des Schalters mitliefern. Dann könnte man den auch immer aktualisieren.
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

Hallo BlackJack

Das mit dem 'while True' ist einleuchtend. Ich habe dies stumpf 1:1 aus meinem Konsolen-Programm so übernommen. Auch wurde nun eine ID anstelle eines Tags im Template eingefügt. Ich habe noch eine Variable 'global alarm' in Verwendung, weil ich so den Zustand auch nach dem Schliessen des Browser speichern kann. Da ich eh schon eine DB habe, wäre es wohl besser, dies direkt in die DB zu schreiben und abzufragen. (?)
BlackJack hat geschrieben:Wenn man sowieso schon regelmässig die Anzahl der Bewegungen abfragt, könnte man auch gleich den Zustand des Schalters mitliefern. Dann könnte man den auch immer aktualisieren.
Der Sensor liefert nur 1 oder 0 zurück. Also wenn eine Bewegung erkannt wird, dann ist der GPIO Input ("GPIO.input(PIR_PIN)") für ca. 1/2 Sekunde auf 1 und dann wieder auf 0.

Code: Alles auswählen

def get_time_of_motion():
    time_of_motion = dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    return time_of_motion
    
    
def counter(func):
    @wraps(func)
    def tmp(*args, **kwargs):
        tmp.count += 1
        tmp.motion_time = get_time_of_motion()
        tmp.motion_liste = create_motion_list()
        return func(*args, **kwargs)

    tmp.count = 0
    tmp.motion_time = '0:0:0:0'
    tmp.motion_liste = []
    return tmp


motion_list = []


def create_motion_list():
    motion_list.append([get_motion.count, get_motion.motion_time])
    return motion_list

    
@app.route('/info',  method='POST')
def info_post():
    global alarm
    GPIO.setup(PIR_PIN, GPIO.IN)
    alarm =  int(request.forms.get('checkbox'))
    
    if alarm == 1:
        print 'Start PIR-Sensor'
        GPIO.add_event_detect(PIR_PIN, GPIO.RISING, callback=get_motion)

    elif alarm == 0:
        print 'Exit'
        GPIO.cleanup()

Code: Alles auswählen

% rebase('layout', title='Informations', username=username)
<div class="container">
  <h2>Bla Bla 1</h2>
  <p class="page-header">Bewegungsmelder ein oder ausschalten</p>
  <h4>Alarmanlage:  <status>{{alarm}}</status></h4>
  <input type="checkbox" name="my-checkbox">
  <hr>
  <div id="my_table"></div>
  </div>

Code: Alles auswählen

      $("#my_table").load( "bewegung" );
       var refreshId = setInterval(function() {
         $("#my_table").load( "bewegung" );
        }, 1000);
Ausgabe:

Code: Alles auswählen

Bewegung	Datum
1	2014-10-09 14:37:17
2	2014-10-09 14:37:26
3	2014-10-09 14:37:40
4	2014-10-09 14:38:10
5	2014-10-09 14:38:29
6	2014-10-09 14:38:46
7	2014-10-09 14:39:04
8	2014-10-09 14:39:12
EDIT:
Ups, hab gerade noch gesehen, dass im Template noch ein Tag '<status>' anstelle einer ID ist. Muss ich noch ändern.
BlackJack

@lackschuh: Mit Schalter meine ich den „Alarm ist an/aus”-Schalter, also Dein globales `alarm`. Nicht die Hardware die da abgefragt wird.

Du übertreibst das echt mit dem Dekorator und was Du da alles als Attribute an die Funktion heftest. Eine Klasse wäre hier glaube ich deutlich verständlicher.

Und die neue globale Variable `motion_list` ist auch unschön. Das `motion_liste`-Attribut wird gesetzt aber überhaupt nicht verwendet!?

Das Anhängen von konkreten Typen an Namen ist auch nicht gut. Vielleicht ändert man ja irgendwann mal den Typ, zum Beispiel zu einer `collections.deque` damit man nicht *alle* Bewegungen sammelt sondern nur die letzten x Bewegungen. Und dann ist der Name plötzlich falsch und man muss den überall ändern.

Die '/info'-URL gibt gar nichts zurück‽ Würde ich bei dem Namen aber irgendwie erwarten.
Antworten