15m notification util
This commit is contained in:
124
python/utils/interval_timer_ding.py
Normal file
124
python/utils/interval_timer_ding.py
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Plays a ding/timer sound at regular intervals aligned to round period marks
|
||||
(e.g. every 15 min), with an optional offset so the sound plays a few seconds
|
||||
before each mark.
|
||||
|
||||
Default: period 15 minutes, offset 15 seconds.
|
||||
Notification plays at :14:45, :29:45, :44:45, :59:45.
|
||||
|
||||
Works on Windows and Linux (stdlib + optional system players on Linux).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def _seconds_since_midnight(dt: datetime) -> int:
|
||||
return dt.hour * 3600 + dt.minute * 60 + dt.second
|
||||
|
||||
|
||||
def _next_ring_seconds(period_sec: int, offset_sec: int, now_sec: int) -> int:
|
||||
next_boundary = math.ceil(now_sec / period_sec) * period_sec
|
||||
ring_sec = next_boundary - offset_sec
|
||||
if ring_sec <= now_sec:
|
||||
ring_sec += period_sec
|
||||
return ring_sec
|
||||
|
||||
|
||||
def _wait_seconds(period_sec: int, offset_sec: int) -> int:
|
||||
now = datetime.now()
|
||||
now_sec = _seconds_since_midnight(now)
|
||||
ring_sec = _next_ring_seconds(period_sec, offset_sec, now_sec)
|
||||
wait = ring_sec - now_sec
|
||||
return max(1, wait) if wait > 0 else 1
|
||||
|
||||
|
||||
def _play_ding_windows() -> None:
|
||||
try:
|
||||
import winsound
|
||||
winsound.MessageBeep(winsound.MB_ICONASTERISK)
|
||||
except Exception:
|
||||
try:
|
||||
import winsound
|
||||
winsound.Beep(800, 200)
|
||||
except Exception:
|
||||
sys.stdout.write("\a")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _play_ding_linux() -> None:
|
||||
candidates = [
|
||||
["paplay", "/usr/share/sounds/freedesktop/stereo/bell.oga"],
|
||||
["paplay", "/usr/share/sounds/freedesktop/stereo/message.oga"],
|
||||
["aplay", "/usr/share/sounds/alsa/Front_Center.wav"],
|
||||
["aplay", "/usr/share/sounds/speech-dispatcher/test.wav"],
|
||||
]
|
||||
for cmd in candidates:
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, timeout=2)
|
||||
return
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
||||
continue
|
||||
sys.stdout.write("\a")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def play_ding() -> None:
|
||||
if sys.platform == "win32":
|
||||
_play_ding_windows()
|
||||
else:
|
||||
_play_ding_linux()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Play a ding every N minutes, offset seconds before each round mark."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p", "--period",
|
||||
type=int,
|
||||
default=15,
|
||||
metavar="MIN",
|
||||
help="Interval in minutes between notifications (default: 15)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o", "--offset",
|
||||
type=int,
|
||||
default=15,
|
||||
metavar="SEC",
|
||||
help="Seconds before each round mark to play (default: 15)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.period < 1:
|
||||
parser.error("period must be >= 1")
|
||||
if args.offset < 0:
|
||||
parser.error("offset must be >= 0")
|
||||
|
||||
period_sec = args.period * 60
|
||||
now = datetime.now()
|
||||
now_sec = _seconds_since_midnight(now)
|
||||
next_ring = _next_ring_seconds(period_sec, args.offset, now_sec)
|
||||
h = next_ring // 3600
|
||||
m = (next_ring % 3600) // 60
|
||||
s = next_ring % 60
|
||||
print(
|
||||
f"Period: {args.period} min, offset: {args.offset} s. "
|
||||
f"Next ding at {h:02d}:{m:02d}:{s:02d} (then every {args.period} min). "
|
||||
"Press Ctrl+C to stop."
|
||||
)
|
||||
|
||||
while True:
|
||||
wait_sec = _wait_seconds(period_sec, args.offset)
|
||||
time.sleep(wait_sec)
|
||||
play_ding()
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] ding")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user