Saving Electricity by Planning AC in a Server Room

Published 16 September 2022

Historical article: This article describes a server-room cooling and monitoring solution built in 2022. Product availability, software packages and installation methods may have changed since then.

Servers in a server room need to be kept within a suitable temperature range to avoid reduced reliability and accelerated ageing of components. With increasing electricity prices, it also becomes important to minimise the energy used for cooling.

Standard server-room cooling

A typical small server room has a split air-conditioning unit. The servers heat the air inside the room and the AC unit removes that heat, maintaining a temperature of, for example, 20 °C.

If the heat load is significant, the air conditioner may need to run its compressor almost continuously.

Conventional server room cooling

Low-cost cooling

A split AC is sometimes replaced by a portable floor-standing air conditioner. It blows cool air into the room and exhausts hot air through a hose, typically through a window or wall.

Such a unit has a lower installation cost and can easily be replaced if it fails. However, because it exhausts air from the room, replacement air must enter from outside. This makes the arrangement less efficient when the outdoor temperature is high.

For a small or medium-sized unit, the difference in electrical consumption can be substantial: approximately 900 W with the compressor running compared with perhaps 50 W when only the internal fan is operating.

  • Low installation cost
  • Easy and quick to replace
  • Less efficient when the outside temperature is high
Portable air conditioner in a server room

Making use of outside air

In Sweden the outside air is often cold. Even in southern Sweden, temperatures are frequently below 10 °C during winter. Instead of running a refrigeration compressor, we can therefore use outside air directly to cool the servers.

In this example, outside air is drawn through an inlet in the ceiling in front of the server rack. If a portable AC is also used, this inlet supplies its replacement air during summer. The inlet should therefore preferably not be placed where it is heated by direct sunlight.

Outside air inlet in front of server rack

An air inlet alone is not sufficient during winter. The AC fan may be too small, or may not run at all when the compressor is stopped. An additional exhaust fan can therefore be installed behind the rack to remove the heated air from the room.

Server rack with outside air inlet and exhaust fan

Controlling the exhaust fan

If the AC setpoint is 20 °C, the compressor will eventually stop when the outdoor temperature becomes low enough. If the outside temperature rises again, however, the exhaust fan should not continue pulling warm outside air into the room.

A simple first condition could be:

FAN_ON = if (To < 16)

where To is the outside temperature.

A better approach is to compare the room temperature Ta with the outside temperature:

FAN_ON = if (Ta > To)

This requires at least two temperature sensors. Once sensors are available, the temperatures can also be logged and alarms generated if something goes wrong.

Cooling control using outside and inside temperature

We probably do not want the server-room temperature to follow the outside temperature all the way below freezing. A lower room-temperature limit can therefore be added. I used 12 °C:

FAN_ON = if (Ta > To) && (Ta > 12)
Server room cooling with minimum temperature limit

The ordinary AC remains as the upper temperature control and backup cooling system. The exhaust fan provides low-power cooling whenever the outside conditions allow it.

The TSTR04

While looking for a simple controller I found the TSTR04 relay board from TinySine. Since there are already powerful servers running nearby, it seemed unnecessary to add another complete Raspberry Pi or similar computer merely to control a fan.

The TSTR04 connects directly to a server through USB and has four onboard relays. It also supports four external waterproof temperature sensors. The relays can operate using an automatic thermostat function or can be controlled manually over USB.

The USB interface appears as a normal virtual COM port operating at 9600 baud, 8N1.

TinySine TSTR04 temperature and relay controller

Serial commands

A  Set Auto mode
B  Set Manual mode
C  Get working mode
D  Get state: 9 bytes, relay + temperature

O  Set ch 1 threshold: MSB+LSB(ON)+MSB+LSB(OFF)
P  Set ch 2 threshold: MSB+LSB(ON)+MSB+LSB(OFF)
Q  Set ch 3 threshold: MSB+LSB(ON)+MSB+LSB(OFF)
R  Set ch 4 threshold: MSB+LSB(ON)+MSB+LSB(OFF)
S  Set ch 5 threshold: MSB+LSB(ON)+MSB+LSB(OFF)
T  Set ch 6 threshold: MSB+LSB(ON)+MSB+LSB(OFF)
U  Set ch 7 threshold: MSB+LSB(ON)+MSB+LSB(OFF)
V  Set ch 8 threshold: MSB+LSB(ON)+MSB+LSB(OFF)

Z  Get version, 2 bytes: module ID + software version
[  Get relay states (1 byte)
\  Set relay states, 255 = all on

d  All relays to position 1
e  Relay 1 to position 1
f  Relay 2 to position 1
g  Relay 3 to position 1
h  Relay 4 to position 1
i  Relay 5 to position 1
j  Relay 6 to position 1
k  Relay 7 to position 1
l  Relay 8 to position 1

n  All relays to position 0
o  Relay 1 to position 0
p  Relay 2 to position 0
q  Relay 3 to position 0
r  Relay 4 to position 0
s  Relay 5 to position 0
t  Relay 6 to position 0
u  Relay 7 to position 0
v  Relay 8 to position 0

Some TSTR04 observations

  • The unit restarts when the serial port is opened under Windows. Startup takes approximately five seconds, during which it does not respond to commands. This makes a script periodically started from cron inconvenient.
  • The built-in thermostat Auto mode is intended for heating, i.e. TOFF > TON. Since the relays are SPDT, it can nevertheless be wired for cooling.
  • In Auto mode the relay does not activate if the unit starts inside the hysteresis band. For example, with TON=15°C and TOFF=30°C, starting at 20 °C does not activate the relay. This appears to be intentional behaviour.
  • The small display is OLED, which may suffer from ageing or burn-in during continuous operation.
  • The waterproof DS18B20 sensor wiring supplied with my unit was: red = VCC, yellow = DATA, green = GND.

I did not use Auto mode because my fan-control condition depends on two temperature measurements. For a simpler installation the internal thermostat could still be useful.

Creating an RRD database

The goal is to connect the sensor/relay board to a Linux server, let the server control the cooling fan and store temperature history for graphs.

RRDTool and its Python support were installed with:

apt-get install rrdtool python-rrdtool librrd-dev
pip3 install rrdtool

The database was created using:

#!/bin/bash

XDBXFILE=/var/log/temp_rack.rrd

if [ ! -f "$XDBXFILE" ]; then

    /usr/bin/rrdtool create "$XDBXFILE" \
        --step 150s \
        DS:temp1:GAUGE:180:-30:50 \
        DS:temp2:GAUGE:180:-30:50 \
        DS:temp3:GAUGE:180:-30:50 \
        DS:temp4:GAUGE:180:-30:50 \
        RRA:LAST:0.5:150s:7d \
        RRA:AVERAGE:0.5:1d:10y \
        RRA:MIN:0.5:1d:10y \
        RRA:MAX:0.5:1d:10y
fi

The resulting database is small — approximately 472 kB — while retaining years of temperature history.

Reading temperatures and controlling the fan

Since the fan-control decision requires measurements from two sensors, I controlled the TSTR04 manually from a Python program.

The program reads the sensors once per second, evaluates the relay state once per minute to avoid unnecessary relay and fan cycling, and updates the RRD database every 150 seconds.

# -*- coding: utf-8 -*-

import rrdtool
import time
from time import sleep
import serial

TSTRSerialPortName="/dev/ttyUSB2"

def print_hex_list(data):
    print('[{}]'.format(', '.join("0x%02x"%x for x in data)), end='')

def sbin(num):
    global b
    if(num > 1):
        sbin(num // 2)
    b=b+str(num % 2)

def Print_bin(n):
    global b
    b=""
    sbin(n)
    b="00000000"+b
    b=b[-8:]
    print(b)

def TSTR_ask(s):
    serportTSTR.reset_input_buffer()
    serportTSTR.write(bytes(s,'utf-8'))
    r=serportTSTR.read_until()
    return r

def TSTR_askfor(s,n):
    serportTSTR.reset_input_buffer()
    serportTSTR.write(bytes(s,'utf-8'))
    r=serportTSTR.read(n)
    return r

def Temp_Get():
    t={}
    v=TSTR_askfor('D',9)

    t[0]=0.0625*int.from_bytes(v[1:3], "little", signed=True)
    t[1]=0.0625*int.from_bytes(v[3:5], "little", signed=True)
    t[2]=0.0625*int.from_bytes(v[5:7], "little", signed=True)
    t[3]=0.0625*int.from_bytes(v[7:9], "little", signed=True)

    return t,v[0]

# Temp_SetMode(0): Auto
# Temp_SetMode(1): Manual
def Temp_SetMode(m):
    serportTSTR.write((b'A'[0]+m).to_bytes(1,'big'))

def Temp_GetMode():
    return int.from_bytes(TSTR_askfor('C',1),'big')-65

def Relay_Set(r,s):
    if s:
        serportTSTR.write((b'e'[0]+r).to_bytes(1,'big'))
    else:
        serportTSTR.write((b'o'[0]+r).to_bytes(1,'big'))

def Relay_AllOn():
    serportTSTR.write(b'd')

def Relay_AllOff():
    serportTSTR.write(b'n')

if __name__ == "__main__":

    serportTSTR=serial.Serial(
        port=TSTRSerialPortName,
        baudrate=9600,
        bytesize=8,
        timeout=1,
        stopbits=serial.STOPBITS_ONE)

    v=TSTR_ask('C')
    print("Starting...")
    time.sleep(4)

    serportTSTR.timeout=1

    print("Version:",end='')
    v=TSTR_askfor('Z',2)
    print_hex_list(v)
    print("")

    Temp_SetMode(1)
    Relay_AllOff()

    ticks=0
    ticks_relay=0

    while 1:

        t,r=Temp_Get()

        for i in range(0,4):
            print(str(ticks).zfill(4)+": T"+str(i)+": "+str(t[i]))

        print(str(ticks).zfill(4)+": Relay:",end="")
        Print_bin(r)

        if ticks_relay >= 60:
            ticks_relay=0

            # T0 = outside temperature
            # T1 = rack rear / exhaust temperature
            # T2 = rack front / inlet temperature

            if (t[2] > 12) and (t[0] < t[2]):
                Relay_Set(0,1)
            else:
                Relay_Set(0,0)

        if ticks >= 150:
            ticks=0
            rrdtool.update(
                '/var/log/temp_rack.rrd',
                'N:'+str(t[0])+":"+str(t[1])+":"+str(t[2])+":"+str(t[3])
            )

        time.sleep(1)
        ticks += 1
        ticks_relay += 1

    serportTSTR.close()
If the program is not run as root, the user normally needs permission to access /dev/ttyUSB2, for example by membership in the dialout group. The user also needs write access to the RRD database.

The sensor assignments in my installation were:

  • T0 = To — outside temperature
  • T1 = Te — rear of rack / exhaust air
  • T2 = Ta — front of rack / inlet air
  • T3 — additional room/side measurement

Typical output:

Starting...
Version:[0x0f, 0x02]

0000: T0: 12.125
0000: T1: 23.125
0000: T2: 16.1875
0000: T3: 20.5625
0000: Relay:0000

0001: T0: 12.1875
0001: T1: 23.1875
0001: T2: 16.3125
0001: T3: 20.5
0001: Relay:0001

Generating graphs

RRDTool can generate daily, weekly and yearly PNG graphs. In my installation the graph-generation script created:

rack_daily-temperature.png
rack_weekly-temperatures.png
rack_yearly-temperature_outside.png
rack_yearly-temperature_rear.png
rack_yearly-temperature_front.png
rack_yearly-temperature_side.png

The original script used rrdtool graph with separate AVERAGE, MIN and MAX data sources for the different time ranges. A resulting daily graph looked like this:

Daily server rack temperature graph

The blue marker at 12 °C shows the lower temperature limit. The exhaust fan regulates the front temperature so that it does not fall below this point. At the other end, the AC unit would start if the front temperature rose above its setpoint, for example 20 °C.

Displaying the graphs on a web page

I generated the graphs on demand from a PHP page. The essential part looked like this:

<?php

$cmd="sudo /var/www/lan/sh/temps/rack_generate_graphs.sh";

header('X-Accel-Buffering: no');

while (@ob_end_flush());

$proc = popen($cmd, 'r');

while (!feof($proc)) {
    echo fread($proc, 4096);
    @flush();
}

echo '
<img src="/temps/rack_daily-temperature.png?dummy=' . rand() . '">
<img src="/temps/rack_weekly-temperatures.png?dummy=' . rand() . '">
';

?>

The randomized ?dummy=... query parameter was used to prevent browsers, particularly Chrome on Android in my testing, from displaying an old cached graph.

Since the PHP process used sudo to execute the graph-generation script, the installation had a narrowly specified sudoers entry:

www-data ALL = NOPASSWD:/var/www/lan/sh/temps/rack_generate_graphs.sh
Allowing a web-server process to execute commands through sudo has security implications. If this approach is used, restrict the sudoers rule to the exact command and ensure that neither the script nor anything it executes is writable by the web-server user.

Result

The result is a relatively simple hybrid cooling system. During warm weather the air conditioner provides conventional cooling. When the outside air is sufficiently cold, a much lower-power exhaust fan takes over.

The same temperature sensors used for control also provide long-term monitoring, making it possible to verify the cooling behaviour and detect abnormal temperatures.