はじめに
ついつい夜更かしをしてしまうの方に向けにGoogle HomeとNature Remoを組み合わせて「指定した時間以降、部屋が明るければGoogle Homeより早く寝るように警告を発する装置」をNode.jsで実装する作例をご紹介します!!
市販品を組み合わせるだけなのでお手頃に作れます!!(たぶん)
ちなみに似たような作例はよくありますが、多くの記事では「google-home-notifier」と呼ばれるGoole Homeに簡単にプッシュ発話をさせるライブラリが使われており、google-ttsの仕様に依存していたり、バグが多かったりして動かないことが多いのでなるべく根本ロジックから実装する方法でやっていきます。
構成
Image may be NSFW.
Clik here to view.
必要なもの
Nature Remo
所謂スマートリモコン。TVやエアコンなどの赤外線リモコン信号を記憶し、WiFiに繋がったこの製品から記録した赤外線信号を発することで、スマホからまとめて家電操作を可能にする製品。 帰宅前に会社からエアコンを操作して部屋を冷やしとくなどの使い方ができます。
色々なメーカーから類似品が大量に販売されていますが、このNature Remoには温度、湿度、照度、人感センサーなどが搭載されており、しかも外部APIから値を取得することができるのです!! このAPIを応用して部屋の照度を取得します。
Google Home (miniでも可)
言わずと知れたGoogle製スマートスピーカー。 このGoogle Homeより「早く寝ろ!!」といった感じに音声で警告するようにします。しかし、現状の公式SDKでは自発的に発音させることができません。。。
そこで、Google製スマートデバイスで搭載されている「キャスト機能」
を活用します。キャスト機能とは、「音楽や動画をストリーミングデータとして他のデバイスに配信する」機能のことで、スマホのYoutubeアプリからTV(クロムキャスト)に飛ばして再生したり、Spotifyアプリから音楽をGoogle Homeに飛ばして再生することができます。 このキャスト機能を応用することで、任意の音声ファイルをGoogle Homeに再生させることができます。
ローカルサーバーか何か(RaspberryPiやNASなど)
基本的にGoogle Homeにキャストできるのはローカルデバイスに限られるので、ローカルネットワーク内で常時動かせるNode.js環境が必要になります。 ルータに穴を開ける、ngrokを使って中継させる方法もあるようです?? 今回は、自宅に置いてあるSynology製NASのNode.js上でタスクスケジューラーを起点に動かします。
(一から用意する人は、お手軽なWiFi搭載マイコンボードESP8266がコスパ最強かも? 「ESP8266 から Google Home に喋らせるライブラリ」 https://qiita.com/horihiro/items/4ab0edf415916a2cd542)
設計
照度の取得
「Nature Remo Cloud API」より取得します。 アクセストークンの発行が必要です。
https://developer.nature.global/
音声合成
警告音声の生成には無料で手軽に使える音声合成API「VoiceText Web API」
を使います。 感情の操作もでき、本家にも劣らないほど流暢です。 使うにはAPIキーが必要なので、下記より利用申請しましょう。 「VoiceText Web API」
(レファレンス)
https://cloud.voicetext.jp/webapi
ゴリゴリにリクエストパラメータを書いて実装も良いですが、面倒なので有志の方が作られたNode.js向けライブラリもあるのでそちらを使います。「node-voicetext」
https://github.com/pchw/node-voicetext
どんな感じの音声か知りたい人はこちらから⬇️
http://voicetext.jp/samplevoice/
キャスト機能
キャスト機能の機器間の詳細なプロトコル情報はGoogle公式からは公開されていないよう??ですが、有志の方が解析して再現したライブラリ?があるのでそちらを使いキャスト機能のクライアント部分を実装します。「castv2」
https://github.com/thibauts/node-castv2
当初、VoiceText Web APIで生成したmp3音声ファイルをGoogle Homeにストリーミングで投げればいけると思っていましたが、どうやらGoogle Homeが受け取れるのはURL情報だけっぽい?ので、mp3ファイルを配信するWEBサーバを建てる実装も必要になります。
Google HomeのIPアドレスの特定
キャストするには相手先のIPアドレス特定する必要があります。幸い、Google Homeは「mDNS」
というプロトコルに対応しており、マルチキャストアドレス224.0.0.251、UDP5353ポートに向けてmDNS queryと呼ばれるメッセージを投げると対応している機器が一斉にIPアドレスを返します。また、この応答情報内のTXTレコードにGoogle Homeのデバイス名情報があるので、それで識別することができます。こちらも一から実装するのは面倒なのでライブラリに頼ります。「mdns」
https://github.com/agnat/node_mdns
interfaceIndex: 6,
type:
ServiceType {
name: 'googlecast',
protocol: 'tcp',
subtypes: [],
fullyQualified: true },
replyDomain: 'local.',
flags: 2,
name: 'Google-Home-Mini-ccebf2ba7hogehogehoge5ff2e9',
networkInterface: 'en0',
fullname: 'Google-Home-Mini-ccebhogehogehoge._googlecast._tcp.local.',
host: 'ccebf2ba-7c7d-ed85-6986-c0af325ff2e9.local.',
port: 8009,
rawTxtRecord: <Buffer 23 69 64 3d 63 63 65 62 66 32 62 61 37 63 99 64 65 64 38 35 36 39 38 36 63 30 61 66 33 32 99 66 66 32 65 39 23 63 64 3d 36 42 44 33 99 33 33 36 36 30 ... >,
txtRecord:
{ id: 'ccebf2ba7c7ded9999af325ff2e9',
cd: '6BD343366053C3C999990F6AF76C',
rm: 'E705D56999969244F',
ve: '05',
md: 'Google Home Mini',
ic: '/setup/icon.png',
fn: 'ダイニング ルーム',
ca: '198660',
st: '0',
bs: 'FA8FCA855A41',
nf: '1',
rs: '' },
addresses: [ '192.168.1.21' ]
実装
照度の取得
レファレンスに書いてある通りヘッダーにアクセストークンを入れてリクエストを投げます。
asyncfunctiongetIl(){returnaxios.get('https://api.nature.global/1/devices',{headers:{'Accept':'application/json','Authorization':'Bearer AjmyJhogehogKSRFg3owta-DfnahogehodksQ2g.ezTpIVtf-V8znl7HSfg-PYhogehoge0r0WxXTFA'},data:{}}).then(response=>{returnresponse.data[0].newest_events.il.val;}).catch(err=>{console.log(err);reject(err);});}
音声ファイルの生成
話者、感情値などの各種パラメータを設定しVoiceText Web APIより音声ファイルを取得してローカルに保存します。
functionconvertToText(text){returnnewPromise(function(resolve,reject){voice.speaker(voice.SPEAKER.TAKERU).emotion(voice.EMOTION.HAPPINESS).emotion_level(voice.EMOTION_LEVEL.HIGH).volume(150).format('mp3').speak(text,function(e,buf){if(e){console.error(e);reject(e);}else{fs.writeFileSync(OUT_PATH,buf,'binary');resolve();}});});}
キャスト用音声ファイル配信WEBサーバを建てる
ExpressでWEBサーバーを建て、キャスト用のmp3音声ファイルを配信します。
functionstartSever(){constserver=app.listen(PORT,console.log('port: '+PORT));app.get(`/audio/${FILE_NAME}`,(req,res)=>{fs.readFile(`./${FILE_NAME}`,(err,data)=>{if(err)res.status(400).send(err.toString());else{res.setHeader('Content-Length',data.length);res.write(data);res.end();}});});returnserver;}
Google HomeのIPアドレスを取得
mdnsクエリーを投げてレスポンスを数秒待ちます。 検索結果の中より対象デバイス名のIPアドレスを抜き出します。
functionsearchDeviceIp(device_name){letdevice_ip='';returnnewPromise((resolve,reject)=>{browser.start();// serviceUpイベントより情報を抜き出すbrowser.on('serviceUp',(service)=>{if(device_name.replace(/\s+/g,'')===service.txtRecord.fn.replace(/\s+/g,'')){device_ip=service.addresses[0];}});// 検索時間setTimeout(()=>{browser.stop();resolve(device_ip);},SEARCH_TIMEOUT);});}
キャスト機能
キャストする際にGoogle Homeの音量をコントロールすることができます。エラーで死んだ時に必ずコネクションをクローズ!!
functionsay(host,content){constserver=startSever();constclient=newClient();client.connect(host,()=>{client.setVolume({level:0.5},(err,newvol)=>{if(err)console.log("there was an error setting the volume");});client.launch(DefaultMediaReceiver,(err,player)=>{if(err){console.log(err);server.close();return;}player.on('status',status=>{console.log(`status broadcast playerState=${status.playerState}`);});constmedia={contentId:content,contentType:'audio/mp3',streamType:'BUFFERED'};player.load(media,{autoplay:true},(err,status)=>{client.close();server.close();});});});client.on('error',err=>{console.log(`Error: ${err.message}`);client.close();server.close();});};
最終実装
/*****************************************************************//* コンフィグ&パラメーター *//*****************************************************************/constNAS_IP='192.168.22.9'/* NASのIPアドレス */constPORT=8083;/* NASのポート */constFILE_NAME='voice.mp3';/* 音声合成出力ファイル名 */constGOOGLE_HOME_NAME='ダイニング ルーム';constVOICE_MESSAGE='寝る時間です! 直ちに歯磨きをして就寝しましょう!!'constNATURE_REMO_API_KEY='AjmyJV5syfoghogehogeREJdksQ2g.ezTpIVtf-V8znl7HJjrF0r0WxXTFA';/* Nature Remo Cloud API アクセストークン */constVOICE_TEXT_API_KEY='w7ahogehogebiam';/* VoiceText Web API アクセストークン *//* パラメーター */constSEARCH_TIMEOUT=2000;/* Goole Homeの探索時間(ms) */constIL_THRESHOLD=100;/* 照度の閾値 */constATTENTION_INTERVAL=90*1000;/* 警告のインターバル(ms) */constMONITORING_ENDTIME=dayjs().hour(3).minute(15).add(1,'day');/* 監視終了時間 次の日の3時15分(日本時間)*/constGOOGLE_HOME_VOLUME=0.5;/* Google Homeのボリューム*//*****************************************************************//* ライブラリ *//*****************************************************************//* 各種ライブラリ */constfs=require('fs');constaxios=require('axios');constapp=require('express')();constClient=require('castv2-client').Client;constDefaultMediaReceiver=require('castv2-client').DefaultMediaReceiver;constVoiceText=require('voicetext');constvoice=newVoiceText(VOICE_TEXT_API_KEY);constmdns=require('mdns');constbrowser=mdns.createBrowser(mdns.tcp('googlecast'));/* 日付ライブラリ&日本ロケール設定 */require('dayjs/locale/ja');constdayjs=require('dayjs');dayjs.locale('ja');/*****************************************************************//* メインルーチン *//*****************************************************************/constOUTPUT_PATH=`./${FILE_NAME}`;/* 音声ファイル出力先 */constURL=`http://${NAS_IP}:${PORT}/audio/${FILE_NAME}`;/* NAS上の音声ファイルURL */constgoogle_home_ip=searchDeviceIp(GOOGLE_HOME_NAME);(async()=>{// 音声データ作成awaitconvertToText(VOICE_MESSAGE,OUTPUT_PATH);// 初回チェックif(!awaitisSleep()){say(google_home_ip,URL,GOOGLE_HOME_VOLUME);}// 監視開始constid=setInterval(async()=>{if(!awaitisSleep()){say(GOOGLE_HOME_IP,URL,GOOGLE_HOME_VOLUME);}elseif(dayjs().isAfter(MONITORING_ENDTIME)){// 監視終了clearInterval(id);}},ATTENTION_INTERVAL);})();/*****************************************************************//* 各種関数 *//*****************************************************************/functionsearchDeviceIp(device_name){letdevice_ip='';returnnewPromise((resolve,reject)=>{browser.start();// serviceUpイベントより情報を抜き出すbrowser.on('serviceUp',(service)=>{if(device_name.replace(/\s+/g,'')===service.txtRecord.fn.replace(/\s+/g,'')){device_ip=service.addresses[0];}});// 検索時間setTimeout(()=>{browser.stop();resolve(device_ip);},SEARCH_TIMEOUT);});}functionstartSever(port,file_name){constserver=app.listen(port,console.log('port: '+port));// ルーティング設定app.get(`/audio/${file_name}`,(req,res)=>{fs.readFile(`./${file_name}`,(err,data)=>{if(err)res.status(400).send(err.toString());else{res.setHeader('Content-Length',data.length);res.write(data);res.end();}});});returnserver;}functionsay(host,content,volume){constserver=startSever(PORT,FILE_NAME);constclient=newClient();// デバイスへの接続client.connect(host,()=>{// 音量設定client.setVolume({level:volume},(err,newvol)=>{if(err)console.log("there was an error setting the volume");});// キャストの開始client.launch(DefaultMediaReceiver,(err,player)=>{if(err){console.log(err);server.close();return;}player.on('status',status=>{console.log(`status broadcast playerState=${status.playerState}`);});constmedia={contentId:content,contentType:'audio/mp3',streamType:'BUFFERED'};// 音声再生player.load(media,{autoplay:true},(err,status)=>{client.close();server.close();});});});// 接続できない場合のエラー処理client.on('error',err=>{console.log(`Error: ${err.message}`);client.close();server.close();});};functionconvertToText(text,output_path){returnnewPromise((resolve,reject)=>{voice.speaker(voice.SPEAKER.TAKERU).emotion(voice.EMOTION.HAPPINESS).emotion_level(voice.EMOTION_LEVEL.HIGH).volume(150).format('mp3').speak(text,(e,buf)=>{if(e){console.error(e);reject(e);}else{fs.writeFileSync(output_path,buf,'binary');resolve();}});});}asyncfunctiongetIl(api_key){returnaxios.get('https://api.nature.global/1/devices',{headers:{'Accept':'application/json','Authorization':`Bearer ${api_key}`},data:{}}).then(res=>{returnres.data[0].newest_events.il.val;}).catch(err=>{console.log(err);reject(err);});}asyncfunctionisSleep(){constil=awaitgetIl(NATURE_REMO_API_KEY);return(il<IL_THRESHOLD);}
運用
Google Homeのデバイス名の確認
自分で設定したデバイス名をお忘れの方は、スマホのGoogle Homeアプリより確認することができます。
Image may be NSFW.
Clik here to view.
NASへの設置、タスクスケジューラーの設定
NASの適当なディレクトリに実装したソースファイルを設置し、消灯時間に自動実行するようにタスクスケジューラーに登録します。
SynologyNASであれば、コントロールパネル→タスクスケジューラーより登録できます!
Image may be NSFW.
Clik here to view.