Skip to content

Commit 29b6128

Browse files
committed
generalize to use windows netsh or linux nmcli
mls requires at least 2 BSSID
1 parent e2beba1 commit 29b6128

File tree

7 files changed

+151
-32
lines changed

7 files changed

+151
-32
lines changed

MozLoc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
#!/usr/bin/env python
22
"""
33
https://mozilla.github.io/ichnaea/api/geolocate.html
4+
https://ichnaea.readthedocs.io/en/latest/api/geolocate.html
5+
46
you should get your own Mozilla Location Services API key
57
68
Don't abuse the API or you'll get banned (excessive polling rate)
7-
8-
Uses ``nmcli`` from Linux only. Could be extended to other tools and OS.
99
"""
1010
from mozloc import log_wifi_loc
1111
from argparse import ArgumentParser

README.md

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,65 @@
1+
# Mozilla Location Services from Python
2+
13
[![Build Status](https://travis-ci.com/scivision/mozilla-location-wifi-python.svg?branch=master)](https://travis-ci.com/scivision/mozilla-location-wifi-python)
24
[![Python versions (PyPI)](https://img.shields.io/pypi/pyversions/mozilla-location-python.svg)](https://pypi.python.org/pypi/mozilla-location-python)
35
[![PyPi Download stats](http://pepy.tech/badge/mozilla-location-python)](http://pepy.tech/project/mozilla-location-python)
46

7+
Uses command line access to WiFi information in a short, simple Mozilla Location Services with Wifi from Python.
8+
The command line programs used to access WiFi inforamtion include:
59

6-
# mozilla-location-python
7-
Uses nmcli on Linux in a short, simple Mozilla Location Services with Wifi from Python.
8-
Goal was to be as simple as possible.
10+
* Linux: `nmcli` [NetworkManager](https://developer.gnome.org/NetworkManager/stable/nmcli.html)
11+
* Windows: [`netsh`](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/cc755301(v=ws.10)?redirectedfrom=MSDN)
912

1013
Note that a similar service with better accuracy is available from
1114
[Google](https://developers.google.com/maps/documentation/geolocation/intro).
1215
Let us know if you're interested.
1316

1417
## Install
18+
1519
```sh
1620
python -m pip install -e .
1721
```
1822

19-
### prereqs
20-
Linux system with NetworkManager (e.g. Ubuntu, Raspberry Pi, etc.).
21-
22-
23-
2423
## Usage
24+
2525
```sh
26-
./MozLoc.py
26+
python MozLoc.py
2727
```
2828

2929
Returns `dict()` containing `lat` `lng` `accuracy` `N BSSIDs heard`.
3030
In urban areas, accuracy ~ 5 - 100 meters.
3131

32-
3332
### convert to KML
34-
You can display your logged data in Google Earth or other KML value after converting by
3533

36-
./csv2kml.py in.log out.kml
34+
Display logged data in Google Earth or other KML viewer after converting from CSV to KML:
3735

38-
with
36+
```sh
37+
python csv2kml.py in.log out.kml
38+
```
3939

40-
pip install simplekml
40+
which uses
41+
42+
```sh
43+
pip install simplekml
44+
```
4145

4246
Note that your time MUST be in ISO 8601 format or some KML reading programs such as Google Earth will just show a blank file.
4347
E.g.
4448

4549
2016-07-24T12:34:56
4650

51+
## TODO
4752

48-
## Contributing
49-
Pull request if you have another favorite approach.
50-
Would like to add Bluetooth, should be simple.
51-
53+
Would like to add Bluetooth beacons.
5254

5355
## Notes
5456

5557
* [Inspired by](https://github.com/flyinva/mozlosh)
5658
* [Alternative using Skyhook and geoclue](https://github.com/scivision/python-geoclue)
5759
* [Raspberry Pi NetworkManager](https://raspberrypi.stackexchange.com/a/73816)
5860

59-
### Raspberry Pi 3 / Zero W
61+
### Raspberry Pi 3 / 4 / Zero W
62+
6063
Debian comes without NetworkManager by default.
6164
Be careful as you lose Wifi password etc. by this procedure
6265

mozloc/base.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
from time import sleep
22
from pathlib import Path
3-
4-
from .netman import nm_config_check, get_nmcli
5-
3+
import sys
4+
5+
if sys.platform == "win32":
6+
from .netsh import cli_config_check, get_cli
7+
elif sys.platform == "linux":
8+
from .netman import cli_config_check, get_cli
9+
else:
10+
raise ImportError(f"MozLoc doesn't yet know how to work with platform {sys.platform}")
611
HEADER = "time lat lon accuracy NumBSSIDs"
712

813

@@ -16,10 +21,10 @@ def log_wifi_loc(T: float, logfile: Path):
1621
print(f"updating every {T} seconds")
1722
print(HEADER)
1823

19-
nm_config_check()
24+
cli_config_check()
2025
sleep(0.5) # nmcli errored for less than about 0.2 sec.
2126
while True:
22-
loc = get_nmcli()
27+
loc = get_cli()
2328
if loc is None:
2429
sleep(T)
2530
continue

mozloc/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
URL = "https://location.services.mozilla.com/v1/geolocate?key=test"

mozloc/netman.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,26 @@
99
import requests
1010
from datetime import datetime
1111

12+
from .config import URL
13+
1214
NMCLI = shutil.which("nmcli")
1315
if not NMCLI:
14-
raise ImportError('This program relies on NetworkManager via "nmcli"')
16+
raise ImportError('Could not find NetworkManager "nmcli"')
1517

16-
URL = "https://location.services.mozilla.com/v1/geolocate?key=test"
1718
NMCMD = [NMCLI, "-g", "SSID,BSSID,FREQ,SIGNAL", "device", "wifi"] # Debian stretch, Ubuntu 18.04
1819
NMLEG = [NMCLI, "-t", "-f", "SSID,BSSID,FREQ,SIGNAL", "device", "wifi"] # ubuntu 16.04
1920
NMSCAN = [NMCLI, "device", "wifi", "rescan"]
2021

2122

22-
def nm_config_check():
23+
def cli_config_check():
2324
# %% check that NetworkManager CLI is available and WiFi is active
2425
ret = subprocess.check_output([NMCLI, "-t", "radio", "wifi"], universal_newlines=True, timeout=1.0).strip().split(":")
2526

2627
if "enabled" not in ret and "disabled" in ret:
2728
raise ConnectionError("must enable WiFi, perhaps via nmcli radio wifi on")
2829

2930

30-
def get_nmcli() -> typing.Dict[str, typing.Any]:
31+
def get_cli() -> typing.Dict[str, typing.Any]:
3132

3233
ret = subprocess.run(NMCMD, timeout=1.0)
3334
if ret.returncode != 0:
@@ -51,6 +52,9 @@ def get_nmcli() -> typing.Dict[str, typing.Any]:
5152
)
5253
# %% optout
5354
dat = dat[~dat["ssid"].str.endswith("_nomap")]
55+
if dat.shape[0] < 2:
56+
logging.warning("cannot locate since at least 2 BSSIDs required")
57+
return None
5458
# %% cleanup
5559
dat["ssid"] = dat["ssid"].str.replace("nan", "")
5660
dat["macAddress"] = dat["macAddress"].str.replace(r"\\:", ":")

mozloc/netsh.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
""" Network Manager CLI (nmcli) functions """
2+
import subprocess
3+
import typing
4+
import logging
5+
import shutil
6+
import io
7+
import requests
8+
import json
9+
from math import log10
10+
from datetime import datetime
11+
12+
from .config import URL
13+
14+
CLI = shutil.which("netsh")
15+
if not CLI:
16+
raise ImportError('Could not find NetSH "netsh"')
17+
18+
CMD = [CLI, "wlan", "show", "networks", "mode=bssid"]
19+
20+
21+
def cli_config_check():
22+
# %% check that NetSH CLI is available and WiFi is active
23+
ret = subprocess.check_output(CMD, universal_newlines=True, timeout=1.0)
24+
for line in ret.split("\n"):
25+
if "networks currently visible" in line:
26+
return
27+
if "The wireless local area network interface is powered down and doesn't support the requested operation" in line:
28+
raise ConnectionError("must enable WiFi, it appears to be turned off.")
29+
logging.error("could not determine WiFi state.")
30+
31+
32+
def get_cli() -> typing.Dict[str, typing.Any]:
33+
""" get signal strength using CLI """
34+
ret = subprocess.run(CMD, timeout=1.0, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
35+
if ret.returncode != 0:
36+
logging.error(f"consider slowing scan cadence. {ret.stderr}")
37+
38+
dat: typing.List[typing.Dict[str, str]] = []
39+
out = io.StringIO(ret.stdout)
40+
for line in out:
41+
d: typing.Dict[str, str] = {}
42+
if not line.startswith("SSID"):
43+
continue
44+
ssid = line.split(":", 1)[1].strip()
45+
# optout
46+
if ssid.endswith("_nomap"):
47+
continue
48+
# find BSSID MAC address
49+
for line in out:
50+
if not line[4:9] == "BSSID":
51+
continue
52+
d["macAddress"] = line.split(":", 1)[1].strip()
53+
for line in out:
54+
if not line[9:15] == "Signal":
55+
continue
56+
signal_percent = int(line.split(":", 1)[1][:3])
57+
d["signalStrength"] = str(signal_percent_to_dbm(signal_percent))
58+
d["ssid"] = ssid
59+
dat.append(d)
60+
d = {}
61+
break
62+
if len(dat) < 2:
63+
logging.warning("cannot locate since at least 2 BSSIDs required")
64+
return None
65+
# %% JSON
66+
jdat = json.dumps(dat)
67+
jdat = '{ "wifiAccessPoints":' + jdat + "}"
68+
logging.debug(jdat)
69+
# %% cloud MLS
70+
try:
71+
req = requests.post(URL, data=jdat)
72+
if req.status_code != 200:
73+
logging.error(req.text)
74+
return None
75+
except requests.exceptions.ConnectionError as e:
76+
logging.error(f"no network connection. {e}")
77+
return None
78+
# %% process MLS response
79+
jres = req.json()
80+
loc = jres["location"]
81+
loc["accuracy"] = jres["accuracy"]
82+
loc["N"] = len(dat) # number of BSSIDs used
83+
loc["t"] = datetime.now()
84+
85+
return loc
86+
87+
88+
def signal_percent_to_dbm(percent: int) -> int:
89+
""" arbitrary conversion factor from Windows WiFi signal % to dBm
90+
assumes 100% is -30 dBm
91+
92+
Parameters
93+
----------
94+
percent: int
95+
signal strength as percent 0..100
96+
97+
Returns
98+
-------
99+
meas_dBm: int
100+
truncate to nearest integer because of uncertainties
101+
"""
102+
REF = -30 # dBm
103+
ref_mW = 10 ** (REF / 10) / 1000
104+
meas_mW = max(ref_mW * percent / 100, 1e-7)
105+
meas_dBm = 10 * log10(meas_mW) + 30
106+
return int(meas_dBm)

tests/test_all.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def test_nm_loc():
88
mozloc = pytest.importorskip("mozloc")
99

1010
try:
11-
loc = mozloc.netman.get_nmcli()
11+
loc = mozloc.netman.get_cli()
1212
except subprocess.CalledProcessError as e:
1313
pytest.skip(f"problem with NMCLI API--old NMCLI version? {e}")
1414

@@ -20,7 +20,7 @@ def test_nm_connection():
2020
mozloc = pytest.importorskip("mozloc")
2121

2222
try:
23-
mozloc.netman.nm_config_check()
23+
mozloc.netman.cli_config_check()
2424
except subprocess.CalledProcessError as e:
2525
pytest.skip(f"problem with NMCLI WiFi API--do you have WiFi? {e}")
2626

0 commit comments

Comments
 (0)