Saving Energy: Home Server That Automatically Suspends to RAM and Wakes Up Again

... which is used for Plex media streaming and Time Machine backups.
Last update: March 2023
Author: Maximilian Golla
Keywords: Wake-on-LAN (WOL), Suspend to RAM (S3), Plex, Time Machine, Home Assistant (HASS), Saving Energy, Python, Ubuntu

43 Watts Is Just Too Much!

It does not sound like much, but running a 24/7 home server that draws 43 watts in idle is quiet expensive, considering the latest electricity prices of about 49 euro cent/kWh here in Germany (as of October, 2022). A simple solution to this problem is a script that automatically suspends the server, when it is not used, and another script to wake the server up again, in case there is work to do. To my surprise I could not find any out-of-the-box solution, so I thought, it is a worthwhile effort to write about it.

 Hello HN: Because so many people asked for it, the server specs are: Intel i5-11400; ASRock B560 Pro4; Crucial DDR4 2x 8GB; 5x disks (1x M.2 NVMe SSD, 3x 5400 RPM, 1x 7200 RPM). I update the UEFI and activated Active-State Power Management (ASPM), then I installed PowerTOP 2 and tuned some settings (e.g., enabled audio codec and SATA link power management). This way I reduced the consumption to 39 watts in idle and only 2 watts during sleep. I also decided to install hd-idle, a software that suspends the hard disks after 5 minutes of inactivity, which reduced the power consumption from 39 to just 23 watts during idle.

Power Consumption Comparison

 By the way: Why aren't there any good solutions out there? Why isn't this build into every major operating system by now? Why isn't this the default (or at least a configuration option)? Seriously, there is soooo much potential to save energy in IT.

Requirements & Specification

Currently, the server is primarily used for two things:

So I was forced to find a way to automatically suspend the server, but prevent it from sleeping, if any of the two services are in use. Moreover, I needed a way to wake up the server remotely, so in case that I am not at home, I could still access Plex, which led to the need for the following two components:

  1. A script that suspends the server automatically, in case the server is not used
  2. A (mobile friendly) website that allows to wake up the server, in case I need it

Finally, I do have a Raspberry Pi 4 Model B 4GB at home that is running all the time. This Pi is used for hosting Home Assistant (HASS) and Pi-hole and it could become very handy for monitoring the sleep state of the server and hosting the wake up website.

The Idea

The high-level idea that came to my mind looks similar to this: Macs in my local network access the server via SMB for Time Machine backups. From remote, I access Plex via HTTPS for streaming. In case the server sleeps, I need a convenient solution to wake the server remotely via my smartphone. The Raspberry Pi is running 24/7 and already hosts Home Assistant that offers a Wake on LAN and Device tracker integration.

Network Architecture

The Implementation (on the Shoulders of Giants)

Fast forward a number of weeks testing various options, my setup currently looks like this: The server implements a ring buffer and checks for activities once per minute. To monitor Plex activities, we access the local Plex API and for Time Machine we simply monitor any file access at /mnt using lsof. In case there has been no activity for 15 consecutive minutes, the server goes to sleep. (Nobody streams, pausing a video doesn't count as activity, and no backup is running.) A Web server on the Raspberry Pi hosts a website that obtains the current state of the server provided via the Home Assistant REST API. In case the server sleeps, and I like to backup or stream something, I can wake the server using a simple button press that sends a magic packet using a wakeonlan Perl script.

Software Architecture


How to Sleep (And When Better Not)

Monitoring Backups:

My backup disk is mounted under /mnt/backup. We monitor whether there is any file access on /mnt.

def is_someone_accessing_files_check():
    try:
        print("New file access check...having a look at '/mnt'")
        response = subprocess.getoutput("lsof -w /mnt/* | grep /mnt/ | wc -l")
        if response == '0':
            return False
        return True
    except Exception:
        return True

Monitoring Plex:

There are many possible solutions to this problem. The most reliable was to use the local Plex API. In contrast to simpler solutions this comes with the advantage that we can differentiate between active playbacks and people that paused the streaming (but still have the browser tab open, usually preventing the server from sleeping 😈). You can find your account authentication token for Plex here.

PLEX_AUTH_TOKEN = 'your-plex-auth-token'

def is_someone_streaming_check():
    """Query the Plex API and obtain the number and state of current sessions."""
    try:
        print("New streaming check...having a look at Plex sessions")
        my_headers = {}
        my_headers['X-Plex-Token'] = PLEX_AUTH_TOKEN
        my_headers['accept'] = 'application/json'
        url = 'http://127.0.0.1:32400/status/sessions'
        data = requests.get(url, headers=my_headers).json()
        size = None
        if 'MediaContainer' in data:
            if 'size' in data['MediaContainer']:
                size = data['MediaContainer']['size']
        print(f"Currently there are {size} users online ...")
        users = []
        if 'MediaContainer' in data:
            if 'Metadata' in data['MediaContainer']:
                for session in data['MediaContainer']['Metadata']:
                    user = {'User': None, 'State': None, 'Platform': None, 'IP': None, 'Started': None}
                    if 'User' in session:
                        if 'title' in session['User']:
                            user['User'] = session['User']['title']
                    if 'Player' in session:
                        if 'state' in session['Player']:
                            user['State'] = session['Player']['state']
                    ...
                    users.append(user)
        streaming = False
        for user in users:
            print(user)
            if user['State'] != 'paused':
                streaming = True
        return streaming
    except Exception:
        return True

Putting It All Together:

The script runs once per minute and suspends the server after 15 consecutive minutes without any activity by calling /usr/sbin/pm-suspend. To avoid the need to run the script as root, consider adding my_user ALL=NOPASSWD:/usr/sbin/pm-suspend to your /etc/sudoers file (then execute sudo pm-suspend).

###### 2022-10-14 15:35:56
New file access check...having a look at '/mnt'
File access: True
New streaming check...having a look at Plex sessions
Currently there are 2 users online ...
{'User': 'my_user1', 'State': 'paused',  'Platform': 'Firefox', 'IP': 'a.b.c.d', 'Started': '2022-10-14 12:29:18'}
{'User': 'my_user2', 'State': 'playing', 'Platform': 'Chrome',  'IP': 'a.b.c.d', 'Started': '2022-10-14 15:34:57'}
Streaming: True
Currently there are 14 activities in the past 15 minutes...
Will check again in 60 seconds...

How to Wake Up Again

Enable Wake on LAN (WoL):

There are many tutorials out there. Check your BIOS/UEFI first, then install ethtool, and configure a wol.service that re-enables WoL every time you reboot the machine.

The Web Interface

We create a mobile-friendly website that queries and displays the current server status using the Home Assistant REST API. It also offers a button that sends a magic packet using a wakeonlan Perl script (Python 3 version). Currently, there is no authentication in place. If you feel the need, it should be super easy to add HTTP Basic Authentication by tweaking the configuration of your Web server.
 Note: The Home Assistant Wake on LAN integration also allows to wake the machine, but I explicitly opted for this Perl script-setup, because it enables me to wake the server even in the case Home Assistant is down or currently unavailable.

Web Interface


Querying Home Assistant:

OMG, PHP! Yes, I know. 🤦 After installing and configuring the Home Assistant Wake on LAN integration, we can query the Home Assistant REST API. For authentication, you need a Long-Lived Access Token (valid for 10 years) that you can generate in your user profile (https://example.org/profile).

header('Content-Type: application/json; charset=utf-8');

$info = json_decode(getStateFromHASS(), true);
$state = $info['state'];
$last_changed = parseDate($info['last_changed']);
$output = json_encode(array('state' => $state, 'last_changed' => $last_changed));
echo $output; // {"state":"on","last_changed":"2022-10-14 10:17:07"}

function getStateFromHASS() {
    // Init
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, 'https://example.org/api/states/switch.wake_on_lan'); // Home Assistant's REST API
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer abc-secret-123...'));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    // Query
    $result = curl_exec($ch);
    curl_close($ch);
    return $result;  // {..., "state": "on", "last_changed": "2022-10-14T09:17:07.277838+00:00", ...}
}

Waking Up the Server:

To wake the server, we run the Perl script and hardcode the server's MAC address (shell_exec is  dangerous, do not allow user input here).
 Hint: First test the wakeonlan Perl script on your shell, before integrating and executing it via PHP.

header('Content-Type: application/json; charset=utf-8');

if (!empty($_GET['act'])) {
    // This is evil, we do not allow any user input here!
    // https://github.com/jpoliv/wakeonlan/blob/master/wakeonlan
    shell_exec('./wakeonlan DE:AD:BE:EF:12:34');
    $output = json_encode(array('info' => 'Wake up signal was sent.'));
} else {
    $output = json_encode(array('info' => 'No wake up signal sent.'));
}

echo $output; //{"info":"Wake up signal was sent."}

Configuring Time Machine:

Wake on LAN (WoL) only works when you use the hostname of the server, not its IP address. This way, macOS will automatically send a magic packet every time it starts a backup, which will wake the server. Big thanks to @jessikat for this hint.

Use the Hostname Not the IP Address for Time Machine Backups to Enable Wake on LAN (WoL)


Sharing is Caring

You can find the source code of all those scripts here:

Download Scripts from GitHub

I hope this is helpful for some 😊. There are properly many issues with it, so please feel free to fork and improve!


Questions and Answers:

  • How much energy have you saved with this already?
    In 140 days about 60 Euros. The sever was sleeping 89% of the time. Without the script 140 Days * 43 Watts * 24h = 144 kWh, which accounts for 70 Euros (assuming the 49 euro cent/kWh from the top). Instead, due to the script, the server slept for 125 days, and was only "idling" for about 15 days, resulting in about 27 kWh in total, which accounts for ~13 Euros.
  • What if Time Machine starts a backup, but the server is not available because it is sleeping?
    There is an easy solution for this. Just make sure that you use the hostname of the server, not its IP address. This way, macOS will automatically send a magic packet every time it starts a backup. Big thanks to @jessikat for this hint.
  • What if I start Plex on my TV, but forgot to wake the server?
    This is not an issue. Just wake the server, and after about 30 seconds Plex will show all your media and streaming content.