|
| 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) |
0 commit comments