今更感がありますが、つい先日GoogleNestMiniを購入しました。購入に至った経緯は、アクリルオーナメントを音声で光らせたかったからです。明るさが就寝灯としてちょうどよさそうなので、暗い場所で音声の操作がしたくなってスマートスピーカーの購入に踏み切りました。
この記事では、スマートスピーカーの使い勝手を軽く述べた後不便さをラズパイで解消していく過程を(備忘録として)まとめます。
筆者の環境は以下の通りです
windows10
raspberrypi3B+ Raspbian, python3.7.3
##1. 使用感
###1.1. 便利な点
まずは単体で使ってみました。驚くべきは音声認識の精度の高さです。ウェイクワードの「ok google」や使用頻度が高いアラームや天気に関することは雑に言ってもちゃんと聞いてくれます。
当初の目的であったアクリルオーナメントの音声操作は、スマートプラグを使うことで簡単に解決しました。今後スマートリモコンを購入(or自作)してエアコンなんかも操作できるとより便利になりそうです。
あとgoogle homeアプリのルーティン機能が便利でした。複数のコマンドをまとめられるのでシェルスクリプト感があります。時報にもできるので重宝しています。
###1.2 不便と感じた点
不便に感じた点をいくつか述べます。期待してた機能が使えなくて悲しかったので口調が若干きつめになります...
####1.2.1 曲の再生がイマイチ
ストリーミングサービスをやってないと人権がありません。ローカルに保存した曲の再生は(おそらく)できないでしょう。ループ再生の機能もなさそうです。標準でyoutube musicが使えるようですが、プレミアム会員にならないと細かい指定ができないので使い物になりませんでした。
↓
1か月のお試しでプレミアム会員になりましたが、マイナーな曲しか聞かないせいかうまく行きませんでした。ループ再生もスピーカーにキャストすると使えなかったのでプレミアム会員はやめようと思います。
1.2.2 自発的に喋ってくれない
GoogleNest君はシャイなのか、こちらから話しかけないと喋ってくれません。LINEやSlackの通知が来たら喋ってくれると嬉しかったので残念です。IFTTTで簡単にできそうですが、私が探した限りはそういった項目はありませんでした。
1.2.3 「ok google」は長い、変更もできない
ウェイクワードが長いというのは致命的な欠点です。何度も言う言葉なのでできる限り短くしたいところですが、変更はできないようです。
1.2.4 アラームの音を自由に変更できない
1.2.1と同様に、ローカルのファイルを参照できないので好きな曲を設定できません(ストリーミングサービスにあるものは設定できます)。あとアラーム設定の度に曲名を言うのは少し面倒そうです。
##2.ラズパイで快適な音声操作を実現する
以上4点を解消するべくいろんなサービスとラズパイを組み合わせていきます。
この記事の最終目標は以下のような図を作ることです。
初めてNode-REDを使いましたがめっちゃ便利です。javascriptの経験もなかったのでとても助かりました。
。
この図はいくつかの流れに分解できます。一つずつは大したこと無いので実装したい機能だけ選んでもらえればいいと思います。
0,GoogleNestMiniで家電を操作する
1,SNSの通知に合わせてGoogleNestMiniを喋らせる
2,声で音楽の再生をコントロールする
3,GoogleNestMiniからラズパイで任意のコードを実行できるようにする
0は標準機能なので特に言及はしません。1から順に詳細を述べていきます。
2.0 準備:ラズパイの固定ip化
ラズパイにipアドレスでアクセスすることになるので今後のために固定ipにします。
①デフォルトゲートウェイとDNSサーバーのipを控える
windowsで
ipconfig /all
とやればわかると思います。
Wireless LAN adapter Wi-Fi:のデフォルトゲートウェイとDNSサーバーの欄です。
私はどちらとも192.168.0.1でした。この値をメモしておきます。
②ラズパイの設定をいじる
次にラズパイで次のコードを実行します
sudo nano /etc/dhcpcd.conf
いろいろ出てきますが、最下段に以下を追記します。
3,4行目が先ほどメモした値になります。2行目は固定したいipアドレスで24はサブネットマスク長なので特にいじる必要はありません。
interface wlan0
static ip_address=192.168.0.20/24
static routers=192.168.0.1
static domain_name_servers=192.168.0.1
これで再起動すれば指定したipアドレスになっているはずです。
ipを固定しておくとssh接続の際も楽なのでオススメです。
2.1 SNSの通知に合わせてGoogleNestMiniを喋らせる
2.1.1 GoogleNestMiniにこんにちはと言ってもらう
まずGoogleNestMiniが任意のワードを喋れるようにします。
ラズパイでNode-REDを起動します。
node-red-pi
もしインストールしていない場合は
sudo apt-get install nodered
でインストールしてください。
PCなどのブラウザでhttp://192.168.0.20:1880とすれば開けます。
各自自分のラズパイのipアドレスに変更してください。
開くとこんな感じの画面になります(画面中央のフローはないはずです)。
Node-REDからchromecastを利用します。右上の横三本線のパレットの管理からノードの追加で「node-red-contrib-cast」と検索して出てきたものを追加します。
その後、同様に右上から読み込み→クリップボードと進み、以下のjsonファイルを貼り付けてください。
[{"id":"a64170bf.9480a","type":"tab","label":"chromecast","disabled":false,"info":""},{"id":"3ba88805.d91f18","type":"cast-to-client","z":"a64170bf.9480a","name":"","url":"","contentType":"","message":"","language":"ja","ip":"192.168.0.9","port":"","volume":"","x":730,"y":320,"wires":[[]]},{"id":"4f29a341.5aeecc","type":"inject","z":"a64170bf.9480a","name":"","topic":"","payload":"こんにちは","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":220,"y":240,"wires":[["d61a70cd.bcf2a"]]},{"id":"adc062fb.88ef2","type":"http in","z":"a64170bf.9480a","name":"テキストの読み上げ","url":"/cast_text","method":"post","upload":false,"swaggerDoc":"","x":230,"y":320,"wires":[["4ce77c2a.2b0a54","9b505044.bc258","3ba88805.d91f18"]]},{"id":"f59cca2b.2f7078","type":"http response","z":"a64170bf.9480a","name":"","statusCode":"","headers":{},"x":550,"y":380,"wires":[]},{"id":"4ce77c2a.2b0a54","type":"change","z":"a64170bf.9480a","name":"応答","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"result\":\"OK\"}","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":380,"wires":[["f59cca2b.2f7078"]]},{"id":"9b505044.bc258","type":"debug","z":"a64170bf.9480a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":450,"y":440,"wires":[]},{"id":"d61a70cd.bcf2a","type":"change","z":"a64170bf.9480a","name":"テキストの入力","rules":[{"t":"set","p":"message","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":460,"y":240,"wires":[["3ba88805.d91f18"]]},{"id":"755c7c0.fde7c84","type":"http in","z":"a64170bf.9480a","name":"音楽の再生","url":"/cast_music","method":"post","upload":false,"swaggerDoc":"","x":200,"y":640,"wires":[["cb3a313a.c391","a9a7f8af.d5e798"]]},{"id":"cb3a313a.c391","type":"change","z":"a64170bf.9480a","name":"応答","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"result\":\"OK\"}","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":580,"wires":[["3522715f.2c47ae"]]},{"id":"3522715f.2c47ae","type":"http response","z":"a64170bf.9480a","name":"","statusCode":"","headers":{},"x":550,"y":580,"wires":[]},{"id":"a9a7f8af.d5e798","type":"change","z":"a64170bf.9480a","name":"flow変数の設定","rules":[{"t":"set","p":"loop","pt":"flow","to":"10","tot":"num"},{"t":"set","p":"url","pt":"flow","to":"payload.url","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":460,"y":640,"wires":[["258bb7ab.957068"]]},{"id":"d56cf773.69a6b8","type":"switch","z":"a64170bf.9480a","name":"ループ再生","property":"loop","propertyType":"flow","rules":[{"t":"lte","v":"0","vt":"num"},{"t":"gt","v":"","vt":"num"}],"checkall":"true","repair":false,"outputs":2,"x":850,"y":640,"wires":[["534baf17.af58c"],["ff0d120a.29dfe"]]},{"id":"11e3a8cf.c1c607","type":"cast-to-client","z":"a64170bf.9480a","name":"","url":"http://192.168.0.20:8000","contentType":"audio/basic","message":"","language":"en","ip":"192.168.0.21","port":"","volume":"","x":610,"y":1000,"wires":[["a5c36293.7beec"]]},{"id":"c16a36ec.d6b328","type":"http in","z":"a64170bf.9480a","name":"再生の停止","url":"stop_music","method":"get","upload":false,"swaggerDoc":"","x":220,"y":1000,"wires":[["11e3a8cf.c1c607","e4f97815.068388"]]},{"id":"452277dd.7cee58","type":"inject","z":"a64170bf.9480a","name":"","topic":"","payload":"0","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":230,"y":1060,"wires":[["11e3a8cf.c1c607"]]},{"id":"a5c36293.7beec","type":"change","z":"a64170bf.9480a","name":"ループ解除","rules":[{"t":"set","p":"playing","pt":"flow","to":"false","tot":"bool"},{"t":"set","p":"loop","pt":"flow","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":1000,"wires":[[]]},{"id":"f0307438.6b9158","type":"function","z":"a64170bf.9480a","name":"flow変数の設定","func":"if (msg.payload.playerState == \"PLAYING\"){\n flow.set(\"playing\",true);\n flow.set(\"loop\", flow.get(\"loop\")-1)\n return msg;\n}","outputs":1,"noerr":0,"x":460,"y":740,"wires":[["bbaf9994.87b718"]]},{"id":"bbaf9994.87b718","type":"switch","z":"a64170bf.9480a","name":"再生終了まで待機","property":"playing","propertyType":"flow","rules":[{"t":"false"},{"t":"true"}],"checkall":"true","repair":false,"outputs":2,"x":670,"y":740,"wires":[["ac2f0698.7ef7f8"],["8f6afb8a.5afc88"]]},{"id":"8f6afb8a.5afc88","type":"delay","z":"a64170bf.9480a","name":"","pauseType":"delay","timeout":"1000","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":560,"y":820,"wires":[["74d83080.c5b98"]]},{"id":"74d83080.c5b98","type":"function","z":"a64170bf.9480a","name":"再生終了の判定","func":"msg.payload.media.duration -= 1;\nif (msg.payload.media.duration < 0){\n flow.set(\"playing\",false);\n}\nreturn msg;","outputs":1,"noerr":0,"x":740,"y":820,"wires":[["bbaf9994.87b718"]]},{"id":"ff0d120a.29dfe","type":"cast-to-client","z":"a64170bf.9480a","name":"","url":"","contentType":"audio/flac","message":"","language":"en","ip":"192.168.0.21","port":"","volume":"15","x":1070,"y":640,"wires":[["f0307438.6b9158"]]},{"id":"ac2f0698.7ef7f8","type":"change","z":"a64170bf.9480a","name":"値のリセット","rules":[{"t":"delete","p":"payload","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":880,"y":740,"wires":[["258bb7ab.957068"]]},{"id":"534baf17.af58c","type":"debug","z":"a64170bf.9480a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1050,"y":580,"wires":[]},{"id":"6e31ddc.00c6f24","type":"http in","z":"a64170bf.9480a","name":"音量の調整","url":"/change_volume","method":"post","upload":false,"swaggerDoc":"","x":220,"y":1120,"wires":[["3cfd4823.78dca8","e4f97815.068388"]]},{"id":"3cfd4823.78dca8","type":"cast-to-client","z":"a64170bf.9480a","name":"","url":"","contentType":"","message":"","language":"en","ip":"192.168.0.21","port":"","volume":"","x":610,"y":1120,"wires":[[]]},{"id":"6c969711.6fbb38","type":"inject","z":"a64170bf.9480a","name":"","topic":"","payload":"10","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":230,"y":1180,"wires":[["491f6be2.a9e894"]]},{"id":"258bb7ab.957068","type":"change","z":"a64170bf.9480a","name":"URLの設定","rules":[{"t":"set","p":"url","pt":"msg","to":"url","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":670,"y":640,"wires":[["d56cf773.69a6b8"]]},{"id":"e4f97815.068388","type":"change","z":"a64170bf.9480a","name":"応答","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"result\":\"OK\"}","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":570,"y":940,"wires":[["437a2c8d.73fa74"]]},{"id":"437a2c8d.73fa74","type":"http response","z":"a64170bf.9480a","name":"","statusCode":"","headers":{},"x":730,"y":940,"wires":[]},{"id":"23db5c54.392524","type":"catch","z":"a64170bf.9480a","name":"停止時のエラーをキャッチ","scope":null,"uncaught":false,"x":630,"y":1060,"wires":[["25f4883b.eff458"]]},{"id":"25f4883b.eff458","type":"debug","z":"a64170bf.9480a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":830,"y":1060,"wires":[]},{"id":"491f6be2.a9e894","type":"change","z":"a64170bf.9480a","name":"音量の入力","rules":[{"t":"set","p":"volume","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":370,"y":1180,"wires":[["3cfd4823.78dca8"]]}]
そうすると上の画像のようなフローが読み込まれます。下の部分は音楽の再生に使用します。
GoogleNestのipアドレスをスマートフォンのGoogleHomeアプリなどから確認し、Castの部分のipアドレスを書き換えます。(画像の192.168.0.9の部分です)
これでフロー左上の青いボタンを押すと喋ってくれるはずです。
2.1.2 任意のワードを喋らせる
「テキストの読み上げ」と書かれたノードはPOSTメソッドに反応しメッセージを流します。つまり、pythonなどでリクエストを送ればGoogleNestは任意のワードを喋ってくれます。
import json
import urllib.request, urllib.parse
def send_text_to_GoogleNest(text, ip="192.168.0.20"):
url = f"http://{ip}:1880/cast_text"
data = {"message":text}
headers = {"Content-Type": "application/json"}
req = urllib.request.Request(url, json.dumps(data).encode(), headers)
with urllib.request.urlopen(req) as res:
bo = res.read()
2.1.3 SNSの通知を受け取る
利用するSNSによって詳細が異なるので省略します。
多くは開発者登録をした後、herokuなどでサーバーを動かしてwebhookでリクエストを送るのが一般的なように思います。
Node-REDの拡張を利用してもいいと思います(この方が楽そう)。
SNSではないですが、ngrokを使ってIFTTTを絡めることで天気の変化などを通知できるそうです。
参考記事:https://liginc.co.jp/517843
2.2 声で音楽の再生をコントロールする
次はローカルの音源を音声操作でコントロールしようと思います。
これに関してはGoogleNestはほとんど関係なく、ラズパイからchromecastで音源を飛ばします。
最初はGoogleNestで「ok google,ラズパイで〇〇を再生して」で再生をしようと思ったんですが、コマンドが長かったので断念しました。そこで、ラズパイにUSBマイクを取り付け、ラズパイで音声認識から音楽データのキャストまで行います。
2.2.1 音声認識
pythonのspeech_recognitionを利用します。
import speech_recognition as sr
def main():
while True:
print("Speak something...")
r = sr.Recognizer()
with sr.Microphone() as mic:
audio = r.listen(mic)
try:
text = r.recognize_google(audio, language="ja-JP")
except sr.UnknownValueError:
pass
except sr.RequestError as e:
print(e)
else:
print(text)
いろいろを端折るとこんな感じで動くはずです。pyaudioも必要みたいなのでインストールしておいてください。このコードの変数textに特定のフレーズが含まれるかでコマンドを分けていこうと思います。
スマートスピーカーのようにウェイクワードを付けたい場合はtextに指定したいウェイクワードが入ってなければ何もしないといったコードを書けばいいでしょう。
余談1
GoogleNestを買ってから3日ぐらいはどうにかしてウェイクワードを変えようと奮闘していました。有力な手段であったsnowboyは昨年末でサービスを終了しており、新たに自作のウェイクワードを作れなくなってしまったので、しょうがなくUSBマイクを購入してspeech_recognitionを使いました。
余談2
本来、誤作動を防ぐためにウェイクワードのようなものを付けるべきですが、私はあえて付けずに使っています。せっかく短いコマンドで指示を出せるようになったので、あえて長くしたくなかったからです。うれしいことに(?)、私の買った500円USBマイクはそんなに感度が高くないので、少し大きめな声でしゃべらないと反応しないという欠点があります。それを逆に利用して、指示を出したいときは大きめな声でしゃべることで誤作動が起きにくくなりました。
2.2.2 曲の再生
私が所持しているスピーカーが偶然chromecastに対応していたので、これもしゃべらせたときと同じようにchromecastを利用します。ここでは簡単のためにNode-REDを利用しますが、pythonのpychromecastを利用しても問題ないです。非対応の場合は有線接続で直接再生するのも良いと思います。
Bluetooth接続はラズパイのCUIだけではうまく接続できないことがあったのでオススメはしません。
先ほどのNode-REDの下のフローで、castノードのmedia urlに流したい曲のurlを入れれば曲の再生ができます。chromecastはキャストするデータがウェブ上にある必要があるので、簡易的にhttpサーバーを立ち上げます。
音源はラズパイにUSBメモリを指して保存しています。hogeは音楽データのある階層です。
cd /hoge
python3 -m http.server
(ラズパイのipが192.168.0.20なので)http://192.168.0.20:8000/ にアクセスすると、LANの中ならその階層以下のファイルを見ることができます。
Node-REDのキャストノードの設定例です。まずは正しくファイルが読み込めてるか確認しましょう。
IP :192.168.0.21
Media Url :http://192.168.0.20:8000/xi/Parousia/07.Ascension%20to%20Heaven.mp3
Media Type :audio/mp3
IPはスピーカーのipアドレスです。テストするだけならGoogleNestのipを指定してもいいと思います。
2.2.3 曲のurlを取得する
さて、とりあえずcastで曲を流せるようになったわけですが、毎回Node-REDを開くのでは意味がありません。「音楽の再生」ノードに曲のurlを乗せてアクセスすることで好きな曲を流せるようにします。
import glob
def search_music(voice:str, raspi_ip="192.168.0.20", poot=8000):
voice = voice[:-3].strip()
print(f"Searching for songs containing the letter {voice}")
files = glob.glob(f"hoge/**/[0-9][0-9]*{voice}*.*", recursive=True)
files = [f"http://{raspi_ip}:{poot}/" + file[28:] for file in files]
if len(files) > 0:
file = files[0]
music_name = os.path.splitext(os.path.basename(file))
print(music_name)
return file
else:
try:
raise FileNotFoundError("No matching")
except:
return None
この関数は先ほど認識した声の文字を引数voiceとして受け取ります。私は「〇〇を再生」と言ったときにこの関数を動かすようにしているので、最後の3文字を削っています。また、どうしても空白が入ってしまうことがあるので事前に取り除くとよいと思います。
音楽データがあるかいそうhogeの中を正規表現(?)で検索します。
上のコードでは
xi/Parousia/07.Ascension to Heaven.mp3
のようなパスにマッチします。パスの不要な部分をカットし、http:を付けることでキャストに使うurlが取得できました。
キャストの方法は喋らせたときとまったく同じです。
ここでurlの最後が/cast_musicになっていることに注意してください(音楽の再生ノードの中を見ればわかります)
data = {"url": media_url,}
headers = {'Content-Type': 'application/json',}
req = urllib.request.Request(url, json.dumps(data).encode(), headers)
with urllib.request.urlopen(req) as res:
bo = res.read()
2.2.4 さらなるコントロール
以上でとりあえず再生はできたと思います。先ほどのフローでは標準で10回のループ再生をしていますが、この値はノードやpythonのdataを適当にいじることで調整できます。また、再生の停止や音量の調整は以下のコードで行えます。
def stop_casting(mode="music"):
if mode != "music":
return None
try:
url = "http://192.168.0.20:1880/stop_music"
res = urllib.request.urlopen(url)
res.getcode()
html = res.read()
except Exception as e:
print(e)
def change_volume(volume:int):
url = "http://192.168.0.20:1880/change_volume"
data = {"volume":min(volume, 25)}
try:
headers = {"Content-Type": "application/json"}
req = urllib.request.Request(url, json.dumps(data).encode(), headers)
with urllib.request.urlopen(req) as res:
bo = res.read()
except Exception as e:
print(e)
曲名によってはうまく聞き取ってくれない場合もありますが、なんとか使えるレベルにはなりました。わかりやすい名前でプレイリストを作って再生すれば、普段使いで気になることはないでしょう。
2.3 GoogleNestMiniからラズパイで任意のコードを実行できるようにする
2.3.1 任意のコードの実行
@minatomirai21さんの以下の記事を参考にさせて頂きました。
https://qiita.com/minatomirai21/items/4c4e777b43ede1e42900
Node-REDは以下のjsonの読み込んでください
[{"id":"aefcf43e.a1d8b8","type":"tab","label":"フロー 2","disabled":false,"info":""},{"id":"e08ce7d2.6c6108","type":"mqtt in","z":"aefcf43e.a1d8b8","name":"","topic":"raspberrypi/action","qos":"2","datatype":"auto","broker":"803f70ef.00b1b","x":150,"y":100,"wires":[["1a1ee8d.f4f4d17"]]},{"id":"1a1ee8d.f4f4d17","type":"json","z":"aefcf43e.a1d8b8","name":"","property":"payload","action":"","pretty":false,"x":330,"y":100,"wires":[["87aa9d57.c831f"]]},{"id":"2dac8b99.d63814","type":"exec","z":"aefcf43e.a1d8b8","command":"python3 node_red_handler.py","addpay":true,"append":"","useSpawn":"false","timer":"","oldrc":true,"name":"","x":730,"y":100,"wires":[[],["16deb77a.9b0a69"],[]],"info":"import sys\n\nprint(sys.argv[1])"},{"id":"87aa9d57.c831f","type":"function","z":"aefcf43e.a1d8b8","name":"exclude text","func":"var cmd = msg.payload.data;\nmsg.payload = cmd;\nreturn msg;","outputs":1,"noerr":0,"x":490,"y":100,"wires":[["2dac8b99.d63814"]]},{"id":"16deb77a.9b0a69","type":"debug","z":"aefcf43e.a1d8b8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":970,"y":100,"wires":[]},{"id":"803f70ef.00b1b","type":"mqtt-broker","z":"","name":"","broker":"mqtt.beebotte.com","port":"8883","tls":"","clientid":"","usetls":true,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]
execノードのnode_red_handler.pyはラズパイの/home/piに配置しています。実行させるときは
python3 node_red_handler.py msg.payload
のように、msg.payloadが引数として渡されます。ここにGoogleNestからのデータが格納されているので好きなコードを実行できます。
2.3.2 おまけ(応用編):アラームの設定
ラズパイから好きなコードを動かせるようになったので、アラームを設定してみようと思います。
使い勝手のところで述べたように好きな曲を設定できなかったので、ラズパイから音楽データをGoogleNestにキャストすることでアラームとします。
開始時間の設定はラズパイのcron機能を使います。
GoogleHomeアプリのルーティンの中で使いたいので流れは次のようになります。
1,GoogleNestからラズパイにキーワードを送信
2,キーワードに反応して、GoogleNestにラズパイから「アラームを何時に設定するか」と喋らせる
3,ラズパイ側の音声認識を使って時間を設定しcronに書き込む
4,時間になったらラズパイから曲をキャスト
アラームの設定自体は3だけでできますが、、GoogleNestにアラームを設定するのと同じような雰囲気にしたかったので喋らせることにしました。
以下は3で音声認識で時間を宣言した後の処理を行うコードです。
import subprocess, re
def set_alarm(text):
time = re.findall(f"\D*(\d+)\D*", text)
if len(time) == 0:
try:
raise ValueError("時間を言ってください")
except:
return
elif len(time) == 1:
if "半" in text:
h, m = time[0], 30
else:
h, m = time[0], 0
elif len(time) == 2:
h, m = time
else:
try:
raise ValueError("不正な表現です")
except:
return
subprocess.run("crontab -l > crontab.old", shell=True)
with open("crontab.old", mode="r") as f:
cron = f.readlines()
cron = cron[:-1]
cron.append(f"{m} {h} * * * sh /home/pi/alarm.sh\n")
with open("crontab.old", mode="w") as f:
f.writelines(cron)
subprocess.run("crontab crontab.old", shell=True)
play_text_handler(f"アラームを{h}時{m}分に設定しました。")
正規表現で時間を取り出しsubprocessを使ってcronに書き込んでいます。毎日使うことを想定しているので、cronの最終行を削除しています。cronを他の用途でも使っている人はバックアップを取っておくと安心だと思います。
play_text_handlerはGoogleNestを喋らせるときに使った関数です。
cronにはalarm.shを実行することしか書いていませんが、このファイルの中でpythonコードを実行しており、好きな曲をキャストしています。
3 終わりに
以上になります。うまく組み合わせればアイデア次第で曲の再生やアラームの設定に限らず多くのことができると思います。
1つ心残りなのが、ラズパイからGoogleNestのコマンドを実行できないことです。もし何かアイデアがありましたら教えていただけると嬉しいです。