Displaying Birdnet-go detections

Thanks for all of the ideas!

I threw this together with some help from gemini to make the sparkline.

I’m using the /api/v2/analytics/species/daily endpoint on my BirdNET-Go instance and populating my card with that. I store the JSON in the sensor attribute and pull it out for markdown card.

I added a sparkline (I think it’s 7AM - 7PM, but I could be off by an hour). I clamp any values before or after those times so it isn’t lost.

This is taking in the hourly_counts values from the api endpoint.

First, make a command_line sensor (this requires that jq is installed on Home Assistant deployment and available to it. I have no idea if all installation types include that.):

command_line:
  - sensor:
      # Updated name to match your sensor
      name: "BirdNET Summary"
      unique_id: "birdnet_summary"
      command: "curl -s 'http://192.168.1.100:8080/api/v2/analytics/species/daily' | jq '{species_list: .}'"
      value_template: >
        {% if value_json is defined and value_json.species_list is iterable and value_json.species_list is not string %}
          {{ value_json.species_list | length }}
        {% else %}
          unavailable
        {% endif %}
      unit_of_measurement: "species"
      json_attributes:
        - species_list
      scan_interval: 60 # Or adjust as needed
      command_timeout: 25

I also prevent my recorder from saving the history for the sensor because why bother?

(optional)

recorder:
  exclude:
    entities:
      - sensor.birdnet_summary

Reboot. (also, you can decrease the scan interval and instead trigger a sensor update from the mqtt sensor if you want).

and this is my ugly markdown card.

type: markdown
title: Bird Summary (Daily)
content: >-
  {% set species_data = state_attr('sensor.birdnet_summary', 'species_list') %}

  {% if species_data and species_data | count > 0 %}

  Last Seen | Species | Count | Sparkline (7AM-7PM)

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

  {% for bird in species_data %}

  {#- Basic Info -#}

  {%- set time = bird.latest_seen -%}

  {%- set name = bird.common_name -%}

  {%- set count = bird.count -%}


  {#--- Sparkline Calculation (Full Day Aggregated - 15 slots) ---#}

  {%- set hourly = bird.hourly_counts -%}

  {%- set sparkline_str = "N/A" -%} {#- Default value -#}

  {%- if hourly is defined and hourly is iterable and hourly | count == 24 -%}
    {#- Aggregate counts: [0-8], 7..18, [21-23] (15 slots total, safe concatenation) -#}
    {%- set early_sum = hourly[0:6] | sum -%}
    {%- set middle_part = hourly[6:18] -%}
    {%- set late_sum = hourly[18:24] | sum -%}
    {%- set aggregated_counts = [early_sum] + middle_part + [late_sum] -%}

    {# Check if list has expected count before finding max #}
    {%- if aggregated_counts | count == 14 -%}
      {#- Find Max for Scaling -#}
      {%- set max_val = aggregated_counts | max -%}

      {#- Define Sparkline Characters (7 levels, U+2581 to U+2587) -#}
      {%- set spark_chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇'] -%} {#<-- 1. Removed full block '█' #}
      {%- set sparkline = namespace(text="") -%}

      {%- for v in aggregated_counts -%}
        {#- Map value to character index (0-6), scale factor 7, max index 6 -#}
        {%- set char_index = ([0, (v * 7 / max_val)|int , 6] | sort)[1] if max_val > 0 else 0 -%} {#<-- 2. Use 7 levels for scaling/clamping #}
        {#- Append character -#}
        {%- set sparkline.text = sparkline.text ~ spark_chars[char_index] -%}
      {%- endfor -%}
      {%- set sparkline_str = sparkline.text -%}
    {%- else -%}
       {% set sparkline_str = "?" * 12 %}
    {%- endif -%}
  {%- endif -%}

  {#--- End Sparkline Calculation ---#}


  {#- Output Row -#}

  {{ today_at(time) | as_timestamp  | timestamp_custom('%H:%M', true)}} | {{
  name }} | {{ count }} | {{ sparkline_str }}

  {% endfor %}


  ---

  {{ species_data | sum(attribute='count') | int }} Detections

  {{ species_data | count }} Species

  {% else %}

  No bird species data available or list is empty.

  {% endif %}

Wishlist:

  • I miss a lot is that I don’t get the ebird hyperlinks anymore on the species name because the API doesn’t expose the ebird id at that endpoint. I’m sure I could hack together a wikipedia search query or the sort. Let me know if someone has a working idea!
  • Minor tweaks for the UI. But it’s functional right now.
2 Likes