Baseline Blog

Intro to Home Automation for the Parents

I live in Colorado, my parents in Florida, but I usually head there for the holidays and this year I planned on introducing them to some basic home automation. As anyone reading this blog likely knows, there are thousands of different products out there, all with different levels of support and security. I've learned some of those lessons on my own home automation setup and hoped to apply the learnings to my parents' house.

Screenshot 2026-01-03 at 1

Goals

Approach

Knowing I was using Home Assistant and that my parents' home was a relatively small (for US homes) single-floor 2000 sq foot home with limited WiFi coverage, I leaned into using Zigbee for as much as I could. I wanted to avoid as much hardwiring as possible in case this wasn't for them. With some consulting I landed on the following BOM:

As you can see I went heavily on Aqara. A few reasons made sense to me: if Home Assistant starts to not work for them or goes down, the Aqara ecosystem will still work. The Aqara camera has a Zigbee hub and could connect those devices. They would lose BirdNet and some other stuff but the hardware wouldn't be useless. It's also dead simple to set up.

Initial Setup

While I was still in Colorado I started with flashing the SD card, setting up myself as an admin, and installing a few add-ons that would make remote access easy: Cloudflared and Tailscale.

I chose Cloudflared because it will lock down all ports other than the main Home Assistant and it will make external access easy with a dedicated subdomain. I also used Tailscale because I use it myself and my devices are always on the network, so I would quickly be able to debug issues remotely for my parents by setting up the Pi as a way to access their full network.

I then installed HACS so I would have access to a bunch more plugins and front-end themes.

From there I added the MQTT add-on, set up creds, installed the Zigbee2MQTT add-on, and added the creds I set up.

Lastly I installed but didn't configure BirdNet-Go.

I also set up user accounts for them, saved the passwords in our password manager, and added some basic theming. From there I was ready to go to Florida.

Device Setup

Getting to Florida I unpacked all the devices I shipped there and started getting everything set up. I started with getting the Pi wired into their router. Turns out they didn't have an ethernet cable, so we set up the cameras instead. That was pretty straightforward and Aqara walks you through it.

When I got an ethernet cable we were back up and running. I started by installing the Zigbee radio and setting up Zigbee2MQTT, ensuring everything was up and running.

Leak Sensors

From there I started with the leak sensors. It was as simple as pulling a pull tab separating the battery, holding down a button, tapping "allow join" in the Zigbee2MQTT UI, and voilà—the device was found. I named it and it was adopted into the network.

Screenshot 2026-01-03 at 1

We found where to place them around the house and did a few tests to make sure the state would change. Voilà.

Temp Sensors

These followed the same exact method except they came with double-sided tape, so we had a good debate about where they belonged. We landed on one under the front patio, one under the back patio, and one in the kitchen away from the stove.

Screenshot 2026-01-03 at 1

Smart Switches

Since these are by Third Reality they automatically went into pairing mode when plugged in, so these were even simpler. They also act as hubs which should repeat any Zigbee requests to the hub. They also track usage which is nice if these were powering high-power devices.

Screenshot 2026-01-03 at 1

I set up some automations here; simple ones that look for if no one is home and then are arriving home to turn on the hallway and entranceway lights if they are not already on.

Thermostat

Nothing too fancy here, just the default Home Assistant addition. I don't like messing with automations when it comes to thermostats as it can be a costly mistake.

Cameras

Not too much to say on these. They are great quality and allow for an RTSP stream of audio I can send to BirdNet. I didn't do anything outside of the ordinary other than setting up WebRTC camera to lighten the load on the Raspberry Pi when streaming the feed to the UI. The card setup is pretty straightforward:

type: custom:webrtc-camera
url: aqara_g5_sub
mode: webrtc
ui: true
streams:
  - url: aqara_g5_sub
    name: SD
  - url: aqara_g5_main
    name: HD

With the WebRTC device set up like this:

streams:
  aqara_g5_main:
    - rtsp://user:pass@192.168.1.44:8554/ch1

  aqara_g5_sub:
    - rtsp://user:pass@192.168.1.44:8554/ch4

  aqara_g5_audio:
    - "ffmpeg:rtsp://user:pass@192.168.1.44:8554/ch4#audio=pcm"
  
  aqara_100_main:
    - rtsp://user:pass@192.168.1.45:8554/ch1

  aqara_100_sub:
    - rtsp://user:pass@192.168.1.45:8554/ch3

As you can see I have an audio-only stream for use with BirdNet.

BirdNet

This was the main reason for this whole project. Getting BirdNet-Go installed was pretty easy but getting it to play well in HA was a bit more annoying. I approached it with a few different goals in mind:

  1. Since this is running on an SD card I didn't want to record audio. The sample ones on All About Birds sound better anyway, so let's just link to those pages.
  2. I want to make sure the cameras are facing where the birds are so the stream can pick it up.
  3. I want HA to just show basic information and not replace the BirdNet UI entirely.

So what I did is take a blend of the BirdNet docs and some of my own code and end up having these nice two simple UIs in Home Assistant:

Last Bird Birds Today

The Last Bird card is the following:

type: vertical-stack
cards:
  - type: custom:mushroom-template-card
    entity: sensor.birdnet_go
    primary: "{{ state_attr('sensor.birdnet_go', 'CommonName') }}"
    secondary: >-
      {{ state_attr('sensor.birdnet_go', 'ScientificName') }} • {{
      (state_attr('sensor.birdnet_go', 'Confidence') * 100) | round(0) }}% • {{
      state_attr('sensor.birdnet_go', 'Time') }}
    picture: "{{ state_attr('sensor.birdnet_go', 'BirdImage')['URL'] }}"
    features_position: bottom
    multiline_secondary: true

And for the Birds Since Midnight:

type: markdown
title: Birds Since Midnight
content: >-
  Time|  Bird Name|Number Today|    Max
  [Confidence](http://192.168.1.3: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://www.allaboutbirds.org/guide/{{ubird.name
  | replace%28' ', '_'%29}}) | {{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,REDACTED");
            height: 20px;
            width: 60px;
            margin-top: -10px;
            padding-left: 8px;
            padding-right: 18px;
      }

This can certainly be improved, and I would love any comments on how!

Getting this data to be available to the UI means I need to add this to my config.yaml:

mqtt:
  sensor:
    - name: "Birdnet-Go"
      state_topic: "birdnet"
      value_template: "{{ today_at(value_json.Time) }}"
      json_attributes_topic: "birdnet"
      json_attributes_template: "{{ value_json | tojson }}"
    - name: "Birdnet-Go Bird Image Url"
      state_topic: "birdnet"
      value_template: "{{ value_json.BirdImage.URL }}"
    - name: "Birdnet-Go Clip Name"
      state_topic: "birdnet"
      value_template: "{{ value_json.ClipName }}"
    - name: "Birdnet-Go Common Name"
      state_topic: "birdnet"
      value_template: "{{ value_json.CommonName }}"
    - name: "Birdnet-Go Confidence"
      state_topic: "birdnet"
      value_template: "{{ (value_json.Confidence | float * 100) | round(2) }}"
      unit_of_measurement: "%"
    - name: "Birdnet-Go Date"
      state_topic: "birdnet"
      value_template: "{{ value_json.Date }}"
    - name: "Birdnet-Go ProcessingTime"
      state_topic: "birdnet"
      value_template: "{{ (value_json.ProcessingTime | float / 1000000000 ) | round(4) }}"
      unit_of_measurement: "s"
    - name: "Birdnet-Go Scientific Name"
      state_topic: "birdnet"
      value_template: "{{ value_json.ScientificName }}"
    - name: "Birdnet-Go Sensitivity"
      state_topic: "birdnet"
      value_template: "{{ value_json.Sensitivity }}"
    - name: "Birdnet-Go Source"
      state_topic: "birdnet"
      value_template: "{{ value_json.Source }}"
    - name: "Birdnet-Go Species Code"
      state_topic: "birdnet"
      value_template: "{{ value_json.SpeciesCode }}"
    - name: "Birdnet-Go Threshold"
      state_topic: "birdnet"
      value_template: "{{ value_json.Threshold }}"
    - name: "Birdnet-Go Time"
      state_topic: "birdnet"
      value_template: "{{ today_at(value_json.Time) }}"

template:
  - trigger:
      - platform: mqtt
        topic: "birdnet"
        id: birdnet
      - platform: time
        at: "00:00:00"
        id: reset
    sensor:
      - unique_id: c893533c-3c06-4ebe-a5bb-da833da0a947
        name: BirdNET-Go Events
        state: >
          {% if trigger.id == 'reset' %}
            {{ now() }}
          {% elif trigger.id == 'birdnet' %}
            {{ today_at(trigger.payload_json.Time) }}
          {% endif %}
        attributes:
          bird_events: >
            {% if trigger.id == 'reset' %}
              {{ [] }}
            {% else %}
              {% set time = trigger.payload_json.Time | trim %}
              {% set name = trigger.payload_json.CommonName | trim %}
              {% 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] }}
            {% endif %}

This works quite well, and if you want to hear the bird call you click on its name and it goes over to All About Birds. If you want to see local information it's just a tap into the BirdNet-Go UI:

Bird Analytics Daily Detections Species Info

Automations

Knowing I was going to be remote and I didn't want to do too much to maintain this after I was back in Colorado, I added some quality-of-life automations to keep everything running well.

What I think may be of most significant interest is the email sent when batteries of any device are low. This uses a few different methods to determine a battery is low and sends an email using the Mailgun add-on to me and my mother with the device that is low.

That automation is as follows:

alias: Weekly Battery Report
description: Sends a weekly email with all battery statuses
triggers:
  - trigger: time
    at: "09:00:00"
conditions:
  - condition: time
    weekday:
      - mon
  - condition: template
    value_template: |
      {{ states.sensor 
         | selectattr('attributes.device_class', 'eq', 'battery')
         | map(attribute='state') | map('int', 0)
         | select('le', 20) | list | count > 0 }}
actions:
  - action: notify.mailgun_daily
    data:
      title: Weekly Battery Report
      message: ""
      data:
        html: >
          <h2>Battery Status Report</h2> <table style="border-collapse:
          collapse; width: 100%;">
            <tr style="background-color: #f2f2f2;">
              <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Device</th>
              <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Level</th>
            </tr>
            {% for state in states.sensor 
               | selectattr('attributes.device_class', 'defined')
               | selectattr('attributes.device_class', 'eq', 'battery') 
               | sort(attribute='state') %}
            {% set level = state.state | int(0) %}
            {% set color = 'red' if level <= 20 else 'green' %}
            <tr>
              <td style="padding: 8px; border: 1px solid #ddd;">{{ state.attributes.friendly_name }}</td>
              <td style="padding: 8px; border: 1px solid #ddd; color: {{ color }}; font-weight: {% if level <= 20 %}bold{% else %}normal{% endif %};">{{ level }}%</td>
            </tr>
            {% endfor %}
          </table> <p style="color: #666; font-size: 12px;">Sent from Home
          Assistant</p>
mode: single

And the email is extremely simple but useful:

Sample Email

To set up Mailgun you can just follow the HA documentation.

Conclusion

Overall this was a fun project to do and helpful for my parents. They are eased into home automation, they get to discover their bird neighbors, and I got to learn some new concepts I can apply to my much more mature Home Assistant installation. It was nice over winter break to not have to focus on my day job and just explore this without deadlines or expectations beyond those I make for myself.

As always, if you have any questions hit me up on Blue Sky and I'll do my best to help out!

#birdnet #home assistant #yaml #zigbee