Files
Autoparts-DB/console/screens/vin_decoder.py
2026-02-15 01:49:15 +00:00

260 lines
8.1 KiB
Python

"""
VIN decoder screen for the AUTOPARTES console application.
Prompts for a 17-character Vehicle Identification Number, decodes it
via the NHTSA vPIC API (with local caching), and displays the decoded
vehicle information. The user can then navigate to the parts catalog
filtered by the matched vehicle.
"""
import json
from console.core.screens import Screen
from console.core.keybindings import Key
from console.config import APP_NAME, VERSION
from console.utils.vin_api import decode_vin_nhtsa
# Footer labels
_FOOTER_INPUT = [
("ENTER", "Decodificar"),
("ESC", "Atras"),
]
_FOOTER_RESULT = [
("1", "Ver partes"),
("2/F3", "Nuevo VIN"),
("ESC", "Atras"),
]
_FOOTER_ERROR = [
("F3", "Nuevo VIN"),
("ESC", "Atras"),
]
class VinDecoderScreen(Screen):
"""VIN decoder with NHTSA API integration and local cache."""
def __init__(self):
super().__init__(name="vin_decoder", title="Decodificador VIN")
self._vin = None
self._decoded = None
self._error = None
self._needs_input = True
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _decode(self, vin, db):
"""Decode a VIN using cache first, then NHTSA API."""
self._vin = vin.upper().strip()
self._decoded = None
self._error = None
# Check cache
cached = db.get_vin_cache(self._vin)
if cached:
self._decoded = {
"make": cached.get("make", ""),
"model": cached.get("model", ""),
"year": cached.get("year", ""),
"engine_info": cached.get("engine_info", ""),
"body_class": cached.get("body_class", ""),
"drive_type": cached.get("drive_type", ""),
}
return
# Call NHTSA API
result = decode_vin_nhtsa(self._vin)
if "error" in result:
self._error = result["error"]
return
# Extract fields
make = result.get("make", "")
model = result.get("model", "")
year = result.get("year", "")
body_class = result.get("body_class", "")
drive_type = result.get("drive_type", "")
# Build engine info string
engine_info_dict = result.get("engine_info", {})
engine_parts = []
if engine_info_dict.get("displacement_l"):
engine_parts.append(f"{engine_info_dict['displacement_l']}L")
if engine_info_dict.get("cylinders"):
engine_parts.append(f"{engine_info_dict['cylinders']}cil")
if engine_info_dict.get("fuel_type"):
engine_parts.append(engine_info_dict["fuel_type"])
if engine_info_dict.get("power_hp"):
engine_parts.append(f"{engine_info_dict['power_hp']}hp")
engine_info = " ".join(engine_parts)
self._decoded = {
"make": make,
"model": model,
"year": year,
"engine_info": engine_info,
"body_class": body_class,
"drive_type": drive_type,
}
# Cache the result
try:
year_int = int(year) if year else 0
except (ValueError, TypeError):
year_int = 0
try:
db.save_vin_cache(
vin=self._vin,
data=json.dumps(result),
make=make,
model=model,
year=year_int,
engine_info=engine_info,
body_class=body_class,
drive_type=drive_type,
)
except Exception:
pass # Non-critical: caching failure should not break the flow
def _try_match_vehicle(self, db):
"""Try to match the decoded VIN to a vehicle in the database.
Returns a context dict for the catalogo screen if a match is
found, or None if no match exists.
"""
if not self._decoded:
return None
make = self._decoded.get("make", "")
model = self._decoded.get("model", "")
year = self._decoded.get("year", "")
if not make or not model:
return None
try:
year_int = int(year) if year else None
except (ValueError, TypeError):
year_int = None
# Try to find matching model_year_engine records
if year_int:
mye_records = db.get_model_year_engine(make, model, year_int)
else:
mye_records = []
ctx = {
"level": "categories",
"brand": make,
"model": model,
"year": year,
"engine": self._decoded.get("engine_info", ""),
}
if mye_records:
# Use the first match
ctx["mye_id"] = mye_records[0]["id"]
return ctx
# ------------------------------------------------------------------
# Render
# ------------------------------------------------------------------
def render(self, context, db, renderer):
# Header
renderer.draw_header(
f" {APP_NAME} v{VERSION}",
" DECODIFICADOR VIN ",
)
if self._needs_input:
renderer.draw_footer(_FOOTER_INPUT)
return
if self._error:
renderer.draw_text(5, 4, f"Error: {self._error}", "error")
renderer.draw_footer(_FOOTER_ERROR)
return
if self._decoded is None:
renderer.draw_text(5, 4, "Presione F3 para ingresar un VIN", "info")
renderer.draw_footer(_FOOTER_ERROR)
return
# Display decoded VIN info
fields = [
("VIN", self._vin or ""),
("Marca", self._decoded.get("make", "")),
("Modelo", self._decoded.get("model", "")),
("Ano", str(self._decoded.get("year", ""))),
("Motor", self._decoded.get("engine_info", "")),
("Carroceria", self._decoded.get("body_class", "")),
("Traccion", self._decoded.get("drive_type", "")),
]
renderer.draw_detail(fields, title="INFORMACION DEL VEHICULO")
# Action menu below detail
h, _w = renderer.get_size()
action_row = 5 + len(fields) + 3
if action_row < h - 4:
renderer.draw_text(action_row, 4, "1. Ver partes compatibles", "normal")
renderer.draw_text(action_row + 1, 4, "2. Nueva consulta VIN", "normal")
renderer.draw_footer(_FOOTER_RESULT)
# ------------------------------------------------------------------
# Key handling
# ------------------------------------------------------------------
def on_key(self, key, context, db, renderer, nav):
# If we need input, show the input dialog
if self._needs_input:
self._needs_input = False
value = renderer.show_input("VIN (17 caracteres)", max_len=17)
if value is None:
# User pressed ESC in input dialog
if self._decoded is not None:
return None
return "back"
value = value.strip()
if len(value) != 17:
self._error = "El VIN debe tener exactamente 17 caracteres"
self._decoded = None
return None
self._decode(value, db)
return None
# ESC: go back
if key == Key.ESCAPE:
return "back"
# F3 or '2': new VIN input
if key == Key.F3 or key == ord("2"):
self._needs_input = True
self._error = None
return None
# '1': view compatible parts
if key == ord("1"):
if self._decoded:
cat_context = self._try_match_vehicle(db)
if cat_context:
return ("catalogo", cat_context, "Catalogo")
else:
renderer.show_message(
"No se encontro el vehiculo en la base de datos.\n"
"Se mostrara el catalogo completo.",
"info",
)
return ("catalogo", {"level": "categories"}, "Catalogo")
return None
return None