Displaying Birdnet-go detections

Great topic! I’ve added MQTT to the birdnet-pi addon too with the same MQTT handle so that those cards can mostly be used as such

Given it publishes the SpeciesCode you could also use : https://ebird.org/species/{{SpeciesCode}} or https://ebird.org/species/{{SpeciesCode}}?siteLanguage={{language}} where language can be whatever you want to have it in your own language

Just got Birdnet-go setup (using the add-on). Would there be an easy way to also pull the thumbnail/picture of each identified bird into the card?

At the moment the solution is to open a BirdWeather account to connect your BirdNet to and then use this: Bird detector sensors and dashboard

Tangentially related, I’d like to setup two additional template sensors: The total number of detections and the total number of species identified (both above a defined confidence level and for each day, same as the markup card and main template sensor). Has anyone done this or care to show me how? Interacting with lists of data in the attributes is just a touch beyond my native abilities. Thanks in advance!

Combining a bunch of the suggestions here including @alexbelgium 's ebird links and @brooksben11 's request for totals

In my HA config file I set up the triggered template sensor (note the speciesCode field I added that the previous examples didn’t include):

template:
  - trigger:
      - platform: mqtt
        topic: "birdnet"
      - platform: time
        at: "00:00:00"
        id: reset
    sensor:
      - unique_id: c893533c-3c06-4ebe-a5bb-da833da0a947
        name: BirdNET-Go Events
        state: "{{ today_at(trigger.payload_json.Time) }}"
        attributes:
          bird_events: >
            {% if trigger.id == 'reset' %}
              {{ [] }}
            {% else %}
              {% set time = trigger.payload_json.Time %}
              {% set name = trigger.payload_json.CommonName %}
              {% set speciesCode = trigger.payload_json.SpeciesCode %}
              {% set confidence = trigger.payload_json.Confidence|round(2) * 100 ~ '%' %}
              {% set current = this.attributes.get('bird_events', []) %}
              {% set new = dict(time=time, name=name, speciesCode=speciesCode, confidence=confidence) %}
              {{ current + [new] }}
            {% endif %}                                                                                                 

I then addeed a card to my dashboard with this yaml:

type: markdown
content: >-
  Time|  Common Name|Day Count|  Max Confidence

  :---|:---|:---:|:---:

  {%- set t = now() %}

  {%- set bird_list = state_attr('sensor.birdnet_go_events','bird_events') |
  sort(attribute='time', reverse=true) | map(attribute='name') | unique | list
  %}

  {%- set bird_objects = state_attr('sensor.birdnet_go_events','bird_events') |
  sort(attribute='time', reverse=true) %}

  {%- for thisbird in bird_list or [] %}

  {%- set ubird = ((bird_objects | selectattr("name", "equalto", thisbird)) |
  list)[0] %}

  {%- set ubird_count = ((bird_objects | selectattr("name", "equalto",
  thisbird)) | list) | length %}

  {%- set ubird_max_confidence = ((bird_objects | selectattr("name", "equalto",
  thisbird)) | map(attribute='confidence') | max) %}

  {{ubird.time}} | 
  [{{ubird.name}}](https://ebird.org/species/{{ubird.speciesCode}}) |
  {{ubird_count}} |  {{ubird_max_confidence }}

  {%- endfor %}


  Total Detections: {{bird_objects | count}}

  Total Species Detected: {{bird_list | count}}
title: Birds Seen Today

Which gives me a card that looks like this:

As an aside, my setup runs the mic outside just dumping to RTSP so since I already had the RTSP set up I included that on my dashboard as well (with a live spectrogram :slight_smile: )
image

2 Likes

Thanks for sharing! I’d already put together a daily total tracker, but this helped me get what I was ultimately after.

I actually care less about displaying the totals/counts and more about graphing it over time, so I setup template sensors using your code.

      - name: BirdNET-Go Daily Bird Count
        unique_id: birdnet_go_daily_bird_count
        state: >
          {%- set bird_list = state_attr('sensor.birdnet_go_events_above_70','bird_events') |
          sort(attribute='time', reverse=true) | map(attribute='name') | unique | list
          %}
        
          {%- set bird_objects = state_attr('sensor.birdnet_go_events_above_70','bird_events') |
          sort(attribute='time', reverse=true) %}
              
            {{bird_objects | count}}
        icon: mdi:counter
        unit_of_measurement: 'birds'
        state_class: total_increasing

#Daily Species Count
      - name: BirdNET-Go Daily Species Count
        unique_id: birdnet_go_daily_species_count
        state: >
          {%- set bird_list = state_attr('sensor.birdnet_go_events_above_70','bird_events') |
          sort(attribute='time', reverse=true) | map(attribute='name') | unique | list
          %}
        
          {%- set bird_objects = state_attr('sensor.birdnet_go_events_above_70','bird_events') |
          sort(attribute='time', reverse=true) %}
               
            {{bird_list | count}}
        icon: mdi:counter
        unit_of_measurement: 'species'
        state_class: total_increasing

I also only really want to track things that are of a decent confidence level, so I updated my main sensor to only capture identifications above 70%.

      - unique_id: birdnet_go_events_above_70%
        name: BirdNET-Go Events Above 70%
        icon: mdi:bird
        state: >
          {% if trigger.id == 'reset' %}
            {{ now() }}
          {% elif (trigger.payload_json.Confidence > 0.7) %}
            {{ today_at(trigger.payload_json.Time) }}
          {% else %}
            {{ this.state }}
          {% endif %}
        attributes:
          bird_events: >
            {% if trigger.id == 'reset' %}
              {{ [] }}
            {% elif (trigger.payload_json.Confidence > 0.7) %}
              {% set time = trigger.payload_json.Time %}
              {% set name = trigger.payload_json.CommonName %}
              {% set confidence = trigger.payload_json.Confidence|round(2) * 100 ~ '%' %}
              {% set current = this.attributes.get('bird_events', []) %}
              {% set new = dict(time=time, name=name, confidence=confidence) %}
              {{ current + [new] }}
            {% else %}
              {{ this.attributes.get('bird_events', []) }}
            {% endif %}

Seems to be working pretty well; I’m sure I’ll still end up tweaking things as time goes on.

1 Like

Problem:

Ok, iget this up and running. But: In BirdNET-Pi as HA AddOn i count Haussperling 85 times in HA are only 31 and some Birds are compleatly missing.

Whith MQTT Explorer i could see, the last detection is missing!!!

Is this a problem by the BirdNET AddOn?

Would help if you posted what code you’re using for your sensor, but if you used what I posted then it’s filtering out anything less than 70% confidence.

Hi, we are working on the birdnet pi add-on mqtt publish here it will be solved soon 🐛 [BirdNET-Pi] MQTT is not showing all detections · Issue #1515 · alexbelgium/hassio-addons · GitHub

BirdNET-Pi Addon is fixed since Version 0.13.85. To show the Picture of the last detected Bird you need 0.13.86.

Now i have the same detections in home assistant as in BirdNET-Pi. MQTT sends now all detections. Yeah! Thanks Alex.

I have adjusted the Markdown map and now sort by number. I also have a map that shows the last bird identified. For the sake of completeness, here is everything again:

configuration.yaml

template: !include templates.yaml

templates.yaml

(If your templates.yaml starts with sensor: change this to - sensor:)

- trigger:
    - platform: mqtt
      topic: birdnet
    - platform: time
      at: "00:00:00"
      id: reset
  sensor:
    - name: BirdNET
      unique_id: birdnet
      state: "{{ today_at(trigger.payload_json.Time) }}"
      icon: mdi:bird
      attributes:
        birds: >
          {% if trigger.id == 'reset' %}
            {{ [] }}
          {% else %}
            {% set date = trigger.payload_json.Date %}
            {% set time = trigger.payload_json.Time %}
            {% set scientificName = trigger.payload_json.ScientificName %}
            {% set commonName = trigger.payload_json.CommonName %}
            {% set confidence = ( trigger.payload_json.Confidence | round(2) * 100 ) | int() ~ '%' %}
            {% set speciesCode = trigger.payload_json.SpeciesCode %}
            {% set clipName = trigger.payload_json.ClipName %}
            {% set url = trigger.payload_json.url.replace('en.', 'de.') %}
            {% set flickrImage = trigger.payload_json.FlickrImage %}
            {% set current = this.attributes.get('birds', []) %}
            {% set new = dict(time=time, scientificName=scientificName, commonName=commonName, confidence=confidence, speciesCode=speciesCode, clipName=clipName, url=url, flickrImage=flickrImage) %}
            {{ current + [new] }}
          {% endif %}

Markdown Card with Bird-List (sorted by count)

{%- if states('sensor.birdnet') != "unavailable"  %}

{%- set bird_species = state_attr('sensor.birdnet','birds') | sort(attribute='time', reverse=true) | map(attribute='commonName') | unique() | list() %}
{%- set bird_objects = state_attr('sensor.birdnet', 'birds') | sort(attribute='time', reverse=true) %}

{%- set ns = namespace(birds_data=[]) %}

{%- for bird_s in bird_species %}
  {%- set bird_o = ((bird_objects | selectattr("commonName", "equalto", bird_s)) | list)[0] %}
  {%- set bird_o_count = ((bird_objects | selectattr("commonName", "equalto", bird_s)) | list) | length %}
  {%- set bird_o_max_confidence = ((bird_objects | selectattr("commonName", "equalto", bird_s)) | map(attribute='confidence') | max) %}

  {%- set bird_entry = {
      'time': bird_o.time,
      'commonName': bird_o.commonName,
      'speciesCode': bird_o.speciesCode,
      'url': bird_o.url,
      'count': bird_o_count,
      'max_confidence': bird_o_max_confidence
    } %}

  {%- set ns.birds_data = ns.birds_data + [bird_entry] %}
{%- endfor %}

{%- set birds_sorted = ns.birds_data | sort(attribute='count', reverse=true) %}

Zuletzt |     Wer (eBird) | Wiki |     Wie oft |     max.
:-:|:-|:-:|-:|:-:

{%- for bird in birds_sorted %}
  {{ bird.time }} |     [{{ bird.commonName }}](https://ebird.org/species/{{ bird.speciesCode }})     | [⧉]({{ bird.url }}) |     {{ bird.count }} |     {{ bird.max_confidence }}
{%- endfor %}

{{ bird_objects | count }} Erkennungen
{{ bird_species | count }} Arten

{%- else %}

Noch keine Erkennung vorhanden.

{%- endif %}

Markdown Card with Picture of the last detected Bird

{%- if states('sensor.birdnet') != "unavailable" %}

{%- set bird_objects = state_attr('sensor.birdnet', 'birds') | sort(attribute='time', reverse=true) | list() %}

{%- set last_bird = bird_objects[0] %}

![{{ last_bird.commonName }}]({{ last_bird.flickrImage }})

{%- else %}

Noch keine Erkennung vorhanden.

{%- endif %}

Thanks to you all.

If you want to upload you BirdNET detections to eBird?

automation to create eBird csv-file

alias: BirdNET zu eBird CSV-Datei schreiben
description: ""
trigger:
  - platform: mqtt
    topic: birdnet
condition: []
action:
  - action: shell_command.birdnet_to_ebird
    data:
      arg1: "{{ trigger.payload_json.CommonName | default('') }}"
      arg2: ""
      arg3: "{{ trigger.payload_json.ScientificName | default('') }}"
      arg4: ""
      arg5: ""
      arg6: your city
      arg7: "your longitude"
      arg8: "your latitude"
      arg9: >-
        {{ trigger.payload_json.Date | default('') | as_timestamp |
        timestamp_custom('%m/%d/%Y', true) }}
      arg10: "{{ trigger.payload_json.Time | default('') }}"
      arg11: your_state (BW/TX...)
      arg12: your_country (DE/US...)
      arg13: stationary
      arg14: "1"
      arg15: "1"
      arg16: "Y"
      arg17: ""
      arg18: ""
      arg19: ""
mode: single

automation to get the file via email

alias: BirdNET zu eBird CSV-Datei per E-Mail senden und löschen
description: ""
trigger:
  - platform: time
    at: "01:00:00"
condition: []
action:
  - variables:
      filename: >-
        /config/www/birdnet-to-ebird_{{ (now() -
        timedelta(days=1)).strftime('%Y-%m-%d') }}.csv
  - data:
      title: BirdNET zu eBird
      target:
        - [email protected]
      message: >-
        BirdNET zu eBird


        Bitte die beigefügte CSV-Datei aus BirdNET vom {{ ( now() -
        timedelta(days=1)).strftime('%d.%m.%Y') }}

        bei eBird im "eBird Aufzeichnungsformat (erweitert)" hochladen.

        https://ebird.org/import/upload.form
      data:
        images:
          - "{{ filename }}"
        html: >
          <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html
          lang="en" xmlns="http://www.w3.org/1999/xhtml">
            <head>
              <meta charset="UTF-8">
              <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
              <meta name="viewport" content="width=device-width, initial-scale=1.0">
              <title>BirdNET zu eBird</title>
              <style type="text/css">
                h1,h2,h3,h4,h5,h6 {
                font-family: Arial, sans-serif;
                }
              </style>
            </head>
            <body>
              <h3>BirdNET zu eBird</h3>
              Bitte die beigefügte CSV-Datei aus <b>BirdNET</b> vom {{ ( now() - timedelta(days=1)).strftime('%d.%m.%Y') }}
              <br />
              bei <b>eBird</b> im <i>eBird Aufzeichnungsformat (erweitert)</i> hochladen.
              <br />
              https://ebird.org/import/upload.form
            </body>
          </html>
    action: notify.smtp
  - action: shell_command.rm
    data:
      filename: "{{ filename }}"
mode: single

configuration.yaml

notify:
  - platform: smtp
    name: smtp
    sender: [email protected]
    recipient: []
    server: your smtp servername here
    port: 25
    encryption: starttls
    sender_name: Home Assistant
    debug: true
    verify_ssl: false
1 Like

How did you extract that spectrogram? Does anyone know how to extract it as a plain image? My main goal is to use it in a picture-elements card, like this, but it’s still just the thumb of the sound file and renders too slow to sync with the sensor update, so it show as unavailable when a new bird is recognized.

Skjermbilde 2024-08-22 kl. 16.15.06

The card:

  - type: picture-elements
    image_entity: image.garden_bird_photo
    elements:
      - type: custom:config-template-card
        entities:
          - sensor.garden_bird_recording
        element:
          type: image
          image: ${ states['sensor.garden_bird_recording'].state }
        style:
          top: 13%
          left: 16%
          width: 30%
          opacity: 0.6
          border: 2px solid white

The sensor:

template:
  - sensor:
      - name: "garden bird recording"
        state: "{{ 'http://192.168.86.64:8123/api/hassio_ingress/**token**/spectrogram?clip=' ~ state_attr('sensor.garden_birds', 'ClipName') }}"
        attributes:
          url: "{{ '/api/hassio_ingress/**token**/spectrogram?clip=' ~ state_attr('sensor.garden_birds', 'ClipName') }}"

A post was merged into an existing topic: BirdNET discussion

My spectrogram isn’t scraped from my birdnet install at all.

As I sort of mentioned previously (though maybe not clearly), I split up my outdoor station (with the microphone) from the actual processing. I’m using birdnet-go rather than birdnet-pi, and the go version supports receiving microphone input via RTSP.

My outdoor node with the mic is running two services:

  1. mediamtx, which serves the rtsp stream
  2. a service that runs ffmpeg which reads from the mic, generates the spectrogram, and passes that audio+video feed to the rtsp socket run by the previous service

Here is my ffmpeg command (some of this may be hardware-specific):

ffmpeg -hide_banner -f alsa -ac 1 -i "hw:CARD=Device,DEV=0" -filter_complex "[0:a]showspectrum=s=hd480:mode=combined:slide=scroll:saturation=0.5:color=rainbow:scale=log,fps=15,format=yuv420p[v]" -map "[v]" -map 0:a -c:v libx264 -c:a aac -b:a 128k -f rtsp -rtsp_transport tcp rtsp://localhost:8554/birds

My indoor node just ingests that RTSP stream and handles the detection.

I’m not sure that any of that helps with your goal though, since it is pretty heavily geared for running it live… If you are saving the sound files somewhere you may be able to use ffmpeg to generate non-thumbnail versions of those spectrograms? Here is a spectrogram of a 15s recording of an osprey that I picked up yesterday, generated with

ffmpeg -i pandion_haliaetus_100p_20240902T180913Z.wav -lavfi showspectrumpic pandion_haliaetus_100p_20240902T180913Z.png

This took my machine ~1.5s to generate. You could obviously have ffmpeg drop the labels and stuff and generate a different resolution or whatever else you wanted as well.

1 Like

This is awesome @UlrichC ! Thanks also for sharing your implementation to add pictures.

Could you share what is in your shell command “shell_command.birdnet_to_ebird”?

I’d love to try out the eBird connection you shared.

Here is a short hint for anyone worried about their home assistant filled with BirdNet data or getting too many log entries like:

State attributes for sensor.birdnet_go_events exceed maximum size ...

You can avoid recording all events by defining configuring your recorder as follows, replacing sensor.birdnet_go_events with the name of your BirdNet event sensor:

recorder:
  exclude:
    entities:
      - sensor.birdnet_go_events

Just be aware that if you do that you will lose your sensor contents after a restart as the mqtt messages are not sent with the retained flag set.

Hi @tom_l

Are you sure about this?

I restarted several times and did not lose the latest value (sensor contents), just the recorded history. From my understanding of the recorder integration, it is all about persisting the state history, not the current/latest value itself.

As I am storing the day’s history in the latest value, it seems to work well.

The mqtt sensor will lose all previous detections after a restart.