tado°: dialogare con il sistema tramite Python

| |

Ammettilo, questa cosa ha un po’ il sapore del già visto e già vissuto. Parzialmente vero, rispondo io. Negli ultimi tempi mi sta capitando sempre più spesso di mettere mano a codice sorgente scritto in Python e ci sto un po’ entrando in confidenza, c’è un mondo di librerie e supporto lì fuori al quale non avevo mai dato retta.

Ci sono cose che in Python puoi risolvere più facilmente che in altra maniera, perché quindi non approfittarne considerato che ormai spesso delego dei compiti ripetitivi e automatizzati al Raspberry Pi o al NAS? Questa è quindi la naturale evoluzione di ciò che avevo scritto in passato (tado°: dialogare con il sistema tramite bash) e che oggi approda su codice Python, decisamente più snello e ottimizzato (spero e credo).

Si riparte

Io parto dando per scontato che i fondamentali tu li abbia affrontati nel vecchio articolo (che rimane comunque valido). Se così non fosse, credo che dargli un’occhiata possa essere un’ottima idea:

tado°: dialogare con il sistema tramite bash

Detto ciò, quello che forse ti interesserà sapere, è che quel “clientSecret” presente nel file my.tado.com/webapp/env.js non è mai cambiato. Forse resterà tale anche in futuro, al momento ho scelto di inserirlo all’interno delle variabili dello script in maniera secca, non recuperata dinamicamente dallo script stesso. Se un domani cambierà, vorrà dire che sarà cura dell’utilizzatore andare a modificare il valore della variabile contenuta nel file di .env sul sistema che fa uso dello script Python che spiegherò di seguito.

Lo script in Python

Non ti far spaventare. Preferisco stavolta portarti immediatamente allo script e poi spiegartelo passo-passo.

from dotenv import load_dotenv, find_dotenv
from libtado import api
from fritzconnection.lib.fritzhosts import FritzHosts
import datetime
import requests
import base64
import os

load_dotenv(find_dotenv())
TADO_USER = os.environ.get("TADO_USER")
TADO_PWD = os.environ.get("TADO_PWD")
TADO_TOKEN = os.environ.get("TADO_TOKEN")
FRITZ_IP = os.environ.get("FRITZ_IP")
MACS = os.environ.get("MACS")
BOT_TOKEN = os.environ.get("BOT_TOKEN")
CHAT_ID = os.environ.get("CHAT_ID")

MSG_TADO_HOME = "tado° in modalità Home. Bentornati nell'area di casa 🏠 (%s)" % datetime.datetime.now().strftime("%d/%m/%Y alle %H:%M:%S") # https://stackoverflow.com/a/7999977
MSG_TADO_AWAY = "tado° in modalità Away. Abbasso il riscaldamento, a più tardi 👋 (%s)" % datetime.datetime.now().strftime("%d/%m/%Y alle %H:%M:%S")

def sendtotelegram(token,chatid,message):
    url = "https://api.telegram.org/bot%s/sendMessage?chat_id=%s&text=%s" % (token, chatid, message)
    response = requests.post(url)
    return response

def get_home_state(TADO_USER, TADO_PWD, TADO_TOKEN):
    homestate = api.Tado(TADO_USER, TADO_PWD, TADO_TOKEN)
    response = homestate.get_home_state()
    return list(response.values())[0]

def set_home_state(TADO_USER, TADO_PWD, TADO_TOKEN, STATE):
    homestate = api.Tado(TADO_USER, TADO_PWD, TADO_TOKEN)
    if str(STATE) == "True":
        homestate.set_home_state(True)
    if str(STATE) == "False":
        homestate.set_home_state(False)
    return

def main():
    if TADO_USER is None or TADO_PWD is None or TADO_TOKEN is None or FRITZ_IP is None or MACS is None:
        print("Environment variables have not been loaded!")
        return

    fh = FritzHosts(address=FRITZ_IP)
    macaddresses = os.environ['MACS'].split() # https://stackoverflow.com/a/54027239
    presence = 0
    homestate = get_home_state(TADO_USER, TADO_PWD, TADO_TOKEN)
    for mac in macaddresses:
        check = fh.get_host_status(mac)
        print("Check", mac, ":", check)
        if str(check) == "True":
            presence += 1
    print("Check presence:", presence)
    if presence > 0:
        if str(homestate) == "AWAY":
            print("Found at least one device in home, set tado° to HOME")
            sendtotelegram(BOT_TOKEN,CHAT_ID,MSG_TADO_HOME)
            set_home_state(TADO_USER, TADO_PWD, TADO_TOKEN, True)
    else:
        if str(homestate) == "HOME":
            print("No devices found in home, set tado° to AWAY")
            sendtotelegram(BOT_TOKEN,CHAT_ID,MSG_TADO_AWAY)
            set_home_state(TADO_USER, TADO_PWD, TADO_TOKEN, False)

main()

Le variabili usate

Ho fatto uso ancora una volta del file .env per indicare le variabili richiamate poi all’interno dello script Python ogni volta che c’è stata la necessità. L’ho già fatto negli ultimi repository pubblicati su GitHub (tipo netflix-leaving e spotify-save-new-music-friday) e anche stavolta non si fa eccezione. Il file è strutturato banalmente in questa maniera (e si deve trovare nella stessa identica cartella in cui risiede lo script Python):

TADO_USER=user@contoso.com
TADO_PWD=MySuperSecretPassword
TADO_TOKEN=wZaRN7rpjn3FoNyF5IFuxg9uMzYJcvOoQ8QWiIqS3hfk6gLhVlG57j5YNoZL2Rtc
FRITZ_IP=192.168.178.1
MACS=XX:XX:XX:XX:XX:XX YY:YY:YY:YY:YY:YY ZZ:ZZ:ZZ:ZZ:ZZ:ZZ
BOT_TOKEN=MyTelegramBotToken
CHAT_ID=-000000000
  • TADO_USER*: è l’utente con il quale sei registrato su my.tado.com.
  • TADO_PWD*: devo seriamente specificarlo?
  • TADO_TOKEN*: il clientSecret recuperato da my.tado.com/webapp/env.js.
  • FRITZ_IP*: l’IP del tuo router casalingo.
  • MACS*: uno o più MAC Address da verificare (se sono collegati o meno alla WiFi / LAN casalinga), da specificare secondo la formula nell’esempio, quindi distanziati l’uno dall’altro semplicemente con un colpo di barra spaziatrice, non c’è un reale limite al numero massimo di MAC Address controllabili.
  • BOT_TOKEN: ho voluto mantenere l’uso dell’alert inviato su Telegram, quindi questa variabile contiene il token che mi permette di lavorare con il bot di casa.
  • CHAT_ID: completa quanto detto poco sopra. L’ID del gruppo / utente con cui far comunicare il bot Telegram in caso di alert.

Tutti i parametri con * vicino sono obbligatori, gli altri puoi bellamente ignorarli se modifichi un pelo lo script, ti spiego come tra poco.

All’interno dello script Python trovi anche le due variabili relative ai messaggi da spedire via Telegram (MSG_TADO_AWAY e MSG_TADO_HOME), puoi ritoccarli secondo tua preferenza, occhio solo a non toccare le variabili richiamate.

Librerie usate

Tralasciando quelle più d’uso comune, in questo script ho integrato due librerie importanti per parlare con il router FRITZ!Box e con il sistema API di tado°. Si tratta rispettivamente della fritzconnection e della libtado, entrambe disponibili sia su GitHub (github.com/kbr/fritzconnection e github.com/germainlefebvre4/libtado) che su pypi.org (pypi.org/project/fritzconnection e pypi.org/project/libtado), installabili quindi tramite Pip. Ho preferito raccogliere i requisiti necessari all’interno di un file requirements.txt che puoi quindi richiamare facilmente tramite pip install -r requirements.txt:

certifi==2020.12.5
chardet==4.0.0
idna==2.10
python-dotenv==0.16.0
requests==2.25.1
urllib3==1.26.5
libtado==3.2.3
fritzconnection==1.9.1

Chi, cosa, come

Le due librerie godono di ottima documentazione disponibile online, rispondono rispettivamente all’URL fritzconnection.readthedocs.io/en/1.9.1/index.htmllibtado.readthedocs.io/index.html. In linea di massima si può anche effettuare una connessione di test per entrambe, utilizzando gli snippet di codice proposti nei repository GitHub. Per il sistema tado° si può effettuare connessione tramite username e password (e clientSecret) e farsi rispondere con i dettagli sull’account, sulla casa, sulle zone e sullo stato, il tutto con queste poche righe di codice:

import libtado.api
t = api.Tado('my@email.com', 'myPassword', 'client_secret')
print(t.get_me())
print(t.get_home())
print(t.get_zones())
print(t.get_state(1))

Per quanto invece riguarda il router FRITZ!Box in uso, puoi per esempio chiedere di conoscere il modello rilevato dalla libreria, così (inutile dire che al posto di 192.168.178.1 andrà indicato il reale IP assegnato al tuo router casalingo, giusto?):

from fritzconnection import FritzConnection
fc = FritzConnection(address='192.168.178.1')
print(fc)  # print router model informations

Se entrambe le connessioni di test vanno a buon fine, direi che sei già a metà dell’opera :-)

Lo scopo del gioco

È capire se in casa c’è almeno un dispositivo riconosciuto (il suo MAC Address è stato specificato nella lista MACS) e mantenere quindi acceso il sistema di riscaldamento, in posizione Home e non Away. Per farlo mi sono basato su un ragionamento tanto stupido quanto efficace: se il conteggio dispositivi è maggiore di zero allora resta in “Home“, altrimenti spostati in “Away“:

presence = 0
for mac in macaddresses:
        check = fh.get_host_status(mac)
        print("Check", mac, ":", check)
        if str(check) == "True":
            presence += 1
print("Check presence:", presence)

Quella variabile “presence” precedentemente impostata a zero (prima del controllo dei MAC) potrà uscire dal check pari ancora a zero (quindi Away) o pari/maggiore di uno (quindi Home). Le chiamate di check al router le faccio tramite l’API get_host_status, quelle al sistema tado° sono due e rispondono al get_home_state e (con molta poca fantasia) al set_home_state per modificare lo stato di presenza automaticamente tramite script.

Non uso e non voglio Telegram

Non c’è problema, le variabili relative al token del bot e della chat in cui inviare gli alert sono infatti facoltative. Nello script però non ho attualmente previsto un controllo che tenga conto variabili volutamente lasciate vuote per evitare di richiamare la funzione di invio messaggio a Telegram. Se vuoi togliere di mezzo questo passaggio, ti basterà commentare le due chiamate alla funzione “sendtotelegram“, parlo di queste:

sendtotelegram(BOT_TOKEN,CHAT_ID,MSG_TADO_HOME)
sendtotelegram(BOT_TOKEN,CHAT_ID,MSG_TADO_AWAY)

Aggiungi un cancelletto prima (esempio: #sendtotelegram(BOT_TOKEN,CHAT_ID,MSG_TADO_AWAY)) e avrai così commentato la riga. Lo script funzionerà tranquillamente ma non proverà neanche a chiamare in causa Telegram.

Automatizza e concludi

Lo script è pronto, se lo lanci funziona e sei ora un bambino contento, mi fa piacere :-)

Cosa manca? Crontab. Uno script senza un’esecuzione programmata e ripetuta nel tempo è un po’ un ammasso di codice fermo al palo delle inutilità. A questo punto dovrai quindi decidere ogni quanto tempo eseguire questo controllo sul tuo Raspberry Pi (o altra macchina equivalente, poco importa, basta avere qualcosa di acceso e che possa eseguire Python), io ho scelto di farlo fare al mio RPi ogni 5 minuti:

*/5 * * * * /usr/local/bin/python3.8 /home/pi/Scripts/Tado/tado.py

Nel tuo caso quasi certamente cambierà la posizione dello script e magari anche quella di Python, dovrai quindi ritoccare un pelo quanto riportato sopra, in ogni caso dovrebbe essere abbastanza chiaro come comportarsi. Se vuoi essere certo che lo script vada in esecuzione puoi modificarlo e aggiungere queste righe di codice prima di main():

# Debug Crontab
with open('/home/pi/Scripts/Tado/debug.txt', 'w') as f:
 f.write(datetime.datetime.now().strftime("%d/%m/%Y alle %H:%M:%S"))

Anche in questo caso potrebbe cambiare la posizione del file di testo da scrivere, occhio quindi a non prendere per “bibbia” cosa ti riporto io a titolo di esempio. Andando a sbirciare il file di debug troverai la data e l’ora dell’ultima esecuzione del controllo. Puoi in qualsiasi momento – se lo reputi necessario – cancellare le righe di codice relative al Debug Crontab e dormire sonni tranquilli (e caldi, d’inverno).

L’articolo si conclude qui. Se vuoi – come sempre – l’area commenti è a tua totale disposizione per dubbi, ulteriori informazioni o suggerimenti riguardo possibili miglioramenti. Io presto pubblicherò il tutto su GitHub, aggiornerò quindi questo articolo. Nel frattempo ho disattivato questi controlli su IFTTT e presto metterò le mani anche ai controlli Arlo per spegnere le ulteriori Applet e tornare all’abbonamento gratuito di base, portando in casa tutto ciò che mi serve tenere d’occhio.

#StaySafe


Riconoscimenti (oltre quelli già riportati nell’articolo):
w3schools.com/python/python_dictionaries.asp
w3schools.com/python/gloss_python_array_loop.asp
w3schools.com/python/python_conditions.asp
stackoverflow.com/a/2632687
Gioxx's Wall

Se hai correzioni o suggerimenti puoi lasciare un commento nell'apposita area qui di seguito o contattarmi privatamente.
Ti è piaciuto l'articolo? Offrimi un caffè! Satispay / PayPal / Buy Me A Coffee / Patreon

Condividi l'articolo con i tuoi contatti:
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

9 Commenti
Oldest
Newest Most Voted
Inline Feedbacks
View all comments