Bird detector sensors and dashboard

The Birdweather PUC is really interesting (and admittedly expensive) self-contained microphone array that records bird calls and updates a cloud database, adds the bird identification magic, and feeds an IOS app that updates almost in real time. Although the PUC is dependent on using their no-charge cloud back-end, the company provides pretty extensive APIs. Note, there is also a BirdNet-Pi project that provides the same functionality and is included in the Birdweather.com database so these sensors should work. Using either input, it seemed like a natural HA project to import the JSON data and update a dashboard.

Birdweather provides two extensive APIs: a GraphQL API and a REST API, both of which provide various JSON responses. I chose to use the REST API, 'cause it seemed easier.

Here’s the end result so far, updating the latest detection in the big box, and the day’s top 10 in order of number of detections. Lots more tinkering to do.

Step 1. Reach out to Tim at Birdweather.com for an API token. He got back to me via email after a day or two.

Step 2. Modify your HA configuration.yml file to include the new sensors.

The code is not very artful, but it works. I failed several times looking for a way to create the individual top 10 sensors programmatically (which would make it less tedious and more elegant to expand it to 50), but alas I ended up just repeating the code, altering the placeholder for each of the top 10. I think the solution is to create a script to create the individual sensors.

Here are the two rest sensors I used. The first retrieves the top 50 species detected in the past day. The value template creates a sensor.top_50_bird_species that uses the common name of the most detected species as the state, and includes a lot of JSON data packaged as the attribute species. I use that attribute later to create sensors to track each of the top 10 most detected species.

The second rest sensor retrieves the most recent detection. The scan interval default is 30 seconds and I’m going with that.

sensor:
  - platform: rest
    name: Top 50 Bird Species
    resource: https://app.birdweather.com/api/v1/stations/{token}/species/?period=day&limit=50
    
    value_template: "{{ value_json.species[0].commonName }}"
    json_attributes:
      - species
      
  - platform: rest
    name: Latest Bird Detection
    resource: https://app.birdweather.com/api/v1/stations/{token}/detections/?limit=1
    value_template: >
      {% if value_json.detections and value_json.detections | length > 0 %}
        {{ value_json.detections[0].species.commonName }}
      {% else %}
        No detections
      {% endif %}
    json_attributes:
      - detections

The real challenge for me was figuring out how to extract the right bits of data and get it to show up on the dashboard. The path I took was to define a separate sensor for each of the top 10 detected species. For each sensor, the underlying attributes provide the data I want to display. Here’s what it looks like, but instead of including all 10, I just included the first two. I look forward to seeing how this can be made much more elegant with a for loop.

template:
  - sensor:
      - name: "Top Bird Species 0"
        state: >
          {{ state_attr('sensor.top_50_bird_species', 'species')[0]['commonName'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
        attributes:
          ImageURL: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[0]['imageUrl'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
          id: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[0]['id'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
          detections: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[0]['detections'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
          last_detection_time: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[0]['latestDetectionAt'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}

      - name: "Top Bird Species 1"
        state: >
          {{ state_attr('sensor.top_50_bird_species', 'species')[1]['commonName'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
        attributes:
          ImageURL: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[1]['imageUrl'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
          id: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[1]['id'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
          detections: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[1]['detections'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
          last_detection_time: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[1]['latestDetectionAt'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}

tip You have to restart the first time you add the configuration code, but after that, when debugging your yaml, you can quick reload (Settings>System>Upper Right Corner>Quick Reload) instead of Restarting everything.

Step 3. Add the dashboard

Again, I’m absolutely sure there’s a better way to do this with less repetitive code, but I was surprised how difficult it was to work with sensor attribute data. After a bunch of false starts, I landed on a simple markdown card. I used a fresh dashboard page sized for an old iPad Pro setup as the bird dash. Here’s the code (again, you’ll get the idea after the first two markdown cards):


  - path: bird-dash
    type: sections
    sections:
      - type: grid
        cards:
          - type: markdown
            content: |+
              ### Latest Detection


              ![image]({{ state_attr('sensor.latest_bird_detection',
                    'detections')[0]['species']['imageUrl'] }})


              ## {{ states('sensor.latest_bird_detection') }}



                    {% set raw_timestamp = state_attr('sensor.latest_bird_detection',
                    'detections')[0]['timestamp'] %}
                              {% set timestamp = strptime(raw_timestamp, '%Y-%m-%dT%H:%M:%S.%f%z') %}
              ##### {{ timestamp.strftime('%B %d, %I:%M %p') }}

        title: Today’s Top 10 Detected Birds
      - type: grid
        cards:
          - square: false
            type: grid
            cards:
              - type: markdown
                content: >

                  ### #1 / 10


                  ![image]({{
                  states.sensor.top_bird_species_0.attributes.ImageURL }})


                  #### {{ states('sensor.top_bird_species_0') }}


                  ##### Detections:  {{
                  states.sensor.top_bird_species_0.attributes.detections.total
                  }}


                  {% set date_str = state_attr('sensor.top_bird_species_0',
                  'last_detection_time') %}
                    {% if date_str %}
                      {% set date_obj = as_datetime(date_str) %}
                      {% set local_date = as_local(date_obj) %}
                  ##### {{ local_date.strftime('%b %d, %I:%M:%S %p') }}
                    {% else %}
                      No detections
                    {% endif %}
              - type: markdown
                content: >

                  ### #2 / 10


                  ![image]({{
                  states.sensor.top_bird_species_1.attributes.ImageURL }})


                  #### {{ states('sensor.top_bird_species_1') }}


                  ##### Detections:  {{
                  states.sensor.top_bird_species_1.attributes.detections.total
                  }}


                  {% set date_str = state_attr('sensor.top_bird_species_1',
                  'last_detection_time') %}
                    {% if date_str %}
                      {% set date_obj = as_datetime(date_str) %}
                      {% set local_date = as_local(date_obj) %}
                  ##### {{ local_date.strftime('%b %d, %I:%M:%S %p') }}
                    {% else %}
                      No detections
                    {% endif %}
            columns: 2

cards: []
    max_columns: 4
    icon: mdi:bird

I’ve only been working with it off and on for a couple of weeks, so no doubt it’ll evolve. Hope you find it interesting or useful.

Cheers.

6 Likes

This is great, just what I’ve been looking out for. My wife has a PUC and it would be much better to show the data in HA rather than a separate dashboard. I’ll reach out to Tim for the API code, try it out and let you know how I get on. Thanks!

That’s great. Glad to start the conversation. There’s a lot of improvement to be made, but lots to learn along the way. Let me know how it goes for you.

I have it working using your code as a basis, many thanks, and my wife likes it too. I’ll work on the code as well, and post back when I have something new to offer, but it may be slow progress as I have limited time at the moment unfortunately.

The YAML editor is giving me an error about identation at 4;9 (line 4, position 9), using your Dashboard code – and I am afraid that I don’t use Markdown, so I don’t know where to fix it.[quote=“drharris9121, post:1, topic:742615”]

- path: bird-dash
    type: sections
    sections:
      - type: grid
        ^

Grateful for any help!

I was just reading the docs (Markdown card - Home Assistant), which no doubt will make perfect sense as soon as I already know Markdown. :roll_eyes:

I think the problem may be in the sections part of the code above the grid card. I realized I started with a blank dashboard page using the new experimental Sections view type. If you start with a blank page (once in edit mode on the page, click the + in the header to start a new page. I suspect you may be trying to plug that code blob into a custom card. You can do that, but take out the first three lines (through sections:) and then adjust the indents as appropriate. hope that helps.

1 Like

I had a similar syntax error with the dashboard code when I tried this but managed to get over it with this, slightly adapted, code. It’s a very rudimentary dashboard but I haven’t had the time to format and extend it recently. So, from ‘Show Code Editor’ rather than the UI

square: false
type: grid
cards:
  - type: markdown
    content: |
      ### Latest Detection

      ![image]({{ state_attr('sensor.latest_bird_detection',
            'detections')[0]['species']['imageUrl'] }})


      ## {{ states('sensor.latest_bird_detection') }}



            {% set raw_timestamp = state_attr('sensor.latest_bird_detection',
            'detections')[0]['timestamp'] %}
                      {% set timestamp = strptime(raw_timestamp, '%Y-%m-%dT%H:%M:%S.%f%z') %}
      ##### {{ timestamp.strftime('%B %d, %I:%M %p') }}
    title: 'Latest Detection '
  - type: markdown
    content: >
      ### #1 / 10


      ![image]({{ states.sensor.top_bird_species_0.attributes.ImageURL }})


      #### {{ states('sensor.top_bird_species_0') }}


      ##### Detections:  {{
      states.sensor.top_bird_species_0.attributes.detections.total }}


      {% set date_str = state_attr('sensor.top_bird_species_0',
      'last_detection_time') %}
        {% if date_str %}
          {% set date_obj = as_datetime(date_str) %}
          {% set local_date = as_local(date_obj) %}
      ##### {{ local_date.strftime('%b %d, %I:%M:%S %p') }}
        {% else %}
          No detections
        {% endif %}
    title: Today's Top 10 Detected Birds
title: bird-dash
columns: 4
1 Like

Just request the API key and waiting for approval to test it, thank you!

Thanks a lot for your work!
I ordered a puc two days ago and can’t wait to add it to my HA

(standalone birdnetpi from hassio-addons/birdnet-pi at master · alexbelgium/hassio-addons · GitHub monitoring my garden already works fine since months :slight_smile: )

@tryingtoohard and @drharris9121 this is great – thank you! It gave me a good start, and I have been able to figure out how to slightly customize it. And now I am learning a little about Markdown, too.

Much appreciated!!

Hey, that’s great. Please continue the thread by posting revisions and tips!

1 Like

I’ve been (very) slowly playing with the configuration.yaml and the dashboard code. My latest ambition has been to list the 10 recent detections as well as the top 10 list.

So this is my latest:
configuration.yaml - insert your own API url at ‘resource:’ (twice) instead of xyz123 and I’ve only included 2 of each rather than 10 of each block. Add those back in ad nauseam to get to the 10 of each, or other length, as you wish.

# https://community.home-assistant.io/t/bird-detector-sensors-and-dashboard/742615
sensor:
  - platform: rest
    name: Top 50 Bird Species
    resource: https://app.birdweather.com/api/v1/stations/xyz123/species/?period=day&limit=10
    value_template: "{{ value_json.species[0].commonName }}"
    json_attributes:
     species
  - platform: rest
    name: Latest Bird Detections
    resource: https://app.birdweather.com/api/v1/stations/xyz123/detections/?limit=10
    value_template: "{{ value_json.detections[0].commonName }}"
    json_attributes:
      - detections
      
template:
  - sensor:
      - name: "Top Bird Species 0"
        state: >
          {{ state_attr('sensor.top_50_bird_species', 'species')[0]['commonName'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
        attributes:
          ImageURL: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[0]['imageUrl'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
          id: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[0]['id'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
          detections: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[0]['detections'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
          last_detection_time: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[0]['latestDetectionAt'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}

      - name: "Top Bird Species 1"
        state: >
          {{ state_attr('sensor.top_50_bird_species', 'species')[1]['commonName'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
        attributes:
          ImageURL: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[1]['imageUrl'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
          id: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[1]['id'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
          detections: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[1]['detections'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}
          last_detection_time: >
            {{ state_attr('sensor.top_50_bird_species', 'species')[1]['latestDetectionAt'] if state_attr('sensor.top_50_bird_species', 'species') is defined and state_attr('sensor.top_50_bird_species', 'species')|length > 0 else 'Unknown' }}

  - sensor:
      - name: "Latest Bird Detections 0"
        state: >
          {{ state_attr('sensor.latest_bird_detections', 'detections')[0]['species']['commonName'] if state_attr('sensor.latest_bird_detections', 'detections') is defined and state_attr('sensor.latest_bird_detections', 'detections')|length > 0 else 'Unknown' }}
        attributes:
          ImageURL: >
            {{ state_attr('sensor.latest_bird_detections', 'detections')[0]['species']['imageUrl'] if state_attr('sensor.latest_bird_detections', 'detections') is defined and state_attr('sensor.latest_bird_detections', 'detections')|length > 0 else 'Unknown' }}
          timestamp: >
            {{ state_attr('sensor.latest_bird_detections', 'detections')[0]['timestamp'] if state_attr('sensor.latest_bird_detections', 'detections') is defined and state_attr('sensor.latest_bird_detections', 'detections')|length > 0 else 'Unknown' }}
          certainty: >
            {{ state_attr('sensor.latest_bird_detections', 'detections')[0]['certainty'] if state_attr('sensor.latest_bird_detections', 'detections') is defined and state_attr('sensor.latest_bird_detections', 'detections')|length > 0 else 'Unknown' }}

      - name: "Latest Bird Detections 1"
        state: >
          {{ state_attr('sensor.latest_bird_detections', 'detections')[1]['species']['commonName'] if state_attr('sensor.latest_bird_detections', 'detections') is defined and state_attr('sensor.latest_bird_detections', 'detections')|length > 0 else 'Unknown' }}
        attributes:
          ImageURL: >
            {{ state_attr('sensor.latest_bird_detections', 'detections')[1]['species']['imageUrl'] if state_attr('sensor.latest_bird_detections', 'detections') is defined and state_attr('sensor.latest_bird_detections', 'detections')|length > 0 else 'Unknown' }}
          timestamp: >
            {{ state_attr('sensor.latest_bird_detections', 'detections')[1]['timestamp'] if state_attr('sensor.latest_bird_detections', 'detections') is defined and state_attr('sensor.latest_bird_detections', 'detections')|length > 0 else 'Unknown' }}
          certainty: >
            {{ state_attr('sensor.latest_bird_detections', 'detections')[1]['certainty'] if state_attr('sensor.latest_bird_detections', 'detections') is defined and state_attr('sensor.latest_bird_detections', 'detections')|length > 0 else 'Unknown' }}
            

And in the dashboard (again, cut down to two of each to avoid too much repetition)

square: false
type: grid
cards:
  - type: markdown
    content: >+
      ### Latest Detection #1


      ![image]({{ states.sensor.latest_bird_detections_0.attributes.ImageURL }})


      #### {{ states('sensor.latest_bird_detections_0') }}


      ##### Certainty:  {{
      states.sensor.latest_bird_detections_0.attributes.certainty }}
            
            {% set raw_timestamp = state_attr('sensor.latest_bird_detections',
            'detections')[0]['timestamp'] %}
                      {% set timestamp = strptime(raw_timestamp, '%Y-%m-%dT%H:%M:%S.%f%z') %}
      ##### {{ timestamp.strftime('%B %d, %I:%M %p') }}


      ### Latest Detection #2

        ![image]({{ states.sensor.latest_bird_detections_1.attributes.ImageURL }})

        #### {{ states('sensor.latest_bird_detections_1') }}

      ##### Certainty:  {{
      states.sensor.latest_bird_detections_1.attributes.certainty }}
            
            {% set raw_timestamp = state_attr('sensor.latest_bird_detections',
            'detections')[1]['timestamp'] %}
                      {% set timestamp = strptime(raw_timestamp, '%Y-%m-%dT%H:%M:%S.%f%z') %}
      ##### {{ timestamp.strftime('%B %d, %I:%M %p') }}


    title: "Latest Detection "
  - type: markdown
    content: >-
      ### #1 / 10


      ![image]({{ states.sensor.top_bird_species_0.attributes.ImageURL }})


      #### {{ states('sensor.top_bird_species_0') }}


      ##### Detections:  {{
      states.sensor.top_bird_species_0.attributes.detections.total }}


      {% set date_str = state_attr('sensor.top_bird_species_0',
      'last_detection_time') %}
        {% if date_str %}
          {% set date_obj = as_datetime(date_str) %}
          {% set local_date = as_local(date_obj) %}
      ##### {{ local_date.strftime('%b %d, %I:%M:%S %p') }}
        {% else %}
          No detections
        {% endif %}

        
      ### #2 / 10


      ![image]({{ states.sensor.top_bird_species_1.attributes.ImageURL }})


      #### {{ states('sensor.top_bird_species_1') }}


      ##### Detections:  {{
      states.sensor.top_bird_species_1.attributes.detections.total }}


      {% set date_str = state_attr('sensor.top_bird_species_1',
      'last_detection_time') %}
        {% if date_str %}
          {% set date_obj = as_datetime(date_str) %}
          {% set local_date = as_local(date_obj) %}
      ##### {{ local_date.strftime('%b %d, %I:%M:%S %p') }}
        {% else %}
          No detections
        {% endif %}

       
    title: Today's Top 10 Detected Birds
title: Bird-Dash
columns: 2

Hope that helps