SDK
Ich habe, für die Entwicklung neuer Skills, ein eigenes SDK (Software Development Kit) erstellt.
Dieses kümmert sich, in Zusammenarbeit mit dem Skillmanager, um nützliche Funktionen wie zum Beispiel die Sprachausgabe, oder aber den Umgang mit den Antwortsätzen.
Konfiguration und Initialisierung
Dieser Abschnitt beschreibt die Kommunikation vom Skillmanager mit dem SDK.
Damit der Skillmanager die Skills korrekt verwenden kann, müssen bei der Initialisierung einige Informationen definiert werden (wie z.B. unter welcher Adresse der MQTT-Broker erreichbar ist).
Config
Beim start des Skillmanagers wird über das SDK ein configObject
erstellt, in dem einige Konfigurationen gespeichert werden.
let configObject = {
mqtt: 'localhost',
intentHandler: () => {},
variables: {},
zigbeeTopic: "",
zigbeeUpdater: () => {},
zigbeeDevices: [],
zigbeeGroups: []
}
Unter mqtt
wird die Host-IP des MQTT-Brokers gespeichert, über den Rhasspy mit den einzelnen Komponenten kommuniziert.
Über diesen Broker kommen auch alle Intents an, welche dann vom intentHandler
verarbeitet werden.
Beim intentHandler
handelt es sich um eine Callback-Funktion, welche in skillManager.js
als customIntentHandler
definiert wurde.
In Variables werden die von den Endnutzerinnen und Endnutzern gesetzten Optionen gespeichert, damit diese von den Skills über eine SDK-Funktion abgerufen werden können.
Bei zigbeeTopic
handelt es sich um das Topic, auf welches eine, mögliche Zigbee2MQTT-Instanz ihre Daten veröffentlicht.
Zigbee2MQTT ist hierbei optional.
Sollte eine solche Anwendung vorliegen, so wird eine Liste mit allen Geräten und Gruppen ausgelesen und an den zigbeeUpdater
übergeben.
Dabei handelt es sich wieder um eine Callback-Funktion, die diese Liste bei Rhasspy als Slot registriert.
zigbeeDevices
und zigbeeGroups
beinhalten die jeweiligen Einträge.
Diese Angaben werden vom Skillmanager mittels der config
-Funktion gesetzt.
function config(options = {}){
for (let i in options){
if (!configObject.hasOwnProperty(i) || options[i] === undefined || options[i] === null) continue;
configObject[i] = options[i];
}
}
Bei dieser Funktion handelt es sich im Grunde um eine einfache Setter Funktion, welche jedoch darauf achtet, dass nur im configObject
deklarierte Felder gesetzt und verändert werden können.
Init
Mit der init
-Funktion verbindet sich der Skillmanager mit dem MQTT-Broker und abonniert einige Topics:
async function init() {
client = await mqtt.connect(`mqtt://${configObject.mqtt}`);
client.on("connect", function () {
client.subscribe(`${configObject.zigbeeTopic}/bridge/devices`);
client.subscribe(`${configObject.zigbeeTopic}/bridge/groups`);
client.subscribe('hermes/intent/#');
client.on('message', (topic, message) => {
// ...
});
});
}
Zwei der Topics dienen dazu, eine Liste aller Geräte und Gruppen von Zigbee2MQTT auszulesen und alle Änderungen mitzubekommen.
Diese Liste wird dann als Slot mit dem Namen “zigbee2mqtt” an Rhasspy gesendet, damit die Spracherkennung die Wörter erkennen kann.
Das dritte Topic dient dazu alle eingehenden Intents von Rhasspy aufzufangen.
Nachdem ein Intent erkannt wird, werden sofort einige Informationen im JavaScript-Objekt sessionData
gespeichert und die Nachricht an den im configObject
gespeicherten intentHandler
weitergegeben.
Sitzungsdaten
Damit Entwicklerinnen und Entwickler sich nicht darum kümmern muss, über welchen Satelliten die Sprache ausgegeben wird oder in welcher Sitzung man sich derzeit befindet, gibt es die sog. sessionData
.
let sessionData = {
siteId: "default",
sessionId: "",
skill: "",
answer: ""
};
Hier werden einige Informationen gespeichert, welche direkt von Rhasspy kommen, wie zum Beispiel die sessionId
oder auch die siteId
.
Diese werden unter anderem von der say
Funktion genutzt, damit die Sprachausgabe auf dem Satelliten wiedergegeben wird, über welchen die Eingabe stattfand.
Aber auch Informationen eines Skills werden hier gespeichert.
So wird zum Beispiel basierend auf der ausgewählten Sprache, der Antwortsatz aus den jeweiligen locale
-Dateien ausgelesen.
Sprachausgabe
Die Funktion say
kann genutzt werden, um einen Satz von Rhasspy sprechen zu lassen.
function say(text = ""){
let message = {
text: text,
siteId: sessionData["siteId"],
sessionId: sessionData["sessionId"]
};
client.publish('hermes/tts/say', JSON.stringify(message));
}
Der Funktion wird lediglich ein String übergeben.
Die restlichen Informationen bezieht sie über das sessionData
-Objekt.
Zum Schluss werden die Daten als String auf das Topic hermes/tts/say
veröffentlicht.
Beispiel
const customSdk = require("@fwehn/custom_sdk");
function helloWorld(hello, world){
customSdk.say(`${hello} ${world}`);
}
Bei diesem Beispiel werden die beiden Strings hello
und world
aneinander gehangen und ausgegeben.
Antwort generieren
Entwicklerinnen und Entwickler können Antwortsätze in verschiedenen Sprachen definieren.
Damit diese jedoch mit einigen Werten erweitert werden können, muss ein solcher Satz generiert werden.
Dazu gibt man einen Satz wie zum Beispiel Es ist # Uhr #
(aus GetTime) an.
Die Funktion generateAnswer
ersetzt dann jeden Separator (standardmäßig #
) mit den Werten, die als Array übergeben werden.
Mit der Variable answerIndex
kann man deklarieren, welcher Antwortsatz ausgewählt werden soll.
function generateAnswer(answerIndex = 0 ,vars = [""], separator = "#"){
let parts = sessionData.answers[answerIndex].split(separator);
let answer = parts[0];
for (let i = 1; i < parts.length; i++){
answer = answer + vars[i-1] + parts[i];
}
return answer;
}
Möchte man das Zeichen #
selbst benutzen, kann man einen eigenen Separator verwenden, dazu muss dieser jedoch auch der Funktion übergeben werden.
Der jeweilige Satz wird vom Skillmanager in der jeweiligen Sprache geladen und mit der einfachen Setter-Funktion setAnswer
im Objekt sessionData
gespeichert.
Von dort wird die Antwort ausgelesen.
Für mehr Variation kann man sich einen zufälligen Satz aus der Liste der definierten Sätze auswählen und generieren lassen.
Dazu habe ich die Funktion generateRandomAnswer
erstellt.
function generateRandomAnswer(vars = [""], separator = "#"){
let randomAnswerIndex = Math.floor(Math.random() * sessionData.answers.length);
return generateAnswer(randomAnswerIndex, vars, separator);
}
Beispiel
const customSdk = require("@fwehn/custom_sdk");
function getTime(){
let time = new Date();
let answer = customSdk.generateAnswer(0, [time.getHours(), time.getMinutes()]);
customSdk.say(answer);
}
Hier wird der Satz Es ist # Uhr #
um die aktuelle Stunde und die aktuelle Minute erweitert, also z.B. Es ist 11 Uhr 34
.
Dieser Satz wird dann mit der say
-Funktion ausgegeben.
Raw-Tokens
Rhasspy bietet uns die Möglichkeit, dass wenn ein Slot mit einem Namen aufgerufen wird, dieser in einen Wert übersetzt wird.
Wenn ich also beispielsweise frage wie das Wetter am Montag
wird, kann man den Slot in Rhasspy so hinterlegen, dass statt Montag
der Wert 1
für den ersten Tag der Woche zurückgegeben wird.
Das ist sehr nützlich, wenn man plant Skills in verschiedenen Sprachen um zusetzten.
So kann der Tag Montag
im Code immer den Wert 1
haben, im Sprachbefehl aber mit Montag
oder Monday
aufgerufen werden.
Möchte man jedoch in der Sprachausgabe jedoch Teile der Frage aufgreifen, so wäre es hilfreich den Namen des Slots verwenden zu können.
“Wie wird das Wetter am Montag
?”:
- Mit dem Wert: “Am
1
wird es …” - Mit dem Namen: “Am
Montag
wird es …”
Um dieses Problem zu umgehen, habe ich zwei einfache Funktionen erstellt: setRawTokens
und getRawToken
Sobald der Skillmanager einen Intent von Rhasspy erhält, werden die in diesem JavaScript-Object enthaltenen rawValue
-Werte mittels der setRawTokens
-Funktion in einer Liste gespeichert.
function setRawTokens(rawTokenList) {
rawTokens = rawTokenList;
}
Um diese Werte auszulesen, kann die Funktion getRawToken
verwendet werden, der man dann den Namen des Slots übergibt.
function getRawToken(tokenName) {
return rawTokens[tokenName] || "Token Undefined";
}
Beispiel
In folgendem Beispiel wird der Raw-Token des Slots days
ausgelesen und in der Variable dayName
gespeichert.
Dadurch kann der Name des Tages in der Antwort verwendet werden:
“… wie wird das Wetter am Mittwoch
?” → “Am Mittwoch
werden es Temperaturen von …”
let forecastDay = data[date.toISOString().split("T")[0]];
let dayName = customSdk.getRawToken("days");
answer = customSdk.generateAnswer(0, [dayName, Math.floor(forecastDay["temp_min"]), Math.floor(forecastDay["temp_max"])]);
customSdk.say(answer);
Config Variablen
Mit der Funktion config
wird nun das Feld variables
im JS-Object configObject
gesetzt.
Beim Aufruf der Funktionen getVaraibles
und getVariable
werden jetzt die, zum im sessionData
gespeicherten skill
passenden, Variablen ausgelesen und zurückgegeben.
function getAllVariables(){
return new Promise((resolve, reject) => {
try{
let variables = configObject.variables[sessionData["skill"]];
if (variables){
resolve(variables);
}else{
reject("Variable undefined!");
}
}catch (e) {
reject(e);
}
});
}
Bei der Funktion getVariable
wird lediglich der Wert, der angegeben Variable, zurückgegeben.
Beispiel
const customSdk = require("@fwehn/custom_sdk");
function getUrl(){
return new Promise((resolve, reject) => {
customSdk.getAllVariables()
.then(variables => {
resolve(`https://api.openweathermap.org/data/2.5/forecast?zip=${variables["city"]},${variables["country"]}&appid=${variables["APIKey"]}&lang=${variables["language"]}&units=${variables["units"]}`);
}).catch(reject);
});
}
In diesem Beispiel werden die Angaben, die über das Webinterface gesetzt wurden, dazu genutzt, eine URL zu generieren, um auf die API von OpenWeather zuzugreifen.
Publish
Um möglichen Entwicklerinnen und Entwicklern mehr Möglichkeiten zu bieten, habe ich eine Funktion implementiert, mit der man eigene MQTT-Nachrichten senden kann.
function publishMQTT(topic = "", payload ){
if (typeof payload === "string"){
client.publish(topic, payload);
}else if (typeof payload === "object"){
client.publish(topic, JSON.stringify(payload));
}else{
fail();
}
}
Dazu müssen die zu sendenden Daten entweder als String oder als JS-Object vorliegen.
- Previous
- Next