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.
Sommaire
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.
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
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 »
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.
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.
Bonjour, oui probablement. L’api de la freebox offre de nombreuses capacités.
Bonjour,
Excellent tuto. J’avais fait un soft du même type pour utiliser l’API de ma freebox 4K.
Il fonctionnait très bien avec l’API en V6 mais je n’arrive plus à ouvrir une session depuis que je suis en V8, 8.3 pour être précis….
Et la documentation du SDK s’est arrêté à la V4. Est-ce que votre appli fonctionne toujours aujourd’hui?
Pour ma part l’autorisation de l’appli dans la box fonctionne bien et j’ai bien noté le token.
Mais quand je veux créer une session j’obtiens un réponse ‘invalid_token’. Je cherche depuis 2 jours sans succès…
Merci de votre aide.