Pilotage de la Freebox Révolution en python

J’ai découvert récemment que la Freebox Révolution exposait une API donnant accès à beaucoup de fonctionnalités intéressantes. Je m’en suis servi pour ajouter à ma tablette murale la capacité d’activer et désactiver la planification du wifi.

 

Mon besoin

Mon besoin était le suivant : J’utilise la planification du wifi offerte par la Freebox pour couper le wifi pendant les repas, ce n’est pas tant l’usage du wifi que je voulais couper (car mes enfants n’ont pas encore l’age d’utiliser Internet) mais surtout l’exposition aux ondes (ma box est très près de la table dans la cuisine). Mais une fois le repas terminé, je réactivais souvent le wifi en désactivant la planification via l’appli mobile Freebox Compagnon, en oubliant parfois de réactiver la planification…

J’ai donc ajouté à l’interface de ma tablette dans la cuisine un bouton me permettant de désactiver la planification du wifi (et ainsi réactiver le wifi). Une tâche cron régulière (10 min) se chargeant ensuite de réactiver la planification dès la fin de la période de coupure du wifi.

 

Présentation de l’API

Les fonctionnalités offertes par l’API son très riches. Cela va de la gestion des téléchargements, des fichiers stockés sur le disque dur interne à la configuration des paramètres de la freebox (serveur FTP, firewall, port forwarding ou ce qui nous intéresse ici le Wifi).

L’API est un webservice de type RESTful dont l’url sur le réseau local est https://mafreebox.freebox.fr/api/v4/.

v4 étant la version de l’API à la date d’écriture de cet article.

 

Enregistrement de notre application auprès de notre Freebox

La première étape pour effectuer des requêtes RESTful à notre Freebox est de déclarer notre application auprès de celle-ci. Ceci consiste à renseigner un app_id, un app_name et un device_name pour obtenir en retour un app_token qui sera utilisé lors des requêtes en guise d’authentification.  Nous devrons activer cet app_token en effectuant une manipulation physique sur la Freebox).

Voici le code que j’ai écris en Python 3 pour créer un accès à mon application dans la Freebox :

import datetime
import hashlib
import hmac
import json
import pickle
import time

import requests
import urllib3

URL_BASE = 'https://mafreebox.freebox.fr/api/v4/'
APP_ID = 'fr.freebox.fbxpy'
APP_NAME = 'Fbxpy'
APP_VERSION = '1'
DEVICE_NAME = 'pi'

TOKEN = ''
TRACK_ID = ''

def fancy_print(data):
    print(json.dumps(data, indent=2, separators=(',', ': ')))

def connexion_post(method, data=None, session=None):
    url = URL_BASE + method
    if data: data = json.dumps(data)
    return json.loads(session.post(url, data=data).text)

def connexion_get(method, session=None):
    url = URL_BASE + method
    return json.loads(session.get(url).text)

def register():
    global TOKEN, TRACK_ID
    payload = {'app_id': APP_ID, 'app_name': APP_NAME, 'app_version': APP_VERSION, 'device_name': DEVICE_NAME}
    content = connexion_post('login/authorize/', payload)
    fancy_print(content)
    TOKEN = str(content["result"]["app_token"])
    TRACK_ID = str(content["result"]["track_id"])

register()

 

L’exécution de la méthode register va déclarer notre application Fbxpi à la Freebox et afficher l’app_token ainsi reçu. Conservez le, nous en aurons besoin par la suite. Pour terminer l’autorisation d’accès à l’API par notre application il faut se rendre physiquement sur la Freebox Révolution. Celle-ci affiche un message demandant si elle doit autoriser l’accès à l’API à notre application.

Demande activation Freebox

 

Ceci fait, on peut vérifier la création de notre accès sur l’interface web de gestion de la Freebox (Paramètres de la Freebox > Gestion des accès).

Il faut à présent autoriser notre application Fbxpi à effectuer des modifications des réglages de la Freebox, pour cela :

  • Aller dans « Freebox OS/Paramètres de la Freebox/Mode Simplifié/Gestion des accès/Applications/Fbxpy »
  • Cliquer sur l’icône « Editer »
  • Cocher « Modification des réglages de la Freebox »

 

Authentification

Notre accès à l’API est créé et autorisé. Mais avant de pouvoir effectuer des requêtes via l’API nous devons utiliser le token reçu pour demander un session_token. Celui ci sera utilisé dans chacune de nos requêtes. Ce session_token est limité dans le temps, il faudra en redemander un à son expiration.

La méthode create_session() suivante permet de créer ce session_token et le conserver pour nos requêtes futures :

TOKEN = '' # Insérer ici le token reçu précédemment

def create_session():
    global TOKEN
    session = requests.session()
    session.verify = False
    challenge = str(connexion_get("login/", session)["result"]["challenge"])
    token_bytes = bytes(TOKEN, 'latin-1')
    challenge_bytes = bytes(challenge, 'latin-1')
    password = hmac.new(token_bytes, challenge_bytes, hashlib.sha1).hexdigest()
    data = {
        "app_id": APP_ID,
        "app_version": APP_VERSION,
        "password": password
    }
    content = connexion_post("login/session/", data, session)
    session.headers = {"X-Fbx-App-Auth": content["result"]["session_token"]}
    return session

 

Cette méthode sera à appeler au début de notre programme.

 

Pilotage du wifi de la Freebox

Passons aux choses sérieuses et utilisons tout ceci pour piloter le wifi de notre Freebox :

def get_wifi_planning_state(session):
    try:
        method = "/wifi/planning/"
        result = connexion_get(method, session)
        if result["result"]["use_planning"]:
            return 'true'
        else:
            return 'false'
    except:
        return 'unknown'

def get_wifi_state(session):
    try:
        method = "wifi/ap/"
        result = connexion_get(method, session)
        if result["result"][0]["status"]["state"] == 'active':
            if get_wifi_planning_state(session):
                return 'active_planning'
            else:
                return 'active'
        if result["result"][0]["status"]["state"]:
            return 'true'
        else:
            return 'false'
    except:
        return 'unknown'

session = create_session()
state = get_wifi_state(session)

 

Notre méthode get_wifi_state retournera :

  • active si le wifi est activé
  • active_planif si le wifi est activé et que la planification est également active (et donc que nous sommes dans une plage horaire où le wifi doit être activé)
  • disabled si le wifi est désactivé
  • scanning si le wifi vient d’être activé et scanne les différents canaux en vue de chercher les meilleurs canaux non encombrés
  • no_param si le wifi n’est pas configuré
  • bad_param si le wifi est mal configuré
  • disabled_planning si le wifi est désactivé du fait de l’application de la planification
  • no_active_bss si le BSS configuré n’est pas trouvé
  • starting si le wifi est en phase d’activation
  • acs si le wifi est en train de chercher les meilleurs canaux non encombrés
  • ht_scan si le wifi est en train de chercher à se connecter à un autre point d’accès
  • dfs si le wifi est en train de sélectionner le bon jeu de fréquences
  • failed si l’activation du wifi a échouée

Je vous recommande de décommenter la ligne fancy_print(resultat) qui permet de visualiser la réponse brute reçue de l’API.

En fonction de l’état du wifi nous pouvons demander à désactiver la planification du wifi pour forcer son activation si nous sommes dans une plage horaire sans wifi :

def set_wifi_planning_state(value, session):
    try:
        method = "/wifi/planning/"
        data = {
            "use_planning": value
        }
        result = connexion_put(method, data, session)
        if result["success"]:
            if result["result"]["use_planning"]:
                return 'true'
            else:
                return 'false'
        else:
            return 'false'
        except:
            return 'unknown'

session = create_session()
set_wifi_planning_state(False, session)

 

C’est cette méthode qui est déclenchée via ma tablette quand je veux réactiver le wifi. Une tâche cron se chargeant de réactiver le wifi lors de la fin de la plage horaire sans wifi.

Voici l’ensemble final que j’ai placé dans un module nommé Fbxpy.py :

import datetime
import hashlib
import hmac
import json
import pickle
import time
from threading import Thread, Lock

import requests
import urllib3

URL_BASE = 'https://mafreebox.freebox.fr/api/v4/'
APP_ID = 'fr.freebox.fbxpy'
APP_NAME = 'Fbxpy'
APP_VERSION = '1'
DEVICE_NAME = 'pi'

TOKEN = ''
TRACK_ID = ''
SESSION_TOKEN = ''

FILE_WIFI_PLANNING = ''

urllib3.disable_warnings()


class Fbxpy():
    def __init__(self):
        super().__init__()
        self.lock = Lock()
        self.current_session = None
        self.last_use = 0

    def get_session(self):
        if self.current_session is None:
            self.create_session()
        return self.current_session

    def check_time(self):
        can_continue = True
        while can_continue:
            time.sleep(10)
            if self.current_session is None:
                can_continue = False
            elif (time.time() - self.last_use) >= 60:
                can_continue = False
                self.close_session()

    def fancy_print(self, data):
        print(json.dumps(data, indent=2, separators=(',', ': ')))

    def connexion_post(self, method, data=None):
        url = URL_BASE + method
        if data: data = json.dumps(data)
        with self.lock:
            result = json.loads(self.get_session().post(url, data=data).text)
            self.last_use = time.time()
        return result

    def connexion_post_without_connection(self, method, data=None, session=None):
        url = URL_BASE + method
        if data: data = json.dumps(data)
        return json.loads(session.post(url, data=data).text)

    def connexion_get(self, method):
        url = URL_BASE + method
        with self.lock:
            result = json.loads(self.get_session().get(url).text)
            self.last_use = time.time()
        return result

    def connexion_get_without_connection(self, method, session):
        url = URL_BASE + method
        return json.loads(session.get(url).text)

    def connexion_put(self, method, data=None):
        url = URL_BASE + method
        if data: data = json.dumps(data)
        with self.lock:
            result = json.loads(self.get_session().put(url, data=data).text)
            self.last_use = time.time()
        return result

    def register(self):
        global TOKEN, TRACK_ID
        payload = {'app_id': APP_ID, 'app_name': APP_NAME, 'app_version': APP_VERSION, 'device_name': DEVICE_NAME}
        content = self.connexion_post('login/authorize/', payload)
        self.fancy_print(content)
        TOKEN = str(content["result"]["app_token"])
        TRACK_ID = str(content["result"]["track_id"])

    def progress(self):
        content = self.connexion_get('login/authorize/' + TRACK_ID)
        self.fancy_print(content)

    def pause(self):
        print("pause...")
        self.raw_input()
        print("\n")

    def create_session(self):
        global TOKEN
        session = requests.session()
        session.verify = False
        challenge = str(self.connexion_get_without_connection("login/", session)["result"]["challenge"])
        token_bytes = bytes(TOKEN, 'latin-1')
        challenge_bytes = bytes(challenge, 'latin-1')
        password = hmac.new(token_bytes, challenge_bytes, hashlib.sha1).hexdigest()
        data = {
            "app_id": APP_ID,
            "app_version": APP_VERSION,
            "password": password
        }
        content = self.connexion_post_without_connection("login/session/", data, session)
        session.headers = {"X-Fbx-App-Auth": content["result"]["session_token"]}
        self.current_session = session
        self.last_use = time.time()
        Thread(None, self.check_time, 'check_time_thread', (), {}).start()
        return session

    def close_session(self):
        self.connexion_post('login/logout/')
        self.current_session = None

    def load_wifi_planning_file(self):
        try:
            infile = open(FILE_WIFI_PLANNING, 'rb')
            wifi_planning = pickle.load(infile)
            infile.close()
            return wifi_planning
        except:
            time.sleep(5)
            return self.load_wifi_planning_file()

    def save_wifi_planning_file(self, wifiplanif):
        try:
            outfile = open(FILE_WIFI_PLANNING, 'wb')
            pickle.dump(wifiplanif, outfile)
            outfile.close()
        except:
            time.sleep(5)
            self.save_wifi_planning_file(wifiplanif)

    def get_wifi_state(self):
        try:
            method = "wifi/ap/"
            result = self.connexion_get(method)
            if result["result"][0]["status"]["state"] == 'active':
                if self.get_wifi_planning_state():
                    return 'active_planif'
                else:
                    return 'active'
            if result["result"][0]["status"]["state"]:
                return 'true'
            else:
                return 'false'
        except:
            return 'unknown'

    def get_wifi_planning_state(self):
        try:
            method = "/wifi/planning/"
            result = self.connexion_get(method)
            if result["result"]["use_planning"]:
                return 'true'
            else:
                return 'false'
        except:
            return 'unknown'

    def set_wifi_planning_state(self, value):
        try:
            method = "/wifi/planning/"
            data = {
                "use_planning": value
            }
            result = self.connexion_put(method, data=data)
            if result["success"]:
                if result["result"]["use_planning"]:
                    return 'true'
                else:
                    return 'false'
            else:
                return 'false'
        except:
            return 'unknown'

    def is_wifi_must_enable_by_planning(self):
        try:
            method = "/wifi/planning/"
            result = self.connexion_get(method)
            date = datetime.datetime.now()
            interval = date.weekday() * 48 + date.hour * 2 + (int(date.minute) // 30)
            if result["result"]["mapping"][interval] == "on":
                return 'true'
            else:
                return 'false'
        except:
            return 'unknown'

    def active_wifi(self):
        method = "wifi/config/"
        data = {
            "enabled": True
        }
        result = self.connexion_put(method, data=data)
        if result["success"]:
            self.save_active_wifi_file(int(time.time()))
            return 'true'
        else:
            return 'false'

    def stop_wifi(self):
        try:
            method = "wifi/config/"
            data = {
                "enabled": False
            }
            result = self.connexion_put(method, data=data)
            if result["success"]:
                if not result["result"]["enabled"]:
                    self.save_active_wifi_file(0)
                    return 'true'
                else:
                    return 'false'
            else:
                return 'false'
        except:
            return 'unknown'

singleton = Fbxpy()


def get_singleton():
    return singleton

4 réactions sur “ Pilotage de la Freebox Révolution en python ”

  1. Philippe Garnier Réponse

    Bonjour Clément,

    Un grand merci pour cet excellent document (très didactique et concis à la fois). Un vrai régal! Je me permettrai juste un commentaire: peut-être préciser que l’on doit modifier les paramètres de la Freebox pour faire des PUT/POST. Pour cela:
    – aller dans “Freebox OS/Paramètres de la Freebox/Mode Simplifié/Gestion des accès/Applications/Fbxpy”
    – cliquer sur l’icône “Editer”
    – cocher “Modification des réglages de la Freebox”

    • Clément Caillaud Auteur ArticleRéponse

      Merci Philippe, bien vu j’ai ajouté la précision. J’en ai profité également pour mettre à jour le code en utilisant l’objet Session qui permet notamment de conserver la connexion TCP entre les requêtes.

  2. Regnier Réponse

    Bonjour,je possède une box cozytouch et son application qui pilotent la températures de mes nouveaux radiateurs.La box de pilotage est branchée a ma freebox rèv. en permanence.Peut t’on créer une api qui peut fonctionner avec freebox compagnion?
    p.s qu’elle m’indique une coupure de courant ou autre par exemple?
    Cordialement.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *