ESPHome for Aquarium Lights

Hi,

I have an existing setup where I control two channels of 12 CREE LEDs each (12 white and 12 blue) using an Arduino Uno. I made this quite some time ago and want to now shift to ESP32. Here is what I want to do.

  1. I want to control the start and end times of the white and blue channels separately.
  2. I want these timings to be controllable from HA. I have created Helper entries for these.
  3. I visualise the total timing for each channel to be 6 hours, broken up into two sections of 3 hours each - from 0 to x% for 3 hours and x% to 0 for the next 3 hours.
  4. My design is to a) use Number to store and update the brightness level of each channel b) check if the current time on the ESPHome end is equal to the time set in HA for each channel separately, c) turn on the light and d) run a script to increase the brightness at a certain frequency till I reach the target percentage.

My Number part of the code looks like this…

number:
  - platform: template   
    name: Ramp Up Light Target Brightness White
    id: ramp_up_brightness_1
    min_value: 0
    max_value: 1
    step: 0.001
    initial_value: 0
    update_interval: 60s
    optimistic: true

My time check part of the code looks like this…

- lambda: >-
          float start_time_seconds = id(start_time_helper_timestamp_white).state;
          int current_hour = id(ha_time).now().hour;
          int current_minute = id(ha_time).now().minute;
          int current_time_seconds = current_hour * 3600 + current_minute * 60;
          return current_time_seconds == (int)start_time_seconds;

Here, ‘start_time_helper_timestamp_white’ is the Id of the Start Time Helper set in HA.

My script to increase the brightness is something like this…

script:
  - id: gradual_brightness_increase_1
    mode: single
    sequence:
      - repeat:
          while:
            - lambda: return id(ramp_up_brightness_1).state < 0.72;
          sequence:
            - delay: 60s
            - number.set:
                id: ramp_up_brightness_1
                value: return id(ramp_up_brightness_1).state + 0.004;
            - light.turn_on:
                id: output_led_1
                brightness: >-
                  return id(ramp_up_brightness_1).state; 
                transition_length: 60s
            - light.turn_on:
                id: output_led_3
                brightness: >-
                  return id(ramp_up_brightness_1).state; 
                transition_length: 60s

I am able to understand the sequence in which these actions are to be taken, however not being a programmer myself, I wish to check if a) my approach is correct and b) how do I combine these three - the Number, the Time Check and the Script in one single flow of logic ? Meaning - if the time equals the start time, turn on the lights, update the brightness and send that number via the PWM pin to the LED driver ?

I suggest you to read esphome documentation. Instead of sequence, esphome uses then.

You don’t necessarily need to use template number for brightness.
As simplest form it would be just light.turn_on with 3 hours transition length and then back. I don’t see it best approach though .
I personally would use something like this (not tested):

script:
  - id: six_hour_cycle
    mode: single
    then:
      - repeat:
          count: 180
          then:
            - light.turn_on:
                id: output_led_1
                brightness: !lambda "return (iteration + 1) * 0.004;"
                transition_length: 60s
            - delay: 60s

      - repeat:
          count: 180
          then:
            - light.turn_on:
                id: output_led_1
                brightness: !lambda "return 0.72 - ((iteration + 1) * 0.004);"
                transition_length: 60s
            - delay: 60s
      - light.turn_off: output_led_1

You could also look at using a ESPHome datetime helper along with on_time.

In many cases it’s best to do as much as you can on the esp rather than on the HA side.

1 Like

Thank you so much Karosm. I learnt quite a number of new things - repeat and the entire usage of the Light component.

I spent the day using your suggestion and modifying the code. It finally compiled 5 mins back. Will test it overnight and share the outcome.

Thanks Mahko. You are right to suggest keeping most of the things on the ESP side. Have tried Karosm’s suggestion in the meanwhile…lets see how it works.

You’re welcome. And I totally agree with Mahko to keep everything possible on esphome side for several reasons.

@Karosm , here is the code that compiled…

esphome:
  name: esphome-web-b3b444
  friendly_name: Aquarium Light Controller
  min_version: 2025.11.0
  name_add_mac_suffix: false

esp32:
  variant: esp32
  framework:
    type: esp-idf

# Enable logging

logger: null

packages:
  general_settings: !include general_settings.yaml  
  
time:
  - platform: homeassistant
    id: ha_time

i2c:
  sda: GPIO21
  scl: GPIO22
  scan: true

display:
  - platform: lcd_pcf8574
    id: lcd_display
    dimensions: 16x2
    address: 0x27
  
datetime:
  - platform: template
    name: "Turn on White LED "
    id: turn_on_time_white 
    type: time   
    optimistic: true 
  - platform: template
    name: "Turn on Blue LED "
    id: turn_on_time_blue 
    type: time   
    optimistic: true   
 
output:
  - platform: ledc
    pin: GPIO14
    id: output_led_1
    frequency: 2000Hz

  - platform: ledc
    pin: GPIO16
    id: output_led_2
    frequency: 2000Hz

  - platform: ledc
    pin: GPIO17
    id: output_led_3
    frequency: 2000Hz
    
light:
  - platform: monochromatic
    name: White LED 1
    output: output_led_1
    gamma_correct: 2.8
    id: white_led1

  - platform: monochromatic
    name: Blue LED 1
    output: output_led_2
    gamma_correct: 2.8
    id: blue_led2
    
  - platform: monochromatic
    name: White LED 2
    output: output_led_3
    gamma_correct: 2.8
    id: white_led3
 
script:
  - id: six_hour_cycle_white
    mode: single
    then:
      - repeat:
          count: 180
          then:
            - light.turn_on:
                id: white_led1
                brightness: !lambda return (iteration + 1) * 0.004;
                transition_length: 60s
            - light.turn_on:
                id: white_led3
                brightness: !lambda return (iteration + 1) * 0.004;
                transition_length: 60s    
            - delay: 60s            
      - repeat:
          count: 180
          then:
            - light.turn_on:
                id: white_led1
                brightness: !lambda return 0.72 - ((iteration + 1) * 0.004);
                transition_length: 60s
            - light.turn_on:
                id: white_led3
                brightness: !lambda return 0.72 - ((iteration + 1) * 0.004);
                transition_length: 60s    
            - delay: 60s            
      - light.turn_off: white_led1
      - light.turn_off: white_led3    
  - id: six_hour_cycle_blue
    mode: single
    then:
      - repeat:
          count: 180
          then:
            - light.turn_on:
                id: blue_led2
                brightness: !lambda return (iteration + 1) * 0.004;
                transition_length: 60s
            - delay: 60s            
      - repeat:
          count: 180
          then:
            - light.turn_on:
                id: blue_led2
                brightness: !lambda return 0.72 - ((iteration + 1) * 0.004);
                transition_length: 60s
            - delay: 60s            
      - light.turn_off: blue_led2

interval:
  - interval: 1min
    then:
    - if:
       condition:
        lambda: |-
          auto current_time = id(ha_time).now();
          auto trigger_time_state = id(turn_on_time_white).state_as_esptime();
          if (!current_time.is_valid() || !trigger_time_state.is_valid()) return false;
          return current_time.hour == trigger_time_state.hour && current_time.minute == trigger_time_state.minute;
       then: 
         - script.execute: six_hour_cycle_white
       else:
         - logger.log: "Lights are off"      
  - interval: 1min
    then:
    - if:
       condition:
        lambda: |-
          auto current_time = id(ha_time).now();
          auto trigger_time_state = id(turn_on_time_blue).state_as_esptime();
          if (!current_time.is_valid() || !trigger_time_state.is_valid()) return false;
          return current_time.hour == trigger_time_state.hour && current_time.minute == trigger_time_state.minute;
       then:
         - script.execute: six_hour_cycle_blue       
       else:
         - logger.log: "Lights are off"

The display part can be ignored…I am still working on it. I see 5 entries in the HA UI and set the light switch on time to 22:15. The problem I am facing is that the script is not being executed. The log is as below…

[22:12:59][W][component:490]: wifi took a long time for an operation (225 ms)
[22:12:59][W][component:493]: Components should block for at most 30 ms
[22:12:59][I][wifi:1149]: Connected
[22:12:59][C][wifi:897]:   Local MAC: D4:E9:F4:B3:B4:44
[22:12:59][C][wifi:904]:   IP Address: 192.168.0.111
[22:12:59][C][wifi:908]:   SSID: 'Mandrake'[redacted]
[22:12:59][C][wifi:908]:  BSSID: 3C:84:6A:F3:B5:F4[redacted]
[22:12:59][C][wifi:908]:  Hostname: 'esphome-web-b3b444'
[22:12:59][C][wifi:908]:  Signal strength: -54 dB ▂▄▆█
[22:12:59][C][wifi:908]:  Channel: 2
[22:12:59][C][wifi:908]:  Subnet: 255.255.255.0
[22:12:59][C][wifi:908]:  Gateway: 192.168.0.1
[22:12:59][C][wifi:908]:  DNS1: 192.168.0.1
[22:12:59][C][wifi:908]:  DNS2: 0.0.0.0
[22:12:59][D][wifi:1173]: Disabling AP
[22:12:59][W][component:373]: wifi cleared Warning flag
[22:12:59][D][api:136]: Accept 192.168.0.116
[22:12:59][W][component:373]: api cleared Warning flag
[22:12:59][W][component:490]: api took a long time for an operation (53 ms)
[22:12:59][W][component:493]: Components should block for at most 30 ms
[22:12:59][D][api.connection:1398]: Home Assistant 2025.12.5 (192.168.0.116) connected
[22:12:59][D][time:068]: Synchronized time: 2026-01-03 22:12:37
[22:12:59][D][datetime.time_entity:056]: 'Turn on White LED ' - Setting
[22:12:59][D][datetime.time_entity:058]:  Hour: 22
[22:12:59][D][datetime.time_entity:061]:  Minute: 0
[22:12:59][D][datetime.time_entity:064]:  Second: 0
[22:12:59][D][datetime.time_entity:029]: 'Turn on White LED ': Sending time 22:00:00
[22:12:59][D][datetime.time_entity:056]: 'Turn on White LED ' - Setting
[22:12:59][D][datetime.time_entity:058]:  Hour: 22
[22:12:59][D][datetime.time_entity:061]:  Minute: 15
[22:12:59][D][datetime.time_entity:064]:  Second: 0
[22:12:59][D][datetime.time_entity:029]: 'Turn on White LED ': Sending time 22:15:00
[22:12:59][D][datetime.time_entity:056]: 'Turn on Blue LED ' - Setting
[22:12:59][D][datetime.time_entity:058]:  Hour: 22
[22:12:59][D][datetime.time_entity:061]:  Minute: 0
[22:12:59][D][datetime.time_entity:064]:  Second: 0
[22:12:59][D][datetime.time_entity:029]: 'Turn on Blue LED ': Sending time 22:00:00
[22:12:59][D][datetime.time_entity:056]: 'Turn on Blue LED ' - Setting
[22:12:59][D][datetime.time_entity:058]:  Hour: 22
[22:12:59][D][datetime.time_entity:061]:  Minute: 15
[22:12:59][D][datetime.time_entity:064]:  Second: 0
[22:12:59][D][datetime.time_entity:029]: 'Turn on Blue LED ': Sending time 22:15:00
[22:13:29][I][safe_mode:042]: Boot seems successful; resetting boot loop counter
[22:13:29][D][esp32.preferences:149]: Writing 1 items: 0 cached, 1 written, 0 failed
[22:13:31][D][main:148]: Lights are off
[22:13:32][D][main:161]: Lights are off
[22:14:31][D][main:148]: Lights are off
[22:14:32][D][main:161]: Lights are off
[22:15:31][D][main:148]: Lights are off
[22:15:32][D][main:161]: Lights are off

I have pasted the portion of the log from the time that the Wifi got connected. If you see towards the end of the log, the script has not kicked in.

Any suggestions on what I may be doing wrong here ?

First step, add a button to fire the script, so you can be sure it works like intended.
Second step, dig deeper into the daytime component (I’m not familiar with it). state_as_esptime() especially, is it valid just for time without date…
Maybe make condition for .hour and .minute instead.