Birdweather PUC - Birds, Birding, etc

Wanted to share my dashboards for my BirdWeather PUC, that I call Hermes. This is an amazing device that allows you to "easily connect your PUC to your home WiFi, and it will continuously monitor the sounds in your backyard, identifying bird songs in real-time." Even better, the device has an open API that allows you to pull the data in and visualize in a dashboard.

BirdWeather site: https://www.birdweather.com/

BirdWeather API docs: BirdWeather API

I have created two dashboards so far for a Fire HD10 tablet. One is a daily view of bird activity and the second is an Almanac view showing weekly data trends of bird species.


I will include the sensor config and pastebin links for both dashboards to help any new users expedite their birding journey. I am not a professional developer, so any errors or mistakes, my bad.

In the sensor.config, I have noted the specific paramaters you will need to update with your station specific info like API, Station ID, Lat, Long, Timezone, etc. You can obtain the API Token and Station ID from the Advanced Settings in the BirdWeather App.

Please feel free to ask any questions. I can't promise I can answer them, but I hope this helps anyone enjoy the amazing world of birds right outside your door!

Prerequisite cards for my dashboards to work: custom:mushroom-title-card, custom:mushroom-template-card, custom:apexcharts-card, custom:button-card, and card-mod

Daily - title: ðŸŠķ Aviaryviews: - title: Aviary path: birdweather icon: mdi - Pastebin.com
Almanac - title: 📖 Almanacviews: - title: Almanac path: birdweather-almanac - Pastebin.com

# ─────────────────────────────────────────────────────────────
# BIRDWEATHER SENSOR CONFIG
# ─────────────────────────────────────────────────────────────
# Before using, replace the following placeholders:
#   YOUR_API_KEY     — BirdWeather API token (Advanced Settings in the BirdWeather app)
#   YOUR_STATION_ID  — BirdWeather Station ID (Advanced Settings in the BirdWeather app)
#   YOUR_UTC_OFFSET  — Your UTC offset, e.g. -05:00 for ET standard, -04:00 for ET daylight
#   YOUR_NE_LAT      — Northeast latitude of your regional bounding box (~50mi radius)
#   YOUR_NE_LON      — Northeast longitude of your regional bounding box
#   YOUR_SW_LAT      — Southwest latitude of your regional bounding box
#   YOUR_SW_LON      — Southwest longitude of your regional bounding box
#
# To find your bounding box coordinates, use: https://boundingbox.klokantech.com/
# ─────────────────────────────────────────────────────────────

  # Latest 100 detections — drives Most Recent + Recent Detections cards
  - platform: rest
    name: Latest Bird Detections
    resource: https://app.birdweather.com/api/v1/stations/YOUR_STATION_ID/detections/?limit=100&order=desc&token=YOUR_API_KEY
    value_template: "{{ value_json.detections[0].species.commonName }}"
    json_attributes:
      - detections
    scan_interval: 60

  # Top species by count for the current day — drives Top Species Today cards
  - platform: rest
    name: Top 100 Bird Species
    resource: https://app.birdweather.com/graphql
    method: POST
    headers:
      Content-Type: application/json
      Authorization: Bearer YOUR_API_KEY
    payload_template: >-
      {"query": "{ station(id: \"YOUR_STATION_ID\") { topSpecies(limit: 100, period: { count: {{ [now().hour, 1] | max }}, unit: \"hour\" }) { count species { commonName imageUrl } } } }"}
    value_template: "{{ value_json.data.station.topSpecies[0].species.commonName }}"
    json_attributes_path: "$.data.station"
    json_attributes:
      - topSpecies
    scan_interval: 300

  # Daily species count — number of distinct species detected today
  - platform: rest
    resource_template: "https://app.birdweather.com/api/v1/stations/YOUR_STATION_ID/species?period=day&from={{ now().replace(hour=0,minute=0,second=0,microsecond=0).strftime('%Y-%m-%d') }}T00:00:00YOUR_UTC_OFFSET&token=YOUR_API_KEY"
    scan_interval: 300
    name: "Bird Daily Species"
    unique_id: bird_daily_species
    value_template: >
      {% set today = now().strftime('%Y-%m-%d') %}
      {{ value_json.species | selectattr('latestDetectionAt', 'search', today) | list | length }}

  # Daily detection count — total detections across all species today
  - platform: rest
    resource_template: "https://app.birdweather.com/api/v1/stations/YOUR_STATION_ID/species?period=day&from={{ now().replace(hour=0,minute=0,second=0,microsecond=0).strftime('%Y-%m-%d') }}T00:00:00YOUR_UTC_OFFSET&token=YOUR_API_KEY"
    scan_interval: 300
    name: "Bird Daily Detections"
    unique_id: bird_daily_detections
    value_template: "{{ value_json.species | map(attribute='detections') | map(attribute='total') | sum }}"

  # Weekly species count — distinct species since the most recent Sunday
  # scan_interval aligned to 300 (matches daily) to prevent Sunday rollover
  # mismatch caused by stale hourly data sitting next to fresh 5-min daily data.
  - platform: rest
    resource_template: >-
      https://app.birdweather.com/api/v1/stations/YOUR_STATION_ID/species?period=day&limit=100&from={{
      (now().date() - timedelta(days=(now().weekday() + 1) % 7)).strftime('%Y-%m-%d')
      }}T00:00:00YOUR_UTC_OFFSET&token=YOUR_API_KEY
    scan_interval: 300
    name: "Bird Weekly Species"
    unique_id: bird_weekly_species
    value_template: "{{ value_json.species | length }}"

  # Weekly detection count — total detections since the most recent Sunday
  # scan_interval aligned to 300 for the same reason as above.
  - platform: rest
    resource_template: >-
      https://app.birdweather.com/api/v1/stations/YOUR_STATION_ID/species?period=day&limit=100&from={{
      (now().date() - timedelta(days=(now().weekday() + 1) % 7)).strftime('%Y-%m-%d')
      }}T00:00:00YOUR_UTC_OFFSET&token=YOUR_API_KEY
    scan_interval: 300
    name: "Bird Weekly Detections"
    unique_id: bird_weekly_detections
    value_template: "{{ value_json.species | map(attribute='detections') | map(attribute='total') | sum }}"

  - platform: rest
    name: "Bird Hourly Activity"
    unique_id: bird_hourly_activity
    resource: https://app.birdweather.com/graphql
    method: POST
    headers:
      Content-Type: application/json
      Authorization: Bearer YOUR_API_KEY
    payload_template: >-
      {"query": "{ timeOfDayDetectionCounts(stationIds: [YOUR_STATION_ID], period: {count: {{ [now().hour, 1] | max }}, unit: \"hour\"}) { bins { key count } count species { commonName } } }"}
    value_template: "{{ value_json.data.timeOfDayDetectionCounts | length }}"
    json_attributes_path: "$.data"
    json_attributes:
      - timeOfDayDetectionCounts
    scan_interval: 300

  - platform: rest
    name: bird_earliest_detection_today
    resource: "https://app.birdweather.com/graphql"
    method: POST
    headers:
      Authorization: Bearer YOUR_API_KEY
      Content-Type: "application/json"
    payload: "{\"query\": \"query { detections(last: 1, stationIds: [YOUR_STATION_ID], period: {count: 1, unit: \\\"day\\\"}) { nodes { timestamp } } }\"}"
    value_template: >
      {{ value_json.data.detections.nodes[0].timestamp
      if value_json.data.detections.nodes | length > 0
      else '' }}
    scan_interval: 300

  # Regional top species — bounding box defines your ~50mi search radius
  # Generate your coordinates at: https://boundingbox.klokantech.com/
  - platform: rest
    name: bird_regional_top_species
    resource: "https://app.birdweather.com/graphql"
    method: POST
    headers:
      Authorization: Bearer YOUR_API_KEY
      Content-Type: "application/json"
    payload: >
      {"query": "query { topSpecies(limit: 5, ne: {lat: YOUR_NE_LAT, lon: YOUR_NE_LON}, sw: {lat: YOUR_SW_LAT, lon: YOUR_SW_LON}) { count species { commonName scientificName imageUrl } } }"}
    value_template: "{{ value_json.data.topSpecies | length }}"
    json_attributes_path: "$.data"
    json_attributes:
      - topSpecies
    scan_interval: 3600

  - platform: rest
    name: bird_weekly_stats
    resource: https://app.birdweather.com/graphql
    method: POST
    headers:
      Content-Type: application/json
      Authorization: Bearer YOUR_API_KEY
    payload: >
      {"query": "{ dailyDetectionCounts(period: {count: 7, unit: \"day\"}, stationIds: [YOUR_STATION_ID]) { date total counts { count species { commonName } } } }"}
    value_template: "{{ value_json.data.dailyDetectionCounts | length }}"
    json_attributes_path: "$.data"
    json_attributes:
      - dailyDetectionCounts
    scan_interval: 3600

  - platform: rest
    name: bird_monthly_stats
    resource: https://app.birdweather.com/graphql
    method: POST
    headers:
      Content-Type: application/json
      Authorization: Bearer YOUR_API_KEY
    payload: >
      {"query": "{ dailyDetectionCounts(period: {count: 30, unit: \"day\"}, stationIds: [YOUR_STATION_ID]) { date total counts { count species { commonName } } } }"}
    value_template: "{{ value_json.data.dailyDetectionCounts | length }}"
    json_attributes_path: "$.data"
    json_attributes:
      - dailyDetectionCounts
    scan_interval: 3600
1 Like

FYI there is a much cheaper option: Displaying Birdnet-go detections