LE TEMPERATURE LETTE DAL CLOCK-ALARM SUL WEB PARTE II

In questa seconda parte illustrerò come predisporre lo script per accedere dall'esterno e vedere la temperatura di casa e le statistiche.
Utilizzerò lo script del sito www.raspberrywebserver.com, che ringrazio ancora per la disponibilità.
Ho voluto spiegare ogni singolo passaggio perché possa essere un tutorial alla programmazione in python per dummies (come me). Ho notato che spesso i tutorial, anche se semplici e dettagliati, danno per scontato alcuni passaggi. vorrei invece provare a fare un breve (é solo un cgi-script) script e provare a spiegare ogni riga.

#!/usr/bin/env python

questa riga all'inizio dello script preceduta da #! (o shebang) fa sì che unix legga lo script come eseguibile ed utilizzerà l'interprete specificato, cioé python per noi.

import sqlite3
import sys
import cgi
import cgitb

qui si importano i moduli necessari: il database sqlite3 che verrà interrogato, il modulo sys che contiene funzioni di base come time, il modulo cgi che serve a rendere eseguibile questo cgi-script ed il modulo cgitb (traceback) che serve per l'eventuale debugging e mostra la posizione dove si é generato l'errore in fase di visualizzazione errata della pagina web.

dbname='/var/www/weather.db'

qui scriviamo il nome completo di percorso del nostro dbase in modo che dopo non riscriveremo tutto il percorso ma solo "dbname".

def printHTTPheader():
    print ("Content-type: text/html\n\n")

definiamo una funzione che chiamiamo HTTPheader che stamperà il codice racchiuso dagli apici. Quando richiameremo questa funzione nel main, all'inizio di tutto, diremo al browser che stiamo per inviare una pagina html. Ci deve essere una riga vuota dopo ciò, quindi vedete un primo \n per andare a capo ed il secondo per creare la riga vuota.

def printHTMLHead(title, table):
    print ("<head>")
    print ("   <title>")
    print (title)
    print ("   </title>")
    print_graph_script(table)
    print ("</head>")

qui stampiamo la head section in cui si mette il titolo della pagina come in qualsiasi html page. In questa pagina, in più, si metterà il codice java che permetterà di creare il grafico di google. Il codice "print_graph_script" con il parametro table indicato in fase di runtime (cioé quando si genererà il codice) viene scritto più avanti in una funzione così chiamata. Noterete sempre gli apici, servono a fare scrivere al cgi-script una stringa che poi si incollerà nel browser come se stesse caricando una pagina html composta di stringhe.

def get_data(interval):

    conn=sqlite3.connect(dbname)
    curs=conn.cursor()

    if interval == None:
        curs.execute("SELECT * FROM temps")
    else:
        curs.execute("SELECT * FROM temps WHERE /-
        -/ timestamp>datetime('now','localtime','-%s hours')" % interval)

    rows=curs.fetchall()
    conn.close()
    return rows

questa funzione serve a prelevare i dati dal database, specificando l'intervallo di tempo da considerare, intervallo definito successivamente. La prima riga crea una connessione al database di nome "dbname" specificato prima. La seconda riga crea un "cursore", per semplificare é come fosse il dito che si fa scorrere leggendo righe di testo.
La terza riga: se l'intervallo é None cioé non é specificato, prendi tutto, viceversa prendi ciò che dice l'interval(lo).
Alla "lettera": esegui SELECT * FROM temps WHERE cioé prendi dal db temps dove...
....timestamp (il campo data e ora del dbase) é maggiore della data/ora scritto da interval(lo).
Notare la parentesi: now vuole dire adesso, 'localtime' é importante perché senza di questo prende il tempo di UTC, non considera che siamo in Italia.
Poi, dal tempo di "ora", "adesso", togli il numero di ore scritte da interval. Se per esempio ora é 2014 aprile 03, le 18 e 34 ed abbiamo scelto le ultime 6 ore di interval, il parametro datetime diventerà 2014/04/03 18:34 - 6 hrs = 2014/04/03 12:34. il cursore prenderà quindi tutti i dati registrati nel db maggiori di quest'ultimo time.
La riga successiva memorizzerà in rows tutti i record appena letti dal cursore. Poi chiude la connessione ed infine scriverà in memoria i dati in una tuple (formato necessario a google chart per leggere i dati):

[("time(a)",temp(a)),("time(b)",temp(b)), .....("time(n)",temp(n))]

Quanto sopra ha questo significato: ogni elemento della tupla, incluso in due () é composto da due record, il tempo, che deve essere in formato stringa ("") e la temperatura, in formato numero. Tutti i record nella tupla sono in riga mentre google chart vuole i dati in tabella. La funzione successiva farà ciò.

def create_table(rows):
    chart_table=""

    for row in rows[:-1]:
        rowstr="['{0}', {1}],\n".format(str(row[0]),str(row[1]))
        chart_table+=rowstr

    row=rows[-1]
    rowstr="['{0}', {1}]\n".format(str(row[0]),str(row[1]))
    chart_table+=rowstr

    return chart_table

Qui si crea la funzione create_table che ha come parametro rows (attenzione al plurale). Prima di tutto creiamo la tabella "chart_table" vuota (i due apici). Adesso c'é un ciclo for, for row in rows farebbe pensare ad un ciclo che legge le righe. Invece no, ogni row in rows é in realtà ogni elemento della tupla di cui sopra. quindi per ogni elemento (row) della tupla (rows) fino all'ultimo escluso (:-1) prendi:

il primo elemento di ciascuna coppia (la coppia nella parentesi tonda) in formato stringa ('{0}') e il secondo elemento in formato numero ({1}), aggiungi un salto riga (cioé scrivi il prossimo su una nuova riga) e scrivi questi dati in formato stringa (.format(str...)) nella variabile rowstr. notare i doppi apici nel ciclo for che indicano come dovranno essere scritti i dati. infatti si dovranno scrivere tra parentesi quadre perché possano essere utilizzati dalla tabella javascript che google chart leggerà.
la parte finale dice di scrivere quanto sopra nella chart_table (chart_table= chart_table + rowstr). se non ci fosse /n i dati verrebbero scritti uno di fianco all'altro, così saranno in formato tabella. fatto questo per tutti i record della tupla tranne l'ultimo, passiamo a questo. se notate prima c'era una virgola dopo la "]". serve a google chart a capire dov'é la fine della coppia di dati nella tabella per passare a quella successiva (x,y). però prima o poi deve arrivare alla fine per il grafico, la coppia senza virgola dirà che questo é l'ultimo valore.
quindi senza usare un ciclo di ripetizione diciamo solo che ora row é assegnato all'ultimo elemento della tupla (rows[-1]) e su quello prendiamo e scriviamo i dati nella tabella.
con "return" memorizziamo temporaneamente la tabella appena creata.

se volte vedere cosa scrive aggiungete qui sotto print (chart_table) e lanciate lo script da idle, serve per il debugging o solo per rendersi conto di cosa fa.

def print_graph_script(table):

    chart_code="""
    <script type="text/javascript" src="https://www.google.com/jsapi"></script>
    <script type="text/javascript">
      google.load("visualization", "1", {packages:["corechart"]});
      google.setOnLoadCallback(drawChart);
      function drawChart() {
        var data = google.visualization.arrayToDataTable([
          ['Time', 'Temperature'],
%s
        ]);

        var options = {
          backgroundColor: 'black',
          colors: ['red'],
          hAxis: {
              
              textStyle: {color: '#00ff00'},
              
          },
          vAxis: {
             baselineColor: '#00ff00',
             textStyle: {color: '#00ff00'},
             gridlines: {
                color: '#00ff00'
                }
          },
          legend: {
              textStyle: {color: '#00ff00'}
          }
        };
    
        var chart = new google.visualization.LineChart (document.getElementById('chart_div'));
        chart.draw(data, options);
      }
    </script>"""

    print (chart_code % (table))def create_table(rows):
    chart_table=""

questo sopra é il passaggio di creazione del grafico. Non entrerò nel dettaglio in quanto richiederebbe molte righe ed in realtà si tratta di uno script java sviluppato da google che potete trovare sulle sue pagine di aiuto per i webmaster. Si parte dal codice del grafico che definiamo e creiamo vuoto (chart_code=""). quello che segue é quanto verrà scritto per creare il grafico. Il nostro grafico é di tipo a linea ed é definito in ["corechart"].
Dove vedete:

['Time', 'Temperature'], %s


é la tabella con indicate le etichette di Time per le x e Temperature per le y. Qui si dovranno inserire i valori creati prima dalla chart_table, uno in colonna all'altro. Per fare ciò vedete la %s che sta ad indicare che sostituiremo queste etichette con i valori definiti alla fine, in formato stringa (s). Sarà la riga:

print (chart_code % (table))

a fare ciò. Si usa il print perché in realtà lanciando questa funzione in fase di scrittura del codice html, nella posizione giusta, scriveremo tutto quanto contenuto nel chart_code con al posto di Time e Temperature i valori che ci serviranno. Notare le "var options": qui potete personalizzare il grafico in colori di sfondo e linee. con il lessico giusto (googolando un pò si trova) potete sbizzarrirvi.
def show_graph():
    print ("<h2>Temperature Chart</h2>")
    print ('<div id="chart_div" style="width: 900px; height: 700px;">

Questa funzione che verrà richiamata nel body dell'html stamperà in esso il titolo del grafico che scegliamo e lo stesso grafico con la larghezza e altezza qui scelti. L'ultima riga dello javascript sopra diceva appunto di creare un id di nome chart_div. qui ora si dice di mettere nel div questo id, cioé di visualizzare il grafico creato dallo javascript nel div che vogliamo.
def show_stats(option):

    conn=sqlite3.connect(dbname)
    curs=conn.cursor()

    if option is None:
        option = str(24)

    curs.execute("SELECT timestamp,max(temp) FROM temps WHERE timestamp>datetime('now','localtime','-%s hour')" % option)
    rowmax=curs.fetchone()
    rowstrmax="{0}&nbsp&nbsp&nbsp{1}&nbspC".format(str(rowmax[0]),str(rowmax[1]))

    curs.execute("SELECT timestamp,min(temp) FROM temps WHERE timestamp>datetime('now','localtime','-%s hour')" % option)
    rowmin=curs.fetchone()
    rowstrmin="{0}&nbsp&nbsp&nbsp{1}&nbspC".format(str(rowmin[0]),str(rowmin[1]))

    curs.execute("SELECT avg(temp) FROM temps WHERE timestamp>datetime('now','localtime','-%s hour')" % option)
    rowavg=curs.fetchone()

    print ("<h2>Minimum temperature&nbsp</h2>")
    print (rowstrmin)
    print ("<h2>Maximum temperature</h2>")
    print (rowstrmax)
    print ("<h2>Average temperature</h2>")
    print ("%.1f" % rowavg+" C")

    print ("<h2>In the last hour:</h2>")
    print ("<table>")
    print ("<tr><td><strong>Date/Time</strong></td>
<td><strong>Temperature</strong></td></tr>")

    rows=curs.execute("SELECT * FROM temps WHERE timestamp>datetime('now','localtime','-1 hour')")
    for row in rows:
        rowstr="<tr><td>{0}&emsp;&emsp;</td><td>{1}&nbspC</td></tr>".format(str(row[0]),str(row[1]))
        print (rowstr)
    print ("</table>")

    conn.close()

quella sopra é la funzione che permetterà di visualizzare un intervallo scelto di tempo e quindi aggiornerà su questa base le statistiche tipo media, max...
Il parametro option é proprio la nostra scelta che limiterà il periodo di analisi.
Creiamo, come prima, la connessione al dbase ed il cursore. Se "option" non é specificata, cioé se apriamo lo script senza fare altro, settiamo "option" a 24 ore. Per l'esattezza option verrà definito dopo, qui si prende per come é stato definito.
La riga successiva interroga il dbase: esegui la ricerca (curs.execute) dei record "data" e "temperatura massima" dall'intervallo definito dalla data, dove la data (e ora) é maggiore di adesso meno un numero di ore definito da option. quindi se option é 168 ore (7 giorni), sottrarremo 168 ore ad adesso e da lì in avanti prenderemo quei dati e cercheremo il valore max.
Poi assegnamo a rowmax il valore letto (fetchone) dal cursore. questo rowmax, fatto da due campi, la data/ora e la temperatura, si scriverà in rowstrmax, cioé i valori che verranno scritti a video. Le scritte &nbsp (not breaking space) aggiungono uno spazio o più ai valori. le righe successive fanno lo stesso ma per la temperatura minima e media.
Arriviamo quindi ai print che stamperanno il codice html che sarà caricato dal browser: si stampa prima il codice che scrive il titolo <h2>...</h2> e sotto il valore memorizzato poco prima che si desidera (es. rowstrmin).
Il blocco successivo scrive le temperature dell'ultima ora. Nel mio caso che ho fatto una lettura ogni ora comparirà sempre solo un valore ma é strutturato a tabella per contenere più valori.
Qui stampiamo direttamente l'header, scriviamo nel codice html che iniziamo una tabella (<table>). Poi scriviamo, sempre nel codice, i titoli dei campi delle colonne. ora lanciamo un ciclo for come sopra per row in rows, dove però ora rows é una ricerca del dbase dove si trovano i campi che hanno l'ora uguale all'ultima registrata (1 o più). Per ogni valore che troviamo, che ricordo é una coppia di una tupla fatta da data/ora e temperatura, mettiamola in una riga della tabella che stiamo stampando nell'html e che stiamo chiamando rowstr. Notare &emsp al posto di &nbsp che mette uno spazio equivalente alle dimensioni in punti del font utilizzato. stampiamo nel codice la rowstr appena definita. Dopo avere fatto ciò per tutte le coppie di valori che hanno in comune la stessa ultima ora, stampiamo nel codice la fine della tabella (</table>).
chiudiamo la connessione al dbase.

def print_time_selector(option):

    print ("""<form action="/cgi-bin/webguii.py" method="POST">
        Show the temperature logs for  
        <select name="timeinterval">""")


    if option is not None:

        if option == "6":
            print ("<option value=\"6\" selected=\"selected\">the last 6 hours</option>")
        else:
            print ("<option value=\"6\">the last 6 hours</option>")

        if option == "12":
            print ("<option value=\"12\" selected=\"selected\">the last 12 hours</option>")
        else:
            print ("<option value=\"12\">the last 12 hours</option>")

        if option == "24":
            print ("<option value=\"24\" selected=\"selected\">the last 24 hours</option>")
        else:
            print ("<option value=\"24\">the last 24 hours</option>")

        if option == "72":
            print ("<option value=\"72\" selected=\"selected\">the last 3 days</option>")
        else:
            print ("<option value=\"72\">the last 3 days</option>")
            
        if option == "168":
            print ("<option value=\"168\" selected=\"selected\">the last 7 days</option>")
        else:
            print ("<option value=\"168\">the last 7 days</option>")
    else:
        print ("""<option value="6">the last 6 hours</option>
            <option value="12">the last 12 hours</option>
            <option value="24" selected="selected">the last 22 hours</option>""")

    print ("""        </select>
        <input type="submit" value="Display">

    </form>""")


Qui definiamo una funzione per stampare un codice html che permetterà di avere un pulsante a cascata dove selezionare l'intervallo desiderato di analisi. si inizia da un print con tre """ per includere nella stringa anche altri apici e leggerli per quello che sono. di seguito si scrive il codice html che richiamerà un pulsante (form "action" e "method=post"). ricordarsi di usare post al posto di get per rendere più sicuro il metodo. attenzione: qui vedete /cgi-bin/temponweb.py che si riferisce alla configurazione senza suexec, dopo spiegata. avevamo creato uno script che apache eseguiva da cgi-bin. se installiamo suexec per una migliore protezione web, qui dovremo scrivere al posto di cgi-bin la directory in cui abbiamo messo il nostro script (vedrete dopo che sarà cgiexec).
Dopo scriviamo una frase che vedremo nella pagina web per spiegare  quanto apparirà e poi assegnamo al selettore il nome di "timeinterval" che verrà richiamato dalla funzione successiva. qui ora mettiamo vari if per fare delle scelte. si parte da se option non é none (cioé se option é stato assegnato) allora option potrebbe essere:
se option é "6" (cioé se assegnamo al parametro di questa funzione, option, il valore di 6) option diventa il valore "selected", cioé tra tutte le possibilità offerte dal pulsante a cascata questo valore diventa quello selezionato. quando faremo click su "the last 6 hours" (così apparirà scritto) questo rimarrà visibile. notare la scritta =\"6\" serve a far scrivere un numero come una stringa, all'interno di una stringa (prima ci sono già gli apici). se quanto detto non é vero (non abbiamo selezionato 6) scrivi nell'elenco a cascata solo "the last 6 hours". così per altre scelte che possiamo inventarci come preferiamo, sempre basate sul numero di ore.
Qui poi c'é la possibilità che "option is none" ma su questa opzione prevale quanto sopra abbiamo già stabilito: se "option is none, option=str(24)".
Qui poi stampiamo nel codice la fine del blocco "select" e successivamente assegnamo al pulsante "display" la funzione di passare quanto scelto al codice (fare il "submit" della nostra scelta).
Chiudiamo con la fine del "form".

# check that the option is valid
# and not an SQL injection
def validate_input(option_str):
    # check that the option string represents a number
    if option_str.isalnum():
        # check that the option is within a specific range
        if int(option_str) > 0 and int(option_str) <= 168:
            return option_str
        else:
            return None
    else: 
        return None


Quanto sopra é una forma di sicurezza, serve ad evitare attacchi al dbase di tipo SQL injection che sfruttano un modo di accedere al dbase sostituendosi come administrator. In pratica la funzione verifica che il dato passato sia un numero e che questo numero sia compreso in un certo range. Se modificate il valore massimo di ore, ricordate di cambiarlo anche qui (dove ora vedete 168).

#return the option passed to the script
def get_option():
    form=cgi.FieldStorage()
    if "timeinterval" in form:
        option = form["timeinterval"].value
        return validate_input (option)
    else:
        return None


Questo sopra serve a memorizzare il nostro intervallo di tempo nella variabile option che sarà utilizzata dallo script. qui si utilizza una variabile tipica dei cgi: FieldStorage. se nel form (prima scritto) c'é un timeinterval, assegniamo ad option il valore timeinterval del form. memorizziamolo nella funzione validate_input che farà il controllo sopra descritto. se non c'é un timeinterval, memorizziamo None, che diventerà poi 24 (hrs).

ora siamo al main!! il blocco che eseguirà tutte le funzioni sopra descritte.
def main():

    cgitb.enable()

    option=get_option()

    if option is None:
        option = str(24)

    records=get_data(option)

    printHTTPheader()

    if len(records) != 0:
        table=create_table(records)
    else:
        print ("No data")
        return

    print ("<html>")

    printHTMLHead("Temperatures  my home", table)
  
    print ("<body bgcolor=#000000 text=#00ff00>")
    print ("<h1>Temperatures outside my home</h1>")
    
    print ("<hr>")

    print_time_selector(option)
    
    print ("<table>")
      
    print ("<td valign=top>")
    show_graph()
    print ("<td>")
    show_stats(option)
    print ("</td>")
    print ("</table>")
    print ("</body>")
    print ("</html>")
    
    sys.stdout.flush()

if __name__=="__main__":
    main()


si comincia da una funzione la "main" che alla fine verrà eseguita con: se name é uguale a main, esegui la funzione main
("if __name__=="__main__":
    main().")

Si comincia dall'attivare il trace back, il debugging del programma: cgitb.enable.
Assegnamo ad option ciò che la funzione funzione "get_option()" passa, il suo risultato. la funzione get_option riportava, se esisteva un timeinterval, il valore di questo intervallo, altrimenti riportava none.
Difatti subito dopo il blocco verifica che se option é none, option diventa la stringa "24".
Poi assegnamo alla variabile "records" il risultato della funzione get_data(option), con il parametro prima definito di "option".
La funzione get_data eseguiva un'interrogazione nel dbase per ottenere tutti i valori di timestamp e temperature che appartengono all'intervallo definito da option (il parametro di questa funzione prima definito).
Subito dopo c'é la funzione printhttpheader che stamperà in memoria che stiamo per fare leggere al browser che si tratta di una pagina html.
A questo punto dovremo incominciare a fare scrivere il contenuto della pagina html. prima del body (non mi dilungo sul codice html) si dovrà scrivere la parte che crea il grafico, lo javascript. questo però ha bisogno dei dati in forma di tabella, allora prima prepariamo la tabella. qui in realtà c'é una condizione if, perché se i records sono a 0, cioé il database é stato appena creato e non é passata ancora la prima ora, scriveremo no data. se invece ci sono dei records, assegnamo a table il risultato della funzione create_table(records) con, come parametro quei records che abbiamo memorizzato poco prima e che si sostituiranno alle "rows". l'output della funzione create_table é una tabella, la chart_table, che servirà a google-chart. quindi, abbiamo interrogato il dbase, preso un intervallo di dati e memorizzato queste coppie (data e temperatura) in una tabella utilizzabile per il grafico.
Qui ora si comincia con la vera pagina html che sarà caricata/letta dal browser.
Si scriverà "<html>", poi si lancerà la funzione printhtmlhead con i parametri del titolo (temperature my home) e la tabella da utilizzare (table prima definita).
A sua volta la funzione che stiamo chiamando, printhtmlhead, lanciava la funzione print_graph_script (con table come tabella da utilizzare). questa funzione scriveva la parte del codice html che crea il grafico basato sui dati di "table" (non lo stiamo ancora visualizzando!).
Poi stampiamo il codice per generare il "body" della pagina.
Qui, nel mio caso, setto un background ed un colore di testo valido per tutta la pagina (i colori del grafico solamente erano settati nella funzione che genera il grafico).
Poi stampiamo il titolo della pagina web.
Stampiamo una linea orizzontale ("hr")
Lanciamo ora la funzione print_time_selector che non fa altro che fare apparire sulla pagine il selettore per definire l'intervallo di analisi.
Qui ora creiamo un blocco di tipo a tabella. questo l'ho voluto per avere il grafico a sinistra e le statistiche a destra.
Ho messo poi un allineamento della colonna del grafico al "top".
In questo "div" lanciamo la funzione show_graph() che metterà e renderà visibile il grafico creato prima.
Aggiungiamo una colonna alla tabella (<td>) e lanciamo show_stats(option). questa funzione ricorderete rendeva visibili nella pagina le statistiche basate sull'intervallo definito dal solito option.
Chiudiamo colonna, tabella, body ed html.
Qui per ultimo c'é una funzione che aiuta nello svuotamento del buffer (non la conosco a fondo quindi evito di dire cose che non so).

sotto, il grafico che appare dal web:



Siamo arrivati alla fine, questo tutorial spero possa essere utile a chi vuole avvicinarsi alla programmazione in python. sono stati usati volutamente termini poco "tecnici" ma lo scopo é prima di fare comprendere le cose poi di approfondirle.

pagina 4 del talking alarm clock on the web