とあるお兄さんの雑記

基本的に技術系の内容を書きますが、何を書くかは私の気分です。

LINE Notifyでゴミ出しを通知する

明日ってゴミ出す日だっけ?、不燃ごみっていつだっけ?などいちいち確認がめんどくさく思い、LINEで、明日がゴミを出す日か、出すとしたら何のゴミかを通知するだけのものを作りました。

環境

macOS Big Sur 
Python 3.9.6
pytest 6.2.2
requests 2.25.1
LINE Notify
Heroku

PythonでLINE Notifyで通知

今回はただメッセージを送るだけなので、LINE Notifyで通知をしたいと思います。

参考にした記事は以下です。

qiita.com

アクセストークンを取得するため、公式サイトから自分のLINEアカウントでログインし、マイページ > トークンを発行するでアクセストークンを取得します(下記画像)。発行したアクセストークンはどこかに保存しておきましょう。

f:id:kurasher:20210821115612p:plain

ソースコード概要

自分の用意したファイル構成は以下のような感じです。dockerとかを用意していますが、結局使いませんでした。何となく、自分のローカルで試してみたかっただけなので、docker配下の説明は省きます。

NotificationGomidashi/
├── Application
│   ├── __init__.py
│   ├── entity
│   │   ├── __init__.py
│   │   ├── calendar_entity.py
│   │   └── garbage_removal_entity.py
│   ├── main.py
│   └── service
│       ├── __init__.py
│       └── notify_service.py
├── Procfile
├── docker
│   ├── Dockerfile
│   └──  docker-compose.yml
├── requirements.txt
├── runtime.txt
└── tests
    ├── __init__.py
    ├── entity
    │   ├── __init__.py
    │   ├── test_Calendar.py
    │   └── test_garbage_removal.py
    └── service
        └── __init__.py

main.pyについて

#coding:UTF-8

from service.notify_service import NotifyService as sv

import requests

def main(message):
    url = "https://notify-api.line.me/api/notify"
    token = "YOUR ACCESS TOKEN" 
    headers = {"Authorization" : "Bearer "+ token}

    payload = {"message" :  message}

    r = requests.post(url ,headers = headers ,params=payload)

if __name__ == '__main__':
    sv = sv()
    n_th_week, week_day = sv.get_time()
    message = '\n明日は、第' + str(n_th_week) + week_day + '曜日\n' + sv.get_judge_case(n_th_week, week_day)

    main(message)

token部分で先ほど発行したアクセストークンを貼り付けます。sv.get_time()で明日がその月の第何週目か、何曜日かを取得してきてくれます。

notify_service.pyについて

from entity.calendar_entity import CalendarEntity as cal
from entity.garbage_removal_entity import GarbageRemovalEntity as remo

class NotifyService(object):
    # 現在日時を取得
    def get_time(self):
        date = cal().get_now_time() 

        week_day = self.get_week_day(date)
        n_th_week = self.get_n_th_week(date.day)

        return n_th_week, week_day

    # 曜日の取得
    def get_week_day(self, date):
        return cal().get_week_day(date)


    # 月の第n週目を取得
    def get_n_th_week(self, day):
        return cal().get_nth_week(day)

    def get_judge_case(self, n_th_week, week_day):
        return remo().judge_case(n_th_week, week_day)

notify_service.pyentityから必要な処理を呼び出し、値を受け取るだけの仕事を行なっています。データの加工などについてはentity側だけの仕事にするようにしました。

calendar_entity.pyについて

from datetime import datetime, timedelta, timezone
import calendar

class CalendarEntity(object):
    def get_now_time(self):
        # タイムゾーンの生成
        JST = timezone(timedelta(hours=+9), 'JST')
        
        # 現在日時
        today = datetime.now(JST)

        # 次の日時
        tomorrow = today + timedelta(days=1)

        return tomorrow

    # 月の第何週目かを返却
    def get_nth_week(self, day):
        return (day - 1) // 7 + 1


    def get_nth_dow(self):
        date = get_now_time()

        # 曜日(数値)を取得
        week_day_number = calendar.weekday(date.year, date.month, date.day)

        return get_nth_week(date.day), get_week_day(week_day_number)


    def get_week_day(self, date):
        week_day = ['月', '火', '水', '木', '金', '土', '日']
        week_day_number = calendar.weekday(date.year, date.month, date.day)

        return week_day[week_day_number]

calendar_entity.pyで明日が何週目か、何曜日かということを処理し、値を返しています。

garbage_removal_entity.pyについて

class GarbageRemovalEntity(object):
    def judge_case(self, n_th_week, week_day):
        garbage = ['可燃ごみ', '資源ごみ', '不燃ごみ', 'ゴミ出しなし']

        if week_day == '水' or week_day == '土':
            return garbage[0]
        elif week_day == '火' :
            return garbage[1]
        elif (n_th_week == 2 or n_th_week == 4) and week_day == '月':
            return garbage[2]
        else:
            return garbage[3]

garbage_removal_entity.pyでserviceからjudge_caseのメソッドが呼び出された時にどのゴミを出すか、はたまた出さないのかという処理を行なっています。

Herokuにデプロイ

今回は作ったものをHerokuにデプロイし、定期的に実行するようにします。参考にした記事は以下です。

qiita.com

ログイン

まずはHerokuにログインしますが、HerokuCLIがインストールされていなければ、HerokuCLIをインストールしましょう。

Herokuでアカウントを作れば、brewコマンドを使ってHerokuCLIをインストールする方法が出てくるはずです。

heroku login

Herokuへログインできたら、

heroku create アプリ名

でアプリを作ります。私は

heroku create garbage-notify

としました。

デプロイ前の準備

Herokuへデプロイする前にrequirements.txtに必要なモジュールを、ProcfileにHerokuサーバーで実行する方法を、runtime.txtpythonのバージョンを書いておきます。

requirements.txt

requests

Procfile

# Procfile
web: python Application/main.py

runtime.txt

python-3.9.6

Herokuへデプロイ

次にgitを使ってHerokuへデプロイしていきます。

git add .
git commit -m "make it better"
git push heroku main

上記コマンドを上から順に実行していってください。最後のgit push heroku mainで人によってはgit push heroku masterなどの可能性もあるので、どのブランチをHerokuにプッシュしたいかをしっかり把握しておいてください。

Herokuで動作確認

プッシュが終わったら、動作確認をしてみましょう。

 heroku run python Application/main.py

として、LINE Notifyから通知メッセージが来ればOKです。

定期実行の設定

Herokuから定期的に実行してもらうために、Herokuにアドオンを追加します。この時、アドオンを使用するため、クレジットカードの登録が必要です。登録したところで勝手に有料プランになったり、チャージされることもありません。

クレジットカードを登録したら、ターミナルから

heroku addons:add scheduler:standard

と打ちます。そのあとはHerokuに移動し、下記画像のHeroku Schedulerを開きます。

f:id:kurasher:20210821123356p:plain

ここで、Add new Job を押下し、設定を行ってSaveを押せば定期実行になります。ここで、時間はUTCなので、もし日本時間で22時に実行して欲しい場合は、UTCでPM1時を設定します。

これで毎日夜の22時に明日は何のゴミを出すかを通知してくるアプリができました。

おまけ:pytestでテストする

以前業務でpytestを使っており、テストを書かないと和田卓人さんに怒られるので、せっかくなら使ってみようと思い、テストを書いてみました。

test_Calendar.pyについて

import pytest
from datetime import datetime, timedelta, timezone

from Application.entity.calendar_entity import CalendarEntity

class TestCalendar(object):
    @classmethod
    def setup_class(cls):
        print('Test start')
        cls.entity = CalendarEntity()

    @classmethod
    def teardown_class(cls):
        print('Test finish')
        del cls.entity

    def setup_method(self, method):
        print('method={}'.format(method.__name__))

    def teardown_method(self, method):
        print('method={}'.format(method.__name__))


    def test_get_n_th_week(self):
        date = datetime(2021, 8, 1)
        actual = self.entity.get_nth_week(date.day)

        assert actual == 1


    def test_get_week_day(self):
        date = datetime(2021, 8, 1)
        actual = self.entity.get_week_day(date)

        assert actual == '日'

    def test_get_now_time(self):
        # タイムゾーンの生成
        JST = timezone(timedelta(hours=+9), 'JST')
        
        # 現在日時
        today = datetime.now(JST)

        actualDate = self.entity.get_now_time()

        assert today < actualDate

大したことはやっていません。

test_get_n_th_weekで指定した日の週がその月の第何週目なのかを、
test_get_week_dayで指定した日の週の曜日が合っているかを、
test_get_now_timeでちゃんと明日の日付が取れているかを確認しています。

test_garbage_removal.pyについて

import pytest
from Application.entity.garbage_removal_entity import GarbageRemovalEntity 

class TestGarbageRemoval(object):
    @classmethod
    def setup_class(cls):
        print('Test start')
        cls.entity = GarbageRemovalEntity()

    @classmethod
    def teardown_class(cls):
        print('Test finish')
        del cls.entity

    def setup_method(self, method):
        print('method={}'.format(method.__name__))

    def teardown_method(self, method):
        print('method={}'.format(method.__name__))

    def test_judge_case(self):
        actual1 = self.entity.judge_case(1, '水')
        actual2 = self.entity.judge_case(4, '土')
        actual3 = self.entity.judge_case(2, '火')
        actual4 = self.entity.judge_case(3, '火')
        actual5 = self.entity.judge_case(2, '月')
        actual6 = self.entity.judge_case(4, '月')
        actual7 = self.entity.judge_case(2, '木')
        actual8 = self.entity.judge_case(4, '日')

        assert actual1 == '可燃ごみ'
        assert actual2 == '可燃ごみ'
        assert actual3 == '資源ごみ'
        assert actual4 == '資源ごみ'
        assert actual5 == '不燃ごみ'
        assert actual6 == '不燃ごみ'
        assert actual7 == 'ゴミ出しなし'
        assert actual8 == 'ゴミ出しなし'

こちらも大したことはやっていません。月の何週目かと曜日を与えて、帰ってくる値が正しいかを判定しています。

実行方法はNotificationGomidashiのところで、

pytest

と打てば、tests配下のテストをしてくれます。

参考記事

pythonでLINENotify使ってみる - Qiita

Herokuでお天気Pythonの定期実行 - Qiita