スイッチでAlexa定型アクションを起動~スキルテスト編~

有効にしたAlexaスマートホームスキルをテストしてみします。

Step1:リフレッシュコードの取得

スキルの有効時にアクセス権限を許可され、リフレッシュトークンを取得したはずですので確認します。AWSのページでCloudWatchサービスを選択します。
CloudWatchのメニューっから「ロググループ」を選択します。

作成した関数のロググループができているので、クリックします。

ログストリームにスキルを有効にした時刻のタイムスタンプでログができているので、選択します。

上から2番目のログでINFO StatusCode:200と表示されアクセス権限の取得に成功しているのがわかります。その下のログはINFO AcceptGrant:token_=となっており取得したトークンを記録しています。ログ左側の▲をクリックしてログを展開します。

コピーを押下してログをコピーし、リフレッシュトークンをメモしておきます。AcceptGrant:token=より先がリフレッシュトークンとなります。

Step2:イベント送信プログラムの完成

イベント送信プログラムは「あんスマ」ではphpで書かれていましたが、phpのインストール手間が面倒なのでpythonで書き直しました。動作上の違いは「あんスマ」のものは検出イベント送信後、直ぐに非検出を送信していたのですが、問題なさそうなので検出だけにしました。

client_ID = 、client_secret = にはスキルビルド画面のアクセス権限メニューから取得したAlexaクライアントIDとAlexaクライアントシークレットを貼り付けます。(index.jsと同じもの)

refresh_token=にはStep1で取得したリフレッシュトークンを貼り付けます。 ファイル名はalexa_trigger.pyとしました。

import sys
import os
import json
import datetime
from trace import Trace
import requests

client_ID = ''
client_secret = ''
refresh_token=''
sensor_name = 'dummy_sensor_' + format(sys.argv[1], '0>3')
token_file = './token.txt'
today = datetime.datetime.today()

# アクセストークンが有効かどうかチェックする
access_token = ''
access_token_valid = False
if os.path.exists(token_file):
  #ファイル有、データ読み込み
  f = open(token_file)
  data = f.read()
  f.close()
  jdata = json.loads(data)
  expires = jdata['expires_in']
  f_time = os.path.getmtime(token_file)
  file_time = datetime.datetime.fromtimestamp(f_time)
  expiring_date = datetime.datetime.fromtimestamp(f_time + expires)
  if today < expiring_date:
    # アクセストークン有効、取得
    access_token_valid = True
    access_token = jdata['access_token']

# アクセストークンが無効の場合はoa2サーバーから取得
if not access_token_valid:
  header_str = {
    'Content-Type': 'application/x-www-form-urlencoded;Accept-Charset=UTF-8'
  }
  body_str = {
    "grant_type": "refresh_token",
    "refresh_token": refresh_token,
    "client_id": client_ID,
    "client_secret": client_secret
  }
  response = requests.post('https://api.amazon.com/auth/o2/token', data = body_str, headers = header_str)
  if response.status_code != 200 :
    print(response.status_code)
    exit()
  jdata = json.loads(response.text)
  access_token = jdata['access_token']
  if len(access_token) > 0:
    access_token_valid = True
    # ファイルに書き込み
    f = open(token_file, 'w')
    f.write(response.text)
    f.close()

if not access_token_valid:
  print('can not get access_token')
  exit()

# イベントゲートウェイにイベントを送信する
message_id = sensor_name + '_' + str(today)
timeOfSample = today.strftime('%Y-%m-%dT%H:%M:%S.00Z')
header_str = {
    'Authorization': 'Bearer ' + access_token,
    'Content-Type': 'application/json'
}
json_str = {
  "event":{
    "header":{
      "namespace":"Alexa",
      "name":"ChangeReport",
      "messageId":message_id,
      "payloadVersion":"3"
    },
    "endpoint":{
      "scope":{
        "type":"BearerToken",
        "token":access_token
       },
       "endpointId":sensor_name
    },
    "payload":{
      "change":{
        "cause":{
          "type":"PHYSICAL_INTERACTION"
        },
        "properties":[
          {
            "namespace":"Alexa.MotionSensor",
            "name":"detectionState",
            "value":"DETECTED",
            "timeOfSample":timeOfSample,
            "uncertaintyInMilliseconds":0
          }
        ]
      }
    }
  },
  "context":{
    "properties": [
      {
        "namespace": "Alexa.EndpointHealth",
        "name": "connectivity",
        "value": {
          "value": "OK"
        },
        "timeOfSample": timeOfSample,
        "uncertaintyInMilliseconds": 0
      }
    ]
  }
}

response = requests.post('https://api.fe.amazonalexa.com/v3/events', json = json_str, headers = header_str)
if response.status_code == 202:
  print('success')
else:
  print(response)

起動方法は引数にセンサーNo.を渡します。
例えばダミーセンサー0を検出状態にするにはコマンドラインで以下のように実行します。

>python alexa_trigger.py 0

このプログラムの注意点はカレントディレクトリにtoken.txtいうファイルを生成することです。取得したアクセストークンには有効期限があるので、有効期限内は保存したファイルからアクセストークンを取り出します。初回や有効期限が切れた場合は新たにアクセストークンを通信により取得します。有効期限はアクセストークンが含まれたメッセージ中に存在しますが、3600秒=1時間となっています。アクセストークン取得時はレスポンスがやや遅くなります。
アクセストークンを取得後、イベントゲートウェイにイベントを送信します。

Step3:定型アクションの作成とテスト

テスト用の定型アクションを作成します。
実行条件に「スマートホーム」-「ダミーセンサー000」を指定し、モーションの状態を検出にします。
アクションは「Alexaのおしゃべり」-「カスタム」で「ダミーセンサーゼロ!」とテキトーに設定し、保存します。

>python alexa_trigger.py 0

をWindowsのコマンドラインで実行すると”success”と出力され、echoが「ダミーセンサーゼロ」と喋るはずです。
成功しなかった場合は401などHTTPステータスコードを出力します。
成功した場合は202が帰ります。これは受理したが処理はまだ、という意味で、表面的なチェックは成功したということです。エンドポイントの文字列を間違っている場合は202が帰ってきますが、イベントは発生しません。

ここまでうまくいきましたでしょうか?ここまでできれば終わったも同然です。後は不要と言う方も多い事でしょう。
以上でテスト編は終わりです。
次はラズベリーパイセットアップ編です。

スイッチでAlexa定型アクションを起動~スキル有効化編~

作成したAlexaスマートホームスキルをAlexaアプリで有効にします。

Step1:グループ作成

作成したスマートホームスキルを有効にすると10個のセンサーが検出されます。最初にそれらを格納するグループを作ります。
スマホのAlexaアプリを開き、「デバイス」ページを開きます。
右上の+を押して「グループを追加」をクリックします。

「部屋またはデバイスのグループを作成」を選択します。

「次へ」を選択します。

「カスタム名」を選択し名前を入力します。「ダミーセンサー」にしてみました。
「次へ」を選択します。

ダミーセンサーにデバイスを追加画面で「スキップ」を選択します。

ダミーセンサーは使用できます、と表示されたので「完了」を選択します。

Step2:スマートホームスキルを有効にする

Alexaアプリで「その他」-「スキル・ゲーム」ページを開きます。
「有効なスキル」タブから「開発」カテゴリを選択します。
今回作成したスキルをタッチします。

「有効にして使用する」を選択します。

アマゾンのログイン画面が表示されます。メールアドレスとパスワードを入力し、「ログイン」をタッチします。

「アカウントが正常にリンクされました。」と表示されたら「閉じる」をタッチします。

Step3:センサーデバイスの検出

「端末を検出」を選択して端末の検出を開始します。

デバイスが接続されたようです。「次へ」を選択します。

「最初にセットアップする接続デバイスを選択してください。」との表示が出ます。どれか一つのセンサーを選択し、「デバイスのセットアップ」をタッチします。

「モーションセンサーはどこにありますか?」という表示に対して、作成したグループを選択し、「グループに追加」をタッチします。

グループに追加されたので、「次へ」を選択します。

「次へ」をタッチします。

これを全てのセンサーに対して行います。

全てのセンサーをセットアップしました。

以上でスキル有効化編を終わります。
次はイベントゲートウェイに送信し、動作を確認します。

スイッチでAlexa定型アクションを起動~スキル作成編~

Alexaスマートホームスキルを作成します。

Step1:ログイン

Alexa開発者コンソール(https://developer.amazon.com/alexa/console/ask)にログインします。Amazonの買い物アカウントがある場合はそのアカウントでログインします。開発者アカウントについての詳細は以下を参照してください。

開発者アカウントの作成
https://developer.amazon.com/ja/docs/app-submission/manage-account-and-permissions.html#create_account

今回のスキルは一般公開せず、開発中のままで使うことになります。買い物アカウントと開発中のアカウントを共通にするのは、現在使用しているechoで開発中のスキルを使用できるようにするためです。

Step2:スキルの作成を開始

ダッシュボード画面が表示されたら、Alexa Skills Kitを選択します。

スキルのメイン画面で「スキルの作成」ボタンを選択します。

新しいスキルを作成画面でスキル名を入力し、スキルに追加するモデルに「スマートホーム」を選択します。スキル名は「MotionSensors」にしてみました。
「スキルを作成」ボタンを押下します。

スマートホーム画面でスキルIDをコピーし、メモしておきます。

Step3:AWSアカウントの作成

以下のページの説明に従い、AWSアカウントを作成します。
クレジットカードまたはデビッドカードの請求情報が必要になります。
サポートプランは無料のベーシックプランで構いません。

AWS アカウント作成の流れ
https://aws.amazon.com/jp/register-flow/

Step4:Lambda関数の作成

AWSにログインしたら、左上のサービスメニューからLambdaを選択します。

右上に表示されているリージョンを「北米西部(オレゴン)」にします。日本からのAlexaスマートホームスキルはオレゴンに置かなくてはなりません。

オレゴンになったのを確認したら、「関数の作成」をクリックします。

関数の作成画面では「一から作成」を選択します。
関数名は「MotionSensorsFunc」にしました。
ランタイムは「Node.js 16.x」、アーキテクチャーは「arm64」を選択しました。
x86でも全く問題はありませんが、arm64にしたのは万が一課金が発生したとしても料金が若干安いからです。
「関数の作成」を選択します。

関数の概要の「+トリガーを追加」をクリックします。

トリガーの設定画面で「Alexa」、「Alexa Smart Home」を選択し、Skill IDにはStep2の最後にメモしておいたスキルIDを入力します。「追加」ボタンを押下します。

画面下部のほうで「コード」タブを選択し、index.jsを選択します。ここにソースを貼り付けます。「あんスマ」の物で全く問題ありませんが、少し手を加えたソースを下に掲載します。

「あんスマ」のコードから変更した内容は、以下の3点です。
1)sensor_num=10の宣言を変更するだけでセンサー数を変更できるようにした。
2)ボタンは不要なのでボタン関係の部分を削除
3)その他ハマっていた時にコチョコチョと変えた部分。例えば57行目のステータスコードのログ出力など。プログラムロジックには変更ありません。
以下からindex.jsをダウンロードできます。

コードをWebページのindex.jsにコピー&ペーストして「Deploy」を押下します。deployとは展開する、配置するという意味で、「AWSに内容を送信」という意味でしょうか。Node.jsはスクリプト言語ですのでビルドする必要はありません。プログラムに問題があると、Webページ上の行頭に印が付いて教えてくれます。エラーがあるプログラムを実行するとログにエラー内容が記録されます。今回の場合、コードの修正をしなければエラーは発生しない筈です。
クライアントIDとシークレットIDはまだ空欄のままです。後程設定します。

Step5:Lambda関数のARNをスマートホームスキルのエンドポイントに設定

Lambda関数のARN(Amazon Resource Name)をコピーします。

コピーしたARNをスマートホームスキル画面の「デフォルトのエンドポイント」に貼り付けます。「保存」ボタンで変更を保存します。

Step6:LWA(Login with Amazon)のセキュリティプロファイルの作成

Login with AmazonとはAmazonのユーザー認証サーバーであり、「Googleでログイン」、「facebookでログイン」と同じようなものかと思います。この設定を行い、スキルとリンクさせます。以下に詳しい説明があります。

Login with Amazonについて
https://developer.amazon.com/ja/apps-and-games/login-with-amazon

Login with Amazonに開発者アカウントでログインし、「セキュリティプロファイルの新規作成」をクリックします。

Login with Amazon
https://developer.amazon.com/loginwithamazon/console/site/lwa/overview.html

セキュリティプロファイル管理画面で「セキュリティプロファイル名」、「セキュリティプロファイルの説明」、「プライバシー規約同意書URL」を入力します。テキトーで構わないようですが、これはスキル開発用のプロファイルですという感じにすれば良いのではないでしょうか。URLにはhttps://example.comを使用させていただきました。
「保存」ボタンで保存します。

「ウェブ設定」タブを押下し「クライアントID」と「クライアントシークレット」をコピーします。クライアントシークレットは「シークレットを表示」をクリックして表示させます。(ちなみにこのIDとシークレットはLambda関数のindex.jsに貼り付けるものではありません。ご注意を。)

Step7:スキルのアカウントリンクの設定

スキルにアカウントリンクの設定を行います。スキルを有効にしたときのユーザー認証画面の設定になります。
スマートホームスキル画面の左メニューから「アカウントリンク」を選択します。

各項目には次の値を入力します。
Web認証画面のURI:https://www.amazon.com/ap/oa
アクセストークンのURI:https://api.amazon.com/auth/o2/token
ユーザーのクライアントID:Login with Amazonで取得したクライアントID
ユーザーのシークレット:Login with Amazonで取得したクライアントシークレット
ユーザーの認可スキーム:HTTP Basic認証(推奨)
スコープ:profile、またはprofile:user_id
※詳細は以下を参照
Configure account linking
https://developer.amazon.com/en-US/docs/alexa/smarthome/set-up-account-linking-tutorial.html#account-link
ユーザープロファイル
https://developer.amazon.com/ja/docs/login-with-amazon/customer-profile.html

ここまで入力したらページ右上の「保存」ボタンを押下します。
また、この画面にある「Alexaのリダイレクト先URL」3つをコピーします。

Login with Amazonのウェブ設定画面に戻り、「Alexaのリダイレクト先URL」からコピーした3つのURLを、「別のものを追加する」をクリックして「許可された返信URL」に貼り付けます。「保存」ボタンを押下します。

Step8:アクセス権限の設定

スキルを有効にした際、AccessGrantディレクティブがスキルに送信されるようにします。この設定をしないとリフレッシュトークンを取得できず、イベントゲートウェイに送信するときに必要なアクセストークンが取得できなくなります。
スマートホームスキル画面の左メニューから「アクセス権限」を選択します。
「Alexaイベントを送る」をON状態します。そうするとAlexaスキルメッセージングの項目が有効になります。「AlexaクライアントID」と「Alexaクライアントシークレット」をコピーしメモします。
クライアントシークレットは「表示」ボタンで表示させます。
このページに保存ボタンはないですが、他の画面の「保存」ボタンで変更を保存します。
このクライアントIDとクライアントシークレットをLambda関数のindex.jsにコピーします。

Step9:Lambda関数の完成

AWS Lambdaの関数画面に戻り、コードindex.jsの先頭にあるclient_ID=とclient_secret=にアクセス権限からコピーした値を貼り付けます。
「Deploy」ボタンをクリックして変更を反映します。

以上でスキルの作成は完了です。
次にAlexaアプリでスキルを有効にしてみます

スイッチでAlexa定型アクションを起動~導入編~

Alexaは音声でいろいろ動作するので便利なのですが、声に出すのが面倒だったり、声を出したくない場面があったりするので、スイッチで定型アクションを起動できる方法がないか探してみました。
そうするとSTREAM DECKでそれを実現しているページが見つかりました。

STREAM DECKでAlexaの定型アクションを起動!IFTTTを使わずスマートコントロールパネルを作る方法【Mac向け】
https://misc-log.com/streamdeck-alexa/

STREAM DECKは1万円以上もする高価なものですし、自分はMacユーザーでもないのでそのまま実行するのは躊躇してしまいますが、核となる部分は「あんスマ」の以下の記事のものであり、一読するとSTREAM DECKやMacを使わなくてもできそうでした。

実質有料化のIFTTT代替!自作Alexaスキルでスマートホーム機器を自在に活用しよう
https://android-smart.com/2020/10/alexa_smarthomeskill.html

今回、ラズベリーパイとダイソーのテンキーで同様の機能を実現することができました。

「あんスマ」の記事通りにやれば半日もあればできると思いますが、私にとっては結構な時間がかかりました。理由には以下が挙げられます。
1)記事に書かれている通りにやっていない。手順を間違えている。記事には手順しか書かれておらず、Alexaスキルに対する知識がないので、間違っていても原因にたどり着くのに時間がかかってしまいました。
2)Alexaスキルについてわかりやすく書かれているページや書籍が少なく、どういう仕組みで動作するのか理解するのに時間がかかりました。

今回でそれなりにAlexaスキルについての知識が付きましたので、それについても記載したいと思います。

購入品

自分の場合、ラズベリーパイは持っていましたので、購入したものはダイソーのテンキー(税込330円)のみとなります。

写真ではラズベリーパイ3B+を使用していますが、新たにラズベリーパイを購入する場合は、ラズベリーパイ ゼロ W(税込1,540円)で充分です。最近はラズベリーパイが品薄になっており、高値で取引されていますが、ラズベリーパイゼロ2 W(税込2,508円)が出回るようになってきました。

SWITCH SCIENCE Raspberry Pi Zero 2 W
https://www.switch-science.com/catalog/7600/

ラズベリーパイZEROはUSB microBで給電すれば起動できるので、その辺に転がっているUSBのACアダプタや100均のケーブルで賄えます。しかし、HDMI(mini HDMI)とUSBコネクタが特殊なので実際に使うにはさらなる購入品が必要です。mini HDMIケーブルは稼働時には不要ですが、インストール時に必要です。1つあれば十分でしょう。USB-microBオス-USB-Aメス変換コネクタはCan★Doで売っておりました。Amazonの怪しい最安品でも400円程するのでこれは助かります。

microSDカードはCan★Doの16GB(税込550円)で充分です。Amazonでは32GB 約600円程度が最安値でした。

スマートホームスキル

スイッチで定型アクションを動作するにはAlexaスマートホームスキルというものを作成する必要があります。
以下はAmazon alexa developer documentation(https://developer.amazon.com/ja-JP/docs/alexa/smarthome/understand-the-smart-home-skill-api.html)に掲載のスマートホームスキルの概要図です。

どうやらスマートホームスキルとは、Alexaクラウドとスマートホームデバイスメーカーのクラウドを仲介するプログラムであるようです。
例えばNature Remoのような赤外線リモコンで電気をつける場合、以下のような動きになると考えられます。
1) echoに「アレクサ、電気を点けて」というとechoは音声をAlexaクラウドに送信する。
2) Alexaクラウドは音声を解析し、Remoのスマートホームスキルに指令(ディレクティブ)を送る。
3) スマートホームスキルは電気を点けるようにNature Remoのクラウドに送信する。
4) Nature Remoのクラウドは家庭のRemoに電気をつける信号を出すように送信する。
5)各家庭のRemoは電気をつける赤外線信号を送出し、照明がそれを感知して点灯する。

温度などの状態をechoにたずねる場合も同じ経路をたどりますが、逆の経路で温度データが戻ってきてechoに届くという処理が追加されます。

センサーが自発的に通知する場合、例えばモーションセンサーが人の動きを察知してechoに通知する場合は、図中の「非同期応答、プロアクティブな状態更新」で示されるように、スマートホームスキルを経由せずにメーカーのクラウドからAlexaクラウドに直接通信され、echoに届きます。この時のAlexaクラウドの受け口をイベントゲートウェイといいます。

今回作成するスマートホームスキル

上記で説明される通常のスマートホームスキルに対して、今回作成するスキルは以下のようになります。

一見してわかるように今回作成するスマートホームスキルにはメーカーのクラウドや家の中のデバイスは存在しません。
今回作成するスキルは以下のような働きをします。
1) スキルが有効になった際にデバイスの検出を行いますが、その際にスキルは実際には存在しない架空のセンサーがあるかのように振舞います。今回はテンキーのそれぞれのキーに割り当てる10個のセンサーがあるとAlexaクラウドに返します。
2) ユーザーからの状態要求や変更要求にはテキトーに応答したり、無視したりします。そもそもデバイス検出時のプロパティにはそのような機能はないとしています。イベントの変更通知だけは可能なプロパティとなっています。
3) テンキーでキーが押されるとラズベリーパイはAlexaクラウドのイベントゲートウェイに対してセンサーが検出したというイベントの送信を行います。Alexaクラウドはechoにセンサーが検出したことを通知します。

Alexaアプリでセンサーの検出を実行条件にした定型アクションを作成しておくことで、キー操作によって定型アクションを起動することができます。

AWS lambdaとは何か

上図で示されているように今回作成するスマートホームスキルはAWS lambda上で動作します。
AWSとはAmazon Web Servicesの略で、要するにレンタルサーバーです。通常のレンタルサーバーはサーバー上のリソースを一定期間定額でレンタルするものですが、AWSは動作した分だけ課金する、というのが特徴です。
例えばある会社がキャンペーンを行った際、アクセスが多すぎてサーバーがダウンしてしまった、というのは聞いたことがあると思います。このような時、AWSに頼めばアクセスが発生した分だけAWSのサーバー割り当てが自動的に拡大されていき、決してサーバーダウンすることはありません。料金もCPUが稼働した分だけかかるという仕組みなので自社サーバーを増設するコストと手間を考えると圧倒的に安上がりになるらしいのです。自社サーバーの全てをAWSに移行することもできるし、一時的に自社サーバーとAWSを連携させるような運用をすることもできます。AWSのページにログインしページ上から簡単な設定を行うことで、あらゆるサービスを利用することができるようなのです。
Lambdaというのはサーバー上でプログラムを実行するというAWSのサービスの一つです。料金は発生しますが、その料金はリクエストの数とコードの実行時間に基づいて課金されます。100万リクエストまでは無料で利用できますので、今回のスキルでは何かの間違いがない限り料金は発生しないでしょう。
今回の作業でいろいろな画面を行ったり来たりして非常に煩わしかったのですが、使っているのはAWSのサービスのごく一部で、それを無料で使わせてもらっているのだな、という事が本を読んでわかりました。Alexaスキル専用のものではないということで、多少の煩わしさにも仕方がないと納得しました。この本を読んだからと言って技術的に何かできるようになるわけではないですが、非常に興味深く、なおかつ、わかりやすかったので紹介します。

図解 Amazon Web Servicesの仕組みとサービスがたった1日でよくわかる
\2,200 SBクリエイティブ (2022/2/2) ISBN-13: 978-4815612818

AWSのサーバーは世界各地にありますが、日本でホームスキルを作成する場合、米国西部(オレゴン)のAWSにプログラムを置く必要があります。これはエンドポイント(デバイス)が世界のどこにあるかによって、どのリージョンのAWSサーバーにディレクティブが送信されるのかが自動的に決められているからです。日本のAWSにプログラムを置いても動作しないので注意が必要です。

使用する必要のあるエンドポイントのリージョンと関連するAWSのリージョン
https://developer.amazon.com/ja-JP/docs/alexa/smarthome/develop-smart-home-skills-in-multiple-languages.html#deploy

Alexaイベントゲートウェイへのアクセス権限

センサーが検知したとの通知はAlexaクラウドのイベントゲートウェイへイベントを送信しますが、これを行うためにはアクセス権限が必要になります。
具体的にはAmazon LWA OAuthサーバーからアクセストークンというものを発行してもらい、そのアクセストークンというものを使ってイベントゲートウェイに通信します。
LWAとはLogin with Amazonの略です。
アクセストークンには有効期限と言うものがあって、これはトークンと同時に発行されますが、3600秒=1時間となります。1時間を過ぎたらLWA OAuthサーバーにアクセスして新しいアクセストークンを発行してもらわなくてはなりません。
アクセストークンを発行してもらう際には、リフレッシュトークンが必要になります。
リフレッシュトークンは次のようにして取得します。スキルが有効になった際に、Alexaクラウドからスキルに対してAcceptGrantディレクティブが送信されます。AcceptGrantを受け取ったスキルはそこに含まれているcodeと、クライアントID/クライアントシークレットを添えてLWA OAuthサーバーに要求するとリフレッシュトークンが返されます。リフレッシュトークンはスキルが有効な間は有効で、スキルを無効にすると無効になります。再度スキルを有効にすると新たなリフレッシュトークンが発行されます。
イベントゲートウェイに送信するスキルのみがAcceptGrantディレクティブが必要になります。AcceptGrantディレクティブを受け取るには、スキルの作成時にアクセス権限をデフォルトの無効から有効へ変更をしなければなりません。
以下はAmazonDeveloperDocument記載のOAuth2.0メッセージフローの図です。

詳細は以下に記載されています。

Alexaイベントゲートウェイへのアクセス権限のリクエスト
https://developer.amazon.com/ja-JP/docs/alexa/smarthome/authenticate-a-customer-permissions.html

デバイスの検出

スキルが有効になった後、デバイスの検出がされますが、その時AlexaからDiscoveryディレクティブが送信されます。Discoverディレクティブを受け取ったスキルはデバイスの種類別の形式に則ったフォーマットでデバイスのプロパティを返信します。
今回はMotionSensorとして登録します。フォーマットは以下に示されています。

Alexa.MotionSensor Interface Discovery
https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-motionsensor.html#discovery

上記で示されたサンプルフォーマットで注意するべき点は各インターフェースのRetrievable、ProactivelyReportedという2つのプロパティです。
Retrievableプロパティをtrueにするとそのデバイスの状態をスキルに照会できるようになります。サンプルフォーマットではtrueになっていますが、今回の目的には不要でプログラムも実装しないため、falseにします。
ProactivelyReportedプロパティをtrueにするとイベントゲートウェイで検出イベントを受け取ると、AlexaにChangeReportイベントを送信するようになります。
プロパティについての詳細は以下のDeveloper Documentを参照してください。

状態および変更レポートのサポートを指定する
https://developer.amazon.com/ja-JP/docs/alexa/smarthome/state-reporting-for-a-smart-home-skill.html#discovery

以上で導入編は終わりになります。
「あんスマ」の記事にあるプログラムがやろうとしていることの一通りの説明、または理解へのヒントは示せたのではないかと思います。私が躓いたことを中心に書きましたが、もっと理解したい方はAmazonのDeveloper Documentを読み進めていけばよいと思います。Alexaスキルに関してはあまりよさげな本がありませんし、探し方が悪いのかもしれませんが、Amazon以外のサイトでは記事が少ないように感じました。
次は具体的な方法を記載します。殆ど「あんスマ」の手順とプログラムをなぞっただけです。記事が消えてしまうかもしれないので、忘備録として書きます。このサイトの方が寿命短いと思いますが。

Alexaによる野球チャンネルの自動化3(Nature Remo Local API)

前回はNatutre RemoのクラウドAPIを使用してテレビのリモコン信号を制御しましたが、この方法だとNature Remoのクラウドサーバーを経由することになり、5分間30回のリクエスト回数の制限に引っかかりやすい、Nature Remoのサーバーが落ちていると使えない、という問題がありました。これを回避するために、Local APIを使用して、サーバーを経由せず直接Nature Remoに通信してリモコン信号を出す方法を探りました。
※方法など前回と重複するものは記載しません。

Nature Remoのアドレスを固定する

Nature Remoに対して直接通信するわけですが、Remoは今までwifiルーターのDHCPによってIPアドレスが割り当てられていました。このまま状態でプログラムを作ると使用開始前にRemoのIPアドレスを調べて、それに対して通信を行うという面倒なものになりそうなので、まずアドレスを固定しました。
他のwifiルーターにも同じような機能はあると思うのですが、私の持っているwifiルーターBuffalo Air Station WSR-1166DHPL2(古い安物)では、DHCPにMACアドレスに対して固定のIPアドレスを割り当てるという機能がありましたので、それを使用します。

まず、RemoのMACアドレスを調べます。
Test3.py
from remo import NatureRemoAPI
api = NatureRemoAPI(‘XXXアクセストークンXXX’)
devices = api.get_devices()
print(devices)

実行結果にmac_address=’xx:xx:xx:xx:xx:xx’とRemoのMACアドレスがあるので、それをもとにアドレスを固定化します。JSON整形ツールで整形するとMACアドレスを探しやすいです。
wifiルータにログインし、[詳細設定]-[LAN]-[DHCPリース]画面を開き、該当のMACアドレスに対して固定のIPアドレスが割り当てられるように設定します。wifiルーターの再起動で反映されます。

Nature Remo Local APIのページ(https://local-swagger.nature.global/)にもありますし、それを説明したサイトにもありますが、dns-sdコマンドを使っています。これについて調べてみました。

dns-sdコマンドはmDNS(マルチキャストDNS)に関係しており、既にmDNSはWindows10 に搭載している、ということまではわかりましたが、dns-sdコマンドをコマンドプロンプトで打ってもそんなコマンドがないという感じです。
探した結果、以下のページが参考になりました。

networking – dns-sdコマンドラインテストツールをWindowsまたはLinuxにインストールする方法
https://tutorialmore.com/questions-1800406.htm

dns-sdはアップルのサイトでBonjour SDKをインストールすると使えるようになるとのことでした。以下のサイトにアクセスし、[Bonjour SDK for Windows]をクリックし、ダウンロードします。Apple IDの入力を求められます。アカウントがない場合は作成します。
https://developer.apple.com/bonjour/

Bonjour SDKをインストールするとdns-sdコマンドがコマンドプロンプト使えるようになりました。

>dns-sd -B _remo._tcp

しかし、いつまで待っても何の応答もありません。Ctrl+Cで強制終了しました。
いろいろ調べてみましたが、wifiルーターの以下の設定が気になりました。
[詳細設定]-[無線設定]-[マルチキャスト制御]画面のSnooping機能が「使用する」になっていました。これはネットワークの余計なトラフィックを減少させるため、セキュリティのためにある機能のようです。理解したわけではありませんが、これを一時的に解除し、試してみました。終わったら使用する、に戻しました。

> dns-sd -B _remo._tcp
Browsing for _remo._tcp
Timestamp     A/R Flags if Domain                    Service Type              Instance Name
11:20:11.476  Add     2 24 local.                    _remo._tcp.               Remo-XXXXXX

Nature Remoが見つかったようです。このあと終わらないのでCtrl+Cで強制終了しました。得られた名前に対して.localを付け加えて以下のコマンドも試しました。

> dns-sd -G v4 Remo-XXXXXX.local
Timestamp     A/R Flags if Hostname                  Address                                      TTL
11:23:47.264  Add     2 24 Remo-XXXXXX.local.        192.168.xx.xx                                120

固定で割り当てたアドレスが取得できました。dns-sdなるものをネットで調べるのにかなり時間がかかってしまいましたので、得られた回答に少し拍子抜けしました。要は名前をマルチキャストすると該当するデバイスが返事を返す仕組みになっていて、それを使ってIPアドレスを取得しろ、ということだったのかな。Bonjour SDKはこれ以上必要ないのでアンインストールしました。

Nature Remo Local APIについて

Local APIにはGET /messageとPOST /message2つのインターフェースしかありません。GETは引数無し、POSTはメッセージのみです。この2つで何をどうすれというのでしょうか?
以下のページを参照にしました。

Nature RemoのローカルAPIを叩いて家電を操作する
https://takagi.blog/controlling-home-appliances-using-local-api-with-nature-remo/

どういうことかというと、まずリモコンをRemoに向けて、あるボタンを押します。Remoはリモコンの信号を受信すると青く点滅し、その赤外線信号のデータを一定時間記憶します。
GETするとRemoは記憶している赤外線の信号データをメッセージとして返信します。返信されたメッセージをそのボタンの信号としてメモしておきます。
メモした赤外線信号データをPOSTすると、Remoから赤外線信号を送出する、ということのようです。
どうやらRemo本体にはテレビや照明、エアコンの型式別のデータは持っておらず、リモコンのボタン1つ分のデータしか保持していないようです。

コマンドプロンプトで以下のコマンドを発行し、GETコマンドを送信します。192.168.xx.xxはRemoのIPアドレスです。

>curl -X GET http://192.168.xx.xx/messages -H “X-Requested-With: curl” -H “Expect:”

レスポンスとして以下のようなデータが返されます。
{“format”:”us”,”freq”:36,”data”:[3401,1821,353,516,……中略…… ,362,1380,351]}
レスポンスとして表示されたデータをボタンと対応させてメモしておきます。これを必要なボタンの種類だけ行います。
同じボタンでも押したときによってデータの値が微妙に違います。データの数値の意味はわかりませんが、Remoの赤外線信号の計測データなのでしょう。
※freq:36とあるのは36Hz=27.8ms間隔?、dataは27.8ms毎の信号レベル?どうでもいいことかもしれないが若干気になります。

Pythonのnature remoライブラリget_ir_signal関数を使っても赤外線信号データを取得できますが、出力がライブラリ独自のものに整形されてしまっているので、そのままでは使えませんでした。このため、curlコマンドを使用しました。curlコマンドはWindows10で標準になったようです。
送信は簡単なのでnature remoライブラリ関数send_ir_signal()を使用します。

Local API版プログラム

上記を踏まえ、今日の野球チャンネルに切り替えるプログラムを以下のように改修しました。
remo_ip=にはRemoのIPアドレスに置き換えてください。
xx-button=の部分はcurlコマンドで取得した赤外線信号のデータに置き換えてください。
プログラムをftpで転送し、上書きます。

BaseballCh.py
import pandas as pd
import openpyxl
import datetime
from remo import NatureRemoLocalAPI

excel_file = './FightersCh.xlsx'
remo_ip = '192.168.xx.xx'
bs_button = '---bs_button_data---'
cs_button = '---cs_button_data---'
down_button = '---down_button_data---'
ok_button = '---ok_button_data---'
s_button = '---s_button_data---'
ch1_button = '---ch1_button_data---'
ch2_button = '---ch2_button_data---'
ch3_button = '---ch3_button_data---'
ch4_button = '---ch4_button_data---'
ch5_button = '---ch5_button_data---'
ch6_button = '---ch6_button_data---'
ch7_button = '---ch7_button_data---'
ch8_button = '---ch8_button_data---'
ch9_button = '---ch9_button_data---'
ch0_button = '---ch10_button_data---'

# 日付の取得
dt = datetime.datetime.today()
month = dt.month
day = dt.day

# 今日のチャンネルの取得
openpyxl.reader.excel.warnings.simplefilter('ignore')
df = pd.read_excel(excel_file, sheet_name=str(month))
ch = df.iloc[day-1, 3]
if ch == ch:
    # チャンネルあり、チャンネル名の操作取得
    df_ch = pd.read_excel(excel_file, sheet_name=str('channel'))
    for row in range(len(df_ch)):
        if ch == df_ch.iloc[row, 1]:
            # chの操作数取得
            num = df_ch.iloc[row, 2]
            # Nature Remoオブジェクトの取得
            api = NatureRemoLocalAPI(remo_ip)
            # 操作数でループ
            for i in range(num):
                # ボタン操作を送信
                button = str(df_ch.iloc[row, 3+i]).upper()
                if button == 'BS':
                    api.send_ir_signal(bs_button)
                elif button == 'CS':
                    api.send_ir_signal(cs_button)
                elif button == 'S':
                    api.send_ir_signal(s_button)
                elif button == 'DOWN':
                    api.send_ir_signal(down_button)
                elif button == 'OK':
                    api.send_ir_signal(ok_button)
                elif button == '0':
                    api.send_ir_signal(ch0_button)
                elif button == '1':
                    api.send_ir_signal(ch1_button)
                elif button == '2':
                    api.send_ir_signal(ch2_button)
                elif button == '3':
                    api.send_ir_signal(ch3_button)
                elif button == '4':
                    api.send_ir_signal(ch4_button)
                elif button == '5':
                    api.send_ir_signal(ch5_button)
                elif button == '6':
                    api.send_ir_signal(ch6_button)
                elif button == '7':
                    api.send_ir_signal(ch7_button)
                elif button == '8':
                    api.send_ir_signal(ch8_button)
                elif button == '9':
                    api.send_ir_signal(ch9_button)
            break

課題

テストしてわかったのですが、5分間30回というリクエスト回数制限はLocal APIを使っても変わりませんでした。何故かそう思いこんでいたようです。
サーバーを経由してないため、Nature Remoサーバーが落ちていても動作できるのは、多分そうだと思いますが、試していないのでわかりません。
同じボタンなのに、試行毎にちょっとずつ違うデータが取得されるというのも少し気になります。(後日、数字の9をテレビが認識してくれないのが発覚し、データを再設定しました。)

Alexaによる野球チャンネルの自動化2(Nature API)

定型アクションによる自動チャンネル変更は節が変わる度に定型アクションのメンテナンスが必要でした。年間放送スケジュールは決まっているため、これを使ってチャンネルを変更する方法を考えてみました。 リモコンを自動化するNature RemoはAlexaと連携するだけではなく、Nature APIと呼ばれるHTTP通信でのリモコン機能も提供しています。これにより、個人が独自に作成したプログラムからNature Remoを制御できます。

Nature Developer Page
https://developer.nature.global/

チャンネル変更のテスト

まず、Nature APIでチャンネルを変更するテストをしてみました。
以下のページを参考にしました。

Nature Remo を Python から操作する
https://qiita.com/morinokami/items/6eb2ac6bed48d2c7534b

テスト環境はWindows10+VisualStudioCodeにしました。Pythonも含め、既に過去にインストールしていたからです。Pythonがインストールされていればどの環境でも問題ないと思われます。
とりあえず、PythonのNatureRemoライブラリをインストールしました。

>pip install -U nature-remo
-Uオプションは、指定されたすべてのパッケージを、利用可能な最新バージョンにアップグレードするという意味のようです。

Nature APIを使用するには以下のサイトでアクセストークンと呼ばれるIDを取得しなければなりません。
https://home.nature.global/
アクセストークンは取得したときの画面でしか確認することができず、後で見ることができないので注意してください。画面に表示された文字列をコピーして保存しておきます。

リモコン操作を行うには、アクセストークンの他にもAppliance IDと呼ばれる家電IDも必要です。家電IDを取得するテストプログラムは以下のようになります。

Test1.py

from remo import NatureRemoAPI
api = NatureRemoAPI('XXXXXXXXX')
appliances = api.get_appliances()
print(appliances)

XXXXXXXXXの部分はNatureのWebサイトで取得したアクセストークンになります。使用したNatureRemoライブラリはHTTP通信が隠蔽されていて、とても簡単に使用できます。
テストプログラムを実行し、成功するとJSON文字列が出力されます。これをJSONを見やすくしてくれるサイトで整形します。
https://json.onl.jp/

得られた結果からテレビのAppliance IDを探し、メモします。我が家の場合、照明、エアコンの下にテレビの記載がありました。

また、必要なボタン名(name=で示される)もメモしておきます。本当はシーンを直接指定したかったのですが、私はNatureAPIからシーンを取得することはできませんでした。

上記で得られた情報でチャンネルを切り替えるテストプログラムです。
GAORA sports(CS254)に切り替えます。
ご自身で取得したアクセストークンとAppliance ID、ボタン名をコピーして実行してください。
チャンネルの切り替え操作はご自分のテレビの操作に合わせたプログラムにしてください。
Test2.py

from remo import NatureRemoAPI

access_token = '-----アクセストークン-----'
appliancees_id = '-----Appliance ID-----'
bs_button = 'input-bs'
cs_button = 'input-cs'
down_button = 'down'
ok_button = 'ok'
s_button = 'submenu'
ch1_button = 'ch-1'
ch2_button = 'ch-2'
ch3_button = 'ch-3'
ch4_button = 'ch-4'
ch5_button = 'ch-5'
ch6_button = 'ch-6'
ch7_button = 'ch-7'
ch8_button = 'ch-8'
ch9_button = 'ch-9'
ch0_button = 'ch-10'

api = NatureRemoAPI(access_token)

api.send_tv_infrared_signal(appliancees_id, cs_button)
api.send_tv_infrared_signal(appliancees_id, s_button)
api.send_tv_infrared_signal(appliancees_id, down_button)
api.send_tv_infrared_signal(appliancees_id, down_button)
api.send_tv_infrared_signal(appliancees_id, ok_button)
api.send_tv_infrared_signal(appliancees_id, ch2_button)
api.send_tv_infrared_signal(appliancees_id, ch5_button)
api.send_tv_infrared_signal(appliancees_id, ch4_button)

チャンネルが切り替わったでしょうか?
注意点があります。NatureRemoAPIは5分間に30回の使用制限があります。上記の場合は1回のチャンネル切り替えに8回の送信を行っていますので、連続して行うと3回までしか成功しません。4回目の途中でエラーになってしまいます。

次にその日のチャンネルを取得する方法です。Pythonのpandasというライブラリを使用すればExcelファイルを操作できるという事で、Excelに情報を格納することにしました。

その日のチャンネルを取得し、変更するプログラム

まず、以下のようなExcelファイルを作りました。月ごとにシートを作りました。
曜日、対戦相手はプログラムでは使いませんが、見やすくするために項目を作りました。ミスなく入力できるように、対戦相手とチャンネルは[channel]シート、[team]シートで定義したリストから選択するようにしています。
曜日は1日の曜日を入力後、ドラッグすれば以下の曜日が入ります。

[channel]シートにはチャンネル名とチャンネルへのリモコン操作も定義しました。ボタン操作数も汎用性を持たせるために定義しておきました。

Excelファイルはダウンロードできるようにしておきました。FightersCh.xlsxというファイル名です。

このExcelファイルを読み込み、チャンネルを変更するPythonプログラムは以下のようになりました。
BaseballCh.py

import pandas as pd
import openpyxl
import datetime
from remo import NatureRemoAPI

excel_file = './FightersCh.xlsx'
access_token = '-----アクセストークン-----'
appliancees_id = '-----Appliance ID-----'
bs_button = 'input-bs'
cs_button = 'input-cs'
down_button = 'down'
ok_button = 'ok'
s_button = 'submenu'
ch1_button = 'ch-1'
ch2_button = 'ch-2'
ch3_button = 'ch-3'
ch4_button = 'ch-4'
ch5_button = 'ch-5'
ch6_button = 'ch-6'
ch7_button = 'ch-7'
ch8_button = 'ch-8'
ch9_button = 'ch-9'
ch0_button = 'ch-10'

# 日付の取得
dt = datetime.datetime.today()
month = dt.month
day = dt.day

# 今日のチャンネルの取得
openpyxl.reader.excel.warnings.simplefilter('ignore')
df = pd.read_excel(excel_file, sheet_name=str(month))
ch = df.iloc[day-1, 3]
if ch == ch:
    # チャンネルあり、チャンネル名の操作取得
    df_ch = pd.read_excel(excel_file, sheet_name=str('channel'))
    for row in range(len(df_ch)):
        if ch == df_ch.iloc[row, 1]:
            # chの操作数取得
            num = df_ch.iloc[row, 2]
            # Nature Remoオブジェクトの取得
            api = NatureRemoAPI(access_token)
            # 操作数でループ
            for i in range(num):
                # ボタン操作を送信
                button = str(df_ch.iloc[row, 3+i]).upper()
                if button == 'BS':
                    api.send_tv_infrared_signal(appliancees_id, bs_button)
                elif button == 'CS':
                    api.send_tv_infrared_signal(appliancees_id, cs_button)
                elif button == 'S':
                    api.send_tv_infrared_signal(appliancees_id, s_button)
                elif button == 'DOWN':
                    api.send_tv_infrared_signal(appliancees_id, down_button)
                elif button == 'OK':
                    api.send_tv_infrared_signal(appliancees_id, ok_button)
                elif button == '0':
                    api.send_tv_infrared_signal(appliancees_id, ch0_button)
                elif button == '1':
                    api.send_tv_infrared_signal(appliancees_id, ch1_button)
                elif button == '2':
                    api.send_tv_infrared_signal(appliancees_id, ch2_button)
                elif button == '3':
                    api.send_tv_infrared_signal(appliancees_id, ch3_button)
                elif button == '4':
                    api.send_tv_infrared_signal(appliancees_id, ch4_button)
                elif button == '5':
                    api.send_tv_infrared_signal(appliancees_id, ch5_button)
                elif button == '6':
                    api.send_tv_infrared_signal(appliancees_id, ch6_button)
                elif button == '7':
                    api.send_tv_infrared_signal(appliancees_id, ch7_button)
                elif button == '8':
                    api.send_tv_infrared_signal(appliancees_id, ch8_button)
                elif button == '9':
                    api.send_tv_infrared_signal(appliancees_id, ch9_button)
            break

今月の月のシートを開き、今日のチャンネル名を取得します。
チャンネルがあればチャンネルシートで該当するチャンネルの行を見つけ、その行の操作数と操作を取得し、ボタン操作を送信します。
このプログラムを動作させるために、追加でpandas,numpy,openpyxlをインストールする必要があります。

>pip install pandas
>pip install numpy
>pip install openpyxl

このプログラムの場合BaseballCh.pyとFightersCh.xlsxは同じディレクトリになければなりません。
プログラムを実行させ、意図通りに機能することを確認します。

AlexaとPythonプログラムをNode-Redで繋げる1

AlexaとPythonプログラムを連携させるためにNode-REDを使います。
以前、以下のページを見たときは衝撃的でした。これに従って進めます。

WM×LI: Amazon Echo と Raspberry Pi を Node-RED Alexa Home Skill Bridge で連携させて声でパソコンを起動したりシャットダウンしたりする.
https://nort-wmli.blogspot.com/2018/11/amazon-echo-raspberry-pi-node-red-alexa.html

以下のURLでNode-REDアカウントを作成し、デバイスを作成します。
Login or RegisterのRegisterでアカウントを作成してから、Loginします。
https://alexa-node-red.bm.hardill.me.uk/

Add Deviceを押下し、デバイスを追加します。
名前と動作を入力します。
今回は居間にある玄関カメラモニター用のラズパイでPythonプログラムを作動させる予定なのでRaspberryPiLivingという名前にしてみました。
ActionsはOnにしかチェックしませんでした。
Application Typeはよくわかりませんが、ACTIVITYにしてみました。
OKを選択し、デバイスを追加します。

次にNode-REDスキルを有効にして作成したデバイスを検出します。
スマホのAlexaアプリを起動して、「その他」-「スキル・ゲーム」-「検索」でNode-REDを検索します。

Node-REDを選択し、有効にします。

Node-REDのページで登録したユーザー名とパスワードを入力します。

正常にリンクができたことを確認し、閉じるボタンを押下します。

「端末の検出」を選択します。

シーンとしてデバイスが検出されたようです。「完了」を押します。
(2022/08/02追記)
Application TypeをACTIVITYにしたためにシーンとして検出されたようです。
それ以外だとデバイスとして検出されます。

Alexaアプリの「デバイス」-「シーン」で確認すると、作成したデバイス「RaspberryPiLiving」が追加されたのが確認できました。

ラズベリーパイにNode-REDをインストール

ラズベリーパイにNode-REDをインストールします。
最初は以前セットアップした防犯カメラモニター用のラズベリーパイにNode-REDだけをインストールしようとしましたが、OSの再インストールからすることにしました。
(私の理解力が無く、最新のOSをインストールしないと実現できないと思ってしまったのです。)

Raspberry PiのホームページからRaspberry Pi Imagerをダウンロードし、インストールします。最新のOSをインストールするために最新のImagerをインストールします。
https://www.raspberrypi.com/software/
ImagerでDesktop(Recommended)を選択し、イメージをSDに書き込みます。

書き込んだSDをRaspberryPiにセットし電源を入れます。
画面の指示に従い、国設定、ユーザー/パスワード設定、画面ブランク設定、Wifi設定、Softwareアップデートを行い、再起動を行います。(スクリーンショットが撮れませんでした)
メインメニューから「設定」-「Raspberry Piの設定」を選択し、インターフェースタブでSSHを有効にします。(有効にしないとftpが使えません)

メインメニューから「設定」-「Recommanded Software」を選択し、「Programming」カテゴリのNode-REDにチェックを入れ、「Apply」ボタンを押下します。
ダウンロードとインストールが始まり、成功したら「Installation complete」と表示されます。

ターミナルを開き、Node-REDの自動起動を有効にして、Node-REDを起動します。
$ sudo systemctl enable nodered
$ sudo systemctl start nodered

AlexaとPythonプログラムをNode-Redで繋げる2

ブラウザを起動し、http://xxx.xxx.xxx.xxx:1880を開きます。xxx.xxx.xxx.xxxはラズベリーパイのIPアドレスです。ラズベリーパイでも別のPCからでも構いません。

画面右上の三本線をクリックし、「パレットの管理」を選択します。
「ノードを追加」を選択して,「alexa-home-skill」と入力します。
「no-red-contrib-alexa-home-skill」が表示されたら「ノードを追加」ボタンを押下します。

「追加」ボタンを押下します。追加されたら「閉じる」ボタンでパレットの管理画面を閉じます。

画面左側のパレットの下にalexaが追加されます。

パレットから「alexa-home」と「debug」をフロー図に配置し、繋げます。
「debug」ノードは配置すると「msg.payload」の表示に変わってしまいました。
通信内容を表示する、という意味なのでしょう。

「Alexa Home」ノードをクリックします。Acount横の鉛筆アイコンをクリックします。

Node-REDのユーザー名とパスワードを入力し、「追加」ボタンを押下します。

DeviceにNode-REDのページで作成したデバイスを選択し、「完了」を押します。

Alexa Homeノードがデバイス名の表示に変わります。

右上の「デプロイ」ボタンを押下して設定内容を送信します。

テストしてみます。
スマホのAlexaアプリで「その他」-「定型アクション」で定型アクションを追加します。
「アクションを追加」を押下し、「スマートホーム」-「シーンをコントロール」と選択していき、デバイス「RaspberryPiLiving」を選択します。確認画面で「追加」を押します。

画面右上の虫アイコンを選択し、デバッグウインドウを表示します。
定型アクションを実行すると、デバッグウインドウにメッセージが表示されます。
AlexaとNode-REDが繋がったことが確認できました。

AlexaとPythonプログラムをNode-Redで繋げる3

次にRaspberryPiのPythonプログラムを転送します。
ftpアプリでBaseballCh.pyとFightersCh.xlsxを転送します。
ftpはバイナリモードになっていることを確認してください。テキストモードで転送するとファイルが壊れます。
転送先は適切なディレクトリを選択すべきですが、ここでは仮に/home/pi/にしておきます。

ターミナルで必要なPythonライブラリのpandas、numpy、openpyxl、NatureRemoAPIをインストールします
$ pip install -U pandas
$ pip install -U numpy
$ pip install -U openpyxl
$ pip install -U nature-remo

ここで転送したプログラムを起動してみます。
$ python BaseballCh.py
すると次のようなエラーが発生しました。

IMPORTANT: PLEASE READ THIS FOR ADVICE ON HOW TO SOLVE THIS ISSUE!

Importing the numpy C-extensions failed. This error can happen for
many reasons, often due to issues with your setup or how NumPy was
installed.

We have compiled some common reasons and troubleshooting tips at:

    https://numpy.org/devdocs/user/troubleshooting-importerror.html

Please note and check the following:

  * The Python version is: Python3.9 from "/usr/bin/python"
  * The NumPy version is: "1.22.4"

and make sure that they are the versions you expect.
Please carefully study the documentation linked above for further help.

Original error was: libcblas.so.3: cannot open shared object file: No such file or directory

エラーメッセージ示されたページで確認すると、libatlas-base-devをインストールするか、numpyをアンインストールし、python3-numpyをインストールしてみろ、とのことでした。

指示通りlibatlas-base-devをインストールしたところ、エラーなく動作するようになりました。
$ sudo apt install libatlas-base-dev

Node-REDに戻り、debugノードを削除して代わりに「exec」ノードを配置、接続します。
「exec」ノードをクリックします。

コマンドに「python /home/pi/BaseballCh.py」、名前に「チャンネル変更」と入力し、「完了」ボタンを押下します。

「デプロイ」ボタンでフローを送信します。

最後に「日ハムの試合にして」という定型アクションを変更します。チャンネルのシーンを削除し、Node-REDデバイスを追加します。これで作成完了です。

『アレクサ、日ハムの試合にして』と言うと北海道日本ハムファイターズを放送するチャンネルに切り替わるはずです。

課題

節の変わり目でいちいちメンテナンスする必要がないようにシステムを変更する事が出来ました。しかし、不満な点があります。

1)常に稼働するコンピュータが必要である。
自分の場合は防犯カメラで24時間稼働中のラズパイがありそれを使用しましたが、ない場合はこれだけのために稼働させることになるため、スマートな感じがしないです。
Alexaスキルを作成できれば解決するかもしれません。

2)NatureAPIの回数制限に引っかかりやすい。
1回のチャンネル変更に8回のNatureRemoサーバーとの通信が発生するので、5分間30回の制限に到達しやすいです。実際使用する上では1日1回程度しか使わないので規制に引っかかることは少ないと思いますが、昼間だと赤外線が通りにくいときがあり、何回も言ってしまうことがあるかもしれません。デバッグの時は動作確認が捗りませんでした。Nature Remoのサーバーをタダで使わせてもらっているので、制限自体は理解できます。もっと制限を緩くしてとは思いません。
解決策としては以下が思い浮かびます
a.シーン指定をできる仕組みがあれば、1回の通信で済むので回数制限を意識することは殆どなくなると思います。
b.Nature RemoのLocal APIを使えればクラウドサーバーを経由せず、LAN内で完結するので回数制限と関係なくなります。NatureRemoサーバーがダウンしていても使えるという利点もあります。

3)雨天順延等のスケジュール変更に手間がかかる。
雨天順延でスケジュールが変更になった場合、Excelファイルを修正し、それをラズベリーパイにアップロードします。これは定型アクションでチャンネル操作をまとめたシーンを指定するやり方と比べ、手間がかかります。注意してExcelファイルを作成したつもりでも間違っている心配もあります。

4)家にいないと、メンテナンスできない
ExcelファイルのアップロードはLAN内でしかできないため、家に不在のときは作業を行うことができません。発端は機械音痴の家族のためのシステムなので、これは片手落ちと言っても過言ではないかもしれません。
ただ、この場合は定型アクションで追加するアクションを一時的にNode-REDデバイスからチャンネルのシーン指定に戻すことで解決します。やはり、シンプル・イズ・ベスト。単純な方が良いシステムなのだと思わさります。

Alexa定型アクションによる野球チャンネルの自動化

最近Amazon Echoにハマっています。スマートリモコンであるNature Remo miniを同時に買ったのですが、これとEchoを組み合わせると、言葉でテレビや照明、エアコン等を操作でき、まるで未来の家のようです。

しかしながら、音声によるテレビ操作は『アレクサ、テレビつけて/消して』と言えば電源のON/OFFができるので、この限りにおいてはまあ良しですが、チャンネル変更に至っては今一つ便利でありません。リモコンのボタン操作一つ一つをアレクサに言葉で言うことになり、BSやCSのチャンネルに合わせるのはひと苦労です。

若干切実な問題もあります。同居の親が機械音痴で何回教えてもスカパーのプロ野球にチャンネルを合わせることができません。できるときもありますが、チャンネルを見つけることができない日もあるのです。年齢を考えるとこの先改善する可能性も低いです。Echoを買う前にもいろいろ試してみましたが、イマイチしっくりきませんでした。

『アレクサ、日ハムの試合にして』と言うだけでスカパーの応援するチームのチャンネルに合わせる方法を探りました。

シーンの作成

調べたところ、シーンという機能がNature Remoにあることがわかりました。シーンは本来、複数の家電製品を同時に制御する機能です。例えば『おやすみ』のシーンでは照明、エアコン、テレビを同時に消す、というように、複数の家電製品の操作をシーンに合わせてひとまとめにするときに使います。このシーンという機能を使いチャンネル選局をすることを思いつきました。
居間にあるテレビはパナソニックの古いビエラですが、『3桁入力選曲』という機能を使って選局することにします。

パナソニックのビエラのリモコン

例えばGAORA<CS254>を選局する場合、『CS』に合わせてからサブメニューである『S』ボタンを押しサブメニューを表示します。3桁入力選局は上から3番目ですので、『▼』『▼』『決定』という操作になります。次にチャンネル番号『2』『5』『4』を入力すると、GAORAが選局されます。

つまり、『CS』『S』『▼』『▼』『決定』『2』『5』『4』というテレビリモコンの一連の操作をひとまとめにした『GAORA』という名前のシーンを作成します。ボタン操作の間に間隔は不要のようです。

スマホのNature Remoアプリでシーンを作成

プロ野球を放送するチャンネルの数だけ、シーンを作成します。

フジテレビTWO、スポーツライブ+、日テレNEWS24、J SPORTS 1、J SPORTS 2、J SPORTS 3、J SPORTS 4、スカイA、日テレジータス、TBSチャンネル2、フジテレビONE

全部作ると、結構大変ですね。

定型アクションの作成

スマホのアレクサアプリの定型アクションを作成し、『アレクサ、日ハムの試合にして』と言うだけでスカパーの日ハムの試合に自動的にチャンネルが切り替わるようにします。

定型アクション名:日ハムテレビ

実行条件:音声『アレクサ、日ハムの試合にして』

アクション:[スマートホーム]-[シーンをコントロール]-[GAORA]

これで作成が完了です。

アクションで選択するシーンは節ごとに都度日本ハムファイターズの公式ホームページで確認して、定型アクションの設定を変更します。

https://www.fighters.co.jp/news/broadcast/

1週間に2度ほど定型アクションのメンテナンス作業が必要なシステムはカッコ悪いですが、これが一番簡単で最も手間がかからない方法です。現在、もっと自動的でスマートな方法を検討中ですが、調べているうちに結構な時間を費やしてしまいました。総合的に判断してこれが一番賢い方法だと自信を持って言えます。(笑)

ラズベリーパイ防犯カメラ ~再生秒数の長い録画だけを抽出する機能を追加~

実際に玄関まで来た録画のみをリストアップ

防犯カメラ機能を実際に使っていると家の前を数秒で通り過ぎる録画が沢山出来てしまい、玄関に訪問した録画だけを抽出する機能が欲しくなりました。

調べてみると郵便受けに投函するだけの配達は20秒以上、チャイムを鳴らす配達や訪問は1分以上かかることがわかりました。時間で絞り込めばよいのではないかと考え、機能を追加しました。

時間で録画を絞り込む機能を追加

mp4ファイルの再生時間を抽出するのに、当初はffmpegを使用することを検討していましたが、このコマンドは動画ファイルの全てのプロパティをキャラクタで出力し、そこから再生時間だけを抜き出すという処理が必要になるので、1ファイルにつき0.5秒程かかります。現行の仕組みに組み込むと画面表示にかなりの時間がかかるため、再生時間だけを出力するプログラムを自作することにしました。

解析ツールの作成に当たり、以下のURLを参考にしました。
mp4のファイル構造を解説
https://qiita.com/satken2/items/d14b4113fe3fb5f5597b
NovieファイルのAtom情報を解析
https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html
mp4 mvhd 解析
http://wikil.lwwhome.cn:28080/?p=607
mp4の情報を表示するツール Header Reader MP4
https://dev.onionsoft.net/seed/info.ax?id=1303
mp4の情報を表示するツール Mp4Reader
https://github.com/suzutsuki0220/Mp4Reader

作成したプログラムmp4lenのソースは以下のようになります。
コマンド成功で戻り値に0を返し、標準出力に再生時間をmsで返します。
コメントが少なすぎますがご容赦を
main.cpp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int cnv4b2int( unsigned char* buf );

int main(int argc, char** argv){
  int ret = 1;

  if( argc < 2 ){
    return 1;
  }
  FILE* fp  = fopen( argv[1], "rb" );
  if( fp == NULL ){
    return 1;
  }

  for( int i = 0; i < 100; i++ ){
    unsigned char typeBuff[8] = { 0 };  // size(4)type(4)のバッファ
    if( fread( typeBuff, 1, 8, fp ) < 8 ){
      break;
    }
    int chklen = cnv4b2int( &typeBuff[0] );
    if( memcmp( "moov", &typeBuff[4], 4 ) == 0 ){
      for( int j = 0; j < 100; j++){
        if( fread( typeBuff, 1, 8, fp) < 8 ){
          break;
        }
        int chklen2 = cnv4b2int( &typeBuff[0] );
        if( memcmp( "mvhd", &typeBuff[4], 4 ) == 0 ){
          if( fseek( fp, 16, SEEK_CUR ) != 0 ){
            break;
          }
          if( fread( typeBuff, 1, 4, fp ) < 4 ){
            break;
          }
          printf( "%u\n", cnv4b2int( typeBuff ) );
          ret = 0;
          break;
        }
        if( fseek( fp, chklen2 - 8, SEEK_CUR ) != 0 ){
          break;
        }
      }
      break;
    } else {
      if( fseek( fp, chklen - 8, SEEK_CUR ) != 0 ){
        break;
      }
    }
  }
  fclose( fp );
  return ret;
}

int cnv4b2int( unsigned char* buf ){
  return (buf[0]*0x1000000 + buf[1]*0x10000 + buf[2]*0x100 + buf[3]);
}

mp4lenプログラムはビルドして以下の圧縮ファイルに同梱していますが、自分でビルドする場合は以下のコマンドを実行します。
$gcc main.cpp -o mp4len
実行時間は15msほどになりました。60ファイルで1秒ほどかかりちょっと時間がかかりますが、大分改善されました。

ftpアプリを使い、更新した新しいhtmlファイルの一式を/var/www/htmlに上書き転送します。
※これまでのファイルは削除しても構いません。
※「カメラ用ラズパイ・サーバーセットアップ」を参照

アップロードしたmp4lenプログラムに実行権限を付与します。
$chmod a+x /var/www/html/mp4len

これで時間で絞り込む機能が追加されます。
※ボタンがうまく表示されない場合は、chromiumブラウザのキャッシュをクリアします。

絞り込み時間を変更する場合は/func/config.iniの項目を変更します。
middle=20
long=60

ラズベリーパイ防犯カメラ ~CLIモードセットアップメモ~

忘備録として

カメラ用ラズパイのセットアップはSSHを経由してセットアップする前提で記載しましたが、モニタやキーボードを直接ラズベリーパイに接続して操作するためのセットアップを忘備録として記載します。

モニタの解像度変更

$sudo raspi-config⏎

2 Display Options – D1 Resolutionを選択します。

解像度を選択します。※CEAはテレビ出力、DMTはPC用モニタとなります。

設定後、<Finish>を選択しコンフィグを終了させ、リブート確認画面で<Yes>を選択すると再起動し、解像度が変更されます。

この設定は/boot/config.txtを直接変更することでも変更できます。

#framebuffer_width=xxxxx
#framebuffer_height=xxxxx
の行のコメント#を削除し以下のように編集します。
framebuffer_width=640
framebuffer_height=480

もしくはは
#hdmi_group=x
#hdmi_mode=xx
を以下のように編集します
hdmi_group=2
hdmi_mode=4

※hdmi_groupの説明
0 自動検出
1 CEA
2 DMT

hdmi_modeは下記URLを参照して設定します。hdmi_grouppで変わるので注意。
https://www.raspberrypi.com/documentation/computers/config_txt.html#hdmi-mode

日本語キーボードの設定

デフォルトの設定ではShuft+;で”+”などの文字が打てなくなるので日本語キーボードを設定します。

$sudo raspi-config

5 Location Options – L3 Keybordを選択します。

Generic 105-key PC (intl.)を選択<OK>
Otherを選択<OK>
Japaneseを選択<OK>
Japanese -Japanese (OADG 109A)を選択<OK>
The Default for the keyboard layoutを選択<OK>
No compose keyを選択<OK>

日本語の文字化けが気になる場合

$sudo raspi-config

5 Location Options – L1 Localeを選択します。

ja_JP.UTF-8 UTF-8を選択後、ja_JP.UTF-8ではなく、en_GB.UTF8を選択する。
これでメニューが英語になり、日本語表示しなくなるので文字化けが無くなります。

■日本語表示と日本語入力の設定

日本語表示させたい場合は以下の設定を行います。

日本語フォントのインストール
notoフォント
$sudo apt install fonts-noto

日本語入力メソッドをインストール
$sudo apt install uim uim-anthy

日本語対応ターミナルをインストール
$sudo apt install jfbterm

ローカルコンソールでの”jfbterm”の起動を自動化
$sudo nano .bashrc

 If this is an xterm set the title to user@host:dir
case "$TERM" in
xterm*|rxvt*)
    PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"
    ;;
linux)					@@行追加@@
    if [ -c /dev/fb0 ]; then		@@行追加@@
        jfbterm -q -e uim-fep -u anthy	@@行追加@@
        exit				@@行追加@@
    fi					@@行追加@@
    ;;					@@行追加@@
*)
    ;;
esac

「CTRLキー」+「スペースキー」で日本語入力モードに切り替える設定
$sudo nano /usr/share/uim/generic-key-custom.scm

(define-custom 'generic-on-key '("zenkaku-hankaku" "<Control> ")	@@ShiftをControlに変更@@
               '(global-keys1)
               '(key)
               (N_ "[Global] on")
               (N_ "long description will be here"))

(define-custom 'generic-off-key '("zenkaku-hankaku" "<Control> ")	@@ShiftをControlに変更@@
               '(global-keys1)
               '(key)
               (N_ "[Global] off")
               (N_ "long description will be here"))

ラズベリーパイ防犯カメラ ~ブラウザ用ラズパイが止まる?~

chromiumの不具合?

ブラウザ用ラズパイをセットアップしなおしたところ、起動して暫くすると現在の画面が更新されずに停止してしまいます。

chromiumの設定をいろいろ変更しても変わりませんでした。

ソフトウエアのアップデートをスキップしたところ、これまで通り止まらずに動作するようになりました。

最新のchromiumブラウザに何か問題があるのかもしれません。

(2022/6/11追記)
セットアップしなおしたところ、現行では解決しているようです。
また、Raspbery Pi Imagerは最新のOSイメージがあればダウンロードしてそれを使うと思っていたのですが、最新のOSイメージを使う場合は最新のRaspberry Pi Imagerをインストールしなおさなければならないようです。