Parsing a CSV file with Python
In general I think this code looks excellent.  You have made good use of comprehensions.  I have two suggestions: 

More Comprehensions:

This:

def _get_tank_names(tanks=None, data=None):
    names = list()
    for n in tanks:
        for tank_dict in data:
            if tank_dict['Tank'] == str(n):
                names.append(tank_dict['Product'])
                break
return names


Can be expressed as:    

def _get_tank_names(tanks=None, data=None):
    tanks = {str(n) for n in tanks}
    return {t['Product'] for t in data if t['Tank'] in tanks}


This converts tanks to a set while performing the str() conversion, to clean things up a bit.  Then the result can be generated in a single statement.

Gathering like elements

So this entire section:

# Create new row template
tank_names = _get_tank_names(tanks=tanks, data=d1)
row_template = {key: None for key in (tank_names + [primary_key])}

# New rows from row template
d4 = []
for row in d3:
    # update new row with available keys
    new_row = {key: row.get(key) for key in row_template}

    # update new row with matching values
    for key in new_row:
        if key in row.values():
            new_row[key] = row[data_key]

    # remove keys with None value
    new_row = {k: v for k, v in new_row.items() if v is not None}
    d4.append(new_row)

# Merge all rows based on Time key
merged = {}
for row in d4:
    if row[primary_key] in merged:
        merged[row[primary_key]].update(row)
    else:
        merged[row[primary_key]] = row

return [value for value in merged.values()]


Can be reduced down to:        

merged = {}
for row in d3:
    merged.setdefault(row['Time'], {'Time': row['Time']}) \
        [row['Product']] = row['Volume']

return [v for k, v in sorted(merged.items())]


The cornerstone here is using dict.setdefault to accumulate the records that match the timestamp.

So how does the setdefault work?

We create an empty dict, the keys to which will be the timestamps we will be accumulating data for.

merged = {}


Loop through the data, a row at a time:

for row in d3:


Look up the element in our dict corresponding to the timestamp for this row:

merged.setdefault(row['Time'], {}) 


This is most of the magic, it will populate merged[this rows timestamp] with an empty dict if one is not already there, and then return the dict, so that we can put something in it.  An empty dict would often be sufficient, but in this case you also need the timestamp in the final dict, so we can extend the statement to be:

merged.setdefault(row['Time'], {'Time': row['Time']})


This will return a dict with the timestamp already populated, the first time we see that timestamp.  In subsequent loops when we see the timestamp, it will return the dict that was built the previous time we saw the timestamp.

To finish the dict creation code we set the Product and Volume data into the dict with:

merged.setdefault(row['Time'], {'Time': row['Time']}) \
    [row['Product']] = row['Volume']


Lastly, return the list of dicts we built, sorted by the timestamp with:

return [v for k, v in sorted(merged.items())]


Ain't Python fun? :-)