Fritzbox Gäste WLAN mit Alexa steuern

Im Folgenden wird erläutert, wie man mit einem selbst gebauten Alexa Skill sein Gäste WLAN einer Fritzbox ein- und ausschalten kann. Ebenso kann das Passwort für das WLAN vorgelesen werden.

Voraussetzungen

Um das Steuern der Fritzbox zu ermöglichen, müssen einige Punkte erfüllt sein.

  1. Man braucht einen Developer Account bei Amazon sowie einen AWS Account.
  2. Da die Steuerung über ein PHP Skript realisiert wird, wird ebenfalls ein lokaler Webserver im Netzwerk benötigt. In diesem Fall wird ein Raspberry PI verwendet.
  3. Dieser Webserver muss aus dem Internet erreichbar sein, damit Alexa darauf zugreifen kann.
  4. Da der Skill auf die eigenen Bedürfnisse angepasst werden muss (Netzwerk, IP Adressen, Benutzernamen, Passwörter etc.), wird der Skill nicht im Store auffindbar sein und nur für einen einzigen Anwendungsfall funktionieren.

Fritzbox Benutzer einrichten

Die im Folgenden benutzte SOAP Funktion, die die Anfragen an die Fritzbox stellt, benötigt zur Authentifizierung einen Benutzernamen bzw. ein Passwort. Daher sollte im Vorhinein ein neuer Benutzer dafür angelegt werden. Es geht auch das Standardpasswort der Fritzbox Oberfläche, aber Safety First 😉

https://fritz.box/ bzw. die IP der Box aufrufen. Unter System – Fritz!Box Benutzer kann ein neuer Benutzer angelegt werden:

Die hier angegebenen Daten werden nachher für die PHP Skripte benötigt.

Raspberry einrichten / Apache installieren

In meinem Fall wird ein Raspberry PI für die Ausführung des Webservers benutzt.

Falls noch nicht schon vorhanden, müssen folgende Pakete installiert werden:

apt-get install apache2 php7.0 php7.0-soap

Nach der Installation kann noch das SSL Modul aktiviert werden, allerdings wird Alexa nur gültige Zertifikate akzeptieren. Selbst signierte Zertifikate werden nicht funktionieren. Aus diesem Grund bleiben wir hier bei einer unverschlüsselten Verbindung.

Der Standardpfad ins Webverzeichnis ist /var/www/html

Das wird umgemappt auf /var/www. Kann man machen, muss man aber nicht. Es ist später allerdings wichtig, dass man weiß, wie die URL zu den Skripten lautet. Für die WLAN Skripte lege ich ein neues Verzeichnis an.

mkdir /var/www/wlan
nano /etc/apache2/sites-enabled/000-default.conf

aus

DocumentRoot /var/www/html

wird

DocumentRoot /var/www

PHP Skript zum Ein- und Ausschalten

In das Verzeichnis werden nun zwei Skripte abgelegt. Eins für das Einschalten des Gäste WLANs und eins für das Ausschalten.

WLAN aus:

nano /var/www/wlan/wlan_aus.php

<?php $client1 = new SoapClient(null,array('location'=> "http://192.168.178.1:49000/upnp/control/wlanconfig2", // IP der FritzBox anpassen
										'uri'		=> "urn:dslforum-org:service:WLANConfiguration:2",
										'soapaction'	=> "urn:dslforum-org:service:WLANConfiguration:3#SetEnable",
										'noroot'	=> True,
										'login'		=> "username", // Username, ist aber irrelevant
										'password'	=> "password" // Passwort
));
$client1->SetEnable(new SoapParam(0, 'NewEnable'));
?>

WLAN ein:

nano /var/www/wlan/wlan_ein.php

<?php $client1 = new SoapClient(null,array('location'=> "http://192.168.0.1:49000/upnp/control/wlanconfig2",
										'uri'			=> "urn:dslforum-org:service:WLANConfiguration:2",
										'soapaction'	=> "urn:dslforum-org:service:WLANConfiguration:2#SetEnable",
										'noroot'		=> True,
										'login'			=> "username",
										'password'		=> "password"
	));
$client1->SetEnable(new SoapParam(1, 'NewEnable'));
?>

Jeweils anzupassen ist hierbei die IP Adresse der Fritzbox sowie das Passwort für den Zugang. Der Benutzername kann auch hinterlegt werden, allerdings wird dieser bei internen Abfragen ignoriert.

Wichtig zu wissen ist, dass die verwendeten Parameter wlanconfig2, WLANConfiguration:2 und WLANConfiguration:2#SetEnable abweichen können. Sie beziehen sich auf die von der Fritzbox vergebene Nummerierung der WLAN Netze. Nummer 1 ist das “normale” WLAN und Nummer 2 sollte das für das Gäste WLAN gelten. Kontrollieren kann man es auf der XML Seite der TR-064 API: http://fritz.box:49000/tr64desc.xml

Dort sind alle “WLAN Nummern” aufgeführt

Anschließend kann man die Skripte via Browser aufrufen und überprüfen, ob das WLAN auf der Fritzbox ausgeschaltet wurde oder nicht. Fehler findet man im Apache Error Log unter /var/log/apache2/error.log. Sollte es Authentifizierungsprobleme o.ä. geben, findet man die Meldungen dort. Wichtig ist auch, dass das PHP SOAP Modul aktiviert ist. Ansonsten kommt es zu Unknown Class Exceptions im PHP.

Weiterleitung der externen URL auf den Webserver

Damit man von Außerhalb auf die URL zugreifen kann, muss eine Portweiterleitung auf der Fritzbox eingerichtet werden. Internet – Freigaben – Portfreigaben aufrufen und eine neue Weiterleitung hinterlegen. Gerät auswählen, externen Port und internen Port des Geräts auswählen, fertig. In dem Fall wurde der externe Port 81 auf den http Port vom Raspberry gemappt.

Anschließend sollte man über http://Externe-IP:81/wlan/wlan_aus.php das Skript aufrufen können. Damit man nicht jeden Tag die IP im Alexa Skill ändern muss, macht eine Nutzung von DynDNS oder der kostenlosen MyFritz Variante Sinn!

Damit wären alle grundlegenden Vorbereitungen zur Steuerung der Fritzbox gegeben. Es fehlt noch die Anbindung als Alexa Skill.

Lambda Funktion erstellen

Man benötigt nun den Account fürs Amazon AWS.

Damit meldet man sich bei der Management Console an (https://aws.amazon.com/de/console/) und erstellt eine neue Lambda Funktion

Anschließend wählt man Create Function, klickt auf Blueprints und wählt “alexa-skill-kit-sdk-factskill“.

Nun gibt man der Funktion einen Namen, wählt “Create new role from template” und nennt die Rolle lambda_basic_execution.

Der nun vorgefertigte Code wird wiefolgt abgeändert:

var speechOutput;
var reprompt;

var myURL = 'http://url.myfritz.net:81/wlan/';
var myWLAN = 'Schnorrer Hotspot';
var myPass = "<break time='1s'/> Passwort<break time='1s'/> Der Buchstabe von jedem neuen Wort wird groß geschrieben.<break time='1s'/> ";

var welcomeOutput = "Hallo! Na, wie geht's denn so?";
// Womit kann ich behilflich sein? Für einen Überblick, was ich erledigen kann, sage Menü oder Liste.
var welcomeReprompt = "Wie kann ich helfen? Sage z.B. Menü oder Hilfe";

 // 2. Skill Code =======================================================================================================
"use strict";
var Alexa = require('alexa-sdk');
var APP_ID = '';  // TODO replace with your app ID (OPTIONAL).
var speechOutput = '';
var handlers = {
    'LaunchRequest': function () {
          this.emit(':ask', welcomeOutput, welcomeReprompt);
    },
	'AMAZON.HelpIntent': function () {
        speechOutput = 'Dieser Skill kann dir z.B. das Passwort zum Gäste WLAN vorlesen. Sage dazu, Alexa frage Gäste WLAN, wie lautet das Passwort? Alternativ kannst du zum Beispiel das Gäste WLAN ein oder ausschalten. Womit kann ich dir nun dienen?';
        reprompt = 'probiere es mit nenne mir das Passwort';
        this.emit(':ask', speechOutput, reprompt);
    },
    'AMAZON.CancelIntent': function () {
        speechOutput = 'ok';
        this.emit(':tell', speechOutput);
    },
    'AMAZON.StopIntent': function () {
        speechOutput = 'ok, hau rein';
        this.emit(':tell', speechOutput);
    },
    'SessionEndedRequest': function () {
        speechOutput = '';
        //this.emit(':saveState', true);
        this.emit(':tell', speechOutput);
    },
     'AMAZON.YesIntent': function () {
        speechOutput = "OK ich wiederhole. " + myPass + " Soll ich es noch einmal wiederholen?";
        this.emit(':ask', speechOutput, speechOutput);
    },
     'AMAZON.NoIntent': function () {
        speechOutput = 'OK dann wünsche ich deinen Gästen viel Spaß beim surfen im Internet';
        this.emit(':tell', speechOutput);
    },
	"MenuIntent": function () {
	speechOutput = "Sage z.B. Gäste WLAN ein oder ausschalten. Du kannst mich auch fragen, wie ist das Passwort. Womit kann ich dir nun dienen?";
        this.emit(":ask", speechOutput, speechOutput);
    },
	"WLANIntent": function () {
	speechOutput = "Das Passwort lautet" + myPass +" Soll ich es noch einmal wiederholen?";
        this.emit(":ask", speechOutput, speechOutput);
    },
	"WLANOnIntent": function () {
		speechOutput = "Ok, ich habe das Gäste WLAN aktiviert. Der Name lautet " + myWLAN + ". Wenn du das Passwort brauchst, sag einfach: Alexa frage Gäste WLAN, wie lautet das Passwort?";

        var url = 'wlan_ein.php';
        getValue(url,(result)=>{
            this.emit(":tell", speechOutput);
        });

    },
    "WLANOffIntent": function () {
	speechOutput = "Ok, ich habe nun das Gäste WLAN deaktiviert";
        var url = 'wlan_aus.php';
        getValue(url,(result)=>{

            this.emit(":tell", speechOutput);
        });
    },
	'Unhandled': function () {
        speechOutput = "Ich habe dich leider nicht verstanden. Kannst du das Bitte noch einmal wiederholen?";
        this.emit(':ask', speechOutput, speechOutput);
    }
};

function getValue(loc,cb) {
    var https = require('http');
    let endpoint = myURL + loc;
    let something = "";
    let body = "";
    https.get(endpoint, (response) => {
        response.on('data', (chunk) => {
            body += chunk;
        });
        response.on('end', () => {
            cb(null);
        });
    });
}

exports.handler = (event, context) => {
    var alexa = Alexa.handler(event, context);
    alexa.APP_ID = APP_ID;
    alexa.registerHandlers(handlers);
    alexa.execute();
};


function resolveCanonical(slot){
	//this function looks at the entity resolution part of request and returns the slot value if a synonyms is provided
   var canonical = '';
    try{
		canonical = slot.resolutions.resolutionsPerAuthority[0].values[0].value.name;
	}catch(err){
	    console.log(err.message);
	    canonical = slot.value;
	}
	return canonical;
}

function delegateSlotCollection(){
  console.log("in delegateSlotCollection");
  console.log("current dialogState: "+this.event.request.dialogState);
    if (this.event.request.dialogState === "STARTED") {
      console.log("in Beginning");
	  var updatedIntent= null;
	  // updatedIntent=this.event.request.intent;
      //optionally pre-fill slots: update the intent object with slot values for which
      //you have defaults, then return Dialog.Delegate with this updated intent
      // in the updatedIntent property
      //this.emit(":delegate", updatedIntent); //uncomment this is using ASK SDK 1.0.9 or newer

	  //this code is necessary if using ASK SDK versions prior to 1.0.9
	  if(this.isOverridden()) {
			return;
		}
		this.handler.response = buildSpeechletResponse({
			sessionAttributes: this.attributes,
			directives: getDialogDirectives('Dialog.Delegate', updatedIntent, null),
			shouldEndSession: false
		});
		this.emit(':responseReady', updatedIntent);

    } else if (this.event.request.dialogState !== "COMPLETED") {
      console.log("in not completed");
      // return a Dialog.Delegate directive with no updatedIntent property.
      //this.emit(":delegate"); //uncomment this is using ASK SDK 1.0.9 or newer

	  //this code necessary is using ASK SDK versions prior to 1.0.9
		if(this.isOverridden()) {
			return;
		}
		this.handler.response = buildSpeechletResponse({
			sessionAttributes: this.attributes,
			directives: getDialogDirectives('Dialog.Delegate', updatedIntent, null),
			shouldEndSession: false
		});
		this.emit(':responseReady');

    } else {
      console.log("in completed");
      console.log("returning: "+ JSON.stringify(this.event.request.intent));
      // Dialog is now complete and all required slots should be filled,
      // so call your normal intent handler.
      return this.event.request.intent;
    }
}


function randomPhrase(array) {
    // the argument is an array [] of words or phrases
    var i = 0;
    i = Math.floor(Math.random() * array.length);
    return(array[i]);
}
function isSlotValid(request, slotName){
        var slot = request.intent.slots[slotName];
        //console.log("request = "+JSON.stringify(request)); //uncomment if you want to see the request
        var slotValue;

        //if we have a slot, get the text and store it into speechOutput
        if (slot && slot.value) {
            //we have a value in the slot
            slotValue = slot.value.toLowerCase();
            return slotValue;
        } else {
            //we didn't get a value in the slot.
            return false;
        }
}

//These functions are here to allow dialog directives to work with SDK versions prior to 1.0.9
//will be removed once Lambda templates are updated with the latest SDK

function createSpeechObject(optionsParam) {
    if (optionsParam && optionsParam.type === 'SSML') {
        return {
            type: optionsParam.type,
            ssml: optionsParam['speech']
        };
    } else {
        return {
            type: optionsParam.type || 'PlainText',
            text: optionsParam['speech'] || optionsParam
        };
    }
}

function buildSpeechletResponse(options) {
    var alexaResponse = {
        shouldEndSession: options.shouldEndSession
    };

    if (options.output) {
        alexaResponse.outputSpeech = createSpeechObject(options.output);
    }

    if (options.reprompt) {
        alexaResponse.reprompt = {
            outputSpeech: createSpeechObject(options.reprompt)
        };
    }

    if (options.directives) {
        alexaResponse.directives = options.directives;
    }

    if (options.cardTitle && options.cardContent) {
        alexaResponse.card = {
            type: 'Simple',
            title: options.cardTitle,
            content: options.cardContent
        };

        if(options.cardImage && (options.cardImage.smallImageUrl || options.cardImage.largeImageUrl)) {
            alexaResponse.card.type = 'Standard';
            alexaResponse.card['image'] = {};

            delete alexaResponse.card.content;
            alexaResponse.card.text = options.cardContent;

            if(options.cardImage.smallImageUrl) {
                alexaResponse.card.image['smallImageUrl'] = options.cardImage.smallImageUrl;
            }

            if(options.cardImage.largeImageUrl) {
                alexaResponse.card.image['largeImageUrl'] = options.cardImage.largeImageUrl;
            }
        }
    } else if (options.cardType === 'LinkAccount') {
        alexaResponse.card = {
            type: 'LinkAccount'
        };
    } else if (options.cardType === 'AskForPermissionsConsent') {
        alexaResponse.card = {
            type: 'AskForPermissionsConsent',
            permissions: options.permissions
        };
    }

    var returnResult = {
        version: '1.0',
        response: alexaResponse
    };

    if (options.sessionAttributes) {
        returnResult.sessionAttributes = options.sessionAttributes;
    }
    return returnResult;
}

function getDialogDirectives(dialogType, updatedIntent, slotName) {
    let directive = {
        type: dialogType
    };

    if (dialogType === 'Dialog.ElicitSlot') {
        directive.slotToElicit = slotName;
    } else if (dialogType === 'Dialog.ConfirmSlot') {
        directive.slotToConfirm = slotName;
    }

    if (updatedIntent) {
        directive.updatedIntent = updatedIntent;
    }
    return [directive];
}

Wichtig hierbei sind die Variablen myURL, myWLAN, myPass. Diese passt man für dich an.

Die Ausgaben von Alexa kann man in den jeweiligen Intents anpassen. Sollte die Webseite mit den PHP Skripten nicht SSL verschlüsselt sein, muss der Abschnitt “var https = require(‘https’);” abgeändert werden nach “var https = require(‘http’);”. Die Änderung sollte man vornehmen, wenn man kein offizielles Zertifikat besitzt.

Anschließend muss noch ein Trigger zur Funktion hinzugefügt werden. Dazu von der Liste “Alexa Skill Kit” auswählen und nach rechts verschieben. Sollte “Alexa Skill Kit”, muss man evtl. oben rechts die Region ändern. Bei “N. Virginia” waren die Trigger verfügbar.

Zuletzt kopiert man sich den Amazon Resource Name (ARN), er wird später vom Alexa Skill benötigt.

Alexa Skill erstellen

Man benötigt nun einen Account bei https://developer.amazon.com/ und öffnet https://developer.amazon.com/edw/home.html#/skill/create/, um einen neuen Skill anzulegen.

Beim Anlegen des Skill, vergibt man einen internen Namen und den “Invocation Name”, mit dem der Skill später aufgerufen wird.

Anschließend geht es im “Interaction Model” weiter. Hier wird der Skill Beta Builder genutzt. Im Code Editor wird folgendes hinterlegt:

{
  "intents": [
    {
      "name": "AMAZON.CancelIntent",
      "samples": []
    },
    {
      "name": "AMAZON.HelpIntent",
      "samples": []
    },
    {
      "name": "AMAZON.NoIntent",
      "samples": [
        "Nein",
        "sei ruhig",
        "danke",
        "no"
      ]
    },
    {
      "name": "AMAZON.StopIntent",
      "samples": []
    },
    {
      "name": "AMAZON.YesIntent",
      "samples": [
        "Ja",
        "natürlich",
        "jawohl",
        "sicher",
        "klar",
        "si"
      ]
    },
    {
      "name": "MenuIntent",
      "samples": [
        "Menü",
        "Liste",
        "Übersicht",
        "was kann ich sagen"
      ],
      "slots": []
    },
    {
      "name": "WLANIntent",
      "samples": [
        "wie lautet das Passwort",
        "nach dem Passwort",
        "wie ist das Passwort für das WLAN",
        "wie ist das Passwort für das Gäste WLAN",
        "sage mir das Passwort",
        "nenne mir das Passwort"
      ],
      "slots": []
    },
    {
      "name": "WLANOffIntent",
      "samples": [
        "Gäste WLAN ausschalten",
        "Gäste WLAN deaktivieren",
        "schalte das Gäste WLAN aus"
      ],
      "slots": []
    },
    {
      "name": "WLANOnIntent",
      "samples": [
        "WLAN einschalten",
        "Gäste WLAN einschalten",
        "schalte das Gäste WLAN ein",
        "aktiviere das Gäste WLAN"
      ],
      "slots": []
    }
  ]
}

Anschließend geht man auf “Build Model” und danach auf “Konfiguration”, um wieder zur Skill Ansicht zurück zu gehen. Hier wird nun die Lambda ARN hinterlegt.

Im nächsten Abschnitt “Test”, muss der Schalter aktiviert werden. Ab jetzt kann der Skill von Alexa aufgerufen werden.

Testen

Über ein Tail auf das Apache Access Log kann man gut nachverfolgen, ob die URL aufgerufen wird:

tail /var/log/apache/access.log

Hier gibt es die notwenigen Dateien zum Download.

Advertisements