Python smbus2 — DimmerLink control script for Raspberry Pi

Right, so following on from my previous thread about getting DimmerLink detected on the Pi — I’ve now got a working Python script using smbus2. Sharing it here for anyone else running DimmerLink on a Raspberry Pi.

The script controls two channels and includes a simple fade function:

from smbus2 import SMBus
import time

DL_ADDR = 0x50
DL_CH1 = 0x01
DL_CH2 = 0x02

bus = SMBus(1)

def set_dimmer(channel, power):  # power 0-100
    bus.write_byte_data(DL_ADDR, channel, power)

def fade(channel, target, step_ms=20):
    current = 0
    while current <= target:
        set_dimmer(channel, current)
        time.sleep(step_ms / 1000)
        current += 1

# Example usage
set_dimmer(DL_CH1, 75)   # channel 1 to 75%
fade(DL_CH2, 100, 15)    # fade channel 2 to 100% in 15ms steps

bus.close()

Works nicely on Pi Zero 2W with Bookworm. Install smbus2 with pip install smbus2 if you haven’t already.

Before anyone asks — yes, I’ve switched the DimmerLink to I2C mode first (see my other thread). The default I2C address is 0x50.

Would appreciate a code review if anyone spots issues. I’m a sysadmin not a Python dev so there might be things I’m doing wrong here.

Issue: no error handling around write_byte_data. If the I2C bus has a glitch or the DimmerLink doesn’t ACK, write_byte_data raises OSError. Your script will crash with no useful output.

Wrap the I2C write in a try/except:

def set_dimmer(channel, power):
    try:
        bus.write_byte_data(DL_ADDR, channel, power)
    except OSError as e:
        print(f'I2C write error on channel {channel}: {e}')

This is especially important for the fade function — a single failed write at step 47 of 100 will kill the entire fade instead of skipping one step and continuing.

Also add a bounds check. write_byte_data sends one byte so values above 255 will cause an OverflowError, but the DimmerLink expects 0-100. Anything above 100 is undefined behaviour on the module side:

def set_dimmer(channel, power):
    power = max(0, min(100, power))
    try:
        bus.write_byte_data(DL_ADDR, channel, power)
    except OSError as e:
        print(f'I2C write error on channel {channel}: {e}')

See the smbus2 docs for the full list of exceptions that write_byte_data can raise.

One more issue: bus = SMBus(1) at module level with bus.close() at the end works for a simple script, but if an exception occurs between open and close, the bus handle leaks.

The smbus2 library supports context managers. Safer pattern:

def set_dimmer(channel, power):
    power = max(0, min(100, power))
    try:
        with SMBus(1) as bus:
            bus.write_byte_data(DL_ADDR, channel, power)
    except OSError as e:
        print(f'I2C write error on channel {channel}: {e}')

With with SMBus(1) as bus: the bus is automatically closed when the block exits, even if an exception occurs. No need for explicit bus.close() and no leaked file descriptors.

For the fade function, opening and closing the bus on every step adds some overhead. Alternative: open the bus once for the entire fade with a context manager wrapping the loop:

def fade(channel, target, step_ms=20):
    with SMBus(1) as bus:
        for power in range(target + 1):
            try:
                bus.write_byte_data(DL_ADDR, channel, max(0, min(100, power)))
            except OSError as e:
                print(f'I2C error at step {power}: {e}')
            time.sleep(step_ms / 1000)

Environment where I tested: Raspberry Pi 4B, Bookworm, Python 3.11, smbus2 0.4.3.

Both excellent points — I’ve updated the script with error handling and context managers.

Here’s the final version:

from smbus2 import SMBus
import time

DL_ADDR = 0x50
DL_CH1 = 0x01
DL_CH2 = 0x02

def set_dimmer(channel, power):
    power = max(0, min(100, power))
    try:
        with SMBus(1) as bus:
            bus.write_byte_data(DL_ADDR, channel, power)
    except OSError as e:
        print(f'I2C write error on channel {channel}: {e}')

def fade(channel, target, step_ms=20):
    target = max(0, min(100, target))
    with SMBus(1) as bus:
        for power in range(target + 1):
            try:
                bus.write_byte_data(DL_ADDR, channel, power)
            except OSError as e:
                print(f'I2C error at step {power}: {e}')
            time.sleep(step_ms / 1000)

if __name__ == '__main__':
    set_dimmer(DL_CH1, 75)
    fade(DL_CH2, 100, 15)
    print('Done')

No more leaked bus handles and I2C errors get caught instead of crashing the script. Cheers Kai and Yuki — proper code review that was.

For anyone looking for the full Raspberry Pi + DimmerLink guide, the rbdimmer docs page covers everything from wiring to I2C mode switching to Python examples:
https://rbdimmer.com/faq/raspberry-pi-ac-dimmer-control-via-dimmerlink-28

[SOLVED]

Verified. The revised script handles both points correctly:

  1. OSError caught in both set_dimmer and fade — I2C bus errors won’t crash the script
  2. Context manager (with SMBus(1) as bus:) ensures proper cleanup in all cases
  3. Bounds check max(0, min(100, power)) prevents undefined DimmerLink behaviour

Tested on my Pi 4B — fade runs smoothly with no resource warnings. Good final version.