「アレクサ、部屋の温度を教えて」を実現のための第7歩になります。
Alexaと対話できるようになったのですが、Blynkをローカルサーバーに変えたら使えなくなりました。原因はインターネットからBlynk local serverが見えていなかったことと、Blynkを使う上でのポート設定でした。
しっかりDocumentを読めば書いてあるのですが、そこを飛ばして、見よう見まねとコピペでやっているので、なかなかうまく回りません。
状況
クラウドサーバのBlynkでは、問題なくAlexaが温度をアナウンスしてくれたのですが、BlynkのTokenをlocal server発行のものに変えたら、「スキルがリクエストに正しく応答できませんでした。」となりうまくいきません。
などとやると、温度センサ(BME280)の値は読めています。
LAN内ではOKなので、外からBlynk local serverが見えていないようです。Blynkがそういうことをやってくれていると勝手に思っていたのですが、勝手な思い込みでした。
対策1:Raspberry Piに外部ネットワークからアクセス
Raspberry Piに外部ネットワークからアクセスできる様にして携帯でペットを遠隔監視する方法 - Qiita
はじめに 前回、赤外線カメラモジュールを繋げて「mjpg-streamer」を使い、家内のネットワークのブラウザから動画を見れる様にしました。 【Raspberry Piで赤外線カメラモジュールを使ってみる】 今回はその続きで...
1. Raspberry PiのプライベートIPアドレスの固定
2. 外部にサーバを公開
1)ルータにグローバルIPアドレスを設定する
2)ポートマッピングの設定 (ルーターのポートを解放する)
外部からRaspberryPiアクセスできる様にルーターのポートを解放し、解放したポートを先ほど固定したRaspberryPiのプライベートIPアドレスと紐づけます。
ブラウザから、192.168.0.1 などとややって、ルータにログインします。
左列の「詳細設定」から「IPv4ポートマッピング設定」を選びます。次の上の画面は既に設定された画面です。新規に設定するときは、右下の「追加」をタップします。
2番目の画面になり、ここで設定をします。
- LAN側ホスト:Blynk local serverのIPアドレスを指定します。たとえば、192.168.0.22
- プロトコル:TCPを指定します。
- ポート番号 :8080 です。8080だけを開けたいので、左・右の空白に8080と入力します。
- 優先度:1~50の間で入力します。一番初めにポート開放するなら1です。次は2,3,4….となります。
3)IPパケットフィルタリングの設定(確認のみ)
4)ルータでも、Raspberry PiのプライベートIPアドレスを固定
Raspberry PiのMACアドレスとRaspberry PiのIPv4アドレスを紐づけ(固定化)します。
コマンドプロンプトで「arp -a」とやるとリストが出てきます。b8-27で始まるのがRaspberry pi のMACアドレスで、その行の左がRaspberry pi のIPアドレスです。
ルーターに戻って、「詳細設定」のなかの「DHCPv4固定割当設定」をクリックして、右下の「追加」をクリックします。3. ファイアウォール設定

【パクろう】ラズパイでファイアーウォールを設定する方法
みなさんラズパイでファイアウォールの設定をしていますか?ここではその設定方法や、そのまま使えるお勧め設定を載せました。ラズパイのファイアウォール設定はこれを見れば完璧です。
Blynk側のセキュリティは、次のように記されています。
Blynkサーバーには、さまざまなセキュリティレベル用に開いている5つのポートがあります。ハードウェアは、その機能に応じて、443(9443)または80(8080)への接続を選択する場合があります。アプリとサーバー間の接続は常にSSL / TLSを介して行われるため、常に保護されています。ハードウェアとサーバー間の接続は、ハードウェアの機能によって異なります。引用元:https://docs.blynk.cc/Security
- 80-ハードウェアのプレーンTCP接続(セキュリティなし)
- 8080-ハードウェア用のプレーンTCP接続(セキュリティなし)
- 443-モバイルアプリとSSLを使用したハードウェアのSSL / TLS接続
- 9443-モバイルアプリとSSLを使用したハードウェアのSSL / TLS接続
対策2:AWS Lambda関数の変更
クラウドのBlynkサーバー用に作ったLambda関数をBlynk local server用に変更します。変更箇所はクラウドサーバーをローカルサーバーに変え、加えてポートを変更します。
変更前
http.get({
host: 'blynk-cloud.com',
path: '/' + blynkAuthToken + '/get/' + blynkPin,
port: '80'
}
変更後
http.get({
host: globalIP, // globalIPはルータのグローバルIPアドレス
path: '/' + blynkAuthToken + '/get/' + blynkPin,
port: '8080'
}
portについては、上の方にも書きましたがGitHubにつぎのようにあるので'8080'にしたらうまくいきました。
Run the server on default 'hardware port 8080' and default 'application port 9443' (SSL port)
Alexa Skill ローカルサーバー用スクリプト
対話モデルのjsonコード
{
"interactionModel": {
"languageModel": {
"invocationName": "ローカル",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "HelloWorldIntent",
"slots": [],
"samples": [
"hello",
"ハロー",
"こんにちは"
]
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "GetMeasurement",
"slots": [
{
"name": "Location",
"type": "LIST_OF_LOCATIONS"
},
{
"name": "Measurement",
"type": "LIST_OF_MEASUREMENTS"
}
],
"samples": [
"GetMeasurement {Location} ",
"GetMeasurement {Location} は",
"GetMeasurement {Location} はいくつ",
"GetMeasurement {Location} を教えて",
"GetMeasurement {Location} の {Measurement} はどんだけ",
"GetMeasurement {Measurement} はどんだけ",
"GetMeasurement {Location} は {Measurement}",
"GetMeasurement {Location} の {Measurement} を教えて",
"GetMeasurement {Location} の {Measurement} はいくつ",
"GetMeasurement {Location} の {Measurement} は",
"GetMeasurement {Location} の {Measurement}",
"GetMeasurement {Measurement} を教えて",
"GetMeasurement {Measurement} はいくつ",
"GetMeasurement {Measurement} は",
"GetMeasurement {Measurement}"
]
}
],
"types": [
{
"name": "LIST_OF_LOCATIONS",
"values": [
{
"name": {
"value": "屋根裏",
"synonyms": [
"天井裏"
]
}
},
{
"name": {
"value": "仏間",
"synonyms": [
"和室"
]
}
},
{
"name": {
"value": "二階"
}
}
]
},
{
"name": "LIST_OF_MEASUREMENTS",
"values": [
{
"name": {
"value": "熱中度指数",
"synonyms": [
"暑さ度",
"熱中度",
"暑さ指数"
]
}
},
{
"name": {
"value": "気圧",
"synonyms": [
"大気圧"
]
}
},
{
"name": {
"value": "湿度",
"synonyms": [
"湿気"
]
}
},
{
"name": {
"value": "気温",
"synonyms": [
"何度",
"室温",
"温度"
]
}
}
]
}
]
}
}
}
AWS Lambda
index.js;
'use strict';
const http = require('http');
// --------------- Helpers that build all of the responses -----------------------
function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
return {
outputSpeech: {
type: 'PlainText',
text: output,
},
card: {
type: 'Simple',
title: `SessionSpeechlet - ${title}`,
content: `SessionSpeechlet - ${output}`,
},
reprompt: {
outputSpeech: {
type: 'PlainText',
text: repromptText,
},
},
shouldEndSession,
};
}
function buildResponse(sessionAttributes, speechletResponse) {
return {
version: '1.0',
sessionAttributes,
response: speechletResponse,
};
}
// --------------- Functions that control the skill's behavior -----------------------
function getWelcomeResponse(callback) {
const sessionAttributes = {};
const cardTitle = 'Welcome';
const speechOutput = "二階の温度を教えてなどと問いかけると、それを答えます。";
const repromptText = "私はこの部屋を見てるわよっ!何が知りたいのっ! ってかっ";
const shouldEndSession = false;
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
function handleSessionEndRequest(callback) {
const cardTitle = 'Session Ended';
const speechOutput = 'Thank you for trying the Alexa Skills Kit sample. Have a nice day!';
const shouldEndSession = true;
callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}
function createLocationAttributes(location) {
return { location };
}
async function readTemperatureInSession(intent, session, callback) {
const cardTitle = intent.name;
const LocationSlot = intent.slots.Location;
const MeasurementSlot = intent.slots.Measurement;
let repromptText = '';
let sessionAttributes = {};
const shouldEndSession = false;
let speechOutput = '';
let body = '';
const globalIP = '***.***.***.***';
const blynkport = '8080';
let blynkAuthToken;
let blynkPin;
let location = LocationSlot.value || (session.attributes && session.attributes.location) || '二階';
switch (location) {
case '二階':
blynkAuthToken = '********************************';
break;
case '仏間':
blynkAuthToken = '********************************';
break;
case '屋根裏':
blynkAuthToken = '********************************';
break;
}
let measurement = MeasurementSlot.value || '温度';
switch (measurement) {
case '何度':
case '気温':
case '室温':
case '温度':
blynkPin = 'V0';
break;
case '湿気':
case '湿度':
blynkPin = 'V1';
break;
case '大気圧':
case '気圧':
blynkPin = 'V2';
break;
case '熱中度指数':
case '熱中度':
case '暑さ指数':
case '暑さ度':
case '暑さ':
blynkPin = 'V4';
break;
}
try {
const httpPromise = new Promise((resolve, reject) => {
http.get({
host: globalIP,
path: `/${blynkAuthToken}/get/${blynkPin}`,
port: blynkport,
}, (response) => {
response.on('data', (d) => {
body += d;
});
response.on('end', () => {
resolve('Done Sending');
});
});
});
await httpPromise;
const info = parseFloat(JSON.parse(body));
speechOutput = location + 'の ' + measurement + 'は ';
repromptText = speechOutput;
switch (measurement) {
default:
case '何度':
case '気温':
case '室温':
case '温度':
speechOutput += `${info.toFixed(1)}度 です。`;
repromptText += `${info.toFixed(1)}℃です。`;
break;
case '湿気':
case '湿度':
speechOutput += `${info.toFixed(1)}パーセント です。`;
repromptText += `${info.toFixed(1)}%です。`;
break;
case '大気圧':
case '気圧':
speechOutput += `${info.toFixed(0)}ヘクトパスカル です。`;
repromptText += `${info.toFixed(0)}hPaです。`;
break;
case '熱中度指数':
case '熱中度':
case '暑さ指数':
case '暑さ度':
case '暑さ':
let annotation = '';
if (info >= 31) {
annotation = '危険! ブラックです。';
} else if (info >= 28) {
annotation = '厳重警戒! レッドです。';
} else if (info >= 25) {
annotation = '警戒! オレンジです。';
} else if (info >= 21) {
annotation = '注意です。';
}
speechOutput += `${info.toFixed(0)}度 です。${annotation}`;
repromptText += `${info.toFixed(0)}℃です。${annotation}`;
break;
}
sessionAttributes = createLocationAttributes(location);
callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
} catch (error) {
console.error(`Error: ${error}`);
throw new Error('Error in reading temperature');
}
}
// --------------- Events -----------------------
function onSessionStarted(sessionStartedRequest, session) {
console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}`);
}
function onLaunch(launchRequest, session, callback) {
console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`);
getWelcomeResponse((sessionAttributes, speechletResponse) => {
callback(sessionAttributes, speechletResponse);
});
}
function onIntent(intentRequest, session, callback) {
console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);
const intent = intentRequest.intent;
const intentName = intentRequest.intent.name;
if (intentName === 'GetMeasurement') {
readTemperatureInSession(intent, session, callback);
} else if (intentName === 'AMAZON.HelpIntent') {
getWelcomeResponse((sessionAttributes, speechletResponse) => {
callback(sessionAttributes, speechletResponse);
});
} else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
handleSessionEndRequest((sessionAttributes, speechletResponse) => {
callback(sessionAttributes, speechletResponse);
});
} else {
throw new Error('Invalid intent');
}
}
function onSessionEnded(sessionEndedRequest, session) {
console.log(`onSessionEnded requestId=${sessionEndedRequest.requestId}, sessionId=${session.sessionId}`);
}
exports.handler = (event, context) => {
try {
console.log(`event.session.application.applicationId=${event.session.application.applicationId}`);
if (event.session.new) {
onSessionStarted({ requestId: event.request.requestId }, event.session);
}
if (event.request.type === 'LaunchRequest') {
onLaunch(event.request, event.session, (sessionAttributes, speechletResponse) => {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === 'IntentRequest') {
onIntent(event.request, event.session, (sessionAttributes, speechletResponse) => {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === 'SessionEndedRequest') {
onSessionEnded(event.request, event.session);
context.succeed();
}
} catch (e) {
console.error(`Exception: ${e}`);
context.fail(`Exception: ${e}`);
}
};
グローバルIPアドレスの自動取得:失敗
- アドレス取得をLambda関数に実装 (
http.get('http://checkip.amazonaws.com')
) すると、 LambdaのパブリックIP を取得してしまうため、ユーザーの実際のグローバルIPを取得できない。 - AWS Lambda関数が外部サイトからグローバルIPアドレスを取得できない理由は、Lambda関数が固定のIPアドレスを持たないかららしい。Lambda関数は、実行されるたびに異なるIPアドレスが割り当てられるため、外部サイトから見ると常に変わることになる。
- 「ユーザーのグローバルIP」を取得したい場合は、クライアント(ユーザーのデバイスやブラウザ)でIPを取得し、それをLambdaにプッシュ送信する設計にする必要がある。
'use strict';
const http = require('http');
const https = require('https');
// --------------- Helpers that build all of the responses -----------------------
function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
return {
outputSpeech: {
type: 'PlainText',
text: output,
},
card: {
type: 'Simple',
title: `SessionSpeechlet - ${title}`,
content: `SessionSpeechlet - ${output}`,
},
reprompt: {
outputSpeech: {
type: 'PlainText',
text: repromptText,
},
},
shouldEndSession,
};
}
function buildResponse(sessionAttributes, speechletResponse) {
return {
version: '1.0',
sessionAttributes,
response: speechletResponse,
};
}
// --------------- Functions that control the skill's behavior -----------------------
function getWelcomeResponse(callback) {
const sessionAttributes = {};
const cardTitle = 'Welcome';
const speechOutput = "二階の温度を教えてなどと問いかけると、それを答えます。";
const repromptText = "私はこの部屋を見てるわよっ!何が知りたいのっ! ってかっ";
const shouldEndSession = false;
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
function handleSessionEndRequest(callback) {
const cardTitle = 'Session Ended';
const speechOutput = 'Thank you for trying the Alexa Skills Kit sample. Have a nice day!';
const shouldEndSession = true;
callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}
function createLocationAttributes(location) {
return { location };
}
async function getGlobalIP() {
return new Promise((resolve, reject) => {
http.get('https://checkip.amazonaws.com', (resp) => {
let data = '';
resp.on('data', (chunk) => { data += chunk; });
resp.on('end', () => {
try {
const ip = data.trim(); // JSON.parse は不要
console.log(`Global IP: ${ip}`);
resolve(ip);
} catch (e) {
reject(`Error parsing IP: ${e.message}`);
}
});
}).on("error", (err) => {
reject(`Error: ${err.message}`);
});
});
}
async function readTemperatureInSession(intent, session, callback) {
const cardTitle = intent.name;
const LocationSlot = intent.slots.Location;
const MeasurementSlot = intent.slots.Measurement;
let repromptText = '';
let sessionAttributes = {};
const shouldEndSession = false;
let speechOutput = '';
let body = '';
const blynkport = '8080';
let blynkAuthToken;
let blynkPin;
let location = LocationSlot.value || (session.attributes && session.attributes.location) || '二階';
switch (location) {
case '二階':
blynkAuthToken = '********************************';
break;
case '仏間':
blynkAuthToken = '********************************';
break;
case '屋根裏':
blynkAuthToken = '********************************';
break;
}
let measurement = MeasurementSlot.value || '温度';
switch (measurement) {
case '何度':
case '気温':
case '室温':
case '温度':
blynkPin = 'V0';
break;
case '湿気':
case '湿度':
blynkPin = 'V1';
break;
case '大気圧':
case '気圧':
blynkPin = 'V2';
break;
case '熱中度指数':
case '熱中度':
case '暑さ指数':
case '暑さ度':
case '暑さ':
blynkPin = 'V4';
break;
}
try {
const globalIP = await getGlobalIP();
console.log(`Global IP used: ${globalIP}`); // デバッグ用のログ出力
const httpPromise = new Promise((resolve, reject) => {
http.get({
host: globalIP,
path: `/${blynkAuthToken}/get/${blynkPin}`,
port: blynkport,
}, (response) => {
response.on('data', (d) => {
body += d;
});
response.on('end', () => {
resolve('Done Sending');
});
}).on("error", (err) => {
reject(`Error: ${err.message}`);
});
});
await httpPromise;
const info = parseFloat(JSON.parse(body));
speechOutput = location + 'の ' + measurement + 'は ';
repromptText = speechOutput;
switch (measurement) {
default:
case '何度':
case '気温':
case '室温':
case '温度':
speechOutput += `${info.toFixed(1)}度 です。`;
repromptText += `${info.toFixed(1)}℃です。`;
break;
case '湿気':
case '湿度':
speechOutput += `${info.toFixed(1)}パーセント です。`;
repromptText += `${info.toFixed(1)}%です。`;
break;
case '大気圧':
case '気圧':
speechOutput += `${info.toFixed(0)}ヘクトパスカル です。`;
repromptText += `${info.toFixed(0)}hPaです。`;
break;
case '熱中度指数':
case '熱中度':
case '暑さ指数':
case '暑さ度':
case '暑さ':
let annotation = '';
if (info >= 31) {
annotation = '危険! ブラックです。';
} else if (info >= 28) {
annotation = '厳重警戒! レッドです。';
} else if (info >= 25) {
annotation = '警戒! オレンジです。';
} else if (info >= 21) {
annotation = '注意です。';
}
speechOutput += `${info.toFixed(0)}度 です。${annotation}`;
repromptText += `${info.toFixed(0)}℃です。${annotation}`;
break;
}
sessionAttributes = createLocationAttributes(location);
callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
} catch (error) {
console.error(`Error: ${error}`);
throw new Error('Error in reading temperature');
}
}
// --------------- Events -----------------------
function onSessionStarted(sessionStartedRequest, session) {
console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}`);
}
function onLaunch(launchRequest, session, callback) {
console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`);
getWelcomeResponse((sessionAttributes, speechletResponse) => {
callback(sessionAttributes, speechletResponse);
});
}
function onIntent(intentRequest, session, callback) {
console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);
const intent = intentRequest.intent;
const intentName = intentRequest.intent.name;
if (intentName === 'GetMeasurement') {
readTemperatureInSession(intent, session, callback);
} else if (intentName === 'AMAZON.HelpIntent') {
getWelcomeResponse((sessionAttributes, speechletResponse) => {
callback(sessionAttributes, speechletResponse);
});
} else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
handleSessionEndRequest((sessionAttributes, speechletResponse) => {
callback(sessionAttributes, speechletResponse);
});
} else {
throw new Error('Invalid intent');
}
}
function onSessionEnded(sessionEndedRequest, session) {
console.log(`onSessionEnded requestId=${sessionEndedRequest.requestId}, sessionId=${session.sessionId}`);
}
exports.handler = (event, context) => {
try {
console.log(`event.session.application.applicationId=${event.session.application.applicationId}`);
if (event.session.new) {
onSessionStarted({ requestId: event.request.requestId }, event.session);
}
if (event.request.type === 'LaunchRequest') {
onLaunch(event.request, event.session, (sessionAttributes, speechletResponse) => {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === 'IntentRequest') {
onIntent(event.request, event.session, (sessionAttributes, speechletResponse) => {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === 'SessionEndedRequest') {
onSessionEnded(event.request, event.session);
context.succeed();
}
} catch (e) {
console.error(`Exception: ${e}`);
context.fail(`Exception: ${e}`);
}
};
;
Lambda関数のIPアドレスを固定化する
その考え方は正しいです。AWS Lambda関数が外部サイトからのIP制限を受けることなくグローバルIPを取得するには、固定のIPアドレスを持たせる方法を検討する必要があります。いくつかの方法がありますが、それぞれメリット・デメリットがあります。
Lambda関数に固定IPを持たせる方法
① VPC + NAT Gateway を利用
LambdaをVPC(仮想プライベートクラウド)内で実行し、NAT Gateway を経由してインターネットへ接続させることで、固定IPアドレスを割り当てる方法です。
✅ メリット
- 完全に固定のIPアドレスを使用可能
- 他のAWSリソース(RDS, EC2など)と安全に連携可能
- AWS公式の方法であり、サポートが手厚い
❌ デメリット
- VPCのセットアップが必要(サブネット、NAT Gateway、ルートテーブル設定)
- 追加コストが発生(NAT Gatewayの使用料金がかかる)
設定方法
- LambdaをVPC内のプライベートサブネットに配置
- NAT Gateway を作成
- Elastic IP を関連付ける(これが固定のIPアドレスとなる)
- VPCルートテーブルを設定
- プライベートサブネットからインターネットアクセスがNAT Gatewayを経由するように設定
- LambdaのVPC設定でプライベートサブネットを選択
- Lambda関数を実行し、外部サイトへアクセス
- NAT Gateway経由でインターネットへアクセスするため、固定IPが適用される
② AWS Global Accelerator を利用
AWS Global Acceleratorを使用して、Lambdaが経由するIPアドレスを固定する方法です。
✅ メリット
- VPCなしでも使用可能
- 複数のリージョンで利用可能
- レイテンシ最適化
❌ デメリット
- 追加コストが発生
- セットアップが複雑
- すべての外部APIに対応するわけではない
→ NAT Gatewayを使う方がシンプルで一般的
③ API Gateway + Static IP Proxy
API Gateway と EC2 インスタンスを組み合わせ、Lambdaの外部通信を固定IPを持つプロキシサーバー経由にする方法です。
✅ メリット
- Lambdaの外部アクセスを制御しやすい
- EC2に複数のIPを割り当てることが可能
❌ デメリット
- EC2の運用管理が必要
- プロキシのメンテナンスが必要
→ 基本的にはNAT Gatewayの方が推奨される
結論
- 確実に固定IPを使いたいなら → LambdaをVPC + NAT Gateway に配置
- コストを抑えつつ高速化もしたいなら → AWS Global Accelerator
- 独自のプロキシを運用したいなら → EC2を利用したAPI Gateway Proxy
AWS Lambdaはデフォルトでは固定IPを持たない仕様ですが、VPC + NAT Gateway を使うことで固定IPを実現可能です。コストはかかりますが、運用上の安定性やセキュリティを考えるとVPC + NAT Gateway の方法が最も一般的で推奨されるでしょう。
Alexa Skills Kitでテスト
課題
(1) オリジナルスキルは「ローカルを開いて」とかの呼び出し名から始めなければならず。2回のやり取りが必要です。温度を知りたいだけならば、Alexaに聞くのは面倒です。Blynkアプリを開く方が簡単です。
(2) ルーターのグローバルIPアドレスはルーターが再起動すると変わります。私の環境では何かの理由でルーターが再起動することがよくあり、Alexaが答えなくなります。そのたびに、AWS Lambdaを開いて、index.jsでグローバルIPアドレスを書き換える必要があります。
(3) Lambdaの問題かもしれませんが、グローバルIPアドレスが変わって新しい温度を読み込んでいなくても、Alexaは覚えていた温度を答えます。
以上の理由から、「アレクサ、部屋の温度を教えて」をほとんど使わなくなりました。グローバルIPアドレスを都度見に行くようなLambda関数を作れれば問題解決なのですが、そのような能力はありません。