feat: asset naming, location extraction, and location display
- New asset naming: {MID} - {Customer} / {Address} / {Building details}
- Extracted 660 building_names from customer data (Disney resorts, hotels)
- 1,704 assets now have location data (building_name, floor, etc.)
- Location section in detail view now shows building, floor, trailer, room
- Added has_gps filter to API for map loading
- Added location_source column to track GPS origin
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migrate asset names to readable format and populate location fields.
|
||||
|
||||
Name format: {machine_id} - {customer} / {address} / {building_name}, Floor {floor}, Room {room}
|
||||
|
||||
Also populates missing location fields (building_name, floor, etc.)
|
||||
from seed_location, customer_name, and address data.
|
||||
|
||||
Usage:
|
||||
python3 scripts/migrate_asset_names.py # dry-run
|
||||
python3 scripts/migrate_asset_names.py --apply # write to DB
|
||||
python3 scripts/migrate_asset_names.py --force # overwrite existing location fields too
|
||||
"""
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ASSET_DB = str(Path(__file__).resolve().parent.parent / "assets.db")
|
||||
|
||||
|
||||
# ─── Building name extraction patterns ──────────────────────────────────────
|
||||
|
||||
# Disney hotel/resort name extraction from customer_name
|
||||
# "D-All Star Music GUEST" → "All Star Music"
|
||||
# "D-Port Orleans Rvsd GUEST" → "Port Orleans Riverside"
|
||||
DISNEY_RESORT_PATTERNS = [
|
||||
# "D-All Star Music GUEST" -> "All Star Music"
|
||||
(re.compile(r'^D[- ](.*?)\s+(?:GUEST|CAST|GUEST ROOM|GST)$', re.I), lambda m: m.group(1).strip()),
|
||||
# "D-All Star Music GUEST - MOV10" -> "All Star Music"
|
||||
(re.compile(r'^D[- ](.*?)\s+GUEST\s*-\s*\w+$', re.I), lambda m: m.group(1).strip()),
|
||||
# "D-CORONADO SPRINGS GUEST" -> "Coronado Springs"
|
||||
(re.compile(r'^D[- ](.*?)\s+(?:GUEST|CAST|ROOM|GUEST ROOM)$', re.I), lambda m: m.group(1).strip().title()),
|
||||
# Universal Studios / Valencia / etc.
|
||||
(re.compile(r'^Universal Studios (.+)$', re.I), lambda m: f'Universal Studios {m.group(1).strip()}'),
|
||||
]
|
||||
|
||||
# Manual overrides
|
||||
BUILDING_NAME_MAP = {
|
||||
"D-Port Orleans Rvsd GUEST": "Port Orleans Riverside",
|
||||
"D-Port Orleans Frrnc GUEST": "Port Orleans French Quarter",
|
||||
"D-CORONADO SPRINGS GUEST": "Coronado Springs",
|
||||
"D-All Star Music GUEST": "All Star Music",
|
||||
"D-All Star Movie Guest": "All Star Movies",
|
||||
"D-All Star Sports Guest": "All Star Sports",
|
||||
"D-ART OF ANIMATION Guest": "Art of Animation",
|
||||
"D-POP CENTURY GUEST": "Pop Century",
|
||||
"D-CARIBBEAN BEACH GUEST": "Caribbean Beach",
|
||||
"D-Saratoga Springs GUEST": "Saratoga Springs",
|
||||
"D-POLYNESIAN RESORT GUEST": "Polynesian Village Resort",
|
||||
"D-Animal Kngdm LodgeGuest": "Animal Kingdom Lodge",
|
||||
"D-Contemporary Guest": "Contemporary Resort",
|
||||
"D-Grand Floridian Guest": "Grand Floridian Resort",
|
||||
"D-WILDERNESS LODGE GUEST": "Wilderness Lodge",
|
||||
"D-Yacht & Beach Guest": "Yacht & Beach Club",
|
||||
"D-BoardWalk Guest": "BoardWalk Inn",
|
||||
"D-Riviera Resort Guest": "Riviera Resort",
|
||||
"D-Island Tower Polynesian": "Polynesian Village Resort - Island Tower",
|
||||
"D-DISNEY VENDING CAST": "Disney Vending",
|
||||
"D-Disney Springs CAST": "Disney Springs",
|
||||
"D-Magic Kingdom CAST": "Magic Kingdom",
|
||||
"D-Epcot CAST": "Epcot",
|
||||
"D-Hollywood Studios CAST": "Hollywood Studios",
|
||||
"D-Animal Kingdom CAST": "Animal Kingdom",
|
||||
"D-Celebration CAST": "Celebration",
|
||||
"D-WIDE WORLD SPORTS GUEST": "ESPN Wide World of Sports",
|
||||
"D-DISNEY WORLD SS CAST": "Disney World Shared Services",
|
||||
}
|
||||
|
||||
|
||||
def clean_customer_name(raw: str) -> str:
|
||||
"""Clean a customer name for display.
|
||||
|
||||
D-All Star Music GUEST → All Star Music Guest
|
||||
Home Depot #0232 Southlan → Home Depot #0232 Southlan
|
||||
"""
|
||||
if not raw:
|
||||
return ""
|
||||
name = raw
|
||||
# Remove D- prefix (Disney identifier)
|
||||
if name.startswith("D-") or name.startswith("D "):
|
||||
name = name[2:]
|
||||
# Remove GUEST/CAST/ROOM suffixes for cleaner display
|
||||
name = re.sub(r'\s+(?:GUEST|CAST|GUEST ROOM|GST|GUEST?)\s*$', '', name, flags=re.I)
|
||||
name = name.strip()
|
||||
return name
|
||||
|
||||
|
||||
def best_customer_for_name(asset: dict) -> str:
|
||||
"""Get the best customer string for the asset name."""
|
||||
c = asset.get("customer_name", "") or ""
|
||||
if c:
|
||||
return clean_customer_name(c)
|
||||
return ""
|
||||
|
||||
|
||||
def build_location_string(asset: dict) -> str:
|
||||
"""Build the location/address part for the asset name."""
|
||||
# Use address if available
|
||||
addr = (asset.get("address") or "").strip()
|
||||
# If no address, try seed_location minus customer prefix
|
||||
if not addr:
|
||||
seed = (asset.get("seed_location") or "").strip()
|
||||
if seed and " - " in seed:
|
||||
addr = seed.split(" - ", 1)[1].strip()
|
||||
elif seed:
|
||||
addr = seed
|
||||
return addr
|
||||
|
||||
|
||||
def build_building_details(asset: dict) -> str:
|
||||
"""Build building details string (building_name, floor, etc.)."""
|
||||
parts = []
|
||||
|
||||
bldg = (asset.get("building_name") or "").strip()
|
||||
floor = (asset.get("floor") or "").strip()
|
||||
room = (asset.get("room") or "").strip()
|
||||
bldg_num = (asset.get("building_number") or "").strip()
|
||||
trailer = (asset.get("trailer_number") or "").strip()
|
||||
|
||||
if bldg:
|
||||
parts.append(bldg)
|
||||
if bldg_num and (bldg not in bldg_num and bldg_num not in bldg):
|
||||
parts.append(f"Bldg {bldg_num}")
|
||||
if trailer:
|
||||
parts.append(f"Trailer {trailer}")
|
||||
if floor:
|
||||
parts.append(f"Floor {floor}")
|
||||
if room:
|
||||
parts.append(f"Room {room}")
|
||||
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
def build_new_name(asset: dict) -> str:
|
||||
"""Build the new asset name."""
|
||||
mid = (asset.get("machine_id") or "").strip()
|
||||
|
||||
# Customer — cleaned
|
||||
customer = best_customer_for_name(asset)
|
||||
|
||||
# Location (street address or seed_location minus customer prefix)
|
||||
location = build_location_string(asset)
|
||||
loc_clean = ""
|
||||
if location:
|
||||
loc_clean = location.split(",")[0].strip()
|
||||
|
||||
# Building details
|
||||
building = build_building_details(asset)
|
||||
|
||||
# Avoid dupes: if building starts with loc_clean, drop the building
|
||||
if building and location:
|
||||
bldg_lower = building.lower()
|
||||
loc_lower = loc_clean.lower()
|
||||
if bldg_lower.startswith(loc_lower) or loc_lower.startswith(bldg_lower):
|
||||
# Dupes — keep whichever is more descriptive
|
||||
if len(building) > len(loc_clean):
|
||||
loc_clean = ""
|
||||
else:
|
||||
building = ""
|
||||
|
||||
# Assemble
|
||||
parts = [mid]
|
||||
|
||||
if customer:
|
||||
parts.append(f" - {customer}")
|
||||
|
||||
if location and loc_clean:
|
||||
parts.append(f" / {loc_clean}")
|
||||
|
||||
if building:
|
||||
parts.append(f" / {building}")
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def infer_building_from_customer(customer_name: str) -> str:
|
||||
"""Try to extract a building/resort name from the customer name."""
|
||||
if not customer_name:
|
||||
return ""
|
||||
if customer_name in BUILDING_NAME_MAP:
|
||||
return BUILDING_NAME_MAP[customer_name]
|
||||
for pat, func in DISNEY_RESORT_PATTERNS:
|
||||
m = pat.search(customer_name)
|
||||
if m:
|
||||
return func(m)
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
apply_changes = "--apply" in sys.argv
|
||||
force = "--force" in sys.argv
|
||||
mode = "DRY RUN" if not apply_changes else "APPLYING"
|
||||
|
||||
print(f"DB: {ASSET_DB}")
|
||||
print(f"Mode: {mode}")
|
||||
if force:
|
||||
print(" (--force: overwrite existing location fields)")
|
||||
print()
|
||||
|
||||
conn = sqlite3.connect(ASSET_DB)
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
|
||||
# Load all assets
|
||||
cur = conn.execute("""
|
||||
SELECT id, machine_id, name, customer_name, seed_location, address,
|
||||
building_name, floor, room, trailer_number, building_number,
|
||||
location_area, place
|
||||
FROM assets ORDER BY id
|
||||
""")
|
||||
rows = cur.fetchall()
|
||||
total = len(rows)
|
||||
print(f"Loaded {total} assets")
|
||||
|
||||
cols = ["id", "machine_id", "name", "customer_name", "seed_location", "address",
|
||||
"building_name", "floor", "room", "trailer_number", "building_number",
|
||||
"location_area", "place"]
|
||||
|
||||
stats = {
|
||||
"name_changed": 0,
|
||||
"building_name_filled": 0,
|
||||
"floor_filled": 0,
|
||||
"building_number_filled": 0,
|
||||
}
|
||||
|
||||
name_updates = []
|
||||
bldg_updates = []
|
||||
|
||||
for row in rows:
|
||||
asset = dict(zip(cols, row))
|
||||
|
||||
# ── 1. Infer building_name from customer_name if empty ──
|
||||
old_bldg = (asset["building_name"] or "").strip()
|
||||
new_bldg = old_bldg
|
||||
if not old_bldg or force:
|
||||
inferred = infer_building_from_customer(asset["customer_name"] or "")
|
||||
if inferred and inferred != old_bldg:
|
||||
new_bldg = inferred
|
||||
if not old_bldg:
|
||||
stats["building_name_filled"] += 1
|
||||
if apply_changes:
|
||||
bldg_updates.append((new_bldg, "building_name", asset["id"]))
|
||||
if mode == "DRY RUN" and not old_bldg:
|
||||
print(f" [{asset['machine_id']}] building_name '{inferred}' ← from '{asset['customer_name']}'")
|
||||
|
||||
# ── 2. Build new name ──
|
||||
old_name = (asset["name"] or "").strip()
|
||||
new_name = build_new_name(asset)
|
||||
|
||||
if new_name != old_name:
|
||||
stats["name_changed"] += 1
|
||||
name_updates.append((new_name, asset["id"]))
|
||||
if mode == "DRY RUN" and stats["name_changed"] <= 10:
|
||||
print(f" Name: {old_name[:60]}")
|
||||
print(f" → {new_name[:80]}")
|
||||
print()
|
||||
|
||||
# ── Apply ──
|
||||
if apply_changes:
|
||||
if name_updates:
|
||||
conn.executemany(
|
||||
"UPDATE assets SET name=?, updated_at=datetime('now') WHERE id=?",
|
||||
name_updates,
|
||||
)
|
||||
# Apply location updates
|
||||
for value, column, aid in bldg_updates:
|
||||
conn.execute(f"UPDATE assets SET {column}=?, updated_at=datetime('now') WHERE id=?", (value, aid))
|
||||
conn.commit()
|
||||
|
||||
# Verify
|
||||
cur = conn.execute("SELECT COUNT(*) FROM assets WHERE building_name != ''")
|
||||
bldg_count = cur.fetchone()[0]
|
||||
cur = conn.execute("SELECT COUNT(*) FROM assets WHERE floor != ''")
|
||||
floor_count = cur.fetchone()[0]
|
||||
print(f"\n ✅ Applied {len(name_updates)} name changes, {len(bldg_updates)} location updates")
|
||||
else:
|
||||
# Show some sample new names
|
||||
bldg_filled = stats["building_name_filled"]
|
||||
if name_updates:
|
||||
print(f"\n Sample new names:")
|
||||
conn2 = sqlite3.connect(ASSET_DB)
|
||||
for mid in ["95339", "95417", "98271", "90009", "95301", "68601"]:
|
||||
cur2 = conn2.execute(
|
||||
"SELECT id, machine_id, name, customer_name, address, building_name, floor FROM assets WHERE machine_id=?",
|
||||
(mid,)
|
||||
)
|
||||
for r2 in cur2.fetchall():
|
||||
a = dict(zip(cols, r2))
|
||||
print(f"\n {a['machine_id']}")
|
||||
print(f" Old: {a['name'][:70]}")
|
||||
print(f" New: {build_new_name(a)[:70]}")
|
||||
if a.get("customer_name"):
|
||||
print(f" Bldg inferred: '{infer_building_from_customer(a['customer_name'])}'")
|
||||
conn2.close()
|
||||
|
||||
# Summary
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"SUMMARY")
|
||||
print(f"{'=' * 60}")
|
||||
print(f" Names changed: {stats['name_changed']}")
|
||||
print(f" Building names filled: {stats['building_name_filled']}")
|
||||
|
||||
if apply_changes:
|
||||
cur = conn.execute("SELECT COUNT(*) FROM assets WHERE building_name != ''")
|
||||
print(f" Total with building_name: {cur.fetchone()[0]}")
|
||||
cur = conn.execute("SELECT COUNT(*) FROM assets WHERE floor != ''")
|
||||
print(f" Total with floor: {cur.fetchone()[0]}")
|
||||
cur = conn.execute("SELECT COUNT(*) FROM assets WHERE building_name != '' OR floor != '' OR building_number != '' OR room != ''")
|
||||
print(f" Total with ANY location data: {cur.fetchone()[0]}")
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user