Circadian Lightning is the concept of adapting the light in your home to follow the human circadian rhythm. So the light will change from warm white in the morning, to cold white in the afternoon, and back to warm white in the evening. My goal was to imitate this concept at my desk while working from home. I found this nice Home Assistant plugin that I’m using. For the hardware I chose the Mi Smart Led Desk Lamp Pro – it runs an ESP32 and can easily be flashed with ESPHome.

The blog post is divided into four steps:

  1. Hardware setup
  2. Software setup
  3. Flash Mi Smart Led Desk Lamp Pro
  4. Automation configuration

Hardware setup

I run the following hardware:

  • Raspberry Pi running Home Assistant
  • Mi Smart Led Desk Lamp Pro
  • Esp-prog board for flashing the ESP32 with ESPHome

To open the lamp you have to remove 4 screws on the bottom which are under the rubber stickers. Then you can access the board with the ESP32 directly. The pins to flash the ESP32 are on the bottom and therefore there is no need to take the PCB out.

These pins are needed:

  • TXD – transmit pin for UART
  • RXD – receive pin for UART
  • GPIO 0 – set ESP32 to boot mode
  • Enable – used to restart the microcontroller
  • GND – Ground
  • VDD – to apply voltage

If you are as smart as me and rip out one of the pads on the back you need to solder the pins directly to the chip. To reach the ESP32 remove the two screws on the back of the PCB and pop it out. This works fine too. Next, I connected all the pins to their corresponding pins on the esp-prog board.

Software setup

ESPHome has a standard configuration for the lamp which I used as a starting point. This configuration can’t be built on the Raspberry Pi. So I built the configuration on my laptop and uploaded it to the lamp. First I pulled the latest ESPHome Docker image

docker pull esphome/esphome

I run the docker container and access the ESPHome web interface.

docker run --rm --net=host -it esphome/esphome

Flashing it the first time, I downloaded the binary and uploaded it via the esp-prog. Once ESPHome is installed on the lamp I used the install wireless feature. If I want to update the firmware I run the docker container and paste the current (or new) configuration as a new device. ESPHome picks it up, connects to it, and it can upload a new configuration.

Flash Mi Smart Led Desk Lamp Pro

Since I run ESPHome in an additional container I need to put in the Wifi credentials in clear text. I added two things to the default configuration:

  • only sending commands if the value of the rotary sensor is not 0. Because the value is set back to 0 the default configuration sends each command twice.
  • add a switch interface which can be changed with a double click on the physical knob on the lamp. If the switch is on the color temperature can’t be changed via the knob.

Here is the configuration file:

esphome:
  name: xiaomi-lamp-pro-1

esp32:
  board: esp32doit-devkit-v1
  framework:
    type: esp-idf
    version: recommended
    sdkconfig_options:
      CONFIG_FREERTOS_UNICORE: y
    advanced:
      ignore_efuse_mac_crc: true

# Enable logging
logger:
  level: VERBOSE

ota:
  password: "**************"

wifi:
  ssid: ************
  password: ************

# Enable fallback hotspot (captive portal) in case wifi connection fails
api:
  reboot_timeout: 0s

sensor:
  - platform: rotary_encoder
    id: rotation
    pin_a: GPIO26
    pin_b: GPIO27
    resolution: 2
    on_value_range:
      - below: -0.5
        then:
          - if:
              condition:
                and:
                - binary_sensor.is_on: on_off
                - sensor.in_range:
                    id: rotation
                    above: 0
                - switch.is_off: xiaomi_lamp_pro_1_circadian
              then:
                # If Button is pressed, change CW/WW
                - lambda: |-
                    auto min_temp = id(light1).get_traits().get_min_mireds();
                    auto max_temp = id(light1).get_traits().get_max_mireds();
                    auto cur_temp = id(light1).current_values.get_color_temperature();
                    auto new_temp = max(min_temp, min(max_temp, cur_temp + (x*20)));
                    auto call = id(light1).turn_on();
                    call.set_color_temperature(new_temp);
                    call.perform();
              else:
                # If Button is not pressed, change brightness
                - light.dim_relative:
                    id: light1
                    relative_brightness: !lambda |-
                      return x / 20.0;
          # Reset Rotation to 0
          - sensor.rotary_encoder.set_value:
              id: rotation
              value: 0
      - above: 0.5
        then:
          - if:
              condition:
                and:
                - binary_sensor.is_on: on_off
                - sensor.in_range:
                    id: rotation
                    above: 0
                - switch.is_off: xiaomi_lamp_pro_1_circadian
              then:
                # If Button is pressed, change CW/WW
                - lambda: |-
                    auto min_temp = id(light1).get_traits().get_min_mireds();
                    auto max_temp = id(light1).get_traits().get_max_mireds();
                    auto cur_temp = id(light1).current_values.get_color_temperature();
                    auto new_temp = max(min_temp, min(max_temp, cur_temp + (x*20)));
                    auto call = id(light1).turn_on();
                    call.set_color_temperature(new_temp);
                    call.perform();
              else:
                # If Button is not pressed, change brightness
                - light.dim_relative:
                    id: light1
                    relative_brightness: !lambda |-
                      return x / 20.0;
    on_value:
      then:
          # Reset Rotation to 0
          - sensor.rotary_encoder.set_value:
              id: rotation
              value: 0

binary_sensor:
  - platform: gpio
    id: on_off
    pin:
      number: GPIO33
      inverted: True
      mode: INPUT_PULLDOWN
    on_click:
      then:
        - light.toggle: light1
    on_double_click:
      then:
        - switch.toggle: xiaomi_lamp_pro_1_circadian


switch:
  - platform: template
    optimistic: true
    id:  xiaomi_lamp_pro_1_circadian
    name: "enable circadian ligthning xiaomi lamp pro 1"

output:
  - platform: ledc
    pin: GPIO2
    id: output_cw
    power_supply: power
    frequency: 9765Hz
  - platform: ledc
    pin: GPIO4
    id: output_ww
    power_supply: power
    frequency: 9765Hz

power_supply:
  - id: power
    pin: GPIO12
    enable_time: 0s
    keep_on_time: 0s

light:
  - platform: cwww
    id: light1
    default_transition_length: 0s
    constant_brightness: true
    name: "Mi Desk Lamp Pro"
    cold_white: output_cw
    warm_white: output_ww
    cold_white_color_temperature: 4800 K
    warm_white_color_temperature: 2500 K #2500k is the original value of the lamp. To correct binning for 2700k to look more like 2700k use 2650k instead
    restore_mode: RESTORE_DEFAULT_OFF
    gamma_correct: 1
    

Automation configuration

Now it’s time for magic. I installed the circadian lightning component and configured a switch in the configuration.yaml file. The color temperatures are within the limits of the lamp. As soon as the switch is turned on it controls the color temperature of the lamp. And as a result, I have circadian lightning at my desk

circadian_lighting:
  min_colortemp: 2500
  max_colortemp: 4800
 
switch:
  - platform: circadian_lighting
    name:  circadian_lighting_mi_desk_lamp_pro_1
    disable_brightness_adjust: true
    lights_ct:
      - light.mi_desk_lamp_pro_1

Now there are two individual switches, one on the lamp and one which controls the circadian lightning. The final step is to create an automation to synch both of them – so I can control the circadian lightning with the lamp itself.

alias: sync circadian lightning - xiaomi lamp pro
description: ""
trigger:
  - platform: device
    type: changed_states
    device_id: *******************
    entity_id: switch.enable_circadian_ligthning_xiaomi_lamp_pro
    domain: switch
condition: []
action:
  - if:
      - condition: device
        type: is_on
        device_id: *******************
        entity_id: switch.enable_circadian_ligthning_xiaomi_lamp_pro
        domain: switch
    then:
      - service: switch.turn_on
        data: {}
        target:
          entity_id: switch.circadian_lighting_circadian_lighting_mi_desk_lamp_pro
    else:
      - service: switch.turn_off
        data: {}
        target:
          entity_id: switch.circadian_lighting_circadian_lighting_mi_desk_lamp_pro
mode: single

Conclusion

I now enjoy circadian lightning at my desk. Controlling the brightness is still up to me since I might work late in the evening and don’t want to sit in the dark. It works great and I can already tell a difference in concentration during the day. The lamp itself is very bright and comfortable to work with. Considering I had Home Assistant already running and the esp-prog board is the solution quite affordable. You can find it new for around 60-70€ or buy a used one like myself since you won’t be able to claim a warranty after tinkering with it.