Search code examples
python-3.xtimepyqt5qtimer

How to align a QTimer interval with the system time


How do I start my function every 5 minutes and 20 seconds? I used:

self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.updateplot)
self.timer.start(310000)

But I'd like to set it by the time of my PC - so if I start the program at 3:23, the update track is at 3:25:20, 3:30:20 etc.


Solution

  • Two timers are needed for this: a single-shot to get to the starting point, which then triggers a second timer which times-out every 5 minutes (at the 20th second). The starting point can be calculated with QDateTime.msecsTo, so the code would look something like this:

    mins = 5
    secs = 20
    self.timer = QtCore.QTimer(self)
    self.timer.timeout.connect(self.updateplot)
    self.timer.setInterval(mins * 60 * 1000)
    def start_point():
        self.timer.timeout.emit()
        self.timer.start()
    d1 = QtCore.QDateTime.currentDateTimeUtc()
    d2 = QtCore.QDateTime(d1)
    t1 = d1.time()
    d2.setTime(QtCore.QTime(t1.hour(), t1.minute(), secs))
    if t1.second() > secs:
        d2 = d2.addSecs((mins - t1.minute() % mins) * 60)
    QtCore.QTimer.singleShot(d1.msecsTo(d2), start_point)
    

    Note that the accuracy of this depends on the system clock, and there will also be a fraction of a second lag whilst the start point is calculated - so don't expect it to stay perfectly in sync with the network time. If you need it to run for a long time, you could check the current time in updateplot and restart the timer if it starts to drift beyond a certain threshold.

    UPDATE:

    Here's a demo that uses the above approach:

    enter image description here

    import sys
    from PyQt5 import QtCore, QtWidgets
    
    class Window(QtWidgets.QWidget):
        def __init__(self):
            super(Window, self).__init__()
            self.spinMins = QtWidgets.QSpinBox()
            self.spinMins.setRange(0, 59)
            self.spinMins.setValue(1)
            self.spinSecs = QtWidgets.QSpinBox()
            self.spinSecs.setRange(0, 59)
            self.spinSecs.setValue(5)
            self.button = QtWidgets.QPushButton('Start')
            self.button.clicked.connect(self.resetTimer)
            self.edit = QtWidgets.QTextEdit()
            self.edit.setReadOnly(True)
            layout = QtWidgets.QGridLayout(self)
            layout.addWidget(self.edit, 0, 0, 1, 3)
            layout.addWidget(self.spinMins, 1, 0)
            layout.addWidget(self.spinSecs, 1, 1)
            layout.addWidget(self.button, 1, 2)
            self.mainTimer = QtCore.QTimer(self)
            self.mainTimer.timeout.connect(self.updateplot)
            self.startTimer = QtCore.QTimer(self)
            self.startTimer.setSingleShot(True)
            def start_point():
                self.mainTimer.timeout.emit()
                self.mainTimer.start()
            self.startTimer.timeout.connect(start_point)
            self.resetTimer()
    
        def resetTimer(self):
            self.mainTimer.stop()
            self.startTimer.stop()
            mins = self.spinMins.value()
            secs = self.spinSecs.value()
            self.edit.append('restarting timer... (%dm %ds)' % (mins, secs))
            self.mainTimer.setInterval(mins * 60 * 1000)
            d1 = QtCore.QDateTime.currentDateTimeUtc()
            d2 = QtCore.QDateTime(d1)
            t1 = d1.time()
            d2.setTime(QtCore.QTime(t1.hour(), t1.minute(), secs))
            if t1.second() > secs:
                d2 = d2.addSecs((mins - t1.minute() % mins) * 60)
            self.startTimer.start(d1.msecsTo(d2))
    
        def updateplot(self, t=None):
            t = QtCore.QTime.currentTime()
            self.edit.append('timeout: %s' % t.toString('HH:mm:ss.zzz'))
    
    if __name__ == '__main__':
    
        app = QtWidgets.QApplication(sys.argv)
        window = Window()
        window.setWindowTitle('Timer Test')
        window.setGeometry(600, 100, 300, 200)
        window.show()
        sys.exit(app.exec_())