Files
got-fiber/scripts/build_starlink.py
T

152 lines
5.3 KiB
Python

import argparse
import csv
import io
import json
import os
import sys
import zipfile
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
import h3
sys.path.insert(0, str(Path(__file__).resolve().parent))
from build_fcc import CACHE, DATA, FIPS, USPS, api_download, as_of_dates, dump, log, parse_states, reset_dir
from build_bead import availability_files
RES = 7
SHARD = 4
TECH = "61"
def to_int(v):
try:
return int(float(v))
except (TypeError, ValueError):
return 0
def is_starlink(brand, provider):
s = f"{brand} {provider}".lower()
return "starlink" in s or "space exploration" in s or "spacex" in s
def rows(path):
openers = []
if path.suffix.lower() == ".zip":
z = zipfile.ZipFile(path)
for n in z.namelist():
if n.lower().endswith(".csv"):
openers.append(lambda n=n: io.TextIOWrapper(z.open(n), encoding="utf-8-sig", newline=""))
else:
openers.append(lambda: open(path, encoding="utf-8-sig", newline=""))
for o in openers:
with o() as fh:
r = csv.reader(fh)
head = next(r, None)
if not head:
continue
idx = {k: head.index(k) if k in head else None for k in
("brand_name", "provider_id", "technology", "h3_res8_id",
"max_advertised_download_speed", "max_advertised_upload_speed")}
if idx["h3_res8_id"] is None:
continue
top = max(i for i in idx.values() if i is not None)
g = lambda row, k: row[idx[k]] if idx[k] is not None else ""
for row in r:
if len(row) <= top:
continue
if idx["technology"] is not None and g(row, "technology").strip() != TECH:
continue
yield g(row, "brand_name"), g(row, "provider_id"), g(row, "h3_res8_id"), g(row, "max_advertised_download_speed"), g(row, "max_advertised_upload_speed")
def ingest(path, cells):
n = 0
for brand, provider, cell, down, up in rows(path):
if not is_starlink(brand, provider):
continue
cell = cell.strip().lower()
if not h3.is_valid_cell(cell):
continue
c7 = h3.cell_to_parent(cell, RES)
e = cells.get(c7)
d, u = to_int(down), to_int(up)
if e is None:
cells[c7] = [d, u]
else:
e[0] = max(e[0], d)
e[1] = max(e[1], u)
n += 1
return n
def write(cells, states, as_of):
root = DATA / "starlink"
reset_dir(root)
shards = defaultdict(dict)
for c, v in cells.items():
shards[h3.cell_to_parent(c, SHARD)][c] = v
for sid, cs in shards.items():
tiers = sorted({tuple(v) for v in cs.values()})
idx = {t: i for i, t in enumerate(tiers)}
dump(root / f"{sid}.json", {"t": [list(t) for t in tiers], "c": {c: idx[tuple(v)] for c, v in cs.items()}})
path = DATA / "manifest.json"
m = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
m.update({
"starlinkStates": sorted(states),
"starlinkAsOf": as_of,
"starlinkRes": RES,
"starlinkShardRes": SHARD,
"starlinkGenerated": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
})
path.write_text(json.dumps(m, indent=2) + "\n", encoding="utf-8")
log(f"Starlink: {len(cells)} cells in {len(shards)} shards, states {sorted(states)}")
def main():
ap = argparse.ArgumentParser(description="Build Starlink availability tiles from FCC data")
ap.add_argument("--states", default=os.environ.get("STATES", "AZ"))
ap.add_argument("--local", help="Folder of FCC satellite availability CSV or ZIP files")
ap.add_argument("--as-of", default="local data")
a = ap.parse_args()
states = parse_states(a.states)
cells, done = {}, set()
if a.local:
for p in sorted(Path(a.local).iterdir()):
if p.suffix.lower() in (".zip", ".csv"):
log(f"{p.name}: {ingest(p, cells)} Starlink rows")
done = set(states)
as_of = a.as_of
else:
user = os.environ.get("FCC_USERNAME", "").strip()
token = os.environ.get("FCC_API_TOKEN", "").strip()
if not user or not token:
sys.exit("Set FCC_USERNAME and FCC_API_TOKEN.")
wanted = {FIPS[s] for s in states}
files, as_of = {}, None
for as_of in as_of_dates(user, token)[:4]:
files = availability_files(as_of, wanted, user, token)
if any(TECH in v for v in files.values()):
break
CACHE.mkdir(exist_ok=True)
for fips in sorted(files):
st = USPS[fips]
for f in files[fips].get(TECH, []):
fid = f.get("file_id")
dest = CACHE / f"{as_of}_{fid}.zip"
if not dest.exists():
log(f"{st}: downloading {f.get('file_name', fid)}")
api_download(f"/downloads/downloadFile/availability/{fid}", user, token, dest)
log(f"{st}: {ingest(dest, cells)} Starlink rows, {len(cells)} cells so far")
dest.unlink()
done.add(st)
if not cells:
sys.exit("No Starlink rows found. Nothing written.")
write(cells, done, as_of)
if __name__ == "__main__":
main()