slackで1日の天気予報を通知してみた。

この記事は「オフィス上空の天気予報を通知してみた。」シリーズの第4回です。

「オフィス上空の天気予報を通知してみた。」シリーズ(全4回)

はじめに

前回から、出勤前にオフィス周辺の天気を確認できる機能の実装を行っています。

前回の記事、「OpenWeatherMapで天気予報を取得。」ではOpenWeatherMapの説明をしました。

今回は実際にOpenWeatherMapを使ってslackに1日の天気予報を通知してくれる機能を追加していきます。

構成

AWS Lambda上で今まで降雨情報の取得を行っていた関数(test_rain)とは別に、1日の天気予報を取得し、通知するための関数(test_weather_day)を作ります。

1日の天気予報(test_weather_day)のフロー

  1. 営業日の朝8時にOpenWeatherMapから3時間ごとの天気予報を取得

  2. 通知メッセージの作成

  3. slackに1日の天気予報を送信

  4. タイムスタンプ情報の更新

  5. S3に更新したタイムスタンプ情報を保存

  6. 不要なメッセージの削除

現在の降雨情報(test_rain)のフロー

  1. 営業時間中、10分毎にYOLP(Yahoo! Open Local Platform)の気象情報APIからTOWNのオフィス上空の降雨情報を取得

  2. S3からログファイルを読み込む

  3. 通知メッセージの作成とログファイルのアップデート

  4. slackへの降雨情報メッセージの送信or更新(朝9時のみ送信)

  5. タイムスタンプ情報の更新(朝9時のみ)

  6. S3にログファイルを保存

  7. 不要なメッセージの削除

仕様

  • 朝に1日の天気予報を通知
  • 3時間毎の天気、最高気温、最低気温を表示
  • 雨が降る可能性がある場合はメンションをつける
  • slackへのメッセージ送信時に、不要なメッセージを削除
  • 常に今日の天気予報が上に、現在の降雨情報が下に表示されるようにする

S3の準備

今回はS3に1日の天気予報と現在の降雨情報のタイムスタンプを記録しておくファイルを用意しました。これは不要なメッセージを削除するときに使います。このファイルに記録されているタイムスタンプを持つメッセージのみを残し、その他のメッセージは全て削除するようにします。

ts.json

{
    "Now_ts": "1545963845.003600",
    "Day_ts": "1551330916.000200"
}

準備するもの

  • OpenWeatherMapのAPIキー
  • 降雨情報を取得したい場所の緯度・経度
  • slack appとToken
  • 投稿するchannelのID
  • AWSのアクセスキー、シークレットアクセスキー
  • Amazon S3のバケット
  • タイムスタンプを記録するファイル:”ts.json”

これらの取得方法がわからない場合はこのシリーズの前の記事を参照してください。

コード

1日の天気予報(test_weather_day)

import json
import datetime
import requests
import boto3
from slackclient import SlackClient

### OpenWeatherMap ###
API_KEY = 'XXXXXXXXXX'
LAT = 'XXXXXXXXXX'
LON = 'XXXXXXXXXX'
API_URL = 'http://api.openweathermap.org/data/2.5/forecast?lat={0}&lon={1}&units=metric&lang=ja&APP\
ID={2}'

### SLACK ###
slack_token = "XXXXXXXXXX"
slack_client = SlackClient(slack_token)

### channel_id for 降雨情報 ###
channel_id="XXXXXXXXXX"

### AWS ###
accesskey = "XXXXXXXXXX"
secretkey = "XXXXXXXXXX"
region = "ap-northeast-1"
s3_client = boto3.client('s3',region_name=region)
file_name = "ts.json"
bucket_name = "weather-notification-log"


def lambda_handler(event, context):
    temp_max = None
    temp_min = None
    message = ""
    mention = ""
    rain = 0
    test_item = ""    
    today = datetime.datetime.now().strftime('%Y-%m-%d')
    date_text = datetime.datetime.now().strftime("%-m/%-d")   

    ### 天気予報取得 ###
    url = API_URL.format(LAT, LON, API_KEY)
    forecastData = requests.get(url).json()

    ### タイムスタンプ情報の読み込み ###
    ts_log = json.loads(s3_client.get_object(Bucket=bucket_name,Key=file_name)["Body"].read())
    ts_now = ts_log["Now_ts"]

    for item in forecastData['list']:                                                                                                 
        forecastDate = item["dt_txt"].split()[0]
        forecastTime = item["dt_txt"].split()[1].split(":")[0].rjust(2)

        if(today == forecastDate and 9 <= int(forecastTime) <= 18):
            weatherId = item["weather"][0]["id"]
            temperature = item['main']['temp']

            if 'rain' in item and '3h' in item['rain']:
                rain = 1

            ### 最高・最低気温の算出 ###
            if(temp_max == None):
                temp_max = temperature
            else:
                if(temperature >= temp_max):
                    temp_max = temperature
            if(temp_min == None):
                temp_min = temperature
            else:
                if(temperature <= temp_min):
                    temp_min = temperature

            ### 天気予報メッセージの作成 ###
            message = make_message(message, weatherId, forecastTime)

    ### 曜日の取得 ###
    weekday = get_weekday(today)

    if(rain):
        mention = "<!channel> \n"

    ### メッセージ全文 ###
    text = (mention + "本日の天気予報をお知らせします。\n" + date + "(" + weekday + ")" + "    " + "最低気温:" + str(temp_min) + "℃" + "/" + "最高気温:" + str(temp_max) + "℃" + message)

    ### Slackにメッセージを送信 ###
    slack_response = send_message(slack_client, channel_id, text)

    ### タイムスタンプ情報を更新 ###
    ts_log = json.dumps(update_ts_log(ts_log, slack_response),indent=4)
    s3_response = send_s3(file_name,ts_log)

    ts_day = slack_response["ts"]

    ### 降雨情報以外のメッセージを削除 ###
    for history in get_history(slack_client,channel_id)["messages"]:
        ts=history["ts"]
        if (ts != ts_now and ts != ts_day):
            response=delete_message(slack_client,channel_id,str(ts))    

    return slack_response


def make_message(message, weatherId, forecastTime):

    message += "\n" + forecastTime + "時 : "

    if(weatherId == 800):
        message += ":sunny:"
    elif(801 <= weatherId <= 804):
        message += ":cloud:"
    elif(weatherId == 500):
        message += ":closed_umbrella:"
    elif(weatherId >= 501):
        message += ":umbrella_with_rain_drops:"
    elif(600 <= weatherId <700):
        message += ":snowman:"
    elif(200 <= weatherId < 300):
        message += ":thunder_cloud_and_rain:"
    elif(300 <= weatherId < 400):
        message += ":umbrella_with_rain_drops:"
    elif(700 <= weatherId <800)

    return message


def channel_list(client):
    response = client.api_call("channels.list")
    if channels['ok']:
        return channels['channels']
    else:
        return response


def send_message(client, channel_id, message):
    response = client.api_call(
        "chat.postMessage",
        channel=channel_id,
        username="Weather",
        text=message,
        icon_emoji=":rainbow:"
    )
    return response


def update_message(client, channel_id, message,ts):
    response = client.api_call(
        "chat.update",
        channel=channel_id,
        text=message,
        ts=ts
    )
    return response


def delete_message(client,channel_id,ts):
    response = client.api_call(
        "chat.delete",
        channel=channel_id,
        ts=ts
    )
    return response


def get_history(client,channel_id):
    response = client.api_call(
        "channels.history",
        channel=channel_id,
    )
    return response


def send_s3(filename,body):

    response = s3_client.put_object(
        Bucket=bucket_name,
        Key=filename,
        Body=body
    )

    return response


def get_weekday(today):

    week_list = ["月","火","水","木","金","土","日"]
    d = datetime.datetime.strptime(today, "%Y-%m-%d")

    return week_list[d.weekday()]


def update_ts_log(ts_log, response):

    ts_log["Day_ts"] = response["ts"]

    return ts_log

現在の降雨情報(test_rain)(変更箇所のみ抜粋)

def lambda_handler(event, context):

    today = datetime.datetime.now().strftime("%Y%m%d")
    post_time = today + "0850"

    ### タイムスタンプ情報の読み込み ###
    ts_log = json.loads(s3_client.get_object(Bucket=bucket_name,Key=file_ts)["Body"].read())
    ts_now = ts_log["Now_ts"]
    ts_day = ts_log["Day_ts"]

〜〜〜〜〜〜〜〜〜中略〜〜〜〜〜〜〜〜〜

    ### Slackにメッセージを送信 ###
    if(post_time == now_time):
        slack_response = send_message(slack_client, channel_id, message)
        ### タイムスタンプ情報を更新 ###
        ts_log = json.dumps(update_ts_log(ts_log, slack_response),indent=4)
        ts_now = slack_response["ts"]
        send_s3(file_ts,ts_log)
    else:
        slack_response = update_message(slack_client, channel_id, message, ts_now)

    ### S3にファイルをアップロード ###
    s3_response_analyse = send_s3(file_analyse,body_analyse)
    s3_response_log = send_s3(file_log,body_log)

    ### 降雨情報以外のメッセージを削除 ###
    for history in get_history(slack_client,channel_id)["messages"]:
        ts=history["ts"]
        if (ts != ts_now and ts != ts_day):
            response=delete_message(slack_client,channel_id,str(ts))

    return slack_response

〜〜〜〜〜〜〜〜〜中略〜〜〜〜〜〜〜〜〜

def update_ts_log(ts_log, response):

    ts_log["Now_ts"] = response["ts"]

    return ts_log

ちなみに…

test_weather_day関数では今日の予報だけがほしいのでdatetimeを使って日付を取得しているのですが、最初は望むような動作をしてくれませんでした。調べてみたら原因はdatetimeの変数宣言の場所とタイムゾーンでした。という話を以下の記事に書いているので同じところで躓いている場合は以下の記事を参考にしてみてください。

トリガーの設定

今回は営業日である平日の毎朝8時に実行するようにしたいのでCloudwatchEventsをトリガーにしてtest_weather_day関数を実行するようにスケジュール式を設定します。トリガーの設定方法については以下の記事を参考にしてください。

Lambdaで夜間にec2インスタンスを止める

  • cron式
cron(0 23 ? * SUN-THU *)

cron式が思い通りの動作をしてくれない場合は式がUTC時刻で正しく記述されているかを確認してみてください。私がcron式のUTC時刻で躓いた話を下の記事で紹介しています。

cron式の曜日指定でUTC時刻を考慮していなかった…。

動作確認

営業日の毎朝8時に以下のようにメッセージが送信されるようになりました。

さいごに

これで出勤前にオフィス周辺の天気を確認できるようになりました。雨の日はメンションもつけてくれるので傘を忘れる心配も減りますね。

「オフィス上空の天気予報を通知してみた。」シリーズ(全4回)