数年前に作成したAlexaスキル「アレクサ、部屋の温度を教えて」を実現のための第8歩になります。
その時は、AWS のLambda関数に事前に調べたルータのグローバルIPアドレスを書き込む方法で開発しました。(Blynk local server を使うとAlexaが答えてくれない)
Blynkが答えるためには、ルータにぶら下がるBlynk Local serverが外部インターネットから見えてる必要があります。
このときの問題は、ルータが再起動するとグローバルIPアドレスが変わってしまうことにありました。そのたびに、Lambda関数のグローバルIPアドレスのコードをアップデートしなければならず、非常に使い勝手の悪いものでした。最近、その対策を思い立ちLambda関数のコードを見直して、外部のサイトを利用してIPアドレスの取得を試みましたが、次の根本的問題がわかり頓挫してしまいました。
- アドレス取得をLambda関数に実装 (http.get('http://checkip.amazonaws.com')) すると、 ユーザーの実際のグローバルIPではなくLambdaのパブリックIP を取得してしまう。理由はわからないが、そういう仕様らしい。
- AWS Lambda関数が外部サイトからグローバルIPアドレスを取得できない理由は、Lambda関数が固定のIPアドレスを持たないかららしい。Lambda関数は、実行されるたびに異なるIPアドレスが割り当てられるため、外部サイトから見ると常に変わることになる。
- この問題はLambda関数が固定のIPアドレスをもつようにすれば解決できるが、そのためには有料のサービスの利用が必要である。(VPC + NAT Gateway、AWS Global Acceleratorなど。まったく何のことかわからない。)
- 「ユーザーのグローバルIP」を取得したい場合は、クライアント(ユーザーのデバイスやブラウザ)でIPを取得し、それをLambdaにプッシュ送信する設計にする必要がある。
4番目の問題点を眺めていると、Bylnk Local server を使えば、3,4番の問題は解決する気がして来ました。そして、悪戦苦闘しながらIPアドレスの自動取得に成功した記録です。
Bylnk Local server が定期的に自動で取得した globalIP を使用して、Blynk Local server へセンサーのデータを取得をリクエストできます。
もっとも、AWSの知識はないし習得する気もないので、
ChatGPTや
Copilotを駆使して開発しました。
🔹 手順
-
準備
(1-1) Raspberry Pi に Python3 がインストールされていることを確認
(1-2) 必要なライブラリのインストール -
Python スクリプトの作成
(2-1) グローバルIPを取得するスクリプトの作成
(2-2) スクリプトを Raspberry Pi に保存 -
定期実行の設定
(3-1)crontab
を使って 10 分ごとにスクリプトを実行 -
AWS Lambda との通信
(4-1) API Gateway を設定して Lambda 関数を呼び出せるようにする
(4-2) Lambda 関数を作成し、IP を受け取る処理を書く
(4-3) Raspberry Pi から API Gateway にリクエストを送る -
動作確認とトラブルシューティング
(5-1) Raspberry Pi のスクリプトが正しく動いているか確認
(5-2) API Gateway と Lambda の接続を確認
(5-3) ログをチェックし、エラーを修正
1. 準備
この章では、Raspberry Pi に必要な環境を整えます。
(1-1) Raspberry Pi に Python3 がインストールされていることを確認
Raspberry Pi には通常 Python3 がプリインストールされていますが、念のため確認し、なければインストールします。
✅ Python3 のバージョンを確認する
以下のコマンドを Raspberry Pi のターミナルで実行してください。
python3 --version
表示例:
Python 3.9.2
「Python 3.x.x」と表示されれば OK です。
🔧 Python3 がインストールされていない場合
もし command not found
などのエラーが出る場合、以下のコマンドでインストールします。
sudo apt update
sudo apt install -y python3 python3-pip
(1-2) 必要なライブラリのインストール
Raspberry Pi の Python スクリプトでは、requests
というライブラリを使って API を呼び出します。
以下のコマンドを実行し、requests
をインストールしてください。
pip3 install requests
✅ インストールの確認
次のコマンドを実行し、正しくインストールされたか確認します。
python3 -c "import requests; print(requests.__version__)"
バージョン番号(例:2.25.1
)が表示されれば OK です。
エラーが出た場合は、インストールがうまくいっていない可能性があります。
これで 「1. 準備」 が完了しました。
次は 「2. Python スクリプトの作成」 に進みますが、ここまでで不明点はありますか?
2. Python スクリプトの作成
この章では、Raspberry Pi 上で Python スクリプトを作成し、定期的にグローバル IP を取得して AWS Lambda に送信する処理を実装します。
(2-1) Python スクリプトの作成
✅ スクリプトを保存するディレクトリに移動
まず、スクリプトを保存するためのディレクトリを作成し、そこに移動します。
mkdir -p ~/blynk-ip-reporter
cd ~/blynk-ip-reporter
✅ Python スクリプトを作成する
テキストエディタ (nano
) を使って、新しい Python スクリプトを作成します。
nano report_ip.py
以下のコードをコピーして貼り付けてください。
import requests
import time
# AWS API Gateway のエンドポイント(後で設定)
LAMBDA_ENDPOINT = "https://your-api-gateway-url.amazonaws.com/prod/ip-report"
def get_global_ip():
"""外部サービスを使ってグローバル 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:
data = {"ip": ip}
response = requests.post(LAMBDA_ENDPOINT, json=data)
print(f"Sent IP: {ip}, Response: {response.status_code}, {response.text}")
except Exception as e:
print(f"Error sending IP to Lambda: {e}")
if __name__ == "__main__":
while True:
ip = get_global_ip()
if ip:
send_ip_to_lambda(ip)
time.sleep(600) # 10分ごとに実行
編集が終わったら、Ctrl + X
→ Y
→ Enter
で保存して終了します。
(2-2) スクリプトの実行権限を付与
作成した Python スクリプトを実行できるように、以下のコマンドを実行してください。
chmod +x report_ip.py
(2-3) スクリプトの動作確認
以下のコマンドを実行し、スクリプトが正しく動作するか確認します。
python3 report_ip.py
- 正常に動作すれば、以下のようなメッセージが表示されます。
Sent IP: 123.45.67.89, Response: 200, OK
- もしエラーが出た場合、どのようなエラーなのか教えてください。
これで 「2. Python スクリプトの作成」 は完了です。
次は 「3. Raspberry Pi でスクリプトを自動実行」 に進みますが、ここまでで不明点はありますか?
(2-4) エラーの修正 1
SyntaxError: Non-UTF-8 code starting with '\xa4' in file /home/pi/blynk-ip-reporter/report_ip.py on line 4, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details
このエラーは、Python スクリプトの文字エンコーディングが UTF-8
でないために発生しています。これは、ファイルの保存時に文字コードが ASCII
や ISO-8859-1
になってしまった可能性があります。
以下の手順で修正してください。
(1) ファイルのエンコーディングを確認
まず、現在のファイルのエンコーディングを確認します。
file -i report_ip.py
utf-8
でない場合は、次の手順で修正します。
(2) ファイルのエンコーディングを UTF-8 に修正
-
ファイルを開き直す
nano
エディタでファイルを開いて、上部にUTF-8
のエンコーディング宣言を追加します。nano report_ip.py
-
1行目に以下のコメントを追加
# -*- coding: utf-8 -*-
修正後の最上部は以下のようになります。
# -*- coding: utf-8 -*- import requests import time
-
保存して終了
Ctrl + X
→Y
→Enter
で保存してください。
(3) スクリプトの実行権限を再設定
chmod +x report_ip.py
(4) 再度スクリプトを実行
もう一度、スクリプトを実行してください。
python3 report_ip.py
この手順で修正できるはずですが、もしまだエラーが出る場合は、エラーメッセージを教えてください。
(2-5) エラーの修正 2
File "/home/pi/blynk-ip-reporter/report_ip.py", line 9 """\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\u04e5\ufffd\ufffd\ufffd\u0224ä\u01a5\ufffd\ufffd\ufffd\ufffd\ufffd\ufffdХ\ufffd IP \ufffd\ufffd\ufffd\ufffd\ufffd""" ^ SyntaxError: (unicode error) 'utf-8' codec can't decode byte 0xb3 in position 0: invalid start byte
エラーの原因は、report_ip.py
のファイル内部に文字化けが発生していることです。これは、ファイルのエンコーディングが UTF-8
以外の形式で保存されている、または編集時に壊れた可能性があります。
(1) まず、現在のファイルのエンコーディングを確認する
以下のコマンドを実行し、ファイルのエンコーディングを確認してください。
file -i report_ip.py
出力例:
report_ip.py: text/x-python; charset=iso-8859-1
もし utf-8
以外の charset=iso-8859-1
などが表示された場合、エンコーディングを UTF-8
に変換する必要があります。
(2) ファイルのエンコーディングを UTF-8
に変換する
以下のコマンドを実行して、エンコーディングを修正してください。
iconv -f ISO-8859-1 -t UTF-8 report_ip.py -o report_ip_utf8.py
もし ISO-8859-1
以外だった場合、-f
の値を file -i
コマンドの出力に合わせて変更してください。
変換後、元のファイルを置き換えます。
mv report_ip_utf8.py report_ip.py
(3) nano
エディタで開いて確認する
nano report_ip.py
- 文字化けがないことを確認する
- 最上部に
UTF-8
のエンコーディング宣言があることを確認する# -*- coding: utf-8 -*-
- 保存方法:
Ctrl + X
→Y
→Enter
(4) Python スクリプトを実行
python3 report_ip.py
(5) それでも解決しない場合
もしまだエラーが出る場合、次の情報を教えてください。
file -i report_ip.py
の結果cat report_ip.py
の出力(文字化けがある場合、部分的に教えてください)nano report_ip.py
で開いたときに、ファイルの内容が読めるか
この手順で修正できる可能性が高いですが、もしうまくいかなければ、追加の対策を考えます。
了解しました。では、API Gateway の設定 から一緒に進めていきましょう。
「API Gateway の作成」 から説明します。
4. AWS Lambda との通信
(4-1) API Gateway の作成(REST API を使用)
1. API Gateway を開く
- AWS マネジメントコンソール にログイン
- 検索バー に「API Gateway」と入力し、開く
2. REST API を作成
- 「API の作成」 をクリック
- 「REST API」 を選択
- 「HTTP API」ではなく、REST API を使います(設定が簡単なため)
- 「構築」 をクリック
3. API の基本設定
- 「新しい API を作成」 を選択
- API 名 を入力(例:
ip-reporter-api
) - エンドポイントタイプ は「リージョン」を選択
- 「API を作成」 をクリック
4. リソースとメソッドを作成
① リソースを作成
- 左のメニューで「リソース」を開く
- 「アクション」 → 「リソースの作成」 をクリック
- リソース名を
ip-report
に設定 - リソースパスを
/ip-report
に設定 - 「リソースの作成」をクリック
② メソッドを作成
- 「アクション」 → 「メソッドの作成」 をクリック
- 「POST」 を選択し、チェックマークをクリック
- 統合タイプ:「Lambda 関数」 を選択
- Lambda 関数名に「ip-reporter-lambda」(後で作成)を入力
- 「保存」をクリックし、「OK」で確認
5. API のデプロイ
- 左メニューで「ステージ」を開く
- 「ステージの作成」をクリック
- ステージ名を
prod
に設定(他の名前でもOK) - 「デプロイ」をクリック
- 「Invoke URL」 が表示されるので、コピー(例:
https://abcd1234.execute-api.ap-northeast-1.amazonaws.com/prod
)
6. Raspberry Pi に設定
report_ip.py
を開くLAMBDA_ENDPOINT
を以下のように変更LAMBDA_ENDPOINT = "https://abcd1234.execute-api.ap-northeast-1.amazonaws.com/prod/ip-report"
- スクリプトを実行してテスト
python3 report_ip.py
これで API Gateway のエンドポイントが完成し、Raspberry Pi からデータを送信できるようになります。
ここまでの手順で不明点があれば教えてください!
おっしゃる通り、**「ステージの作成」**をクリックした際に、「Deployment is required」というエラーが表示される場合、ステージを作成する前にデプロイを行う必要があるという状況です。このエラーは、API Gatewayにリソースやメソッドを設定した後、その設定を「デプロイ」していないため、ステージを作成できないことを意味しています。
解決方法
-
デプロイの確認
ステージを作成する前に、まずAPIの設定をデプロイする必要があります。これを行わないと、ステージが作成できません。以下の手順でデプロイを行ってください。- API Gateway ダッシュボードにアクセス
- 左側のメニューから 「API」 を選択し、作成したAPIをクリック
- 左のメニューから 「アクション」 ボタンをクリックし、その中から 「デプロイ」 を選択
- 「デプロイ先ステージ」 で 「新しいステージを作成」 または既存のステージを選択
- ステージ名 を入力(例:
prod
) - デプロイ をクリック
この操作を行うことで、API設定がデプロイされ、その後にステージ作成が可能になります。
-
ステージの作成
デプロイが成功した後、以下の手順でステージを作成できます。- API Gateway ダッシュボードに戻り、再度ステージの作成を行います。
- 先程のデプロイ時に作成したステージ(例えば
prod
)を選び、ステージが正常に作成されるはずです。
補足
- **ステージ作成に必要な「デプロイ」**がまだ行われていない場合、どんな設定をしてもステージは作成できません。デプロイを先に行うことで、その後ステージ作成が可能になります。
もしこの手順でも問題が解決しない場合、もう一度API Gatewayの設定やエラーメッセージについて確認をお願いします。
(4-2) Lambda 関数を作成
そこで、今までのLambda関数を次の内容に改変する。
- 定数 globalIP を追加し、Raspberry Pi から受信した時にこの値を更新します。
- Alexaスキルを呼び出す際には、格納された globalIP を使います。
Lambda関数のコード順の処理説明
'use strict';
const http = require('http');
let globalIP = 'デフォルトのIPアドレス';
- グローバルIPアドレスの初期化:
globalIP
変数を初期化します。Raspberry Piから受信するまでデフォルトのIPアドレスが設定されています。
function buildSpeechletResponse(title, output, repromptText, shouldEndSession) { /*...*/ }
function buildResponse(sessionAttributes, speechletResponse) { /*...*/ }
- レスポンスの生成関数:
- Alexaスキルのレスポンスを生成するためのヘルパー関数です。タイトル、メッセージ、リプロンプトテキスト、セッション終了フラグを受け取り、スキルの応答を構築します。
function getWelcomeResponse(callback) { /*...*/ }
- ウェルカムメッセージの生成:
- スキルが起動されたときのウェルカムメッセージを生成します。このメッセージはユーザーにスキルの使い方を案内します。
function handleSessionEndRequest(callback) { /*...*/ }
- セッション終了時の処理:
- ユーザーがスキルを終了したときに呼び出される関数です。終了メッセージを生成してセッションを終了します。
function createLocationAttributes(location) { /*...*/ }
- セッション属性の生成:
- 指定された場所の属性をセッションに追加します。
async function readTemperatureInSession(intent, session, callback) { /*...*/ }
- 温度情報の取得:
- 指定された場所と測定項目(例:温度、湿度、気圧など)に基づいてBlynkサーバーからセンサーデータを取得し、ユーザーに提供します。
- エラーハンドリングを行い、データの取得に失敗した場合に適切なメッセージを表示します。
function onSessionStarted(sessionStartedRequest, session) { /*...*/ }
- セッション開始時の処理:
- スキルのセッションが開始されたときに呼び出される関数です。セッションの開始をログに記録します。
function onLaunch(launchRequest, session, callback) { /*...*/ }
- スキルの起動時の処理:
- スキルが起動されたときにウェルカムメッセージを表示する関数です。
function onIntent(intentRequest, session, callback) { /*...*/ }
- インテントリクエストの処理:
- ユーザーがインテント(特定のアクションや質問)を発したときに呼び出される関数です。対応する処理関数(例:
readTemperatureInSession
)を呼び出します。
- ユーザーがインテント(特定のアクションや質問)を発したときに呼び出される関数です。対応する処理関数(例:
function onSessionEnded(sessionEndedRequest, session) { /*...*/ }
- セッション終了時の処理:
- スキルのセッションが終了されたときに呼び出される関数です。セッションの終了をログに記録します。
exports.handler = (event, context) => { /*...*/ }
- Lambda関数のエントリーポイント:
- すべてのリクエストを受け取り、適切な処理関数を呼び出します。
- グローバルIPアドレスの更新、セッションの管理、ロギングとデバッグを行います。
これで各処理がどの関数で行われているかをコードと対比して理解できるようになりました。他に質問や修正が必要な箇所があれば教えてくださいね。
Lambda関数のコード
'use strict';
const http = require('http');
let globalIP = 'デフォルトのIPアドレス'; // 初期化
// --------------- 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 = session.attributes || {};
const shouldEndSession = false;
let speechOutput = '';
let body = '';
const blynkport = '8080';
let blynkAuthToken;
let blynkPin;
// Raspberry PiからのglobalIPを使用
let location = LocationSlot.value || sessionAttributes.location || '二階';
sessionAttributes.location = location;
let ip = sessionAttributes.globalIP || globalIP;
switch (location) {
case '二階':
blynkAuthToken = 't1uC6ciQZrTtNt1wpbmLskRMBvSyi9Lw';
break;
case '仏間':
blynkAuthToken = 'slWRpvQMI-NTHGPb28JBMKIRS7vi4ch2';
break;
case '屋根裏':
blynkAuthToken = 'uwPwuHpaIxEo7t9FRfaCCQ6R4268eEeI';
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);
});
});
await httpPromise;
console.log(`Received response from Blynk server: ${body}`);
const info = parseFloat(JSON.parse(body));
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: ${error}`);
speechOutput = '温度の取得中にエラーが発生しました。';
repromptText = 'もう一度お試しください。';
callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
}
// --------------- 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:', JSON.stringify(event)); // イベント全体をログに記録
// Initialize session attributes if not present
if (!event.session.attributes) {
event.session.attributes = {};
}
// グローバルIPアドレスをイベントから取得して設定
if (event.ip) {
globalIP = event.ip;
event.session.attributes.globalIP = globalIP; // IPをセッション属性に保存
console.log(`Updated globalIP to: ${globalIP}`);
} else if (event.session.attributes.globalIP) {
globalIP = event.session.attributes.globalIP; // セッション属性からIPを取得
console.log(`Using globalIP from session attributes: ${globalIP}`);
} else {
console.error('IPアドレスが提供されていません。');
}
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 が見つかりません。');
}
} catch (e) {
console.error(`Exception: ${e}`);
context.fail(`Exception: ${e}`);
}
};
3. 定期実行の設定
(2-1) Python スクリプトの作成で作った report_ip.py は10分毎にIPアドレスを送信するので、これを修正します。
以下は、crontab を利用して 10 分ごとに実行するため、スクリプト自体は単発実行になるように修正したコード例です。
(※ 元のコードの内容はほぼそのまま残し、ループと time.sleep を削除しています。)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import requests
import json
# 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
}
headers = {'Content-Type': 'application/json', 'x-api-key': API_KEY}
response = requests.post(LAMBDA_ENDPOINT, json=payload, headers=headers)
print(f"Sent IP: {ip}, Response: {response.status_code}, {response.text}")
except Exception as e:
print(f"Error sending IP to Lambda: {e}")
if __name__ == "__main__":
ip = get_global_ip()
if ip:
send_ip_to_lambda(ip)
(3-1) crontab を使って 10 分ごとにスクリプトを実行
-
スクリプトの保存場所と実行権限の確認
このコードを/home/pi/blynk-ip-reporter/report_ip.py
に保存してください。
また、実行権限を付与します。chmod +x /home/pi/blynk-ip-reporter/report_ip.py
-
crontab の編集
ターミナルで以下のコマンドを実行して crontab エディタを開きます。crontab -e
-
10 分ごとの実行スケジュールを追加
エディタの一番下に、以下の行を追加します。*/10 * * * * /usr/bin/python3 /home/pi/blynk-ip-reporter/report_ip.py >> /home/pi/blynk-ip-reporter/report_ip.log 2>&1
*/10 * * * *
は「毎時 10 分ごと」に実行する設定です。/usr/bin/python3
は Python3 のパスです。環境に合わせてwhich python3
で確認してください。- ログは
/home/pi/blynk-ip-reporter/report_ip.log
に出力されます。
-
crontab の保存
nano などのエディタの場合、Ctrl+X
→Y
→Enter
で保存します。 -
設定確認
次のコマンドで現在の crontab 設定を確認できます。crontab -l
ポイント
- このコードは単発実行用に作成しています。
- crontab によって 10 分ごとにスクリプトが起動されるため、スクリプト内で無限ループする必要はありません。
- 送信する JSON は、Alexa スキル用の情報も含んでいますので、元のコードの構造をほぼ踏襲しています。
このように設定することで、Raspberry Pi のスクリプトが crontab によって 10 分ごとに実行され、グローバル IP アドレスが Lambda に送信されるようになります。
(3-2) 補足: ログファイルが大きくなりすぎるのを防ぐ
ログファイルが大きくなりすぎるのを防ぐためには、以下のような方法があります。
1. logrotate を利用する方法
- logrotate は Linux の標準的なログローテーションツールです。
/etc/logrotate.conf
や/etc/logrotate.d/
内に設定ファイルを作成し、一定期間またはサイズに応じてログファイルをローテーション(分割・圧縮・削除)できます。
/etc/logrotate.d/report_ip
以下のような設定ファイルを作成します(root 権限で作業してください)。
/home/pi/blynk-ip-reporter/report_ip.log {
daily
rotate 7
compress
missingok
notifempty
create 644 pi pi
}
以下、それぞれの設定の意味を簡単に説明します。- daily → ログファイルを毎日1回、新しいファイルに切り替えます。
- rotate 7 → 古いログファイルを7世代まで(直近7日分)保持します。7日より前のログは自動的に削除されます。
- compress → ローテーションしたログファイルをgzipで圧縮し、ディスク容量を節約します。
- missingok → ログファイルが存在しなくてもエラーにせず、処理を続けます。
- notifempty → ログファイルが空の場合は、ローテーション(切り替え)を行いません。
- create 644 pi pi → 新しく作成するログファイルに、パーミッション「644」と、所有者をユーザー「pi」とグループ「pi」に設定します。
つまり、この設定では毎日ログが切り替えられ、直近7日分のログが保存され、古いログは削除されます。また、空のログは保存されず、ログファイルは圧縮されるのでディスク容量が節約されます。
2. スクリプト内でログ出力を最小限にする
- 例えば、デバッグ用のログ出力を必要最低限にするか、または不要な情報を出力しないように変更します。
- また、
>>
でなく>
にすると、実行毎に上書きされます。ただし、過去のログが必要な場合は注意が必要です。
例:
crontab のエントリを以下のように変更すると、ログが毎回上書きされます(前回のログが失われるので、履歴が不要な場合のみ推奨):
*/10 * * * * /usr/bin/python3 /home/pi/blynk-ip-reporter/report_ip.py > /home/pi/blynk-ip-reporter/report_ip.log 2>&1
まとめ
- 長期間のログ蓄積が不要なら、
>
を使って上書きする方法もあります。 - 履歴を保持しつつログサイズを管理するなら、logrotate を設定するのが一般的です。
どちらの方法が適しているかは、用途と必要なログ履歴に応じて選んでください。
課題
- 定数 globalIP を追加し、Raspberry Pi から受信した時にこの値を更新します。
- Alexaスキルを呼び出す際には、格納された globalIP を使います。
Raspberry pi は10分毎にIPアドレスをAPI Gatewayを介して、Lambda関数に渡します。Lambda関数はセッション(Alexaとの対話)の間、渡された globalIPを使えますが、セッションが終了すると、globalIPが消失してしまします。
言い換えれば、Raspberry pi からLambda関数に渡されたIPアドレスは、Lambda関数が終了すると消えてしまい、次のRaspberry pi からの送信までIPアドレスがない状態になります。この間にスキルからLambda関数を呼び出して、セッションを開始してもエラーになります。
実験をした結果、10分ではダメで、5分ならOKでした。Lambda関数のタイムアウト(セッションを持続する時間)は3秒で、API Gatewayのタイムアウトは29秒なので、時間スケールが異なります。なぜこの時間になるのかはわかりません。
もう一点の課題は、常にLambda関数が動いていなければ、スキルが正常に動作しないことです。Alexaに部屋の温度を聞くのは、極まれに気の向いた時だけなのに、その時のためにLambda関数は常に動かしている必要があることです。本来であれば、スキルが呼び出されたら、動作を始めるべきだと思います。