Linux script for updating your laptop location using ipinfo.io

with ipinfo.io you can get some location (the city) of your linux device.
I used this to make a script.

#!/usr/bin/bash
# Variables
DEVICE_NAME="device_tracker.laptop"  # Replace with your device's entity ID
HA_URL="https://home.assistant..com"  # Replace with your Home Assistant URL
API_TOKEN="12345678"  # Replace with your Home Assistant long-lived access token

STATE="not_home"  # Default state if no zone matches

# Get IP location using ipinfo.io
LOCATION=$(curl -s https://ipinfo.io/json)
LATITUDE=$(echo "$LOCATION" | jq -r '.loc' | cut -d',' -f1)
LONGITUDE=$(echo "$LOCATION" | jq -r '.loc' | cut -d',' -f2)
CITY=$(echo "$LOCATION" | jq -r '.city')
COUNTRY=$(echo "$LOCATION" | jq -r '.country')
GPS_ACCURACY=50  # Default GPS accuracy value

# Fetch all zones from Home Assistant
ZONES=$(curl -s -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" "$HA_URL/api/states" | jq '.[] | select(.entity_id | startswith("zone."))')

# Function to calculate distance between two coordinates
calculate_distance() {
    LAT1=$1
    LON1=$2
    LAT2=$3
    LON2=$4
    gawk -v lat1="$LAT1" -v lon1="$LON1" -v lat2="$LAT2" -v lon2="$LON2" '
    function acos(x) {
        return atan2(sqrt(1 - x * x), x);
    }
    BEGIN {
        pi = 3.141592653589793238;
        radlat1 = pi * lat1 / 180;
        radlat2 = pi * lat2 / 180;
        theta = lon1 - lon2;
        radtheta = pi * theta / 180;
        dist = sin(radlat1) * sin(radlat2) + cos(radlat1) * cos(radlat2) * cos(radtheta);
        if (dist > 1) dist = 1;
        dist = acos(dist);
        dist = dist * 180 / pi;
        dist = dist * 60 * 1.1515 * 1.609344;  # Convert to kilometers
        print dist;
    }'
}

# Determine the zone based on the closest match
BEST_ZONE="not_home"
BEST_DISTANCE=100000  # Start with a large distance
while read -r ZONE; do
    ZONE_LAT=$(echo "$ZONE" | jq -r '.attributes.latitude')
    ZONE_LON=$(echo "$ZONE" | jq -r '.attributes.longitude')
    ZONE_RADIUS=$(echo "$ZONE" | jq -r '.attributes.radius')
    ZONE_NAME=$(echo "$ZONE" | jq -r '.attributes.friendly_name')

    DISTANCE=$(calculate_distance "$LATITUDE" "$LONGITUDE" "$ZONE_LAT" "$ZONE_LON")
    if (( $(echo "$DISTANCE < $ZONE_RADIUS / 1000" | bc -l) )); then
        if (( $(echo "$DISTANCE < $BEST_DISTANCE" | bc -l) )); then
            BEST_DISTANCE=$DISTANCE
            BEST_ZONE=$ZONE_NAME
        fi
    fi
done <<< "$(echo "$ZONES" | jq -c '.')"

# Update Home Assistant device tracker state
curl -X POST \
     -H "Authorization: Bearer $API_TOKEN" \
     -H "Content-Type: application/json" \
     -d "{
       \"state\": \"$BEST_ZONE\",
       \"attributes\": {
         \"latitude\": $LATITUDE,
         \"longitude\": $LONGITUDE,
         \"gps_accuracy\": $GPS_ACCURACY,
         \"city\": \"$CITY\",
         \"country\": \"$COUNTRY\"
       }
     }" \
     "$HA_URL/api/states/$DEVICE_NAME"

# Print success message
echo "Location updated for $DEVICE_NAME to $BEST_ZONE zone ($LATITUDE, $LONGITUDE)."

also a script to geep ipinfo.io out of a local vpn.

#!/bin/bash

# Destination domain
DEST_DOMAIN="ipinfo.io"

# Fetch the current IP address of the destination domain
DEST_IP=$(nslookup $DEST_DOMAIN | awk '/^Address: / { print $2 }' | tail -n1)

if [ -z "$DEST_IP" ]; then
    echo "Error: Unable to resolve $DEST_DOMAIN."
    exit 1
fi

# Fetch the current default gateway and associated interface (excluding VPNs)
DEFAULT_ROUTE=$(ip route | grep -m 1 '^default' | grep -v tun)
DEFAULT_GATEWAY=$(echo "$DEFAULT_ROUTE" | awk '{print $3}')
NETWORK_INTERFACE=$(echo "$DEFAULT_ROUTE" | awk '{print $5}')

if [ -z "$DEFAULT_GATEWAY" ] || [ -z "$NETWORK_INTERFACE" ]; then
    echo "Error: Unable to determine the default gateway or network interface."
    exit 1
fi

# Check if a route for the destination IP already exists
EXISTING_ROUTE=$(ip route | grep "$DEST_IP")

if [[ "$EXISTING_ROUTE" == *"$DEFAULT_GATEWAY"* && "$EXISTING_ROUTE" == *"$NETWORK_INTERFACE"* ]]; then
    echo "Route for $DEST_IP already exists and is correct."
else
    # Remove the existing route if it points to a different gateway/interface
    sudo ip route del $DEST_IP 2>/dev/null

    # Add the new route
    sudo ip route add $DEST_IP via $DEFAULT_GATEWAY dev $NETWORK_INTERFACE
    echo "Added route for $DEST_IP via $DEFAULT_GATEWAY ($NETWORK_INTERFACE)."
fi

# Display the current route for the destination
echo "Current routing table entry for $DEST_IP:"
ip route get $DEST_IP

happy scripting

1 Like

Thanks for your script, I’m quite surprised that while device location is standard procedure in Android, there is almost no such software for Linux. Not even for Home Assistant, which has integrations for everything else.

ipinfo.io gives quite strange results at least in my wifi at home (locates me in a town 200km away), so I tried to use the integrated gps of the mobile modem of my laptop. Therefore I somehow beefed up your script to use the ModemManager GPS data and to uses ipinfo only as fallback. This seems to work for my hw/sw configuration, but as I don’t move around much it is not really tested yet. Maybe somebody else has some ideas, too?

I also set the zone name manually to the string “home” when in the home zone, otherwise the device tracker entity does not report that state correctly (why?). GPS also reports altitude, so this is sent to HA, too. The interval for the GPS scans can be set. The script is a endless loop which can be started at boot with a simple systemd service.

#!/usr/bin/bash

# Variables
DEVICE_NAME="device_tracker.location_service_xxxx"  # Replace with your device's entity ID
FRIENDLY_NAME="Location Service xxxx" # Friendly name
HA_URL="xxxx"  # Replace with your Home Assistant URL
API_TOKEN="xxxx"  # Replace with your Home Assistant long-lived access token
UPDATE_INTERVAL=300 # Update frequency (sec)
HOME_ZONE_NAME="xxx" # Name of the home zone

while true
do
	echo "Device tracker($DEVICE_NAME) updated to Home Assistant instance at $HA_URL."

	mmcli -m any --location-enable-gps-raw &>/dev/null && mmcli -m any --location-set-gps-refresh-rate=$UPDATE_INTERVAL &>/dev/null

	if [ $? -eq 0 ]; then
		# Get IP location using gps
		LOCATION=$(mmcli -m any --location-get -J)
		LATITUDE=$(echo "$LOCATION" | jq -r '.modem.location.gps.latitude' | tr , .)
		ALTITUDE=$(echo "$LOCATION" | jq -r '.modem.location.gps.altitude' | tr , .)
		LONGITUDE=$(echo "$LOCATION" | jq -r '.modem.location.gps.longitude' | tr , .)
		CITY=unknown
		COUNTRY=unknown
		GPS_ACCURACY=50
	else
		# Get IP location using ipinfo.io
		echo "ModemManager gps not available, fallback to ipinfo.io"
		LOCATION=$(curl -s https://ipinfo.io/json)
		LATITUDE=$(echo "$LOCATION" | jq -r '.loc' | cut -d',' -f1)
		LONGITUDE=$(echo "$LOCATION" | jq -r '.loc' | cut -d',' -f2)
		CITY=$(echo "$LOCATION" | jq -r '.city')
		COUNTRY=$(echo "$LOCATION" | jq -r '.country')
		ALTITUDE=unknown
		GPS_ACCURACY=50  # Default GPS accuracy value
	fi

	# Fetch all zones from Home Assistant
	ZONES=$(curl -s -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" "$HA_URL/api/states" | jq '.[] | select(.entity_id | startswith("zone."))')

	# Function to calculate distance between two coordinates
	calculate_distance() {
		LAT1=$1
		LON1=$2
		LAT2=$3
		LON2=$4
		gawk -v lat1="$LAT1" -v lon1="$LON1" -v lat2="$LAT2" -v lon2="$LON2" '
		function acos(x) {
        		return atan2(sqrt(1 - x * x), x);
    		}
    		BEGIN {
        		pi = 3.141592653589793238;
        		radlat1 = pi * lat1 / 180;
        		radlat2 = pi * lat2 / 180;
        		theta = lon1 - lon2;
        		radtheta = pi * theta / 180;
        		dist = sin(radlat1) * sin(radlat2) + cos(radlat1) * cos(radlat2) * cos(radtheta);
        		if (dist > 1) dist = 1;
        			dist = acos(dist);
        			dist = dist * 180 / pi;
        			dist = dist * 60 * 1.1515 * 1.609344;  # Convert to kilometers
        			print dist;
    			}'
	}

	# Determine the zone based on the closest match
	BEST_ZONE="not_home"
	BEST_DISTANCE=100000  # Start with a large distance
	while read -r ZONE; do
    		ZONE_LAT=$(echo "$ZONE" | jq -r '.attributes.latitude')
    		ZONE_LON=$(echo "$ZONE" | jq -r '.attributes.longitude')
    		ZONE_RADIUS=$(echo "$ZONE" | jq -r '.attributes.radius')
    		ZONE_NAME=$(echo "$ZONE" | jq -r '.attributes.friendly_name')

    		DISTANCE=$(calculate_distance "$LATITUDE" "$LONGITUDE" "$ZONE_LAT" "$ZONE_LON")
    		if (( $(echo "$DISTANCE < $ZONE_RADIUS / 1000" | bc -l) )); then
        		if (( $(echo "$DISTANCE < $BEST_DISTANCE" | bc -l) )); then
            			BEST_DISTANCE=$DISTANCE
            			BEST_ZONE=$ZONE_NAME
        		fi
    		fi
	done <<< "$(echo "$ZONES" | jq -c '.')"

	# report state "home" instead of the home zone name to get a proper device tracker state
	if [ "$BEST_ZONE" = "$HOME_ZONE_NAME" ]; then
		BEST_ZONE="home"
	fi

	# Update Home Assistant device tracker state
	curl --silent -o /dev/null -X POST \
     		-H "Authorization: Bearer $API_TOKEN" \
     		-H "Content-Type: application/json" \
     		-d "{
       		\"state\": \"$BEST_ZONE\",
       		\"attributes\": {
         	\"latitude\": $LATITUDE,
         	\"longitude\": $LONGITUDE,
		\"altitude\": $ALTITUDE,
         	\"gps_accuracy\": $GPS_ACCURACY,
         	\"city\": \"$CITY\",
         	\"country\": \"$COUNTRY\",
		\"friendly_name\": \"$FRIENDLY_NAME\"
       		}
     		}" \
     		"$HA_URL/api/states/$DEVICE_NAME"

		# Print success message
		echo "Location: $BEST_ZONE ($LATITUDE, $LONGITUDE)."
		echo "Next update: $UPDATE_INTERVAL s"
		sleep $UPDATE_INTERVAL
done

Isnt opensource software great.

Your IP address isn’t very accurate, it is mostly the location from your provider

The city is most likely correct.
You can use this for city public transport for example.

Not in my case, 10km off in the next town.

In my case the IP from ipinfo in my home wifi seems to be the location of my provider (a major German company), varying between a city 200km away and another city 500km away. I already noticed that from other services guessing the location from the ip, so it’s not only an ipinfo problem.

But as I guess almost every current mobile broadband card has an integrated gps (at least so I think when reading the - very fragmented - docs of ModemManager), the “mmcli —get-location” variant works quite good on two of my laptops.

You could also think of saving energy and doing reverse geolocation from the cell information only, but that was too much for my limited coding skills and an small evening scripting project ;-).

Concerning the code:
I don’t really mind the city and country information, as I use the Home Assistant “places” integration to look that up and didn’t further handle it in the script.
I don’t really understand where to report the device tracker’s “home” and “not_home” states. That seems to be only special cases of the defined zones (so your “home” might not be called “home” at all). Therefore the hackish renaming of the zone name in this case. @Daft 's script has a default “STATE” variable which seems not to be used, however.
Thinking over it, you might also be able to report laptop battery etc. to Home Assistant this way?!