#!/usr/bin/env python3 """google-geolocate -- find your location from nearby WiFi via the Google Geolocation API. An example adapter for wttrin's `wttrin-geolocation-command'. It scans nearby WiFi access points, sends them to Google's Geolocation API, and prints the resulting coordinates (plus a reverse-geocoded address) as JSON on stdout: {"lat": 41.3222, "lng": -71.8113, "accuracy_m": 25.0, "address": "Westerly, Rhode Island, USA"} wttrin reads `lat' and `lng' to fetch weather, and shows `address' on the buffer's "Location:" line. Why this beats IP geolocation: a real WiFi scan fed to Google's database resolves to street level, where IP geolocation only finds your network's exit point -- often the wrong city on a VPN or a cellular hotspot. Requirements: - Python 3 (standard library only). - nmcli (NetworkManager) for the WiFi scan. On a system without NetworkManager, replace `scan_wifi' with your platform's scanner. - A Google API key with the Geolocation API enabled, in the environment variable GOOGLE_GEOLOCATION_API_KEY. See the README for setup and pricing. Wire it into Emacs: (setq wttrin-geolocation-command "/path/to/google-geolocate.py") On any failure this exits non-zero, so wttrin falls back to its IP provider. """ import json import os import re import subprocess import sys import urllib.error import urllib.request from typing import NoReturn GEOLOCATE_URL = "https://www.googleapis.com/geolocation/v1/geolocate?key={key}" NOMINATIM_URL = "https://nominatim.openstreetmap.org/reverse?lat={lat}&lon={lng}&format=jsonv2" USER_AGENT = "wttrin-google-geolocate-example/1.0" AUTHINFO_MACHINE = "googleapis.com" # the "machine" entry this looks for in authinfo def fail(message) -> NoReturn: """Print MESSAGE to stderr and exit non-zero (wttrin falls back to IP).""" print(f"google-geolocate: {message}", file=sys.stderr) sys.exit(1) def read_api_key(): """Return the Google API key, or None if it cannot be found. Checks the environment variable GOOGLE_GEOLOCATION_API_KEY first, then ~/.authinfo.gpg (the encrypted store Emacs's auth-source uses). See the README for the authinfo line format. """ key = os.environ.get("GOOGLE_GEOLOCATION_API_KEY") if key: return key return read_key_from_authinfo() def read_key_from_authinfo(): """Return the password for the AUTHINFO_MACHINE entry in ~/.authinfo.gpg, or None. Decrypts the file with gpg (gpg-agent supplies the passphrase) and reads a netrc-style line: machine login password . """ path = os.path.expanduser("~/.authinfo.gpg") if not os.path.exists(path): return None try: decrypted = subprocess.run( ["gpg", "--quiet", "--batch", "--decrypt", path], capture_output=True, text=True, timeout=30, check=True).stdout except (FileNotFoundError, subprocess.SubprocessError): return None for line in decrypted.splitlines(): tokens = line.split() if "machine" in tokens and "password" in tokens: pairs = dict(zip(tokens[::2], tokens[1::2])) if pairs.get("machine") == AUTHINFO_MACHINE: return pairs.get("password") return None def scan_wifi(): """Return a list of {"macAddress", "signalStrength"} for visible access points. Uses nmcli. Its SIGNAL column is a 0-100 quality percentage; NetworkManager maps quality = 2 * (dBm + 100), so dBm = quality / 2 - 100, which is the unit Google's API expects. Replace this function to support a non-NetworkManager system (for example macOS via CoreWLAN); it is the only platform-specific part. """ try: completed = subprocess.run( ["nmcli", "-t", "-f", "SIGNAL,BSSID", "device", "wifi", "list", "--rescan", "yes"], capture_output=True, text=True, timeout=20, check=True) except FileNotFoundError: fail("nmcli not found; replace scan_wifi for your system") except subprocess.SubprocessError as error: fail(f"wifi scan failed: {error}") access_points = [] for line in completed.stdout.splitlines(): # In nmcli -t output, fields are colon-separated and literal colons # inside a field (the BSSID) are backslash-escaped. Splitting on the # first unescaped colon separates SIGNAL from the BSSID, then we unescape. parts = [p.replace("\\:", ":") for p in re.split(r"(?