const Alexa = require('ask-sdk-core'); const https = require('https'); // ---- Konfiguration ---- // Werte kommen aus der .env-Datei im selben Verzeichnis (siehe unten) require('dotenv').config(); 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 // Raum-ID (aus dem RoomList Slot) -> climate Entity-ID in Home Assistant const roomToClimateEntity = { 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 }; // 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', }, }; 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(); }); } 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 GetTemperatureIntentHandler = { canHandle(handlerInput) { return ( Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' && Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetTemperatureIntent' ); }, 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 // (nicht den roh gesprochenen Text, der z.B. "wohnzimmer ist" statt "wohnzimmer" sein kann) const resolvedValue = roomSlot?.resolutions?.resolutionsPerAuthority?.[0]?.values?.[0]?.value; const roomId = resolvedValue?.id; const roomNameSpoken = resolvedValue?.name || roomSlot?.value || 'diesem Raum'; 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 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() .addRequestHandlers( LaunchRequestHandler, GetTemperatureIntentHandler, HelpIntentHandler, CancelAndStopIntentHandler, FallbackIntentHandler, SessionEndedRequestHandler ) .addErrorHandlers(ErrorHandler) .lambda();