Reputation: 101
I'm trying to create a pause-able timer in Python. During my search I came across this answer. https://stackoverflow.com/a/60027719/11892518
I modified the code a little and added the timer in a while loop I also used the keyboard
module for human interaction while the loop is running:
from datetime import datetime, timedelta
import time
import keyboard
class MyTimer:
"""
timer.start() - should start the timer
timer.pause() - should pause the timer
timer.resume() - should resume the timer
timer.get() - should return the current time
"""
def __init__(self):
print("Initializing timer")
self.time_started : datetime = None
self.time_paused : datetime = None
self.is_paused : bool = False
def Start(self):
""" Starts an internal timer by recording the current time """
#print("Starting timer")
self.time_started = datetime.now()
def Pause(self):
""" Pauses the timer """
if self.time_started is None:
pass
#raise ValueError("Timer not started")
if self.is_paused:
pass
#raise ValueError("Timer is already paused")
#print("Pausing timer")
self.time_paused = datetime.now()
self.is_paused = True
def Resume(self):
""" Resumes the timer by adding the pause time to the start time """
if self.time_started is None:
pass
#raise ValueError("Timer not started")
if not self.is_paused:
pass
#raise ValueError("Timer is not paused")
#print("Resuming timer")
dt : timedelta = datetime.now() - self.time_paused
#print(dt)
#print(self.time_started)
#print(self.time_started + dt)
self.time_started = self.time_started + dt
self.is_paused = False
def Get(self):
""" Returns a timedelta object showing the amount of time elapsed since the start time, less any pauses """
#print("Get timer value")
if self.time_started is None:
pass
#raise ValueError("Timer not started")
now : timedelta = datetime.now() - self.time_started
if self.is_paused:
return self.time_paused - self.time_started
else:
return now
def timer_loop():
t1 = MyTimer()
while True:
try:
if keyboard.is_pressed('s'):
t1.Start()
elif keyboard.is_pressed('p'):
t1.Pause()
elif keyboard.is_pressed('r'):
t1.Resume()
elif keyboard.is_pressed('g'):
print(t1.Get())
elif keyboard.is_pressed('q'):
print("Terminating...")
break
except Exception as ex:
print(ex)
break
if __name__ == '__main__':
timer_loop()
The problem appears after the timer has been started, paused, resumed and paused again.
The result is like this:
-1 day, 21:12:31.704640
-1 day, 21:12:31.704640
-1 day, 21:12:31.704640
Note: when the loop is running press s
to start the timer, p
to pause, g
to get value, r
to resume the timer, and q
to break the loop and exit the program.
Could anyone please explain why this happens? I added type hinting because I saw on the internet that it could be caused by conversion between time and float, but still that didn't resolve it.
SOLUTION: This is my final code in case anyone stumbles upon this question:
from datetime import datetime, timedelta
import time
import keyboard
class MyTimer:
"""
timer.start() - should start the timer
timer.pause() - should pause the timer
timer.resume() - should resume the timer
timer.get() - should return the current time
"""
def __init__(self):
print("Initializing timer")
self.time_started : datetime = None
self.time_paused : datetime = None
self.is_paused : bool = False
def Start(self):
""" Starts an internal timer by recording the current time """
print("Starting timer")
self.time_started = datetime.now()
def Pause(self):
""" Pauses the timer """
if self.time_started is None:
#raise ValueError("Timer not started")
return
if self.is_paused:
#raise ValueError("Timer is already paused")
return
print("Pausing timer")
self.time_paused = datetime.now()
self.is_paused = True
def Resume(self):
""" Resumes the timer by adding the pause time to the start time """
if self.time_started is None:
#raise ValueError("Timer not started")
return
if not self.is_paused:
#raise ValueError("Timer is not paused")
return
print("Resuming timer")
dt : timedelta = datetime.now() - self.time_paused
self.time_started = self.time_started + dt
self.is_paused = False
def Get(self):
""" Returns a timedelta object showing the amount of time elapsed since the start time, less any pauses """
print("Get timer value")
if self.time_started is None:
#raise ValueError("Timer not started")
return
if self.is_paused:
paused_at : timedelta = self.time_paused - self.time_started
print(paused_at)
return paused_at
else:
now : timedelta = datetime.now() - self.time_started
print(now)
return now
def timer_loop():
t1 = MyTimer()
keyboard.add_hotkey('s', t1.Start)
keyboard.add_hotkey('p', t1.Pause)
keyboard.add_hotkey('r', t1.Resume)
keyboard.add_hotkey('g', t1.Get)
try:
keyboard.wait('q')
except Exception as ex:
print(ex)
if __name__ == '__main__':
timer_loop()
Upvotes: 0
Views: 219
Reputation: 942
The way your keyboard
loop works is that it calls the method multiple times while the key is pressed (evident from the multiple prints when you press g
).
When the timer is resumed, the pause time is subtracted. In your version, the block is disabled for only continuing if the timer is actually paused, so the pause time gets subtracted multiple times, ultimately resulting in a negative timer display if that happens several times.
One way to fix that is to replace pass
with return
if the timer is not paused.
Additionally, a pattern described in the keyboard
docs is to remove the loop and add callbacks. This ensures that the method is only called once per keypress.
def timer_loop():
t1 = MyTimer()
keyboard.add_hotkey('s', t1.Start)
keyboard.add_hotkey('p', t1.Pause)
keyboard.add_hotkey('r', t1.Resume)
keyboard.add_hotkey('g', lambda: print(t1.Get()))
try:
keyboard.wait('q')
except Exception as ex:
print(ex)
However, this still leaves the possibility of a negative output if r
is pressed multiple times in succession. You can see this if you press the sequence s
(wait a few seconds) g
p
g
g
(to confirm it's paused) then [r
g
] a few times - you'll see the timer go down and then negative as the pause time is repeatedly subtracted.
Two additional points to call out:
Get
ting the timer state before it's started raises an exception because self.time_started
is subtracted from datetime.now()
before self.time_started
is initialized.Reinstating all the ValueError
lines, or at least preventing code continuation within those methods, would avoid all the above issues including your original issue.
Upvotes: 2