Tænd/Sluk tidsplaner - Opret tidsplan
Source: Download script
Last Updated: 13. December 2024 by Sune Koch Hansen (sune@kvalitetsit.dk)
Parameters:
Navn | Type | Standardværdi | Påkrævet |
---|---|---|---|
Click to see the source code
#!/bin/sh
# SYNOPSIS
# on_off_schedule_set.sh PLAN MODE
#
# DESCRIPTION
# This script installs a system service that manages the planned on/off schedule
# by updating the crontab with the next planned shutdown and startup process
# whenever the computer starts.
#
# The shutdown and startup processes are handled by rtcwake.
#
# It takes two mandatory parameters: the planned on/off schedule and the desired rtcwake mode.
# The planned on/off schedule should be represented by a json-file.
# The desired rtcwake mode should be represented by a string.
#
# For use with the "on_off_schedule_remove.sh" script
#
# IMPLEMENTATION
# version on_off_schedule_set.sh (magenta.dk) 1.0.0
# copyright Copyright 2022 Magenta ApS
# license GNU General Public License
#
# TECHNICAL DESCRIPTION
# This script creates and starts "os2borgerpc-set_on-off_schedule.service"
# which runs the script "set_on-off_schedule.py" as a daemon.
#
# "set_on-off_schedule.py" is a python-script that runs on startup and updates the crontab
# with an entry for the next planned shutdown and startup process.
#
# The shutdown and startup process is handled by rtcwake, which shuts the computer down
# in the specified mode and starts it again after a specified number of seconds.
set -x
MONDAY_START=$1
MONDAY_STOP=$2
TUESDAY_START=$3
TUESDAY_STOP=$4
WEDNESDAY_START=$5
WEDNESDAY_STOP=$6
THURSDAY_START=$7
THURSDAY_STOP=$8
FRIDAY_START=$9
FRIDAY_STOP=${10}
SATURDAY_START=${11}
SATURDAY_STOP=${12}
SUNDAY_START=${13}
SUNDAY_STOP=${14}
CUSTOM_DATES=${15}
MODE=${16}
WAKE_PLAN_FILE=/etc/os2borgerpc/plan.json
SCHEDULE_CREATION_SCRIPT="/usr/local/lib/os2borgerpc/make_schedule_plan.py"
ON_OFF_SCHEDULE_SERVICE="/etc/systemd/system/os2borgerpc-set_on-off_schedule.service"
ON_OFF_SCHEDULE_SCRIPT="/usr/local/lib/os2borgerpc/set_on-off_schedule.py"
SCHEDULED_OFF_SCRIPT="/usr/local/lib/os2borgerpc/scheduled_off.sh"
USERCRON="/etc/os2borgerpc/usercron"
USER_CLEANUP="/usr/share/os2borgerpc/bin/user-cleanup.bash"
mkdir -p /usr/local/lib/os2borgerpc
# Ensure that the usercron-file exists and has the correct permissions
touch $USERCRON
chmod 700 $USERCRON
# Ensure that user-cleanup resets the user crontab
if [ -f "$USER_CLEANUP" ] && ! grep --quiet "crontab" $USER_CLEANUP; then
echo "crontab -u user $USERCRON" >> $USER_CLEANUP
fi
# Make the schedule plan.json
cat < $SCHEDULE_CREATION_SCRIPT
#!/usr/bin/env python3
import json
import datetime
from os2borgerpc.client.config import get_config
FILE = "/etc/os2borgerpc/plan.json"
MONDAY_START = "$MONDAY_START"
MONDAY_STOP = "$MONDAY_STOP"
TUESDAY_START = "$TUESDAY_START"
TUESDAY_STOP = "$TUESDAY_STOP"
WEDNESDAY_START = "$WEDNESDAY_START"
WEDNESDAY_STOP = "$WEDNESDAY_STOP"
THURSDAY_START = "$THURSDAY_START"
THURSDAY_STOP = "$THURSDAY_STOP"
FRIDAY_START = "$FRIDAY_START"
FRIDAY_STOP = "$FRIDAY_STOP"
SATURDAY_START = "$SATURDAY_START"
SATURDAY_STOP = "$SATURDAY_STOP"
SUNDAY_START = "$SUNDAY_START"
SUNDAY_STOP = "$SUNDAY_STOP"
CUSTOM_DATES = "$CUSTOM_DATES"
def make_schedule():
"""Make the schedule plan.json"""
# Make the schedule plan dictionary
plan = {'week_plan': {'monday': {'start': MONDAY_START, 'stop': MONDAY_STOP}}}
plan['week_plan']['tuesday'] = {'start': TUESDAY_START, 'stop': TUESDAY_STOP}
plan['week_plan']['wednesday'] = {'start': WEDNESDAY_START, 'stop': WEDNESDAY_STOP}
plan['week_plan']['thursday'] = {'start': THURSDAY_START, 'stop': THURSDAY_STOP}
plan['week_plan']['friday'] = {'start': FRIDAY_START, 'stop': FRIDAY_STOP}
plan['week_plan']['saturday'] = {'start': SATURDAY_START, 'stop': SATURDAY_STOP}
plan['week_plan']['sunday'] = {'start': SUNDAY_START, 'stop': SUNDAY_STOP}
custom_dict = {}
if CUSTOM_DATES:
wake_change_events = CUSTOM_DATES.split('|')
for event in wake_change_events:
settings = event.split(';')
start_date = datetime.datetime.strptime(settings[0], '%d-%m-%Y')
end_date = datetime.datetime.strptime(settings[1], '%d-%m-%Y')
date = start_date
while date <= end_date:
custom_dict[date.strftime('%d-%m-%Y')] = {'start': settings[2], 'stop': settings[3]}
date = date + datetime.timedelta(days=1)
plan['custom_dates'] = custom_dict
# Check the product type and include it in the plan
product = get_config("os2_product")
plan['product'] = product
# Save the plan
with open(FILE, 'w') as file:
json.dump(plan, file, indent=4)
if __name__ == "__main__":
make_schedule()
EOF
python3 $SCHEDULE_CREATION_SCRIPT
rm -f $SCHEDULE_CREATION_SCRIPT
cat < $SCHEDULED_OFF_SCRIPT
#!/usr/bin/env bash
MODE=\$1
DURATION=\$2
pkill -KILL -u user
pkill -KILL -u superuser
/usr/sbin/rtcwake --mode \$MODE --seconds \$DURATION
EOF
chmod 700 $SCHEDULED_OFF_SCRIPT
cat < $ON_OFF_SCHEDULE_SCRIPT
#!/usr/bin/env python3
import json
import datetime
import subprocess
import os
FILE = "$WAKE_PLAN_FILE" # "/etc/os2borgerpc/" + "$PLAN_NAME"
MODE = "$MODE".lower()
LOCALE_FILE = "/etc/default/locale"
def check_weekday(plan, date):
"""Helper method that returns the week plan settings for the week day
corresponding to a given date represented by a datetime.date object"""
week_days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
week_day = week_days[date.weekday()]
return plan['week_plan'][week_day]
def check_custom_date(plan, date):
"""Helper method that returns either the custom settings for
a given date represented by a datetime.date object or
None if no custom settings exist"""
date = date.strftime('%d-%m-%Y')
try:
return plan["custom_dates"][date]
except KeyError:
return None
def check_if_date_is_custom_or_regular(plan, date):
"""Helper method that returns either the custom settings for
a given date represented by a datetime.date object or
the week plan settings for the corresponding week day
if no custom settings exist"""
date_settings = check_custom_date(plan, date)
if date_settings is None:
date_settings = check_weekday(plan, date)
return date_settings
def get_shutdown_and_startup_datetimes(plan, current_datetime):
"""Get the next shutdown/startup date and time
and return them as datetime.datetime objects"""
# Get shutdown date and time
shutdown_date = current_datetime
shutdown_settings = check_if_date_is_custom_or_regular(plan, shutdown_date)
shutdown_time = shutdown_settings['stop']
# Get startup date and time
startup_date = shutdown_date + datetime.timedelta(days=1)
startup_settings = check_if_date_is_custom_or_regular(plan, startup_date)
while startup_settings['start'] == "None":
startup_date = startup_date + datetime.timedelta(days=1)
startup_settings = check_if_date_is_custom_or_regular(plan, startup_date)
if startup_date > shutdown_date + datetime.timedelta(days=31):
raise Exception("Ingen gyldig start-dato fundet")
startup_time = startup_settings['start']
# Get the first-coming planned startup
# This value is only different from startup when a machine is manually turned on
# after its shutdown time
planned_startup = convert_to_datetime_helper(startup_date, startup_time)
# Get next shutdown date and time
next_shutdown_date = shutdown_date + datetime.timedelta(days=1)
next_shutdown_settings = check_if_date_is_custom_or_regular(plan, next_shutdown_date)
next_shutdown_time = next_shutdown_settings['stop']
# Get next startup date and time
next_startup_date = next_shutdown_date + datetime.timedelta(days=1)
next_startup_settings = check_if_date_is_custom_or_regular(plan, next_startup_date)
while next_startup_settings['start'] == "None":
next_startup_date = next_startup_date + datetime.timedelta(days=1)
next_startup_settings = check_if_date_is_custom_or_regular(plan, next_startup_date)
next_startup_time = next_startup_settings['start']
startup_time_today = shutdown_settings['start']
# Handle None values
if shutdown_time == "None" and next_shutdown_time == "None":
shutdown_time = f"{current_datetime.hour}:{current_datetime.minute}"
shutdown_date = next_shutdown_date
startup_time_today = "0:1"
elif shutdown_time == "None" and next_shutdown_time != "None":
shutdown_date, shutdown_time = next_shutdown_date, next_shutdown_time
startup_date, startup_time = next_startup_date, next_startup_time
startup_time_today = "0:1"
elif shutdown_time != "None" and next_shutdown_time == "None":
next_shutdown_time = shutdown_time
# Check whether the machine has been turned on manually,
# i.e. if start is before stop, but current time is after stop
shutdown, startup = convert_to_datetime(shutdown_date, shutdown_time, shutdown_date, startup_time_today)
if startup < shutdown and shutdown < current_datetime:
startup_date, startup_time = next_startup_date, next_startup_time
shutdown_date, shutdown_time = next_shutdown_date, next_shutdown_time
# Convert to datetime.datetime object
shutdown, startup = convert_to_datetime(shutdown_date, shutdown_time, startup_date, startup_time)
# Handle shutdown times after 24:00
shutdown, startup = handle_late_stop(shutdown, startup, current_datetime)
return shutdown, startup, planned_startup
def convert_to_datetime(shutdown_date, shutdown_time, startup_date, startup_time):
"""Helper method that converts the shutdown/startup date and time
to datetime.datetime objects and returns those"""
if shutdown_time == "None":
shutdown = datetime.datetime.now() + datetime.timedelta(minutes=20)
else:
shutdown = convert_to_datetime_helper(shutdown_date, shutdown_time)
startup = convert_to_datetime_helper(startup_date, startup_time)
return shutdown, startup
def convert_to_datetime_helper(date, time):
"""Subfunction for the convert_to_datetime helper method"""
value = datetime.datetime(date.year, date.month, date.day,
int(time[: time.index(":")]),
int(time[time.index(":") + 1 :]))
return value
def handle_late_stop(shutdown, startup, current_datetime):
"""Helper method to handle late stops"""
if shutdown < current_datetime:
shutdown = shutdown + datetime.timedelta(days=1)
if startup < shutdown:
startup = shutdown + datetime.timedelta(minutes=5)
return shutdown, startup
def main():
# Add some functionality to ensure that the time settings are correct?
# Load the plan
with open(FILE) as file:
plan = json.load(file)
# Get the current datetime
current_datetime = datetime.datetime.today()
# Get shutdown and startup datetimes
shutdown, startup, planned_startup = get_shutdown_and_startup_datetimes(plan, current_datetime)
# Refresh the crontab 5 minutes after startup
# That way, even if a mode such as "mem,"
# which does not cause the service to be run again, is used,
# the crontab will still be updated with the next shutdown.
# To that end, determine the datetime that is 5 minutes after startup
refresh = startup + datetime.timedelta(minutes=5)
# Refresh the crontab 5 minutes before planned_startup
# This ensures that if a machine is manually turned on
# after its shutdown time and then left on, it will not
# end in a state where no startup is planned
refresh2 = planned_startup - datetime.timedelta(minutes=5)
# Determine off time
off_time = int((startup - shutdown).total_seconds())
# Make sure the machine will wake up as planned even if it is not shut down by the schedule
startup_string = f"{planned_startup.year}-{planned_startup.month}-{planned_startup.day} "\
f"{planned_startup.hour}:{planned_startup.minute}"
subprocess.run(["rtcwake", "-m", "no", "--date", startup_string])
# Update crontab
# Get current entries
TCRON = "/tmp/oldcron"
with open(TCRON, 'w') as cronfile:
subprocess.run(["crontab", "-l"], stdout=cronfile)
# Remove old entries
with open(TCRON, 'r') as cronfile:
cronentries = cronfile.readlines()
with open(TCRON, 'w') as cronfile:
for entry in cronentries:
if "scheduled_off" not in entry and "set_on-off_schedule" not in entry and \
"shutdown" not in entry and "rtcwake" not in entry:
cronfile.write(entry)
# Add entry for next shutdown and refreshes
with open(TCRON, 'a') as cronfile:
cronfile.write(f"{shutdown.minute} {shutdown.hour} {shutdown.day} {shutdown.month} *"
f" $SCHEDULED_OFF_SCRIPT {MODE} {off_time}\n")
cronfile.write(f"{refresh.minute} {refresh.hour} {refresh.day} {refresh.month} *"
f" $ON_OFF_SCHEDULE_SCRIPT\n")
cronfile.write(f"{refresh2.minute} {refresh2.hour} {refresh2.day} {refresh2.month} *"
f" $ON_OFF_SCHEDULE_SCRIPT\n")
subprocess.run(["crontab", TCRON])
if os.path.exists(TCRON):
os.remove(TCRON)
# Check the product type and add a notification 5 minutes before shutdown on OS2BorgerPC machines
if plan['product'] == 'os2borgerpc':
# Set the notification text based on the chosen language
locale = "None"
if os.path.exists(LOCALE_FILE):
with open(LOCALE_FILE, "r") as file:
locale = file.read()
if 'LANG=sv' in locale or 'LANG="sv' in locale:
MESSAGE = 'VARNING: Den här datorn stängs av om fem minuter'
elif 'LANG=en' in locale or 'LANG="en' in locale:
MESSAGE = 'WARNING: This computer will shut down in five minutes'
else:
MESSAGE = 'ADVARSEL: Denne computer lukker ned om fem minutter'
# Find the time 5 minutes before shutdown
notify_time = shutdown - datetime.timedelta(minutes=5)
# Get current entries
USERCRON = "/etc/os2borgerpc/usercron"
# Remove old entries
with open(USERCRON, 'r') as cronfile:
cronentries = cronfile.readlines()
with open(USERCRON, 'w') as cronfile:
for entry in cronentries:
if "zenity" not in entry and "notify-send" not in entry:
cronfile.write(entry)
# Add notification for next shutdown
with open(USERCRON, 'a') as cronfile:
cronfile.write(f"{notify_time.minute} {notify_time.hour} {notify_time.day} {notify_time.month} *"
f" export DISPLAY=:0 && /usr/bin/zenity --warning --text '{MESSAGE}'\n")
subprocess.run(["crontab", "-u", "user", USERCRON])
if __name__ == "__main__":
main()
EOF
chmod 700 $ON_OFF_SCHEDULE_SCRIPT
cat < $ON_OFF_SCHEDULE_SERVICE
[Unit]
Description=OS2borgerPC on/off schedule service
[Service]
Type=simple
ExecStart=$ON_OFF_SCHEDULE_SCRIPT
[Install]
WantedBy=multi-user.target
EOF
systemctl enable --now "$(basename $ON_OFF_SCHEDULE_SERVICE)"
Beskrivelse
Opret tænd sluk tidsplan