Skip to content

Commit 21a5dcb

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

4 files changed

Lines changed: 298 additions & 12 deletions

File tree

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