出かけるときに雨が降っていたらAlexaが教えてくれるシステム【TWELITE PAL】

家や職場を出たものの、雨が降っているのに傘を持っていくのを忘れて取りに戻った経験はありますか？
私はめちゃくちゃあります。
戸建てなら玄関を出てすぐ雨だと気づけますが、マンションやビルなどの建物だと取りに戻るのがすごく面倒ですよね。

そんなミスを二度とおかさないようにAlexaが教えてくれるシステムを作ってみました。

## 完成品
とりあえず見てもらえばわかりやすいと思います。
※音が流れるので注意
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">これは去年作った、出かけるときに雨が降っていたらAlexaが教えてくれるシステムです<br><br>※音が流れるので注意 <a href="https://t.co/pgB6oWJNT2">pic.twitter.com/pgB6oWJNT2</a></p>&mdash; やど (@giga_yadoran) <a href="https://twitter.com/giga_yadoran/status/1507995771311824896?ref_src=twsrc%5Etfw">March 27, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 使ったもの
- **[TWELITE PAL](https://mono-wireless.com/jp/products/twelite-pal/index.html)**: 電子工作不要ですぐに使える無線センサー。コイン電池で動くので配線も不要。
  - **OPEN-CLOSE SENSE PAL**: 開閉センサー
  - **MONOSTICK**: 受信機
- **Amazon Echo Dot (Alexa)**
- **[Yahoo 気象情報API](https://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/weather.html)**
- Windows 10 Pro + python 3.9.0

## システム概要図
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265114/8ca540f5-3881-cc98-afb1-2e629980b9db.png" width="500px"> 

## 動作の流れ
以下、サンプルコードは説明に必要な部分のみを抜粋して載せています。

### 1. 「出かけるとき」を検知する
OPEN-CLOSE SENSE PAL (開閉センサー) はセンサー部分に磁石が近づいた/遠ざかったことを検知してドアの開閉状態を検知するセンサーです。
「ドアを開けたとき」だと雨を知らせるタイミングとしては少し遅いかな？と思ったので、今回は「ドアにぶら下げているキーケースを取ったとき」を発火タイミングにしました。
普段使ってるキーケースに磁石を仕込んで、ドアにセンサーを貼り付けました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265114/38562b65-6bf1-8eff-c692-09c0d744e55d.png" width="400px"> 
※実際に使うときは動画のようにセンサーの上にキーケースを重ねています

センサーの状態検知はメーカー公式が配布しているサンプルスクリプト [TWELITE PAL Script](https://palscript.twelite.info/) をベースにpythonのアプリを構築しました。

```python:RainAlert.py
class RainAlert():
    def start(self):
        self.loopFlag = True
        isOpen = False
        while self.loopFlag:
            try:
                # データがあるかどうかの確認
                if self.PAL.ReadSensorData():
                    # センサーの状態を取得
                    status = self.PAL.ShowGateSensor()
                    if isOpen != status:
                        isOpen = status
                        if isOpen == True:
                            # 開いたときの処理
                            WeatherApi.live()
        del self.PAL

    def stop(self):
        self.loopFlag = False
```

`loopFlag` が `True` である限り常にセンサーの状態を監視して、開いた(キーケースを取った)瞬間に `WeatherApi.live()` を実行します。
`loopFlag` を `False` にする `stop()` の呼び出し部分についてはのちほど解説します。

### 2. 天気情報を取得する
[Yahoo 気象情報API](https://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/weather.html)を使いました。
この辺は好みもあると思います。(おすすめのAPIがある方はぜひ教えてください！)
現在位置の緯度・経度をパラメータとして与えると、以下のような形式でレスポンスが返却されます。


```json:レスポンスの一部抜粋
{
  "Feature": [
    {
      "Property": {
        "WeatherList": {
          "Weather": [
            {
              "Type": "observation",
              "Date": "202105052105",
              "Rainfall": 1.05
            },
            {
              "Type": "forecast",
              "Date": "202105052115",
              "Rainfall": 0.45
            },
            {
              "Type": "forecast",
              "Date": "202105052125",
              "Rainfall": 0
            }
            ...
}
```
Weather配列の中の `"Type": "observation"` が現在の天気なので、その二個下の `"Rainfall": 1.05` は「現在の降水量は1.05mm/h」ということになります。
その次以降の `"Type": "forecast"` となっているのは今後の予報で、上の例だと10分刻みになっています。
このレスポンスを元にAlexaにしゃべってもらいます。

```python:WeatherApi.py
def live():
    # レスポンスを取得
    url = "https://map.yahooapis.jp/weather/V1/place"
    params = {
        "coordinates": "139.732293,35.663613", # 東京都港区六本木付近の緯度・経度
        "appid": "xxxxx",
        "output": "json",
    }
    req = urllib.request.Request("{}?{}".format(url, urllib.parse.urlencode(params)))
    with urllib.request.urlopen(req) as res:
        body = json.load(res)

    # 天気データ配列
    weather_list = body['Feature'][0]['Property']['WeatherList']['Weather']

    if weather_list[0]['Rainfall'] > 0:
        # 現在雨が降っている場合
        run_alexa("現在、雨が降っています。傘をお忘れなく！")
    elif weather_list[1]['Rainfall'] > 0 or weather_list[2]['Rainfall'] > 0 :
        # 20分後までに雨が降る予報の場合
        run_alexa("もうすぐ雨が降りそうです。外の様子を見てみましょう！")
```

`run_alexa(message)` で `message` をAlexaがしゃべってくれるようにしています(後述)
現在雨が降っている場合・20分後までに雨が降る予報の場合で分岐して、それ以外はしゃべらないように実装しました。
この辺もいろいろカスタマイズできそうですね。

### 3. Alexaに教えてもらう
Alexaにしゃべってもらう方法はいくつかあるようですが、今回は [alexa-remote-control](https://github.com/thorsten-gehrig/alexa-remote-control) を使いました。
参考：[Alexa(Amazon Echo)をコマンドラインから自由に喋らせる方法 - Qiita](https://qiita.com/shge/items/169de61c8f246d26c110)

```python:WeatherApi.py
def run_alexa(message):
    os.system('./alexa_remote_control_plain.sh -e speak:' + message)
```
コマンドラインでシェルを実行しているだけですね。

### (その他)
このシステムを稼働しておくにはpythonのアプリケーションを立ち上げた状態でPCをつけっぱなしにしておく必要があります。
PCは基本的にいつも起動状態にしているので問題ないのですが、再起動するたびにアプリも立ち上げないといけないのは面倒なので、Windowsのスタートアップに登録しておきました。
ついでに、アプリをタスクトレイに表示して問題なく起動できていることを視覚的に確認できるようにし、タスクトレーから操作もできるようにしました。

[tkinterでタスクトレイに常駐させたいそこのあなた。諦めてwxPythonを使って幸せになろう](https://qiita.com/toriiico/items/0b26ef583e176eecce5d)
上記記事のコードをベースに作成させていただきました。感謝。
差分のみ記載します。

```python:gui.py
class TaskBarIcon(wx.adv.TaskBarIcon):

    # メニューリストの作成
    def CreatePopupMenu(self):
        menu = wx.Menu()
        create_menu_item(menu, 'Show Directory', self.show_directory)
        menu.AppendSeparator()
        create_menu_item(menu, 'Exit', self.on_exit)

        return menu

    # Exitを選択したときの処理
    def on_exit(self, event):
        print('exit')
        wx.CallAfter(self.Destroy)
        self.rain.stop() # センサー検知終了
        self.frame.Close()


class App(wx.App):

    # 初期実行処理
    def OnInit(self):
        loop = asyncio.get_event_loop()

        frame=wx.Frame(None)
        self.SetTopWindow(frame)

        rain = RainAlert() 

        TaskBarIcon(frame, rain) # タスクトレイにアイコンを表示
        loop.run_in_executor(None, rain.start) # センサー検知開始

        return True
```
`loop.run_in_executor(None, rain.start)` で `RainAlert.py` のセンサー検知処理を開始しています。
アイコンを右クリックして `Exit` を選択すると `RainAlert.py` の `stop()` を呼び出してセンサー検知を終了し、タスクトレイの表示も消すようにしています。
ここはもっと他の機能も充実させていく予定です。

![icon1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265114/ff819b72-a958-6171-0fdf-d79ff68c859f.png)  ![icon2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265114/44bb5c6e-f78c-cab8-9ea9-99ee0bce6e47.png)
↑ 実際の表示 (傘アイコンにしてみました)

## センサーの電池寿命について
[メーカー公式](https://mono-wireless.com/jp/products/twelite-pal/sense/openclose-pal.html)によるとコイン電池で約4年持つとのこと。

> 電池寿命
> 1日に200回の開閉を行なった場合、約4年です。

調査段階では電池式のものは数か月で寿命が尽きるものが多かったので、4年持つのはかなり嬉しい。

## 今回は実装を見送った機能など
- **PCの常時起動不要で動作するシステム**  
Raspberry Piを使ってサーバーを立てたりすれば実現できそうですが、労力的に今回は見送りに。
- **Alexaと追加で会話する機能**  
  例えば「2時間後は降ってる？」と聞いたりとか。これは割と簡単にできそうなのでやりたい。
- **[Echo Show](https://www.amazon.co.jp/dp/B07KD87NCM/ref=cm_sw_r_tw_dp_MX1PNZF7HXP6C8KAQ6Z6) の画面に情報表示したり操作したり**  
いわゆるスマートディスプレイを活用するやつ。  
めちゃくちゃ便利そうで夢は広がるけど、今回実現したかったことから逸れそうなので一旦見送り。

## まとめ
PCを常時起動しないといけないこと以外は割と満足いく出来になりました。
あとAlexa便利ですね。いろいろとやりたいことが浮かんできて楽しいです。

## その他参考にしたサイト
- [TWELITE OPEN-CLOSE SENSE PALを使って窓の鍵開閉を検知してみた - Qiita](https://qiita.com/K-1/items/b8bcf28611fae932abd9)
- [TWELITE PALを使って窓の施錠確認システムを電子工作なしで作る - じーろぐ](https://zlog.hateblo.jp/entry/2019/08/18/Twelite-pal-window-monitor)
- [橋本商会 » Node.jsでYahooから天気予報とリアルタイム降雨データを取得する](http://shokai.org/blog/archives/10542)
- [Python の HTTP クライアントは urllib.request で十分 - Qiita](https://qiita.com/hoto17296/items/8fcf55cc6cd823a18217)