Fuel/Gas Prices integration

I do a “Integration” of Andorra Fuel/Gas prices with command line. If it’s helpful.

1 Like

Looks like that might use some Ajax calls so could potentially add as a source

If only I could. Perhaps I should lobby my MP to make it mandatory that all fuel retailers upload their data!

Good to see that Costco is now included though so thanks for that!

1 Like

Ask your MP to get the CMA to hurry up and make a decision about what they want to do lol, the public need good quality fuel price data in an accessible way, which will drive competition in the market. You are more likely to go to that Shell garage at 136.9 than the other Shell garage a mile away costing 154.9.

Plus for towns etc it could boost local economy… all a win win, but I guess the usual gov red tape gets in the way. Anyway too much politics now for my liking haha.

1 Like

Hi all.

Got this up and running for my home location. Works great!

Working on adding a similar list but using my phone location for lat/long.
The plan would be to load up the dashboard and call the script from a refresh button or maybe update it periodically.

I’ve got the script working and returning the correct information, but I can’t figure out how to get the script response into an entity that I can use on the dashboard.
I’m guessing I should set the value of templated device in the script, but can’t wrap my head around it!

Script below:

find_fuel:
  sequence:
    - data:
        type: E10
        location:
          latitude: |
            {{ state_attr('device_tracker.my_phone', 'latitude') | float }}
          longitude: |
            {{ state_attr('device_tracker.my_phone', 'longitude') | float }}
          radius: |
            {{ 10000 }}
      response_variable: data
      action: fuel_prices.find_fuels
    - if:
        - condition: template
          value_template: "{{ (data.fuels | count) > 0 }}"
      then:
        - variables:
            response: |
              {{ { 'value': data.fuels } }}
        - stop: ''
          response_variable: response

This returns the following if I call the script from developer tools (truncated for ease!)

value:
  - id: asda_gcjt9rht7c4s
    name: Asda CF40 2JQ
    address: Colliers Way,, Tonypandy
    postal_code: CF40 2JQ
    latitude: 51.62609
    longitude: -3.45367
    brand: Asda
    available_fuels:
      E10: 1.317
      B7: 1.4069999999999998
    fuel_details:
      E10: {}
      B7: {}
    currency: GBP
    last_updated: "2025-01-12T16:13:16.383004"
    next_update: "2025-01-13T16:13:16.335234"
    props:
      source: asda
      source_id: gcjt9rht7c4s
      prevent_cache_cleanup: true
    distance: 4.263773178123694
    cost: 1.317
  - id: motorfuelgroup_gcjtbdjwg42k
    name: Texaco CF41 7PN
    address: MFG Ton Pentre, Ystrad Road, Pentre
    postal_code: CF41 7PN
    latitude: 51.6479
    longitude: -3.48597
    brand: Texaco
    available_fuels:
      E10: 1.339
      E5: 1.5590000000000002
      B7: 1.429
      SDV: 1.599
    fuel_details:
      E10: {}
      E5: {}
      B7: {}
      SDV: {}
    currency: GBP
    last_updated: "2025-01-12T16:13:16.466836"
    next_update: "2025-01-13T16:13:16.432061"
    props:
      source: motorfuelgroup
      source_id: gcjtbdjwg42k
      prevent_cache_cleanup: true
    distance: 3.342210286266886
    cost: 1.339

Appreciate any help you can offer. :slight_smile:

Hi,

Trigger templates are what you are after. There is lots of info and examples on the forms. But as a starting point:

I will post an example of what I use for tracking fuel prices around my car (Traccar).

Thanks @11harveyj.

I thought it would be something like this, but I still can’t wrap my head around how it’s supposed to work!

Any examples much appreciated!

Here you go, basically this updates whenever sensor.entity_with_location moves or when home assistant starts, or when I change a input boolean to use cheapest fuels (if off it will use nearest) or finally whenever a button is pressed. This is for your configuration.yaml file under the template key.

The service call expects the location in meters (because thats how the selector works in the UI), so I’ve done 5 * 1609.34 for 5 miles.

- trigger: 
    - platform: state
      entity_id:
        - sensor.entity_with_location
      attribute: longitude
    - platform: state
      entity_id:
        - sensor.entity_with_location
      attribute: latitude
    - platform: homeassistant
      event: start
    - platform: state
      entity_id:
        - input_button.car_update_nearest_fuels
        - input_boolean.car_use_cheapest_station
  action:
    - service: fuel_prices.find_fuel_station
      data:
        location:
          latitude: |
            {{ state_attr('sensor.entity_with_location', 'latitude') | float }}
          longitude: |
            {{ state_attr('sensor.entity_with_location', 'longitude') | float }}
          radius: |
            {{ 5 * 1609.34 }}
      response_variable: data
    - variables:
        data: >
          {% if states('input_boolean.car_use_cheapest_station') == 'on' %}
            {{ data['items'] | sort(attribute='available_fuels.B7') }}
          {% else %}
            {{ data['items'] | sort(attribute='distance') }}
          {% endif %}
  sensor:
    - name: Car Nearest Fuel Station
      unique_id: Car Nearest Fuel Station
      state: |
        {{ data[0].available_fuels.B7 | float }}
      availability: |
        {{ data | count > 0 }}
      state_class: total
      device_class: monetary
      unit_of_measurement: GBP
      attributes:
          other_stations: |
            {{ data }}
          latitude: |
            {{ data[0].latitude }}
          longitude: |
            {{ data[0].longitude }}
          name: |
            {{ data[0].name }}
          station: |
            {{ data[0] }}

If all goes well you should get an entity like this:

I’m only interested in diesel so have selected B7 as the state, but you can change that to whatever fuel type you like (E10, E5 etc.)

If you want multiple sensors, just add more to the bottom under the sensor key. All of the data from the service call will still be available under the data variable.

Looks great! :slight_smile:
Won’t quite work for me unfortunately. Having a list of prices and postcodes is a necessity!

I’ve managed to get it working, but can’t get it styled nicely. :frowning:

Managed to get it working and looking nice too (both with a little help from ChatGPT!!)

This can be updated by clicking/tapping the header, and returns a list of petrol stations within a 10 mile radius of whatever GPS device you’re using (my phone in this case)
Clicking/tapping on the station opens a link in Google Maps.

If anyone’s interested in how I did it, let me know!

1 Like

I’ve got a docs site, I can publish your solution (crediting you of course) onto there if you like?

I’ll put it all together later. There are quite a few requirements to getting it to work!
There’s probably a more elegant solution too…

2 Likes

Do you mind sharing your code for this card?

Nearby Stations

Some prerequisites:

  1. You will need the python scripts integration running.
    Add the following to your configuration.yaml if it’s not already there.
python_script:
  1. For the Lovelace card, you will need the Lovelace HTML Jinja2 Template card.
    Download here: https://github.com/PiotrMachowski/Home-Assistant-Lovelace-HTML-Jinja2-Template-card
    Or install via HACS.

Once that’s done, we need to create two input_text entities to handle the fuel station info. I split the post/zip code into a separate entity to increase the number of results before hitting the 255 character limit.

Add the following to configuration.yaml

input_text: #no need to duplicate this if it already exists!
  fuel_prices_data:
    name: Fuel Prices Data
    initial: Reloaded
    max: 255

  fuel_postcodes:
    name: Fuel Postcodes
    initial: Reloaded
    max: 255

We also need to add a template sensor to the template section of configuration.yaml.

template: #no need to duplicate this if it already exists!
  - sensor:
    - name: "Fuel Price List"
      state: >
        {% set fuel_list = states('input_text.fuel_prices_data') or 'No fuel data available' %}
        {{ fuel_list  }}
      attributes:
        formatted_list: >
          {% set fuels = states('input_text.fuel_prices_data') %}
          {% if fuels %}
            {% if fuels != 'reloading'%}
              {% set postcodes = states('input_text.fuel_postcodes') %}
              {% set fuels = fuels.split(', ') %}
              {% set postcodes = postcodes.split(',') %}
              {% for fuel in fuels %}
                {% set index = loop.index0 %}
                {% set parts = fuel.split(': ') %}
                {% set parts2 = parts[0].split(' - ') %}
                {% set fuel_name = parts2[1].replace(' ', '+') %}
                {% set postcode = postcodes[index].replace(' ', '+') %}
                {% set query = fuel_name ~ '+' ~ postcode %}

                <div class="fuelrow" style="width: 100%; display: flex; align-items: center; padding: 0px; margin-bottom: 8px;">
                  <span style="color: rgb(68, 115, 158); height: 40px; width: 40px; line-height: 40px; text-align: center;"><ha-icon icon="mdi:gas-station"></ha-icon></span>
                  <span style="margin-left: 16px;"><a href="https://www.google.com/maps/search/?api=1&query={{ query }} petrol" target="_blank" style="text-decoration: none; color: inherit;">{{ parts[0] }}</a></span><span style='margin-left: auto;'>£{{ parts[1] }}</span>
                </div>
              {% endfor %}
            {% else %}
              <div>Searching...</div>
            {% endif %}
          {% else %}
            <div>No fuel data available.</div>
          {% endif %}

Then add the following to scripts.yaml, set the fuel type and radius, and also change the device tracker to whatever device you are using.

find_fuel:
  sequence:
    - action: input_text.set_value
      target:
        entity_id: input_text.fuel_prices_data
      data:
        value: reloading
    - data:
        type: E10 #change to whatever fuel type you need
        location:
          latitude: |
            {{ state_attr('device_tracker.your_device', 'latitude') | float }}
          longitude: |
            {{ state_attr('device_tracker.your_device', 'longitude') | float }}
          radius: | 
            {{ 10 * 1609.34 }}
          #change 10 to whatever radius you want in miles.
      response_variable: data
      action: fuel_prices.find_fuels
    - if:
        - condition: template
          value_template: "{{ (data.fuels | count) > 0 }}"
      then:
        - variables:
            response: |
              {{ { 'value': data.fuels } }}
        - service: python_script.set_fuel_prices
          data:
            fuels: "{{ data.fuels }}"
        - stop: ''
          response_variable: response

Last step is to save the following as set_fuel_prices.py in the python_scripts folder in your config folder.
This iterates through the results from fuel_prices.find_fuels and sends the results to the text_inputs we set up earlier.

fuels = data.get('fuels', [])

fuel_strings = [f"{fuel['distance']:.1f} mi - {fuel['brand']}: {fuel['cost']:.2f}" for fuel in fuels]
fuel_postcodes = [f"{fuel['postal_code']}" for fuel in fuels]
max_length = 255

restricted_fuel_strings = []
final_fuel_postcodes = []
length_so_far = 0

i = 0

for fuel_string in fuel_strings:
    additional_length = len(fuel_string) + (2 if restricted_fuel_strings else 0)
    if length_so_far + additional_length <= max_length:
        restricted_fuel_strings.append(fuel_string)
        final_fuel_postcodes.append(fuel_postcodes[i])
        length_so_far += additional_length
        i += 1
    else:
        break

fuel_data_str = ", ".join(restricted_fuel_strings)
fuel_postcode_str = ",".join(final_fuel_postcodes)

hass.services.call('input_text', 'set_value', {
    'entity_id': 'input_text.fuel_prices_data',
    'value': fuel_data_str
})

hass.services.call('input_text', 'set_value', {
    'entity_id': 'input_text.fuel_postcodes',
    'value': fuel_postcode_str
})

Restart everything and head over to your dashboard.
Add the following card:

type: grid
cards:
  - type: heading
    icon: mdi:gas-station
    heading: Nearby
    heading_style: title
    tap_action:
      action: perform-action
      perform_action: script.find_fuel
      target: {}
  - type: custom:html-template-card
    content: |
      {{ state_attr('sensor.fuel_price_list', 'formatted_list') }}

And hopefully, that should do it.
Press the “Nearby” header to reload the list. Tap a result to open directions in Google Maps.

By default, results are ordered by price. I’ve not managed to figure out how to sort by distance yet, but I’m sure it’s pretty straight-forward if you know how!

I’m also sure there is room for improvement here.
We could add options to the frontend to choose fuel type and to order by distance, price, etc. That should all be pretty easy. :slight_smile:

1 Like

After copy/pasting the above I get the error:

Failed to perform the action script/find_fuel. Template output exceeded maximum size of 262144 characters

Any ideas on what the problem is? Thanks.

It’s probably the Fuel Price List template sensor going over the maximum size. Not something I’d encountered, or even knew existed to be honest!

Let’s try taking out the Google Maps link, see if that works.

Replace the template sensor in configuration with the following:

template:
  - sensor:
    - name: "Fuel Price List"
      state: >
        {% set fuel_list = states('input_text.fuel_prices_data') or 'No fuel data available' %}
        {{ fuel_list  }}
      attributes:
        formatted_list: >
          {% set fuels = states('input_text.fuel_prices_data') %}
          {% if fuels %}
            {% if fuels != 'reloading'%}
              {% set postcodes = states('input_text.fuel_postcodes') %}
              {% set fuels = fuels.split(', ') %}
              {% set postcodes = postcodes.split(',') %}
              {% for fuel in fuels %}
                {% set index = loop.index0 %}
                {% set parts = fuel.split(': ') %}
                {% set parts2 = parts[0].split(' - ') %}
                {% set fuel_name = parts2[1].replace(' ', '+') %}
                {% set postcode = postcodes[index].replace(' ', '+') %}
                {% set query = fuel_name ~ '+' ~ postcode %}

                <div class="fuelrow" style="width: 100%; display: flex; align-items: center; padding: 0px; margin-bottom: 8px;">
                  <span style="color: rgb(68, 115, 158); height: 40px; width: 40px; line-height: 40px; text-align: center;"><ha-icon icon="mdi:gas-station"></ha-icon></span>
                  <span style="margin-left: 16px;">{{ parts[0] }}</span><span style='margin-left: auto;'>£{{ parts[1] }}</span>
                </div>
              {% endfor %}
            {% else %}
              <div>Searching...</div>
            {% endif %}
          {% else %}
            <div>No fuel data available.</div>
          {% endif %}

If that works, but you want the map link in there, we’ll figure something out. :slight_smile:

Replaced the syntax but same error result. Thanks.

Hmmmm…
Can you run the script from Developer Tools and paste the output here?

I have run ‘Scripts.find_fuel’ and get the same error message

Failed to perform the action script/find_fuel. Template output exceeded maximum size of 262144 characters

I would say the template engine is internally rendering the template into text before it’s being sent to the python script.

You might want to try lowering your radius, I know there are some providers who give us loads of attributes, and other providers who give us hardly any.

I think the contents of the HA script could probably done inside the python script though to eliminate that step entirely.