Skip to content

Commit b817ec0

Browse files
committed
Draft implementation of epa fueleconomy.gov data fetching
1 parent c37f0e2 commit b817ec0

File tree

4 files changed

+301
-12
lines changed

4 files changed

+301
-12
lines changed

libvin/decoding.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
(c) Copyright 2016 Dan Kegel <[email protected]>
55
"""
66

7-
from libvin.static import *
7+
from static import *
88

99
class Vin(object):
1010
def __init__(self, vin):

libvin/epa.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
"""
2+
Fetch data from fueleconomy.gov
3+
(c) Copyright 2016 Dan Kegel <[email protected]>
4+
License: AGPL v3.0
5+
"""
6+
7+
# Note: client app may wish to 'import requests_cache' and install a cache
8+
# to avoid duplicate fetches
9+
import requests
10+
import requests_cache
11+
# Cache responses for 7 days to be kind to nhtsa's server
12+
requests_cache.install_cache('libvin_tests_cache', expire_after=7*24*60*60)
13+
import itertools
14+
import json
15+
import xmltodict
16+
17+
# Local
18+
from decoding import Vin
19+
from nhtsa import *
20+
21+
class EPAVin(Vin):
22+
23+
# Public interfaces
24+
25+
def __init__(self, vin):
26+
super(EPAVin, self).__init__(vin)
27+
28+
self.__nhtsa = nhtsa_decode(vin)
29+
self.__model = self.__get_model()
30+
self.__id = self.__get_id()
31+
self.__eco = self.__get_vehicle_economy()
32+
33+
@property
34+
def nhtsa(self):
35+
'''
36+
NHTSA info dictionary for this vehicle.
37+
'''
38+
return self.__nhtsa
39+
40+
@property
41+
def nhtsaModel(self):
42+
'''
43+
NHTSA model name for this vehicle.
44+
'''
45+
return self.nhtsa['Model']
46+
47+
@property
48+
def model(self):
49+
'''
50+
EPA model name for this vehicle.
51+
'''
52+
return self.__model
53+
54+
@property
55+
def id(self):
56+
'''
57+
EPA id for this vehicle.
58+
'''
59+
return self.__id
60+
61+
@property
62+
def eco(self):
63+
'''
64+
EPA fuel economy info dictionary for this vehicle.
65+
Fields of interest:
66+
- co2TailpipeGpm - present for most vehicles
67+
- co2TailpipeAGpm - present for some vehicles, matches EPA website
68+
'''
69+
return self.__eco
70+
71+
# Private interfaces
72+
73+
def __get_possible_models(self):
74+
'''
75+
Return list of possible models for given year of given make.
76+
The models are those needed by get_vehicle_ids().
77+
'''
78+
79+
models = []
80+
url = 'http://www.fueleconomy.gov/ws/rest/vehicle/menu/model?year=%s&make=%s' % (self.year, self.make)
81+
try:
82+
r = requests.get(url)
83+
except requests.Timeout:
84+
print "epa:__get_possible_models: connection timed out"
85+
return None
86+
except requests.ConnectionError:
87+
print "epa:__get_possible_models: connection failed"
88+
return None
89+
try:
90+
content = r.content
91+
# You can't make this stuff up. I love xml.
92+
for item in xmltodict.parse(content).popitem()[1].items()[0][1]:
93+
models.append(item.popitem()[1])
94+
except AttributeError:
95+
print "epa:__get_possible_models: no models for year %s, make %s" % (self.year, self.make)
96+
return None
97+
except ValueError:
98+
print "epa:__get_possible_models: could not parse result"
99+
return None
100+
return models
101+
102+
def __get_model(self):
103+
'''
104+
Given a decoded vin and its nhtsa data, look up its epa model name
105+
'''
106+
models = self.__get_possible_models()
107+
if models == None:
108+
return None
109+
110+
# Get candidate modifier strings
111+
modifiers = []
112+
driveType = self.nhtsa['DriveType']
113+
if 'AWD' in driveType or '4WD' in driveType or '4x4' in driveType:
114+
modifiers.append("4WD")
115+
modifiers.append("AWD")
116+
# Special cases
117+
if self.make == 'GMC' and self.nhtsaModel == 'Sierra':
118+
modifiers.append("K15")
119+
elif 'Front' in driveType or 'FWD' in driveType or '4x2' in driveType:
120+
modifiers.append("2WD")
121+
modifiers.append("FWD")
122+
# Special cases
123+
if self.make == 'GMC' and self.nhtsaModel == 'Sierra':
124+
modifiers.append("C15")
125+
else:
126+
# special cases
127+
if self.make == 'Ford' and self.nhtsaModel == 'Focus':
128+
modifiers.append("FWD")
129+
if 'Trim' in self.nhtsa and self.nhtsa['Trim'] != "":
130+
modifiers.append(self.nhtsa['Trim'])
131+
if 'BodyClass' in self.nhtsa and self.nhtsa['BodyClass'] != "":
132+
modifiers.append(self.nhtsa['BodyClass'])
133+
if 'Series' in self.nhtsa and self.nhtsa['Series'] != "":
134+
modifiers.append(self.nhtsa['Series'])
135+
136+
# Throw them against the wall and see what sticks
137+
# FIXME: pick model with highest number of matches regardless of order
138+
for L in range(len(modifiers)+1, 0, -1):
139+
for subset in itertools.permutations(modifiers, L):
140+
modified_model = self.nhtsaModel + " " + " ".join(subset)
141+
if modified_model in models:
142+
return modified_model
143+
144+
if self.nhtsaModel in models:
145+
return self.nhtsaModel
146+
147+
print "epa:__get_model: Failed to find model for %s" % self.vin
148+
return None
149+
150+
def __get_possible_ids(self):
151+
'''
152+
Return dictionary of id -> vehicle trim string from fueleconomy.gov, or None on error.
153+
The id's are those needed by get_vehicle_economy().
154+
'''
155+
156+
id2trim = dict()
157+
url = 'http://www.fueleconomy.gov/ws/rest/vehicle/menu/options?year=%s&make=%s&model=%s' % (self.year, self.make, self.model)
158+
try:
159+
r = requests.get(url)
160+
except requests.Timeout:
161+
print "epa:__get_possible_ids: connection timed out"
162+
return None
163+
except requests.ConnectionError:
164+
print "epa:__get_possible_ids: connection failed"
165+
return None
166+
try:
167+
content = r.content
168+
# You can't make this stuff up. I love xml.
169+
parsed = xmltodict.parse(content)
170+
innards = parsed.popitem()[1].items()[0][1]
171+
# special case for N=1
172+
if not isinstance(innards, list):
173+
innards = [ innards ]
174+
for item in innards:
175+
id = item.popitem()[1]
176+
trim = item.popitem()[1]
177+
id2trim[id] = trim
178+
except ValueError:
179+
print "epa:__get_possible_ids: could not parse result"
180+
return None
181+
return id2trim
182+
183+
def __get_id(self):
184+
'''
185+
Given a decoded vin, look up its epa id, or return None on failure
186+
'''
187+
if self.model == None:
188+
return None
189+
id2trim = self.__get_possible_ids()
190+
191+
# If only one choice, return it
192+
if (len(id2trim) == 1):
193+
key, value = id2trim.popitem()
194+
return key
195+
196+
# Filter by engine displacement
197+
displacement = '%s L' % self.nhtsa['DisplacementL']
198+
matches = [key for key, value in id2trim.items() if displacement in value.upper()]
199+
if (len(matches) == 1):
200+
return matches[0]
201+
202+
# Filter by transmission
203+
tran = None
204+
if 'Manual' in self.nhtsa['TransmissionStyle']:
205+
tran = 'MAN'
206+
if 'Auto' in self.nhtsa['TransmissionStyle']:
207+
tran = 'AUTO'
208+
if tran != None:
209+
matches = [key for key, value in id2trim.items() if tran in value.upper()]
210+
if (len(matches) == 1):
211+
return matches[0]
212+
213+
print "epa:__get_id: Failed to match trim for %s" % self.vin
214+
return None
215+
216+
def __get_vehicle_economy(self):
217+
'''
218+
Return dictionary of a particular vehicle's economy data from fueleconomy.gov, or None on error.
219+
id is from __get_vehicle_ids().
220+
'''
221+
222+
url = 'http://www.fueleconomy.gov/ws/rest/vehicle/%s' % self.id
223+
try:
224+
r = requests.get(url)
225+
except requests.Timeout:
226+
print "epa:__get_vehicle_economy: connection timed out"
227+
return None
228+
except requests.ConnectionError:
229+
print "epa:__get_vehicle_economy: connection failed"
230+
return None
231+
try:
232+
content = r.content
233+
return xmltodict.parse(content).popitem()[1]
234+
except ValueError:
235+
print "epa:__get_vehicle_economy: could not parse result"
236+
return None
237+
return None

tests/__init__.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,47 @@
11
# Sorted alphabetically by VIN
22
TEST_DATA = [
3-
# http://www.vindecoder.net/?vin=137ZA903X1E412677&submit=Decode unchecked
4-
{'VIN': '137ZA903X1E412677', 'WMI': '137', 'VDS': 'ZA903X', 'VIS': '1E412677',
5-
'MODEL': 'H1', 'MAKE': 'Hummer', 'YEAR': 2001, 'COUNTRY': 'United States',
6-
'REGION': 'north_america', 'SEQUENTIAL_NUMBER': '412677', 'FEWER_THAN_500_PER_YEAR': False},
7-
83
# http://www.vindecoder.net/?vin=1C4RJEAG2EC476429&submit=Decode
94
{'VIN': '1C4RJEAG2EC476429', 'WMI': '1C4', 'VDS': 'RJEAG2', 'VIS': 'EC476429',
105
'MODEL': 'Grand Cherokee', 'MAKE': 'Jeep', 'YEAR': 2014, 'COUNTRY': 'United States',
11-
'REGION': 'north_america', 'SEQUENTIAL_NUMBER': '476429', 'FEWER_THAN_500_PER_YEAR': False},
6+
'REGION': 'north_america', 'SEQUENTIAL_NUMBER': '476429', 'FEWER_THAN_500_PER_YEAR': False,
7+
'nhtsa.model' : 'Grand Cherokee',
8+
'epa.model' : 'Grand Cherokee 2WD', 'epa.co2TailpipeGpm': '443.0',
9+
},
1210

1311
# http://www.vindecoder.net/?vin=1D7RB1CP8BS798034&submit=Decode
1412
{'VIN': '1D7RB1CP8BS798034', 'WMI': '1D7', 'VDS': 'RB1CP8', 'VIS': 'BS798034',
1513
'MODEL': 'Ram 1500', 'MAKE': 'Dodge', 'YEAR': 2011, 'COUNTRY': 'United States',
16-
'REGION': 'north_america', 'SEQUENTIAL_NUMBER': '798034', 'FEWER_THAN_500_PER_YEAR': False},
14+
'REGION': 'north_america', 'SEQUENTIAL_NUMBER': '798034', 'FEWER_THAN_500_PER_YEAR': False,
15+
'nhtsa.model' : 'Ram',
16+
'epa.model' : 'Ram 1500 Pickup 2WD', 'epa.co2TailpipeGpm': '592.4666666666667',
17+
},
1718

1819
# http://www.vindecoder.net/?vin=1D7RB1CT1BS488952&submit=Decode
1920
{'VIN': '1D7RB1CT1BS488952', 'WMI': '1D7', 'VDS': 'RB1CT1', 'VIS': 'BS488952',
2021
'MODEL': 'Ram 1500', 'MAKE': 'Dodge', 'YEAR': 2011, 'COUNTRY': 'United States',
21-
'REGION': 'north_america', 'SEQUENTIAL_NUMBER': '488952', 'FEWER_THAN_500_PER_YEAR': False},
22+
'REGION': 'north_america', 'SEQUENTIAL_NUMBER': '488952', 'FEWER_THAN_500_PER_YEAR': False,
23+
'nhtsa.model' : 'Ram',
24+
'epa.model' : 'Ram 1500 Pickup 2WD', 'epa.co2TailpipeGpm': '555.4375',
25+
},
2226

2327
# http://www.vindecoder.net/?vin=19UUA65694A043249&submit=Decode
2428
# http://acurazine.com/forums/vindecoder.php?vin=19UUA65694A043249
2529
{'VIN': '19UUA65694A043249', 'WMI': '19U', 'VDS': 'UA6569', 'VIS': '4A043249',
2630
'MODEL': 'TL', 'MAKE': 'Acura', 'YEAR': 2004, 'COUNTRY': 'United States',
27-
'REGION': 'north_america', 'SEQUENTIAL_NUMBER': '043249', 'FEWER_THAN_500_PER_YEAR': False},
31+
'REGION': 'north_america', 'SEQUENTIAL_NUMBER': '043249', 'FEWER_THAN_500_PER_YEAR': False,
32+
'nhtsa.model' : 'TL',
33+
'epa.model' : 'TL', 'epa.co2TailpipeGpm': '423.1904761904762',
34+
},
2835

2936
# http://www.vindecoder.net/?vin=19XFB4F24DE547421&submit=Decode says unknown
3037
# http://www.civicx.com/threads/2016-civic-vin-translator-decoder-guide.889/
38+
# http://honda-tech.com/forums/vindecoder.php?vin=19XFB4F24DE547421
3139
{'VIN': '19XFB4F24DE547421', 'WMI': '19X', 'VDS': 'FB4F24', 'VIS': 'DE547421',
3240
'MODEL': 'Civic Hybrid', 'MAKE': 'Honda', 'YEAR': 2013, 'COUNTRY': 'United States',
33-
'REGION': 'north_america', 'SEQUENTIAL_NUMBER': '547421', 'FEWER_THAN_500_PER_YEAR': False},
41+
'REGION': 'north_america', 'SEQUENTIAL_NUMBER': '547421', 'FEWER_THAN_500_PER_YEAR': False,
42+
'nhtsa.model' : 'Civic',
43+
'epa.model' : 'Civic Hybrid', 'epa.co2TailpipeGpm': '200.0',
44+
},
3445

3546
# http://www.vindecoder.net/?vin=1FAHP3FN8AW139719&submit=Decode
3647
{'VIN': '1FAHP3FN8AW139719', 'WMI': '1FA', 'VDS': 'HP3FN8', 'VIS': 'AW139719',
@@ -40,7 +51,10 @@
4051
# http://www.vindecoder.net/?vin=1GKEV13728J123735&submit=Decode
4152
{'VIN': '1GKEV13728J123735', 'WMI': '1GK', 'VDS': 'EV1372', 'VIS': '8J123735',
4253
'MODEL': 'Acadia', 'MAKE': 'GMC', 'YEAR': 2008, 'COUNTRY': 'United States',
43-
'REGION': 'north_america', 'SEQUENTIAL_NUMBER': '123735', 'FEWER_THAN_500_PER_YEAR': False},
54+
'REGION': 'north_america', 'SEQUENTIAL_NUMBER': '123735', 'FEWER_THAN_500_PER_YEAR': False,
55+
'nhtsa.model' : 'Acadia',
56+
'epa.model' : 'Acadia AWD', 'epa.co2TailpipeGpm': '493.72222222222223',
57+
},
4458

4559
# http://www.vindecoder.net/?vin=1GT020CG4EF828544&submit=Decode
4660
{'VIN': '1GT020CG4EF828544', 'WMI': '1GT', 'VDS': '020CG4', 'VIS': 'EF828544',

tests/test_epa.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# -*- coding: utf-8 -*-
2+
from nose.tools import assert_equals, assert_true, raises
3+
4+
# To run tests that depend on network, do e.g. 'NETWORK_OK=1 nose2'
5+
import os
6+
if not 'NETWORK_OK' in os.environ:
7+
print "skipping network tests; set NETWORK_OK=1 to run"
8+
else:
9+
from libvin.epa import EPAVin
10+
from libvin.static import *
11+
from . import TEST_DATA
12+
13+
# Cache responses for 7 days to be kind to EPA's and nhtsa's servers
14+
import requests_cache
15+
requests_cache.install_cache('libvin_tests_cache', expire_after=7*24*60*60)
16+
17+
class TestEPA(object):
18+
19+
def test_model(self):
20+
for test in TEST_DATA:
21+
v = EPAVin(test['VIN'])
22+
if not 'nhtsa.model' in test:
23+
continue
24+
print "Testing nhtsaModel of %s - %s" % (test['VIN'], v.nhtsaModel)
25+
assert_equals(v.nhtsaModel, test['nhtsa.model'])
26+
if not 'epa.model' in test:
27+
continue
28+
print "Testing model of %s - %s" % (test['VIN'], v.model)
29+
assert_equals(v.model, test['epa.model'])
30+
31+
def test_co2(self):
32+
for test in TEST_DATA:
33+
v = EPAVin(test['VIN'])
34+
if not 'epa.co2TailpipeGpm' in test:
35+
continue
36+
co2 = v.eco['co2TailpipeGpm']
37+
print "Testing co2 of %s - %s" % (test['VIN'], co2)
38+
assert_equals(co2, test['epa.co2TailpipeGpm'])

0 commit comments

Comments
 (0)