メインコンテンツに移動
日常で楽しむクラウドテクノロジー

クラウドから IoT 電球を動かす~ AWS Summit Japan 展示の裏側

2025-09-04 | Author : 服部一成 (kzhattor)

概要

今年の AWS Summit Japan 2025 はご来場いただけましたでしょうか ? AWS Summit Japan では Chaos Kitty という IoT 電球と Amazon Echo を用いたゲームを展示しましたが、この記事の読者の方もブースで体験いただいた方がいらっしゃるかと思います。市販の IoT 電球と Amazon Echo を使ったゲームはいかがだったでしょうか ?

実は、AWS から 市販の IoT デバイスを操作するにはちょっとしたテクニックが必要です。例えば市販の IoT デバイスをクラウドから操作する場合、デバイスメーカーが API を公開してなければクラウドから直接操作することは困難になります。また市販デバイスはデバイスメーカーのクラウドサービスと連携することが多く、自分の AWS アカウントの AWS IoT Core で Thing として登録することは基本的にできません。

本日は、多くのお客様にご来場いただいた Chaos Kitty ブースの IoT 電球の制御の実装を例に、 市販の IoT デバイスのクラウドを起点とした操作と Alexa Skill を活用した実装方法について解説します。

X ポスト » | Facebook シェア » | はてブ »

Missing alt text value

ご注意

本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。

*ハンズオン記事およびソースコードにおける免責事項 »

builders.flash メールメンバー登録

builders.flash メールメンバー登録で、毎月の最新アップデート情報とともに、AWS を無料でお試しいただけるクレジットコードを受け取ることができます。

今すぐ登録 »

全体の構成

Alexa Skill (アレクサスキル) とは、Amazon の音声アシスタント「Alexa」に新しい機能を追加するための拡張アプリのようなものです。スマートフォンでアプリをインストールしてできることが増えるのと同様に、Alexa にスキルを追加することで、ニュースの読み上げ、音楽再生、家電操作、ゲーム、予定管理など、さまざまなサービスを音声で利用できるようになります。Alexa Skill の開発や実行には、AWS との連携が非常に重要です。AWS Lambda を使うことで、スキルの裏側の処理 (バックエンド) をクラウド上で実装でき、サーバー管理の手間なく、必要なときだけコードが自動実行されるため、効率的かつスケーラブルなスキル開発が可能です。また Amazon DynamoDB などと組み合わせることで、データの保存や外部サービスとの連携も容易に実現できます。

今回は「スマートホームスキル」を利用して IoT 電球を操作します。スマートホームスキルは、Amazon Alexa がユーザーのスマートホームデバイス (照明、エアコン、センサーなど) を音声や Alexa アプリから制御できるようにするための専用スキルです。

各 Alexa Skill の役割は以下の通りです。

  • Alexa Skill Switch : Echo デバイスで動作する仮想スイッチとして動作するよう、デバイス登録機能とスイッチの動作ロジックを AWS Lambda で実装
  • Alexa Skill Sensor : Alexa 定型アクションのトリガーとなる仮想人感センサーのデバイス登録機能を AWS Lambda で実装
  • Alexa Skill Change Color : Alexa Skill Sensor で登録した仮想人感センサーのロジック ChangeReport を AWS Lambda から起動し、 Echo デバイスに登録した定期アクションを起動

今回の ChaosKitty の展示ではスイッチをクリックすると Alexa Skill を経由して AWS Lambda が Amazon API Gateway のエンドポイントへアクセスし、 ChaosKitty のゲームがスタートするように実装しました。

ゲームスタート後は発生した障害内容に応じた電球の色が変わるので、変わった電球に応じて発行された Amazon SNS Topic をトリガーとし、電球の色を変更する AWS Lambda が起動し、Alexa Skill 経由で仮想人感センサーの「人が検知された」状態をクラウドから生成することで、Alexa 定型アクションを起動します。 

Missing alt text value

Login With Amazon とは

Login with Amazon (LWA) は、ユーザーが自分の Amazonアカウント (ID とパスワード) を使って、サードパーティのウェブサイトやモバイルアプリにログインできる認証サービスです。ユーザーは新たなアカウントやパスワードを作成せずに、Amazonアカウントで安全かつ簡単にサインインできます。

このサービスは、OAuth 2.0という標準的な認証プロトコルをベースにしており、ユーザーが「Login with Amazon」ボタンをクリックすると、Amazon のログイン画面にリダイレクトされます。認証後、ユーザーが同意すれば、Amazon に登録されている名前やメールアドレスなどのプロフィール情報が、利用中のウェブサイトやアプリと安全に共有されます。

今回は Alexa Skill の認証に LWA を用いてデバイス登録を行います。

Missing alt text value

開発で用いるデバイスとアカウント

準備するアカウント・デバイス・URL

  • Amazon 開発者アカウント (Amazon Developerアカウント)   
    Alexa Skill を作成・管理・公開するために必須です。Amazon.co.jp でアカウント作成し、その後 Amazon 開発者むけポータル を使って開発者登録を行なって下さい。Login With Amazon でプロファイルを作成する際にも利用します。今回は後述する Alexa アプリと連携する Amazon アカウントと同じものを利用しております。アカウント設定は こちら の手順をご確認ください。
  • AWS アカウント   
    Alexa Skill のバックエンドの開発に必要です。
  • Alexa アプリインストール済みのスマートフォン   
    開発した Alexa Skill 登録しデバイス認証を行うために必要です。事前に Amazon アカウントでログインと初期設定を済ませておきます。
  • Amazon Echo デバイス   
    Alexa に IoT デバイスを登録する際にあった方が便利です。今回はスマートホームデバイスのための標準規格である Matter 対応の IoT 電球を用いており、Echo デバイスが Matter Hubとして機能してます。
  • プライバシー規約同意書 URL   
    Login With Amazon で表示するプライバシー規約同意書の URL
  • プライバシーポリシーの URL   
    Alexa Skill で表示するプライバシーポリシーの URL
  • スキルアイコン   
    小 (108 x 108px) と大 (512 x 512px) の PNG または JPG ファイル
  • IoT デバイス   
    クラウドから連携したい IoT デバイスをご準備ください。今回は IoT 電球を利用した手順となります。 

 

 

 

 

 

Alexa Skill 開発

Alexa Skill スマートホームスキル作成①

1. まずは仮想スイッチを Alexa Skill で作成していきます。 Alexa ディベロッパーコンソール から Amazon Developer アカウントでログインし、「スキルの作成」からスマートホームスキルを作成します。

Missing alt text value

スキルの名前

2. スキルの名前をつけ、プライマリロケールを日本語を選択し、「次へ」を選択します。

Missing alt text value

エクスペリエンスのタイプ

3. エクスペリエンスのタイプを「スマートホーム」を選択し、「次へ」を選択します。

Missing alt text value

スマートホーム画面

4. スマート ホーム画面に遷移すると、「スキル ID」が表示されます。AWS Lambda 関数を作成し ARN を取得するため、AWS コンソールにログインします。スマート ホームのスキル ID は手元で保存するか、別タブ/ウインドウで開いておきます。

Missing alt text value

AWS Lambda 関数作成①

5. AWS コンソールにログインします。ユーザの待ち時間を最小にするため、今回オレゴンリージョンを利用します。

6. 関数を作成します。今回ランタイムは「Python 3.13」を利用します。

Missing alt text value

Alexa を選択

7. 「トリガーを追加」から「Alexa」を選択します。「Alexa Smart Home」を選択し、Alexa Skill スマート ホームのスキル ID を入力しトリガーを追加します。

Missing alt text value

Lambda 関数の ARN

8. 作成した Lambda 関数の ARN を手元で保存するかコピーして下さい。AWS コンソールは別タブ/ウインドウで開いておきます。

Alexa Skill スマートホームスキル作成②

9. 作成中のスマート ホームスキルの画面に戻り、コピーした Lambda 関数の ARN を「デフォルトのエンドポイント」と「極東」に入力します。「保存」を押します。

10. 設定を保存後、「アカウントリンクを設定」をクリックします。

11. セキュリティプロバイダ情報の「Web 認証画面の URI」に https://www.amazon.com/ap/oa 、「アクセストークンの URI」に https://api.amazon.com/auth/o2/token、スコープに profile を入力して保存ます。

Missing alt text value

Alexa のリダイレクト先の URL を保存

11. セキュリティプロバイダ情報の「Alexa のリダイレクト先の URL」をコピーして保存します。保存した情報は「ユーザーのクライアント ID」「ユーザーのシークレット」を得るため Login With Amazon の設定で利用します。また、後述の AWS Lambda の環境変数でも利用します。

Missing alt text value

Login With Amazon(LWA) プロファイル作成

13. LWA から Amazon Developer アカウントでログインします。

Missing alt text value

セキュリティプロファイルの新規作成

14. 「Login With Amazon」タブから「セキュリティプロファイルの新規作成」をクリックします。

Missing alt text value

新しいセキュリティプロファイルに名前をつける

15. セキュリティプロファイル管理画面で「セキュリティプロファイル名」「セキュリティプロファイルの説明」「プライバシー規約同意書 URL」を入力し保存します。セキュリティプロファイルは複数の Alexa Skill で共用することが可能です。

Missing alt text value

セキュリティプロファイルを保存

16. 作成したセキュリティプロファイルの「ウェブ設定」を開き、「編集」をクリックします。

17.「許可された返信 URL」にステップ 10. でコピーした「Alexa のリダイレクト先のURL」を入力し保存します。

18.「クライアント ID」「クライアントシークレット」をコピーして保存します。保存した情報は Alexa Skillの「ユーザーのクライアント ID」「ユーザーのシークレット」で利用します。

Missing alt text value

Alexa Skill スマートホームスキル作成③

19. 作成中のスマート ホームスキルの画面に戻り、アカウントリンクの「ユーザーのクライアント ID」「ユーザーのシークレット」に先ほどステップ 17 でコピーした「クライアント ID」「クライアントシークレット」をそれぞれ入力し保存します。

20. サイドメニューの「アクセス権限」をクリックします。「Alexaイベントを送る」のトグルスイッッチを有効にし、「Alexaクライアント ID」「Alexa クライアントシークレット」をコピーして保存します。保存した情報は AWS Lambda や AWS Secrets Manager に登録します。

Missing alt text value

Amazon DynamoDB を作成

21. Amazon DynamoDB に「AlexaSkillSimulatorSecretTable」を作成します。パーティションキーは文字列型で「endpointid」とします。

22. 作成した「AlexaSkillSimulatorSecretTable」に項目を作成からテーブルデータを追加します。endpointid の値には「switch」、属性名「alexaclientid」の値にはステップ 18. で保存した「Alexa クライアントID」、属性名「alexasecretid」の値にも同様に「Alexa クライアントシークレット」を入力して保存します。

23. Amazon DynamoDB に「AlexaSkillSimulatorTable」を作成します。パーティションキーは文字列型で「endpointid」とします。

Missing alt text value

AWS Lambda 関数作成②

24. ステップ「AWS Lambda 関数作成①」で作成した関数に戻り、設定から環境変数を追加します。キー「ENDPOINT_ID」にステップ20. で入力した endpointid の値「switch」を入力、キー「REDIRECT_URL」にステップ 11. で保存した「Alexa のリダイレクト先の URL」を入力します。

25. 「一般設定」からタイムアウト時間を 10 秒に変更します。

26. 「アクセス権限」のロール名から AWS Lambda にアタッチされた IAM Role を確認し、AWS マネージドポリシー 「AmazonDynamoDBFullAccess」をアタッチします。

Missing alt text value

ソースコードの貼り付け

24. 「コード」タブに移動し、以下のソースコードを貼り付けます。

python
import boto3
from datetime import datetime, timedelta
import json
from urllib import request, parse
import time
from uuid import uuid4
import base64
import os

DEVICES_TABLE_NAME = 'AlexaSkillSimulatorTable'
DEVICES_SECRETS_TABLE_NAME = 'AlexaSkillSimulatorSecretTable'
AMAZON_API_OAUTH_TOKEN = 'https://api.amazon.com/auth/o2/token'
REDIRECT_URL = os.environ.get('REDIRECT_URL', 'https://example.com/callback')
DESCRIPTION = os.environ.get('DESCRIPTION', 'AWS Alexa Smart Home Sample Device')
ENDPOINT_ID = os.environ.get('ENDPOINT_ID', 'sample-device-001')

dynamodb = boto3.resource("dynamodb")

def utc_timestamp():
    return time.strftime("%Y-%m-%dT%H:%M:%S.00Z", time.gmtime())

def get_alexa_secrets():
    try:
        secret_table = dynamodb.Table(DEVICES_SECRETS_TABLE_NAME)
        response = secret_table.get_item(
            Key={'endpointid': ENDPOINT_ID},
            ConsistentRead=True
        )
        
        if 'Item' not in response:
            raise ValueError(f"Secrets not found for endpointId: {ENDPOINT_ID}")
            
        item = response['Item']
        
        return (
            item.get('alexaclientid', ''),
            item.get('alexasecretid', '')
        )
        
    except Exception as e:
        raise ValueError("Failed to retrieve Alexa secrets")

def get_access_token(grant_code):
    alexa_client_id, alexa_secret_id = get_alexa_secrets()
    auth = base64.b64encode(f"{alexa_client_id.strip()}:{alexa_secret_id}".encode()).decode()
    headers = {
        "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
        "Authorization": f"Basic {auth}"
    }
    data = {
        "grant_type": "authorization_code",
        "code": grant_code,
        "redirect_uri": REDIRECT_URL
    }
    
    try:
        # セキュリティ: HTTPSスキームのみ許可
        if not AMAZON_API_OAUTH_TOKEN.startswith('https://'):
            raise ValueError("Only HTTPS URLs are allowed")
            
        req = request.Request(
            AMAZON_API_OAUTH_TOKEN,
            data=parse.urlencode(data).encode(),
            headers=headers,
            method="POST"
        )
        
        with request.urlopen(req) as res:
            if res.status != 200:
                raise Exception(f"LWA Error: {res.read().decode()}")
            return json.loads(res.read())
            
    except Exception as e:
        raise ValueError("Token exchange failed")

def lambda_handler(event, context):
    header = event["directive"]["header"]

    if header["namespace"] == "Alexa.Discovery" and header["name"] == "Discover":
        return discover_device(header, event["directive"]["payload"])
    elif header["namespace"] == "Alexa.PowerController":
        return handle_switch_request(header, event["directive"])
    elif header["namespace"] == "Alexa.EndpointHealth":
        return handle_health_request(header, event["directive"])
    elif header["name"] == "AcceptGrant":
        return handle_accept_grant_request(header, event["directive"]["payload"])
    elif header["namespace"] == "Alexa" and header["name"] == "ReportState":  
        return handle_report_state(header, event["directive"])  
    return None

def handle_switch_request(header, directive):
    endpoint_id = directive["endpoint"]["endpointId"]
    request_token = directive["endpoint"]["scope"]["token"]
    
    # ディレクティブに基づいて電源状態を決定
    if header["name"] == "TurnOn":
        power_state = "ON"
    elif header["name"] == "TurnOff":
        power_state = "OFF"
    else:
        power_state = "OFF"  # デフォルト

    try:
        table = dynamodb.Table(DEVICES_TABLE_NAME)
        table.update_item(
            Key={"endpointId": endpoint_id},
            UpdateExpression="SET power_state = :val",
            ExpressionAttributeValues={":val": power_state}
        )
    except Exception:
        pass

    response = {
        "event": {
            "header": {
                "namespace": "Alexa",
                "name": "Response",
                "messageId": header["messageId"] + "-response",
                "payloadVersion": "3"
            },
            "endpoint": {
                "scope": {"type": "BearerToken", "token": request_token},
                "endpointId": endpoint_id
            },
            "payload": {}
        },
        "context": {
            "properties": [{
                "namespace": "Alexa.PowerController",
                "name": "powerState",
                "value": power_state,
                "timeOfSample": utc_timestamp(),
                "uncertaintyInMilliseconds": 0
            }]
        }
    }
    
    return response

def handle_report_state(header, directive):
    endpoint_id = directive["endpoint"]["endpointId"]
    request_token = directive["endpoint"]["scope"]["token"]
    
    try:
        table = dynamodb.Table(DEVICES_TABLE_NAME)
        response = table.get_item(Key={"endpointId": endpoint_id})
        item = response.get("Item", {})
    except Exception as e:
        # DynamoDBエラー時はエラーレスポンスを返す
        return {
            "event": {
                "header": {
                    "namespace": "Alexa",
                    "name": "ErrorResponse",
                    "messageId": header["messageId"] + "-response",
                    "payloadVersion": "3"
                },
                "endpoint": {
                    "endpointId": endpoint_id
                },
                "payload": {
                    "type": "ENDPOINT_UNREACHABLE",
                    "message": "Device is currently unreachable"
                }
            }
        }

    return {
        "event": {
            "header": {
                "namespace": "Alexa",
                "name": "StateReport",
                "messageId": header["messageId"] + "-response",
                "payloadVersion": "3"
            },
            "endpoint": {
                "scope": {"type": "BearerToken", "token": request_token},
                "endpointId": endpoint_id
            },
            "payload": {}
        },
        "context": {
            "properties": [{
                "namespace": "Alexa.PowerController",
                "name": "powerState",
                "value": item.get("power_state", "OFF"),
                "timeOfSample": utc_timestamp(),
                "uncertaintyInMilliseconds": 0
            }]
        }
    }

def handle_health_request(header, directive):
    endpoint_id = directive["endpoint"]["endpointId"]

    return {
        "event": {
            "header": {
                "namespace": "Alexa",
                "name": "Response",
                "messageId": header["messageId"] + "-response",
                "payloadVersion": "3"
            },
            "endpoint": {
                "scope": {"type": "BearerToken", "token": directive["endpoint"]["scope"]["token"]},
                "endpointId": endpoint_id
            },
            "payload": {}
        },
        "context": {
            "properties": [{
                "namespace": "Alexa.EndpointHealth",
                "name": "connectivity",
                "value": {"value": "OK"},
                "timeOfSample": utc_timestamp(),
                "uncertaintyInMilliseconds": 0
            }]
        }
    }

def discover_device(header, payload):
    
    endpoints = []

    display_category = "SWITCH"
    
    endpoints.append({
        "endpointId": ENDPOINT_ID,
        "manufacturerName": "AWS Sample Chaoskitty",
        "friendlyName": ENDPOINT_ID,
        "description": DESCRIPTION,
        "displayCategories": [display_category],
        "cookie": {},
        "capabilities": [
            {
                "type": "AlexaInterface",
                "interface": "Alexa",
                "version": "3"
            },
            {
                "type": "AlexaInterface",
                "interface": "Alexa.PowerController",
                "version": "3",
                "properties": {
                    "supported": [{"name": "powerState"}],
                    "proactivelyReported": True,
                    "retrievable": True
                }
            },
            {
                "type": "AlexaInterface",
                "interface": "Alexa.EndpointHealth",
                "version": "3",
                "properties": {
                    "supported": [{"name": "connectivity"}],
                    "proactivelyReported": True,
                    "retrievable": True
                }
            }
        ]
    })
    
    response_header = header.copy()
    response_header["name"] = "Discover.Response"
    
    return {
        "event": {
            "header": response_header,
            "payload": {"endpoints": endpoints}
        }
    }

def handle_accept_grant_request(header, payload):
    try:
        grant_code = payload["grant"]["code"]
        token = get_access_token(grant_code)
        
        user_token = payload["grantee"]["token"]
        expire_time = datetime.utcnow() + timedelta(seconds=token["expires_in"])
        
        try:
            table = dynamodb.Table(DEVICES_TABLE_NAME)
            table.put_item(
                Item={
                    "endpointId": ENDPOINT_ID,
                    "user_token": user_token,
                    "access_token": token["access_token"],
                    "refresh_token": token["refresh_token"],
                    "expire_time": expire_time.isoformat(timespec='seconds') + 'Z',
                    "power_state": "OFF"
                }
            )
        except Exception:
            pass
        
        return {
            "event": {
                "header": {
                    "namespace": "Alexa.Authorization",
                    "name": "AcceptGrant.Response",
                    "messageId": str(uuid4()),
                    "payloadVersion": "3"
                },
                "payload": {}
            }
        }
        
    except Exception as e:
        return {
            "event": {
                "header": {
                    "namespace": "Alexa.Authorization",
                    "name": "ErrorResponse",
                    "messageId": str(uuid4()),
                    "payloadVersion": "3"
                },
                "payload": {
                    "type": "ACCEPT_GRANT_FAILED",
                    "message": "Grant processing failed"
                }
            }
        } 

Alexa Skill スマートホームスキル作成④

26. Alexa Skill 編集画面に戻り、「公開」タブからスキルのプレビュー画面を編集します。「公開名」「説明」「詳細な説明」「サンプルフレーズ」「カテゴリー (Smart Home)」「プライバシーポリシーの URL」「小さなスキルアイコン」「大きなスキルアイコン」を入力し保存します。

27. 「プライバシーとコンプライアンス」メニューに移動し、スキルの情報を入力し保存します。

28. 「公開範囲」メニューに移動し、「国と地域を選択する:」でスキルとテストしたい国 (今回は日本) を選択します。

29. 「ベータテスト」から「Start Test」をクリックし、スキルの設定状況の確認を行います。

30. 「ベータテスターを追加」から、 Alexa で利用しているAmazon アカウントの Eメールアドレスを入力します。

31. 「共有リンク」から「リンクをコピーする」でスキルの有効化を行う URL のリンクをコピーして保存します。保存して実行をクリックしスキルの検証を行います。

 

 

Missing alt text value

Alexa アプリでスキルを有効化

32. スマートフォンでステップ 31. で保存した URL をクリックし、アレクサアプリを開きます

画面遷移

Alexa アプリを開く

Missing alt text value

アカウントをリンク中

Missing alt text value

スキルをテストする

Missing alt text value

スキルを有効化する

Missing alt text value

スキルにログインする

33. ログイン・登録画面からステップ 30. で入力した Amazon アカウントでログインします。

34. ログイン後アカウントリンクが正常に動作すると、「正常にリンクされました」と表示され、「次へ」をクリックすると接続するデバイス (仮想スイッチ) の検出画面に遷移します

画面遷移

アカウントリンク完了

Missing alt text value

デバイスを検出中

Missing alt text value

検出結果

Missing alt text value

デバイスをグループに追加

Missing alt text value

デバイス一覧画面

Missing alt text value

実装成功

35. デバイス switch が一覧画面に表示されれば実装成功です。

36. 上記ステップで Alexa Skill Switch 同様に、Alexa Skill Sensor を実装します。endpointid は「iot_bulb」等 Switch と別の名前にします。サンプルコードはこちら

AlexaSkill_Simulator_EC2_Green.zip »

37. AWS Lambda 関数 ChangeColor を作成します。環境変数は上記ステップを参考に追加してください。サンプルコードはこちら

AlexaSkill_Simulator_ChangeColor_Sample.zip »

IoT 電球を Alexa に登録する

IoT デバイスと定型アクションの紐付け

37. 実行したい IoT デバイスの動作を定型アクションで定義します。Alexa アプリの定型アクションから新しい定型アクションを作成します。

38. 「実行条件を設定」から「スマートホーム」を選択し、登録されたセンサーデバイスを選択します。

39. 「モーションの状態」を「検出されたとき」を選択して次へをクリックします。

40. 「ALEXAのアクション」から実行したいアクション(例えば照明のオンオフ、アナウンスなど)を登録し、保存します。

Alexa Skill テスト

Change Color Lambda を実行し、登録した定型アクションが実行されれば成功です。

まとめ

AWS Summit Japan の展示「Chaos Kitty」ブースで紹介された、市販の IoT 電球をAmazon Echo (Alexa) と連携させてゲームを動かす仕組みと、その実装方法について解説しました。仮想人感センサーの ChangeReport をトリガーとして定型アクションを実行させることで、Alexa の定型アクションに登録できる様々な操作をクラウドから実行することができます。

筆者プロフィール

服部一成

アマゾンウェブサービスジャパン合同会社
ソリューションアーキテクト製造業担当 SA

CCoE や IoT 領域の知見を活かして自動車業界のお客様を主に支援してます。趣味は新しいクラフト🍺探し。最近キャンピングカーでキャンプに行ってきたことから、流行から大分遅れながらキャンプに興味が出てきました。

Missing alt text value