Bindicator – The visual bin indicator!

August 7, 2025
9122
Views

The basic, yet so useful bindicatior is a simple little indicator to notify you when rubbish or recycling is due. This consists of a single esp32 or esp8366 and a WS2182B LED or LED strip.

Parts List
ESP32 or ESP8266
NeoPixel ES2182B or any addressable LED
A mini bin thing

Using the HACS integration in Home Assistant, I am able to retrieve a Calander entry of when my recycling and rubbish is due. We can use this data to trigger different colors and flash patterns to indicate what is due and when.

There are two options for this project, the first is to use WLED and use Home Assistant for all automations, this is less flexible, but WLED is pretty cool.

The other is esphome. The process is a little more manual for configuring flashing patterns etc. I will cover both in this article. Firstly, start by downloading and installing Waste Collection Schedule in HACS

All automations in this post assume you have a schedule or an entity to track your rubbish and recycling collection dates. I used Waste Collection Schedule available on HACS and supports a wide range of countries and APIs.

https://github.com/mampfes/hacs_waste_collection_schedule

Using WLED

If you choose to use WLED, firstly start by installing WLED on your esp32 or esp8266.

Install WLED

Once installed and connected to Wi-Fi, add it to home assistant using the WLED integration. Take note of the WLED light entity. In my case, light.bindicator.

Using the following automation, I will dim the LED to 5% when the sun is below horizon, and 100% when the sun is above horizon. The bin LED will be solid for the bin that is due for collection that week, when collection day is 1 day out, it will blink slowly. When the bin is due that day, it will blink fast. When that bin collection schedule is over, it will restart and show the next bin with a solid LED of that bin color until that bin is due for collection, then it will blink slowly when 1 day out and so on.

Below, I am using the entity sensor.all_waste_types provided by Waste Collection Schedule, this provides me all collection dates for all bins in 1 single entity.

- id: bin_wled_indicator
  alias: "Bin WLED Indicator"
  mode: restart
  trigger:
    - platform: state
      entity_id:
        - sensor.all_waste_types
        - sun.sun
    - platform: time_pattern
      hours: "/1"
  action:
    - variables:
        s: "{{ states('sensor.all_waste_types')|lower|trim }}"
        bin_type: >-
          {% if 'recycle' in s %}recycle
          {% elif 'rubbish' in s %}rubbish
          {% else %}unknown{% endif %}
        days: >-
          {% set d = s | regex_findall_index('(\\d+)', 0) %}
          {% if d is not none %}{{ d|int }}{% else %}999{% endif %}
        rgb: >-
          {% if bin_type == 'recycle' %}[0,255,0]
          {% elif bin_type == 'rubbish' %}[255,210,0]
          {% else %}none{% endif %}
        brightness: "{{ 255 if is_state('sun.sun','above_horizon') else 13 }}"
    - choose:
        - conditions: "{{ rgb != 'none' and days == 0 }}"
          sequence:
            - service: select.select_option
              target: { entity_id: select.bindicator_palette }
              data: { option: "Default" }
            - service: light.turn_on
              target: { entity_id: light.bindicator }
              data:
                effect: Solid
                rgb_color: "{{ rgb }}"
                brightness: "{{ brightness }}"
            - delay: "00:00:00.3"
            - service: number.set_value
              target: { entity_id: number.bindicator_effect_speed }
              data: { value: 255 }
            - delay: "00:00:00.3"
            - service: light.turn_on
              target: { entity_id: light.bindicator }
              data:
                effect: Dissolve
                brightness: "{{ brightness }}"
        - conditions: "{{ rgb != 'none' and days == 1 }}"
          sequence:
            - service: select.select_option
              target: { entity_id: select.bindicator_palette }
              data: { option: "Default" }
            - service: light.turn_on
              target: { entity_id: light.bindicator }
              data:
                effect: Solid
                rgb_color: "{{ rgb }}"
                brightness: "{{ brightness }}"
            - delay: "00:00:00.3"
            - service: number.set_value
              target: { entity_id: number.bindicator_effect_speed }
              data: { value: 220 }
            - delay: "00:00:00.3"
            - service: light.turn_on
              target: { entity_id: light.bindicator }
              data:
                effect: Dissolve
                brightness: "{{ brightness }}"
        - conditions: "{{ rgb != 'none' and days > 1 and days < 999 }}"
          sequence:
            - service: select.select_option
              target: { entity_id: select.bindicator_palette }
              data: { option: "Default" }
            - service: light.turn_on
              target: { entity_id: light.bindicator }
              data:
                effect: Solid
                rgb_color: "{{ rgb }}"
                brightness: "{{ brightness }}"
      default:
        - condition: template
          value_template: "{{ bin_type == 'unknown' }}"
        - service: light.turn_off
          target: { entity_id: light.bindicator }

Using esphome

Using the esp8266, use the following code. Here we use the nexopixelbus platform. This includes 3 lambda effects.

light:
  - platform: neopixelbus
    id: bindicator
    name: "bindicator"
    pin: GPIO2
    num_leds: 1
    type: GRB         # WS2812s are usually GRB
    variant: WS2812
    restore_mode: ALWAYS_ON
    effects:
      - pulse:
          name: "Blink Fast"
          # fast, snappy flashes
          transition_length: 120ms
          update_interval: 120ms
          min_brightness: 8%
          max_brightness: 100%

      - pulse:
          name: "Blink Slow"
          # gentle breathing blink
          transition_length: 650ms
          update_interval: 650ms
          min_brightness: 10%
          max_brightness: 100%
      - lambda:
          name: "Christmas"
          update_interval: 120ms   # driver tick; smaller = snappier, larger = lazier
          lambda: |-
            static uint32_t phase_start = 0;
            static uint32_t phase_len = 2500;
            static uint8_t cadence = 0;
            static uint8_t step = 0;
            static uint8_t r = 255, g = 0, b = 0;

            // Helper: pick color with weights and set phase length per color
            auto pick_color = [&]() {
              struct C { uint8_t r,g,b, weight; uint16_t min_ms, max_ms; };
              const C palette[] = {
                {255,  0,  0,  3, 1600, 2800},  // red   — medium/long
                {  0,255,  0,  3, 1600, 2800},  // green — medium/long
                {255,215,  0,  4, 2200, 3600},  // gold  — longest (feature color)
                {  0,  0,255,  2,  900, 1500},  // blue  — shorter
                {255,  0,128,  2, 1200, 2000},  // magenta
                {  0,255,180,  2, 1200, 2000}   // teal
              };
              uint16_t sum = 0;
              for (auto &c : palette) sum += c.weight;
              uint16_t roll = random_uint32() % sum;
              const C* sel = &palette[0];
              for (auto &c : palette) { if (roll < c.weight) { sel = &c; break; } roll -= c.weight; }
              r = sel->r; g = sel->g; b = sel->b;
              phase_len = sel->min_ms + (random_uint32() % (sel->max_ms - sel->min_ms + 1));
            };

            auto pick_cadence = [&]() {
              uint8_t roll = random_uint32() % 100;
              if (roll < 45)      cadence = 0;
              else if (roll < 85) cadence = 1;
              else                cadence = 2;
              step = 0;
            };

            uint32_t now = millis();
            if (phase_start == 0) {
              pick_color(); pick_cadence();
              phase_start = now;
            }

            // Phase rollover
            if ((now - phase_start) > phase_len) {
              // Occasionally HOLD longer on the “feature” colors (gold/red/green)
              bool is_feature = ( (r==255 && g==215 && b==0) || (r==255 && g==0 && b==0) || (r==0 && g==255 && b==0) );
              if (is_feature && (random_uint32() % 100) < 50) {
                phase_start = now;
                phase_len   = 600 + (random_uint32() % 900);  // 0.6–1.5s
                cadence     = 3; 
              } else {
                pick_color(); pick_cadence();
                phase_start = now;
              }
            }

            auto call = id(bindicator).turn_on();
            call.set_rgb(r/255.0f, g/255.0f, b/255.0f);

            float br = 1.0f; // 0.0–1.0
            uint32_t elapsed = now - phase_start;

            switch (cadence) {
              case 0: {
                step = (step + 1) % 8;
                bool on = (step==0 || step==2 || step==4);
                br = on ? 1.0f : ((step==5) ? 0.0f : 0.12f);
                call.set_transition_length(on ? 90 : 90);
                break;
              }
              case 1: {
                float t = (float)elapsed / (float)phase_len;
                if (t > 1.0f) t = 1.0f;
                float smooth = t*t*(3.0f - 2.0f*t);   // ease-in-out
                br = 0.18f + 0.82f * smooth;          // 18%..100%
                call.set_transition_length(240);
                break;
              }
              case 2: {
                br = 0.1f + (random_uint32() % 156) / 155.0f * 0.9f;  // ~0.1..1.0
                call.set_transition_length(80);
                break;
              }
              case 3: {
                br = 1.0f;
                call.set_transition_length(120);
                break;
              }
            }

            call.set_brightness(br);
            call.perform();


switch:
  - platform: template
    name: "Christmas Mode"
    id: christmas_mode
    optimistic: true
    turn_on_action:
      - light.turn_on:
          id: bindicator
          effect: "Christmas"
          brightness: 1.0
    turn_off_action:
      - light.turn_off: bindicator

And using the esp32, we instead use the esp32_rmt_led_strip platform.

light:
  - platform: esp32_rmt_led_strip
    id: bindicator
    rgb_order: GRB
    default_transition_length: 1s
    pin: GPIO16
    num_leds: 1
    rmt_symbols: 96
    chipset: WS2812
    name: "bindicator"
    effects:
      - pulse:
          name: "Blink Fast"
          transition_length: 120ms
          update_interval: 120ms
          min_brightness: 8%
          max_brightness: 100%

      - pulse:
          name: "Blink Slow"
          transition_length: 650ms
          update_interval: 650ms
          min_brightness: 10%
          max_brightness: 100%
      - lambda:
          name: "Christmas"
          update_interval: 120ms
          lambda: |-
            static uint32_t phase_start = 0;
            static uint32_t phase_len = 2500;   
            static uint8_t cadence = 0;         
            static uint8_t step = 0;         
            static uint8_t r = 255, g = 0, b = 0;

            auto pick_color = [&]() {
              struct C { uint8_t r,g,b, weight; uint16_t min_ms, max_ms; };
              const C palette[] = {
                {255,  0,  0,  3, 1600, 2800}, 
                {  0,255,  0,  3, 1600, 2800}, 
                {255,215,  0,  4, 2200, 3600},  
                {  0,  0,255,  2,  900, 1500}, 
                {255,  0,128,  2, 1200, 2000},  
                {  0,255,180,  2, 1200, 2000}   
              };
              uint16_t sum = 0;
              for (auto &c : palette) sum += c.weight;
              uint16_t roll = random_uint32() % sum;
              const C* sel = &palette[0];
              for (auto &c : palette) { if (roll < c.weight) { sel = &c; break; } roll -= c.weight; }
              r = sel->r; g = sel->g; b = sel->b;
              phase_len = sel->min_ms + (random_uint32() % (sel->max_ms - sel->min_ms + 1));
            };

            auto pick_cadence = [&]() {
              uint8_t roll = random_uint32() % 100;
              if (roll < 45)      cadence = 0;  
              else if (roll < 85) cadence = 1;  
              else                cadence = 2;  
              step = 0;
            };

            uint32_t now = millis();
            if (phase_start == 0) {
              pick_color(); pick_cadence();
              phase_start = now;
            }

            if ((now - phase_start) > phase_len) {
              bool is_feature = ( (r==255 && g==215 && b==0) || (r==255 && g==0 && b==0) || (r==0 && g==255 && b==0) );
              if (is_feature && (random_uint32() % 100) < 50) {
                phase_start = now;
                phase_len   = 600 + (random_uint32() % 900);
                cadence     = 3; // hold
              } else {
                pick_color(); pick_cadence();
                phase_start = now;
              }
            }

            // Build call
            auto call = id(bindicator).turn_on();
            call.set_rgb(r/255.0f, g/255.0f, b/255.0f);

            float br = 1.0f;
            uint32_t elapsed = now - phase_start;

            switch (cadence) {
              case 0: {
                step = (step + 1) % 8;
                bool on = (step==0 || step==2 || step==4);
                br = on ? 1.0f : ((step==5) ? 0.0f : 0.12f);
                call.set_transition_length(90);
                break;
              }
              case 1: {
                float t = (float)elapsed / (float)phase_len;
                if (t > 1.0f) t = 1.0f;
                float smooth = t*t*(3.0f - 2.0f*t);
                br = 0.18f + 0.82f * smooth;
                call.set_transition_length(240);
                break;
              }
              case 2: {
                br = 0.1f + (random_uint32() % 156) / 155.0f * 0.9f;
                call.set_transition_length(80);
                break;
              }
              case 3: {
                br = 1.0f;
                call.set_transition_length(120);
                break;
              }
            }

            call.set_brightness(br);
            call.perform();

switch:
  - platform: template
    name: "Christmas Mode"
    id: christmas_mode
    optimistic: true
    turn_on_action:
      - light.turn_on:
          id: bindicator
          effect: "Christmas"
          brightness: 1.0
    turn_off_action:
      - light.turn_off: bindicator

Wiring Diagram

It really doesn’t warrant a diagram. Just make sure when wiring the WS2182B that you use the Data IN pin not the Data OUT pin. Usually, labelled DIN and DOUT Gets me every time.

ESP32WS2812B
5V5V
GNDGND
GPIO4DIN

Buy Bindicator!

Buy Bindicator here!

All Comments

  • Received my Bindicator in the post yesterday 🙂 one issue though… the piece of paper with the instructional QR code can’t be read can you send me the URL

    jamie.bennett September 15, 2025 11:50 pm

Leave a Reply