Mastering Router-Based Presence and Network Monitoring: Integrating OpenWrt with Home Assistant

Represent Mastering Router-Based Presence and Network Monitoring: Integrating OpenWrt with Home Assistant article
7m read

The Challenge: Robust Network Presence & Device Monitoring

Reliable presence detection is the cornerstone of truly intelligent home automation. While methods like Home Assistant Companion App, BLE beacons, or even dedicated ESPresence nodes offer varying degrees of accuracy, they often come with limitations: app battery drain, BLE range issues, or additional hardware dependencies. Beyond human presence, monitoring the online/offline status of critical network devices (servers, IP cameras, media players) is crucial for proactive alerting and automation stability.

This guide explores how to leverage your OpenWrt-powered router – a device already central to your network – to provide highly accurate, network-level presence detection for both people and devices. By integrating OpenWrt with Home Assistant, you gain a robust, always-on mechanism that's less prone to the flakiness of client-side solutions, and unlocks granular control over your smart home ecosystem.

Step-by-Step Setup: Integrating OpenWrt Data

1. Setting Up OpenWrt for Secure Data Export via SSH

First, we need to prepare your OpenWrt router to securely provide network data to Home Assistant. This involves enabling SSH and setting up a dedicated user with appropriate permissions.

  1. Enable SSH & Create a User: Ensure SSH is enabled on your OpenWrt router (System > Administration > SSH Access). For security, create a non-root user specifically for Home Assistant:
  2. ssh root@your_router_ip
    adduser hauser
    passwd hauser
    
  3. Install jq for JSON Parsing (Optional but Recommended): If you plan to parse complex JSON output directly on the router before sending it to Home Assistant, jq is invaluable.
  4. opkg update
    opkg install jq
    
  5. Configure SSH Keys for Passwordless Access: On your Home Assistant host, generate an SSH key pair (if you haven't already) and copy the public key to your OpenWrt router for the hauser user.
  6. # On Home Assistant OS/Container host
    ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_openwrt
    ssh-copy-id -i ~/.ssh/id_rsa_openwrt.pub hauser@your_router_ip
    

    Screenshot Placeholder: OpenWrt SSH Access Configuration Page

2. Integrating SSH Commands via Home Assistant's shell_command

With SSH set up, we can now use Home Assistant's shell_command integration to execute commands on your OpenWrt router and retrieve data.

Add the following to your configuration.yaml (or a dedicated package file):

# configuration.yaml
shell_command:
  openwrt_wifi_clients:
    command: "ssh -i /config/.ssh/id_rsa_openwrt -o StrictHostKeyChecking=no hauser@your_router_ip 'ubus call hostapd.wlan0 get_clients | jq -c . || true'"
  openwrt_arp_table:
    command: "ssh -i /config/.ssh/id_rsa_openwrt -o StrictHostKeyChecking=no hauser@your_router_ip 'cat /proc/net/arp || true'"

Explanation:

  • -i /config/.ssh/id_rsa_openwrt: Specifies the private key for authentication. Ensure /config/.ssh/ is accessible by Home Assistant.
  • -o StrictHostKeyChecking=no: Bypasses the host key check for the first connection. For production, add your router's host key to ~/.ssh/known_hosts on your HA host for better security.
  • ubus call hostapd.wlan0 get_clients | jq -c .: Retrieves connected WiFi clients from the wlan0 interface (adjust if you have multiple, e.g., wlan1 for 5GHz) and outputs it as a compact JSON string.
  • cat /proc/net/arp: Dumps the router's ARP table, showing MAC-to-IP mappings for all devices the router has communicated with recently (both wired and wireless).
  • || true: Ensures the command always exits successfully, preventing Home Assistant from marking the shell_command as failed if the SSH connection or command itself has a minor issue, which can be useful for polling.

3. Creating Template Sensors for Presence Detection

Now, we'll process the data retrieved by our shell_commands using Home Assistant's template sensors to create meaningful binary sensors for presence.

Add to your sensors.yaml (or equivalent):

# sensors.yaml
- platform: template
  sensors:
    person_phone_presence:
      friendly_name: "Person's Phone Presence"
      value_template: >
        {% set mac_address = "XX:XX:XX:XX:XX:XX" %}
        {% set wifi_clients = state_attr('sensor.openwrt_wifi_clients_data', 'json') %}
        {% if wifi_clients is not none %}
          {% set client_macs = wifi_clients | map(attribute='mac') | list %}
          {{ mac_address.lower() in client_macs | map('lower') | list }}
        {% else %}
          false
        {% endif %}
      device_class: connectivity
      unique_id: person_phone_presence_openwrt

    media_server_online:
      friendly_name: "Media Server Online"
      value_template: >
        {% set server_mac = "YY:YY:YY:YY:YY:YY" %}
        {% set arp_table = states('sensor.openwrt_arp_table_data') %}
        {% if arp_table is not none %}
          {{ server_mac.lower() in arp_table.lower() }}
        {% else %}
          false
        {% endif %}
      device_class: connectivity
      unique_id: media_server_online_openwrt

Note: We need an intermediate command_line sensor to capture the output of the shell_command.

# sensors.yaml (continued)
- platform: command_line
  name: OpenWrt WiFi Clients Data
  command: "!include_dir_list .../your_shell_commands/openwrt_wifi_clients.sh"
  json_attributes: "$"
  value_template: "OK"
  scan_interval: 30 # Poll every 30 seconds

- platform: command_line
  name: OpenWrt ARP Table Data
  command: "!include_dir_list .../your_shell_commands/openwrt_arp_table.sh"
  value_template: "OK"
  scan_interval: 60 # Poll every 60 seconds

Replace !include_dir_list .../your_shell_commands/openwrt_wifi_clients.sh with the actual shell_command call or a script that runs it. The shell_command in configuration.yaml cannot directly provide sensor data, it's for triggering actions. For data retrieval, use command_line sensor directly or an automation to call the shell_command and update an input_text helper which then feeds the template sensor.

Correction for data retrieval: shell_command does not return output. For data, we use the command_line sensor. It is more direct.

# sensors.yaml for direct data retrieval
- platform: command_line
  name: OpenWrt WiFi Clients
  command: "ssh -i /config/.ssh/id_rsa_openwrt -o StrictHostKeyChecking=no hauser@your_router_ip 'ubus call hostapd.wlan0 get_clients | jq -c . || true'"
  json_attributes: "$"
  value_template: "{{ now().isoformat() }}" # Just to update state, data is in attributes
  scan_interval: 30

- platform: command_line
  name: OpenWrt ARP Table
  command: "ssh -i /config/.ssh/id_rsa_openwrt -o StrictHostKeyChecking=no hauser@your_router_ip 'cat /proc/net/arp || true'"
  value_template: "{{ value }}" # Raw output as state
  scan_interval: 60

# Now, the template sensors for presence:
- platform: template
  sensors:
    person_phone_presence:
      friendly_name: "Person's Phone Presence"
      value_template: >
        {% set mac_address = "XX:XX:XX:XX:XX:XX" %}
        {% set wifi_clients_list = state_attr('sensor.openwrt_wifi_clients', 'value') %}
        {% if wifi_clients_list is not none %}
          {% set client_macs = wifi_clients_list | from_json | map(attribute='mac') | list %}
          {{ mac_address.lower() in client_macs | map('lower') | list }}
        {% else %}
          false
        {% endif %}
      device_class: connectivity
      unique_id: person_phone_presence_openwrt

    media_server_online:
      friendly_name: "Media Server Online"
      value_template: >
        {% set server_mac = "YY:YY:YY:YY:YY:YY" %}
        {% set arp_table = states('sensor.openwrt_arp_table') %}
        {% if arp_table is not none %}
          {{ server_mac.lower() in arp_table.lower() }}
        {% else %}
          false
        {% endif %}
      device_class: connectivity
      unique_id: media_server_online_openwrt

4. Monitoring Critical Router Metrics (Advanced)

Beyond presence, you can monitor router health. For example, check free memory:

# sensors.yaml
- platform: command_line
  name: OpenWrt Free Memory
  command: "ssh -i /config/.ssh/id_rsa_openwrt -o StrictHostKeyChecking=no hauser@your_router_ip 'free | grep Mem: | awk '{print $4}' || true'"
  unit_of_measurement: "kB"
  value_template: "{{ value | int }}"
  scan_interval: 300 # Every 5 minutes

Troubleshooting Section

  • SSH Connection Issues:
    • Verify SSH keys: Ensure id_rsa_openwrt and id_rsa_openwrt.pub are correctly generated and copied. Permissions on .ssh folder and keys on HA host should be restrictive (e.g., chmod 600 id_rsa_openwrt).
    • Firewall on OpenWrt: Check if SSH port (22) is open for your HA host's IP.
    • Test SSH manually: ssh -i /config/.ssh/id_rsa_openwrt hauser@your_router_ip from the HA terminal to confirm connectivity.
  • Command Not Returning Data / Empty State:
    • Execute the command directly on OpenWrt (e.g., ubus call hostapd.wlan0 get_clients) to verify its output.
    • Check value_template or json_attributes in Home Assistant. Use the Template Editor (Developer Tools) to test your Jinja2 templates with sample data.
    • Ensure jq is installed and working on OpenWrt if you're using it for parsing.
  • Stale Presence Data:
    • Adjust scan_interval for command_line sensors to a shorter duration (e.g., 30-60 seconds for presence). Be mindful of router load.
    • Consider an automation to periodically call homeassistant.update_entity for your command_line sensors if you need more dynamic control over polling.
  • command_line sensor shows unknown: This often means the command failed or timed out. Check Home Assistant logs (Settings > System > Logs) for specific errors. Increase timeout if the command takes long to execute.

Advanced Configuration & Optimization

1. Persistent IP Addresses for Reliable Detection

For critical devices, assign static DHCP leases on your OpenWrt router. This ensures their IP addresses remain consistent, making network monitoring and ARP table lookups more reliable, especially if devices occasionally drop off and rejoin the network.

Screenshot Placeholder: OpenWrt DHCP Static Leases Configuration Page

2. Optimizing Polling Frequency and Router Load

While polling every 30-60 seconds is fine for a few devices, too many command_line sensors polling frequently can put a strain on both your Home Assistant and your router. Consider these optimizations:

  • Staggered Polling: Use different scan_interval values for different sensors.
  • Event-Driven Updates (Advanced): For true real-time, consider making your OpenWrt router push data via MQTT on specific events (e.g., a DHCP lease change, or a device connecting/disconnecting from WiFi, using custom scripts that monitor logread or ubus events). This requires more advanced scripting on OpenWrt.

3. Consolidating Presence with Home Assistant's ping and nmap

For ultimate robustness, combine router-based detection with Home Assistant's built-in ping or nmap device trackers. Router data confirms a device is connected to the network, while ping/nmap confirms it's actively responding. Use a group or a more complex template sensor to combine these states:

# configuration.yaml
device_tracker:
  - platform: ping
    hosts:
      person_phone_ping:
        - your_phone_ip

# sensors.yaml
- platform: template
  sensors:
    person_combined_presence:
      friendly_name: "Person's Combined Presence"
      value_template: >
        {{ is_state('binary_sensor.person_phone_presence', 'on') and
           is_state('device_tracker.person_phone_ping', 'home') }}
      device_class: presence
      unique_id: person_combined_presence_openwrt_ping

4. Security Hardening for SSH Access

  • Restrict User Permissions: On OpenWrt, edit /etc/passwd for the hauser user to restrict their shell (e.g., to /bin/false) and use authorized_keys command="..." option to only allow specific commands.
  • Firewall Rules: Limit SSH access on OpenWrt to only your Home Assistant's IP address.

Real-World Example: Dynamic Home Automation

Leveraging OpenWrt data, you can create highly responsive and intelligent automations. Here's how to automate lighting and notifications:

Example 1: Dynamic Lighting with Occupancy

This automation turns on specific lights when a person's phone is detected as 'home' by the router and it's dark, and then ensures they turn off when the phone leaves.

# automations.yaml
- id: '1678901234567'
  alias: 'Automate Living Room Lights with Phone Presence'
  description: 'Turns on/off living room lights based on phone presence and ambient light'
  trigger:
    - platform: state
      entity_id: binary_sensor.person_phone_presence
      to: 'on'
      for: '00:00:30'
    - platform: state
      entity_id: binary_sensor.person_phone_presence
      to: 'off'
      for: '00:05:00'
  condition:
    - condition: numeric_state
      entity_id: sensor.living_room_light_sensor
      below: 50 # Example: Light level in lux
  action:
    - choose:
      - conditions:
          - condition: state
            entity_id: binary_sensor.person_phone_presence
            to: 'on'
        sequence:
          - service: light.turn_on
            target:
              entity_id: light.living_room_main_lights
            data:
              brightness_pct: 70
              transition: 2
      - conditions:
          - condition: state
            entity_id: binary_sensor.person_phone_presence
            to: 'off'
        sequence:
          - service: light.turn_off
            target:
              entity_id: light.living_room_main_lights
            data:
              transition: 5
  mode: single

Example 2: Critical Device Offline Notifications

Receive immediate alerts if your media server or any other critical network device goes offline, as detected by the router's ARP table.

# automations.yaml
- id: '1678901234568'
  alias: 'Notify if Media Server Goes Offline'
  description: 'Sends a notification if the media server is not detected on the network'
  trigger:
    - platform: state
      entity_id: binary_sensor.media_server_online
      to: 'off'
      for: '00:05:00' # Only trigger if offline for 5 minutes (to avoid transient drops)
  action:
    - service: notify.mobile_app_your_phone
      data:
        title: "Smart Home Alert: Media Server Offline"
        message: "The media server ({{ states('sensor.media_server_online') }}) has been offline for 5 minutes. Please check its status."
        data:
          tag: "media-server-offline"
          priority: "high"
  mode: single

Best Practices & Wrap-up

  • Modularity: Keep your OpenWrt scripts simple and focused on data extraction. Do complex logic and parsing within Home Assistant's templates. This makes debugging easier.
  • Version Control: Treat your Home Assistant configuration (including these sensor definitions) and any custom OpenWrt scripts as code. Use Git to track changes, allowing you to easily revert to previous working states.
  • Regular Backups: Always maintain backups of your Home Assistant configuration and your OpenWrt router's settings.
  • Performance Monitoring: Keep an eye on your Home Assistant's system resource usage and your OpenWrt router's load. If you notice performance degradation, review your scan_interval values and consider optimizing your SSH commands.
  • Security First: Always follow the principle of least privilege. The dedicated SSH user on OpenWrt should only have the minimum necessary permissions.

By integrating OpenWrt with Home Assistant, you transform your router into a powerful data source for your smart home, enabling more reliable presence detection, proactive monitoring, and ultimately, a more intelligent and resilient automation environment.

Avatar picture of NGC 224
Written by:

NGC 224

Author bio: DIY Smart Home Creator

There are no comments yet
loading...