Alexaスキル「アレクサ、部屋の温度を教えて」を実現のための第9歩になります。(最終章だと思います。)
Raspberry Pi でグローバルIPを取得し、AWS Lambda に送信する手順でルータのIPアドレスを自動で取得することに成功しました。しかし、問題があります。- 1つ目の問題は、Lambda関数がセッション終了後にIPアドレスを保持できず、スキルがエラーになることです。
- 2つ目は、Lambda関数が常に動作していないと、Alexaスキルが正常に動作しないことです。
本記事はこの解決策を作った記録です。
を使いました。
最新のIPアドレスを維持するメカニズム
現状の課題は、Raspberry PiからLambda関数に渡されたIPアドレスが、Lambda関数の実行終了とともに消失してしまうことです。このため、次のIPアドレスが送信されるまでの間、AlexaスキルからLambda関数を呼び出すとエラーが発生します。また、Lambda関数が常に実行されている必要があり、リソースの無駄が発生しています。
これらの課題を解決するためには、次の方法が考えられます。
- IPアドレスを保管する場所(データベース)を設け、取得したIPアドレスをこれに格納する。
- スキルが呼ばれ、Lambda関数がIPアドレスを必要なときに格納からIPアドレスを取り出す。
- Raspberry pi が取得したIPアドレスのログ記録
- AWSのDynamoDBに最新のIPアドレスを格納
Lambda関数と同列に構築できるので、DynamoDBを使用することにします。
手順:DynamoDBの利用
- Raspberry PiのPythonスクリプトの変更:DynamoDBへのIPアドレス保存
- Lambda関数の変更:DynamoDBから最新のIPアドレスを取得
- DynamoDBテーブルの作成:IPアドレスを保存するためのテーブルを作成
- Lambda Layerを作成する
- Node.js環境の構築:
- aws-sdkのインストール: Lambda関数のデプロイパッケージにaws-sdkを追加
- zipファイルの作成: aws-sdkのインクルード
- Layerの作成: aws-sdkを含むパッケージをLambdaにアップロード
- Lambda関数へのLayerの関連付け
- IAMロールの作成
- Lambda関数の作成
1. Pythonスクリプト(評価用)
- IPアドレスをpayloadに含めてLambda関数に送信。
- 送信日時とIPアドレス、レスポンスコードをログに出力するように変更。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import requests
import json
import datetime
import time
# AWS API Gateway のエンドポイントと API キー
LAMBDA_ENDPOINT = "https://your-api-gateway-url.amazonaws.com/prod/ip-report"
API_KEY = "YOUR_API_KEY" # 実際の API キーに置き換えてください
def get_global_ip():
"""checkip.amazonaws.com からグローバル IP アドレスを取得する"""
try:
response = requests.get("https://checkip.amazonaws.com")
return response.text.strip()
except Exception as e:
print(f"Error getting global IP: {e}")
return None
def send_ip_to_lambda(ip):
"""取得した IP アドレスを Lambda に送信する"""
try:
payload = {
'session': {
'new': True,
'sessionId': 'SessionId',
'application': {
'applicationId': 'amzn1.ask.skill.[unique-value-here]'
},
'attributes': {},
'user': {
'userId': 'amzn1.ask.account.[unique-value-here]'
}
},
'request': {
'type': 'LaunchRequest',
'requestId': 'EdwRequestId',
'locale': 'ja-JP'
},
'ip': ip # IPアドレスをpayloadに含める
}
headers = {'Content-Type': 'application/json', 'x-api-key': API_KEY}
response = requests.post(LAMBDA_ENDPOINT, json=payload, headers=headers)
log_message = f"{datetime.datetime.now()} - Sent IP: {ip}, Response: {response.status_code}"
print(log_message) # ログを標準出力
except Exception as e:
log_message = f"{datetime.datetime.now()} - Error sending IP to Lambda: {e}"
print(log_message) # ログを標準出力
if __name__ == "__main__":
while True:# 評価用、繰り返し
ip = get_global_ip()
if ip:
send_ip_to_lambda(ip)
time.sleep(600) # 評価用、10分ごとに実行
このスクリプトは、Alexa スキルが Lambda 関数を呼び出す際に送信するイベントを模倣したペイロードを作成し、IP アドレスをそのペイロードに含めて Lambda 関数に送信するものです。
applicationId
と userId
は、Alexa スキルとユーザーを識別するための ID です。
applicationId
- Alexa スキルを識別するための ID です。
- Alexa Developer Console でスキルを作成した際に発行されます。
- 通常は
amzn1.ask.skill.[skillId]
のような形式です。 [unique-value-here]
の部分を実際のスキル ID に置き換えてください。
userId
- Alexa ユーザーを識別するための ID です。
- Amazon アカウントに紐づけられています。
- 通常は
amzn1.ask.account.[accountId]
のような形式です。 [unique-value-here]
の部分を実際のユーザーアカウント ID に置き換えてください。
注意点
payload
は、Alexa スキルから Lambda 関数に送信されるデータの塊です。applicationId
とuserId
は、Alexa スキルとユーザーを識別するための ID です。- これらの ID は、必須ではありませんが、スキルの機能を拡張するために利用されます。
今回のスクリプトでは、IP アドレスを Lambda 関数に送信することが主な目的であるため、applicationId
と userId
を設定しなくても、スキルは問題なく動作します。
2. Lambda関数の変更
2.1 DynamoDBテーブルの作成:
- AWS Management Consoleにログインし、「DynamoDB」を選択します。
- 「テーブルを作成」をクリックします。
- テーブル名を「IPTable」とし、パーティションキーを「id」とします。パーティションキーのタイプは「strings」に設定します。
- テーブルの作成を完了します。
2.2 Lambda Layerを作成
2.2.1 Node.js環境の構築
まず、Node.jsをインストールする必要があります。以下の手順に従って、Node.jsをインストールしてください。
Node.jsのダウンロードとインストール:
Node.jsの公式ウェブサイトにアクセスし、最新のLTSバージョンをダウンロードします。
インストーラーを実行して、画面の指示に従ってインストールします。
環境変数の設定:
インストールが完了したら、コマンドプロンプトを再度開き、
npm
コマンドが認識されるか確認します。
PS C:\WINDOWS\system32> node -v
PS C:\WINDOWS\system32> npm -v
- エラー対応:
PS C:\WINDOWS\system32> node -v
v22.14.0
PS C:\WINDOWS\system32> npm -v
npm : このシステムではスクリプトの実行が無効になっているため、ファイル C:\Program Files\nodejs\npm.ps1 を読み込むことが
できません。詳細については、「about_Execution_Policies」(https://go.microsoft.com/fwlink/?LinkID=135170) を参照してくだ
さい。
発生場所 行:1 文字:1
+ npm -v
+ ~~~
+ CategoryInfo : セキュリティ エラー: (: ) []、PSSecurityException
+ FullyQualifiedErrorId : UnauthorizedAccess
PS C:\WINDOWS\system32> Get-ExecutionPolicy
PS C:\WINDOWS\system32> Set-ExecutionPolicy RemoteSigned -Force
PS C:\WINDOWS\system32> npm -v
の再実行:PS C:\WINDOWS\system32> $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine");
PS C:\WINDOWS\system32> npm -v
10.9.2
2.2.2 AWS SDKのインストール:
PS C:\WINDOWS\system32> mkdir lambda_function
PS C:\WINDOWS\system32> cd lambda_function
PS C:\WINDOWS\system32\lambda_function> mkdir node_modules
PS C:\WINDOWS\system32\lambda_function> npm install aws-sdk --save-dev
このコマンドを実行すると、node_modulesというディレクトリが作成され、AWS SDKがその中にインストールされます。
2.2.3 zipファイルの作成手順
- nodejsディレクトリの作成:
- node_modulesディレクトリの移動:
- nodejsディレクトリの圧縮:
Compress-Archive -Path nodejs -DestinationPath aws-sdk-layer.zip
上記手順で作成したzipファイルの構成を示します。圧縮は \nodejs フォルダを圧縮し、楚々の構成は、\nodejs\node_modules\aws-sdk です。
2.2.4 Layerの作成:
- AWSコンソールにログインし、Lambda > Layers > Create layer に移動します。
- Layer nameに適当な名前(例:
aws-sdk-layer
)を入力します。 - Upload .zip fileを選択し、先ほど作成した
nodejs
ディレクトリを圧縮したzipファイルをアップロードします。 - Compatible runtimesで、Lambda関数で使用しているランタイム(例:
nodejs2.2.x
)を選択します。 - Create layerをクリックしてLayerを作成します。
2.2.5 レイヤーの関連付け方法
- Lambda > Functions に移動し、該当のLambda関数を選択します。
- 「コード」タブを選択します。
- 画面下部の「レイヤー」パネルまでスクロールします。
- 「レイヤーの追加」をクリックします。
- 「ARNを指定」のテキストボックスに、作成したレイヤーのARNを直接入力します。
- レイヤーのARNは、Lambda > Layers で確認できます。
- ARNの形式は arn:aws:lambda:<リージョン>:<アカウントID>:layer:<レイヤー名>:<バージョン> です。
- 「追加」をクリックします。
- 画面右上の「保存」をクリックして設定を保存します。
2.2.6 IAMロールの作成
- AWSコンソールにログインし、IAM > ロール > ロールを作成 に移動します。
- 「Lambda」を選択し、「次のステップ」をクリックします。
- 必要なポリシーを選択します。
- AWS SDKへのアクセス権限: AWSLambdaBasicExecutionRole
- DynamoDBへのアクセス権限: AmazonDynamoDBFullAccess (必要に応じて適切なポリシーを選択)
- ロール名を入力し、「ロールを作成」をクリックします。
3. Lambda関数の作成
'use strict';
const AWS = require('aws-sdk');
const dynamoDb = new AWS.DynamoDB.DocumentClient();
const tableName = 'IPTable'; // DynamoDBのテーブル名
const http = require('http');
// --------------- Helpers that build all of the responses -----------------------
// ... (buildSpeechletResponse, buildResponse関数は変更なし)
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 -----------------------
// ... (getWelcomeResponse, handleSessionEndRequest, createLocationAttributes関数は変更なし)
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 = session.attributes || {};
const shouldEndSession = false;
let speechOutput = '';
let body = '';
const blynkport = '8080';
let blynkAuthToken;
let blynkPin;
let location = LocationSlot.value || sessionAttributes.location || '二階';
sessionAttributes.location = location;
let ip;
try {
// DynamoDBからIPアドレスを取得
const params = {
TableName: tableName,
Key: {
id: 'globalIP' // パーティションキーは固定値
}
};
const data = await dynamoDb.get(params).promise();
const item = data.Item;
if (!item || !item.ipAddress) {
console.error('IPアドレスがDynamoDBに存在しません。');
speechOutput = 'IPアドレスが取得できませんでした。';
repromptText = 'もう一度お試しください。';
callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
ip = item.ipAddress; // DynamoDBから取得したIPアドレス
} catch (error) {
console.error(`DynamoDBからIPアドレスを取得中にエラーが発生しました: ${error}`);
speechOutput = 'IPアドレスの取得中にエラーが発生しました。';
repromptText = 'もう一度お試しください。';
callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return; // エラー発生時は処理を中断
}
switch (location) {
case '二階':
blynkAuthToken = '******wpbmLskRMBvSyi9Lw';
break;
case '仏間':
blynkAuthToken = 'slWRpvQM++++++++BMKIRS7vi4ch2';
break;
case '屋根裏':
blynkAuthToken = 'uwPwuHpaI=======CQ6R4268eEeI';
break;
default:
speechOutput = '指定された場所が見つかりませんでした。';
repromptText = '他の場所を指定してください。';
callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
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;
default:
speechOutput = '指定された測定項目が見つかりませんでした。';
repromptText = '他の測定項目を指定してください。';
callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
return;
}
try {
console.log(`Sending request to Blynk server with Auth Token: ${blynkAuthToken}, Pin: ${blynkPin}, IP: ${ip}`);
const httpPromise = new Promise((resolve, reject) => {
http.get({
host: ip,
path: `/${blynkAuthToken}/get/${blynkPin}`,
port: blynkport,
}, (response) => {
response.on('data', (d) => {
body += d;
});
response.on('end', () => {
resolve('Done Sending');
});
}).on('error', (e) => {
reject(e); // エラーをreject
console.error(`Blynkサーバーへのリクエストエラー: ${e}`); // エラーログ
});
});
await httpPromise;
console.log(`Received response from Blynk server: ${body}`);
let info;
try {
info = parseFloat(JSON.parse(body)); // パースを試みる
} catch (parseError) {
console.error(`JSONパースエラー: ${parseError}, body: ${body}`);
info = NaN; // パースに失敗したらNaNを設定
}
if (isNaN(info)) {
speechOutput = '温度の取得に失敗しました。';
repromptText = 'もう一度お試しください。';
callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
} else {
// ... (温度表示処理)
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;
}
callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
} catch (error) {
console.error(`温度取得処理全体のエラー: ${error}`);
speechOutput = '温度の取得中にエラーが発生しました。';
repromptText = 'もう一度お試しください。';
callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
}
// ... (onSessionStarted, onLaunch, onIntent, onSessionEnded関数は変更なし)
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}`);
// callbackを呼び出す必要はありません。
// context.succeed()も不要です。
}
exports.handler = (event, context) => {
try {
console.log('Event:', JSON.stringify(event));
let globalIP;
if (event.session && event.session.attributes && event.session.attributes.globalIP) {
globalIP = event.session.attributes.globalIP;
console.log(`Using globalIP from session attributes: ${globalIP}`);
continueProcessing(event, context); // IPアドレス取得後に実行したい処理
} else {
console.log("globalIP not found in session attributes. Retrieving from DynamoDB or IP");
// IPアドレスの取得処理 (DynamoDB or event.ip)
if (event.ip) {
globalIP = event.ip;
event.session.attributes.globalIP = globalIP; // セッション属性に保存
console.log(`IP from event: ${globalIP}`);
continueProcessing(event, context); // IPアドレス取得後に実行したい処理
} else {
getIPAddressFromDynamoDB(event, context);
}
}
} catch (e) {
console.error(`Exception: ${e}`);
context.fail(`Exception: ${e}`);
}
};
function updateIPAddress(event, context) {
const params = {
TableName: tableName,
Item: {
id: 'globalIP',
ipAddress: event.ip
}
};
dynamoDb.put(params).promise()
.then(() => {
console.log(`Successfully updated IP address in DynamoDB: ${event.ip}`);
event.session.attributes.globalIP = event.ip;
console.log(`Updated globalIP to: ${event.ip}`);
continueProcessing(event, context);
})
.catch(err => {
console.error(`Error updating IP address in DynamoDB: ${err}`);
context.fail(`Error updating IP address: ${err}`);
});
}
function getIPAddressFromDynamoDB(event, context) {
const params = {
TableName: tableName,
Key: {
id: 'globalIP'
}
};
dynamoDb.get(params).promise()
.then(data => {
if (data.Item) {
const ipAddress = data.Item.ipAddress;
event.session.attributes.globalIP = ipAddress;
console.log(`Retrieved IP address from DynamoDB: ${ipAddress}`);
continueProcessing(event, context);
} else {
console.error('IP address not found in DynamoDB.');
context.fail('IP address not found.');
}
})
.catch(err => {
console.error(`Error retrieving IP address from DynamoDB: ${err}`);
context.fail(`Error retrieving IP address: ${err}`);
});
}
function continueProcessing(event, context) {
if (event.session && event.session.new) {
onSessionStarted({ requestId: event.request.requestId }, event.session);
}
if (event.request && event.request.type) {
if (event.request.type === 'LaunchRequest') {
onLaunch(event.request, event.session, (sessionAttributes, speechletResponse) => {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
console.log('LaunchRequest succeeded');
});
} else if (event.request.type === 'IntentRequest') {
onIntent(event.request, event.session, (sessionAttributes, speechletResponse) => {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
console.log('IntentRequest succeeded');
});
} else if (event.request.type === 'SessionEndedRequest') {
onSessionEnded(event.request, event.session);
context.succeed();
console.log('SessionEndedRequest succeeded');
}
} else {
console.error('event.request が見つかりません。');
context.fail('event.request が見つかりません。');
}
}
// ... (onSessionStarted, onLaunch, onIntent, onSessionEnded, buildResponse, buildSpeechletResponseなどの関数は省略)