Google Home でちょっと未来風のスマート TV を作ってみたよ☆

#<b>■はじめに</b>
昨年から GoogleHome / AmazonEcho やラズパイ、黒豆 (Broadlink RM Mini 3) [[LINK]](https://qiita.com/miso_develop/items/204b2e16b1e58e52dc07) を使った おうちハックにはまっていますが、家電の操作やスマートロック [[LINK]](https://qiita.com/kt-sz/items/e8ccec02605584e62b53) など使った一般的なスマートホーム化が一通り実現し、だんだんやることが無くなってきました（笑）

でもせっかく面白いデバイスが揃っているので、なんとなく未来っぽくて、でも実用的っぽいテレビというのを作ってみました。

一言で言うと「番組名などを言うだけで勝手に電源が点いてその番組に切り替えてくれるスマートTV」です。

#<b>■デモ動画</b>
何はともあれまずはデモをご覧ください。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">TV番組検索DEMO <a href="https://t.co/DfgZo9mHVR">pic.twitter.com/DfgZo9mHVR</a></p>&mdash; ktsz (@ededken) <a href="https://twitter.com/ededken/status/1052847817570496513?ref_src=twsrc%5Etfw">2018年10月18日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

Google Home に向かって見たい番組名・チャンネル番号、TV局名などをしゃべると、（必要なら）TV 電源を点けて、今やっている番組リストから該当番組を探して自動でチャンネルを切り替えたりできます。

「あ！いま"イッテQ"やってる時間だ！」と観たい番組を放送しているのが分かっているときは、いきなりテレビの前で番組名を言うだけで良いのでなかなか便利です。

ちょっと未来っぽくないですか？（自己満足・・笑）

※動画の中で TV に番組表を表示している部分について実装方法の詳細は以下でご紹介していますのでご興味があればどうぞ。

投稿記事：[GoogleHome と Chromecast でキーワード＆画像＆Wikipedia検索～TV表示 - Qiita](https://qiita.com/kt-sz/items/9edea18c4f0f5e8cf631)

#<b>■全体図</b>
この機能は以下のような機器・機能の組合せで実現しています。
<img src="https://qiita-image-store.s3.amazonaws.com/0/241735/838c59dd-c735-af32-ea0a-3059148235e0.png" width=80%>

#<b>■使うもの</b>
#####<b>●Google Home</b>
今回は IFTTT で Google Assistant の自由テキスト認識(Say a phrase with a text ingredient)をトリガーに使っています。なので残念ながら Amazon Echo では(IFTTT 経由では)難しいですが、ちょっとがんばってカスタムスキルを書けばほぼ同じような事が可能です。
#####<b>●IFTTT</b>
Google Assistant トリガーで「OK Google テレビを？？？にして」みたいなフレーズを設定して、取得したテキストを検索番組名として自宅の WEBHOOK サーバーに渡しています。

#####<b>●Raspberry Pi</b>
自宅に設置している WEBHOOK サーバー [[LINK]](https://qiita.com/kt-sz/items/e8ccec02605584e62b53#webhook-%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E6%A7%8B%E7%AF%89) が動作するラズパイです。今回のサンプルを動かすだけならラズパイでなくても、node が動けばなんでもOKです。PC 等の他 FireTVStick 等も利用可能です。

※FireTVStickをlinuxサーバー化 [[Link]](https://scrapbox.io/smart-home/Fire_TV_Stickをhubotサーバ化してRM_Mini3を操作する)

#####<b>●黒豆 (Broadlink RM Mini 3)</b>
https://qiita.com/miso_develop/items/204b2e16b1e58e52dc07
ラズパイなどから赤外線リモコンを持つ家電などをコントロールできる赤外線送信コマンダーです。スマートホームには欠かせないアイテムです♪
今回は黒豆からテレビの電源とチャンネル切り替えを行っていますが、ラズパイからなどコントロール可能な他の赤外線コマンダーでももちろんOKです！
#####<b>●Node.js</b>
以下のEPG取得やひらかなAPI呼び出しの他、IFTTT からの WEBHOOK 受信 や、赤外線送信デバイス黒豆の制御などを行っています。（ここでは詳細は省きます。各LINKをご参照ください）
#####<b>●Gガイド.テレビ王国のEPGデータ</b>
https://tv.so-net.ne.jp/rss/schedulesByCurrentTime.action?group=10&stationAreaId=23
現在放送中のTV番組一覧をRSS配信しているサイトです。このRSSデータの中から、IFTTT で認識した見たい番組名を検索してチャンネル番号を取得しています。
#####<b>●gooラボ ひらがな化API</b>
https://labs.goo.ne.jp/api/jp/hiragana-translation/
テキストを食わせると、いい感じで ひらかな化 してくれる WEB API です。
このAPIを使って上記 G ガイドの番組 EPG データと IFTTT で取得した番組名両方を「ひらかな化」して番組を検索しています。漢字のEPGだけで検索すると英語表記や数字などが Google Home で認識したテキストとは微妙に食い違うときがあるので、両方をひらかなにすることで番組検索の Hit 率を上げています。

#<b>■コード</b>

以下が実際の node のコードになります。
こちらを node がインストールされた機器の上で、引数に検索したい番組名を指定して実行することで該当する番組のチャンネル番号を返します。
あとは IFTTT からの WEBHOOK を受けるように HTTP サーバーを設置したり [[LINK]](https://qiita.com/kt-sz/items/e8ccec02605584e62b53#webhook-%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E6%A7%8B%E7%AF%89) 、黒豆などを使った IR 送信デバイスを呼び出したり [[LINK]](https://qiita.com/miso_develop/items/204b2e16b1e58e52dc07) すれば完成です。
※HTTP 通信を行うための request をインストールしておいてください。

```epg-search.js
var request = require('request');

// 引数チェック
var argExe = process.argv[0];
var argScr = process.argv[1];
if (process.argv.length < 3) {
    console.log('use: ' + argExe + ' ' + argScr + ' keyword');
    return;
}

// キーワード文字列取得
var keywd = process.argv[2];
keywd = keywd.replace(/\s+/g, '');
console.log(`EPG search keywd: ${keywd}`);

// ひらかな変換API用設定
var hirakanaApiPostData = {
   "app_id" : "Gooラボで取得したAPP IDに書き換えてください",
   "sentence" : 'ダミー',
   "output_type" : "hiragana"
};

// アクセスしたいサイト順に URL と付加情報の設定
var urlAry = [
{ // 現在時刻の EPG RSS データ取得用
  url: 'https://tv.so-net.ne.jp/rss/schedulesByCurrentTime.action?group=10&stationAreaId=23',
  method: 'GET',
},
{ // Goo ひらかな変換API用
  url: 'https://labs.goo.ne.jp/api/hiragana',
  method: 'POST',
  headers: { 'Content-Type' : 'application/json' },
  json: true,
  form: hirakanaApiPostData
}
]

// 現在放送中番組の RSS 文字列を変換して必要な情報だけ残す
function epgParse(epgStr) {
      epgStr = epgStr.replace(/<link>http\:.+from=rss<\/link>/g, '');
      epgStr = epgStr.replace(/<description>.+\[/g, '[');
      epgStr = epgStr.replace(/\(Ch./g, '(Ch');
      epgStr = epgStr.replace(/\)\]<\/description>/g, 'チャンネル)]\n');
      epgStr = epgStr.replace(/<title>.+<\/title>/, '');
      epgStr = epgStr.replace(/<description>.+<\/description>/, '');
      epgStr = epgStr.replace(/<dc:.+>("[^"]*"|'[^']*'|[^'">])*<\/dc:.+>/g, '');
      epgStr = epgStr.replace(/\[[字デ終解]\]/g, '');
      epgStr = epgStr.replace(/http:\/\/www.so-net.ne.jp\/tv\//g, '');
      epgStr = epgStr.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g, '');
      epgStr = epgStr.replace(/[\'\"\!\&\?！？＆]/g, '');
      epgStr = epgStr.replace(/[「」『』（）｛｝【】☆★]/g, '');
      epgStr = epgStr.replace(/\s+/g, '');
      return epgStr;
}

// Goo ひらかな変換APIで変換後のひらかなEPG文字列を修正
function hirakanaParse(hirakanaEpg) {
    hirakanaEpg = hirakanaEpg.replace( /\(ちゃんねる/g, '\(Ch' );
    hirakanaEpg = hirakanaEpg.replace( /ろくちゃんねる\)/g, '6チャンネル\)' );
    hirakanaEpg = hirakanaEpg.replace( /よんちゃんねる\)/g, '4チャンネル\)' );
    hirakanaEpg = hirakanaEpg.replace( /ごちゃんねる\)/g, '5チャンネル\)' );
    hirakanaEpg = hirakanaEpg.replace( /はちちゃんねる\)/g, '8チャンネル\)' );
    hirakanaEpg = hirakanaEpg.replace( /じゅーにちゃんねる\)/g, '12チャンネル\)' );
    hirakanaEpg = hirakanaEpg.replace( /きゅーちゃんねる\)/g, '9チャンネル\)' );
    hirakanaEpg = hirakanaEpg.replace( /いちちゃんねる\)/g, '1チャンネル\)' );
    hirakanaEpg = hirakanaEpg.replace( /にちゃんねる\)/g, '2チャンネル\)' );
    hirakanaEpg = hirakanaEpg.replace( /ななちゃんねる\)/g, '7チャンネル\)' );
    hirakanaEpg = hirakanaEpg.replace( /\<きー\>/g, '\<key\>' );
    hirakanaEpg = hirakanaEpg.replace( /[^\u3041-\u3096 \u30A1-\u30FA\:0-9Ch\(\)\[\]\<key\>ー]/g, '' );
    hirakanaEpg = hirakanaEpg.replace( /\s/g, '' );
    return hirakanaEpg;
}

// EPGのRSSデータとひらかなEPGデータの二つからキーワードを含む番組を検索
// 該当番組が見つかったらチャンネル番号を画面表示
function searchProgram( epgStr, hirakanaDt ) {
    var hitCh = 0;
    // 最初にひらかな EPG の末尾に含まれるキーワード文字列を抽出
    if( hirakanaDt.indexOf('<key>') != -1 ){
        hirakanaDt = hirakanaDt.split('\<key\>');
    } else {
        hirakanaDt = hirakanaDt.split('\<きー\>');
    }
    var hirakanaKey = hirakanaDt[1];
    hirakanaDt = hirakanaDt[0];
    console.log(`HirakanaKey: ${hirakanaKey}`);
    //---------------------------------------------
    // EPG データから番組検索
    epgStr = epgStr.split(')]');
    hirakanaDt = hirakanaDt.split(')]');
    for( var i=0 ; i < 12 ; i++ ){
        (function(i) {
            if( epgStr[i] ){
                 if( epgStr[i].indexOf(keywd) != -1 ){
                      var spritCh = epgStr[i].split('(Ch');
                      spritCh[1] = spritCh[1].replace( /([0-9]+)チャンネル/, '$1' );
                      hitCh = spritCh[1];
                 }
             }
             if( hirakanaDt[i] ){
                 if( hirakanaDt[i].indexOf(hirakanaKey) != -1 ){
                      var spritCh =hirakanaDt[i].split('(Ch');
                      spritCh[1] = spritCh[1].replace( /([0-9]+)チャンネル/, '$1' );
                      hitCh = spritCh[1];
                 }
             }
        })(i);
    }
    //---------------------------------------------
    // 番組は見つかったか？
    if( hitCh != 0 ){
       // 以下を IR 送信デバイスなどから TV を操作するコマンドに書き換えてください
       console.log(`tv-ch = ${hitCh} ch`);
    } else {
       console.log("お探しのチャンネルは見つかりませんでした");
    }
}

// 引数 urls の順番で HTTP アクセス（同期モード）
function execRequests(urls, callback) {

  var url;
  var length = urls.length;
  var epgStr;

  function execRequest(idx) {
    url = urls[idx];
    console.log("url:", url.url);
    // exec http request
    request(url, function(error, response, body) {
      if (!error && response.statusCode == 200 ) {
        console.log("statusCode:", response.statusCode);
        if( idx == 0 ){
            // EPG RSS を受信したらひらかな変換用に加工して urls に格納
            body = epgParse(body);
            hirakanaApiPostData.sentence = body + '<key>' + keywd;
            epgStr = body;
            urls[1].form = hirakanaApiPostData;
        } else if( idx == 1 ){
            // ひらかなを受け取ったら変換してキーワード検索してチャンネル切り替え
            var hirakanaData = hirakanaParse(body.converted);
            searchProgram( epgStr, hirakanaData );
        }
      }
      // sleep or return
      if (idx + 1 < length) {
        console.log("exec execRequest() after 50ms...");
        setTimeout(function() {
          execRequest(idx + 1)
        }, 50);
      } else {
        return callback();
      }
    });
  }

  // 最初の URL からアクセス開始
  execRequest(0);
}

// 同期モードによる HTTP アクセス開始
execRequests(urlAry, function() {
  console.log("End.");
});

```

#<b>■最後に</b>
冗談半分のつもりで作ったのですが(笑)その割になかなか実用的です。

音声認識で TV を操作するって聞くと、そんなの面倒！いちいち音量とか声で操作してられないと！と思ってしまいますが、今回の機能を作ってみて、必ずしもそうじゃないかも？実際に生活に役立つ音声認識と家電の組合せは Google Home などの普及が広まってきたら今後色々発見されていくのかな？とちょっと未来を感じました ：D

今後も色々面白くて実用的な使い方を探してみようと思います☆

##<b>※投稿記事一覧</b>
他にも 自分で「おうちハック」した内容を以下に投稿しているので、よろしければ合わせてご覧ください☆

・GoogleHome/AmazonEcho とラズパイでやった事・やりたい事一覧[[LINK]](https://qiita.com/kt-sz/items/86536aff99d80c1cd464)
・Google Home でちょっと未来風のスマート TV を作ってみたよ☆[[LINK]](https://qiita.com/kt-sz/items/0362d5fe3e9a3e478245)
・黒豆 (Broadlink RM Mini 3) の IR 信号解析してみたよ♪ [[LINK]](https://qiita.com/kt-sz/items/1c36a7b22a58359a8e6f)
・スマートロック SESAME の WEB API が便利だった！[[LINK]](https://qiita.com/kt-sz/items/e8ccec02605584e62b53)
・Amazon Echo の沢山ある面白スキルを気軽に遊ぶ方法[[LINK]](https://qiita.com/kt-sz/items/0d01e763d540ddea5816)
・LINEを「ラズパイのターミナル化＆スマートホームのフロントエンド化(ChatOps風)」してみた♪[[LINK]](https://qiita.com/kt-sz/items/9adcb02a3b3a6cf3dd7b)
・LINE と Firebase とラズパイを繋いでみたよ☆[[LINK]](https://qiita.com/kt-sz/items/c008084300a978516153)
・GoogleHome と Chromecast でキーワード＆画像＆Wikipedia検索～TV表示[[LINK]](https://qiita.com/kt-sz/items/9edea18c4f0f5e8cf631)
・スマホ ハック＆スマートホーム連携 [[LINK]](https://qiita.com/kt-sz/items/9e23bd5969f0024b17f8)
・ラズパイとサーモセンサーで自宅のガスコンロ監視（火の用心） [[LINK]](https://qiita.com/kt-sz/items/471f469cc89d2bac0c76)
・トニー・スタークの家にありそうなスマート・テーブル（笑） [[LINK]](https://qiita.com/kt-sz/items/13cb71214604081dbf7c)