Forum Programmation.python Contrôle de FreeboxOS par commande en ligne : wifi, reboot ...

Posté par . Licence CC by-sa
12
15
août
2013

Bonjour,

Ce sujet fait suite au premier sujet que j'avais initialement créé pour contrôler le wifi de la freebox v6.
Vous en retrouverez le fil ici : https://linuxfr.org/forums/programmation-shell/posts/controle-de-la-freebox-v6-par-le-shell-wifi-reboot

Depuis FreeboxOS est arrivé et propose une API de contrôle du Freebox server.

Voici donc un utilitaire en ligne de commandes utilisant cette API et qui permet :
- d'activer/désactiver le wifi
- de rebooter le Freebox Server

Par exemple, on pourra lancer cet utilitaire depuis la crontab pour activer/désactiver le wifi selon la plage horaire voulue.

Attention, il y a deux dépendances à installer pour ce script sinon une exception est levée à l'exécution : python-requests et python-simplejson :

apt-get install python-requests python-simplejson

Une version plus récente du script est disponible sous github, sous mon pseudo : skimpax.

Le script fbxosctrl.py :

#! /usr/bin/env python

""" This utility handles some FreeboxOS commands which are sent to a
freebox server to be executed within FreeboxOS app.
Supported services:
  - set wifi ON
  - set wifi OFF
  - reboot the Freebox Server

Note: once granted, this app must have 'settings' permissions set
 to True in FreeboxOS webgui to be able to modify the configuration. """

import sys
import os
import argparse
import requests
import hmac
import simplejson as json
from hashlib import sha1


# fbxosctrl is a command line utility to get/set dialogs with FreeboxOS
#
# Copyright (C) 2013 Christophe Lherieau (aka skimpax)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# FreeboxOS API is available here: http://dev.freebox.fr/sdk/os


########################################################################
# Configure parameters below on your own configuration
########################################################################

# your own password configured on your Freebox Server
MAFREEBOX_PASSWD = 'xxxxxxxxxx'

# Set to True to enable logging to stdout
gVerbose = False


########################################################################
# Nothing expected to be modified below this line... unless bugs fix ;-)
########################################################################

FBXOSCTRL_VERSION = "1.0.0"

__author__ = "Christophe Lherieau (aka skimpax)"
__copyright__ = "Copyright 2013, Christophe Lherieau"
__credits__ = []
__license__ = "GPL"
__version__ = FBXOSCTRL_VERSION
__maintainer__ = "skimpax"
__email__ = "skimpax@gmail.com"
__status__ = "Production"


# Return code definitions
RC_OK = 0
RC_WIFI_OFF = 0
RC_WIFI_ON = 1

# Descriptor of this app presented to FreeboxOS server to be granted
gAppDesc = {
    "app_id": "fr.freebox.fbxosctrl",
    "app_name": "Skimpax FbxOSCtrl",
    "app_version": FBXOSCTRL_VERSION,
    "device_name": "FbxOS Client"
}


def log(what):
    """ Log to stdout if verbose mode is enabled """
    if True == gVerbose:
        print what


class FbxOSException(Exception):

    """ Exception for FreeboxOS domain """

    def __init__(self, reason):
        self.reason = reason

    def __str__(self):
        return self.reason


class FreeboxOSCtrl:

    """ This class handles connection and dialog with FreeboxOS thanks to
    its exposed REST API """

    def __init__(self, fbxAddress="http://mafreebox.freebox.fr",
                 regSaveFile="fbxosctrl_registration.txt"):
        """ Constructor """
        self.fbxAddress = fbxAddress
        self.isLoggedIn = False
        self.registrationSaveFile = regSaveFile
        self.registration = {'app_token': '', 'track_id': None}
        self.challenge = None
        self.sessionToken = None
        self.permissions = None
        self._loadRegistrationParams()

    def _saveRegistrationParams(self):
        """ Save registration parameters (app_id/token) to a local file """
        log(">>> _saveRegistrationParams")
        with open(self.registrationSaveFile, 'wb') as outfile:
            json.dump(self.registration, outfile)

    def _loadRegistrationParams(self):
        log(">>> _loadRegistrationParams")
        if os.path.exists(self.registrationSaveFile):
            with open(self.registrationSaveFile) as infile:
                self.registration = json.load(infile)

    def _login(self):
        """ Login to FreeboxOS using API credentials """
        log(">>> _login")
        if not self.isLoggedIn:
            if not self.isRegistered():
                raise FbxOSException("This app is not registered yet: you have to register it first!")

            # 1st stage: get challenge
            url = self.fbxAddress + "/api/v1/login/"
            # GET
            log("GET url: %s" % url)
            r = requests.get(url, timeout=3)
            log("GET response: %s" % r.text)
            # ensure status_code is 200, else raise exception
            if requests.codes.ok != r.status_code:
                raise FbxOSException("Get error: %s" % r.text)
            # rc is 200 but did we really succeed?
            resp = json.loads(r.text)
            #log("Obj resp: %s" % resp)
            if resp['success']:
                if not resp['result']['logged_in']:
                    self.challenge = resp['result']['challenge']
            else:
                raise FbxOSException("Challenge failure: %s" % resp)

            # 2nd stage: open a session
            global gAppDesc
            apptoken = self.registration['app_token']
            key = self.challenge
            log("challenge: " + key + ", apptoken: " + apptoken)
            # Hashing token with key
            h = hmac.new(apptoken, key, sha1)
            password = h.hexdigest()
            url = self.fbxAddress + "/api/v1/login/session/"
            headers = {'Content-type': 'application/json',
                       'charset': 'utf-8', 'Accept': 'text/plain'}
            payload = {'app_id': gAppDesc['app_id'], 'password': password}
            #log("Payload: %s" % payload)
            data = json.dumps(payload)
            log("POST url: %s  data: %s" % (url, data))
            # post it
            r = requests.post(url, data, headers=headers, timeout=3)
            # ensure status_code is 200, else raise exception
            log("POST response: %s" % r.text)
            if requests.codes.ok != r.status_code:
                raise FbxOSException("Post response error: %s" % r.text)
            # rc is 200 but did we really succeed?
            resp = json.loads(r.text)
            #log("Obj resp: %s" % resp)
            if resp['success']:
                self.sessionToken = resp['result']['session_token']
                self.permissions = resp['result']['permissions']
                log("Permissions: %s" % self.permissions)
                if not self.permissions['settings']:
                    print "Warning: permission 'settings' has not been allowed yet \
                    in FreeboxOS server. This script may fail!"
            else:
                raise FbxOSException("Session failure: %s" % resp)
            self.isLoggedIn = True

    def _logout(self):
        """ logout from FreeboxOS """
        # Not documented yet in the API
        log(">>> _logout")
        if self.isLoggedIn:
            url = self.fbxAddress + "/api/v1/login/logout/"
            # POST
            log("POST url: %s" % url)
            r = requests.post(url, timeout=3)
            log("POST response: %s" % r.text)
            # ensure status_code is 200, else raise exception
            if requests.codes.ok != r.status_code:
                raise FbxOSException("Post error: %s" % r.text)
            # rc is 200 but did we really succeed?
            resp = json.loads(r.text)
            #log("Obj resp: %s" % resp)
            if not resp['success']:
                raise FbxOSException("Logout failure: %s" % resp)
        self.isLoggedIn = False

    def _setWifiStatus(self, putOn):
        """ Utility to activate or deactivate wifi radio module """
        log(">>> _setWifiStatus")
        self._login()
        # PUT wifi status
        headers = {'X-Fbx-App-Auth': self.sessionToken, 'Accept': 'text/plain'}
        if putOn:
            data = {'ap_params': {'enabled': True}}
        else:
            data = {'ap_params': {'enabled': False}}
        url = self.fbxAddress + "/api/v1/wifi/config/"
        log("PUT url: %s data: %s" % (url, json.dumps(data)))
        # PUT
        try:
            r = requests.put(url, data=json.dumps(data), headers=headers, timeout=1)
            log("PUT response: %s" % r.text)
        except requests.exceptions.Timeout as timeoutExcept:
            if not putOn:
                # If we are connected using wifi, disabling wifi will close connection
                # thus PUT response will never be received: a timeout is expected
                print "Wifi is now OFF"
                return 0
            else:
                # Forward timeout exception as should not occur
                raise timeoutExcept
        # Response received
        # ensure status_code is 200, else raise exception
        if requests.codes.ok != r.status_code:
            raise FbxOSException("Put error: %s" % r.text)
        # rc is 200 but did we really succeed?
        resp = json.loads(r.text)
        #log("Obj resp: %s" % resp)
        isOn = False
        if True == resp['success']:
            if resp['result']['ap_params']['enabled']:
                print "Wifi is now ON"
                isOn = True
            else:
                print "Wifi is now OFF"
        else:
            raise FbxOSException("Challenge failure: %s" % resp)
        self._logout()
        return isOn

    def hasRegistrationParams(self):
        """ Indicate whether registration params look initialized """
        log(">>> hasRegistrationParams")
        return None != self.registration['track_id'] and '' != self.registration['app_token']

    def getRegistrationStatus(self):
        """ Get the current registration status thanks to the track_id """
        log(">>> getRegistrationStatus")
        if self.hasRegistrationParams():
            url = self.fbxAddress + \
                "/api/v1/login/authorize/%s" % self.registration['track_id']
            log(url)
            # GET
            log("GET url: %s" % url)
            r = requests.get(url, timeout=3)
            log("GET response: %s" % r.text)
            # ensure status_code is 200, else raise exception
            if requests.codes.ok != r.status_code:
                raise FbxOSException("Get error: %s" % r.text)
            resp = json.loads(r.text)
            return resp['result']['status']
        else:
            return "Not registered yet!"

    def isRegistered(self):
        """ Check that the app is currently registered (granted) """
        log(">>> isRegistered")
        if self.hasRegistrationParams() and 'granted' == self.getRegistrationStatus():
            return True
        else:
            return False

    def registerApp(self):
        """ Register this app to FreeboxOS to that user grants this apps via Freebox Server
        LCD screen. This command shall be executed only once. """
        log(">>> registerApp")
        register = True
        if self.hasRegistrationParams():
            status = self.getRegistrationStatus()
            if 'granted' == status:
                print "This app is already granted on Freebox Server (app_id = %s). You can now dialog with it." % self.registration['track_id']
                register = False
            elif 'pending' == status:
                print "This app grant is still pending: user should grant it on Freebox Server lcd/touchpad (app_id = %s)." % self.registration['track_id']
                register = False
            elif 'unknown' == status:
                print "This app_id (%s) is unknown by Freebox Server: you have to register again to Freebox Server to get a new app_id." % self.registration['track_id']
            elif 'denied' == status:
                print "This app has been denied by user on Freebox Server (app_id = %s)." % self.registration['track_id']
                register = False
            elif 'timeout' == status:
                print "Timeout occured for this app_id: you have to register again to Freebox Server to get a new app_id (current app_id = %s)." % self.registration['track_id']
            else:
                print "Unexpected response: %s" % status

        if register:
            global gAppDesc
            url = self.fbxAddress + "/api/v1/login/authorize/"
            headers = {
                'Content-type': 'application/json', 'Accept': 'text/plain'}
            # post it
            log("POST url: %s  data: %s" % (url, data))
            r = requests.post(url, data=json.dumps(gAppDesc), headers=headers, timeout=3)
            log("POST response: %s" % r.text)
            # ensure status_code is 200, else raise exception
            if requests.codes.ok != r.status_code:
                raise FbxOSException("Post error: %s" % r.text)
            # rc is 200 but did we really succeed?
            resp = json.loads(r.text)
            #log("Obj resp: %s" % resp)
            if True == resp['success']:
                self.registration['app_token'] = resp['result']['app_token']
                self.registration['track_id'] = resp['result']['track_id']
                self._saveRegistrationParams()
                print "Now you have to accept this app on your Freebox server: take a look on its lcd screen."
            else:
                print "NOK"

    def reboot(self):
        """ Reboot the freebox server now! """
        log(">>> reboot")
        self._login()
        headers = {'X-Fbx-App-Auth': self.sessionToken, 'Accept': 'text/plain'}
        url = self.fbxAddress + "/api/v1/system/reboot/"
        # POST
        log("POST url: %s" % url)
        r = requests.post(url, headers=headers, timeout=3)
        log("POST response: %s" % r.text)
        # ensure status_code is 200, else raise exception
        if requests.codes.ok != r.status_code:
            raise FbxOSException("Post error: %s" % r.text)
        # rc is 200 but did we really succeed?
        resp = json.loads(r.text)
        #log("Obj resp: %s" % resp)
        if not resp['success']:
            raise FbxOSException("Logout failure: %s" % resp)
        print "Freebox Server is rebooting"
        self.isLoggedIn = False
        return True

    def getWifiStatus(self):
        """ Get the current status of wifi: 1 means ON, 0 means OFF """
        log(">>> getWifiStatus")
        self._login()
        # GET wifi status
        headers = {
            'X-Fbx-App-Auth': self.sessionToken, 'Accept': 'text/plain'}
        url = self.fbxAddress + "/api/v1/wifi/"
        # GET
        log("GET url: %s" % url)
        r = requests.get(url, headers=headers, timeout=1)
        log("GET response: %s" % r.text)
        # ensure status_code is 200, else raise exception
        if requests.codes.ok != r.status_code:
            raise FbxOSException("Get error: %s" % r.text)
        # rc is 200 but did we really succeed?
        resp = json.loads(r.text)
        #log("Obj resp: %s" % resp)
        isOn = True
        if True == resp['success']:
            if resp['result']['active']:
                print "Wifi is ON"
                isOn = True
            else:
                print "Wifi is OFF"
                isOn = False
        else:
            raise FbxOSException("Challenge failure: %s" % resp)
        self._logout()
        return isOn

    def setWifiOn(self):
        """ Activate (turn-on) wifi radio module """
        log(">>> setWifiOn")
        return self._setWifiStatus(True)

    def setWifiOff(self):
        """ Deactivate (turn-off) wifi radio module """
        log(">>> setWifiOff")
        return self._setWifiStatus(False)


class FreeboxOSCli:

    """ Command line (cli) interpreter and dispatch commands to controller """

    def __init__(self, controller):
        """ Constructor """
        self.controller = controller
        # Configure parser
        self.parser = argparse.ArgumentParser(
            description='Command line utility to control some FreeboxOS services.')
        # CLI related actions
        self.parser.add_argument(
            '--version', action='version', version="%(prog)s " + __version__)
        self.parser.add_argument(
            '-v', action='store_true', help='verbose mode')
        # Real freeboxOS actions
        group = self.parser.add_mutually_exclusive_group()
        group.add_argument(
            '--registerapp', default=argparse.SUPPRESS, action='store_true',
            help='register this app to FreeboxOS (to be executed only once)')
        group.add_argument('--wifistatus', default=argparse.SUPPRESS,
                           action='store_true', help='get current wifi status')
        group.add_argument(
            '--wifion', default=argparse.SUPPRESS, action='store_true', help='turn wifi ON')
        group.add_argument(
            '--wifioff', default=argparse.SUPPRESS, action='store_true', help='turn wifi OFF')
        self.parser.add_argument(
            '--reboot', default=argparse.SUPPRESS, action='store_true', help='reboot the Freebox now!')
        # Configure cmd=>callback association
        self.cmdCallbacks = {
            'registerapp': self.controller.registerApp,
            'wifistatus': self.controller.getWifiStatus,
            'wifion': self.controller.setWifiOn,
            'wifioff': self.controller.setWifiOff,
            'reboot': self.controller.reboot,
        }

    def cmdExec(self, argv):
        """ Parse the parameters and execute the associated command """
        args = self.parser.parse_args(argv)
        argsdict = vars(args)
        log("Args dict: %s" % argsdict)
        # Activate verbose mode if requested
        if True == argsdict['v']:
            global gVerbose
            gVerbose = True
        # Suppress '-v' command as not a FreeboxOS cmd
        del argsdict['v']
        

  • # à déplacer dans le forum "trucs et astuces" ?

    Posté par (page perso) . Évalué à 2. Dernière modification le 16/08/13 à 10:36.

    merci pour le code.

    If you choose open source because you don't have to pay, but depend on it anyway, you're part of the problem.evloper) February 17, 2014

  • # Cette page bug ?

    Posté par . Évalué à 7.

    Il n'y a que chez moi que ça fait un vilain bug ?
    En haut de la page c'est normal et après le code c'est tout bizarre.

    Please do not feed the trolls

  • # Et la norme Python !!!!

    Posté par . Évalué à 3.

    Elle est où la citation des Monty Python!!!

    Il ne faut pas décorner les boeufs avant d'avoir semé le vent

  • # Effectivement, il y a un problème d'affichage

    Posté par . Évalué à 0.

    Dans le rendu final sur le site, il manque une partie du code, donc le script plante.

    Cependant, l'ensemble du code est bien présent dans le code source de cette page web et d'ailleurs la prévisualisation s'effectue correctement : tout y est. J'avais vérifié avant de poster quand même !

    Seul le rendu final tronque la fin du script. Je ne sais pas pourquoi. Un modérateur/habitué pourrait-il me donner une piste ?

    A suivre donc…

    PS :
    Le code passe le pep8 (sauf critère de longueur de ligne).

    Le code présent mais pas affiché :

            return self.dispatch(argsdict.keys())
    
        def dispatch(self, args):
            """ Call controller action """
            for cmd in args:
                # retrieve callback associated to cmd and execute it, if not found
                # display help
                return self.cmdCallbacks.get(cmd, self.parser.print_help)()
    
    
    if __name__ == '__main__':
            controller = FreeboxOSCtrl()
            cli = FreeboxOSCli(controller)
            rc = cli.cmdExec(sys.argv[1:])
            sys.exit(rc)

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.