Skip to content

Instantly share code, notes, and snippets.

@mjepronk
Created November 20, 2022 14:19
Show Gist options
  • Save mjepronk/6225093799c4c325268761404250d2e2 to your computer and use it in GitHub Desktop.
Save mjepronk/6225093799c4c325268761404250d2e2 to your computer and use it in GitHub Desktop.
Save energy by suspending your Linux machine during off-hours.
#!/usr/bin/env python
"""
Suspend the system from cron on a specified time, and wake-up the
system at another specified time. This script allows the user to
interrupt the shutdown by showing a GTK dialog.
COPYRIGHT AND LICENSE
=====================
Copyright (C) 2014-2022 by Matthias Pronk
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
PORTABILITY
===========
You need to have the Python 3 interpreter installed. No libraries
besides the standard included libraries are needed.
This script should run on most current GNU/Linux distributions that
use systemd. If you do not use systemd, you just need to adapt the
`suspend_system` procedure below (you just need to provide an
alternative suspend command).
INSTALL
=======
Install the zenity package on Debian/Ubuntu:
sudo aptitude install zenity
Or on Fedora:
sudo dnf install zenity
Add to your crontab (crontab -e):
DISPLAY=:0.0
# m h dom mon dow command
0 0 * * 1-5 sudo /path/to/bin/suspend-system.py --wake-up 08:00
0 2 * * 6-7 sudo /path/to/bin/suspend-system.py --wake-up 10:00
If you add the script to the crontab of root (or the system crontab),
you do not need sudo, and you're done now.
Otherwise, add the following line to /etc/sudoers:
myusername ALL=NOPASSWD:/path/to/bin/suspend-system.py
"""
import os
import sys
import argparse
import subprocess
import time
import datetime
import calendar
# When to wake-up the system again using BIOS. The first integer is
# the hour (0-23), the second the minute (0-59).
DEFAULT_WAKEUP_TIME = '08:00'
# Linux RTC interface (https://www.kernel.org/doc/Documentation/rtc.txt)
ALARM_FILE = '/sys/class/rtc/rtc0/wakealarm'
SECONDS_SINCE_EPOCH_FILE = '/sys/class/rtc/rtc0/since_epoch'
# Cancel suspend dialog
DIALOG_TIMEOUT_SECS = 30
DIALOG_TITLE = "System suspend pending..."
DIALOG_MESSAGE ="System is going down in %i seconds. " \
"Press 'Cancel' to abort suspend."
# Paths to binaries
ZENITY_BIN = '/usr/bin/zenity'
SYSTEMCTL_BIN = '/bin/systemctl'
def suspend_system():
"""
Suspend the system. This uses systemd, please adapt if your system
does not use systemd.
"""
subprocess.call([SYSTEMCTL_BIN, 'suspend'])
def schedule_wakeup(wakeup_datetime):
"""
Schedule wake-up using functionality from the machine's BIOS
(using Linux RTC interface).
"""
with open(ALARM_FILE, 'w') as f:
f.write('0')
with open(ALARM_FILE, 'w') as f:
wakeup_timestamp = calendar.timegm(wakeup_datetime.utctimetuple())
f.write('{}'.format(wakeup_timestamp))
def user_is_root():
"""
Check if user is root.
"""
if os.geteuid() != 0:
return False
return True
def user_cancel_suspend():
"""
Use Zenity to show a dialog on the primary X11 display that allows
the user to abort the suspend procedure.
"""
cmd = [
ZENITY_BIN, '--progress', '--percentage=100', '--auto-close',
'--title', DIALOG_TITLE, '--text', DIALOG_MESSAGE %
DIALOG_TIMEOUT_SECS]
process = subprocess.Popen(cmd, stdin=subprocess.PIPE)
for i in range(19, 0, -1):
progress = '%i%%' % (i*5)
try:
process.stdin.write('{}\n'.format(progress).encode('utf-8'))
process.stdin.flush()
except IOError:
break
time.sleep(DIALOG_TIMEOUT_SECS / 19.0)
process.stdin.close()
# Check if the user wants to cancel suspend
if process.wait() != 0:
return True
return False
def get_rtc_localtime_delta():
"""
Calculate the difference between the RTC and localtime in minutes,
rounded to a precision of 30 minutes and return it as a Python
datetime.timedelta.
"""
with open(SECONDS_SINCE_EPOCH_FILE, 'r') as f:
rtc_timestamp = int(f.read())
local_timestamp = calendar.timegm(datetime.datetime.now().utctimetuple())
offset_min = (int(rtc_timestamp) - int(local_timestamp)) / 60.0
return datetime.timedelta(minutes=round(offset_min / 30.0) * 30.0)
def get_wakeup_datetime(time_str):
"""
Get Python datetime.datetime object for wake-up time. If the time
given has yet to come today, it will use today, if the time has
already passed today, it will get the datetime for tomorrow.
"""
now = datetime.datetime.utcnow()
h, m = [int(x) for x in time_str.split(':')]
wakeup_kwargs = dict(
hour=h, minute=m, second=0,
microsecond=0)
if now.replace(**wakeup_kwargs) > now:
return now.replace(**wakeup_kwargs)
else:
tomorrow = now + datetime.timedelta(days=1)
return tomorrow.replace(**wakeup_kwargs)
def main():
parser = argparse.ArgumentParser(
description='Save energy by suspending your machine during off-hours.')
parser.add_argument(
'-w', '--wake-up', dest='wakeup_time', default=DEFAULT_WAKEUP_TIME,
help='The time to wake-up the system in your local timezone. Use a colon to seperate hours and minutes, for example: 08:30 or 23:59, default is %s.' % DEFAULT_WAKEUP_TIME)
parser.add_argument(
'-d', '--dry-run', action='store_true',
help='Do not actually suspend the system.')
args = parser.parse_args()
if not user_is_root():
print("This script should be run as root.")
sys.exit(1)
if user_cancel_suspend():
print("Suspend cancelled!")
sys.exit(2)
wakeup_datetime = get_wakeup_datetime(args.wakeup_time)
rtc_wakeup_datetime = wakeup_datetime + get_rtc_localtime_delta()
schedule_wakeup(rtc_wakeup_datetime)
print("Waking up again at {} local time (that is {} on your hardware clock).".format(
wakeup_datetime, rtc_wakeup_datetime))
if not args.dry_run:
suspend_system()
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment