fixed
https://en.wikipedia.org/wiki/{{bird_name | replace(' ', '_')}}
fixed
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;
}
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 )
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.
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 %}
{%- 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 %}
{%- 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?
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
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
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
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.
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') }}"
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:
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.
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.