Getting an Accurate Weather Forecast using Wi-Fi Position System

I'm travelling a lot with my laptop, and having an accurate weather forecast on-the-go is very convenient.

I'm using Fedora as my main (and only) OS, along with i3wm and polybar. Previously I used GNOME 3 which had a few weather widgets. The most feature-rich was OpenWeather, which worked pretty well.

polybar doesn't have a native weather widget and the one's at x70b1/polybar-scripts suffered from the same drawbacks that OpenWeather did: location had to be manually specified or was ip-geolocation based (which is not accurate enough)

How do I get an accurate location without a GPS? WiPS to the rescue!

Wi-Fi Positioning System

Did you ever notice that you can get accurate location data when your GPS is turned off? The trick is to measure Wi-Fi signal intensity in order to triangulate your location.

Google, for instance, tracks open access points when you walk around and upload these along with your GPS location. When your GPS is turned off, they can use the access points around you to calculate your GPS coordinates. Cool right?

Lifewire published an explanation about Wi-Fi triangulation a few months ago, and Wikipedia has a nice article on the subject as well.

Getting Accurate Geolocation

Google Maps has a neat Geolocation API that gives accurate location data if supplied with the MAC addresses of surrounding access points.

I can use iw to extract these by running:

$ iw <iface> scan | grep -io "[0-9A-F]\{2\}\(:[0-9A-F]\{2\}\)\{5\}" | uniq

But how the heck do I get the wireless interfaces? well, there's /proc/net/wireless for that -

import re

def get_wireless_interfaces():
iface_rx = re.compile('^(.*): ')

# I love one-liners. Hope that doesn't look like perl :)
# - open() returns a read-only file wrapper that is iterable
# - map() executes re.match() on each line returned from open()
# - 'yield from' is used to delegate the generator were creating to another generator
yield from (m.group(1) for m
in map(iface_rx.match, open('/proc/net/wireless'))
if m)

Now, I need to to run iw. Running system commands from python is a pain in the ass.
unless you use the mighty sh package, that is!

import sh

def get_access_points_addresses():
rx = re.compile('([0-9A-F]{2}(:[0-9A-F]{2}){5})', re.IGNORECASE)
for iface in get_wireless_interfaces():
matches = rx.findall(sh.iw(iface, "scan").stdout.decode('utf-8'))
yield from (x[0].strip().lower() for x in matches)

And now all that's left is to kindly ask google to give us our location.
urllib is a pain, so I'm using requests instead.

import requests

def get_geolocation():
# "considerIP" flags indicates if google should fallback to ip-based
# geolocation if it can't find your ip using the given access points.
data = {"considerIp": True,
"wifiAccessPoints": [{"macAddress": hwaddr}
for hwaddr in set(get_access_points())] }



r = requests.post("https://www.googleapis.com/geolocation/v1/geolocate",
json=data,
params={"key": GOOGLE_MAPS_API_ACCESS_KEY })

r.raise_for_status()

location = r.json()["location"]
return location["lng"], location["lat"]

Getting Weather Forecast

OpenWeather widget used OpenWeatherMap API to get up-to-date weather forecast, so I used it as well.

import requests
def get_weather_forecast(longitude, latitude):
# I'm living in a country that uses a unit system that makes sense.
params = {"units": "metric",
"appid": OPEN_WEATHER_MAP_API_KEY,
"lat": latitude, "lon": longitude}

resp = requests.get("https://api.openweathermap.org/data/2.5/weather",
params=params)

resp.raise_for_status()

return resp.json()

And viola!

longitude, latitude = get_geolocation()
forecast = get_weather_forecast(longitude, latitude)
print((f"{forecast['name']} "
f"{forecast['main']['temp']:.0f}°C "
f"[{forecast['weather'][0]['main']}]"))

# Tel-Aviv 18°C [Clear]