LoginSignup
3
4

More than 1 year has passed since last update.

Alexa Widgets をスキルに実装する

Last updated at Posted at 2023-05-21

Alexa Widgetsとは

ウィジェットとは

GUIの一部を切り出して単独のアプリとして使えるようにしたもの。

ウィジェット(widget)は、一般的には、スマートフォンのホーム画面などに設置して常時表示・動作・操作などを可能とする、小規模なアプリ(アプレット)のこと。2010年代半ば以降は、Androidスマートフォンにおける同機能を指す意味で用いられることが多い。「ガジェット」と呼ばれることもある。

もともとウィジェットとはコンピュータのGUI(グラフィカルユーザーインターフェース)上で機能する部品というか構成要素を総称する語である。後にGUIの機能を単独のパーツとして切り出して使えるような形態のソフトウェアを指す用法が主流となった。

2010年代には「Windows 7」の標準機能として「Windows デスクトップ ガジェット」が提供されたが、これはいわゆるウィジェットである。
weblio

Alexa ウィジェットとは

Echo Show 8 / 10 / 15 にウィジェット機能が搭載されている。
Alexa活用法、よく使うスマートホーム、天気、買い物リストなどのウィジェットがあり、最新情報が常に更新され、また必要な情報へ素早くアクセスすることができる。

Alexa ウィジェットはウィジェットギャラリーから追加でき、ウィジェットパネルには最大10個のウィジェットが表示できる。

ウィジェットパネルは、Echo Show 15 はホーム画面の一部として表示されるのでストレスなくアクセスできる。Echo Show 8およびEcho Show 10についてはデバイスの画面を右端から左にスワイプすることでウィジェットパネルが表示される仕組みなので、ちょっとだけ面倒。

前提知識

  • Alexa スキル開発
  • APLパッケージ(APL)

Alexa ウィジェットの構成要素

APLドキュメント

ウィジェットに表示するテンプレートを定義します。ウィジェット ドキュメントでは、標準 APL 機能のサブセットを使用できます。応答性の高いコンポーネントとテンプレートのサブセットはウィジェットをサポートします。

APLパッケージ

ウィジェットをスキルに関連付けるもので、パッケージ内には、APLドキュメント、初期データ、マニフェストおよびその他の情報を定義する。

DataStore

データを保存するための デバイス上にある領域 で、ウィジェットからアクセスができる。わざわざスキルを起動することなくDataStoreデータを取得し表示することがでる。そのため、DataStoreは定期的にData Store REST APIを呼び出してデータを最新化しておく必要がある。ウィジェットに情報を表示しないのであれば、DataStoreは特に意識しなくてよい。

Alexa.DataStore.PackageManager インターフェース

Alexa がデバイス上で APL パッケージをインストール、削除、更新するときにスキルに通知するリクエストを提供します。スキルマニフェストで設定しておく。

Alexa.DataStore インターフェース

データストアにプッシュされた更新が失敗したときにスキルに通知するリクエスト。スキルマニフェストで設定しておく。

Data Store REST API

デバイス上にあるDataStoreを更新するためのAPI。スキル内部で情報更新が発生した場合と、スキル外で情報更新が必要な場合にAPIを呼び出してDataStoreを更新する。スキルの永続化情報をDynamoDBなどに保存している場合は、DataStoreとDyanamoDBとの同期も意識する必要がある。

概要

ポイントはDataStore。ウィジェットは、スキルが起動していなくても最新データをDataStoreから取得することができるところがポイント。DataStoreを更新するには Data Store REST API でDataStore内に保管されているデータを更新してあげる必要がある。ただ Data Store REST API を呼べればデータは更新できるので、必ずしもスキルを実行する必要はなく、APIが呼べる環境であれば何でもよい。注意すべきは、DataStoreはスキルから参照できないので、DynamoDBなどの永続化ストレージでデータを保存している場合は、DynamoDBとDataStoreの双方を更新して同期をとっておく必要がある。そんなに難しいものではない。

image.png

詳細なアーキテクチャは、Alexa Developers Tech Talk: Widgets (YouTube) の25分50秒あたりで表示される図を見ると瞬時に理解できると思う。

実装

ウィジェットを作成する手順は以下になる。

  1. スキルマニフェストの設定をする
  2. データストアパッケージを作る(APLパッケージ)
  3. ハンドラーを実装する(ウィジェット、APL Event)

※ウィジェットで追加や修正が必要なファイルは以下

SKILL
+---lambda
|   |   index.js                      // ハンドラーを実装する(ウィジェット、APL Event)
|   |   :
\---skill-package
    |   skill.json                    // スキルマニフェストの設定をする(インターフェースとか)
    +---dataStorePackages
    |   \---egg-size                  // データストアパッケージを作る(APLパッケージ)
    |       |   manifest.json         //     APLパッケージのマニフェスト
    |       +---datasources
    |       |       default.json      //     画像URLや初期値などのデータセット
    |       +---documents
    |       |       document.json     //     ウィジェットを表示するAPLドキュメント
    |       \---presentations
    |               default.tpl       //     APLパッケージのテンプレート
1. スキルマニフェストの設定をする

データストアパッケージ データストア Alexa Extensions の設定をする。Developer Consoleでも、skill.json でもどちらでもできる。

skill.json
skill.jsoninterfacesに以下を追加する。Developer Console には パケージIDを記載する欄はないみたいだけど、おそらくウィジェット名がここに設定されるのだと思う。

skill.json
      "custom": {
        "interfaces": [
          {
            "packages": [
              {
                "id": "egg-size"                           ※ここにパッケージIDの記載が必要らしい
              }
            ],
            "type": "ALEXA_DATASTORE_PACKAGEMANAGER"       ※Alexa.DataStore.PackageManager インターフェース
          },
          {
            "type": "ALEXA_DATA_STORE"                     ※Alexa.DataStore インターフェース
          },
          {
            "requestedExtensions": [
              {
                "uri": "alexaext:datastore:10"             ※DataStore
              }
            ],
            "type": "ALEXA_EXTENSION"
          }
        ],

 
Developer Console
インターフェースセクションで、データストアパッケージ(Alexa.DataStore.PackageManager インターフェース)とデータストア(Alexa.DataStore インターフェース)の許可をし、AlexaExtensionsのデータストア(DataStore)を使用するをチェックする。
image.png

2. データストアパッケージを作る(APLパッケージ)

こちらもDeveloper Console とコマンドラインどちらかでも作成可能。

コマンドラインでやるなら、skill-package ディレクトリの配下に、dataStorePackages ディレクトリを作成する。その中にAPLパッケージを定義する。ディレクトリ名=パッケージ名=パッケージIDだと思う。

APLパッケージの構造はこんな感じ。

SKILL
\---skill-package
    +---dataStorePackages
    |   \---egg-size                  // APLパッケージ名
    |       |   manifest.json
    |       +---datasources
    |       |       default.json
    |       +---documents
    |       |       document.json
    |       \---presentations
    |               default.tpl

presentations/default.tpl
スキルパッケージテンプレート。拡張子が .tpl になっている。こいつがAPLドキュメントとデータソースを束ねている。

presentations/default.tpl
{
  "type": "APL_PRESENTATION",
  "documentUrl": "documents/document.json",
  "datasourceUrl": "datasources/default.json"
}

datasources/default.json
データソースですね。スキル名とかURLとかを記載しておく。objectIdがサンプルのままでいけてないな。

default.json
{
    "eggTimerData": {
        "type": "object",
        "objectId": "alexaPhotoSample",
        "title": "ゆでたまごタイマー",
        "backgroundImage": {
            "sources": [
                {
                    "url": "https://dl.dropboxusercontent.com/s/qa6uyu774ku8yz6/WidgetBGImage.png",
                    "size": "large"
                }
            ]
        }
    }
}

documents/document.json
APLドキュメント。ウィジェットを表示するための定義。

document.json
{
    "type": "APL",
    "version": "2023.1",
    "license": "Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.\nSPDX-License-Identifier: LicenseRef-.amazon.com.-AmznSL-1.0\nLicensed under the Amazon Software License  http://aws.amazon.com/asl/",
    "import": [
        {
            "name": "alexa-layouts",
            "version": "1.6.0"
        }
    ],
    "extensions": [
        {
            "name": "DataStore",
            "uri": "alexaext:datastore:10"
        }
    ],
    "settings": {
        "DataStore": {
            "dataBindings": [
                {
                    "namespace": "eggTimerNamespace",
                    "key": "eggTimerDataStore",
                    "dataBindingName": "eggTimerDataStore",
                    "dataType": "OBJECT"
                }
            ]
        }
    },
    "mainTemplate": {
        "parameters": [
            "eggTimerData"
        ],
        "bind": [
        ],
        "items": [
            {
                "type": "Container",
                "height": "100vh",
                "width": "100vw",
                "items": [
                    {
                        "type": "AlexaImage",
                        "imageRoundedCorner": false,
                        "imageSource": "${eggTimerData.backgroundImage.sources[0].url}",
                        "imageWidth": "100%",
                        "imageHeight": "100%",
                        "imageScale": "best-fill"
                    },
                    {
                        "type": "VectorGraphic",
                        "position": "absolute",
                        "height": "100%",
                        "width": "100%",
                        "source": "alexaPhotoOverlayScrim",
                        "scale": "fill"
                    },
                    {
                        "type": "TouchWrapper",
                        "position": "absolute",
                        "id": "openSkill",
                        "item": {
                            "type": "AlexaHeader",
                            "position": "absolute",
                            "headerTitle": "${eggTimerData.title}"
                        },
                        "onPress": [
                            {
                                "type": "SendEvent",
                                "arguments": [
                                    "openSkill"
                                ],
                                "flags": {
                                    "interactionMode": "STANDARD"
                                }
                            }
                        ]
                    },
                    {
                        "type": "Container",
                        "position": "absolute",
                        "bottom": "0",
                        "items": [
                            {
                                "type": "AlexaFooterActionButton",
                                "buttonStyle": "ingress",
                                "buttonText": "S サイズ",
                                "primaryAction": [
                                    {
                                        "type": "SendEvent",
                                        "arguments": [
                                            "WidgetSizeId",
                                            "S"
                                        ],
                                        "flags": {
                                            "interactionMode": "INLINE"
                                        }
                                    }
                                ]
                            },
                            {
                                "type": "AlexaFooterActionButton",
                                "buttonStyle": "ingress",
                                "buttonText": "M サイズ",
                                "primaryAction": [
                                    {
                                        "type": "SendEvent",
                                        "arguments": [
                                            "WidgetSizeId",
                                            "M"
                                        ],
                                        "flags": {
                                            "interactionMode": "INLINE"
                                        }
                                    }
                                ]
                            },
                            {
                                "type": "AlexaFooterActionButton",
                                "buttonStyle": "ingress",
                                "buttonText": "L サイズ",
                                "primaryAction": [
                                    {
                                        "type": "SendEvent",
                                        "arguments": [
                                            "WidgetSizeId",
                                            "L"
                                        ],
                                        "flags": {
                                            "interactionMode": "INLINE"
                                        }
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ]
    }
}

Developer Console
マルチモーダルから ウィジェットの作成 をクリックするとウィジェットのテンプレートが選べるので目的に合ったテンプレートを選択する。
image.png

image.png

自分のスキルに合わせて、画像はボタンの配置などを修正する。

image.png

APLパッケージのマニフェストを記載するのだが、ここはJSONで書かないといけないみたい。今回はGUIを使っていないので、このへんは想像で書いてる。

image.png

AlexaFooterActionButtonを3コに増やして、画像を差し替えて完成。

image.png

3. ハンドラーを実装する(ウィジェット、APL Event)

ホスティングで作るか、Lambdaで作るかだけど、基本同じ。
ウィジェットのイベント(インストール、削除、更新、エラー)を拾うハンドラーと、APLのEventを拾うハンドラーを作成する。.addRequestHandlersへの追加もお忘れなく。

index.js
// ウィジェットがインストールされた時に呼ばれる
const InstallWidgetRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === "Alexa.DataStore.PackageManager.UsagesInstalled";
    },
    async handle(handlerInput) {
        // ※DataStoreを更新したり・・・
        // ※特に何もする必要がなければ、ログ出力して、.getResponse(); だけでOK
        return handlerInput.responseBuilder.getResponse();
    }
};

// ウィジェットが削除された時に呼ばれる
const RemoveWidgetRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === "Alexa.DataStore.PackageManager.UsagesRemoved";
    },
    async handle(handlerInput) {
        // ※DataStoreを更新したり・・・
        // ※特に何もする必要がなければ、ログ出力して、.getResponse(); だけでOK
        return handlerInput.responseBuilder.getResponse();
    }
};

// ウィジェットが更新された時に呼ばれる
const UpdateWidgetRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === "Alexa.DataStore.PackageManager.UpdateRequest";
    },
    async handle(handlerInput) {
        // ※DataStoreを更新したり・・・
        // ※特に何もする必要がなければ、ログ出力して、.getResponse(); だけでOK
        return handlerInput.responseBuilder.getResponse();
    }
};

// ウィジェットのインストール時にエラーが発生した場合に呼ばれる
const WidgetInstallationErrorHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === "Alexa.DataStore.PackageManager.InstallationError";
    },
    async handle(handlerInput) {
        :
        :
        return handlerInput.responseBuilder
            .speak('ウィジェットインストールでエラーが発生しました。')
            .getResponse();
    }
};

// APLのSendEventが発動された場合に呼ばれる
const APLEventHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === "Alexa.Presentation.APL.UserEvent";
    },
    async handle(handlerInput) {
        const eventType = handlerInput.requestEnvelope.request.arguments[0]; 
        switch(eventType){
            case 'openSkill': 
                return LaunchRequestHandler.handle(handlerInput);
            case 'WidgetSizeId': 
                return SizeIntentHandler.handle(handlerInput);
            default:
                return CancelAndStopIntentHandler(handlerInput);
        }
    }
};

テスト

インストールを選択して、テストするAlexaデバイスを選択して、送信をクリックすることで、ウィジェットがデバイスへインストールされるので、その後は、Alexaデバイスで実際に操作してみる。

image.png

デバイスにウィジェットがインストールされるので、実際にデバイスを操作してテストを実施する。

IMG_9860.jpg

問題なければ申請する。

公開した

今回はゆでたまごタイマーにAlexa Widgetsを追加して公開した。このウィジェットはハッピーパスのショートカット的なものなのでスキルを知ってる人には便利だと思う(思っている)が、知らない人にとっては何のことかサッパリわからないものになってしまっている。
このへん、もう一工夫する必要がありそう。

 
 

もろもろ足りない部分は今後追記していきます。

参考

3
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4