My DIY Ultraminimalist LED Calendar (Instructables Project) - Now with ESPHome & HA Integration

Hi everyone,

Following up on my previous Linear LED Clock project, I wanted to share another ESPHome integration. This project is based on my original Instructables article: Just Hues: an Ultraminimalist Perpetual Monthly LED Calendar. While the original used custom Arduino code, I wanted to integrate this Ultraminimalist LED Calendar fully into Home Assistant using ESPHome.

Instead of numbers, it uses a grid of 37 WS2812B LEDs (arranged in a specific 6-row layout based on my physical build - see mapping in code) to display the structure of the current month using colors:

  • Green: Weekdays
  • Red: Weekend days (Saturday/Sunday)
  • Blue: The current day

The ESPHome integration leverages the addressable_lambda effect within the light component, optimized to separate the date calculations (which run infrequently) from the fast visual updates (like the glitter effect).

Key Features Implemented in ESPHome:

  • Automatic Calendar Display: Calculates and shows the correct layout for the current month.
  • Selectable Start Day: A switch entity in Home Assistant (Start Day of Week ) allows choosing between Sunday or Monday as the first day of the week, updating the display accordingly.
  • Brightness Control: A number entity (slider) in Home Assistant (Calendar Brightness ) controls the overall brightness of the display.
  • Glitter Effect: A subtle, randomized “glitter” effect (adds white flashes to random active LEDs) for a bit of visual flair. The frequency and intensity are adjustable in the lambda code.
  • Power Control: A simple switch entity in Home Assistant (Calendar Power ) to turn the display ON or OFF.
  • Automatic ON State: The calendar turns on automatically after boot once the time is synced from Home Assistant.
  • State Restoration: Remembers the selected start day and brightness level across reboots (using restore_value: yes ).

ESPHome YAML Code:

Here is the complete YAML configuration. It includes detailed comments explaining the different sections and logic.

# ==============================================================================
# ESPHome Configuration: Ultraminimalist LED Calendar
# ==============================================================================
#
# Project Description:
# This configuration controls a 37-LED WS2812B strip arranged in a custom
# 6-row grid to display a minimalist calendar. It shows the current month's
# structure using distinct colors for weekdays, weekend days, and the current day.
# Includes a subtle glitter effect, brightness control via Home Assistant,
# and allows selecting the start day of the week (Sunday or Monday).
#
# Key Features:
#   - Custom calendar layout rendering.
#   - Dynamic glitter effect.
#   - HA controls: Power, Start Day, Brightness.
#   - Optimized performance: Separates slow date calculations from fast visual updates.
#   - Automatic startup after boot and time sync.
#   - Persistent settings (brightness, start day).
# ==============================================================================

esphome:
  # --- Device Identification ---
  name: ultraminimalist-calendar # Internal hostname (used for mDNS, OTA, API)
  friendly_name: "Ultraminimalist Calendar" # Name displayed in Home Assistant Integrations UI

  # --- Boot Sequence Actions ---
  on_boot:
    # Runs relatively early in the boot sequence (higher priority runs first).
    # This runs before the network or API might be fully ready.
    priority: 10.0 
    then:
      - logger.log: "Device booted. Waiting for time sync."
      # Initialize the global base colors vector with black.
      # This prevents potential errors if the effect lambda runs before the 
      # calculation script has populated the vector for the first time.
      - lambda: id(g_base_colors).assign(37, Color::BLACK);
      # Ensure the LED strip is turned off physically on hardware boot.
      # The actual display will be turned on later by on_time_sync or the power switch.
      - light.turn_off: calendar_led_strip

# ==============================================================================
# Time Synchronization Component
# ==============================================================================
time:
  - platform: homeassistant # Use Home Assistant as the time source via the native API
    id: ha_time # Internal ID to reference this time component
    
    # --- Action on Successful Time Sync ---
    # This trigger runs once each time the ESP successfully syncs time with Home Assistant
    # (e.g., after boot and connection, or after a reconnection).
    on_time_sync:
      then:
        - logger.log: "Time synced. Recalculating calendar and turning ON."
        # 1. Perform the initial calculation of base calendar colors now that the date is known.
        - script.execute: update_base_calendar_colors
        # 2. Turn the light ON using the custom effect.
        #    The effect lambda will read the target brightness from the global variable.
        #    This ensures the calendar starts automatically in the desired ON state after boot.
        - light.turn_on:
            id: calendar_led_strip
            effect: "Calendar Display" # Activate our custom display effect
        - logger.log: "Calendar activated via on_time_sync."
        
    # --- Daily Calendar Recalculation Trigger ---
    # This trigger ensures the calendar display updates for the new day.
    on_time:
      # Run this sequence every day at 00:02:00 AM (2 minutes past midnight).
      # The slight delay ensures the date has definitely changed.
      - seconds: 0
        minutes: 2
        hours: 0
        then:
          - logger.log: "Daily trigger: Recalculating base calendar colors."
          # Execute the script that performs the date-dependent calculations.
          - script.execute: update_base_calendar_colors
          # Note: We don't need to explicitly call light.turn_on here. 
          # The 'Calendar Display' effect is already running (if the light is ON).
          # It will automatically pick up the newly calculated base colors 
          # from the global vector during its next update cycle (within 50ms).

# ==============================================================================
# Global Variables
# ==============================================================================
# These variables store state or configuration values accessible across different parts
# of the ESPHome configuration (lambdas, scripts, etc.).
globals:
  # --- Color Definitions ---
  # Define the colors used for different day types. Can be customized here.
  - id: color_weekday # Color for regular weekdays (Monday-Friday or Sunday-Thursday depending on start day)
    type: Color
    initial_value: '{0, 255, 0}' # Green
  - id: color_weekend # Color for weekend days (Saturday/Sunday)
    type: Color
    initial_value: '{255, 0, 0}' # Red
  - id: color_today # Color for highlighting the current day
    type: Color
    initial_value: '{0, 0, 255}' # Blue
  
  # --- User Configuration Globals ---
  - id: start_day_of_week # Stores the user's preference for the first day of the week.
    type: int             # 0 = Sunday, 1 = Monday
    initial_value: '1'    # Default to Monday as the start day.
    restore_value: yes    # Automatically save this value to flash and restore it on reboot.
  - id: g_target_brightness_pct # Stores the desired brightness percentage (1-100) set via the HA slider.
    type: float
    initial_value: '80.0' # Set the default brightness to 80% on first boot.
    restore_value: yes    # Automatically save and restore this brightness setting across reboots.
  
  # --- Internal State Global (for performance optimization) ---
  - id: g_base_colors # Stores the calculated base colors (weekday/weekend/today/off) for each physical LED.
    type: std::vector<esphome::Color> # A C++ vector holding Color objects. Size matches num_leds.
    # This vector is populated by the 'update_base_calendar_colors' script and read by the fast 'Calendar Display' effect lambda.

# ==============================================================================
# Number Component (Brightness Slider)
# ==============================================================================
# Creates a number entity (displayed as a slider) in Home Assistant for controlling brightness.
number:
  - platform: template
    id: calendar_brightness_number # Internal ID for this component
    name: "Calendar Brightness"    # Name displayed for the entity in Home Assistant UI
    
    # Lambda function: Reports the current state of the slider back to Home Assistant.
    # Reads the target brightness value stored in the global variable.
    lambda: |-
      float target_pct = id(g_target_brightness_pct);
      // Provide a default if the global is somehow invalid (e.g., first boot before restore)
      if (isnan(target_pct)) { target_pct = 80.0f; } 
      // Clamp the value to the defined min/max range before reporting, just in case.
      target_pct = std::max(id(calendar_brightness_number).traits.get_min_value(), std::min(id(calendar_brightness_number).traits.get_max_value(), target_pct)); 
      return target_pct;
      
    # Action executed when the slider's value is changed from Home Assistant
    set_action:
      - logger.log:
          format: "Number set_action: Received target brightness x=%.1f. Storing in global."
          args:
           - 'x' # 'x' is the special variable holding the value sent from HA (1-100)
      # Update the global variable with the new target brightness value
      - globals.set:
          id: g_target_brightness_pct
          value: !lambda 'return x;' # Store the received value 'x'
          
      # Optional: Force an immediate refresh of the light effect.
      # This makes the brightness change feel more instantaneous (response < 50ms)
      # instead of waiting for the next 50ms effect update cycle.
      - if:
          condition:
            light.is_on: calendar_led_strip # Only refresh if the light is actually on
          then:
            - logger.log: "Forcing effect refresh after brightness change."
            # Re-applying the effect will cause its lambda to run immediately,
            # reading the new brightness value from the global.
            - light.turn_on:
                id: calendar_led_strip
                effect: "Calendar Display" 

    # Configuration for the slider entity in Home Assistant
    min_value: 1    # Minimum allowed brightness percentage
    max_value: 100  # Maximum allowed brightness percentage
    step: 1         # Increment/decrement step size for the slider
    unit_of_measurement: "%" # Unit displayed next to the value in HA
    mode: slider    # Display as a slider (alternatives: box)

# ==============================================================================
# Switch Components
# ==============================================================================
# Creates switch entities in Home Assistant for user control.
switch:
  # --- Main Power Switch ---
  - platform: template
    id: calendar_power_switch # Internal ID
    name: "Calendar Power"    # Name displayed in HA
    
    # Lambda: Reports the switch state based on the actual state of the internal light component.
    lambda: |-
      // Return true (ON) if the internal light component is currently on, false (OFF) otherwise.
      return id(calendar_led_strip).current_values.is_on();
      
    # Action when the switch is turned ON from HA
    turn_on_action:
      - logger.log: "Calendar Power Switch ON action started."
      # 1. Ensure the base calendar colors are up-to-date (in case the day changed while off)
      - script.execute: update_base_calendar_colors
      # 2. Turn on the internal light component and activate the custom display effect.
      #    Brightness is handled by the effect reading the global variable.
      - light.turn_on:
          id: calendar_led_strip
          effect: "Calendar Display"
          
    # Action when the switch is turned OFF from HA
    turn_off_action:
      - logger.log: "Calendar Power Switch turned OFF."
      # Turn off the internal light component, stopping the effect.
      - light.turn_off:
          id: calendar_led_strip

  # --- Start Day of Week Switch ---
  - platform: template
    id: start_day_of_week_switch # <<< RENAMED ID
    name: "Start Day of Week"    # <<< RENAMED NAME (Functionality: OFF=Monday, ON=Sunday)
    
    # Lambda: Reports the switch state based on the global variable 'start_day_of_week'.
    # Returns true (ON) if the global is 0 (Sunday), false (OFF) if it's 1 (Monday).
    lambda: |-
      return id(start_day_of_week) == 0;
      
    # Action when switched ON (User wants Sunday as the start day)
    turn_on_action:
      - logger.log: "Setting start day to Sunday (0)."
      # Update the global variable to 0
      - globals.set:
          id: start_day_of_week
          value: '0'
      # Trigger the script to recalculate the base calendar colors immediately
      # using the new setting. The effect will pick up the changes shortly after.
      - script.execute: update_base_calendar_colors
      
    # Action when switched OFF (User wants Monday as the start day)
    turn_off_action:
      - logger.log: "Setting start day to Monday (1)."
      # Update the global variable to 1
      - globals.set:
          id: start_day_of_week
          value: '1'
      # Trigger the script to recalculate the base calendar colors immediately
      # using the new setting. The effect will pick up the changes shortly after.
      - script.execute: update_base_calendar_colors

# ==============================================================================
# Script for Slow Calculations
# ==============================================================================
# Contains the logic for calculating the base calendar colors.
# This runs less frequently (daily, on setting change, on sync) to optimize performance.
script:
  - id: update_base_calendar_colors # Internal ID for this script
    mode: single # Ensures only one instance runs at a time, preventing conflicts
    then:
      # Lambda containing the C++ code for the actual calculations
      - lambda: |-
          // Include necessary C++ headers
          #include <ctime>    // For time calculations (mktime, tm structure)
          #include <vector>   // For std::vector
          #include <cmath>    // For isnan
          #include <algorithm>// For std::fill

          ESP_LOGD("script_calc", "Executing base calendar color calculation...");

          // Ensure the global base colors vector is correctly sized (should match num_leds)
          // This also initializes it with black Color objects if it's the very first run.
          if (id(g_base_colors).size() != 37) { 
             id(g_base_colors).assign(37, Color::BLACK); 
             ESP_LOGD("script_calc", "Initialized/Resized g_base_colors vector to 37.");
          }

          // Get the current time object from the synced time component
          auto current_time_obj = id(ha_time).now();
          // Exit the script if the time is somehow not valid at this point
          if (!current_time_obj.is_valid()) {
            ESP_LOGW("script_calc", "Time not valid during calculation, skipping update.");
            // Keep the previously calculated colors in this case
            return;
          }
          // Extract necessary date components
          int current_year = current_time_obj.year;
          int current_month = current_time_obj.month; // 1-12
          int current_day_of_month = current_time_obj.day_of_month; // 1-31

          // --- Date Calculations ---
          // 1. Calculate the weekday of the 1st day of the current month (0=Sunday, ..., 6=Saturday)
          std::tm t_first_day{}; // Create a time structure
          t_first_day.tm_year = current_year - 1900; // Years since 1900
          t_first_day.tm_mon = current_month - 1;    // Months since January (0-11)
          t_first_day.tm_mday = 1;                   // Day of the month (1st)
          // Set hour to noon to avoid potential DST issues around midnight
          // t_first_day.tm_hour = 12; 
          // t_first_day.tm_isdst = -1; // Let mktime determine DST
          std::mktime(&t_first_day); // mktime calculates the tm_wday (day of week) field
          int first_day_wday_c_std = t_first_day.tm_wday; // 0=Sun, 1=Mon, ..., 6=Sat
          
          // 2. Normalize the first day index (0-6) based on the user's 'start_day_of_week' setting.
          //    This normalized index represents the starting column (0-6) in our conceptual 6x7 grid.
          int first_day_normalized_for_grid;
          if (id(start_day_of_week) == 0) { // User selected Sunday as start day
            // Grid column 0 = Sunday, 1 = Monday, ..., 6 = Saturday
            first_day_normalized_for_grid = first_day_wday_c_std; 
          } else { // User selected Monday as start day (default)
            // Grid column 0 = Monday, 1 = Tuesday, ..., 6 = Sunday
            // We need to map C std weekday (Sun=0) to our grid index (Mon=0)
            first_day_normalized_for_grid = (first_day_wday_c_std == 0) ? 6 : first_day_wday_c_std - 1; 
          }
          
          // 3. Calculate the number of days in the current month, accounting for leap years.
          int days_in_month;
          if (current_month == 2) { // February
            bool is_leap = (current_year % 4 == 0 && current_year % 100 != 0) || (current_year % 400 == 0);
            days_in_month = is_leap ? 29 : 28;
          } else if (current_month == 4 || current_month == 6 || current_month == 9 || current_month == 11) { // April, June, Sept, Nov
            days_in_month = 30;
          } else { // All other months
            days_in_month = 31;
          }
          ESP_LOGD("script_calc", "Month details: Year=%d, Month=%d, Day=%d, 1stDayNorm=%d, DaysInMonth=%d",
                   current_year, current_month, current_day_of_month, first_day_normalized_for_grid, days_in_month);

          // --- LED Mapping Definition ---
          // This array maps the conceptual 6x7 grid cell index (0-41, top-left to bottom-right) 
          // to the physical LED index (0-36) based on the specific hardware wiring/layout.
          // A value of -1 indicates that the conceptual cell does not correspond to any physical LED.
          const int cell_to_physical_led[42] = { 
            // Conceptual Grid Row 0 (Top Row)
            30, 31, 32, 33, 34, 35, 36, 
            // Conceptual Grid Row 1 
            29, 28, 27, 26, 25, 24, 23, 
            // Conceptual Grid Row 2
            16, 17, 18, 19, 20, 21, 22, 
            // Conceptual Grid Row 3
            15, 14, 13, 12, 11, 10, 9,  
            // Conceptual Grid Row 4
            2, 3, 4, 5, 6, 7, 8,       
            // Conceptual Grid Row 5 (Bottom Row)
            1, 0, -1, -1, -1, -1, -1    
          };

          // --- Populate the global base colors vector ---
          // Reset the global vector to all black before calculating the new colors for the month.
          std::fill(id(g_base_colors).begin(), id(g_base_colors).end(), Color::BLACK); 
          
          // Iterate through all 42 conceptual grid cells (6 weeks x 7 days)
          for (int cell_idx = 0; cell_idx < 42; ++cell_idx) {
            // Find the corresponding physical LED index using the mapping array
            int physical_led_idx = cell_to_physical_led[cell_idx];
            
            // Check if this conceptual cell maps to a valid physical LED on our strip
            if (physical_led_idx != -1 && physical_led_idx < 37) { // Ensure index is within 0-36 range
              // Calculate the conceptual column (0-6) for weekend check
              int grid_col = cell_idx % 7; 
              // Calculate the day number this cell should represent based on the 1st day's position
              int day_in_this_cell = cell_idx - first_day_normalized_for_grid + 1; 
              
              // Check if this calculated day number is a valid day within the current month
              if (day_in_this_cell > 0 && day_in_this_cell <= days_in_month) {
                // --- Determine the state and color for this valid day ---
                // Is it the current day?
                bool is_today = (day_in_this_cell == current_day_of_month);
                // Is it a weekend day?
                bool is_weekend_day;
                if (id(start_day_of_week) == 0) { // Sunday start: Sun(col 0) and Sat(col 6) are weekends
                  is_weekend_day = (grid_col == 0 || grid_col == 6); 
                } else { // Monday start: Sat(col 5) and Sun(col 6) are weekends
                  is_weekend_day = (grid_col == 5 || grid_col == 6); 
                }
                
                // Assign the appropriate base color (at full intensity, brightness applied later)
                Color calculated_base_color = id(color_weekday); // Default to weekday color
                if (is_weekend_day) calculated_base_color = id(color_weekend); // Override if weekend
                if (is_today) calculated_base_color = id(color_today); // Override if today
                
                // Store the calculated base color in the global vector at the correct physical LED index
                id(g_base_colors)[physical_led_idx] = calculated_base_color; 
              } 
              // else: This physical LED corresponds to a day outside the current month 
              // (e.g., leading days from previous month or trailing days for next month).
              // It remains black as set by the initial std::fill.
            } 
            // else: This conceptual grid cell does not map to any physical LED, so we ignore it.
          } // --- End of conceptual grid cell loop ---
          ESP_LOGD("script_calc", "Base calendar colors recalculated and stored in global vector.");

# ==============================================================================
# ESP8266 Platform Configuration
# ==============================================================================
esp8266:
  board: d1_mini # Specify the exact board model for correct pin mappings and settings

# ==============================================================================
# WiFi Network Configuration
# ==============================================================================
wifi:
  ssid: mySSID        # Your WiFi network name (SSID)
  password: myPassword  # Your WiFi password
  
  # Fallback Access Point (Optional)
  # Creates a WiFi network named "CalendarFallbackAP" if the device cannot 
  # connect to the main network. Useful for initial setup or recovery.
  ap:
    ssid: "CalendarFallbackAP"
    password: "fallbackpassword"

# ==============================================================================
# Logging Configuration
# ==============================================================================
logger:
  # Set the default log level. Options: NONE, ERROR, WARN, INFO, DEBUG, VERBOSE, VERY_VERBOSE
  # DEBUG is useful for development to see detailed messages (like ESP_LOGD).
  # INFO is generally recommended for normal operation.
  level: DEBUG 

# ==============================================================================
# Native API Configuration
# ==============================================================================
# Enables the highly efficient native ESPHome API for communication with Home Assistant.
api:
  # encryption: # Optional: Add encryption key for secure communication
  #   key: "YOUR_API_ENCRYPTION_KEY" 
  # password: "" # Optional: Set an API password (legacy, encryption preferred)

# ==============================================================================
# Over-The-Air (OTA) Update Configuration
# ==============================================================================
# Allows updating the device firmware wirelessly via WiFi.
ota:
  platform: esphome # Use the standard ESPHome OTA method (recommended)
  # password: "" # Optional: Set a password for OTA updates

# ==============================================================================
# Light Component Configuration
# ==============================================================================
# Defines the physical LED strip and its associated effects.
light:
  - platform: neopixelbus # Use the NeoPixelBus library for controlling WS281x LEDs
    type: GRB           # Color order of the LEDs (Green, Red, Blue). Adjust if needed.
    variant: WS2812X      # Specific chipset variant (WS2812, WS2812B compatible)
    pin: D4             # GPIO pin connected to the LED strip's Data input line
    num_leds: 37        # Total number of LEDs on the physical strip
    id: calendar_led_strip # Internal ID used to reference this light strip in YAML
    name: "Internal Calendar LEDs" # Internal name (not typically visible in HA)
    internal: true      # Hide this base light entity from Home Assistant UI (controlled via switches/number)
    default_transition_length: 0s # Disable default fading between states for instant changes
    restore_mode: RESTORE_DEFAULT_OFF # Ensure the light state is OFF on initial power-up/reset
    
    # --- Define Light Effects ---
    effects:
      # Custom lambda effect for displaying the calendar and glitter
      - addressable_lambda:
          name: "Calendar Display" # Name of the effect
          update_interval: 50ms # How often the lambda code runs (fast for smooth glitter)
          
          # --- C++ Lambda Code (Fast Loop) ---
          # This code runs every 'update_interval' (50ms) when the effect is active.
          # It's responsible for applying glitter and brightness to the pre-calculated base colors.
          lambda: |-
            // Include necessary C++ headers
            #include <vector>   // For std::vector
            #include <cmath>    // For isnan, round, std::max, std::min
            #include <stdlib.h> // For rand(), srand()
            #include <algorithm>// For std::min, std::max

            // --- Random Number Generator Seeding (run only once) ---
            // Ensures the random sequence is different each time the device boots.
            static bool seeded = false;
            if (!seeded) { 
              srand(::time(NULL)); // Use the global C time() function for seeding
              seeded = true; 
            }

            // --- Glitter Effect Parameters ---
            const float GLITTER_PROBABILITY_PER_LED = 0.01f; // 1% chance per LED per 50ms cycle
            const int GLITTER_BRIGHTNESS_ADD = 255;  // Add full white component for intense glitter

            // --- Read Target Brightness ---
            // Get the desired brightness percentage from the global variable (set by the number entity)
            float brightness_pct = id(g_target_brightness_pct);
            // Validate the brightness value, providing a default if it's invalid (e.g., NaN on first read)
            if (isnan(brightness_pct)) { brightness_pct = 80.0f; } // Default to 80% if invalid
            // Clamp the brightness value to the valid range (1-100)
            brightness_pct = std::max(1.0f, std::min(100.0f, brightness_pct)); 
            // Convert the percentage (1-100) to the 0-255 scale needed for manual color scaling
            uint8_t brightness_0_255 = (uint8_t) round((brightness_pct / 100.0f) * 255.0f);

            // --- Safety Check: Ensure Base Colors are Ready ---
            // Check if the global vector holding the base calendar colors has been initialized and populated.
            if (id(g_base_colors).empty() || id(g_base_colors).size() != it.size()) {
                 ESP_LOGW("effect_lambda", "Base colors vector not ready (size=%d, expected=%d), skipping draw.", id(g_base_colors).size(), it.size());
                 // Optionally turn off LEDs completely if base colors are missing
                 // it.all() = Color::BLACK; 
                 return; // Exit the lambda for this cycle
            }

            // --- Apply Colors, Glitter, and Brightness to Physical LEDs ---
            // Iterate through each physical LED in the strip ('it' represents the strip controller)
            for (int i = 0; i < it.size(); ++i) { // it.size() should be 37
              // Get the pre-calculated base calendar color for this LED from the global vector
              Color base_color = id(g_base_colors)[i]; 
              // Start with the base color; this might be modified by glitter
              Color color_before_scaling = base_color; 

              // --- Apply Glitter Effect ---
              // Check if the base color is not black (don't glitter empty spots)
              // and if a random chance (based on probability) passes for this LED
              if (base_color != Color::BLACK && (rand() / (float)RAND_MAX) < GLITTER_PROBABILITY_PER_LED) {
                // Add the glitter brightness component (effectively adding white)
                int new_r = base_color.red + GLITTER_BRIGHTNESS_ADD;
                int new_g = base_color.green + GLITTER_BRIGHTNESS_ADD;
                int new_b = base_color.blue + GLITTER_BRIGHTNESS_ADD;
                // Cap the R, G, B values at 255 to prevent overflow
                color_before_scaling.red   = (uint8_t) std::min(255, new_r);
                color_before_scaling.green = (uint8_t) std::min(255, new_g);
                color_before_scaling.blue  = (uint8_t) std::min(255, new_b);
              }

              // --- Apply Final Brightness Scaling ---
              // Manually scale the R, G, B components of the (potentially glittered) color
              // using the target brightness value (converted to 0-255 scale).
              // Use uint16_t for intermediate multiplication to prevent overflow before dividing by 255.
              uint8_t scaled_r = static_cast<uint8_t>( ( (uint16_t)color_before_scaling.red * brightness_0_255 ) / 255 );
              uint8_t scaled_g = static_cast<uint8_t>( ( (uint16_t)color_before_scaling.green * brightness_0_255 ) / 255 );
              uint8_t scaled_b = static_cast<uint8_t>( ( (uint16_t)color_before_scaling.blue * brightness_0_255 ) / 255 );

              // Set the physical LED ('it[i]') to the final calculated and scaled color
              // 'it' is the special object provided by addressable_lambda to control LEDs
              it[i] = Color(scaled_r, scaled_g, scaled_b);
            } // --- End of LED loop ---

Hardware:

  • Wemos D1 Mini (ESP8266)
  • 37 x WS2812B LEDs (arranged in a specific layout, see cell_to_physical_led map in the code or the original project)
  • 5V Power Supply suitable for the LEDs
  • 3D Printed Enclosure (This is necessary of cours for the calendar display :slight_smile: STL files, photos, and assembly details can be found in the original Instructables article).

This project demonstrates how addressable_lambda effects combined with scripts and globals in ESPHome can create quite complex and optimized custom displays, integrating a DIY hardware project seamlessly into Home Assistant.

Let me know what you think or if you have any questions!

1 Like