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.