You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
209 lines
7.7 KiB
209 lines
7.7 KiB
#!/usr/bin/env python3
|
|
|
|
from apscheduler.schedulers.blocking import BlockingScheduler
|
|
from apscheduler.triggers.cron import CronTrigger
|
|
from apscheduler.events import *
|
|
from collections import namedtuple
|
|
|
|
from tzlocal import get_localzone
|
|
|
|
from datetime import datetime
|
|
from datetime import timezone
|
|
from datetime import timedelta
|
|
|
|
import time
|
|
|
|
import subprocess
|
|
|
|
import os
|
|
import shepherd.plugin
|
|
|
|
import io
|
|
|
|
|
|
def is_raspberry_pi(raise_on_errors=False):
|
|
"""Checks if Raspberry PI.
|
|
:return:
|
|
"""
|
|
try:
|
|
with io.open('/proc/cpuinfo', 'r') as cpuinfo:
|
|
found = False
|
|
for line in cpuinfo:
|
|
if line.startswith('Hardware'):
|
|
found = True
|
|
label, value = line.strip().split(':', 1)
|
|
value = value.strip()
|
|
if value not in (
|
|
'BCM2708',
|
|
'BCM2709',
|
|
'BCM2835',
|
|
'BCM2836'
|
|
):
|
|
if raise_on_errors:
|
|
raise ValueError(
|
|
'This system does not appear to be a '
|
|
'Raspberry Pi.'
|
|
)
|
|
else:
|
|
return False
|
|
if not found:
|
|
if raise_on_errors:
|
|
raise ValueError(
|
|
'Unable to determine if this system is a Raspberry Pi.'
|
|
)
|
|
else:
|
|
return False
|
|
except IOError:
|
|
if raise_on_errors:
|
|
raise ValueError('Unable to open `/proc/cpuinfo`.')
|
|
else:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
_scheduler = None
|
|
|
|
joblist_path = None
|
|
|
|
JobDescription = namedtuple(
|
|
'Job', ['jobstring', 'hour', 'minute', 'second'])
|
|
|
|
|
|
def init_scheduler(core_config):
|
|
global _scheduler
|
|
global joblist_path
|
|
_scheduler = BlockingScheduler()
|
|
joblist_path = os.path.expanduser(core_config["root_dir"])
|
|
joblist_path = os.path.join(joblist_path, "joblist.shp")
|
|
|
|
|
|
def schedule_job(job_desc):
|
|
if not isinstance(job_desc, JobDescription):
|
|
raise Exception(
|
|
"Argument to schedule_job() must be an instance of shepherd.scheduler.JobDescription")
|
|
global _scheduler
|
|
# resolve callable
|
|
splitstring = job_desc.jobstring.split(':')
|
|
if not ((len(splitstring) == 4) and (splitstring[0] == "shepherd")):
|
|
raise Exception(
|
|
"Could not add job - jobstring is not a valid Shepherd job")
|
|
job_plugin, job_function, job_name = splitstring[1:]
|
|
func = getattr(shepherd.plugin.plugin_functions[job_plugin], job_function)
|
|
triggerstr = "!"+job_desc.hour+":"+job_desc.minute+":"+job_desc.second
|
|
for job in _scheduler.get_jobs():
|
|
#Jobstring must be the same, triggerstring can vary and still match
|
|
if job.id.startswith(job_desc.jobstring):
|
|
# Jobstring matches existing job, so update it
|
|
print("Modifying job "+job.id)
|
|
print("next run was "+job.next_run_time)
|
|
job.modify(func=func, trigger=CronTrigger(
|
|
hour=job_desc.hour, minute=job_desc.minute, second=job_desc.second))
|
|
print("next is now "+job.next_run_time)
|
|
break
|
|
else:
|
|
# No matching job found, so new job
|
|
print("Scheduling job "+job_desc.jobstring)
|
|
newjob=_scheduler.add_job(func, id=job_desc.jobstring+triggerstr, coalesce=True, misfire_grace_time=300, trigger=CronTrigger(
|
|
hour=job_desc.hour, minute=job_desc.minute, second=job_desc.second))
|
|
#print("Next scheduled for "+str(newjob.next_run_time))
|
|
|
|
|
|
# Needs to be called after plugins are initialised, so interface functions
|
|
# are available
|
|
|
|
# Remember to wipe job store and not restore on next boot if config file was changed
|
|
|
|
def save_jobs():
|
|
joblist = _scheduler.get_jobs()
|
|
saved_jobs = []
|
|
next_job_time = None
|
|
|
|
for job in joblist:
|
|
jobstring, _, triggerstr = job.id.partition('!')
|
|
jobstring_parts = jobstring.split(':')
|
|
triggerstr_parts = triggerstr.split(':')
|
|
if not ((len(jobstring_parts) == 4) and (jobstring_parts[0] == "shepherd")):
|
|
raise Exception(
|
|
"Could not save scheduler job "+job.id+" - ID is not a valid Shepherd job")
|
|
if not isinstance(job.trigger, CronTrigger):
|
|
raise Exception("Could not save scheduler job " +
|
|
job.id+" - Trigger is not a CronTrigger")
|
|
saved_jobs.append(job.id)
|
|
if next_job_time is not None:
|
|
if job.next_run_time < next_job_time:
|
|
next_job_time = job.next_run_time
|
|
else:
|
|
|
|
next_job_time = job.next_run_time
|
|
|
|
with open(joblist_path+".writing", 'w+') as f:
|
|
for saved_job in saved_jobs:
|
|
f.write("%s\n" % saved_job)
|
|
|
|
os.rename(joblist_path+".writing", joblist_path)
|
|
return next_job_time
|
|
|
|
# Currently to wakeup functionality is based on a hard-coded dependancy on the Scout
|
|
# plugin. We'd like this to instead be independant, and provide hooks for modules to register with that provide this
|
|
# A problem with just providing a "set_alarm" hook or something is that we really need to be able to
|
|
# confirm that an alarm has been set and we _are_ going to wake up gain correctly.
|
|
# Could potentially provide a interface function that others can call to set a "Next Alarm" state variable. This can then be
|
|
# checked after the hook call to verify.
|
|
|
|
|
|
# At the moment shutdown is just triggered 1 minute after setting the alarm - mostly just to allow time for things to upload.
|
|
# Having things be properly event based instead would be better - adding random delays all around the place inevetiably leads
|
|
# "delay creep" - where all the "just in case" delays just build up and up and wind up making the whole thing take ages to do anything.
|
|
# Instead we really need some sort of "current jobs/dependancies" queue thing - containing active stuff to be dealt with that should
|
|
# hold the system active (sidenote - this same system could easily then be used to hold things on to access them remotely)
|
|
# Would ideally have a mechanism that makes it hard to accidentally leave an item/job in the list that stops the system shutting down -
|
|
# maybe look at a context manager used on an object in each plugin?
|
|
|
|
|
|
def _jobs_changed(event):
|
|
next_job_time = save_jobs()
|
|
# default to idle for 5 mins
|
|
|
|
early_wakeup_period = timedelta(minutes=1)
|
|
|
|
now_datetime = datetime.now(get_localzone())
|
|
next_idle_period = timedelta(minutes=5)
|
|
if next_job_time is not None:
|
|
|
|
next_idle_period = next_job_time - now_datetime
|
|
|
|
if next_idle_period > timedelta(hours=6):
|
|
next_idle_period = timedelta(hours=6)
|
|
|
|
wakeup_time = None
|
|
if next_idle_period > timedelta(minutes=4):
|
|
wakeup_time = now_datetime+(next_idle_period-early_wakeup_period)
|
|
|
|
if wakeup_time is not None:
|
|
alarm_str = str(int(wakeup_time.timestamp()))
|
|
print("waking up at "+ str(wakeup_time) + " ("+alarm_str+")")
|
|
|
|
retval=shepherd.plugin.plugin_functions["scout"].set_alarm(alarm_str)
|
|
if retval is not None:
|
|
print(retval)
|
|
if retval == alarm_str:
|
|
if is_raspberry_pi():
|
|
print("Shutting down in 1 minute")
|
|
time.sleep(60)
|
|
|
|
subprocess.run(["shutdown","now"])
|
|
else:
|
|
print("Alarm set response was incorrect")
|
|
else:
|
|
print("Did not get reply from Scout after setting alarm")
|
|
|
|
def restore_jobs():
|
|
pass
|
|
|
|
|
|
def start():
|
|
global _scheduler
|
|
_scheduler.add_listener(_jobs_changed, EVENT_JOB_ADDED | EVENT_JOB_REMOVED | EVENT_JOB_MODIFIED | EVENT_JOB_EXECUTED | EVENT_JOB_MISSED | EVENT_SCHEDULER_STARTED)
|
|
_scheduler.start()
|