Merge pull request 'Refactor: Multi-File-Struktur mit wiederverwendbaren Intent-Factories' (#1) from refactor/multi-file-factories into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architektur (Refactoring)
|
||||||
|
|
||||||
|
Der Code ist jetzt auf mehrere Dateien aufgeteilt, damit neue Sensoren/Geraete
|
||||||
|
nur noch wenige Zeilen Config statt eines kompletten Handlers brauchen:
|
||||||
|
|
||||||
|
```
|
||||||
|
home-assistant-skill/
|
||||||
|
+-- index.js # Nur noch Config: ruft die Factories mit den echten
|
||||||
|
| # Entity-IDs auf und registriert die Handler
|
||||||
|
+-- factories.js # makeApplianceIntent, makeSimpleSensorIntent,
|
||||||
|
| # makeSlotSensorIntent, PERIOD_PHRASES
|
||||||
|
+-- ha-client.js # getHaState() - HA REST API Client
|
||||||
|
+-- static-handlers.js # Launch/Help/Cancel/Fallback/Error (Pflicht-Handler)
|
||||||
|
+-- package.json
|
||||||
|
+-- .env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
Neuer einfacher Sensor (kein Slot), z.B. Luftfeuchtigkeit:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const GetHumidityIntentHandler = makeSimpleSensorIntent({
|
||||||
|
intentName: 'GetHumidityIntent',
|
||||||
|
entityId: 'sensor.wohnzimmer_luftfeuchtigkeit',
|
||||||
|
template: (value, unit) => `Die Luftfeuchtigkeit betraegt ${value} ${unit}.`,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Neues Geraet mit Status/Fortschritt/Endzeit, z.B. Geschirrspueler ohne
|
||||||
|
Fortschritt (nur Status, mit individueller Formulierung ueber
|
||||||
|
`customStatusTemplate`):
|
||||||
|
|
||||||
|
```js
|
||||||
|
const GetDishwasherIntentHandler = makeApplianceIntent({
|
||||||
|
intentName: 'GetDishwasherIntent',
|
||||||
|
deviceName: 'Der Geschirrspueler',
|
||||||
|
statusEntity: 'sensor.geschirrspueler_betriebszustand',
|
||||||
|
customStatusTemplate: (state) => `Der Geschirrspueler laeuft im Programm ${state}.`,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Neue Kostengruppe mit Zeitraum-Slot (Tag/Woche/Monat), nutzt denselben
|
||||||
|
`PeriodList` Slot Type wie `GetEnergyCostIntent`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const GetXyzEnergyCostIntentHandler = makeSlotSensorIntent({
|
||||||
|
intentName: 'GetXyzEnergyCostIntent',
|
||||||
|
slotName: 'Period',
|
||||||
|
idToEntityMap: {
|
||||||
|
tag: 'sensor.xyz_kosten_tag',
|
||||||
|
woche: 'sensor.xyz_kosten_woche',
|
||||||
|
monat: 'sensor.xyz_kosten_monat',
|
||||||
|
},
|
||||||
|
transformValue: (v) => parseFloat(v).toFixed(2).replace('.', ','),
|
||||||
|
template: (value, unit, periodSpoken, periodId) =>
|
||||||
|
`Die Stromkosten fuer ${PERIOD_PHRASES[periodId] || periodSpoken} betragen ${value} ${unit}.`,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
In jedem Fall bleibt bestehen: Der zugehoerige **Intent mit Sample Utterances**
|
||||||
|
muss weiterhin in der Alexa Developer Console angelegt werden - das laesst
|
||||||
|
sich unabhaengig vom Code-Aufbau nicht vermeiden.
|
||||||
|
|
||||||
|
`PERIOD_PHRASES` sorgt fuer korrekte deutsche Grammatik ("diese Woche" statt
|
||||||
|
"diesen Woche"), da `periodId` (die feste Slot-ID) statt des roh gesprochenen
|
||||||
|
Texts fuer die Formulierung verwendet wird.
|
||||||
|
|
||||||
|
Aktuell zusaetzlich vorhanden: `GetPowerKammerUsageIntent` (Leistung
|
||||||
|
Kammer-Serverschrank) und `GetEnergyCostKammerIntent` (Stromkosten
|
||||||
|
Kammer-Serverschrank nach Tag/Woche/Monat) - gleiches Muster wie oben.
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
// ---- Wiederverwendbare Intent-Factories ----
|
||||||
|
// Statt für jedes neue Gerät/jeden neuen Sensor einen kompletten Handler zu
|
||||||
|
// kopieren, reicht hier ein kurzer Config-Aufruf. Neue Intents müssen weiterhin
|
||||||
|
// in der Alexa Developer Console angelegt werden (Intent-Name + Sample Utterances) -
|
||||||
|
// das lässt sich nicht vermeiden, unabhängig vom Backend.
|
||||||
|
|
||||||
|
const Alexa = require('ask-sdk-core');
|
||||||
|
const { getHaState } = require('./ha-client');
|
||||||
|
|
||||||
|
const UNKNOWN_VALUES = ['unknown', 'unavailable', 'none'];
|
||||||
|
const INACTIVE_STATUS_VALUES = ['fertig', 'inaktiv', 'finished', 'inactive'];
|
||||||
|
|
||||||
|
/** Extrahiert die aufgelöste Slot-ID und den sauberen gesprochenen Namen. */
|
||||||
|
function resolveSlot(slot, fallbackText) {
|
||||||
|
const resolvedValue = slot?.resolutions?.resolutionsPerAuthority?.[0]?.values?.[0]?.value;
|
||||||
|
return {
|
||||||
|
id: resolvedValue?.id,
|
||||||
|
name: resolvedValue?.name || slot?.value || fallbackText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut einen Intent-Handler für Geräte mit optionalem Status/Fortschritt/Endzeit
|
||||||
|
* (z.B. Waschmaschine, Trockner, Geschirrspüler).
|
||||||
|
*
|
||||||
|
* @param {object} config
|
||||||
|
* @param {string} config.intentName - Intent-Name aus der Alexa Console, z.B. "GetDryerIntent"
|
||||||
|
* @param {string} config.deviceName - Wie das Gerät in der Antwort genannt wird, z.B. "Der Trockner"
|
||||||
|
* @param {string} [config.statusEntity] - Optional. Entity mit Betriebszustand (z.B. "fertig"/"inaktiv")
|
||||||
|
* @param {string} [config.progressEntity] - Optional. Entity mit Fortschritt in Prozent
|
||||||
|
* @param {string} [config.endTimeEntity] - Optional. Entity mit ISO-Zeitstempel der Endzeit
|
||||||
|
* @param {function} [config.customStatusTemplate] - Optional. (state: string) => string
|
||||||
|
* Wird verwendet, wenn NUR statusEntity gesetzt ist (kein progressEntity), um eine
|
||||||
|
* individuelle Formulierung statt der generischen "ist im Zustand X" zu erzeugen.
|
||||||
|
*/
|
||||||
|
function makeApplianceIntent({
|
||||||
|
intentName,
|
||||||
|
deviceName,
|
||||||
|
statusEntity,
|
||||||
|
progressEntity,
|
||||||
|
endTimeEntity,
|
||||||
|
customStatusTemplate,
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
canHandle(handlerInput) {
|
||||||
|
return (
|
||||||
|
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
||||||
|
Alexa.getIntentName(handlerInput.requestEnvelope) === intentName
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async handle(handlerInput) {
|
||||||
|
try {
|
||||||
|
const statusState = statusEntity
|
||||||
|
? await getHaState(statusEntity).catch(() => null)
|
||||||
|
: null;
|
||||||
|
const betrieb = statusState?.state?.toLowerCase().trim();
|
||||||
|
|
||||||
|
if (betrieb && INACTIVE_STATUS_VALUES.includes(betrieb)) {
|
||||||
|
return handlerInput.responseBuilder
|
||||||
|
.speak(`${deviceName} läuft aktuell nicht.`)
|
||||||
|
.getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!progressEntity) {
|
||||||
|
let speakOutput;
|
||||||
|
if (statusState && customStatusTemplate) {
|
||||||
|
speakOutput = customStatusTemplate(statusState.state);
|
||||||
|
} else if (statusState) {
|
||||||
|
speakOutput = `${deviceName} ist im Zustand ${statusState.state}.`;
|
||||||
|
} else {
|
||||||
|
speakOutput = `Für ${deviceName} habe ich leider keine Informationen.`;
|
||||||
|
}
|
||||||
|
return handlerInput.responseBuilder.speak(speakOutput).getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressState = await getHaState(progressEntity);
|
||||||
|
const progress = progressState.state;
|
||||||
|
|
||||||
|
if (UNKNOWN_VALUES.includes(progress)) {
|
||||||
|
return handlerInput.responseBuilder
|
||||||
|
.speak(`${deviceName} läuft aktuell nicht.`)
|
||||||
|
.getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const roundedProgress = Math.round(parseFloat(progress));
|
||||||
|
|
||||||
|
if (!endTimeEntity) {
|
||||||
|
return handlerInput.responseBuilder
|
||||||
|
.speak(`${deviceName} ist ${roundedProgress} Prozent fertig.`)
|
||||||
|
.getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTimeState = await getHaState(endTimeEntity);
|
||||||
|
const endTimeRaw = endTimeState.state;
|
||||||
|
|
||||||
|
if (UNKNOWN_VALUES.includes(endTimeRaw)) {
|
||||||
|
return handlerInput.responseBuilder
|
||||||
|
.speak(`${deviceName} ist ${roundedProgress} Prozent fertig.`)
|
||||||
|
.getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTime = new Date(endTimeRaw);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
if (endTime.getTime() - now.getTime() <= 0) {
|
||||||
|
return handlerInput.responseBuilder.speak(`${deviceName} ist fertig.`).getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeFormatted = new Intl.DateTimeFormat('de-DE', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
timeZone: 'Europe/Berlin',
|
||||||
|
}).format(endTime);
|
||||||
|
|
||||||
|
const speakOutput = `${deviceName} ist ${roundedProgress} Prozent fertig und endet um ${timeFormatted} Uhr.`;
|
||||||
|
return handlerInput.responseBuilder.speak(speakOutput).getResponse();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`HA API Fehler (${intentName}):`, err);
|
||||||
|
return handlerInput.responseBuilder
|
||||||
|
.speak('Ich konnte gerade keine Verbindung zu Home Assistant herstellen. Bitte versuch es später noch einmal.')
|
||||||
|
.getResponse();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut einen Intent-Handler für einen einzelnen Sensor-Wert ohne Slot
|
||||||
|
* (z.B. aktueller Stromverbrauch, Luftfeuchtigkeit, Füllstand).
|
||||||
|
*
|
||||||
|
* @param {object} config
|
||||||
|
* @param {string} config.intentName - Intent-Name aus der Alexa Console
|
||||||
|
* @param {string} config.entityId - Home-Assistant-Entity-ID
|
||||||
|
* @param {function} config.template - (value, unit, state) => string
|
||||||
|
* @param {function} [config.transformValue] - (rawValue: string) => string|number
|
||||||
|
* @param {string} [config.notFoundMessage] - Antwort, falls Wert unknown/unavailable ist.
|
||||||
|
*/
|
||||||
|
function makeSimpleSensorIntent({
|
||||||
|
intentName,
|
||||||
|
entityId,
|
||||||
|
template,
|
||||||
|
transformValue = (v) => v,
|
||||||
|
notFoundMessage = 'Ich konnte den Wert leider nicht auslesen.',
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
canHandle(handlerInput) {
|
||||||
|
return (
|
||||||
|
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
||||||
|
Alexa.getIntentName(handlerInput.requestEnvelope) === intentName
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async handle(handlerInput) {
|
||||||
|
try {
|
||||||
|
const state = await getHaState(entityId);
|
||||||
|
const rawValue = state.state;
|
||||||
|
|
||||||
|
if (UNKNOWN_VALUES.includes(rawValue) || rawValue === undefined || rawValue === null) {
|
||||||
|
return handlerInput.responseBuilder.speak(notFoundMessage).getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = transformValue(rawValue);
|
||||||
|
const unit = state.attributes?.unit_of_measurement || '';
|
||||||
|
const speakOutput = template(value, unit, state);
|
||||||
|
|
||||||
|
return handlerInput.responseBuilder.speak(speakOutput).getResponse();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`HA API Fehler (${intentName}):`, err);
|
||||||
|
return handlerInput.responseBuilder
|
||||||
|
.speak('Ich konnte gerade keine Verbindung zu Home Assistant herstellen. Bitte versuch es später noch einmal.')
|
||||||
|
.getResponse();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut einen Intent-Handler für Sensor-Werte, deren Entity-ID von einem
|
||||||
|
* gesprochenen Slot abhängt (z.B. Stromkosten nach Tag/Woche/Monat).
|
||||||
|
* Mehrere solcher Intents können sich denselben Slot Type in der Alexa
|
||||||
|
* Console teilen (z.B. "PeriodList" für alle Tag/Woche/Monat-Abfragen).
|
||||||
|
*
|
||||||
|
* @param {object} config
|
||||||
|
* @param {string} config.intentName - Intent-Name aus der Alexa Console
|
||||||
|
* @param {string} config.slotName - Name des Slots im Intent, z.B. "Period"
|
||||||
|
* @param {object} config.idToEntityMap - Slot-ID -> Entity-ID, z.B. { tag: 'sensor...', woche: '...', monat: '...' }
|
||||||
|
* @param {function} config.template - (value, unit, slotNameSpoken, slotId, state) => string
|
||||||
|
* `slotId` ist die feste ID (z.B. "woche"), unabhängig vom genauen Wortlaut -
|
||||||
|
* nützlich für korrekte Grammatik (z.B. "diesen Tag" vs. "diese Woche").
|
||||||
|
* @param {function} [config.transformValue] - (rawValue: string) => string|number
|
||||||
|
* @param {string} [config.unknownSlotMessage] - Antwort, wenn der Slot nicht erkannt wurde.
|
||||||
|
* @param {function} [config.noMappingMessage] - (slotNameSpoken) => string, wenn die ID nicht im idToEntityMap steht.
|
||||||
|
* @param {function} [config.notFoundMessage] - (slotNameSpoken) => string, wenn der Sensorwert unknown/unavailable ist.
|
||||||
|
*/
|
||||||
|
function makeSlotSensorIntent({
|
||||||
|
intentName,
|
||||||
|
slotName,
|
||||||
|
idToEntityMap,
|
||||||
|
template,
|
||||||
|
transformValue = (v) => v,
|
||||||
|
unknownSlotMessage = 'Das habe ich leider nicht verstanden.',
|
||||||
|
noMappingMessage = (spoken) => `Für ${spoken} habe ich leider keine Auswertung.`,
|
||||||
|
notFoundMessage = (spoken) => `Ich konnte den Wert für ${spoken} nicht auslesen.`,
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
canHandle(handlerInput) {
|
||||||
|
return (
|
||||||
|
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
||||||
|
Alexa.getIntentName(handlerInput.requestEnvelope) === intentName
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async handle(handlerInput) {
|
||||||
|
const slots = handlerInput.requestEnvelope.request.intent.slots;
|
||||||
|
const { id, name: spokenName } = resolveSlot(slots[slotName], 'diesem Wert');
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return handlerInput.responseBuilder
|
||||||
|
.speak(unknownSlotMessage)
|
||||||
|
.reprompt(unknownSlotMessage)
|
||||||
|
.getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityId = idToEntityMap[id];
|
||||||
|
|
||||||
|
if (!entityId) {
|
||||||
|
return handlerInput.responseBuilder.speak(noMappingMessage(spokenName)).getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const state = await getHaState(entityId);
|
||||||
|
const rawValue = state.state;
|
||||||
|
|
||||||
|
if (UNKNOWN_VALUES.includes(rawValue) || rawValue === undefined || rawValue === null) {
|
||||||
|
return handlerInput.responseBuilder.speak(notFoundMessage(spokenName)).getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = transformValue(rawValue);
|
||||||
|
const unit = state.attributes?.unit_of_measurement || '';
|
||||||
|
const speakOutput = template(value, unit, spokenName, id, state);
|
||||||
|
|
||||||
|
return handlerInput.responseBuilder.speak(speakOutput).getResponse();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`HA API Fehler (${intentName}):`, err);
|
||||||
|
return handlerInput.responseBuilder
|
||||||
|
.speak('Ich konnte gerade keine Verbindung zu Home Assistant herstellen. Bitte versuch es später noch einmal.')
|
||||||
|
.getResponse();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grammatikalisch korrekte Formulierungen je Zeitraum-ID (Genus beachten:
|
||||||
|
// "der Tag" -> diesen, "die Woche" -> diese, "der Monat" -> diesen).
|
||||||
|
// In Templates verwenden statt periodSpoken direkt zu nehmen, sonst entsteht
|
||||||
|
// z.B. "diesen Woche" (falsch) statt "diese Woche" (richtig).
|
||||||
|
const PERIOD_PHRASES = {
|
||||||
|
tag: 'diesen Tag',
|
||||||
|
woche: 'diese Woche',
|
||||||
|
monat: 'diesen Monat',
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { makeApplianceIntent, makeSimpleSensorIntent, makeSlotSensorIntent, resolveSlot, PERIOD_PHRASES };
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// ---- Home Assistant API Client ----
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
const HA_URL = process.env.HA_URL; // z.B. "https://haos.vogt.de.com"
|
||||||
|
const HA_TOKEN = process.env.HA_TOKEN; // Long-Lived Access Token aus HA
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragt den aktuellen Zustand einer Home-Assistant-Entity ab.
|
||||||
|
* @param {string} entityId - z.B. "sensor.wohnzimmer_temperatur"
|
||||||
|
* @returns {Promise<{state: string, attributes: object}>}
|
||||||
|
*/
|
||||||
|
function getHaState(entityId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(`${HA_URL}/api/states/${entityId}`);
|
||||||
|
const options = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
path: url.pathname,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${HA_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => (data += chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
reject(new Error(`HA API Fehler: ${res.statusCode} - ${data}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(data));
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getHaState };
|
||||||
+67
-391
@@ -1,415 +1,91 @@
|
|||||||
const Alexa = require('ask-sdk-core');
|
const Alexa = require('ask-sdk-core');
|
||||||
const https = require('https');
|
|
||||||
|
|
||||||
// ---- Konfiguration ----
|
const { makeApplianceIntent, makeSimpleSensorIntent, makeSlotSensorIntent, PERIOD_PHRASES } = require('./factories');
|
||||||
// Werte kommen aus der .env-Datei im selben Verzeichnis (siehe unten)
|
const {
|
||||||
require('dotenv').config();
|
LaunchRequestHandler,
|
||||||
const HA_URL = process.env.HA_URL; // z.B. "https://haos.vogt.de.com"
|
HelpIntentHandler,
|
||||||
const HA_TOKEN = process.env.HA_TOKEN; // Long-Lived Access Token aus HA
|
CancelAndStopIntentHandler,
|
||||||
|
FallbackIntentHandler,
|
||||||
|
SessionEndedRequestHandler,
|
||||||
|
ErrorHandler,
|
||||||
|
} = require('./static-handlers');
|
||||||
|
|
||||||
// Raum-ID (aus dem RoomList Slot) -> climate Entity-ID in Home Assistant
|
// Hinweis: Temperaturabfragen laufen jetzt ueber die native Nabu-Casa-
|
||||||
const roomToClimateEntity = {
|
// Smart-Home-Integration, nicht mehr ueber diesen Custom Skill.
|
||||||
badezimmer: 'climate.badezimmer_heizungssteuerung',
|
|
||||||
kinderzimmer: 'climate.kinderzimmer_heizungssteuerung',
|
|
||||||
kueche: 'climate.kueche_heizungssteuerung',
|
|
||||||
schlafzimmer: 'climate.schlafzimmer_heizungssteuerung',
|
|
||||||
wohnzimmer: 'climate.wohnzimmer_heizungssteuerung',
|
|
||||||
// diele, gaeste_wc, balkon_hinten, balkon_vorne, kammer: kein Sensor vorhanden
|
|
||||||
};
|
|
||||||
|
|
||||||
// Aktueller Stromverbrauch (Leistung in Watt)
|
// ---- Einfache Sensor-Werte (kein Slot) ----
|
||||||
const POWER_ENTITY = 'sensor.kammer_netzbezug_plus_keller_power_calc';
|
|
||||||
|
|
||||||
// Stromkosten je Zeitraum (Periode-ID aus dem PeriodList Slot -> Entity-ID)
|
const GetPowerUsageIntentHandler = makeSimpleSensorIntent({
|
||||||
const periodToEnergyCostEntity = {
|
intentName: 'GetPowerUsageIntent',
|
||||||
|
entityId: 'sensor.kammer_netzbezug_plus_keller_power_calc',
|
||||||
|
transformValue: (v) => Math.round(parseFloat(v)),
|
||||||
|
template: (value, unit) => `Der aktuelle Stromverbrauch betraegt ${value} ${unit || 'Watt'}.`,
|
||||||
|
notFoundMessage: 'Ich konnte den aktuellen Stromverbrauch nicht auslesen.',
|
||||||
|
});
|
||||||
|
|
||||||
|
const GetPowerKammerUsageIntentHandler = makeSimpleSensorIntent({
|
||||||
|
intentName: 'GetPowerKammerUsageIntent',
|
||||||
|
entityId: 'sensor.kammer_serverschrank_leistung',
|
||||||
|
transformValue: (v) => Math.round(parseFloat(v)),
|
||||||
|
template: (value, unit) => `Der aktuelle Stromverbrauch in der Kammer betraegt ${value} ${unit || 'Watt'}.`,
|
||||||
|
notFoundMessage: 'Ich konnte den aktuellen Stromverbrauch in der Kammer nicht auslesen.',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- Stromkosten nach Zeitraum (Slot Period) ----
|
||||||
|
|
||||||
|
const GetEnergyCostIntentHandler = makeSlotSensorIntent({
|
||||||
|
intentName: 'GetEnergyCostIntent',
|
||||||
|
slotName: 'Period',
|
||||||
|
idToEntityMap: {
|
||||||
tag: 'sensor.netzbezug_kosten_tag',
|
tag: 'sensor.netzbezug_kosten_tag',
|
||||||
woche: 'sensor.netzbezug_kosten_woche',
|
woche: 'sensor.netzbezug_kosten_woche',
|
||||||
monat: 'sensor.netzbezug_kosten_monat',
|
monat: 'sensor.netzbezug_kosten_monat',
|
||||||
};
|
|
||||||
|
|
||||||
// Waschmaschine
|
|
||||||
const WASHING_MACHINE_STATUS_ENTITY = 'sensor.waschmaschine_betriebszustand';
|
|
||||||
const WASHING_MACHINE_PROGRESS_ENTITY = 'sensor.waschmaschine_programm_fortschritt';
|
|
||||||
const WASHING_MACHINE_END_TIME_ENTITY = 'sensor.waschmaschine_programm_endzeit';
|
|
||||||
|
|
||||||
// Trockner
|
|
||||||
const DRYER_STATUS_ENTITY = 'sensor.trockner_betriebszustand';
|
|
||||||
const DRYER_PROGRESS_ENTITY = 'sensor.trockner_programm_fortschritt';
|
|
||||||
const DRYER_END_TIME_ENTITY = 'sensor.trockner_programm_endzeit';
|
|
||||||
|
|
||||||
// Hilfsfunktion: HA REST API abfragen
|
|
||||||
function getHaState(entityId) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const url = new URL(`${HA_URL}/api/states/${entityId}`);
|
|
||||||
const options = {
|
|
||||||
hostname: url.hostname,
|
|
||||||
path: url.pathname,
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${HA_TOKEN}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
},
|
||||||
};
|
transformValue: (v) => parseFloat(v).toFixed(2).replace('.', ','),
|
||||||
|
template: (value, unit, periodSpoken, periodId) =>
|
||||||
const req = https.request(options, (res) => {
|
`Die Stromkosten fuer ${PERIOD_PHRASES[periodId] || periodSpoken} betragen ${value} ${unit || 'Euro'}.`,
|
||||||
let data = '';
|
unknownSlotMessage: 'Ich habe den Zeitraum leider nicht verstanden. Du kannst zum Beispiel nach Tag, Woche oder Monat fragen.',
|
||||||
res.on('data', (chunk) => (data += chunk));
|
|
||||||
res.on('end', () => {
|
|
||||||
if (res.statusCode !== 200) {
|
|
||||||
reject(new Error(`HA API Fehler: ${res.statusCode} - ${data}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
resolve(JSON.parse(data));
|
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('error', reject);
|
const GetEnergyCostKammerIntentHandler = makeSlotSensorIntent({
|
||||||
req.end();
|
intentName: 'GetEnergyCostKammerIntent',
|
||||||
|
slotName: 'Period',
|
||||||
|
idToEntityMap: {
|
||||||
|
tag: 'sensor.kammer_serverschrank_kosten_tag',
|
||||||
|
woche: 'sensor.kammer_serverschrank_kosten_woche',
|
||||||
|
monat: 'sensor.kammer_serverschrank_kosten_monat',
|
||||||
|
},
|
||||||
|
transformValue: (v) => parseFloat(v).toFixed(2).replace('.', ','),
|
||||||
|
template: (value, unit, periodSpoken, periodId) =>
|
||||||
|
`Die Stromkosten fuer die Kammer ${PERIOD_PHRASES[periodId] || periodSpoken} betragen ${value} ${unit || 'Euro'}.`,
|
||||||
|
unknownSlotMessage: 'Ich habe den Zeitraum leider nicht verstanden. Du kannst zum Beispiel nach Tag, Woche oder Monat fragen.',
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const LaunchRequestHandler = {
|
// ---- Geraete mit Status/Fortschritt/Endzeit ----
|
||||||
canHandle(handlerInput) {
|
|
||||||
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
|
|
||||||
},
|
|
||||||
handle(handlerInput) {
|
|
||||||
const speakOutput = 'Willkommen beim Hausassistent. Du kannst mich zum Beispiel fragen, wie warm es in einem Raum ist.';
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak(speakOutput)
|
|
||||||
.reprompt(speakOutput)
|
|
||||||
.getResponse();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const GetTemperatureIntentHandler = {
|
const GetWashingMachineIntentHandler = makeApplianceIntent({
|
||||||
canHandle(handlerInput) {
|
intentName: 'GetWashingMachineIntent',
|
||||||
return (
|
deviceName: 'Die Waschmaschine',
|
||||||
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
statusEntity: 'sensor.waschmaschine_betriebszustand',
|
||||||
Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetTemperatureIntent'
|
progressEntity: 'sensor.waschmaschine_programm_fortschritt',
|
||||||
);
|
endTimeEntity: 'sensor.waschmaschine_programm_endzeit',
|
||||||
},
|
});
|
||||||
async handle(handlerInput) {
|
|
||||||
const slots = handlerInput.requestEnvelope.request.intent.slots;
|
|
||||||
const roomSlot = slots.Room;
|
|
||||||
|
|
||||||
// Die ID und den sauberen Namen aus dem Slot Type RoomList holen
|
const GetDryerIntentHandler = makeApplianceIntent({
|
||||||
// (nicht den roh gesprochenen Text, der z.B. "wohnzimmer ist" statt "wohnzimmer" sein kann)
|
intentName: 'GetDryerIntent',
|
||||||
const resolvedValue = roomSlot?.resolutions?.resolutionsPerAuthority?.[0]?.values?.[0]?.value;
|
deviceName: 'Der Trockner',
|
||||||
const roomId = resolvedValue?.id;
|
statusEntity: 'sensor.trockner_betriebszustand',
|
||||||
const roomNameSpoken = resolvedValue?.name || roomSlot?.value || 'diesem Raum';
|
progressEntity: 'sensor.trockner_programm_fortschritt',
|
||||||
|
endTimeEntity: 'sensor.trockner_programm_endzeit',
|
||||||
if (!roomId) {
|
});
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak(`Ich kenne den Raum ${roomNameSpoken} leider nicht. Bitte versuch es mit einem anderen Raumnamen.`)
|
|
||||||
.reprompt('Für welchen Raum möchtest du die Temperatur wissen?')
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const entityId = roomToClimateEntity[roomId];
|
|
||||||
|
|
||||||
if (!entityId) {
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak(`Für ${roomNameSpoken} habe ich leider keinen Temperatursensor.`)
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const state = await getHaState(entityId);
|
|
||||||
const currentTemp = state.attributes?.current_temperature;
|
|
||||||
|
|
||||||
if (currentTemp === undefined || currentTemp === null) {
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak(`Ich konnte die aktuelle Temperatur für ${roomNameSpoken} nicht auslesen.`)
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const speakOutput = `Die Temperatur im ${roomNameSpoken} beträgt ${currentTemp} Grad.`;
|
|
||||||
return handlerInput.responseBuilder.speak(speakOutput).getResponse();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('HA API Fehler:', err);
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak('Ich konnte gerade keine Verbindung zu Home Assistant herstellen. Bitte versuch es später noch einmal.')
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const GetPowerUsageIntentHandler = {
|
|
||||||
canHandle(handlerInput) {
|
|
||||||
return (
|
|
||||||
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
|
||||||
Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetPowerUsageIntent'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
async handle(handlerInput) {
|
|
||||||
try {
|
|
||||||
const state = await getHaState(POWER_ENTITY);
|
|
||||||
const powerValue = state.state;
|
|
||||||
|
|
||||||
if (powerValue === undefined || powerValue === null || powerValue === 'unknown' || powerValue === 'unavailable') {
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak('Ich konnte den aktuellen Stromverbrauch nicht auslesen.')
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const unit = state.attributes?.unit_of_measurement || 'Watt';
|
|
||||||
const roundedPower = Math.round(parseFloat(powerValue));
|
|
||||||
const speakOutput = `Der aktuelle Stromverbrauch beträgt ${roundedPower} ${unit}.`;
|
|
||||||
return handlerInput.responseBuilder.speak(speakOutput).getResponse();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('HA API Fehler:', err);
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak('Ich konnte gerade keine Verbindung zu Home Assistant herstellen. Bitte versuch es später noch einmal.')
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const GetEnergyCostIntentHandler = {
|
|
||||||
canHandle(handlerInput) {
|
|
||||||
return (
|
|
||||||
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
|
||||||
Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetEnergyCostIntent'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
async handle(handlerInput) {
|
|
||||||
const slots = handlerInput.requestEnvelope.request.intent.slots;
|
|
||||||
const periodSlot = slots.Period;
|
|
||||||
|
|
||||||
const resolvedValue = periodSlot?.resolutions?.resolutionsPerAuthority?.[0]?.values?.[0]?.value;
|
|
||||||
const periodId = resolvedValue?.id;
|
|
||||||
const periodNameSpoken = resolvedValue?.name || periodSlot?.value || 'diesem Zeitraum';
|
|
||||||
|
|
||||||
if (!periodId) {
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak('Ich habe den Zeitraum leider nicht verstanden. Du kannst zum Beispiel nach Tag, Woche oder Monat fragen.')
|
|
||||||
.reprompt('Für welchen Zeitraum möchtest du die Stromkosten wissen?')
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const entityId = periodToEnergyCostEntity[periodId];
|
|
||||||
|
|
||||||
if (!entityId) {
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak(`Für ${periodNameSpoken} habe ich leider keine Kostenauswertung.`)
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const state = await getHaState(entityId);
|
|
||||||
const costValue = state.state;
|
|
||||||
|
|
||||||
if (costValue === undefined || costValue === null || costValue === 'unknown' || costValue === 'unavailable') {
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak(`Ich konnte die Stromkosten für ${periodNameSpoken} nicht auslesen.`)
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const unit = state.attributes?.unit_of_measurement || 'Euro';
|
|
||||||
const roundedCost = parseFloat(costValue).toFixed(2).replace('.', ',');
|
|
||||||
const speakOutput = `Die Stromkosten für diesen ${periodNameSpoken} betragen ${roundedCost} ${unit}.`;
|
|
||||||
return handlerInput.responseBuilder.speak(speakOutput).getResponse();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('HA API Fehler:', err);
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak('Ich konnte gerade keine Verbindung zu Home Assistant herstellen. Bitte versuch es später noch einmal.')
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const GetWashingMachineIntentHandler = {
|
|
||||||
canHandle(handlerInput) {
|
|
||||||
return (
|
|
||||||
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
|
||||||
Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetWashingMachineIntent'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
async handle(handlerInput) {
|
|
||||||
try {
|
|
||||||
const [statusState, progressState, endTimeState] = await Promise.all([
|
|
||||||
getHaState(WASHING_MACHINE_STATUS_ENTITY).catch(() => null), // optional, falls Entity nicht existiert
|
|
||||||
getHaState(WASHING_MACHINE_PROGRESS_ENTITY),
|
|
||||||
getHaState(WASHING_MACHINE_END_TIME_ENTITY),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Betriebszustand prüfen (z.B. "fertig", "inaktiv") - analog zum HA-Template
|
|
||||||
const betrieb = statusState?.state?.toLowerCase().trim();
|
|
||||||
const inactiveStates = ['fertig', 'inaktiv', 'finished', 'inactive'];
|
|
||||||
if (betrieb && inactiveStates.includes(betrieb)) {
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak('Die Waschmaschine läuft aktuell nicht.')
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const progress = progressState.state;
|
|
||||||
const endTimeRaw = endTimeState.state;
|
|
||||||
const unknownValues = ['unknown', 'unavailable', 'none'];
|
|
||||||
|
|
||||||
if (unknownValues.includes(progress) || unknownValues.includes(endTimeRaw)) {
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak('Die Waschmaschine läuft aktuell nicht.')
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const endTime = new Date(endTimeRaw);
|
|
||||||
const now = new Date();
|
|
||||||
const diffMs = endTime.getTime() - now.getTime();
|
|
||||||
|
|
||||||
if (diffMs <= 0) {
|
|
||||||
return handlerInput.responseBuilder.speak('Die Waschmaschine ist fertig.').getResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const roundedProgress = Math.round(parseFloat(progress));
|
|
||||||
|
|
||||||
// Endzeit als HH:MM in deutscher Zeitzone formatieren
|
|
||||||
const timeFormatted = new Intl.DateTimeFormat('de-DE', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
timeZone: 'Europe/Berlin',
|
|
||||||
}).format(endTime);
|
|
||||||
|
|
||||||
const speakOutput = `Die Waschmaschine ist ${roundedProgress} Prozent fertig und endet um ${timeFormatted} Uhr.`;
|
|
||||||
return handlerInput.responseBuilder.speak(speakOutput).getResponse();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('HA API Fehler:', err);
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak('Ich konnte gerade keine Verbindung zu Home Assistant herstellen. Bitte versuch es später noch einmal.')
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const GetDryerIntentHandler = {
|
|
||||||
canHandle(handlerInput) {
|
|
||||||
return (
|
|
||||||
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
|
||||||
Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetDryerIntent'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
async handle(handlerInput) {
|
|
||||||
try {
|
|
||||||
const [statusState, progressState, endTimeState] = await Promise.all([
|
|
||||||
getHaState(DRYER_STATUS_ENTITY).catch(() => null),
|
|
||||||
getHaState(DRYER_PROGRESS_ENTITY),
|
|
||||||
getHaState(DRYER_END_TIME_ENTITY),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const betrieb = statusState?.state?.toLowerCase().trim();
|
|
||||||
const inactiveStates = ['fertig', 'inaktiv', 'finished', 'inactive'];
|
|
||||||
if (betrieb && inactiveStates.includes(betrieb)) {
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak('Der Trockner läuft aktuell nicht.')
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const progress = progressState.state;
|
|
||||||
const endTimeRaw = endTimeState.state;
|
|
||||||
const unknownValues = ['unknown', 'unavailable', 'none'];
|
|
||||||
|
|
||||||
if (unknownValues.includes(progress) || unknownValues.includes(endTimeRaw)) {
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak('Der Trockner läuft aktuell nicht.')
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const endTime = new Date(endTimeRaw);
|
|
||||||
const now = new Date();
|
|
||||||
const diffMs = endTime.getTime() - now.getTime();
|
|
||||||
|
|
||||||
if (diffMs <= 0) {
|
|
||||||
return handlerInput.responseBuilder.speak('Der Trockner ist fertig.').getResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const roundedProgress = Math.round(parseFloat(progress));
|
|
||||||
|
|
||||||
const timeFormatted = new Intl.DateTimeFormat('de-DE', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
timeZone: 'Europe/Berlin',
|
|
||||||
}).format(endTime);
|
|
||||||
|
|
||||||
const speakOutput = `Der Trockner ist ${roundedProgress} Prozent fertig und endet um ${timeFormatted} Uhr.`;
|
|
||||||
return handlerInput.responseBuilder.speak(speakOutput).getResponse();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('HA API Fehler:', err);
|
|
||||||
return handlerInput.responseBuilder
|
|
||||||
.speak('Ich konnte gerade keine Verbindung zu Home Assistant herstellen. Bitte versuch es später noch einmal.')
|
|
||||||
.getResponse();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const HelpIntentHandler = {
|
|
||||||
canHandle(handlerInput) {
|
|
||||||
return (
|
|
||||||
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
|
||||||
Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
handle(handlerInput) {
|
|
||||||
const speakOutput = 'Du kannst mich zum Beispiel fragen: wie warm ist es im Wohnzimmer.';
|
|
||||||
return handlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const CancelAndStopIntentHandler = {
|
|
||||||
canHandle(handlerInput) {
|
|
||||||
return (
|
|
||||||
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
|
||||||
(Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent' ||
|
|
||||||
Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent')
|
|
||||||
);
|
|
||||||
},
|
|
||||||
handle(handlerInput) {
|
|
||||||
const speakOutput = 'Bis bald!';
|
|
||||||
return handlerInput.responseBuilder.speak(speakOutput).getResponse();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const FallbackIntentHandler = {
|
|
||||||
canHandle(handlerInput) {
|
|
||||||
return (
|
|
||||||
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
|
||||||
Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.FallbackIntent'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
handle(handlerInput) {
|
|
||||||
const speakOutput = 'Das habe ich nicht verstanden. Du kannst mich zum Beispiel fragen, wie warm es im Wohnzimmer ist.';
|
|
||||||
return handlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const SessionEndedRequestHandler = {
|
|
||||||
canHandle(handlerInput) {
|
|
||||||
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
|
|
||||||
},
|
|
||||||
handle(handlerInput) {
|
|
||||||
return handlerInput.responseBuilder.getResponse();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ErrorHandler = {
|
|
||||||
canHandle() {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
handle(handlerInput, error) {
|
|
||||||
console.error(`~~~~ Error handled: ${error.stack}`);
|
|
||||||
const speakOutput = 'Entschuldigung, da ist etwas schiefgelaufen. Bitte versuch es noch einmal.';
|
|
||||||
return handlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.handler = Alexa.SkillBuilders.custom()
|
exports.handler = Alexa.SkillBuilders.custom()
|
||||||
.addRequestHandlers(
|
.addRequestHandlers(
|
||||||
LaunchRequestHandler,
|
LaunchRequestHandler,
|
||||||
GetTemperatureIntentHandler,
|
|
||||||
GetPowerUsageIntentHandler,
|
GetPowerUsageIntentHandler,
|
||||||
|
GetPowerKammerUsageIntentHandler,
|
||||||
GetEnergyCostIntentHandler,
|
GetEnergyCostIntentHandler,
|
||||||
|
GetEnergyCostKammerIntentHandler,
|
||||||
GetWashingMachineIntentHandler,
|
GetWashingMachineIntentHandler,
|
||||||
GetDryerIntentHandler,
|
GetDryerIntentHandler,
|
||||||
HelpIntentHandler,
|
HelpIntentHandler,
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// ---- Standard-Handler (Pflicht für jeden Alexa Skill) ----
|
||||||
|
const Alexa = require('ask-sdk-core');
|
||||||
|
|
||||||
|
const LaunchRequestHandler = {
|
||||||
|
canHandle(handlerInput) {
|
||||||
|
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
|
||||||
|
},
|
||||||
|
handle(handlerInput) {
|
||||||
|
const speakOutput = 'Willkommen beim Hausassistent. Du kannst mich zum Beispiel fragen, wie warm es in einem Raum ist.';
|
||||||
|
return handlerInput.responseBuilder
|
||||||
|
.speak(speakOutput)
|
||||||
|
.reprompt(speakOutput)
|
||||||
|
.getResponse();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const HelpIntentHandler = {
|
||||||
|
canHandle(handlerInput) {
|
||||||
|
return (
|
||||||
|
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
||||||
|
Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
handle(handlerInput) {
|
||||||
|
const speakOutput = 'Du kannst mich zum Beispiel fragen: wie warm ist es im Wohnzimmer.';
|
||||||
|
return handlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const CancelAndStopIntentHandler = {
|
||||||
|
canHandle(handlerInput) {
|
||||||
|
return (
|
||||||
|
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
||||||
|
(Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent' ||
|
||||||
|
Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
handle(handlerInput) {
|
||||||
|
const speakOutput = 'Bis bald!';
|
||||||
|
return handlerInput.responseBuilder.speak(speakOutput).getResponse();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const FallbackIntentHandler = {
|
||||||
|
canHandle(handlerInput) {
|
||||||
|
return (
|
||||||
|
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
|
||||||
|
Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.FallbackIntent'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
handle(handlerInput) {
|
||||||
|
const speakOutput = 'Das habe ich nicht verstanden. Du kannst mich zum Beispiel fragen, wie warm es im Wohnzimmer ist.';
|
||||||
|
return handlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const SessionEndedRequestHandler = {
|
||||||
|
canHandle(handlerInput) {
|
||||||
|
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
|
||||||
|
},
|
||||||
|
handle(handlerInput) {
|
||||||
|
return handlerInput.responseBuilder.getResponse();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ErrorHandler = {
|
||||||
|
canHandle() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
handle(handlerInput, error) {
|
||||||
|
console.error(`~~~~ Error handled: ${error.stack}`);
|
||||||
|
const speakOutput = 'Entschuldigung, da ist etwas schiefgelaufen. Bitte versuch es noch einmal.';
|
||||||
|
return handlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
LaunchRequestHandler,
|
||||||
|
HelpIntentHandler,
|
||||||
|
CancelAndStopIntentHandler,
|
||||||
|
FallbackIntentHandler,
|
||||||
|
SessionEndedRequestHandler,
|
||||||
|
ErrorHandler,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user