diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-25 16:03:18 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-25 16:03:18 -0400 |
| commit | 7027cccea9eb7170ea0f08e1def3f979f2e59932 (patch) | |
| tree | 51752f8cac0d92b4442048d913272e923211b619 /examples/geolocation/README.org | |
| parent | 33621b5a6e5407da190767b89756e287698ef234 (diff) | |
| download | emacs-wttrin-7027cccea9eb7170ea0f08e1def3f979f2e59932.tar.gz emacs-wttrin-7027cccea9eb7170ea0f08e1def3f979f2e59932.zip | |
feat: add external-command geolocation, opt-out, and example adapters
These build on the current-location picker with the rest of the geolocation work.
wttrin-geolocation-command runs a command that prints {lat,lng} (and optionally an address) and queries wttr.in by those coordinates. IP geolocation only finds the network's exit point, which is wrong on a VPN or hotspot. A command that scans nearby WiFi resolves to street level. It runs asynchronously, falls back to the IP provider when unset or failing, and assumes nothing about the OS, so it's inert until set. When the command returns an address, wttrin shows it on a "Location:" line, so the resolved place is readable even though the fetch is by raw coordinates.
wttrin-use-current-location is a labeled command that sets the favorite to auto-detect, so the bare t value never has to be typed into init by hand.
wttrin-geolocation-enabled (default t) turns every geolocation surface off for anyone who wants that: the picker entry, the auto-detect favorite, and the command.
examples/geolocation/ ships two reference adapters for the command: google-geolocate.py (Google API, key via the environment or ~/.authinfo.gpg) and apple-wps.py (Apple's keyless WiFi positioning, which uses an undocumented endpoint, so read its caveat). Both are Python 3 standard library and scan via nmcli, with notes on adapting the scan to other systems.
Diffstat (limited to 'examples/geolocation/README.org')
| -rw-r--r-- | examples/geolocation/README.org | 164 |
1 files changed, 164 insertions, 0 deletions
diff --git a/examples/geolocation/README.org b/examples/geolocation/README.org new file mode 100644 index 0000000..2a5462e --- /dev/null +++ b/examples/geolocation/README.org @@ -0,0 +1,164 @@ +#+TITLE: wttrin geolocation command adapters + +These are example programs for ~wttrin-geolocation-command~. wttrin can run an +external command to find your location with far better accuracy than IP +geolocation (which only finds your network's exit point — wrong on a VPN or a +cellular hotspot). The command scans nearby WiFi access points, looks them up, +and prints coordinates; wttrin then queries wttr.in by those coordinates. + +These are *examples*, not part of the installed package. Copy one, adapt it to +your system, and point the variable at it: + +#+begin_src emacs-lisp + (setq wttrin-geolocation-command "/path/to/google-geolocate.py") +#+end_src + +* The contract + +A command prints one JSON object to standard output and exits zero: + +#+begin_src json + {"lat": 41.3222, "lng": -71.8113, "accuracy_m": 25.0, "address": "Westerly, Rhode Island, USA"} +#+end_src + +- ~lat~ and ~lng~ (numbers) are required. wttrin fetches weather for them. +- ~address~ (or ~label~) is optional. When present, wttrin shows it on a + "Location:" line in the weather buffer, so the resolved place is readable even + though the weather was fetched by raw coordinates. +- Any other keys are ignored. + +On any failure the command should exit non-zero (and may print a message to +standard error). wttrin then falls back to the IP provider named by +~wttrin-geolocation-provider~, so geolocation still works. + +* The adapters + +** google-geolocate.py +Uses the Google Geolocation API. Documented, supported, accurate, broad +coverage. Needs an API key (see below). Recommended starting point. + +** apple-wps.py +Uses Apple's WiFi positioning service. Keyless and accurate, but it queries an +*undocumented* Apple endpoint via a reverse-engineered protocol, which may be +contrary to Apple's terms of service. Read the caveat at the top of the file and +decide for yourself. Provided as an example only. + +* Requirements + +- Python 3 (standard library only — no packages to install). +- A WiFi scanner. Both examples use ~nmcli~ (NetworkManager), which is common on + Linux. On a system without NetworkManager, replace the ~scan_wifi~ function + with your platform's scanner; that is the only platform-specific part. See + "Adapting the WiFi scan" below for systemd-networkd, iwd, and macOS. +- Network access. Both also do a best-effort reverse-geocode via OpenStreetMap + Nominatim to fill in ~address~; if that call fails the coordinates are still + returned, just without an address. + +* Adapting the WiFi scan + +~scan_wifi~ needs, for each nearby access point, its BSSID (MAC address) and a +signal strength. nmcli gives a 0-100 quality (the examples convert it to dBm); +the sources below give dBm directly. Swap in whichever your system has and adjust +the parsing to match the sample line. + +** systemd-networkd (via wpa_supplicant) +networkd handles addressing, not WiFi scanning — the scan comes from +wpa_supplicant. No root needed when wpa_supplicant is already running. + +#+begin_src sh + wpa_cli -i wlan0 scan + wpa_cli -i wlan0 scan_results +#+end_src + +Output columns are =BSSID frequency signal(dBm) flags SSID= (tab-separated): + +#+begin_example + 00:11:22:33:44:55 2437 -65 [WPA2-PSK-CCMP][ESS] MyNetwork +#+end_example + +Take field 1 (BSSID) and field 3 (signal in dBm). + +** iwd +iwd has no scriptable way to list per-AP BSSIDs (=iwctl station <iface> +get-networks= shows SSIDs and aggregated signal, not BSSIDs). On an iwd system, +use =iw= below — it works regardless of the network manager. + +** Any Linux (iw, the low-level tool) +=iw= talks to the kernel directly (nl80211), so it works under NetworkManager, +iwd, wpa_supplicant, or networkd. Triggering a scan needs root. + +#+begin_src sh + sudo iw dev wlan0 scan | grep -E "^BSS|signal:" +#+end_src + +#+begin_example + BSS 00:11:22:33:44:55(on wlan0) + signal: -65.00 dBm +#+end_example + +Pair each =BSS <bssid>= line with the =signal: <dBm>= line that follows it. + +** macOS +macOS restricts WiFi scanning. The old =airport -s= was removed in macOS 14, and +=system_profiler SPAirPortDataType= redacts BSSIDs unless the calling process has +Location Services permission. The reliable path today is a small CoreWLAN helper +(Swift/PyObjC) that, with Location Services granted, reads =CWInterface +scanForNetworksWithName= and returns each network's =bssid= and =rssiValue= +(dBm). Treat macOS as needing that helper rather than a one-liner. + +* Setting up a Google API key + +1. Create a project in the Google Cloud console (https://console.cloud.google.com/). +2. Enable the "Geolocation API" for that project. +3. Create an API key (APIs & Services -> Credentials -> Create credentials -> API key). + Restrict it to the Geolocation API. +4. Note Google's pricing: the Geolocation API is a paid API with a monthly free + allowance. Review current terms before relying on it. +5. Make the key available to the command. The script checks the environment + first, then ~~/.authinfo.gpg~; use whichever you prefer. + +** Option A: an environment variable + +#+begin_src sh + export GOOGLE_GEOLOCATION_API_KEY="your-key-here" +#+end_src + +Put it somewhere your Emacs inherits it (a login shell profile). The script reads +~GOOGLE_GEOLOCATION_API_KEY~ and never stores the key. + +** Option B: ~/.authinfo.gpg (encrypted, recommended) + +Keep the key in the same encrypted store Emacs's auth-source uses. The script +reads it whenever the environment variable is unset. + +1. Open ~~/.authinfo.gpg~ in Emacs. It decrypts transparently for editing. (If + the file doesn't exist, create it; Emacs encrypts it on save once your GnuPG + key is set up.) +2. Add one netrc-style line: + +#+begin_src authinfo + machine googleapis.com login geolocation password YOUR-KEY-HERE +#+end_src + +3. Save the buffer. Emacs re-encrypts the file. +4. Confirm gpg can read it without prompting (gpg-agent caches the passphrase): + +#+begin_src sh + gpg --quiet --decrypt ~/.authinfo.gpg | grep googleapis.com +#+end_src + +The ~login~ field (~geolocation~) is just a label. The script matches on +~machine googleapis.com~ and uses the ~password~ value; the key is never printed +or written in plaintext. + +* Testing an adapter by hand + +Run it in a terminal and check the JSON: + +#+begin_src sh + ./google-geolocate.py + # {"lat": 41.3222, "lng": -71.8113, "accuracy_m": 25.0, "address": "..."} +#+end_src + +If it prints coordinates near you, point ~wttrin-geolocation-command~ at it and +pick "Current location (detect)" in ~M-x wttrin~. |
