Trying to increment a number using lambdas

Hio!

I’m trying to create stairway lighting using a WS2812b light strip. The idea being that when somebody enters the stairs a the bottom, the LEDs ‘wipe’ on from bottom to top. When they exit the stairs at the top, the LEDs ‘wipe’ off from bottom to top. And the same the other way round. Enter the stairs at the top, LEDs ‘wipe’ on from top to bottom, exit the stairs at the bottom, LEDs ‘wipe’ off from top to bottom.

I also want to count the number of people on the stairs so the LEDs don’t turn off until the last person exits the stairs. Typically this would only be one or two people, and nearly always a cat as well. What I don’t want is for me to start walking up the stairs, turn the lights on, then the cat passes me and turns them off before I’m at the top :grin:

I have various WLED(esp8266) devices around the house, but in this instance I want to keep everything within ESPHome as a stand alone device.

Building this on a NodeMCU ESP8266 and for testing purposes I’ve created a couple of switches for HA that turn the LEDs on and off. This works well with the ‘wipe’ effects so I know I’ve got that bit right :slight_smile:

I’m using two VL53L0XV2 Time-of-Flight sensors for the triggers at the top and the bottom. These are both working well and returning on/off values.

I’m using Number templates as counters with on_value to trigger the wipes. I’ve not got as far as testing this so this may not be the best way of achieving this.

Where I’m struggling is getting the ToF sensors to count people passing them to trigger the wipes. Whenever I compile I get the following errors

INFO Reading configuration /config/esphome/esp-dev.yaml...
INFO Detected timezone 'Europe/London'
INFO Generating C++ source...
INFO Compiling app...
Processing esp8266-dev (board: esp01_1m; framework: arduino; platform: platformio/espressif8266 @ 3.2.0)
--------------------------------------------------------------------------------
HARDWARE: ESP8266 80MHz, 80KB RAM, 1MB Flash
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
Dependency Graph
|-- <ESPAsyncTCP-esphome> 1.2.3
|-- <ESPAsyncWebServer-esphome> 2.1.0
|   |-- <ESPAsyncTCP-esphome> 1.2.3
|   |-- <Hash> 1.0
|   |-- <ESP8266WiFi> 1.0
|-- <DNSServer> 1.1.1
|-- <ESP8266WiFi> 1.0
|-- <ESP8266mDNS> 1.2
|-- <Wire> 1.0
|-- <NeoPixelBus> 2.6.9
|   |-- <SPI> 1.0
Compiling /data/esp8266-dev/.pioenvs/esp8266-dev/src/main.cpp.o
/config/esphome/esp-dev.yaml: In lambda function:
/config/esphome/esp-dev.yaml:219:14: error: 'class esphome::number::NumberCall' has no member named 'increment'
  219 |           call.increment(true);
      |              ^~~~~~~~~
/config/esphome/esp-dev.yaml:223:14: error: 'class esphome::number::NumberCall' has no member named 'increment'
  223 |           call.increment(true);
      |              ^~~~~~~~~
/config/esphome/esp-dev.yaml:227:14: error: 'class esphome::number::NumberCall' has no member named 'decrement'
  227 |           call.decrement(true);
      |              ^~~~~~~~~
/config/esphome/esp-dev.yaml: In lambda function:
/config/esphome/esp-dev.yaml:245:14: error: 'class esphome::number::NumberCall' has no member named 'increment'
  245 |           call.increment(true);
      |              ^~~~~~~~~
*** [/data/esp8266-dev/.pioenvs/esp8266-dev/src/main.cpp.o] Error 1
========================= [FAILED] Took 12.63 seconds =========================

I’ve been reading the ESPHome docs for Number Component, Automations & Templates, Template Sensor, and Template Binary Sensor. I’ve also taken inspiration from this post
People counter passing through a door using VL53L0X or VL53L1X Big cheers to SebaVT for the inspiration!

Here’s my complete code so far

esphome:
  name: esp8266-dev

esp8266:
  board: esp01_1m
    
# Enable logging
logger:

# Enable Home Assistant API
api:

ota:
  password: "dontbesilly"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  manual_ip:
    static_ip: 192.168.1.150
    gateway: 192.168.1.254
    subnet: 255.255.255.0
    dns1: 192.168.1.253
    dns2: 8.8.8.8 
    
# Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Esp8266-Dev Fallback Hotspot"
    password: "notherebuddy"

captive_portal:

# Logical switches for testing in HA
switch:
  - platform: template
    id: bottom_switch
    name: "Bottom Switch"
    optimistic: true
    turn_on_action:
      - if:
          condition:
            light.is_off: led_master
          then:
            - light.turn_on:
                id: led_master
                effect: "Wipe In Upwards"
                red: 100%
                green: 71%
                blue: 62%
                brightness: 100%
    turn_off_action:
      - if:
          condition:
            light.is_on: led_master
          then:
            - light.turn_on:
                id: led_master
                effect: "Wipe Out Upwards"
            - delay: 10s
            - light.turn_off: led_master

  - platform: template
    id: top_switch
    name: "Top Switch"
    optimistic: true
    turn_on_action:
      - if:
          condition:
            light.is_off: led_master
          then:
            - light.turn_on:
                id: led_master
                effect: "Wipe In Downwards"
                red: 0%
                green: 100%
                blue: 0%
                brightness: 100%
    turn_off_action:
      - if:
          condition:
            light.is_on: led_master
          then:
            - light.turn_on:
                id: led_master
                effect: "Wipe Out Downwards"
            - delay: 10s
            - light.turn_off: led_master        

# Get current time from HA
time:
  - platform: homeassistant
    id: esptime

# Control of LED strip including up/down and on/off wipes
light:
  - platform: neopixelbus
    variant: WS2812X
    id: led_master
    pin: 3
    num_leds: 254
    type: GRB
    name: "Stairway"
    effects:
      - addressable_lambda:
          name: "Wipe In Upwards"
          lambda: |-
            static int x = 0;
            if (initial_run) {
              x = 0;
              it.all() = ESPColor(0,0,0);
            }
            if (x < it.size()) {
              it[x] = current_color;
              x += 1;
            }
            
      - addressable_lambda:            
          name: "Wipe In Downwards"
          lambda: |-
            static int x = 0;
            if (initial_run) {
              x = it.size();
              it.all() = ESPColor(0,0,0);
            }
            if (x > 0) {
              x -= 1;
              it[x] = current_color;
            }
            
      - addressable_lambda:            
          name: "Wipe Out Upwards"
          lambda: |-
            static int x = 0;
            if (initial_run) {
              x = 0;
              it.all() = current_color;
            }
            if (x < it.size()) {
              it[x] = ESPColor(0,0,0);
              x += 1;
            }
            
      - addressable_lambda:            
          name: "Wipe Out Downwards"
          lambda: |-
            static int x = 0;
            if (initial_run) {
              x = it.size();
            }
            if (x > 0) {
              x -= 1;
              it[x] = ESPColor(0,0,0);
            }
            
# Open up the i2c bus for connecting the ToF sensors
i2c:
  sda: GPIO4
  scl: GPIO5
  id: bus_a
  scan: true

# Logical binary sensors to convert distance to true/false
binary_sensor:
  - platform: template
    id: bottom_laser
    name: "Bottom Laser"
    filters:
      - delayed_off: 100ms
    lambda: !lambda |-
        return id(sens_b).state < 0.5f;
            
  - platform: template
    id: top_laser
    name: "Top Laser"
    filters:
      - delayed_off: 100ms
    lambda: !lambda |-
        return id(sens_t).state < 0.5f;


# Add both ToF sensors
sensor:
  - platform: vl53l0x
    name: "Sensor Bot"
    id: sens_b
    i2c_id: bus_a
    address: 0x41
    update_interval: 1s
    enable_pin: GPIO2
    timeout: 500us
    internal: true
    
  - platform: vl53l0x
    name: "Sensor Top"
    id: sens_t
    i2c_id: bus_a
    address: 0x42
    update_interval: 1s
    enable_pin: GPIO14
    timeout: 500us
    internal: true   

# Update a counter if a ToF is triggered
  - platform: template
    name: "Bottom Trigger"
    lambda: !lambda |-
        if(id(bottom_laser).state == true && id(led_master).current_values.is_on() == false && id(top_counter) == 0){
          auto call = id(bottom_counter).make_call();
          call.increment(true);
          call.perform();
        }else if(id(bottom_laser).state == true && id(led_master).current_values.is_on() == true && id(top_counter) == 0){
          auto call = id(bottom_counter).make_call();
          call.increment(true);
          call.perform();
        }else if(id(bottom_laser).state == true && id(led_master).current_values.is_on() == true && id(top_counter) != 0){
          auto call = id(top_counter).make_call();
          call.decrement(true);
          call.perform();
        }return 0; 
        
        
  - platform: template
    name: "Top Trigger"
    lambda: !lambda |-
        if(id(top_laser).state == true && id(led_master).current_values.is_on() == false && id(bottom_counter) == 0){
          auto call = id(top_counter).make_call();
          call.increment(true);
          call.perform();
        }else if(id(top_laser).state == true && id(led_master).current_values.is_on() == true && id(bottom_counter) == 0){
          auto call = id(top_counter).make_call();
          call.increment(true);
          call.perform();
        }else if(id(top_laser).state == true && id(led_master).current_values.is_on() == true && id(bottom_counter) != 0){
          auto call = id(bottom_counter).make_call();
          call.decrement(true);
          call.perform();
        }return 0; 
        
        
# Counters for stair entry at either top or bottom
number:
  - platform: template
    name: "Bottom Stair Counter"
    id: bottom_counter
    optimistic: true
    step: 1
    min_value: 0
    max_value: 10
    on_value:
      - if:
          condition:
            - number.in_range:
                id: bottom_counter
                above: 1
            - light.is_off: led_master
          then:
            - light.turn_on:
                id: led_master
                effect: "Wipe In Upwards"
                red: 100%
                green: 71%
                blue: 62%
                brightness: 100%
          else:
            - light.turn_on:
                id: led_master
                effect: "Wipe Out Downwards"
            - delay: 6s
            - light.turn_off: led_master        

  - platform: template
    name: "Top Stair Counter"
    id: top_counter
    optimistic: true
    step: 1
    min_value: 0
    max_value: 10
    on_value:
      - if:
          condition:
            - number.in_range:
                id: top_counter
                above: 1
            - light.is_off: led_master
          then:
            - light.turn_on:
                id: led_master
                effect: "Wipe In Downwards"
                red: 100%
                green: 71%
                blue: 62%
                brightness: 100%
          else:
            - light.turn_on:
                id: led_master
                effect: "Wipe Out Upwards"
            - delay: 6s
            - light.turn_off: led_master        

I’m no coder so it’s entirely possible that my code might be complete rubbish :joy:

Can anyone provide any pointers as to where I’m going wrong with the lambda’s, or am I just approaching this from an angle that simply won’t work or doesn’t make any sense.

I’ve not done this, and i’m certainly no expert, but perhaps look at variables? to store the value. It’s how I’d approach it. should then be simple Lambda calculations to read then write the variable, once it’s got an ID you should be able to loop via the variable.

Not related, but what happens if someone starts up the stairs, stops then returns to the bottom? My dog does this a lot… lol

Hey Peter,

Yeah I’ve considered adding global variables as I could still us the on_value as a trigger. I figured it might be easier to use a number and increment/decrement, rather than having to retrieve the current value, add ur subtract, then write it back. I’ll give it a try though and see if it helps.

Blockquote stops then returns to the bottom? My dog does this a lot… lol

Yeah, my cat does the same. He’ll go and sit two or three stairs up for a while, then change his mind. I’ll need to add a timeout of around 1-2 minutes to make sure they don’t stay on. First I need to get it counting though :grin:

OK, so I’ve got it to compile now. The docs here

suggest that call.increment(true) should increment the number. Digging deeper into the API reference here
https://esphome.io/api/number__call_8h_source.html
it should actually be call.number_increment(true)

So this does compile

# Update a counter if a ToF is triggered
  - platform: template
    name: "Bottom Trigger"
    lambda: !lambda |-
        if((id(sens_b).state < 0.5f) && (id(led_master).current_values.is_on() == false) && (id(top_counter) == 0)){
          auto call = id(bottom_counter).make_call();
          call.number_increment(true);
          call.perform();
        }else if((id(sens_b).state < 0.5f) && (id(led_master).current_values.is_on() == true) && (id(top_counter) == 0)){
          auto call = id(bottom_counter).make_call();
          call.number_increment(true);
          call.perform();
        }else if((id(sens_b).state < 0.5f) && (id(led_master).current_values.is_on() == true) && (id(top_counter) != 0)){
          auto call = id(top_counter).make_call();
          call.number_decrement(true);
          call.perform();
        }return id(sens_b).state < 0.5f; 

I’ve also update the first check, to check the sensor directly, rather than checking a binary_sensor.

For some reason this doesn’t increment/decrement the number. I can see all sensors in HA, they all trigger when I put something in front of the ToF, but no incrementing.

I can manually adjust the number counters in HA and that triggers the light effects as I intended so I’m chuffed I got that bit right :grin:

I’ll try changing the number counters to globals later today and see what happens.

So after a few more hours of trying things, I’ve got this working, to the point I’m happy to move on to the hardware installation and sensor callibration.

I’ve learned to not over complicate things, that there’s a key fundamental difference between a sensor and a binary_sensor, and also that (unsurprisingly) working with yaml is a lot easier that lambdas! :joy:

So, I’ve moved the lambda conditions from a sensor to a binary sensor, as this now returns a simple true/false rather than repeated values. I’ve also consolidated most of the conditions into yaml rather than lambdas.

I’ve stuck with incrementing/decrementing numbers for counters rather than variables, as these will still trigger the appropriate lighting effect on_value. I’ve also introduced a timeout for those occasions where an animal (or a human) changes their mind half way up or down the stairs :joy_cat:


# Control of LED strip including up/down and on/off wipes
light:
  - platform: neopixelbus
    variant: WS2812X
    id: led_master
    pin: 3
    num_leds: 254
    type: GRB
    name: "Stairway"
    effects:
      - addressable_lambda:
          name: "Wipe In Upwards"
          lambda: |-
            static int x = 0;
            if (initial_run) {
              x = 0;
              it.all() = ESPColor(0,0,0);
            }
            if (x < it.size()) {
              it[x] = current_color;
              x += 1;
            }
            
      - addressable_lambda:            
          name: "Wipe In Downwards"
          lambda: |-
            static int x = 0;
            if (initial_run) {
              x = it.size();
              it.all() = ESPColor(0,0,0);
            }
            if (x > 0) {
              x -= 1;
              it[x] = current_color;
            }
            
      - addressable_lambda:            
          name: "Wipe Out Upwards"
          lambda: |-
            static int x = 0;
            if (initial_run) {
              x = 0;
              it.all() = current_color;
            }
            if (x < it.size()) {
              it[x] = ESPColor(0,0,0);
              x += 1;
            }
            
      - addressable_lambda:            
          name: "Wipe Out Downwards"
          lambda: |-
            static int x = 0;
            if (initial_run) {
              x = it.size();
            }
            if (x > 0) {
              x -= 1;
              it[x] = ESPColor(0,0,0);
            }
            
# Open up the i2c bus for connecting the ToF sensors
i2c:
  sda: GPIO4
  scl: GPIO5
  id: bus_a
  scan: true

# Add both ToF sensors
sensor:
  - platform: vl53l0x
    name: "Sensor Bot"
    id: sens_b
    i2c_id: bus_a
    address: 0x41
    update_interval: 1s
    enable_pin: GPIO2
    timeout: 500us
    internal: true
    
  - platform: vl53l0x
    name: "Sensor Top"
    id: sens_t
    i2c_id: bus_a
    address: 0x42
    update_interval: 1s
    enable_pin: GPIO14
    timeout: 500us
    internal: true   
    
# Logical binary sensors to work the lighting magic
#
# These do a number of things :
# - Check if maximum calculated distance has been reduced (triggered), and return a state of TRUE
# - If state is TRUE then trun the lights on or off
# - If state is also TRUE, then increment or decrement the top or bpttom counters
#
# Changing the counter value will cause the LED strip to be turned on with the appropriate effect
#
binary_sensor:
  - platform: template
    id: bottom_laser
    name: "Bottom Laser"
    filters:
      - delayed_off: 100ms
    lambda: |-
        return id(sens_b).state < 0.5f;
    on_state:
      - if:
          condition:
            - lambda: return id(bottom_laser).state == true;
            - number.in_range:
                id: top_counter
                below: 0
            - light.is_off: led_master
          then:
            - number.increment:
                id: bottom_counter
                cycle: false
          else:
            - if:
                condition:
                  - lambda: return id(bottom_laser).state == true;
                  - number.in_range:
                      id: top_counter
                      below: 0
                  - light.is_on: led_master
                then:
                  - number.increment:
                      id: bottom_counter
                      cycle: false
                else:
                  - if:
                      condition:
                        - lambda: return id(bottom_laser).state == true;
                        - number.in_range:
                            id: top_counter
                            above: 1
                      then:
                        - number.decrement:
                           id: top_counter
                           cycle: false   
            
  - platform: template
    id: top_laser
    name: "Top Laser"
    filters:
      - delayed_off: 100ms
    lambda: !lambda |-
        return id(sens_t).state < 0.5f;
    on_state:
      - if:
          condition:
            - lambda: return id(top_laser).state == true;
            - number.in_range:
                id: bottom_counter
                below: 0
            - light.is_off: led_master
          then:
            - number.increment:
                id: top_counter
                cycle: false
          else:
            - if:
                condition:
                  - lambda: return id(top_laser).state == true;
                  - number.in_range:
                      id: bottom_counter
                      below: 0
                  - light.is_on: led_master
                then:
                  - number.increment:
                      id: top_counter
                      cycle: false
                else:
                  - if:
                      condition:
                        - lambda: return id(top_laser).state == true;
                        - number.in_range:
                            id: bottom_counter
                            above: 1                        
                      then:
                        - number.decrement:
                           id: bottom_counter
                           cycle: false   
        
# Counters for stair entry at either top or bottom
#
# These counters will tuen the LED strip on/off depending on the counter value
# - >= 1turn on LED strip
# - =< 0 turn off LED strip
#
# Also implements a delay and counter reset. Useful if somebody enters the stairs at one point, then changes their mind
# and exits the stairs at the same point.
#
number:
  - platform: template
    name: "Bottom Stair Counter"
    id: bottom_counter
    optimistic: true
    step: 1
    min_value: 0
    max_value: 10
    on_value:
      - if:
          condition:
            - number.in_range:
                id: bottom_counter
                above: 1
            - light.is_off: led_master
          then:
            - light.turn_on:
                id: led_master
                effect: "Wipe In Upwards"
                red: 100%
                green: 71%
                blue: 62%
                brightness: 100%
            - delay: 120s
            - number.to_min: bottom_counter
          else:
            - if:
                condition:
                  - number.in_range:
                      id: bottom_counter
                      below: 0
                  - light.is_on: led_master
                then:
                  - light.turn_on:
                      id: led_master
                      effect: "Wipe Out Upwards"
                  - delay: 5s
                  - light.turn_off: led_master     

  - platform: template
    name: "Top Stair Counter"
    id: top_counter
    optimistic: true
    step: 1
    min_value: 0
    max_value: 10
    on_value:
      - if:
          condition:
            - number.in_range:
                id: top_counter
                above: 1
            - light.is_off: led_master
          then:
            - light.turn_on:
                id: led_master
                effect: "Wipe In Downwards"
                red: 100%
                green: 71%
                blue: 62%
                brightness: 100%
            - delay: 120s
            - number.to_min: top_counter                
          else:
            - if:
                condition:
                  - number.in_range:
                      id: top_counter
                      below: 0                
                  - light.is_on: led_master
                then:
                  - light.turn_on:
                      id: led_master
                      effect: "Wipe Out Downwards"
                  - delay: 5s
                  - light.turn_off: led_master          
3 Likes