Automatically dimming your monitor at night

May 7, 2019

I have a LG 27” 4K monitor, which is great in the day time, but at night it’s rather bright. I use Redshift to automatically reduce the colour temperature of what is displayed on the screen, but the monitor is still too bright on full brightness. It turns out the brightness of external monitors can be controlled via what’s called the Display Data Channel.

Note that my monitor is connected via HDMI - I assume this would work via DisplayPort too. I’ve tested this on a desktop and on a laptop (connected via a Thunderbolt 3 dongle) and it works flawlessly. This article will explain how to control it, and hook it into Redshift so that your monitor automatically dims at night.

First you need to setup the interface with the Display Data Channel which is exposed via the I²C bus. Most desktop distributions don’t have it enabled by default so you need to do some setup. First check if the I²C devices exist:

# ls -la /dev/i2c-*
#

If there is nothing, the module is probably not loaded, so load the module and set it to automatically load at boot time:

# modprobe i2c-dev
# cat > /etc/modules-load.d/i2c.conf
i2c-dev
[Ctrl-d]
#

Then check again, and you should see a number of devices:

# ls -la /dev/i2c-*
crw-rw---- 1 root root 89, 0 May  7 21:04 /dev/i2c-0
crw-rw---- 1 root root 89, 1 May  7 21:04 /dev/i2c-1
crw-rw---- 1 root root 89, 2 May  7 21:04 /dev/i2c-2
crw-rw---- 1 root root 89, 3 May  7 21:04 /dev/i2c-3
crw-rw---- 1 root root 89, 4 May  7 21:04 /dev/i2c-4
crw-rw---- 1 root root 89, 5 May  7 21:04 /dev/i2c-5
crw-rw---- 1 root root 89, 6 May  7 21:04 /dev/i2c-6
#

Note that the devices are owned by root, so let’s add a new group, add ourselves to it, and setup udev to grant permissions to access I²C:

# groupadd i2c
# cat > /etc/udev/rules.d/99-i2c.rules
SUBSYSTEM=="i2c-dev", GROUP="i2c", MODE="0660"
[Ctrl-d]
# udevadm trigger
# ls -la /dev/i2c-*
crw-rw---- 1 root i2c 89, 0 May  7 21:20 /dev/i2c-0
crw-rw---- 1 root i2c 89, 1 May  7 21:20 /dev/i2c-1
crw-rw---- 1 root i2c 89, 2 May  7 21:20 /dev/i2c-2
crw-rw---- 1 root i2c 89, 3 May  7 21:20 /dev/i2c-3
crw-rw---- 1 root i2c 89, 4 May  7 21:20 /dev/i2c-4
crw-rw---- 1 root i2c 89, 5 May  7 21:20 /dev/i2c-5
crw-rw---- 1 root i2c 89, 6 May  7 21:20 /dev/i2c-6
[Ctrl-d]
$ sudo usermod -aG i2c $USER
$ newgrp i2c

Now let’s start exploring. The tool we need to control the Display Data Channel is called ddccontrol. If this isn’t in your package manager, you can compile it from source.

We can have it automatically probe the I²C devices to find monitors with this command:

$ ddccontrol -p
ddccontrol version 0.4.4
Copyright 2004-2005 Oleg I. Vdovikin (oleg@cs.msu.su)
Copyright 2004-2006 Nicolas Boichat (nicolas@boichat.ch)
This program comes with ABSOLUTELY NO WARRANTY.
You may redistribute copies of this program under the terms of the GNU General Public License.

Probing for available monitors..I/O warning : failed to load external entity "/usr/share/ddccontrol-db/monitor/GSM5B08.xml"
Document not parsed successfully.
.I/O warning : failed to load external entity "/usr/share/ddccontrol-db/monitor/AUO243D.xml"
Document not parsed successfully.
....
Detected monitors :
 - Device: dev:/dev/i2c-4
   DDC/CI supported: Yes
   Monitor Name: LG Standard LCD
   Input type: Digital
  (Automatically selected)
 - Device: dev:/dev/i2c-3
   DDC/CI supported: No
   Monitor Name: VESA standard monitor
   Input type: Digital
Reading EDID and initializing DDC/CI at bus dev:/dev/i2c-4...
I/O warning : failed to load external entity "/usr/share/ddccontrol-db/monitor/GSM5B08.xml"
Document not parsed successfully.

EDID readings:
...
$

(If it returns an error saying there are no monitors detected, try running it as root. If that finds the monitor, check the permissions of the I²C devices and that you are part of the i2c group - you may need to logout and login again).

In my case it found two monitors, as I’m running this on a laptop. The external monitor for me is /dev/i2c-4. The internal monitor is not supported by this tool, but you can use xbacklight to control the brightness of most laptop monitors.

The errors that were spat out can safely be ignored. The final part of the output - which I’ve skipped - is a list of all the detected settings that can be read and written. In my case the tool can also control the color levels, input source, speaker volume, power control and degauss (erm, on an LCD?).

The setting we want to configure is the brightness, which on VESA compatible monitors (basically any external monitor) is at address 0x10. The value can be read and written as follows:

$ ddccontrol -r 0x10 dev:/dev/i2c-4
...
Reading 0x10...
Control 0x10: +/20/100 C [Brightness]
$ ddccontrol -r 0x10 -w 60 -f dev:/dev/i2c-4
...
Writing 0x10, 0x3c(60)...
Control 0x10: +/60/100 C [Brightness]

If you have brightness keys on your keyboard, you may want to hook this up to those.

Now on to the end goal: automatic brightness control. As I said at the beginning I already have Redshift setup to control the temperature of what is displayed on the screen, so if you haven’t go do that now. Redshift supports hooks which are scripts executed whenever the time period changes. Here is a script to automatically change the brightness based on the time of day:

#!/bin/bash

# Sets the brightness to 0 - 100
set () {
  ddccontrol -r 0x10 -w $1 -f dev:/dev/i2c-4 > /dev/null 2>&1
}

# Returns the brightness 0 - 100
read () {
  ddccontrol -r 0x10 dev:/dev/i2c-4 2>&1| grep "Control 0x10" | awk -F "/" ' { print $2 } '
}

case $1 in
  period-changed)
    target=0

    case $3 in
      night)
        target=30
        ;;
      transition)
        target=50
        ;;
      daytime)
        target=100
        ;;
      *)
        (>&2 echo "Unknown target period $3")
        exit 1
        ;;
    esac

    current=$(read)

    case $2 in
      none)
        (>&2 echo "Change from $current to $target")
        set $target
        ;;
      *)
        (>&2 echo "Transition from $current to $target")
        while [[ $current -ne $target ]]; do
          if [[ $current -gt $target ]]; then
            ((current -= 1))
          else
            ((current += 1))
          fi

          set $current
        done
      ;;
    esac
  ;;
esac

Save this to ~/.config/redshift/hooks/brightness.sh, and make it executable. And that’s it! Whenever a transition occurs this will adjust the brightness one percentage point at a time, to provide a smooth transition that is practically unnoticable. Let’s test it - to transition from daytime to night:

~/.config/redshift/hooks/brightness.sh period-changed na night
Transition from 60 to 30

And back to daytime again:

~/.config/redshift/hooks/brightness.sh period-changed na daytime
Transition from 30 to 100

Enjoy not burning your eyes at night :)