Skip to content

Commit a7922c8

Browse files
committed
Fix: Validate VIN & allow vehicles with no transmission
Uses some fancy SQL to remove NULL entries for transmissions when at least one row has a value for the transmission, so that both vehicles with no transmission and with transmissions can properly be decoded. Also adds a few "tests" to validate this.
1 parent 168f2b9 commit a7922c8

File tree

3 files changed

+53
-13
lines changed

3 files changed

+53
-13
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# volvo_vida_db
22

3-
This repository contains scripts to extract and work with data from the internal Volvo VIDA database.
3+
This repository contains scripts to extract and work with data from the internal Volvo VIDA 2014D database.
44

55
Some SQL statements are based on procedures found in the VIDA SQL Server database and from scripts in [Tigo2000's repository](https://github.com/Tigo2000/Volvo-VIDA/).
66

scripts/test_vin_decoder.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env python3
2+
3+
from vin_decoder import PartnerGroup, decode_vin
4+
from read_csv import DatabaseFile, get_csvs
5+
6+
# These are some VINs I found in the VIDA source code which are used for testing I think.
7+
# Entering x0000000000000000 where x is any digit into the VIN input in VIDA will transform into
8+
# the i-ths VIN in the below list. The last two VINs seem to be left-over from testing code in VIDA.
9+
test_vins = [("YV1LZ56DXY2736349", PartnerGroup.AME),
10+
("YV1LW5642W2360395", PartnerGroup.EUR),
11+
("YV1945866W1234629", PartnerGroup.EUR),
12+
("YV1LS61F2Y2667227", PartnerGroup.EUR),
13+
("YV1SW61P212048331", PartnerGroup.EUR),
14+
# "YV1LW65F2Y683325", <-- Found in VIDA but actually invalid.
15+
("YV1RS65P212000500", PartnerGroup.EUR),
16+
("YV1RS65P922122056", PartnerGroup.EUR),
17+
# "YV1TS61R91193741", <-- Also not a valid VIN
18+
("YV1RS59G242370046", PartnerGroup.EUR),
19+
("YV1TS61F2Y1065654", PartnerGroup.EUR),
20+
("YV1KS960XV1104523", PartnerGroup.AME)]
21+
22+
get_csvs(DatabaseFile.vehicle_model, DatabaseFile.model_year, DatabaseFile.partner_group, DatabaseFile.body_style, DatabaseFile.engine, DatabaseFile.transmission)
23+
for vin, partner in test_vins:
24+
try:
25+
vehicle = decode_vin(vin, partner_id=partner, cached=False)
26+
print(vehicle.get_value_description("vehicle_model"))
27+
#vehicle.print()
28+
except ValueError as e:
29+
print("Failed lookup: " + str(e))

scripts/vin_decoder.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ def print(self):
7272
print(f'Transmission: {self.get_value_description("transmission")} [{self.transmission}]')
7373
print(f'VIDA Profiles: {self.get_vehicle_profiles()}')
7474

75+
def validate_vin(vin: str) -> bool:
76+
vin = vin.strip()
77+
if len(vin) != 17:
78+
return False
79+
chassis_no = vin[11:]
80+
return chassis_no.isdigit()
81+
7582
# TODO: Don't default to PartnerGroup.EUR but have some detection or add it as a required input for all callers.
7683
def decode_vin(vin: str, partner_id: PartnerGroup = PartnerGroup.EUR, cached: bool = True) -> Vehicle:
7784
"""
@@ -80,9 +87,11 @@ def decode_vin(vin: str, partner_id: PartnerGroup = PartnerGroup.EUR, cached: bo
8087
There's more information inside the VIN number, which is not decoded here. You can find more information here:
8188
https://en.wikibooks.org/wiki/Vehicle_Identification_Numbers_(VIN_codes)/Volvo/VIN_Codes#Position_1_-_3:_World_Manufacturer_Identifier
8289
"""
90+
if not validate_vin(vin):
91+
raise ValueError('VIN string is not a valid VIN')
92+
8393
if vin[:3] != 'YV1':
84-
print('VIN is not for a Volvo vehicle')
85-
return
94+
raise ValueError('VIN is not for a Volvo vehicle')
8695

8796
if cached and os.path.exists(f"cache/{vin}.p"):
8897
# This import is to avoid namespace problems, making the class always be vin_decoder.Vehicle instead of __main__.Vehicle
@@ -109,33 +118,35 @@ def decode_vin(vin: str, partner_id: PartnerGroup = PartnerGroup.EUR, cached: bo
109118
AND VM.fkPartnerGroup = {partner_id.value}
110119
""")
111120

112-
get_csv(DatabaseFile.vehicle_profile)
113-
114121
# For cases where the VIN represents multiple combinations of engines and transmissions we match the table with itself to create
115-
# rows with every possible combination of engines and transmissions. However, we also want to allow VINs that only represent a single engine.
122+
# rows with every possible combination of engines and transmissions.
123+
# However, we also want to allow VINs where the transmission is not determinable or where no transmission is specified in the database.
116124
combined = duckdb.sql("""
117125
SELECT DISTINCT c1.fkVehicleModel, c1.fkModelYear, c1.fkPartnerGroup, c1.fkBodyStyle, c1.fkEngine, c2.fkTransmission FROM components AS c1, components AS c2
118-
WHERE (c1.fkEngine IS NOT NULL AND c2.fkTransmission IS NOT NULL) OR c1.fkEngine IS NOT NULL
126+
WHERE (c1.fkEngine IS NOT NULL AND c2.fkTransmission IS NOT NULL) OR (c1.fkEngine IS NOT NULL AND c2.fkTransmission IS NULL AND NOT EXISTS (
127+
SELECT 1 FROM components WHERE fkTransmission IS NOT NULL
128+
))
119129
""").df()
120130

121131
# Filter all the engine/transmission combinations for actually valid ones from the VehicleProfile table if more than one exists
122132
if len(combined) > 1:
133+
get_csv(DatabaseFile.vehicle_profile)
123134
filtered = duckdb.sql("""
124135
SELECT DISTINCT combined.* FROM combined
125136
INNER JOIN vehicle_profile vp on vp.fkVehicleModel=combined.fkVehicleModel AND vp.fkModelYear=combined.fkModelYear
126-
WHERE combined.fkEngine=vp.fkEngine AND combined.fkTransmission=vp.fkTransmission
137+
WHERE (combined.fkEngine=vp.fkEngine AND combined.fkTransmission=vp.fkTransmission) OR combined.fkTransmission IS NULL
127138
""").df()
128139
combined = filtered
129140

130141
# Replace possible NaN values with 'None' to avoid having float64 columns, and cast everything to int to not use numpy types
131142
combined = combined.replace({math.nan: 0}).astype('int64')
132-
print(combined)
133143

134144
if combined.empty:
135-
raise ValueError('Failed to find valid vehicle profiles for VIN')
136-
if len(combined) > 1:
137-
raise ValueError('More than 1 vehicle profile appeared for VIN')
138-
145+
raise ValueError('Failed to find valid vehicles for VIN')
146+
if len(combined['fkVehicleModel'].unique()) > 1:
147+
# Only the vehicle model has to be unique. All other values can technically be unspecified by the VIN.
148+
raise ValueError('More than 1 vehicle appeared for VIN')
149+
139150
from vin_decoder import Vehicle
140151
vehicle = Vehicle(vin, combined.loc[0])
141152
if cached:

0 commit comments

Comments
 (0)