Skip to content

Commit ee9e44e

Browse files
authored
Merge pull request #3646 from openatv/Scheduler
[Scheduler]
2 parents 51e78a9 + 0446cc5 commit ee9e44e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+310
-141
lines changed

data/setup.xml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -891,11 +891,23 @@
891891
<item level="0" text="Sunday" description="Select if this timer activates on a Sunday.">self.timerDay['Sun']</item>
892892
</if>
893893
</if>
894-
<item level="0" text="Start time" description="Select the time when this timer will start.">self.timerStartTime</item>
895-
<item level="0" text="Set end time" description="Select 'Yes' if an end time is required for this timer.">self.timerSetEndTime</item>
896-
<if conditional="self.timerSetEndTime.value">
897-
<item level="0" text="End time" description="Select the time when this timer should end.">self.timerEndTime</item>
894+
<if conditional="self.isFunctionTimer()">
895+
<item level="0" text="Timer start window" description="Select the start time after which this timer can be activated. Before this time the timer won't be activated even if other conditions are met.">self.timerStartTime</item>
896+
<item level="0" text="Timer end window" description="Select the end time before which this timer can be activated. After this time the timer won't be activated even if other conditions are met.">self.timerEndTime</item>
898897
<item level="0" text="After event" description="Select the action to perform when the timer ends.">self.timerAfterEvent</item>
898+
<else />
899+
<item level="0" text="Start time" description="Select the time when this timer will start.">self.timerStartTime</item>
900+
<item level="0" text="Set end time" description="Select 'Yes' if an end time is required for this timer." conditional="not self.isFunctionTimer()">self.timerSetEndTime</item>
901+
<if conditional="self.timerSetEndTime.value">
902+
<item level="0" text="End time" description="Select the time when this timer should end.">self.timerEndTime</item>
903+
<item level="0" text="After event" description="Select the action to perform when the timer ends.">self.timerAfterEvent</item>
904+
</if>
905+
</if>
906+
<if conditional="self.isFunctionTimer()">
907+
<item level="0" text="Standby execution" description="Select how the schedule is handled with respect to Standby mode.">self.timerFunctionStandby</item>
908+
<item level="0" text="Start retry" description="Select 'Yes' to reschedule the task to when the receiver goes in to/out of Standby mode. The state change must happen within the task schedule window or else the task will not be run." conditional="self.timerFunctionStandby.value > 0">self.timerFunctionStandbyRetry</item>
909+
<item level="0" text="Error Retry count" description="Select the number of times to retry a task if an error occurs when running the task.">self.timerFunctionRetryCount</item>
910+
<item level="0" text="Error Retry delay" description="Select the amount of time to wait, after a task error, before trying to run the task again." conditional="self.timerFunctionRetryCount.value > 0">self.timerFunctionRetryDelay</item>
899911
</if>
900912
</if>
901913
</setup>

lib/python/Scheduler.py

Lines changed: 171 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
import NavigationInstance
1212
from timer import Timer, TimerEntry
13+
from threading import Thread
14+
1315
from Components.config import config
1416
from Components.SystemInfo import getBoxDisplayName
1517
from Components.TimerSanityCheck import TimerSanityCheck
@@ -91,9 +93,54 @@ def parseEvent(event):
9193
return (begin, end)
9294

9395

96+
class FunctionTimerThread(Thread):
97+
def __init__(self, entryFunction, callbackFunction, timerEnty):
98+
Thread.__init__(self)
99+
self.entryFunction = entryFunction
100+
self.callbackFunction = callbackFunction
101+
self.timerEnty = timerEnty
102+
self.daemon = True
103+
104+
def run(self):
105+
result = self.entryFunction(self.timerEnty)
106+
if self.callbackFunction and callable(self.callbackFunction):
107+
self.callbackFunction(result)
108+
109+
94110
class Scheduler(Timer):
95111
def __init__(self):
96112
Timer.__init__(self)
113+
config.misc.standbyCounter.addNotifier(self.enterStandby, initial_call=False)
114+
115+
def leaveStandby(self):
116+
if DEBUG:
117+
print("[Scheduler] leaveStandby called.")
118+
recheck = False
119+
for timer in self.timer_list:
120+
if DEBUG:
121+
print(f"[Scheduler] timer: {timer}, conditionFlag: {timer.conditionFlag}")
122+
if not timer.disabled and timer.conditionFlag == 2:
123+
timer.conditionFlag = 0
124+
timer.state = SchedulerEntry.StateWaiting
125+
recheck = True
126+
if recheck:
127+
self.calcNextActivation()
128+
129+
def enterStandby(self, value):
130+
if DEBUG:
131+
print("[Scheduler] enterStandby called.")
132+
from Screens.Standby import inStandby
133+
inStandby.onClose.append(self.leaveStandby)
134+
recheck = False
135+
for timer in self.timer_list:
136+
if DEBUG:
137+
print(f"[Scheduler] timer: {timer}, conditionFlag: {timer.conditionFlag}")
138+
if not timer.disabled and timer.conditionFlag == 1:
139+
timer.conditionFlag = 0
140+
timer.state = SchedulerEntry.StateWaiting
141+
recheck = True
142+
if recheck:
143+
self.calcNextActivation()
97144

98145
def loadTimers(self):
99146

@@ -163,6 +210,10 @@ def saveTimers(self):
163210
timerEntry.append(f"ipadress=\"{timer.ipadress}\"")
164211
if timer.function:
165212
timerEntry.append(f"function=\"{timer.function}\"")
213+
timerEntry.append(f"runinstandby=\"{timer.functionStandby}\"")
214+
timerEntry.append(f"runinstandbyretry=\"{int(timer.functionStandbyRetry)}\"")
215+
timerEntry.append(f"retrycount=\"{int(timer.functionRetryCount)}\"")
216+
timerEntry.append(f"retrydelay=\"{int(timer.functionRetryDelay)}\"")
166217

167218
timerLog = []
168219
for logTime, logCode, logMsg in timer.log_entries:
@@ -224,8 +275,15 @@ def createTimer(self, timerDom):
224275
entry.netip = timerDom.get("netip", "false").lower() in ("true", "yes")
225276
entry.ipadress = timerDom.get("ipadress", "0.0.0.0")
226277
entry.function = timerDom.get("function")
278+
if entry.function:
279+
entry.functionStandby = int(timerDom.get("runinstandby", "0"))
280+
entry.functionStandbyRetry = int(timerDom.get("runinstandbyretry", "0"))
281+
entry.functionRetryCount = int(timerDom.get("retrycount", "0"))
282+
entry.functionRetryDelay = int(timerDom.get("retrydelay", "5"))
283+
227284
for log in timerDom.findall("log"):
228285
entry.log_entries.append((int(log.get("time")), int(log.get("code")), log.text.strip()))
286+
entry.isNewTimer = False
229287
return entry
230288

231289
# When activating a timer which has already passed, simply
@@ -239,10 +297,11 @@ def doActivate(self, timer):
239297
# state is kept. The timer entry itself will fix up the delay.
240298
if timer.activate():
241299
timer.state += 1
242-
try:
243-
self.timer_list.remove(timer)
244-
except ValueError:
245-
print("[Scheduler] Remove timer from timer list failed!")
300+
if timer in self.timer_list:
301+
try:
302+
self.timer_list.remove(timer)
303+
except ValueError:
304+
print("[Scheduler] Remove timer from timer list failed!")
246305
if timer.state < SchedulerEntry.StateEnded: # Did this timer reached the last state?
247306
insort(self.timer_list, timer) # No, sort it into active list.
248307
else: # Yes, process repeated, and re-add.
@@ -374,9 +433,6 @@ def removeEntry(self, timer):
374433
timer.abort() # Abort timer. This sets the end time to current time, so timer will be stopped.
375434
if timer.state != timer.StateEnded:
376435
self.timeChanged(timer)
377-
# print("[Scheduler] State: %s." % timer.state)
378-
# print("[Scheduler] In processed: %s." % timer in self.processed_timers)
379-
# print("[Scheduler] In running: %s." % timer in self.timer_list)
380436
if timer.state != TimerEntry.StateEnded: # Disable timer first.
381437
timer.disable()
382438
if not timer.dontSave: # Auto increase instant timer if possible.
@@ -385,6 +441,12 @@ def removeEntry(self, timer):
385441
self.timeChanged(timerItem)
386442
if timer in self.processed_timers: # Now the timer should be in the processed_timers list, remove it from there.
387443
self.processed_timers.remove(timer)
444+
445+
if timer.timerType == TIMERTYPE.OTHER and timer.function:
446+
timer.state = timer.StateEnded - 1
447+
timer.enable() # re-enable to allow function execution
448+
timer.activate() # force cancel
449+
388450
self.saveTimers()
389451

390452
def getNextZapTime(self):
@@ -422,7 +484,8 @@ def isProcessing(self, exceptTimer=None, endedTimer=None):
422484
class SchedulerEntry(TimerEntry):
423485
def __init__(self, begin, end, disabled=False, afterEvent=AFTEREVENT.NONE, timerType=TIMERTYPE.WAKEUP, checkOldTimers=False, autosleepdelay=60):
424486
TimerEntry.__init__(self, int(begin), int(end))
425-
print("[SchedulerEntry] DEBUG: Running init code.")
487+
if DEBUG:
488+
print("[SchedulerEntry] DEBUG: Running init code.")
426489
if checkOldTimers and self.begin < int(time()) - 1209600:
427490
self.begin = int(time())
428491
# Check auto Scheduler.
@@ -458,6 +521,13 @@ def __init__(self, begin, end, disabled=False, afterEvent=AFTEREVENT.NONE, timer
458521
self.resetState()
459522
self.messageBoxAnswerPending = False
460523
self.keyPressHooked = False
524+
self.cancelFunction = None
525+
self.functionStandby = 0 # 0 Always / 1 Standby / 2 Online
526+
self.functionStandbyRetry = False
527+
self.functionRetryCount = 0 # default diabled
528+
self.functionRetryDelay = 5 # 5 minutes
529+
self.functionRetryCounter = 0
530+
self.isNewTimer = True
461531

462532
def __repr__(self, getType=False):
463533
timertype = {
@@ -480,6 +550,8 @@ def __repr__(self, getType=False):
480550
return f"SchedulerEntry(type={timertype}, begin={ctime(self.begin)} Disabled)"
481551

482552
def activate(self):
553+
if DEBUG:
554+
print(f"[Scheduler] DEBUG activate state={self.state}")
483555
global DSsave, InfoBar, RBsave, RSsave, aeDSsave, wasTimerWakeup
484556
if not InfoBar:
485557
try:
@@ -809,38 +881,40 @@ def activate(self):
809881
elif self.timerType == TIMERTYPE.OTHER and self.function:
810882
if DEBUG:
811883
print(f"[Scheduler] self.timerType == TIMERTYPE.OTHER: / function = {self.function}")
812-
functionTimerEntry = functionTimer.getItem(self.function)
884+
functionTimerEntry = functionTimers.getItem(self.function)
813885
if functionTimerEntry:
814-
functionTimerEntryFunction = functionTimerEntry.get("fnc")
815-
816-
doFunc = False
817-
#if self.exec_fnc_when == "standby" and Screens.Standby.inStandby:
818-
# doFunc = True
819-
#elif self.exec_fnc_when == "stb_on" and not Screens.Standby.inStandby:
820-
# doFunc = True
821-
#elif self.exec_fnc_when == "always":
822-
# doFunc = True
886+
functionTimerEntryFunction = functionTimerEntry.get("entryFunction")
887+
functionTimerCancelFunction = functionTimerEntry.get("cancelFunction")
888+
functionTimerUseOwnThread = functionTimerEntry.get("useOwnThread")
889+
if DEBUG:
890+
print(f"[Scheduler] functionTimerEntryFunction = {functionTimerEntryFunction}")
823891

892+
self.conditionFlag = 0
824893
doFunc = True
894+
if self.functionStandby == 1 and not Screens.Standby.inStandby:
895+
doFunc = False
896+
if self.functionStandby == 2 and Screens.Standby.inStandby:
897+
doFunc = False
825898

826899
if doFunc:
827-
self.end += 7200
828-
if functionTimerEntryFunction and callable(functionTimerEntryFunction):
829-
functionTimerEntryFunction()
830-
831-
#if "isThreaded" in functionTimerEntry and not functionTimerEntry["isThreaded"]:
832-
# self.is_threaded = False
833-
#elif "isScreen" in functionTimerEntry and not functionTimerEntry["isScreen"]:
834-
# self.is_threaded = False
835-
#self.execnotifyafter = self.notify_after_t
836-
#if self.notify_t and not Screens.Standby.inStandby:
837-
# Notifications.AddNotificationWithCallback(self.askForScheduledTimer, MessageBox, _("An scheduled task wants to execute following function at your STB\n\n %s \n\nContinue?") % functionTimerEntry["name"], timeout = 20)
838-
#else:
839-
# self.askForScheduledTimer(True)
900+
if functionTimerEntryFunction and callable(functionTimerEntryFunction) and functionTimerCancelFunction and callable(functionTimerCancelFunction):
901+
self.startFunctionTimer(functionTimerEntryFunction, functionTimerCancelFunction, functionTimerUseOwnThread)
902+
elif self.functionStandbyRetry and NavigationInstance.instance.Scheduler:
903+
self.conditionFlag = self.functionStandby # 1 Standby / 2 Online
904+
if DEBUG:
905+
print("[Scheduler] Function timer postponed due to standby state.")
840906

841907
return True
842908

843909
elif nextState == self.StateEnded:
910+
if DEBUG:
911+
print(f"[Scheduler] DEBUG nextState self.StateEnded / self.cancelled={self.cancelled} / self.failed={self.failed}")
912+
if self.timerType == TIMERTYPE.OTHER and self.function and self.cancelled and self.cancelFunction and callable(self.cancelFunction):
913+
if DEBUG:
914+
print("[Scheduler] DEBUG Call cancelFunction")
915+
self.cancelFunction()
916+
self.cancelFunction = None
917+
return True
844918
if self.afterEvent == AFTEREVENT.WAKEUP:
845919
Screens.Standby.TVinStandby.skipHdmiCecNow("wakeuppowertimer")
846920
if Screens.Standby.inStandby:
@@ -919,14 +993,44 @@ def resetTimerWakeup(self): # Reset wakeup state after ending timer.
919993
print("[Scheduler] Reset wakeup state.")
920994
wasTimerWakeup = False
921995

996+
def startFunctionTimer(self, entryFunction, cancelFunction, useOwnThread):
997+
if DEBUG:
998+
print("[Scheduler] DEBUG startFunctionTimer")
999+
self.cancelFunction = cancelFunction
1000+
if useOwnThread:
1001+
result = entryFunction(self.functionTimerCallback, self)
1002+
if DEBUG:
1003+
print(f"[Scheduler] DEBUG startFunctionTimer own thread started {result}")
1004+
else:
1005+
self.timerThread = FunctionTimerThread(entryFunction, self.functionTimerCallback, self)
1006+
self.timerThread.start()
1007+
1008+
def functionTimerCallback(self, success):
1009+
if DEBUG:
1010+
print(f"[Scheduler] DEBUG functionTimerCallback success={success}")
1011+
if self.functionRetryCount > 0 and not success:
1012+
self.functionRetryCounter += 1
1013+
if self.functionRetryCounter <= self.functionRetryCount:
1014+
if DEBUG:
1015+
print(f"[Scheduler] DEBUG functionTimerCallback retry {self.functionRetryCounter} of {self.functionRetryCount} after {self.functionRetryDelay} minutes")
1016+
nextBegin = int(time()) + (self.functionRetryDelay * 60)
1017+
if nextBegin < self.end:
1018+
self.start_prepare = nextBegin
1019+
self.state = self.StateWaiting
1020+
NavigationInstance.instance.Scheduler.doActivate(self)
1021+
return
1022+
self.failed = not success
1023+
self.state = self.StateEnded if success else self.StateFailed
1024+
NavigationInstance.instance.Scheduler.doActivate(self)
1025+
9221026
def getNextActivation(self):
9231027
if self.state in (self.StateEnded, self.StateFailed):
924-
return self.end
1028+
return int(time()) - 1 if self.function else self.end
9251029
nextState = self.state + 1
9261030
return {
9271031
self.StatePrepared: self.start_prepare,
9281032
self.StateRunning: self.begin,
929-
self.StateEnded: self.end
1033+
self.StateEnded: int(time()) + 10 if self.function else self.end
9301034
}[nextState]
9311035

9321036
def timeChanged(self):
@@ -1140,23 +1244,46 @@ def getNetworkTraffic(self, getInitialValue=False):
11401244
return False
11411245

11421246

1143-
class FunctionTimer:
1247+
class FunctionTimers:
11441248
def __init__(self):
11451249
self.items = {}
11461250

1147-
def add(self, fnc):
1148-
if isinstance(fnc, (tuple, list)) and len(fnc) == 2 and isinstance(fnc[0], str) and isinstance(fnc[1], dict) and fnc[0] not in self.items:
1149-
self.items[fnc[0]] = fnc[1]
1251+
def add(self, key, info):
1252+
if isinstance(key, str) and isinstance(info, dict):
1253+
if key not in self.items:
1254+
if callable(info.get("entryFunction")) and callable(info.get("cancelFunction")):
1255+
self.items[key] = info
1256+
else:
1257+
print("[FunctionTimers] Error: Both 'entryFunction' and 'cancelFunction' must be callable functions!")
1258+
else:
1259+
print(f"[FunctionTimers] Error: The key '{key}' is already defined!")
1260+
else:
1261+
print("[FunctionTimers] Error: Parameter 'key' must be a string and 'info' must be a dictionary!")
11501262

1151-
def remove(self, fncid):
1152-
if isinstance(fncid, str) and fncid in self.items:
1153-
self.items.pop(fncid)
1263+
def remove(self, key):
1264+
if key in self.items:
1265+
del self.items[key]
1266+
else:
1267+
print(f"[FunctionTimers] Error: The key '{key}' was not found!")
11541268

1155-
def get(self):
1269+
def getList(self):
11561270
return self.items
11571271

1158-
def getItem(self, item):
1159-
return self.items.get(item)
1272+
def getItem(self, key):
1273+
return self.items.get(key)
1274+
1275+
def getName(self, key):
1276+
return self.items.get(key, {}).get("name")
1277+
1278+
1279+
functionTimers = FunctionTimers()
11601280

11611281

1162-
functionTimer = FunctionTimer()
1282+
def addFunctionTimer(key: str, name: str, entryFunction, cancelFunction, useOwnThread=False):
1283+
"""Convenience wrapper for adding a function timer entry."""
1284+
functionTimers.add(key, {
1285+
"name": name,
1286+
"entryFunction": entryFunction,
1287+
"cancelFunction": cancelFunction,
1288+
"useOwnThread": useOwnThread
1289+
})

0 commit comments

Comments
 (0)