Created
October 29, 2016 07:24
-
-
Save ssokolow/4df34619ba2134ca20163764532f211f to your computer and use it in GitHub Desktop.
Quick script for setting an alarm on Linux
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python2 | |
# -*- coding: utf-8 -*- | |
"""Wrapper script for scheduling an alarm within the desktop session so | |
MPV has access to the audio and video outputs. | |
See --help output for more details. | |
Requirements: | |
- python-dateutil | |
- pytimeparse | |
""" | |
from __future__ import (absolute_import, division, print_function, | |
with_statement, unicode_literals) | |
__author__ = "Stephan Sokolow (deitarion/SSokolow)" | |
__appname__ = "Alarm scheduler" | |
__version__ = "0.1" | |
__license__ = "MIT" | |
import subprocess, time | |
from contextlib import contextmanager | |
from datetime import datetime, timedelta | |
from dateutil.relativedelta import relativedelta | |
from dateutil.parser import parse as dateparse | |
from pytimeparse import parse as deltaparse | |
SAFETY_LIMIT = timedelta(hours=15) | |
ALARM_CMD = ['mpv', '-loop=inf', | |
'/mnt/incoming/Videos/Danger alarm Sound Effect-gTJPpzo4jJ4.mp4'] | |
MIXER_CMD = ['amixer', 'sset', 'Master'] | |
@contextmanager | |
def loud(): | |
"""Context manager to temporarily set master volume to 100%.""" | |
# TODO: Actually store and restore the old volume rather than assuming 50 | |
subprocess.call(MIXER_CMD + ['100']) | |
yield | |
subprocess.call(MIXER_CMD + ['50']) | |
def parse_in(timespec): | |
"""Parse timespecs like '5 hours'""" | |
return timedelta(seconds=deltaparse(timespec)) | |
def parse_at(timespec): | |
"""Parse timespecs like '5am'""" | |
now = datetime.now() | |
date = dateparse(timespec) | |
# One day of drift is expected since pydateparse assumes "today" for | |
# omitted components | |
if date < now: | |
date += relativedelta(days=+1) | |
# More than one day of drift means something funny is going on | |
if date < now: | |
raise Exception("Didn't expect date more than 24 hours in the past. " | |
"Aborting to ensure user doesn't accidentally " | |
"oversleep.") | |
# Switch from dates to deltas as quickly as possible because a timezone | |
# mistake is far more dangerous than even a minute of inaccuracy due to | |
# swap thrashing would be and Python's ecosystem seems designed to punish | |
# anyone who needs a way to operate in "the desktop's local time" rather | |
# than using UTC and MAYBE manually-configured timezones at the edges. | |
return date - datetime.now() | |
def pformat_tdelta(delta): | |
"""A simple pretty-printer for time deltas which doesn't require Arrow""" | |
result = [] | |
days, hours = delta.days, delta.seconds // 3600 | |
minutes = (delta.seconds % 3600) // 60 | |
seconds = delta.seconds % 60 | |
if days > 0: | |
result.append('%s days' % days) | |
if hours > 0: | |
result.append('%s hours' % hours) | |
if minutes > 0: | |
result.append('%s minutes' % minutes) | |
if seconds > 0: | |
result.append('%s seconds' % seconds) | |
return ', '.join(result) | |
def main(): | |
"""The main entry point, compatible with setuptools entry points.""" | |
# If we're running on Python 2, take responsibility for preventing | |
# output from causing UnicodeEncodeErrors. (Done here so it should only | |
# happen when not being imported by some other program.) | |
import sys | |
if sys.version_info.major < 3: | |
reload(sys) | |
sys.setdefaultencoding('utf-8') # pylint: disable=no-member | |
from argparse import ArgumentParser, RawTextHelpFormatter, REMAINDER | |
parser = ArgumentParser(formatter_class=RawTextHelpFormatter, | |
description=__doc__.replace('\r\n', '\n').split('\n--snip--\n')[0]) | |
parser.add_argument('--version', action='version', | |
version="%%(prog)s v%s" % __version__) | |
subparsers = parser.add_subparsers() | |
parser_in = subparsers.add_parser('in') | |
parser_in.set_defaults(parser=parse_in) | |
parser_in.add_argument('timespec_list', nargs=REMAINDER) | |
parser_at = subparsers.add_parser('at') | |
parser_at.set_defaults(parser=parse_at) | |
parser_at.add_argument('timespec_list', nargs=REMAINDER) | |
args = parser.parse_args() | |
delay = args.parser(' '.join(args.timespec_list)) | |
# Now, we've got a timedelta, regardless of what we started with | |
if delay > SAFETY_LIMIT: | |
raise Exception("Delay exceeds safety limit (bad input?): %s" % | |
pformat_tdelta(delay)) | |
print("Sleeping for %s" % pformat_tdelta(delay)) | |
time.sleep(delay.total_seconds()) | |
# FIXME: Wrap this up in something which handles being interrupted by a | |
# signal | |
with loud(): | |
subprocess.call(ALARM_CMD) | |
if __name__ == '__main__': | |
main() | |
# vim: set sw=4 sts=4 expandtab : |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment