Python 3 から Google Home に喋らせる（低遅延）

# 2022/4/13 追記

本記事は名前指定で機器を特定する方法を頑張って調べた話なのですが、pychromecast側で2020年4月にその機能が付いたので、今となっては意味のないものになっています。

参考：[Add helper function get_listed_chromecasts #348](https://github.com/home-assistant-libs/pychromecast/pull/348)

以下、投稿時のまま残します。

# はじめに
Google Home にプッシュで喋らせる場合、google-home-notifier がよく使われると思います。
しかし、なんか大げさな気がする・・・

軽い方法として、Python で pychromecast を使ってしゃべらせる方法があります。

しかし使ってみたら、スクリプトを起動してから喋るまで10秒近くかかっちゃいます。

そこで、できるだけ早く喋り始めさせる方法を模索しました。

# pychromecast のキホン

pychromecast は、音声データのある適当なURLをぶち込むと、それを再生してくれます。（それ以外にもいろいろできますが、ここでの使い方はそれを想定しています）

音声データはどこかのWebサーバに置いておく必要があります。任意の言葉をしゃべらせる場合は、テキスト→音声変換が別途必要になりますね。

喋らせる内容が固定であれば、固定の音声ファイルをWebサーバに置いておけばよいです。

ここでは、 `http://192.168.0.100/test.mp3` に音声ファイルがある前提で進めます。（Google Home から見える場所であれば、ローカルネットワーク内でもインターネット上でもどちらでもいいです。）

基本的な使い方は以下のようになります。

```python:ghspeech1.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import pychromecast

mp3url = 'http://192.168.0.100/test.mp3';

#Chromecastデバイス（Google Homeも）を探す
chromecasts = pychromecast.get_chromecasts()

if len(chromecasts) == 0:
    print("Google Homeが見つかりませんω")
    exit()

# 固定で1個目を使う
googleHome = chromecasts[0]

if not googleHome.is_idle:
    print("Killing current running app")
    googleHome.quit_app()
    time.sleep(5)

#喋らせる
googleHome.wait()
googleHome.media_controller.play_media(mp3url, 'audio/mp3')
googleHome.media_controller.block_until_active()
```

Google Home が複数台ある場合はどうなるでしょうか？

たまたま最初に見つかったGoogle Homeがしゃべります。
仮にGoogle Homeが1台だけでも、Chromecastが存在する場合はそっちが選ばれてしまう可能性があります。

ほとんどのご家庭では実用になりませんね！！！

で、次。名前でGoogle Homeを特定する方法。

```python:ghspeech2.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import pychromecast

mp3url = 'http://192.168.0.100/test.mp3';

#Chromecastデバイス（Google Homeも）を探す
chromecasts = pychromecast.get_chromecasts()

if len(chromecasts) == 0:
    print("Google Homeが見つかりませんω")
    exit()

# 名前で探す
googleHome = next(cc for cc in chromecasts if cc.device.friendly_name == 'リビング')

if not googleHome.is_idle:
    print("Killing current running app")
    googleHome.quit_app()
    time.sleep(5)

# 喋らせる
googleHome.wait()
googleHome.media_controller.play_media(mp3url, 'audio/mp3')
googleHome.media_controller.block_until_active()
```

違いは

```python
#固定で1個目を使う
googleHome = chromecasts[0]
```

が

```python
# 名前で探す
googleHome = next(cc for cc in chromecasts if cc.device.friendly_name == 'リビング')
```

になっただけです。
簡単ですね。

ちなみに、モデル名を使って

```python
#モデル名で探す
googleHome = next(cc for cc in chromecasts if cc.device.model_name == 'Google Home Mini')
```

のように探すこともできます。

ただこれらの方法、しゃべり始めるまでに10秒ぐらいかかります。

どこで時間がかかっているかというと、`pychromecast.get_chromecasts()` のところです。つまり、デバイス一覧を作るところです。pychromecast のソースを見てみたところ、一覧作成時に5秒の待ちがあります。

一覧の作成は飛ばしたい。

# IPアドレス指定でやる

IPアドレス指定でイケる方法が絶対あるはずだ。そう思って調べた結果が以下のソースになります。

```python:ghspeech4.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import pychromecast

mp3url = 'http://192.168.0.100/test.mp3';

# IPアドレスで特定する
googleHome = pychromecast.Chromecast('192.168.0.22')

if not googleHome.is_idle:
    print("Killing current running app")
    googleHome.quit_app()
    time.sleep(5)

#喋らせる
googleHome.wait()
googleHome.media_controller.play_media(mp3url, 'audio/mp3')
googleHome.media_controller.block_until_active()
```

これだと割とすぐ喋ってくれます。

ちなみに、ubuntu 18.04 で `apt install python3-pychromecast` でインスコした pychromecast ではエラーが出てしまいます。pip で最新をインスコしましょう。

これで低遅延で喋らせることができましたが、IPアドレスを直接指定するのは嫌だ！ やっぱり名前で指定したい！！

# 一覧の作成はせず、名前で指定する

名前指定で探して、見つかったら可及的速やかにそいつに喋らせる。不可能ではないはずだ。そう思って5時間ぐらいかけて作ったコードが以下です。

```python:ghspeech5.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import pychromecast
import time
from threading import Event
from zeroconf import ServiceBrowser, Zeroconf

googleHomeName = 'リビング'
mp3url = 'http://192.168.0.100/test.mp3'

class MyListener(object):
    def add_service(self, zeroconf, type, name):
        global ip
        info = zeroconf.get_service_info(type, name)
        if info.properties[b'fn'].decode() == googleHomeName :
            #見つかった機器に割り振られているIPアドレスの最初のIPを採用(IPv6の場合もあるかも)
            ip = info.parsed_addresses()[0]
            #見つかったことを通知
            discover_complete.set() 

discover_complete = Event() #待ち合わせ用
zeroconf = Zeroconf()
listener = MyListener()

# GoogleHomeを探す（非同期実行）
browser = ServiceBrowser(zeroconf, "_googlecast._tcp.local.", listener)

# 見つかるかタイムアウトするまで待ち
discover_complete.wait(5)

googleHome = pychromecast.Chromecast(ip)

if not googleHome.is_idle:
    print("Killing current running app")
    googleHome.quit_app()
    time.sleep(5)

# 喋らせる
googleHome.wait()
googleHome.media_controller.play_media(mp3url, 'audio/mp3')
googleHome.media_controller.block_until_active()
```

zeroconfを使って探索を開始し、指定された名前のが見つかったらすぐにそいつに喋らせます。

探索は非同期実行なので、Event の set() wait() を使っているところがミソです。

# まとめ

- pychromecast を普通に使うとデバイス一覧作成で時間がかかる
- IPアドレス直指定だと低遅延で喋らせることができる
- さらに、zeroconfと組み合わせて、名前指定でも低遅延で喋らせることができる！