カメラのモーション検知時間を記録し、アレクサでお知らせする

2階にいるとチャイムの音が聴こえにくいので、宅配便が来た事に気が付かないことがあります。玄関に人が入ってきたらアレクサにお知らせしてもらおう、という試みです。

以前から玄関フード内にカメラをセットしたラズベリーパイZEROを設置していて、インターホンの横に貼り付けておりました。カメラ付きインターホンのように使えればよいかなと思ったのですが、視野が狭く殆ど使い物になっていません。敢えて言うと冬に外に出なくても雪庇の状態を確認できるという程度です。
今回このカメラを人感センサーとして使おうと思います。

motionは動きを検知したら動画を記録するのですが、そのタイミングをトリガーにしてプログラムを動かせないかを調べたところ、motionにオプションが用意されていました。
on_event_start
https://motion-project.github.io/motion_config.html#on_event_start

動きを検知したらホームディレクトリにあるシェルスクリプトを起動するように/etc/motion/motion.confの最後に以下の一文を追加します。
on_event_start /home/pi/detect.sh

detect.shの内容は以下のようになっています。

#!/usr/bin/bash
cd /home/pi
python ./camera_detect.py

pythonプログラムcamera_detect.pyは、起動されると現在時間をファイルに記録し、ダミーセンサーを検知状態にします。アレクサで定型アクションを作成し、ダミーセンサーの検知を実行条件にしてアレクサにお知らせしてもらうように設定します。
プログラムは以下のようになっております。

camera_detect.py

from datetime import datetime
import os
import subprocess

# 連続回避
SLEEP_TIME = 300
LATEST_FILE = "./latest.txt"
# LOGファイル名
LOGFILE = '/var/www/html/sensor_log.txt'
MAX_LINE = 100

# 連続回避
def isStart(now):
  if os.path.exists(LATEST_FILE):
    f_time = os.path.getmtime(LATEST_FILE)
    expiring_time = datetime.fromtimestamp(f_time + SLEEP_TIME)
    if now > expiring_time:
      f = open(LATEST_FILE, 'w')
      f.close()
      return 1
    else:
      return 0
  else:
    f = open(LATEST_FILE, 'w')
    f.close()
    return 1

# テキスト保存
def saveText(now):
  list = []
  if os.path.exists(LOGFILE):
    f = open( LOGFILE, 'r' )
    for line in f.readlines():
      list.append(line)
    else:
      f.close()
  start = 0
  num = len(list)
  if num >= MAX_LINE:
    start = num - MAX_LINE + 1
  f = open( LOGFILE, 'w' )
  for i in range(start, num):
    f.write( list[i] )
  f.write( now.strftime('%Y/%m/%d %H:%M:%S\n') )
  f.close()

# main関数
if __name__ == '__main__':
  now = datetime.today()
  if isStart(now):
    saveText(now)
    subprocess.run('./send24.sh')

プログラムの説明
最初のnow = datetime.today()では現在日時を取得しています。この時間をイベント発生日時とします。
次のisStart()関数では以降の処理をすべきかどうか判定します。前回の検出から5分(300秒:SLEEP_TIME)経たないイベントは処理しないことにしています。空のファイルlatest.txtを作成し、そのファイルの更新時間と現在日時を比較して判定します。
処理する場合はsaveText()関数で現在日時をファイルsensor_log.txtに書き込みます。古いものは削除し、直近100回(MAX_LINE)のイベント日時を書き込みます。出力ディレクトリを/var/www/html/にしたのはこれをホームページ上で表示させるためです。

次にシェルスクリプトsend24.shを起動します。
send24.sh

#!/usr/bin/bash
python ./alexa_trigger_func.py 24

pythonプログラムalexa_trigger_func.pyを実行し、ダミーセンサーNo.24を検出状態にします。
当初、シェルスクリプトを介さずに直接subprocess.run(‘python ./alexa_trigger_func.py 24’)としていました。Windows環境では上手く動作したのですが、ラズベリーパイではエラーになってしまったので、シェルスクリプトを介して起動することにしました。
そもそも、alexa_trigger_func.pyはメインのプログラムにimportし、関数コールで実行するつもりでした。しかし、それをするとラズベリーパイZEROではちょっと困ったことになったのです。当初のalexa_trigger_funcをimportしたプログラムでは現在日時が実際に発生した日時よりも7~8秒かそれ以上遅れて記録されていました。アレクサへの通知はもっと遅れて10数秒かかっています。プログラムの1行目に日時を取得しそれを書き込んでいるのに、です。ライブラリをimportすると、プログラム実行前にimportしたライブラリとそれに関連するライブラリすべてを読み込んでしまうので、最初のステップを実行する前に数秒かかっているようでした。これをmotionの動作検知イベントで行うと録画処理と並行して実行するのでさらに数秒遅くなるようなのです。ラズベリーパイZEROはCPUの処理能力が低く、それに動画処理をさせているための問題で、通常の環境では問題にならないかもしれません。
alexa_trigger_funcのimportを止めたことにより、発生日時を取得し、ファイルに書き込む処理はそれほど時間がかからずにできるようになりました。しかし、アレクサによる通知は依然として発生から10数秒かかるままです。

これを幾分解消しようと、動画のフレームレートを15から7に落としてみました。動画処理にかかる負担を減らすことでアレクサへの通知処理を速くしようという狙いです。しかしながら結果は改善したように見えませんでした。これ以上は現状のハードウエアでは諦めるしかないかもしれません。ZERO2ならもっと速くなると推測します。いつか試してみたいものです。

alexa_trigger_func.pyはメイン起動するプログラムを関数化したものですが、メイン起動も関数コールもできるように若干の改良を行いました。
alexa_trigger_func.py

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

client_ID = ''
client_secret = ''
refresh_token=''
token_file = './token.txt'

def alexa_trigger(no):
  sensor_name = 'dummy_sensor_' + format(no, '0>3')
  # アクセストークンが有効かどうかチェックする
  access_token = ''
  access_token_valid = False
  today = datetime.datetime.today()
  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)

if __name__ == '__main__':
  alexa_trigger(sys.argv[1])

最後の2行を追加しただけです。
内容の詳細については仕組みを説明した「スイッチでAlexa定型アクションを起動~導入編~(https://km2-system.com/2022/06/27/alexa_switch_1/)」、最初のバージョンについて記載した「スイッチでAlexa定型アクションを起動~スキルテスト編~(https://km2-system.com/2022/06/29/alexa_switch_4/)」、関数化させた「明日の天気によってAlexa定型アクションを起動する(https://km2-system.com/2022/08/22/alexa_switch_8/)」を参照してください。

以下は記録した日時を表示するHTMLファイルです。
sensorlog.html

<!DOCTYPE html>
<html lang='ja'>
<head>
  <meta charset='UTF-8'>
  <meta name='viewport' content='width=device-width, initial-scale=1'>
  <meta http-equiv="Cache-Control" content="no-cache">
  <title>Motion sensor log</title>
  <link rel="stylesheet" href="css/style.css">
  <script type='text/javascript'>
    var req = new XMLHttpRequest();
    req.open('get', 'sensor_log.txt', true);
    req.send(null);
    var result = '';
    req.onload = function() {
      tmp = req.responseText.split('\n');
      for( var i = tmp.length - 1; i >= 0; i-- ) {
        if( tmp[i] == '' ) continue;
        result += tmp[i];
        result += '\n';
      }
      document.getElementById('text').value = result;
    }
  </script>
</head>

<body>
  <p>人感センサー</p>
  <textarea class='list-motion-sensor' id='text' title='sensorlog' cols='25' rows='20' readonly></textarea>
</body>
</html>

/var/www/html/css/style.cssには以下の5行を追加します。ファイルやフォルダがない場合は作ります。

.list-motion-sensor{
  resize: none;
  font-size:14pt;
  background: #C0C0C0;
}

sensorlog.htmlは/var/www/html/に配置します。以下が表示イメージになります。

使った感想としては、今のところ『とても良い』です。
カメラとして視野が狭く、明るさに鈍感で夜若干明かるくなっても反応しない、というのが逆にいいです。
夜、ライトをつけた車が通っても、玄関横のトイレの明かりがついても反応しないのです。
玄関フードに入ると人感センサー付きの電池式ライトが光るので反応します。
視野が狭く、郵便や新聞を配達してくれた際に手が映るか映らないか際どい感じなので反応しないときがありますが、目的としては問題ないです。
むしろもうちょっとだけ上に設置しても良いかもしれません。

人感センサー運用失敗編

「スイッチでAlexa定型アクションを起動~人感センサー編~」で人感センサーが使えるとわかったので、実際に玄関に取り付けてみました。在宅勤務のときにはカメラブラウザ用のラズパイを目の届くところに置いているのですが、常に見ているわけでないので宅配便などお客さんが来たときに気が付かないことが多いのです。2階なのでピンポン音が小さく気が付かないのです。少し便利になるのかと。しかしながら結果は失敗に終わってしまいました。事の顛末を書きます。

まず、動作させるまでが大変でした。手ごろなケースを探して、センサーがうまく嵌るように四角い穴をあけました。センサー検知を記録するのに、もともとそこでカメラとして使っていたラズベリーパイZEROを使うことにしました。目立たないようにGPIOから配線するために工夫して基板の裏側にL字のピンを取り付けます。ちゃんと半田付けしたつもりだったのですが、接触不良で全く動作が不安定でした。不器用で下手糞です。せっかく買った素晴らしいコテを使わず、安いコテを使っていたというのもあります。あまりにも使ってないので忘れてしまっていました。なかなか正常にセンサー検知をせず、こんな簡単なことなのに何故動かないのだろうと悩み、テスターを当ててやっとわかったのです。
いろいろ苦労しながらやっと玄関に設置しても良いかなと思えるところまで仕上がりました。配線も後ろに飛び出たため、5mm厚のゴム板を貼り付け、かさ上げしたりしています。

いざ設置したところ、大体は作動しているものの、不可解な動きをします。丁度1時毎に数秒の誤差で誤検知が派生するのです。部屋で動作確認していた時も誤検知はしていたようですが、エアコンの風に反応しているのかと思い見逃していたのかもしれません。

最初センサーの動作不良を疑いました。ケースにセンサーをはめ込む四角い枠を空ける際にセンサーの基板をペタペタ触りまくったので、壊してしまったのかと思ったのです。人感センサーを買い増したのですが、結果は同じでした。部屋で動作確認しているときは1時間毎の誤検知は発生しませんでした。よしこれで行けると思い玄関に取り付けてみたところ、再発してしまいました。

プログラムに問題があるのかと思い、GPIOの初期化設定でプルダウンを設定してみましたが変わりませんでした。

ラズベリーパイZEROではもともと防犯カメラ機能(motion)とホームページ機能(apache2)を動作させているのですが、記録した動画のサムネイル表示をさせるとそのタイミングで誤検知することを発見しました。もしかしたらCPUの負荷がかかり、電力消費が大きくなると電圧が落ち込み、それが原因で誤検知するのかもしれません。1時間に1回起動する、何か知らないタスクがあるのかも。ACアダプタを調べてみると1.5Aでした。ラズベリーパイZERO単体には充分な電力ですが、カメラや人感センサーを接続すると電力不足になるのかもしれません。しかし、3Aのものに変えてみてもダメでした。カメラを取り外し、motionサービスを停止させてみても変わりません。

人感センサーの問題も捨てきれないので、センサーが反応したらLEDが付く回路を簡易的に作ってみました。電池は何年も前に買った9Vのものを使いました。保証期限はとっくに過ぎていて、あるものは2V迄落ちていて使えませんが、もう1つは7Vありました。7Vあれば大丈夫でしょう。センサーが反応するとLEDが付きます。気が付くようにしばらく机に置いています。見逃しているのかどうかわかりませんが、LEDが点くのを今のところ確認できていません。1時間に一度1秒ほどなので人の目だと厳しいです。ときどき手を伸ばしてかざすとLEDが点くので線が抜けたり、電池が弱くなったわけではありません。

ということで自分としては万策尽きました。ヘボプログラマーにはこれくらいが限界です。心の中の結論としてはこのラズパイが壊れているか、もしくはラズベリーパイZEROの持病です。とにかくこの人感センサーを使うのを諦めました。

もっと良いセンサーはないのか・・・と、ここで気が付きました。カメラもセンサーなのだと。

明日の天気によってAlexa定型アクションを起動する

明日の天気が雨か晴れかによってアクションを起こせないか、という要望をとある掲示板で目にしたので調べたところ、お天気APIというのがあるのを知りました。
自分にはこれといって必要性はないものの、面白そうなので勝手に作ってみました。(特に周知せず)

参考にしたページ
天気予報 API (livedoor 天気互換)のJSONを表示させてみた
https://four4to6.x0.com/800/

【Python】無料で使える天気予報APIで降水確率を取得する
https://creepfablic.site/2021/11/14/python-weather-api/

step1:仮想センサーの増設

雨の場合と晴れの場合で別の定型アクションを動かそうと考え、「スイッチでAlexa定型アクションを起動~スキル作成編~」で作成した仮想センサーを増やしました。
詳細は以下を参照してください。ここでは簡潔に記述します。
スイッチでAlexa定型アクションを起動~スキル作成編~
https://km2-system.com/2022/06/28/alexa_switch_2/

スキルのAWS関数のindex.js内記載のセンサー数を増やします。
sensor_num=10 を sensor_num=30 に変更して「Deploy」します。(数値は必要数にします。自分の場合、他にもやりたいことがあるため、30まで増やしました)

Alexaアプリでデバイスの検出を行うと追加したダミーセンサーが検出されます。既にスキルが有効になっているのであれば無効/有効等の操作は必要ありません。
※デバイスの検出方法:Alexaアプリの[デバイス]タブで一番上の右側+ – [デバイスの追加] – [その他]

step2:pythonプログラム

プログラムは参照ページとほとんど同じですが、もし他の方が使うときに変更する可能性があるところを中心に若干の説明を書きます。

今回、雨の時に起動するセンサー番号は22、晴れの場合は23としました。
rain_sensor_no = 22
shine_sensor_no = 23

地点コードは以下から取得します。
https://weather.tsukumijima.net/primary_area.xml
札幌の地点コードは016010でした。
city_code = “016010” #札幌市

天気は日中の予報で判断します。6時から12時までの予報と12時から18時までの予報どちらかが30%以上の場合、雨と判断するようにしました。
threshold = 30

確認用に取得した天気情報はresponse.txtというファイルに保存しておきます。
response_file = ‘./response.txt’

仮想センサーの起動は「スイッチでAlexa定型アクションを起動~スキルテスト編~(https://km2-system.com/2022/06/29/alexa_switch_4/)」で作成したalexa_trigger.pyをちょっとだけ改造し、関数化してimport可能なファイルにしました。
仮想センサーを起動するプログラムは以下のalexa_trigger_func.pyファイルをimportし、センサー番号を引数に渡して関数alexa_trigger()を呼べば良いだけになります。
※client_ID = 、client_secret = にはスキルビルド画面のアクセス権限メニューから取得したAlexaクライアントIDとAlexaクライアントシークレットを貼り付けます。(index.jsと同じもの) 「スイッチでAlexa定型アクションを起動~スキルテスト編~」参照

alexa_trigger_func.py

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

client_ID = ''
client_secret = ''
token_file = './token.txt'

def alexa_trigger(no):
  sensor_name = 'dummy_sensor_' + format(no, '0>3')
  # アクセストークンが有効かどうかチェックする
  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)
    today = datetime.datetime.today()
    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)

天気を予報するプログラムは以下のようになります。
weather.py

import requests
import json
import alexa_trigger_func

city_code = "016010"    #札幌市
threshold = 30
rain_sensor_no = 22
shine_sensor_no = 23
response_file = './response.txt'

# URL
url = "https://weather.tsukumijima.net/api/forecast/city/" + city_code
try:
    # 天気予報取得
    response = requests.get(url)
    # 通信が正常でなければ例外発生
    response.raise_for_status()
except requests.exceptions.RequestException as e:
    # エラー処理
    res_text = "Error:{}".format(e)
    print(res_text)
else:
    res_text = response.text
    # json解析
    weather_json = response.json()
    # データ取得
    yesterday = weather_json["forecasts"][1]["date"]
    chanceOfRain1 = weather_json["forecasts"][1]["chanceOfRain"]["T06_12"]
    chanceOfRain2 = weather_json["forecasts"][1]["chanceOfRain"]["T12_18"]

    am_per = int(chanceOfRain1[:-1])
    pm_per = int(chanceOfRain2[:-1])
    if am_per >= threshold or pm_per >= threshold:
        alexa_trigger_func.alexa_trigger(rain_sensor_no)
    else:
        alexa_trigger_func.alexa_trigger(shine_sensor_no)
finally:
    # fileに保存
    f = open(response_file, 'w')
    f.write(res_text)
    f.close()

step3:起動される定型アクションの作成

雨の場合と晴れの場合でそれぞれ仮想センサーの検知に対応する定型アクションを作成します。
実行条件で[スマートホーム]から該当するダミーセンサーを選択します。
アクションはとりあえずアレクサが「明日は雨です」、「明日は晴れです」と喋るだけにします。

step4:pythonプログラムの動作確認

同じフォルダにweather.pyをalexa_trigger_func.pyを置き、以下のコマンドを打ち込み動作確認します。pythonがインストールされているならばlinuxでもWindowsでも動作します。
$ python weather.py⏎
Alexaが「明日は雨です」、「明日は晴れです」などと喋れば成功です。
response.txtの内容も確認します。
$ nano response.txt

step5:pythonプログラムの起動設定について

24時間起動したラズベリーパイにプログラムを置き、毎日決まった時間にプログラムを起動するようにします。
crontabなどlinuxのシステムでそれを実現することは可能ですが、メンテナンス性という観点ではAlexaアプリの定型アクションを使う方が良いと考えました。
時刻で起動する定型アクションからNode-REDを使ってpythonプログラムを起動します。全体の流れは以下のようになります。
定型アクション(時刻起動) -> Node-RED -> pythonプログラム -> (晴/雨)定型アクション

step6:Node-REDにデバイス作成

詳細は以下を参照して下さい。ここでは簡潔に書きます。
Alexaによる野球チャンネルの自動化2(Nature API)
https://km2-system.com/2022/06/13/auto_tv_channnel_2/

Node-RED Alexa Home Skill Bridgeのページ(https://alexa-node-red.bm.hardill.me.uk/)にログインします。
[Add Device]ボタンを押下して新しいデバイスを作成します。

NameはここではRaspberryPiLiving2としました。
起動するだけなのでActionsにはOnだけにチェックします。
Action TypeはSWITCHにします。
[OK]ボタンを押下して設定します。

Alexaアプリでデバイスの検出を行い、Node-REDのデバイスを追加します。

step7:ラズベリーパイのNode-REDでフロー作成

ラズベリーパイにNode-REDをインストールしていない場合はインストールします。
※参照;Alexaによる野球チャンネルの自動化2(Nature API)

ブラウザを起動し、http://xxx.xxx.xxx.xxx:1880を開きます。xxx.xxx.xxx.xxxはラズベリーパイのIPアドレスです。ラズベリーパイでも別のPCからでも構いません。
Node-REDにalexa-home-skillを追加していない場合は追加します。
※参照;Alexaによる野球チャンネルの自動化2(Nature API)

パレットから[alexa-home]を配置し、追加したデバイス(上記の例ではRaspberryPiLiving2)を設定します。nodeの表示が[alexa-home] から[RaspberryPiLiving2]に変わります。

パレットから[exec]を配置し、[RaspberryPiLiving2]と繋げます。
execノードをダブルクリックし、コマンドを設定します。
python /home/pi/weather.py ディレクトリ’/home/pi/’の部分は自分の環境に合わせます。
名前にweather triggerなどと入れ[完了]ボタンを押します。

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

step8:alexa定型アクションでフローの起動を設定

定型アクションを作成します。
実行条件の設定で[スケージュール]を選択し、[繰り返し]と[時刻]を設定します。
アクションを追加で[スマートホーム]-[全てのデバイス]-[RaspberryPiLiving2]を選択します。

私はこれを夜寝る時間に設定し、そろそろ寝るだよという合図に使っております。

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

ダイソーのテンキーで定型アクションを起動することはできました。
注文したラズベリーパイゼロ2も到着しました。モバイル機器にできないかと思案したところ、キャン★ドゥのスマホ充電器(税込550円)を使うことを思いつきました。ラズパイゼロ2に繋げたところ、問題なく動きます。

良い箱がないかと物色したところ、キャン★ドゥに丁度良い木箱がありました。

テンキーがピタリと入ります。

木箱にゼロ2の基盤を押し付け、ネジ穴をペンでなぞり、プロクソンのテーブルドリルで2.5mmの穴をあけました。

ゼロ2をネジで止めます。

テンキーはUSB-Aとmicro-Bへの変換コネクタ経由でゼロ2に接続しているのですが、ケーブルが長すぎ、コネクタが邪魔すぎるので切ってしまいました。

テンキー側は赤黒白青、変換ケーブル側は赤黒白緑です。赤-赤、黒-黒、白-白、青-緑と繋げれば良いのかなと、試しに繋いでみましたが動きません。テンキーのLEDすら点きません。他の組み合わせの接続も試してみたのですが、うまくいきませんでした。
最後に思いついたのは、切り取ってしまったコネクタ同士を繋いで導通を測る、でした。

赤-黒、黒-緑、白-赤、青-白を繋げばよいことがわかりました。
変換コネクタの方が普通の色の使い方をしていないです。
テスト後はんだ付けして動作確認しましたが、うまく動きません。調べたところ、キャン★ドゥのSDカードが壊れていることがわかりました。不良品と言うより、接続を試しているときにショートさせてしまい、それが原因で壊してしまった感じがします。
Amazon最安値32GBのものに交換して再セットアップし、動作確認しました。

テンキーで箱の蓋をして使おうということで、テンキーと電池のがたつき防止のために、紙粘土を使うことにしました。これもキャン★ドゥで購入。

ピンク色のケーブルは充電用です。

テンキー裏の針金状のスタンドは電気的に危なそうなので外しました。

テンキーを上に乗せると意外とイイ感じです。

輪ゴムでテンキーを押さえて完成です。

やっつけ感が凄いですが、運用にいろいろ懸念事項があるので当面これで使ってみます。

懸念事項
・電源スイッチはあった方がいいのでは?⇒必要。
・電池の持ち、充電間隔など⇒24時間くらいしか持たない。充電時間は6時間ほど。
・蓋開けが必要となる頻度⇒充電中はバッテリのインジケーターが見たい。

(2022/8/8追記)
電源をブチっと切るとSDカードが壊れて起動できなくなる可能性があることがわかりました。
LegacyのBuster版では対策されているようですが、Bullseye版でもそれらしいメニューがあるのでSDを書き込み禁止にし、一応の電源ぶつ切り対策をしてみました。

あらかじめ、/home/pi/にあるtoken.txtを削除します。
sudo raspi-config⏎
[Performance Option]-[Overlay File System]を選択し、Would you like the overlay file system to be enabled?をにします。
再起動すると、SDは書き込み禁止になっています。
token.txtは作成されますがRAM上にあり、再起動すると元のファイルがない状態に戻っています。

スイッチでAlexa定型アクションを起動~人感センサー編~

キーボード入力から定型アクションが起動できたので、GPIO入力からも当然同じことができる筈です。防犯カメラの時に試しに買っていた人感センサがあるのでそれで試してみました。

以下のページを参照しました。

【Raspberry Pi】自作人感センサーの使い方と活用法
https://chasuke.com/motionsensor/

メスーメスのケーブルを持ってなかったのでブレッドボードを使いました。

ページに掲載したプログラムを動かすとちゃんと動きました。
alexa_trigger.pyは関数化して同じプログラム内に入れ込みました。問題なく動作しました。
以下、プログラムです。

import sys
import os
import json
import datetime
from trace import Trace
import requests
from datetime import datetime
import time
import RPi.GPIO as GPIO

def detect_sensor( no ):
  client_ID = ''
  client_secret = ''
  refresh_token=''
  sensor_name = 'dummy_sensor_' + format(no, '0>3')
  token_file = './token.txt'
  today = 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.fromtimestamp(f_time)
    expiring_date = 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)


# インターバル
INTERVAL = 3
# スリープタイム
SLEEPTIME = 20
# 使用するGPIO
GPIO_PIN = 18

GPIO.setmode(GPIO.BCM)
GPIO.setup(GPIO_PIN, GPIO.IN)

if __name__ == '__main__':
    try:
        print ("処理キャンセル:CTRL+C")
        cnt = 1
        while True:
            # センサー感知
            if(GPIO.input(GPIO_PIN) == GPIO.HIGH):
                print(datetime.now().strftime('%Y/%m/%d %H:%M:%S') +
                ":" + str("{0:05d}".format(cnt)) + "回目の人感知")
                detect_sensor( '0' )
                cnt = cnt + 1
                time.sleep(SLEEPTIME)
            else:
                print(GPIO.input(GPIO_PIN))
                time.sleep(INTERVAL)
    except KeyboardInterrupt:
        print("終了処理中...")
    finally:
        GPIO.cleanup()
        print("GPIO clean完了")

タクトスイッチ等を使っても当然できることでしょう。pico Wの発売が待たれます。

スイッチでAlexa定型アクションを起動~ラズベリーパイ編~

ラズベリーパイをセットアップします。

Step1:OSのインストール

ラズベリーパイのホームページ(https://www.raspberrypi.com/software/)からimagerをダウンロードします。imagerはOSのバージョンが固定されているようなのでOSがバージョンアップされるとimagerもダウンロードしなければならないようです。v.1.7.2で説明します。
imagerを起動し、OSを書き込みます。
CLI版を使用するのでOSは[Raspberry Pi OS(other)]-[Raspberry Pi OS Lite(32bit)]を選択します。
書き込みが終わったらSDをラズベリーパイに差し替え、電源を入れます。
キーボードの設定画面(Configuring Keyboard-Configuration)が現れます。日本語の普通のキーボードの場合、[Other]-[Japanese]-[Japanese – Japanese(OADG 109A)]を選択します。
ユーザー名とパスワード入力します。
コマンドプロンプトが出来たら設定を行います。
$ sudo raspi-config⏎

wifi設定:[1 System Options]-[S1 Wireless LAN]-[JP Japan]-[SSIDの入力]-[パスワードの入力]
SSH設定:[3 Interface Options]-[I2 SSH]-[YES]
ファイルシステムの拡張:[6 Advanced Options]-[A1 Expand Filesystem]

最新バージョンへアップデートします。
$ sudo apt update⏎
$ sudo apt upgrade⏎ Y/nでY

Step2:ファイルの転送

スキルテスト編で作成したpythonプログラム(alexa_trigger.py)とシェルスクリプトをラズベリーパイのホームディレクトリに転送します。
シェルスクリプトは全く単純です。
ファイル名はalexa_key.shとしました。

#!/usr/bin/bash

while :
do
  read CMD
  case "$CMD" in
  "0") python alexa_trigger.py 0;;
  "1") python alexa_trigger.py 1;;
  "2") python alexa_trigger.py 2;;
  "3") python alexa_trigger.py 3;;
  "4") python alexa_trigger.py 4;;
  "5") python alexa_trigger.py 5;;
  "6") python alexa_trigger.py 6;;
  "7") python alexa_trigger.py 7;;
  "8") python alexa_trigger.py 8;;
  "9") python alexa_trigger.py 9;;
esac
done

シェルスクリプトに実行権限を与えます。
$ chmod 777 alexa_key.sh
試しにテストしてみます。
$ ./alexa_key.sh
0⏎
success

成功したら、起動時にシェルスクリプトが起動されるようにします。
ホームディレクトリにある.profileファイルを編集します。
$ nano .profile⏎
最後の行に1行追加します。
./alexa_key.sh

Ctrl + O⏎
Ctrl + W⏎
で保存します。

キーボードをテンキーに付け替え、再起動します。
これで完了です。

定型アクションを作成すれば10個のアクションがキーで動作します。
Lambda関数とシェルスクリプトに少し修正を加えれば動作するアクションを増やすことができるはずです。

.profileに変更を加えたことでSSH接続時にもシェルスクリプトalexa_key.shが実行された状態になっています。まず最初にCtrl+C⏎でシェルスクリプトを終了させる必要があります。

スイッチで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以外のサイトでは記事が少ないように感じました。
次は具体的な方法を記載します。殆ど「あんスマ」の手順とプログラムをなぞっただけです。記事が消えてしまうかもしれないので、忘備録として書きます。このサイトの方が寿命短いと思いますが。