No more pigeon poo on my outdoor table

I’m using Frigate to detect pigeons and a DIY ESPHome device to scare them.
It’s been working great so far.

Thanks to ssieb and nkinnan over on Discord for some wiring advice a while back.

PromoVid_Small-ezgif.com-resize

Overview of deterrants which get triggered…

ESPHome code:
substitutions:
  disco_ball_pin:  GPIO27
  laser_beam_pin:  GPIO32
  water_spray_pin: GPIO25
  buzzer_pin:      GPIO18  

  interlock_wait_time_s: 1s
  switch_auto_off_after_x_secs: 10s
  scare_auto_off_after_x_min: 1min
  scare_cycle_delay: 5s

esphome:
  name: pigeon-punisher
  friendly_name: Pigeon Punisher
esp32:
  board: mhetesp32devkit
  framework:
    type: esp-idf
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
      static_ip: 
      gateway:
      subnet: 
api:
ota:
  - platform: esphome
logger:
  # level: VERBOSE
    # baud_rate: 0

output:
#Rttl buzzer
  - platform: ledc
    pin: ${buzzer_pin}
    id: buzzer_pwm


binary_sensor:

  - platform: homeassistant
    name: "People On Balcony"
    id: "people_on_balcony"
    entity_id: binary_sensor.human_watch_person_occupancy
    icon: mdi:account-question
    publish_initial_state: true
    internal: false

  - platform: copy
    source_id: people_on_balcony
    name: "People On Balcony Delay Off"
    id: "people_on_balcony_delay_off"
    filters:
      - delayed_off: 3min

  - platform: homeassistant
    name: "Bird Detected"
    id: "bird_detected"
    entity_id: binary_sensor.pigeon_watch_bird_occupancy
    icon: mdi:bird
    publish_initial_state: true
    internal: false
    on_release:
      - switch.turn_off: scare_pigeons

  - platform: template
    id: bird_detected_humans_clear
    name: Bird Detected Humans Clear
    icon: mdi:hand-okay
    lambda: |-
      return
           id(bird_detected).state
        && !id(people_on_balcony_delay_off).state     // No people on balcony
        && !id(recently_scared_pigeons).state         // Scare pigeons actions not triggered recently
      ;
    filters:
      - delayed_off: 3sec
    on_press:
      - switch.turn_on: scare_pigeons
    on_release:
      - switch.turn_off: scare_pigeons
      

  - platform: template
    id: recently_scared_pigeons
    name: Recently Scared Pigeons
    # icon: mdi:hand-okay
    lambda: |-
      return id(scare_pigeons).state ;
    filters:
      - delayed_on: 30sec
      - delayed_off: 30min

number:
  - platform: template
    name: "Buzzer Frequency"
    id: buzzer_freq
    min_value: 100
    max_value: 10000
    step: 100
    initial_value: 8000
    unit_of_measurement: Hz
    optimistic: true
    on_value:
      - lambda: |-
          id(buzzer_pwm).update_frequency((int)x);
  - platform: template
    name: "Buzzer Level"
    id: buzzer_level
    min_value: 0
    max_value: 1
    step: 0.05
    initial_value: 0.5
    unit_of_measurement: "%"
    optimistic: true
    on_value:
      - lambda: |-
          id(buzzer_pwm).set_level(x);

globals:
  - id: last_base_freq
    type: int
    initial_value: '4000'
  - id: last_base_level
    type: float
    initial_value: '0.5'

select:
  - platform: template
    name: "Random Buzzer Effect"
    id: buzzer_effect
    options:
      - "IDLE"
      - "WARBLE"
      - "CHIRP"
      - "PULSING"
      - "LONG SHRIEK"
      - "BEATING"
      - "TREMOLO"
    optimistic: true
    initial_option: "IDLE"
    on_value:
    - if:
        condition:
          lambda: 'return id(buzzer_effect).state == "WARBLE";'
        then:
          - repeat:
              count: 15
              then:
                - lambda: |-
                    int freq = 2500 + ((iteration % 2 == 0) ? 300 : -300) + (esphome::random_uint32()%200-100);
                    float lvl = id(last_base_level) * 0.8 + ((esphome::random_uint32()%20)/100.0f - 0.1);
                    if(lvl<0) lvl=0; if(lvl>1) lvl=1;
                    id(buzzer_pwm).update_frequency(freq);
                    id(buzzer_pwm).set_level(lvl);
                    ESP_LOGD("buzzer","WARBLE iteration %d: freq=%d, level=%.2f", iteration, freq, lvl);
                - delay: 100ms
          - output.set_level:
              id: buzzer_pwm
              level: 0
          - delay: 500ms
          - select.set:
              id: buzzer_effect
              option: "IDLE"

    - if:
        condition:
          lambda: 'return id(buzzer_effect).state == "CHIRP";'
        then:
          - repeat:
              count: 30
              then:
                - lambda: |-
                    int step = iteration % 10;
                    int freq = 2200 + step*100 + (esphome::random_uint32()%200-100);
                    float lvl = id(last_base_level) * 0.75 + ((esphome::random_uint32()%20)/100.0f -0.1);
                    if(lvl<0) lvl=0; if(lvl>1) lvl=1;
                    id(buzzer_pwm).update_frequency(freq);
                    id(buzzer_pwm).set_level(lvl);
                - delay: 20ms
                - output.set_level:
                    id: buzzer_pwm
                    level: 0
                - delay: 50ms
          - delay: 200ms
          - select.set:
              id: buzzer_effect
              option: "IDLE"

    - if:
        condition:
          lambda: 'return id(buzzer_effect).state == "PULSING";'
        then:
          - repeat:
              count: 6
              then:
                - lambda: |-
                    int freq = 2300 + (iteration % 300) + (esphome::random_uint32()%200-100);
                    float lvl = ((iteration%2==0)?id(last_base_level)*0.8:id(last_base_level)*0.4) + ((esphome::random_uint32()%20)/100.0f -0.1);
                    if(lvl<0) lvl=0; if(lvl>1) lvl=1;
                    id(buzzer_pwm).update_frequency(freq);
                    id(buzzer_pwm).set_level(lvl);
                - delay: 50ms
          - output.set_level:
              id: buzzer_pwm
              level: 0
          - delay: 200ms
          - select.set:
              id: buzzer_effect
              option: "IDLE"

    - if:
        condition:
          lambda: 'return id(buzzer_effect).state == "LONG SHRIEK";'
        then:
          - lambda: |-
              int freq = 3000 + (esphome::random_uint32()%200-100);
              float lvl = id(last_base_level)*0.8 + ((esphome::random_uint32()%20)/100.0f -0.1);
              if(lvl<0) lvl=0; if(lvl>1) lvl=1;
              id(buzzer_pwm).update_frequency(freq);
              id(buzzer_pwm).set_level(lvl);
          - delay: 700ms
          - output.set_level:
              id: buzzer_pwm
              level: 0
          - delay: 500ms
          - select.set:
              id: buzzer_effect
              option: "IDLE"

    - if:
        condition:
          lambda: 'return id(buzzer_effect).state == "BEATING";'
        then:
          - repeat:
              count: 8
              then:
                - lambda: |-
                    int base = 2500 + (esphome::random_uint32()%200-100);
                    int delta = 50 + (esphome::random_uint32()%50);
                    id(buzzer_pwm).update_frequency(base);
                    id(buzzer_pwm).set_level(0.7);
                - delay: 25ms
                - lambda: |-
                    int base = 2500 + (esphome::random_uint32()%200-100);
                    int delta = 50 + (esphome::random_uint32()%50);
                    id(buzzer_pwm).update_frequency(base+delta);
                    id(buzzer_pwm).set_level(0.7);
                - delay: 25ms
          - output.set_level:
              id: buzzer_pwm
              level: 0
          - delay: 200ms
          - select.set:
              id: buzzer_effect
              option: "IDLE"

    - if:
        condition:
          lambda: 'return id(buzzer_effect).state == "TREMOLO";'
        then:
          - repeat:
              count: 30
              then:
                - lambda: |-
                    id(buzzer_pwm).update_frequency(2600 + (esphome::random_uint32()%200-100));
                    id(buzzer_pwm).set_level(0.65);
                - delay: 10ms
                - lambda: |-
                    id(buzzer_pwm).set_level(0.25);
                - delay: 10ms
          - output.set_level:
              id: buzzer_pwm
              level: 0
          - delay: 300ms
          - select.set:
              id: buzzer_effect
              option: "IDLE"




sensor:
  - platform: uptime
    id: uptime_sensor
    name: "Uptime Sensor"
    update_interval: 5s

switch:
  - platform: template
    name: "Buzzer Enable"
    id: buzzer_enable
    optimistic: true
    turn_on_action:
      - lambda: |-
          id(buzzer_pwm).set_level(id(buzzer_level).state);
    turn_off_action:
      - lambda: |-
          id(buzzer_pwm).set_level(0);

  - name: Disco Ball
    id: disco_ball   
    platform: gpio
    pin: ${disco_ball_pin}
    internal: false
    restore_mode: ALWAYS_OFF    
    interlock:  &interlock_group [disco_ball, laser_beam, water_spray]
    interlock_wait_time:  ${interlock_wait_time_s}
    on_turn_on: # Auto Off timer
      - script.stop: auto_off_disco       # Stop existing count down timers
      - script.execute: auto_off_disco    # Start new timer

  - name: Laser Beam
    id: laser_beam   
    platform: gpio
    pin: ${laser_beam_pin}
    internal: false
    restore_mode: ALWAYS_OFF   
    interlock: *interlock_group
    interlock_wait_time:  ${interlock_wait_time_s}
    on_turn_on: # Auto Off timer
      - script.stop: auto_off_laser       # Stop  existing count down timers
      - script.execute: auto_off_laser    # Start new timer

  - name: Water Spray
    id: water_spray   
    platform: gpio
    pin: ${water_spray_pin}
    internal: false
    restore_mode: ALWAYS_OFF
    interlock: *interlock_group
    interlock_wait_time:  ${interlock_wait_time_s}
    on_turn_on: # Auto Off timer
      - script.stop: auto_off_water_spray       # Stop any existing count down timers
      - script.execute: auto_off_water_spray    # Start new timer

  - platform: template
    name: Scare Pigeons
    id: scare_pigeons
    optimistic: true
    on_turn_on: # Auto Off timer
      - script.stop:    auto_off_scare_pigeons       # Stop any existing count down timers
      - script.execute: auto_off_scare_pigeons       # Start new timer
    turn_on_action:
    - delay: 500ms
    - switch.turn_on: annoying_sounds
    - while:
        condition:
          switch.is_on: scare_pigeons
        then: # Cycle through scare devices with randomised duration of each.
          - switch.turn_on: water_spray
          - delay: !lambda "return (esphome::random_uint32() % 18 + 2) * 1000;"
          - switch.turn_on: laser_beam
          - delay: !lambda "return (esphome::random_uint32() % 18 + 2) * 1000;"
          - switch.turn_on: disco_ball
          - delay: !lambda "return (esphome::random_uint32() % 18 + 2) * 1000;"

    turn_off_action:
      - switch.turn_off: laser_beam
      - switch.turn_off: water_spray
      - switch.turn_off: disco_ball
      - switch.turn_off: annoying_sounds

  - platform: template
    name: "Annoying Sounds"
    id: annoying_sounds
    optimistic: true
    turn_on_action:
      - switch.turn_on: buzzer_enable
      - script.execute: annoying_sounds_loop
    turn_off_action:
      - switch.turn_off: buzzer_enable
      - script.stop: annoying_sounds_loop

script:
  - id: auto_off_disco
    mode: restart
    then:
      - delay: ${switch_auto_off_after_x_secs}
      - switch.turn_off: disco_ball
  - id: auto_off_laser
    mode: restart
    then:
      - delay: ${switch_auto_off_after_x_secs}
      - switch.turn_off: laser_beam
  - id: auto_off_water_spray
    mode: restart
    then:
      - delay: ${switch_auto_off_after_x_secs}
      - switch.turn_off: water_spray
  - id: auto_off_scare_pigeons
    mode: restart
    then:
      - delay: ${scare_auto_off_after_x_min}
      - switch.turn_off: scare_pigeons


# --- Loop script: pick random effect each cycle ---
  - id: annoying_sounds_loop
    mode: restart
    then:
      - delay: 500ms
      - while:
          condition:
            switch.is_on: annoying_sounds
          then:
            - lambda: |-
                auto size = id(buzzer_effect).size();
                if (size == 0) return;
                int idx = esphome::random_uint32() % size;
                auto call = id(buzzer_effect).make_call();
                call.set_index(idx);
                call.perform();
                ESP_LOGD("buzzer", "Random effect selected index: %d", idx);
            - wait_until:
                lambda: 'return id(buzzer_effect).state == "IDLE";'
            - delay: !lambda "return (esphome::random_uint32() % 3000 + 500);" # Delay 0.5–3.5 s before next change

8 Likes

I understand you perfectly, pigeons are my kryptonite

(oooo-ooo oo-oo gruu-gruu)

1 Like

Please do not connect high power lasers to this device to blind them.

1 Like

It’s just one of these low powered ones :wink:

The idea was to copy this kind of approach.

Not sure how effective that aspect of it is to be honest.
The water spray which also makes a pretty loud ossilating mechanical noise seems to be the most effective.

The lasers aren’t so visible during daylight. Perhaps they will help in winter or for would be overnight roosting.

When visiting my friend in Australia, he had purchased a bird scarer online. The birds had devastated his fruit crop. He had tried sirens, gas bang guns, hanging unwanted shiny CDs and curly silver foil from the branches, everything, but still the birds came. Daily, relentless.

A bird scarer gadget he found online seemed ideal. It promised to scare them away by playing sounds of eagles and other birds of prey through a series of loudspeakers scattered throughout the orchard.

Quite expensive, but with many good reviews, he put down his money (grumbling about the poor exchange rate), paid for expedited air freight from the US (which cost him more than the item), and waited.

The big box finally arrived.

Duly unpacked, set up and configured, and I arrived at the orchard in time for first switch on.

There were sounds of shrieks and whooshing filled the air, in multichannel sound, giving the impression there were many birds flying across the orchard.

The milling birds, in their hundreds, looked up from wreaking devastation, and carried on after a few seconds as if nothing had happened .

We were shocked. It wasn’t working. We checked the connections, replaced the batteries, changed the sound track to another supplied, everything. The birds just kept chewing through the fruit. I’m sure I could see a few laughing at us humans, furiously running around.

We even got the manual out to check if we had missed something.

Oh how we laughed. On the front cover, proudly in full color, was a picture of an American bald eagle swooping at prey.

A magnificent picture. In full flight. Something that should scare the living Bejeezuz out of any bird.

Except these birds were Australian. Fair dinkum noisy and hungry, and had never seen, and more importantly, heard an American eagle or bird of prey. They just looked up and ignored us, and the funny sounds filling the orchard. Not afraid at all.

My mate was furious. Waving the manual at the birds. We were falling about in laughter. He contacted the supplier who offered his money back, and asked if he could participate in a test program for sounds of Australian birds of prey in attack mode.

Moral of the story? Always adapt to local conditions.

2 Likes

Interesting.
Yeah I was looking at “predator calls” and “pigeon distress calls” too.
I decided to start with a simple passive buzzer which makes a bunch of “possibly annoying but that’s unconfirmed” noises targeted at thier audio range (or so my AI developer says).
I might put a speaker out there later if needed. I actually got some Atom Echo’s to play with but they are too quiet.

One thing I was reading online is that pigeon’s through either thier intelligence or dumbness, bascially figure out pretty quickly where there is a real threat and also get used to things. So I tried to add a fair bit of variety to my scares and add a bit of randomness to it all. Maybe that helps.
For now the noisey water psitol seems to be the real winner.

Someone on Discord said, “geez those peigeons are going to have a great disco water party” :slight_smile:

The birds should RTFM!!!

Pigeon party!!!

For lulz, with us cheering him along, my mate changed the supplied CD to Pr!nce, and the birds kept away for a few days. They seemed to like Black Sabbath however, chewing to the heavy rock beat.

We tried Barbra Streisand - the shrieks were quite effective.

Spotify tracks for pigeons anyone?

1 Like