前の記事「アレクサ(Amazon Echo)のPythonでのスマートホームスキル開発についての備忘録」で、Amazon Echo→Nature Remoを使った音声でのTV音量の細かい調整プログラムをアレクサスマートホームスキル(Python)で書き始めたのですが、実際のリビングで動くようになったのでソースを含めて紹介しようと思います。

 

 

アレクサのスマートホームスキルのプログラムを書こうと思った目的を整理すると下記になります。

 

・Nature Remo Smart Home Skillは、Nature Remoアプリで登録したデバイス情報をNature Remo Cloud (home.nature.global)から取得します。

Nature Remoアプリで登録したTVデバイスのプリセットでは音量の細かい調整ができず、「TV音量を大きくして」と言うと異常に大きく(+10に)なってしまうので、デフォルトで+2大きくしたいです。

 

・音量増減幅は±1のみで±2できなかったので、±1~±4(MAX)で指定できるようにしたいです。

 

・Nature Remo Smart Home Skill だと認識しない時がたまにありました。

スキルやアプリの更新タイミングが原因なのか、どうもAmazon Echoは電源を抜かないと更新が反映されないことが多かったです。TV操作だけは頻繁にするので上手く動かないと使い物になりません。

 

現状、Amazon Echoには、妻がAmazonプライム会員なので妻のアカウントでサインインしています。

FireTVを介して映画など見れるので。

 

スキル開発は私のAmazonアカウント/Amazon Developerアカウント/AWSアカウントで行いたいので、開発したスキルを妻のアカウントに共有して有効にしたかったのですができませんでした。

サポートの方にはβ版でテストするなどいいろいろ代替案をご提案いただいたのですが、残念ながら根本の解決に至らなかったです。

 

そこで構成は下記の通り。

スキル開発は妻のAmazonアカウント/Amazon Developerアカウントで行い、私のAWSアカウントのlambdaで実装したPython関数を呼び出すことにしました。

これで開発中でもテストが実際のAmazon Echoで試せます。

 

以下手順を追って説明していきます。

 

■Alexa Skill Kit でスマートホームスキルプロジェクトを作成。

※Alexa Skills Kit マニュアル:スマートホームスキルの作成手順

 

 

■スマートホームスキルはアカウントリンク必須なのでユーザーを管理する手段としてAWSのCognitoにユーザープールを登録

 

 

■スマートホームスキルのエンドポイントはAWS(Amazon Web Service)のlambda上のPythonで記述。

 

 

 

Nature Remo Cloud APIを使用したいのですが、AWSでPythonのRequestsライブラリはサポートされていなかったので、別途Ubuntu環境を立ち上げてPyhtonをインストール後、取得したライブラリをAWSのlambdaへインポートします。

 

・pip3(Python3のpip)コマンドでRequestsライブラリを取得。

$ pip3 install requests --user --install-option="--install-purelib=/home/target_dir"

※参考:pip 20.0.2 documentationpip install --target オプションがエラーになって泣きそうになったメモ


・AlexaスマートホームスキルのPythonサンプルコード取得

$ cd /home

$ git clone https://github.com/alexa/alexa-smarthome.git

※参考GitHubリポジトリ:Alexa Smart Home Lambda Function Sample Code.

 

・Requestsモジュールへサンプルコードを追加

https://github.com/alexa/alexa-smarthome/tree/master/sample_lambda/python の下のソースを使います。

$ cd /home/alexa-smarthome/sample_lambda/python

$ cp -r . /home/target_dir

$ cd /home/target_dir

$ zip -r9 ../function.zip .

 

/homeの下にfunction.zipが出来ますので、AWSのlambdaへアップロードします。

※参考 HACK note:lambdaに外部モジュールを読み込ませる

 

・スマートホームスキルのPython実装

スマートホームスキルは、カスタムスキルと異なりインテントやスロットによる音声の構文解析定義部分がありません。

 

あらかじめ定義されているインタフェースに基づいて、インテントに対応したハンドラをPythonでプログラムします。

使用したインターフェースは下記3つです。

Alexa.Speakerインターフェース

Alexa.PowerControllerインターフェース

Alexa.ChannelControllerインターフェース

 

アップロードしたソースの中のlambda.pyのlambda_handler()が音声に対応して起動されるハンドラ関数になります。

 

lambda.pyを追って説明します。

 

まず、デバイスの登録です。

 

音声に対する処理の前に、最初にスキルが有効になった時は、"Alexa.Discovery"インターフェースに基づいてrequest(デバイス検索要求)がハンドラに渡されるので、自分で持っているデバイスの登録処理を行います。

 

※下記ソースコード部分は横スクロールをかけている(SHIFT+マウスホイールスクロール可能)のでPCブラウザモードでご覧ください。

SAMPLE_APPLIANCES = [
    # サンプルデバイス1~8の定義
    { "applianceId": "endpoint-001"},{},{},
 
    # デバイス9に「Smart TV」を「テレビ」というデバイス名で追加します。
    {
        "applianceId": "endpoint-009",
        "manufacturerName": "Sample Manufacturer",
        "modelName": "Smart TV",
        "version": "1",
        "friendlyName": "テレビ",
        "friendlyDescription": "009 TV that can be turned on/off and ajust volume up/down and change channel",
        "isReachable": True,
        "actions": [
            "AdjustVolume",
            "turnOn",
            "turnOff",
            "ChangeChannel",
            "SkipChannels"
        ],
        "additionalApplianceDetails": {}
    }
]

"modelName" が "Smart TV"の"テレビ"というデバイスが、受け付けるコマンドは、音量調節:"AdjustVolume", 電源ON/OFF:"turnOn","turnOff", チャンネル変更:"ChangeChannel","SkipChannels"ということを宣言しています。

 

以下は、Alexaのスキルサンプルコード(そのまま)で、上記デバイスデータを読み込んでデバイスの登録処理をする関数です。

def handle_discovery_v3(request):
    endpoints = []
    for appliance in SAMPLE_APPLIANCES:
        endpoints.append(get_endpoint_from_v2_appliance(appliance))

    response = {
        "event": {
            "header": {
                "namespace": "Alexa.Discovery",
                "name": "Discover.Response",
                "payloadVersion": "3",
                "messageId": get_uuid()
            },
            "payload": {
                "endpoints": endpoints
            }
        }
    }
    return response

SAMPLE_APPLIANCESで定義したデバイスデータから、1~9までのデバイスを持っていること示す"Alexa.Discovery"インターフェースに乗っ取ったresoponseデータを作成してAlexaに返します。

 

get_endpoint_from_v2_appliance(appliance)は、SAMPLE_APPLIANCES を引数で渡すと、それぞれのデバイスが持っている"modelName"毎の能力(インターフェース)の情報を取得することができます。

 

今回作成する"テレビ"というデバイスはmodelNameが"Smart TV"なので、下記のようにAlexa.Speaker、Alexa.PowerController、Alexa.ChannelControllerの3つのインターフェースを持っていることを返します。

capabilities = [
    {
        "type": "AlexaInterface",
        "interface": "Alexa.Speaker",
        "version": "3",
        "properties": {
            "supported": [
                { "name": "AdjustVolume" }
            ],
            "proactivelyReported": True,
            "retrievable": True
        }
    },
    {
        "type": "AlexaInterface",
        "interface": "Alexa.PowerController",
        "version": "3",
        "properties": {
        "supported": [
                { "name": "powerState" }
            ],
            "proactivelyReported": True,
            "retrievable": True
        }
    },
    {
        "type": "AlexaInterface",
        "interface": "Alexa.ChannelController",
        "version": "3",
        "properties": {
            "supported": [
                { "name": "channel" },
                { "name": "channelCount" }
            ],
            "proactivelyReported": True,
            "retrievable": True
        }
    }
]

スキルを有効にした時点で、Alexaにデバイス情報の入ったresponseが返されると下記のようにAlexaアプリのデバイス画面にBaby Cameraなど1~9のデバイスが表示されます。

 

 

デバイスが登録された後、実際に音声が発生されるとrequestが届きます。

 

音声に対応したインテント毎の処理をハンドラに記述していきます。

def handle_non_discovery_v3(request):
    request_namespace = request["directive"]["header"]["namespace"]
    request_name = request["directive"]["header"]["name"]

    # インターフェースの識別
    if request_namespace == "Alexa.PowerController":
        ・・・・
    elif request_namespace == "Alexa.ChannelController":
        ・・・・
    elif request_namespace == "Alexa.Speaker":
        #ボリューム調整
        if request_name == "AdjustVolume":
            request_volume = request["directive"]["payload"]["volume"]
            request_volumeDefault = request["directive"]["payload"]["volumeDefault"]

            #ボリューム増減命令セット
            if request_volume < 0 :
                payload = {'button': 'vol-down'}
            else :
                payload = {'button': 'vol-up'}

            if request_volumeDefault == True :
                # デフォルト2レベル上下したい
                sendCount = 2
            else:
                # ユーザー指定増減幅を使用、最大値4。
                sendCount = min (abs(request_volume), 4)

            #コマンド送信
            for _ in range(sendCount) :
                result = send_command_to_nature_remo(payload)

            # 応答パケット作成
            response = {
                "context": {
                    "properties": [
                        {
                            "namespace": "Alexa.Speaker",
                            "name": "volume",
                            "value": request_volume,
                            "timeOfSample": get_utc_timestamp(),
                            "uncertaintyInMilliseconds": 0
                        },
                        {
                            "namespace": "Alexa.Speaker",
                            "name": "muted",
                            "value": False,
                            "timeOfSample": get_utc_timestamp(),
                            "uncertaintyInMilliseconds": 0
                        }
                    ]
                },
                "event": {
                    "header": {
                        "messageId": get_uuid(),
                        "correlationToken": request["directive"]["header"]["correlationToken"],
                        "namespace": "Alexa",
                        "name": "Response",
                        "payloadVersion": "3"
                    },
                    "endpoint":{
                        "endpointId": request["directive"]["endpoint"]["endpointId"]
                    },
                    "payload": payload
                }
            }
            return response

ハンドラ内で、インテント毎に関する処理はhandle_non_discovery_v3(request)で行われます。

 

上記例では、"Alexa.Speaker"の部分を抜粋して音量変更のインテント処理を載せています。

音量変更のrequestがあったら、最終的にpayloadに音量変更のコマンドをセットしてsend_command_to_nature_remo(payload)を呼んでいます。

 

send_command_to_nature_remo(payload)の中ではNature Remo Cloud API を使用して Nature Remo にTV音量変更などのコマンドを送信します。

def send_command_to_nature_remo(payload):
    # Set Nature Remo API Key
    KEY_IN_HEADERS = {
        'accept': 'application/json',
        'Authorization': 'Bearer Nature Remo Access Token'
    }
    # Nature Remo grobal API for TV control
    URL_OF_GROBAL_API = 'https://api.nature.global/1/appliances/"デバイスのID"/tv'
    result = requests.post(URL_OF_GROBAL_API, headers=KEY_IN_HEADERS, data=payload)

    return result

 

上記のNature Remo Cloud APIを使用するために、あらかじめNature Remo GlobalサイトにログインしてNature Remo Access Tokenを取得しておきます。

 

また、取得したNature Remo Access Tokenを使って、Nature Remo Cloud APIのTV操作対象のデバイスのIDとTV操作のためのコマンドの仕様を得るためデバイス装備を取得するため下記curlを実行します。

 

$ curl -X GET "https://api.nature.global/1/appliances" -H "accept: application/json" -k --header "Authorization: Bearer 'Nature Remo Access Token' "

 

漢字コードがS-JISだと化けるので、Windowsでなくさきほど立ち上げたUbuntuでcurlを実行しました。

 

以下デバイス装備の取得結果Responseの抜粋です。

最初のidの値がデバイスのID、nameの値がTV操作コマンド文字列になります。

[{"id": "デバイスのID"・・・
"type":"TV","nickname":"テレビ","image":"ico_tv","settings":null,"aircon":null,"signals":[],"tv":{"buttons":[
{"name":"power","image":"ico_io","label":"TV_power"},
{"name":"select-input-src","image":"ico_input","label":"TV_source"},{"name":"tv-schedule","image":"ico_tv_guide","label":"TV_schedule"},{"name":"mute","image":"ico_mute","label":"TV_mute"},{"name":"input-terrestrial","image":"ico_tv","label":"TV_terrestrial"},{"name":"input-bs","image":"ico_bs","label":"TV_BS"},{"name":"input-cs","image":"ico_cs","label":"TV_CS"},{"name":"select-audio","image":"ico_select_audio","label":"TV_select_audio"},
{"name":"ch-1","image":"ico_number_1","label":"TV_1"},
{"name":"ch-2","image":"ico_number_2","label":"TV_2"},
{"name":"ch-3","image":"ico_number_3","label":"TV_3"},
{"name":"ch-4","image":"ico_number_4","label":"TV_4"},
{"name":"ch-5","image":"ico_number_5","label":"TV_5"},
{"name":"ch-6","image":"ico_number_6","label":"TV_6"},
{"name":"ch-7","image":"ico_number_7","label":"TV_7"},
{"name":"ch-8","image":"ico_number_8","label":"TV_8"},
{"name":"ch-9","image":"ico_number_9","label":"TV_9"},
{"name":"ch-10","image":"ico_number_10","label":"TV_10"},
{"name":"ch-11","image":"ico_number_11","label":"TV_11"},
{"name":"ch-12","image":"ico_number_12","label":"TV_12"},
{"name":"back","image":"ico_return","label":"TV_back"},
{"name":"home","image":"ico_home","label":"TV_home"},
{"name":"display","image":"ico_display","label":"TV_display_change"},
{"name":"d","image":"ico_d","label":"TV_data"},
{"name":"ch-up","image":"ico_arrow_top","label":"TV_next_channel"},
{"name":"ch-down","image":"ico_arrow_bottom","label":"TV_previous_channel"},
{"name":"left","image":"ico_arrow_left","label":"TV_left"},{"name":"up","image":"ico_arrow_top","label":"TV_top"},{"name":"right","image":"ico_arrow_right","label":"TV_right"},{"name":"down","image":"ico_arrow_bottom","label":"TV_bottom"},{"name":"ok","image":"ico_record","label":"TV_ok"},
{"name":"vol-up","image":"ico_plus","label":"TV_volume_up"},
{"name":"vol-down","image":"ico_minus","label":"TV_volume_down"},],・・・}]

 

Pythonの実装手順は以上です。

 

最後にスキルを動作させるためにAlexaアプリに作成したスマートホームスキルを登録します。

 

■Amazon Alexaアプリを起動して作成したスマートホームスキル「Nature Remo TV Easy Control ver.K」を有効にします。

 

 

「有効にして使用する」ボタンを押すと、認証画面が出ますのでユーザー名とパスワードを入力してOKです。

 

 

また、Nature Remo Smart Home Skill をHub としたデバイスは照明やレコーダーなども繋がっているので、TVデバイスのみ無効にして、自分のスマートホームスキルでのTVデバイスと競合しないようにしてやります。

 

 

上記のテレビ3台の内、上の1台が今回作成した「Nature Remo TV Easy Control ver.K」のハブ上のテレビです。

下の2台がNature RemoとNode-REDのハブ上に繋がっているテレビです。

 

下の2台は無効になっています。

 

 

以上で、自分のスマートホームスキルを介して思ったようなTV音量操作ができるようになりました。

デバイスが認識されないことも少なくなると良いのですが。

 

だいぶ長くなってしまいましたが、今回Alexaスキルを実装して思ったのは下記の通りです。

 

・Pythonのサンプルを公開しているのは良いと思いました。

・スマートホームスキルはカスタムスキルと異なりインテントやスロットによる音声の構文解析定義が全くできないのはなんとかならないか。

・Alexaはインテントの優先順位が操作できないこともなんとかしてほしい。

 

後者2点はGoogole Assistantとは違うなー

 

----編集後記----

実は、Amazon Echoの反応が遅いのと誤判定が多いので、妻用にキッチンでレシピを表示するためにGoogle Home (Google Nest Hub)を購入しました。

 

キッチンに置いたGoogle Homeの反応が早く正確なので、妻はまったくAmazon Echoに話しかけなくなりました。

ちょっと寂しい。。。

(音声アシスタントが2台あると、反応が1秒でも遅い方(Amazon Echo)には全く声を掛けなくなるのは本当です)

 

Amazon Echoに指示したらIFTTTでGoogle Homeにそのまま転送してやれば、少しは役に立つかなと思って調べたところ、Google Home→Amazon Echoの日本語転送はできるのですが、逆の日本語転送はダメでした。

 

Amazon Echoが最初に購入したAIスピーカーとなったのも何かの運命なので、なんとか生かす方法を考えています。(^^)

 

次回はGoogle HomeとAlexaの連携について何かまとまったら書こうと思っております。