日の入りで指定のコマンドを実行するPythonスクリプト


日の入りのタイミングで夕焼け小焼けを再生したかったが、
動的に crontab を書き換えるのも気持ち悪く、at コマンドも使えなかったので、
かわりにPythonで実装した記録。

やりたいこと

runat.py sunset play -q yuyakekoyake.mp3
とすると、日の入りのタイミングで play -q yuyakekoyake.mp3 を実行する。
あとは、このコマンドをcrontabに正午くらいにでも放り込んでおけばよい。

つまずき・実装のポイント

argparse でコマンドを受け取る

引数解析はもちろん argparse にやらせるが、前述の例だと -q もオプション引数と認識されてしまい、unrecognized arguments エラーが出る。
これを回避するには、コマンドを受け取る add_argument メソッドに nargs = argparse.REMAINDER を指定する。
cf. https://docs.python.org/ja/3.7/library/argparse.html#argparse-remainder

日の出の時刻の計算

ephem モジュールにお任せしました。
https://pypi.org/project/ephem/

外部コマンドの実行

Pythonに渡された時点でダブルクォート等の空白処理はなされているので、
絶対に os.system(' '.join(args.command)) なんてことはしないこと!
subprocess.run 関数にリストを渡せるので、これを使いましょう。

以下、作ったスクリプト

runat.py
#!/usr/bin/env python
#encoding : utf-8

import sys
import argparse
import logging
import datetime
import time
import subprocess

try:
    import ephem
except ModuleNotFoundError:
    sys.exit (1)

class choice:
    def __init__ (self, star = None, attr = None):
        self.star = star
        self.attr = attr

CHOICES = {
        'sunrise' : choice (star = ephem.Sun,  attr = 'next_rising' ),
        'sunset'  : choice (star = ephem.Sun,  attr = 'next_setting'),
        'moonrise': choice (star = ephem.Moon, attr = 'next_rising' ),
        'moonset' : choice (star = ephem.Moon, attr = 'next_setting'),
        }

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument ('--city', default = 'Tokyo')
    parser.add_argument ('--longitude')
    parser.add_argument ('--latitude')
    parser.add_argument ('--elevation', type = int)
    parser.add_argument ('at', choices = CHOICES.keys() )
    parser.add_argument ('command', nargs = argparse.REMAINDER)
    args = parser.parse_args()
    logging.debug ('args = %s' % args)

    place = ephem.city (args.city)
    if args.latitude is not None:
        place.lat = args.latitude
    if args.longitude is not None:
        place.lon = args.longitude
    if args.elevation is not None:
        place.elevation = args.elevation

    logging.debug ('place = %s' % place)

    star = CHOICES[args.at].star()
    timeat = getattr(place, CHOICES[args.at].attr) (star)
    delta = timeat.datetime() - datetime.datetime.utcnow()

    logging.debug ('going at %s' % ephem.localtime(timeat))

    try:
        time.sleep (delta.seconds)
        subprocess.run (args.command)
    except KeyboardInterrupt:
        sys.exit(1)

if __name__ == '__main__':
    main()