ラズベリーパイの排熱を改善2

ケースの上に穴をあけ、更にヒートシンクを取り付けてもはっきりと温度が下がらなかったラズベリーパイ3ですが、ファンを取り付けてみました。

100均で買った下敷きを切ってそれに取り付けてみました。

これにより、5℃くらい下がりました。ファンの電源を5Vから撮りたかったのですが、LCDが使っていて開いていなかったので、3.3Vからとりました。

対処後の温度グラフは以下のようになりました。対処したのはPi3 Work 緑の線になります。

これを対処したことによって、Low voltage warning「低電圧の警告」が頻繁に表示されるようになってしまいまいました。現在使っているのは5V3AのACアダプタなのですが、LCDとファンも動かすようになったことで容量不足になったのかもしれません。

居間のラズベリーパイ4ですが、ファンの電源を5Vから取っていたのですが、テレビを点けていないときにファンの音がうるさいことに気が付きました。ファンが大きくて冷却効果も高いのか30℃半ばをキープしていたので3.3Vで常時冷やすことにしました。音も気にならなくなり、温度も40℃を割る位でキープしているようです。

現在の課題
1)Pi3 Workの電源容量不足問題
2)常時稼働ラズパイのファンレス化(庭カメラ用ラズパイの直射日光問題含む)

ラズベリーパイの排熱を改善

各ラズベリーパイのCPU温度グラフを見るようになって、庭を撮影しているラズベリーパイの温度が高いのが気になってきました。日が射すと70度を超えます。窓際に置いているため、ケースに直接日光が当たるためだと思い、画用紙で日よけを作ってみました。いくらか効果あるでしょうか?

対策を施したその日からめっきり秋めいてきたので、明確な比較はできませんが70℃を超えることはなくなりました。ある程度の効果はあったと言えるでしょう。

100均で買った箱をケースにしているラズベリーパイですが、60℃超になっていることが多く、ケース上部が暖かくなっているので穴をあけて排熱を良くしました。

結果は上部からの熱の抜けは良くなったような気はするものの、明確に数字に表れたような感じがしません。敢えて言うと1℃くらい低くなった感じです。それではと思い、ヒートシンクを購入して貼り付けてみました。結果は若干効果あり、60℃を超えることが少なくなりました。それでも2~3℃低くなって59℃台です。

50℃をちょっと超えた温度だったPi Zero2は試しにテンキーを取ってみるとそれだけで5℃下がりました。やはり排熱性が悪かったのでしょう。

CPU温度を測ろうとしたのはファンを温度によって回す設定を発見したところから始まったのですが、常時回すようにしたらどうなるのかと思い、ファンの電源をGPIOの5Vから常時給電するように再セッティングしてみました。

すると一気に下がりました。居間に置いたものは35℃位まで下がりました。庭のラズベリーパイはそれより7℃位高いですが、日除けのせいで排熱性が悪くなっているのでしょう。Pi4 Workは3.3Vから給電してみましたが、5Vで給電しているPiよりも7℃位高いです。Youtubeの動画を見ると50℃以上まで温度が上昇します。

上記の処置により、結果としてヒートシンクだけで冷却しているPi3が他を圧倒して熱いという状況になってしまいました。排熱性が悪くて5℃高くなっているPi Zero2も気になります。

各ラズベリーパイの部屋の温度と部屋の温度を計測しグラフに表示

ラズベリーパイ4は他のシリーズに比べ、CPU性能も高く発熱量も多いのでCPUファン付きのケースに入れていました。ファンの電源はGPIOの5Vに接続し、常時回していました。あるとき、ラズベリーパイの設定にCPUが何度になったらファンを回す、という設定があるのに気が付きました。試してみるとちゃんと動作しています。

CPUの温度で動作するという事は、今何度かわかる方法があるはずです。調べてみると以下のコマンドがありました。
$ sudo vcgencmd measure_temp
temp=57.4’C

気が付くと家にあるラズベリーパイは7台にもなっていました。
居間(Living):居間に防犯カメラのモニターとして常時稼働(Pi4:4GB)
玄関(Entrance):玄関の防犯カメラとして常時稼働(Pi3)
玄関2(Entrance-2):側面方向からみた玄関の防犯カメラとして常時稼働(PiZero)
実験用Pi3(Pi3 Work):実験用としているが、2階のカメラモニタとして半常時稼働 (Pi3)
実験用Pi4(Pi4 Work):実験用として必要時稼働 (Pi4:4GB)
テンキー用 (Alexa Key):テンキーによるAlexa定型アクション起動用として常時稼働 (PiZero2)
庭(Garden):たまにやってくる動物を捉えようと設置したカメラ用。常時稼働(Pi4:2GB)

上記のラズベリーパイの全部のCPU温度を時系列でグラフにしてみたくなりました。居間のNature Remo miniにも室温を計測する機能がありました。室温もグラフに加えます。最近2階用にスマートリモコンEZCONを買ったのですが、これには室温計測機能が無いようです。本当はNature Remoを買い増そうかと思ったのですが、去年買った時よりずいぶん高くなっていたので止めて安いものを買ったのです。

毎時0分と30分の30分おきにデータを収集することにしました。プログラムは以下のようになります。
data_collect.py

from paramiko import SSHClient, AutoAddPolicy, SSHException, AuthenticationException, ssh_exception
import socket
import re
from urllib.parse import urlencode
from urllib.request import urlopen, Request
from urllib.error import HTTPError
from json import loads
import os
from datetime import datetime
import schedule
import time

PI_NAME = ['Living', 'Entrance', 'Entrance-2', 'Pi3 Work', 'Pi4 Work', 'Alexa Key', 'Garden', 'Room']
PI_ADR = ['192.168.xx.xx', '192.168.xx.xx', '192.168.xx.xx', '192.168.xx.xx', '192.168.xx.xx', '192.168.xx.xx', '192.168.xx.xx']
PORT = 22
USER = 'pi'
PASSWORD = 'xxx'
TIMEOUT = 5
CMD_CPU = 'sudo vcgencmd measure_temp'

api_key = ""
url = "https://api.nature.global/1/devices/"
headers = {
    "accept" :"application/json",
    "Authorization" :"Bearer " + api_key,
}

DATA_FILE = '/var/www/html/temperature.csv'
ERROR_FILE = './errorlog.txt'
DATA_NUM = 96

def savedata(data):
  list = []
  if os.path.exists(DATA_FILE):
    f = open( DATA_FILE, 'r' )
    for line in f.readlines():
      list.append(line)
    else:
      f.close()
  start = 1
  num = len(list)
  if num >= (DATA_NUM + 1):
    start = num - DATA_NUM + 1
  f = open( DATA_FILE, 'w' )
  header = 'time,'
  for i in range(len(PI_NAME)):
    if i != 0:
      header += ','
    header += PI_NAME[i]
  f.write(header + '\n')
  for i in range(start, num):
    f.write( list[i] )
  f.write( datetime.now().strftime('%Y/%m/%d %H:%M,') + data + '\n' )
  f.close()

def errorlog(log):
  f = open(ERROR_FILE, 'a')
  f.write( datetime.now().strftime('%Y/%m/%d %H:%M:%S:') + log + '\n' )
  f.close()

def cpu_temp():
  ret = ''
  ssh = SSHClient()
  for name, adr in zip(PI_NAME, PI_ADR):
    try:
      ssh.set_missing_host_key_policy(AutoAddPolicy())
      ssh.connect(adr, PORT, USER, PASSWORD, timeout=TIMEOUT)
      stdin, stdout, stderr = ssh.exec_command(CMD_CPU)
    except AuthenticationException as e:
      errorlog(name + ':AuthenticationException:' + e)
      ret += ','
    except SSHException as e:
      errorlog(name + ':SSHException:' + e)
      ret += ','
    except ssh_exception.NoValidConnectionsError as e:
      ret += ','
    except socket.timeout:
      ret += ','
    else:
      temp = re.sub('\'C\n', '', re.sub('temp=', '', stdout.readline()))
      ret += str(temp) + ','
    finally:
      ssh.close()
  return ret

def room_temp():
  ret = ''
  request = Request(url, headers=headers)
  try:
    with urlopen(request) as response:
      data_byte = response.read()
      data= loads(data_byte)
      device_info = data[0]["newest_events"]
      ret = str(device_info["te"]["val"])
  except HTTPError as e:
      errorlog('Room:HTTPError:' + e)
      ret = ''
  finally:
      response.close()
  return ret

def correct():
  data1 = cpu_temp()
  data2 = room_temp()
  savedata(data1 + data2)

# every 30minute
schedule.every().day.at('00:00').do(correct)
schedule.every().day.at('00:30').do(correct)
schedule.every().day.at('01:00').do(correct)
schedule.every().day.at('01:30').do(correct)
schedule.every().day.at('02:00').do(correct)
schedule.every().day.at('02:30').do(correct)
schedule.every().day.at('03:00').do(correct)
schedule.every().day.at('03:30').do(correct)
schedule.every().day.at('04:00').do(correct)
schedule.every().day.at('04:30').do(correct)
schedule.every().day.at('05:00').do(correct)
schedule.every().day.at('05:30').do(correct)
schedule.every().day.at('06:00').do(correct)
schedule.every().day.at('06:30').do(correct)
schedule.every().day.at('07:00').do(correct)
schedule.every().day.at('07:30').do(correct)
schedule.every().day.at('08:00').do(correct)
schedule.every().day.at('08:30').do(correct)
schedule.every().day.at('09:00').do(correct)
schedule.every().day.at('09:30').do(correct)
schedule.every().day.at('10:00').do(correct)
schedule.every().day.at('10:30').do(correct)
schedule.every().day.at('11:00').do(correct)
schedule.every().day.at('11:30').do(correct)
schedule.every().day.at('12:00').do(correct)
schedule.every().day.at('12:30').do(correct)
schedule.every().day.at('13:00').do(correct)
schedule.every().day.at('13:30').do(correct)
schedule.every().day.at('14:00').do(correct)
schedule.every().day.at('14:30').do(correct)
schedule.every().day.at('15:00').do(correct)
schedule.every().day.at('15:30').do(correct)
schedule.every().day.at('16:00').do(correct)
schedule.every().day.at('16:30').do(correct)
schedule.every().day.at('17:00').do(correct)
schedule.every().day.at('17:30').do(correct)
schedule.every().day.at('18:00').do(correct)
schedule.every().day.at('18:30').do(correct)
schedule.every().day.at('19:00').do(correct)
schedule.every().day.at('19:30').do(correct)
schedule.every().day.at('20:00').do(correct)
schedule.every().day.at('20:30').do(correct)
schedule.every().day.at('21:00').do(correct)
schedule.every().day.at('21:30').do(correct)
schedule.every().day.at('22:00').do(correct)
schedule.every().day.at('22:30').do(correct)
schedule.every().day.at('23:00').do(correct)
schedule.every().day.at('23:30').do(correct)

while True:
  schedule.run_pending()
  time.sleep(10)

このプログラムを動かすにはライブラリparamikoとurllib、scheduleのインストールが必要になります。インストールは以下のように行います。
$ pip install paramiko⏎
$ pip install urllib3⏎
$ pip install schedule⏎

プログラムの説明
PI_NAME =で温度計測対象の名称を配列で設定しています。
PI_ADR =は各ラズベリーパイのアドレスを配列で定義しています。
PORT =はSSH接続するときのポート番号です。
USER =はSSH接続するときのユーザー名、PASSWORD =はパスワードです。
TIMEOUT =はSSH接続する際のタイムアウト時間となります。全てのラズベリーパイが常時稼働しているわけではないため、応答がなければ早めに非稼働中とします。
api_key =はNature Remoのアクセストークンです。Nature APIを使用するには必要で、以下のサイトで取得します。
https://home.nature.global/
url =はNature APIのURL、headersはNature APIに必要なヘッダー情報となります。
DATA_FILE =はこのプログラムのOUTPUファイルとなります。HTMLでグラフ表示するため、/var/www/html/ディレクトリにCSV形式で保存します。
ERROR_FILE =はエラーが発生した時に内容を出力するファイルになります。
DATA_NUM=は保持する最大のデータ数になります。30分おきに1日48データ収集しますので、2日分のデータを保持する設定になっています。

プログラムは最初起動時にスケジューラで起動する時刻を設定します。
schedule.every().day.at(‘XX:XX’).do(correct)
48行書くのは野暮ったいですね。流石ヘボプログラマーです。
while文で無限ループし、10秒おきに起動判定を行います。

関数correct()では最初に各ラズベリーパイの温度を収集し、次に部屋の温度を収集し、最後にデータを保存します。

各ラズベリーパイの温度を収集する関数cpu_temp()ではSSH接続し、温度を取得するコマンドによりデータを取得しています。取得した文字列は「temp=57.4’C⏎」というフォーマットなので数字だけを取り出すために[temp=[と[‘C⏎]を取り除いています。データをCSVファイルで保存するために,(カンマ)で区切っています。timeout以外の予期せぬエラー(例外)はエラーログファイルに内容を出力しています。(この例外処理は発生したことがないので正常に動作するのか試したことはありません・・・)

関数room_temp()ではNature APIを使いデバイス情報をJSONフォーマットで取得します。{“newest_events”:{“te”:{“val”:}}}に格納されている部屋の温度を取得します。(参照:https://swagger.nature.global/#/default/get_1_devices)

関数savedata()では引数に与えられたデータに時刻を追加してCSVファイルに保存しています。直近96データより古いものは破棄しています。

プログラムが出力するcsvは以下のようになっています。
temperature.csv

time,Living,Entrance,Entrance-2,Pi3 Work,Pi4 Work,Alexa Key,Garden,Room⏎
2022/08/24 11:30,52.1,45.1,50.8,62.3,,53.2,72.5,26.5⏎
2022/08/24 12:00,54.5,47.8,,65.5,,53.2,73.0,27⏎
2022/08/24 12:30,56.9,45.1,,65.0,,53.7,70.1,28⏎
2022/08/24 13:00,55.0,46.2,48.7,62.3,,53.7,68.1,28.5⏎
2022/08/24 13:30,51.6,45.1,51.4,62.8,,53.2,67.2,28⏎
・・・後略・・・

プログラムは居間のラズベリーパイに稼働させることにしました。
apache2をインストールします。
$sudo apt install apache2

ラズベリーパイ起動時にプログラムを開始するようにします。
/etc/rc.localのexit 0より上に以下の1行を挿入し、start.shシェルスクリプトをバックグラウンドで起動します。
sudo sh /home/pi/start.sh &

シェルスクリプトstart.shは以下のようになっています。

#!/usr/bin/bash
sleep 10
cd /home/pi/
python ./data_collect.py

最初に10秒sleepしているのは、これがないとプログラムがエラーとなるためです。どうやらこのプログラムが起動している段階ではネットワーク系のサービスがまだ起動していないらしく、それを避けるために簡易的に10秒間待っています。

データを格納するcsvファイルが作成されるようになりました。
それをグラフ表示するhtmlは以下のようになります。グラフはGoogle Chartを使用しています。
/var/www/html/temperature.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>Temperature Graph</title>
  <link rel="stylesheet" href="css/style.css">
  <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
  <script type='text/javascript'>
    google.charts.load('current', {packages: ['corechart', 'bar']});
    google.charts.setOnLoadCallback(drawCurveTypes);

    function drawCurveTypes() {
      var req = new XMLHttpRequest();
      req.open('get', 'temperature.csv', true);
      req.send(null);
      req.onload = function() {
        var ma = convertCSVtoArray( req.responseText );
        var data = new google.visualization.arrayToDataTable( ma );
        var options = {
          focusTarget: 'category',
          pointSize: 3,
          pointShape: 'circle',
          chartArea:{left:'5%',width:'75%',height:'85%',top:'5%'},
          backgroundColor: '#C0C0C0'
        };
        var chart = new google.visualization.LineChart(document.getElementById('chart_div'));
        chart.draw(data, options);
      }
    }

    function convertCSVtoArray(str) {
      var result = [];
      tmp = str.split('\n')
      for( var i = 0; i < tmp.length; ++i ) {
        if( tmp[i] == '' ) continue;
        result[i] = tmp[i].split(',');
        if( i != 0) {
          result[i][0] = new Date(result[i][0]);
          for( var j = 1; j < result[i].length; j++) {
            if( result[i][j].length != 0 ) {
              result[i][j] = Number(result[i][j]);
            } else {
              result[i][j] = NaN;
            }
          }
        }
      }
      return result;
    }
  </script>
</head>
<body>
  <p>温度データ</p>
  <div id='chart_div' class='graph-temperature'></div>
 </body>
</html>

/var/www/html/css/style.cssには以下の4行を追加します。ファイルがない場合は追加します。グラフの横幅はブラウザの横幅を目一杯使うようにしています。縦の540ピクセルはラズパイのモニタの設定ではみ出さないで表示で来る最大の大きさを狙って設定しています。

.graph-temperature{
  width: 100%;
  height: 540px;
}

表示されるページは以下のようなイメージになります。グラフ内にマウスポインタを当てると指定したポイントの値を表示します。

庭を写すカメラを制御しているGardenは窓際に置いているのですが、日が射すと温度が70℃以上になっているのがわかります。曇りの日はそれほど高くならないようです。
2階のモニタ用ラズパイはPi3でタッチパネルと一体にセットされて、100均で買ったプラスティックケースに入れていますが、ファンもヒートシンクも付けていないため、60°をちょっと超えてしまっています。

居間のテレビの裏に置いてあるLivingは時々60℃を超えてはファンが回って冷えている感じなのでしょうか。
PiZero2のAlexa Keyは熱対策なしの上に、テンキーで蓋をされているようになっているためか、はたまたZeroよりCPU能力が高く発熱量が多いためか50℃を超えたところで安定しています。
玄関フード内に取り付けたEntrance-2はラズパイZeroですが、これも熱対策は何もしていません。常に気温より20℃くらい高い温度です。
玄関の内側に取り付けたEntranceはPi3ですが、これは全体がヒートシンクになっているというアルミケースを使用しています。常に40℃前半を維持していて素晴らしい冷却能力を示しています。
部屋は暑くなってきたらエアコンを点けているのがわかります。もう秋なのか夜は25℃以下になることもあるようです。

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

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で自分の持っているCDの曲を再生する

この度Apple musicに加入し、CDで持っていた曲をAmazon Echoから聴けるようになったので、それについて記載します。

1)経緯

Amazon EchoはAmazon Music等の音楽サブスクリプションから音楽を掛けることができますが、基本的に自分の持っているCDの曲を掛けることができません。曲が入ったUSBメモリをEchoに挿してそこに収録された曲を聞く、という事ができないのです。CDからmp3ファイルに変換した曲を大量にもっているのですが、Echoはそれを再生する方法を提供していないのです。

私はライブで購入したCDを結構持っているのですが、世間一般には有名とは言えないミュージシャンのものが多いのです。Amazon Musicなどのサブスクにその曲があればUSBメモリは不要ですが、モアリズムなどの割と有名なミュージシャンでもほんの一部の曲しかサブスクには登録されておらず、とてもUSBメモリの代用をできる代物ではありません。

Amazonの会員ならば無料で利用できるPrimeでも200万もの曲があるらしいのですが、ちょっと物足りません。それならば、ということで、6,500万曲(今は9000万曲と書いてある)もあるというUnlimitedに加入してみたのですが、Primeとさほど変わらないように感じられ、自分にとっては目的を達せられませんでした。本当にAmazon Musicに数千万曲もありそれが聴けるのか、自分としては非常に疑わしいと感じます。いきなりファミリープラン1年分を一括購入したのは自分にとっては殆ど無駄だったとしか言いようがありません。

Alexaの動作にも不満があります。ミュージシャンの名前を言い、それが有名どころでなければ『そんなミュージシャンや曲はありません』、と返答して曲は再生しないという動作ならまだ許せるのですが、平然と似た名前の違う曲をかけるのが非常に腹立たしいです。はっきり言って殺意が沸きます。アマゾンからCDではなく、ファイルで購入した曲でも言う事を聞いてくれないことが多いです。また、自分のスマホは格安SIMを使っており高速で通信できる量はごく限られているので、普段は通信速度が遅い節約モードにしております。車を運転しているときにこの状態でAmazonから購入した曲を聴くと途切れ途切れにしか聴こえないものがあります。どうやら曲をダウンロードするスピードが再生に追い付いていないらしいのです。通常サブスクは通信スピードが遅い場合はビットレートが低いファイルを再生してくれるのでこのようなことが発生することはないのですが、こういう通信節約モードに対応している曲とそうでない曲があるらしいのです。

ファミリープランにしたのは同時に複数台のEchoから曲を聴けるようにするためで、ある時、高齢の家族がAlexaの機能に気が付き音楽を聴いていたので、今後も使うかもと予想しそのプランを決めたのですが、果たしてその後使っていたのかどうか疑わしい状態です。
というわけで殆ど1年の大半はアレクサ任せの曲しか聞かないという、殆ど無駄な使い方をしてしまいました。これでも無理して使った方で、金を払ってなければ全然使わなかったかもしれません。

というわけでUnlimitedの1年が過ぎ、プランを更新しなかったのですが、インターネットの某掲示板でApple Musicならばアップロードしたファイルを聴くことができるという情報を得たので試してみました。

これまでCDを取り込んでmp3ファイルにする際、256kbpsでリッピングしていたのですが、試しにそのままApple Musicにアップロードしてスマホの節約モードで聴いてみたところ、案の定途切れ途切れにしか再生されず、とても聴けるものではありません。ラジコを良く聞きますが、ラジコは音質が悪いと感じませんが音が途切れ途切れになりません。ラジコのビットレートを調べたところ48kbpsだということで、手持ちのファイルを48kbpsに変換する事にしました。

2) VideoProc Converter

音楽ファイルのビットレートを変換する無料のツールをネットで検索するとVideoProc Converterというソフトが見つかりました。早速ダウンロードし、あるアルバムの曲を48kbpsに変換してApple Musicにアップロードしたところ、うまく再生できることがわかりました。しかし、問題があります。

【フォルダ単位でしか変換できない】
複数フォルダのファイルを一気に変換できるようですが、一つのフォルダにしか出力されないので、一気に処理すると元の「アーティスト/アルバム/各楽曲」というフォルダ構成に戻すのが大変です。1アルバム毎に処理するしかないのですが、そうすると何百枚もあるCDを全部処理するのにとても時間と手間がかかります。

【曲名の文字を変えられてしまう】
mp3はファイル名が曲名になっていますが、曲名が「Don’t Slip」のように’(コーテーション)が使われていると_(アンダースコア)に変えられてしまいます。無料ソフトゆえの嫌がらせ機能なのでしょうか?

【参加アーティスト名がVideoProc Converterに置き換えられてしまう】
アップしたファイルの一覧を見てみると、アーティスト名がVideoProc Converterになっています。参加アーティストの情報が全部上書きされてしまっているのです。これはちょっといただけません。これも嫌がらせ機能で有料版を買えば解消されるとでもいうのでしょうか?
(2022/8/27追記:アップロードした曲をEchoで再生する場合は、曲名とアルバム名を表示しますが、アーティスト名は表示されません。Echoで再生する限りにおいては問題は無さそうです。)

上記の問題を考慮して別の方法を検討することにしました。

3)ffmpeg

調べた結果ffmpegというソフトを使うことにしました。
このソフトはコマンドラインで使うもので、マウスで操作するGUIソフトでないのでちょっと操作が面倒かもしれませんが、プログラムと組み合わせて使用する事にしました。

以下のページからダウンロードします。
FFmpeg
https://ffmpeg.org/

ページ上の[Donload]ボタン-[Get packages & executable files]からWindowsの青いマークをクリック

[Windows EXE Files]からwindows builds from gvan.devをクリック

gvan.devのページから[release builds]のlatest releaseからffmpeg-release-full.tzを選択。

ダウンロードしたファイルを右クリックして解凍します。
解凍したフォルダを永続的なフォルダ(例:c:\Soft)にコピーします。
次に、どこからでもffmpegコマンドが使えるようにbinフォルダにパスを通します。

【パスの通し方】
スタートボタンを押下し、ギヤのマークの[設定]を選択すると設定画面を表示します。

[システム]を選択、左側一番下の[詳細情報]を選択、右側下の方の[システムの詳細設定]を選択すると、[システムのプロパティ]画面が開きます。

下の方の[環境変数(N)]をクリックすると、[環境変数]画面が開きます。

環境変数のPathを選択(ユーザーの環境変数でも、システム環境変数どちらのPathでも良い)し、[編集]ボタンをクリックします。

環境変数名の編集]画面が開きます。[新規(N)]ボタンをクリックし、そこにbinフォルダのパスを入力します。(例: C:\Soft\ffmpeg-5.1-full_build\bin)
[OK]ボタンで閉じてゆき、設定完了です。

1ファイルを変換する際のコマンドは以下のページとffmpegのヘルプを参考にして決めました。
[FFmpeg] 48kbpsの低ビットレートで高音質を追求する
http://sogohiroaki.sblo.jp/article/187244377.html

ffmpeg -i 元ファイルパス -vn -acodec mp3 -ar 44100 -b:a 48k -ac 2 出力ファイルパス -y

試しに1曲変換してみましたが、成功しました。
元のファイルと出力ファイルを同じにして、置き換えるような処理は出来ませんでした。
ffmpegのオプションはいっぱいあり、習熟すると目的にとってもっと最適なオプション設定があるかもしれません。

音楽ファイルは256kbpsから48kbpsになったのですから理論上20%以下のサイズになっている筈ですが、実際のファイルサイズもそれくらいか、ちょっと小さめになっていました。

次はフォルダにまたがったファイル群を、フォルダ構成を保ったまま変換するプログラムを作ります。

3)pythonプログラム

pythonでディレクトリを再帰的に処理するようなサンプルプログラムをネットで探したところ、直ぐに見つかりました。
USBメモリをPCに挿し(Dドライブ)、そこにあるファイルパスを全て表示する以下のプログラムを動かしてみたところ、あっさり成功しました。
filelist.py

import os

def find_all_files(directory):
    for cur_dir, dirs, files in os.walk(directory):
        for file in files:
            yield os.path.join(cur_dir, file)

if __name__ == '__main__':
    for file in find_all_files('d:\\'):
        print(file)

これほど短いプログラムで実現できたのは驚きでした。
これを基に作成する変換プログラムの機能を以下のように決めました。
・USBメモリを2つPCに挿し、DドライブとEドライブにします。
・Dドライブに変換したい音楽ファイルをフォルダごとコピーします。
・Eドライブは空にしておきます。
・プログラムを実行すると、Dドライブと同じフォルダ構成がEドライブにもできて、その中に48kに変換されたファイルが格納されます。
・対応する音楽ファイルはmp3,mp4,wavの拡張子を持つファイルで、それらをビットレート48kのmp3ファイルに変換します。

出来たプログラムは以下のようになります。
cnv48k.py

import os
import subprocess

def find_all_files(directory):
    for cur_dir, dirs, files in os.walk(directory):
        for file in files:
            yield os.path.join(cur_dir, file)

if __name__ == '__main__':
    for file in find_all_files('d:\\'):
        path, fname = os.path.split(file)
        root, ext = os.path.splitext(file)
        if ext == '.mp3' or ext == '.mp4' or ext == '.wav':
            newpath = 'e' + path[1:]
            os.makedirs(newpath, exist_ok=True)
            newfile = os.path.join(newpath, fname)
            cmdline = 'ffmpeg -i \"' + file + '\" -vn -acodec mp3 -ar 44100 -b:a 48k -ac 2 \"' + newfile + '\" -y'
            subprocess.run(cmdline, shell=True)

プログラムの実行方法

>python cnv48.py
※pythonをまだインストールしていない場合は「Windowsパソコンにpythonをインストール」とかで検索するとpythonのインストール方法が沢山出てきますので参照してください。

人によっては挿したUSBドライブがDとEにならないかもしれないですが、その場合はプログラムの10行目と14行目にあるdとeの文字を変えて実行してください。
このプログラムはファイルのパスのdという最初の文字をeに置き換えることで処理するという、手抜きプログラムです。変換元と変換先のディレクトリを指定するというちゃんとしたプログラムを作りたい方は、多分簡単なのでご自分で挑戦していただきたいと思います。

変換したいフォルダを一気に処理すると時間がかかり、いつ終わるか分からなくなるので小分けに処理をしたい、という要望もあるかもしれません。その場合はまず、Dドライブに変換したいフォルダだけを入れて処理を実行し、処理が終わったらDドライブのフォルダを削除し、次に処理したいファイルをコピーして実行する、という風にしていけば、Eドライブにファイルが追加されていくので、よいかもしれません。
私はざっくり500ファイル(曲)分ずつのフォルダに分けて処理を行いました。私のPCだと1時間以内には処理が終わったように記憶しています。間違っていたらスイマセン。

4)iTuneにアップロード

AppleMusicが1か月お試しだったので、それを利用することにしました。Apple IDは何故か以前に登録済みだったようです。カード情報などを追加で登録したような気がしますが、ちょっと悪戦苦闘し、整理できていないのでその辺は割愛します。
iTuneアプリはMicrosoft Storeからインストールしました。

Apple Musicのアップロード機能が有効になっていることを確認します。メインメニューの[編集(E)]-[環境設定(F)]-[一般] タブで[iCloudミュージックライブラリ]のチェックがONになっていればOKです。

iTuneをインストールすると[ミュージック]フォルダにiTunesフォルダができていました。その下にiTunes Mediaフォルダがあります。
どの操作が正式な推奨操作かわかりませんが、自分の行った操作はこうです。
iTunes MediaフォルダにMusicフォルダを作り(もしかしたら既にあるかもしれません。テスト用にアップしたファイルを全部消した時にフォルダごと削除されるようです)、そこにEドライブのフォルダをコピーします。

コピーが終わったら、iTuneアプリのメインメニューから[ファイル(F)]メニュー-[フォルダをライブラリに追加(D)]を選択します。
そこでMusicフォルダにあるフォルダを選択します。
これで選択したフォルダの音楽ファイルがクラウドにアップロードされます。
アップロード状況を見るには、メインメニューの下のバーで[ミュージック]を選択し、[ライブラリ]を選択状態にし、左側で[曲]を選択すると追加したファイルの一覧が表示されます。

項目に[iCloudの状況]がない場合は、メインメニューから[表示(V)]-[表示オプションを表示(Ctrl+J)]を表示し、iCloudの状況にチェックを入れます。

iCloudの状況は最初[待機中]になっています。Apple Musicにアップロードが終わると[アップロード済み]となります。この表示は逐次表示されるのではなく、1回に指定したファイルが全部アップロードした段階で一斉に[アップロード済み]、という表示になるようです。
ですので、一度に大量のファイルをアップロードすると[アップロード済み]になるのにとても時間がかかります。それが嫌な場合は小分けに処理した方が良いかもしれません。

iCloudの状況が[マッチ]となるものがあります。これはApple Music内に同じ曲があるということなのでしょう。一般的に有名なアーティストはサブスクの方が、曲が充実していると考え、アップロードしないことにしました。(もしかしたら、同じ曲があるのでアップロードしてないよ、という意味なのかもしれない。)
(2022/8/27追記:マッチとなっている曲をよく見るとアルバムの全ての曲がマッチしているわけではないようです。アルバム中マッチとなっている曲が少ない場合があるかと思えば、1曲だけマッチでない曲もあります。マッチとなっている曲をAppe Musicの曲と置き換えようと考えましたが、いつ変わるかわからないように思えたので止めました。)

iCloudの状況が[拒否]となるものがあります。これはソニーやAmazonから購入した音楽ファイルがそうなっていました。(Amazonから購入した曲全てが拒否されたわけではないようです。)Apple Musicから登録しなおせという事なのでしょう。Apple Musicからアルバムをライブラリに追加しました。

iCloudの状況が[失敗]となるものがありました。これはいったん削除し、再度ファイルを追加すると、[アップロード済み]のステータスになりました。

5)プレイリスト全部

まず、やりたかったのはアップロードした曲全てを含むプレイリストです。全部で3000曲超のファイルを一気にアップロードしたのですが、これのプレイリストを作成するのが大変でした。操作としてはプレイリストにアルバムを次々にドラッグ&ドロップしていくだけなのですが、全部ドラッグしたつもりでもかなりの漏れがあったのです。
操作ミスしたのかiTuneがミスしたのか不明です。
プレイリストの曲数とアップロードしたファイル数が合わない。
プレイリストをファイル化できれば、機械的に比較することができるのですが、それができません。そうするとどれが漏れているのかわかり難いのです。
これを解消するためにファイルの一覧を作成しました。プレイリストを上から順にみていき、あることを確認できた曲をファイル一覧から消していきます。最後にファイル一覧に残ったファイルがプレイリストに追加し忘れたファイルということになります。

以下がファイル一覧を作るプログラムです。
filelist2.py

import os
import sys
import io

TOPDIR = "C:/Users/xxx/Music/iTunes/iTunes Media/Music"

def find_all_files(directory):
    for cur_dir, dirs, files in os.walk(directory):
        for file in files:
            yield os.path.join(cur_dir, file)

if __name__ == '__main__':
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding=sys.stdout.encoding, errors="replace")
    for file in find_all_files(TOPDIR):
        root, ext = os.path.splitext(file)
        if ext == '.mp3' or ext == '.mp4' or ext == '.wav':
            print(file)

TOPDIR=に一覧を作りたいフォルダがあるディレクトリをセットします。
以下のように実行します。

>python filelist2.py > filelist.txt

13行目のsys.stdout = の部分は文字コードによるエラー終了を回避するコードです。
この行なしに、> filelist.txt としてファイルに書き込もうとするとUnicodeError(Unicodeに対応する文字がない)になりプログラムが途中で終了してしまったのです。そういうときは以下の文字がファイル名に含まれていました。
Óや゛(濁点のみ)など

>filelistと書かずに>python filelist2.pyだけだとエラーになりません。
この13行目を加えることによって変換できない文字は?に変換され、エラーで途中終了することが無くなりました。

6)Alexaアプリ設定

Alexaアプリの設定で普段使用するサブスクをAmazon MusicからApple Musicに変更しようと思いましたが、思いとどまりました。
デフォルトのサブスクはAmazon musicのままでApple Musicを使用するときはAlexaに明示的に「Apple Musicで~をかけて」と言う事にしたのです。こうすることで、2台のEchoでAmazon MusicとApple Musicで同時に音楽を聴くことができ、実質2名迄のファミリープランとして使えると考えたからです。Alexa自身の機嫌を保つのにも役立っているかもしれません。
アップロードした曲のプレイリストも「全部」を始めいくつか作成しましたが、それぞれに対して定型アクションを作成し、「アレクサ、apple musicでプレイリスト、~をシャッフル再生して」と言い換えさせることにしました。
これにより、「アレクサ、WCカラス」と言うだけでApple Musicのプレイリスト「WCカラス」がシャッフル再生されるようになりました。

以上、駆け足での説明でしたが、このように設定した結果、Alexaはほんの短い言葉で意図した通りの音楽を再生するとても素晴らしい機械になりました。
これまで気が利かない機械だったechoが突然、意図した通りのことをやってのける相棒のようになったのです。驚きました。
車にはこれまではプレイリスト毎にUSBメモリを作って、差し替えて聞いていたのですが、これからはもうその必要がありません。CDを買うごとにUSBメモリをメンテナンスする必要もなくなりました。echo autoを買っていたのですが、突然輝いて見えるようになりました。(通信が使えないときのためにUSBメモリのメンテは時々行う予定です)
48kbpsにしたことで音質が多少悪くなるかと思いましたが、ほとんど感じ取ることができません。ラジコの音質も劣ると感じたことも今までありません。車の中は雑音だらけですし、耳自体も元々バカ耳なのでしょう。

明日の天気によって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⏎でシェルスクリプトを終了させる必要があります。