毎朝 LINE に天気を通知するアプリを作成

## はじめに

2 歳の子がいるので、毎朝 E テレを観てるのですが、E テレでは天気が出ません
天気を確認するためだけにニュース番組にチャンネルを変えたり戻したりしてるので、天気 API から天気を取得して LINE に Push する簡単なアプリを作ろうと思います
年末の暇つぶしです
もちろん無料の範囲内で

## 構成

<img width="600" alt=" 2019-12-26 14.41.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168197/55afb8e5-6e02-451e-72c9-90f65b563f8d.png">

GitHub Actions を使ったことがなかったので、試しに使ってます
ただ、cron 機能しか使ってないので、これだけであれば GAS のトリガー機能とかの方が簡単で良いと思います

## LINE の Developer アカウントの作成及び、アプリの登録

こちらから作成
https://developers.line.biz/ja/services/messaging-api

また、LINE Developer の管理画面に登録したアプリの QR コードがあるので、お友達登録しておく

## 天気 API のアカウント作成

OpenWeatherMap の API を使わせてもらいます

こちらからアカウント作成
https://home.openweathermap.org/users/sign_up

## 天気を取得して、LINE に Push するコード

```python
import os
import json
import pandas as pd
from dotenv import load_dotenv
from urllib.request import urlopen
from datetime import datetime
from pytz import timezone
from linebot import LineBotApi
from linebot.models import TextSendMessage


def get_weather_icon(icon_str):
    if icon_str == "01d" or icon_str == "01n":
        return "☀️"
    elif (
        icon_str == "02d"
        or icon_str == "02n"
        or icon_str == "03d"
        or icon_str == "03n"
        or icon_str == "04d"
        or icon_str == "04n"
    ):
        return "☁️"
    elif (
        icon_str == "09d" or icon_str == "09n" or icon_str == "10d" or icon_str == "10n"
    ):
        return "☂️"
    elif icon_str == "11d" or icon_str == "11n":
        return "⚡️"
    elif icon_str == "13d" or icon_str == "13n":
        return "☃️"
    else:
        return ""


def send_to_line(df):
    texts = []
    for k, v in df:
        texts.append(f"【{k}】")
        for _, d in v.iterrows():
            texts.append(
                f"{d['time']}時 {get_weather_icon(d['icon'])} {d['temp']}(℃) {d['rain']}(mm/3h)"
            )
        texts.append("")

    line_bot = LineBotApi(os.getenv("LINE_ACCESS_TOKEN"))
    line_bot.push_message(
        os.getenv("LINE_USER_ID"), TextSendMessage(text="\n".join(texts))
    )


def main():
    url = "http://api.openweathermap.org/data/2.5/forecast"
    id = os.getenv("OWM_PLACE_ID")
    api_key = os.getenv("OWM_API_KEY")

    res = urlopen(f"{url}?id={id}&appid={api_key}&lang=ja&units=metric").read()
    res_json = json.loads(res)

    arr_rj = []
    for rj in res_json["list"]:
        conv_rj = {}
        timestamp = timezone("Asia/Tokyo").localize(datetime.fromtimestamp(rj["dt"]))
        conv_rj["date"] = timestamp.strftime("%m/%d %a")
        conv_rj["time"] = timestamp.strftime("%H")
        conv_rj["description"] = rj["weather"][0]["description"]
        conv_rj["icon"] = rj["weather"][0]["icon"]
        conv_rj["temp"] = round(rj["main"]["temp"])
        conv_rj["rain"] = round(rj["rain"]["3h"], 1) if "rain" in rj else 0
        arr_rj.append(conv_rj)

    send_to_line(pd.DataFrame(arr_rj).groupby("date"))


load_dotenv()
main()
```

上記のコードを実行すると、先程登録したアプリへ以下のように通知されます

<img width="245" alt=" 2019-12-25 15.30.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168197/47013b45-3125-7598-6a28-5d973c375706.png">

メッセージを加工している処理が多いので、コードだけ見るとそこそこ長いのですが、天気情報取得と LINE に Push する処理だけにすると以下になります
[python-dotenv](https://github.com/theskumar/python-dotenv)を使って、トークンなどのセキュアな情報は環境変数に設定しています

```python:OpenWeatherMapAPIから天気情報（JSON形式）を取得
def main():
    url = "http://api.openweathermap.org/data/2.5/forecast"
    id = os.getenv("OWM_PLACE_ID")
    api_key = os.getenv("OWM_API_KEY")

    res = urlopen(f"{url}?id={id}&appid={api_key}&lang=ja&units=metric").read()
    ...
```

今回は自分の住んでいる所だけの天気が分かれば良かったので、パラメータの`id`に直接住んでいる地域の ID を渡しています
地域 ID は[こちら](http://bulk.openweathermap.org/sample/)から`city.list.json.gz`をダウンロードして確認できます
`id`以外にも `lat`(緯度)、`lon`(経度)、`zip`(郵便番号)をパラメータに指定して結果を取得することも可能です（今回作ったアプリは対応していません）
API_KEY は[こちら](https://home.openweathermap.org/api_keys)から確認できます

```python:LINEにメッセージをPush
def send_to_line(df):
    ...
    line_bot = LineBotApi(os.getenv("LINE_ACCESS_TOKEN"))
    line_bot.push_message(
        os.getenv("LINE_USER_ID"), TextSendMessage(text="Hello")
```

LINE_ACCESS_TOKEN は LINE Developer の管理画面から発行できます
LINE_USER_ID も同管理画面から確認できます

### 複数人にメッセージを Push するやり方（2019/12/28 追記）

この記事を書いた時点では、作成した天気アプリをお友達登録した人全員にメッセージが通知されるって思い込んでたんですけど、上記のやり方だと自分にしかメッセージが通知されません
（すみません、その辺全然調べずに作ってました。。）

自分以外の人にも送る手順を書きます
自分以外の人に送るには送る人の UserID が分かれば良いのですが、知る方法が少し面倒です

UserID を知りたい人が`アプリをお友達登録した時`に Line からこちらが用意したコールバック関数に Post リクエストで送ってもらって、その関数の中で確認するという方法を取ります
色々やり方はあると思いますが、以下に自分がやったやり方を書きます

#### コールバック関数を用意

GAS で作りました

```js
var SLACK_ACCESS_TOKEN = ''

function doPost(e) {
  SlackApp.create(SLACK_ACCESS_TOKEN).postMessage(
    '#event-line',
    JSON.stringify(e),
    { username: 'kurosame-gas' }
  )
}
```

`console.log(e)`でも確認できますが、その都度 GAS 開いて確認するのは面倒なので、Slack に通知しています
`e`に UserID 等の情報が入っています

#### Webhook URL の取得

GAS の画面から`公開 -> ウェブ アプリケーションとして導入`から Web API として公開できます
この時公開範囲は`全員（匿名も含む）`にしてください
公開するとアプリの URL が画面上に出るので、控えておいてください

#### Webhook URL の設定

以下のように LINE Developer の管理画面で設定できます

<img width="349" alt=" 2019-12-28 23.14.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168197/b5ddbdf2-f7eb-1921-fbea-ad446fd64288.png">
Verifyを実行してSUCCESSが出ればオッケーです

ついでに、同画面で以下も Disabled に設定しておいてください

<img width="271" alt=" 2019-12-28 23.16.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168197/9646a8ec-e5e3-8505-5db7-df1e0b483175.png">

#### 動作確認

作成した Line アプリのトーク画面で文字を送って、その文字と自分の UserID 等の情報が Slack に届いていれば成功です
また、他者がアプリをお友達登録したら、Slack に通知されるので、その人の UserID をゲットできます

#### Python から LINE に Push するコードを変更

```python
# 変更前
line_bot.push_message(
    os.getenv("LINE_USER_ID"), TextSendMessage(text="\n".join(texts))
)

# 変更後
line_bot.multicast(
    os.getenv("LINE_USER_ID").split(","), TextSendMessage(text="\n".join(texts))
)
```

LINE_USER_ID にはカンマ区切りで UserID を設定しておきます

## 定期的に実行する

GitHub Actions の cron 機能を使って、毎朝 7 時に天気が通知されるように設定します
以下は Actions で読み込む yaml ファイルです
以下のファイルをルートディレクトリ上で`.github/workflows`というディレクトリに入れておくだけで GitHub Actions が使えます

```yaml
name: Weather

on:
  schedule:
    - cron: '00 22 * * *' # UTC

jobs:
  weather:
    name: Run weather
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@master
      - name: Setup Python
        uses: actions/setup-python@v1
        with:
          python-version: 3.7
      - name: Install Pipenv
        uses: dschep/install-pipenv-action@v1
      - name: Install dependencies
        run: pipenv sync
      - name: Run weather
        env:
          OWM_PLACE_ID: ${{ secrets.OWM_PLACE_ID }}
          OWM_API_KEY: ${{ secrets.OWM_API_KEY }}
          LINE_ACCESS_TOKEN: ${{ secrets.LINE_ACCESS_TOKEN }}
          LINE_USER_ID: ${{ secrets.LINE_USER_ID }}
        run: pipenv run python3 ./bots/weather.py
```

Python コードの中で環境変数を使っているので、Actions のサーバー上で環境変数を使えるようにしてあげる必要があります
GitHub リポジトリの`Settingsタブ -> Secrets`から Actions で使える環境変数を設定できます
そして上記のように secrets コンテキストを利用してランナーが環境変数を使えるようにしています

使ってみて少し気になったのが、Workflow が実行されるタイミングが cron で指定した時間より 5 分〜6 分くらい遅いですね

## さいごに

LINE は妻も普段使いしているので、このアプリは使ってもらえそうです
（前に Slack 上で操作できるメモアプリを使った時は Slack を起動するのが手間になって使わなくなった）

今回実装したコードは以下にあげてます
https://github.com/kurosame/bots-python