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
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
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?!