15m notification util
This commit is contained in:
1
python/utils/__init__.py
Normal file
1
python/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Utils package
|
||||||
77
python/utils/interval-timer-ding.ps1
Normal file
77
python/utils/interval-timer-ding.ps1
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
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.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Default: period 15 minutes, offset 15 seconds. Notification plays at :14:45, :29:45, :44:45, :59:45.
|
||||||
|
|
||||||
|
.PARAMETER PeriodMinutes
|
||||||
|
Interval in minutes between notifications (default 15). Defines the "round" marks (0, 15, 30, 45 past the hour).
|
||||||
|
|
||||||
|
.PARAMETER OffsetSeconds
|
||||||
|
Seconds before each round mark to play the sound (default 15). E.g. 15 means play at XX:14:45, XX:29:45, etc.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\interval-timer-ding.ps1
|
||||||
|
.\interval-timer-ding.ps1 -PeriodMinutes 30 -OffsetSeconds 10
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[int] $PeriodMinutes = 15,
|
||||||
|
[int] $OffsetSeconds = 15
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
if ($PeriodMinutes -lt 1) { throw "PeriodMinutes must be >= 1" }
|
||||||
|
if ($OffsetSeconds -lt 0) { throw "OffsetSeconds must be >= 0" }
|
||||||
|
|
||||||
|
$periodSec = $PeriodMinutes * 60
|
||||||
|
|
||||||
|
function Get-SecondsSinceMidnight {
|
||||||
|
$now = Get-Date
|
||||||
|
return $now.Hour * 3600 + $now.Minute * 60 + $now.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-NextRingSeconds {
|
||||||
|
$nowSec = Get-SecondsSinceMidnight
|
||||||
|
$nextBoundarySec = [Math]::Ceiling($nowSec / $periodSec) * $periodSec
|
||||||
|
$ringSec = $nextBoundarySec - $OffsetSeconds
|
||||||
|
if ($ringSec -le $nowSec) {
|
||||||
|
$ringSec += $periodSec
|
||||||
|
}
|
||||||
|
return $ringSec
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-WaitSeconds {
|
||||||
|
$nowSec = Get-SecondsSinceMidnight
|
||||||
|
$ringSec = Get-NextRingSeconds
|
||||||
|
$wait = $ringSec - $nowSec
|
||||||
|
if ($wait -le 0) { $wait = 1 }
|
||||||
|
return [int]$wait
|
||||||
|
}
|
||||||
|
|
||||||
|
function Play-Ding {
|
||||||
|
try {
|
||||||
|
[System.Media.SystemSounds]::Asterisk.Play()
|
||||||
|
} catch {
|
||||||
|
[Console]::Beep(800, 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$nextRing = Get-NextRingSeconds
|
||||||
|
$nextH = [Math]::Floor($nextRing / 3600)
|
||||||
|
$nextM = [Math]::Floor(($nextRing % 3600) / 60)
|
||||||
|
$nextS = $nextRing % 60
|
||||||
|
Write-Host "Period: $PeriodMinutes min, offset: $OffsetSeconds s. Next ding at $($nextH.ToString('00')):$($nextM.ToString('00')):$($nextS.ToString('00')) (then every $PeriodMinutes min). Press Ctrl+C to stop."
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$waitSec = Get-WaitSeconds
|
||||||
|
if ($waitSec -gt 0) {
|
||||||
|
Start-Sleep -Seconds $waitSec
|
||||||
|
}
|
||||||
|
Play-Ding
|
||||||
|
$ts = Get-Date -Format "HH:mm:ss"
|
||||||
|
Write-Host "[$ts] ding"
|
||||||
|
}
|
||||||
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