aboutsummaryrefslogtreecommitdiff
path: root/examples/geolocation/README.org
blob: 2a5462ea4c5042970c839c30c221b03ecdc01e02 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
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~.