Displaying Birdnet-go detections

That’s awesome. Thank you for sharing. The only addition I made was a couple of   spaces in front of the name and confidence for legibility:

Screenshot 2024-04-17 at 11-22-44 Mobile View – Home Assistant

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}} | {{ubird_count}} |  {{ubird_max_confidence }}
{%- endfor %}

So I got a new microphone. In the nighttime recordings (after traffic noise has died down) I can hear the waves crashing on the beach 1.3Km away and 100m below.
Lots more bird detections. Too many in-fact:

Logger: homeassistant.components.recorder.db_schema
Source: components/recorder/db_schema.py:598
integration: Recorder (documentation, issues)
First occurred: 15:26:57 (14 occurrences)
Last logged: 15:53:00

State attributes for sensor.birdnet_go_events exceed maximum size of 16384 bytes. This can cause database performance issues; Attributes will not be stored

1 Like

Create multiple sensors and filter based on time?
So 1 for the hours 00:00 - 06:00
the 2nd none for 06:00 - 12:00 etc

Hello, can you share configuration of mqtt sensor for birdnet-go? I’m trying to get the data from birdnet

mqtt:
  sensor:
    - name: "BirdNet"
      state_topic: "birdnet"
      unique_id: bird

I don’t use an mqtt sensor. I use a triggered template sensor as shown above Displaying Birdnet-go detections - #6 by tom_l

And this post: Displaying Birdnet-go detections - #7 by tom_l

1 Like

thanks Tom, it’s works for me, maybe you can tell me how to add a link to the wiki about each bird like this https://community.home-assistant.io/t/birdnet-pi/444654/10

fixed :wink:

https://en.wikipedia.org/wiki/{{bird_name | replace(' ', '_')}}

Thanks a lot for sharing this! It is awesome, and works very well.

I fine-tuned a bit the card to provide a link to the BirdNet-Go UI, filter the results (confidence above 70), embed the Wikipedia links and add a nice icon.

Here is the full card snippet in case you are interested:

It is in German, but easy to translate. You would have to replace “IP_OF_YOUR_BIRDNETGO_UI” by the IP of your HA instance or wherever your BirdNET-Go is running.

  - type: markdown
    title: Vogelbeobachtungen
    content: >-
      Uhr|  Vogelname|Anzahl Heute|    Max
      [Confidence](http://IP_OF_YOUR_BIRDNETGO_UI:8080/)

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

      {%- 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') | map('replace', '%',
      '') | map('float') | max | round(0)) %}

      {%- if ubird_max_confidence > 70 %}

      {{ubird.time}}
      |  [{{ubird.name}}](https://de.wikipedia.org/wiki/{{ubird.name |
      replace(' ', '_')}}) | {{ubird_count}} | {{ ubird_max_confidence }} %

      {%- endif %}

      {%- endfor %}
    card_mod:
      style:
        $: |
          .card-header {
            display: flex !important;
            align-items: center;
          }
          .card-header:before {
            content: url("data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMDYuODcgMTE2LjY2Ij48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2Y0ZTUwNTt9LmNscy0ye2ZpbGw6I2UzMWUyNjt9LmNscy0ze2ZpbGw6I2ZmZjt9PC9zdHlsZT48L2RlZnM+PHBhdGggZD0iTTIwNi4zNywxNi42OHMtMTYuNDQtNC4zNC0yMi43Ni00LjljMCwwLTI1LDEzLjUtMzIsMThhMTkuMTYsMTkuMTYsMCwwLDAtOC42NywxMy44OWwzNS43MS0yNi4zMmgyOEMyMDcuMzEsMTcuMzksMjA2LjM3LDE2LjY4LDIwNi4zNywxNi42OFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgMC42MykiLz48cGF0aCBkPSJNMTQ4LjU1LDI3LjMzYzcuMzItNC45LDMyLjYyLTE4LjczLDMyLjYyLTE4LjczbDAsMEEzMC42OSwzMC42OSwwLDAsMCwxNTktLjYzYTQ0LjIzLDQ0LjIzLDAsMCwwLTIwLjcxLDVIMGMwLDMuNzEsNS42LDYuNTYsMTIuMTQsNi41Nkg1Mi4zNkw4Ni42MiwzNS4xMlY3MS4zN2MwLDE1LjczLDguMjYsMjkuNDQsMjEuNzgsMzcuMzVTMTI4LjY4LDExNiwxMzguNjMsMTE2VjQ2Ljg3QzEzOC42Myw0MC43OCwxNDAuNDcsMzIuNzMsMTQ4LjU1LDI3LjMzWk0xNjcuODcsOGEyLjUxLDIuNTEsMCwxLDEtMi41MSwyLjUxQTIuNTEsMi41MSwwLDAsMSwxNjcuODcsOFptLTI5LjEzLDEzLDE1LjY5LTguNjgsNi44OS41N0wxMzguNzQsMjUuMzZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDAuNjMpIi8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNTIuMzYsMTAuOTFIMTEwYy0xMi44OSwwLTIzLjQsMTAuMzUtMjMuNCwyNC4yMVoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgMC42MykiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xNzgsMTAuMzNBMzEuNzEsMzEuNzEsMCwwLDAsMTU3Ljc4LDIuOVYtLjYxbDEuMjUsMEEzMC42MywzMC42MywwLDAsMSwxODEuMTcsOC42WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAwLjYzKSIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTE3OC42MywxNy4zOWwtMjUsMTguNDNzLS4yOS0yLjcsMy40Ny01Ljc0LDI2LjUtMTguMywyNi41LTE4LjNaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDAuNjMpIi8+PHBhdGggY2xhc3M9ImNscy0zIiBkPSJNMTI4LjE0LDY0LjQ3VjUyLjE1YzAtNS4xOC0yLjExLTguNzctNi45My0xMi4xOEwxMDAuNzksMjUuNTRhMTQuMzIsMTQuMzIsMCwwLDAsMiwyMVoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgMC42MykiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0xMjguMTQsNjQuNDdWNTIuMTVjMC01LjE4LTIuMTEtOC43Ny02LjkzLTEyLjE4TDEwMC43OSwyNS41NGExNC4zMiwxNC4zMiwwLDAsMCwyLDIxWiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAwLjYzKSIvPjxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTE1MS41OSwyOS44MmM3LTQuNTQsMzItMTgsMzItMThhMTYuMjQsMTYuMjQsMCwwLDAtMi40MS0zLjE1bDAsMHMtMjUuMywxMy44My0zMi42MiwxOC43My05LjU3LDEyLjE3LTkuODcsMThsLS4wNSwxLjUxLDQuMjktMy4xNkExOS4xNiwxOS4xNiwwLDAsMSwxNTEuNTksMjkuODJaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDAuNjMpIi8+PHBhdGggY2xhc3M9ImNscy0zIiBkPSJNMTY3Ljg3LDhhMi41MSwyLjUxLDAsMSwxLTIuNTEsMi41MUEyLjUxLDIuNTEsMCwwLDEsMTY3Ljg3LDhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDAuNjMpIi8+PHBvbHlnb24gY2xhc3M9ImNscy0zIiBwb2ludHM9IjEzOC43NCAyMS41NyAxNTQuNDMgMTIuODkgMTYxLjMyIDEzLjQ1IDEzOC43NCAyNS45OCAxMzguNzQgMjEuNTciLz48L3N2Zz4=");
            height: 20px;
            width: 60px;
            margin-top: -10px;
            padding-left: 8px;
            padding-right: 18px;
          }

4 Likes

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