I’m using connections to my home network to indicate presence of family members primarily using the Ping (ICMP) integration, but with this method, iPhones frequently “go offline” because they don’t respond to these pings when trying to safe power. The iPhone Detect integration is great, but it only works if the iPhones are on the same network as the Home Assistant device, which presented a problem in my environment.
The Environment
Here’s what I’m working with:
- Home Assistant OS on Raspberry Pi 4b, on VLAN 1.
- Android and iPhone phones on VLAN 2.
- Synology DSM NAS
- Synology SRM Router
The Solution
The NAS will host a python script that is schedule to run frequently using cron. The script will
- Read a configuration setting in MQTT to get a list of IP addresses that require UDP pinging (these are the iPhone IP addresses), and send a simple message to each of those devices.
- Connect to the router using SSH (the default admin account is not required for this) and download/parse the ARP table to get a list of currently connected IP addresses. The iPhones pinged above should appear on this list.
- Post a list of connected IP addresses to a different MQTT topic.
- Create an MQTT sensor that checks if a given IP address is listed in this topic.
This solution gets around the need for all iPhones to be on the same network as Home Assistant because the router’s ARP table provides visibility across all VLANs.
Setting Up Python on the Synology DSM
Some tips can be found at these sites: https://kb.synology.com/en-nz/DSM/tutorial/Set_up_Python_virtual_environment_on_NAS, and additionally at https://jackgruber.github.io/2021-06-27-install-pip-on-synology/, https://synoguide.com/2023/01/21/install-and-use-python-3-9-in-your-synology/, and https://www.sindastra.de/p/2290/how-to-install-python-pip-on-synology-nas
SSH into the DiskStation and then the process should look something like this: (ImaUser is a fake username.)
cd /volume1/homes/ImaUser/scripts/python3
python3 -m venv ha-presence
cd ha-presence
source bin/activate
python3 -m pip install --upgrade pip
pip install paramiko paho-mqtt
deactivate
The Code
Consider hardening this so your password isn’t just dangling out there in plaintext. Drop the code into that same virtual environment that you just created above.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | import subprocess import shlex import socket import paramiko import paho.mqtt.client as mqtt import time import re import logging from datetime import datetime # Uncomment this for more detailed debugging info # logging.basicConfig(level=logging.DEBUG) # MQTT broker configuration mqtt_broker = "192.168.1.42" # Replace with your MQTT broker address mqtt_port = 1883 mqtt_username = "mqtt_user" mqtt_password = "my mqtt password" mqtt_timeout = 2 # Wait this many seconds for the subscription callback. It should be immediate unless fresh installation pre_ping_topic = "arp_presence/config/pre_ping" connected_topic = "arp_presence/connected" updated_topic = "arp_presence/updated" # SSH server configuration ssh_server_ip = "192.168.1.1" # Replace with the router ssh serer IP address ssh_username = "router_user" ssh_password = "my router password" # Pre-ping configuration-- who gets pinged and at what ports pre_ping_addresses = [] pre_ping_port = 5353 # Function to connect via SSH and retrieve connected IP addresses from the server ARP table def retrieve_connected_ips(mqtt_client): try : # Connect to the SSH server logging.info(f "Connecting to SSH server..." ) ssh_client = paramiko.SSHClient() ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh_client.connect(ssh_server_ip, username = ssh_username, password = ssh_password) # Fetch the ARP table logging.info(f "Fetching the ARP table..." ) stdin, stdout, stderr = ssh_client.exec_command( "arp -a" ) arp_table = stdout.read() logging.debug(f "ARP table: {arp_table}" ) # Regex to capture both the IP and the MAC address. It's only connected if both are there. mac_pattern = re. compile (r "\((?P<ip>(?:[0-9]{1,3}\.){3}[0-9]{1,3})\) at (?P<mac>[0-9a-fA-F]{2}(?:[:-][0-9a-fA-F]{2}){5})" ) connected_ips = [match.group( 'ip' ) for match in mac_pattern.finditer(arp_table.decode())] logging.debug(f "Connected IP addresses:" ) for ip in connected_ips: logging.debug(ip) if mqtt_client.is_connected(): logging.info(f "Publishing connected IP addresses to MQTT..." ) mqtt_client.publish(f "{connected_topic}" , ' ' .join(connected_ips), retain = True ) mqtt_client.publish(f "{updated_topic}" , datetime.now().strftime( "%Y-%m-%d %H:%M:%S" ), retain = True ) else : logging.warning(f "MQTT client not connected. Unable to publish IP addresses." ) except Exception as e: logging.error(f "An unexpected error occurred: {e}" ) finally : # Close the SSH connection ssh_client.close() logging.info(f "SSH connection closed." ) # Callback function for when subscribed messages are received def on_message(client, userdata, msg): # The only configuration topic we care about if ( msg.topic = = pre_ping_topic ): global pre_ping_addresses pre_ping_addresses = msg.payload.decode().split() logging.debug(f "Received IP addresses to pre-ping: {pre_ping_addresses}" ) else : logging.debug(f "Received an unexpected and unsupported toopic: {msg.topic}" ) # Create MQTT client and set the message callback mqtt_client = mqtt.Client() mqtt_client.on_log = lambda client, userdata, level, buf: logging.debug(f "MQTT Log: {buf}" ) mqtt_client.on_message = on_message # Connect to MQTT broker and subscribe to the appropriate topics mqtt_client.username_pw_set(username = mqtt_username, password = mqtt_password) mqtt_client.connect(mqtt_broker, mqtt_port, 60 ) mqtt_client.subscribe(pre_ping_topic) # Start the MQTT loop to listen for 10 seconds or until both the pre-ping and the tracked addresses are received mqtt_client.loop_start() start_time = time.time() try : while not pre_ping_addresses and time.time() - start_time < mqtt_timeout: pass # Only do the pinging if there are pre-ping addresses to ping if not pre_ping_addresses: logging.warning(f "Nothing to ping. The topic '{pre_ping_topic}' is missing or never responded." ) else : # Send UDP messages to pre-ping IP addresses. iPhones should show up in the ARP table after this. logging.info(f "Sending UDP messages..." ) for ip in pre_ping_addresses: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_socket: udp_socket.settimeout( 1 ) udp_socket.sendto(b "ping" , (ip, pre_ping_port )) #logging.debug(f"Sent 'ping' message to {ip}") # Retrieve connected MAC addresses over SSH retrieve_connected_ips(mqtt_client) finally : # Disconnect from MQTT broker mqtt_client.disconnect() logging.info(f "Disconnected from MQTT broker." ) |
The Scheduled Task (cron job)
In the Synology task scheduler, create a task that calls this script as often as you like.
VENV_DIR="/volume1/homes/ImaUser/scripts/python3/ha-presence"
source "$VENV_DIR/bin/activate"
python3 "$VENV_DIR/iphone_presence.py"
deactivate
Some Important Points
- The ARP table on my Synology RT6600ax lists is formatted so that a connected device can be found by matching something like “(192.168.1.2) at 01:23:45:67::89:ab”
- Passwords shouldn’t be in plaintext like this, so generate a key pair, copy it to the server, and connect to SSH using the key file.