Receive fuel prices through dynamic items and save them

Hi there,

i´m currently using the http binding to extract the current fuel prices from the website of a german fuel company.
It´s one channel per gas station that extracts the price of a specific fuel from JSON.
This works really good and i´m able to get instant notifications when a certain price is reached.

Sometimes i´m away from home and would like to check + track fuel prices at my location.

The setup looks like this.
Thing with base URL, Channel with fuel station ID and item that stores the price.
Thing and Channels are configured in the UI, while the items are currently text based.

Thing:

UID: http:url:tanken
label: Fuel Station
thingTypeUID: http:url
configuration:
  authMode: BASIC
  ignoreSSLErrors: false
  baseURL: https://api.fuelstation.oilcompany.de/
  delay: 0
  stateMethod: GET
  refresh: 60
  commandMethod: GET
  contentType: application/json
  timeout: 3000
  bufferSize: 2048

Channel:

channels:
  - id: LocalStationRON102
    channelTypeUID: http:string
    label: Local Station RON 102
    description: ""
    configuration:
      mode: READONLY
      stateExtension: /api/v3/stations/12345/prices
      stateTransformation:
        - JSONPATH:$.data.prices.RON102∩JS:divide100.js

Is it somehow possible to create or modify a Channel from a rule?

I already implemented some rules that control items through Telegram messages.
For example the fuel price at or under i get a notification.
I just send a message to the Telegram bot that contains FuelPrice;1.909 that changes an item.

My idea would be a rule that can change the ID within the Channel configuration.
stateExtension: /api/v3/stations/12345/prices

I would create a new item that holds the ID and then need a rule that can change the stateExtension with the new ID when the ID is changed.

Is this possible?

kind regards
Michael

Raspberry Pi 5 - 8 GB RAM
Debian GNU/Linux 12 (bookworm)
Linux 6.1.0-rpi7-rpi-2712
openHAB 4.3.0

Not an answer, but an alternative:

Unfortunately not, because Tankerkönig doesn´t publish the RON 102 prices.
RON 102 isn´t part of the MTS-K and so it´s not part of the automatic price notification.
That´s the reason why i directly get the prices from the company that offers RON 102.

I already checked the REST API and it could be possible to modify a channel.
Currently i´ve got no idea how to build a rule that can utilize the REST API.

Not that I’m aware of. But since we are talking about the HTTP binding, I think this is an XY Problem. Since you need the rule anyway, why not use the sendHttpGetRequest action from the rule to fetch the data using the location Item to build the URL to fetch? It’s going to be way easier than dynamically creating/modifying Thing Channels through the REST API, which is indeed possible, but you’ll have to use the sendHttpXRequest actions from the rule anyway.

There’s no requirement to use the HTTP binding for this. Just fetch and parse the data from the rule directly.

If you are using other than Rules DSL, see the docs for what ever language you are using for how to invoke the HTTP Actions in that rule language.

1 Like

This is easy to do in jruby. But as @rlkoshak said, the whole set up could probably be done in a much more straight forward manner, with far less code / things / items using just a simple rule to fetch the data and send you the notification.

1 Like

Thanks for the input!
I made a quick draft in my test rule and it seems to work up until the transformation.

    val output = sendHttpGetRequest("https://api.fuelstation.oilcompany.de/api/v3/stations/12345/prices", 1000)
    val rawprice = transform("JSONPATH", "$.data.prices.RON102", output) as DecimalType
    val Number price = rawprice/100

I can get the JSON and even the value i want, but i can´t divide it by 100.
The API gives me the price as 207.90 instead of 2.079.
In my approach with a HTTP channel i just use the JS transformation.
∩JS:divide100.js

How can i do the same thing in a rule?
I already tried the simple method of getting the JSON result as DecimalType and then divide it.
val Number price = rawprice/100
That was not successful :frowning:

Cannot cast from String to DecimalType
Script execution of rule with UID 'Testswitch-1' failed: Could not cast 207.90 to org.openhab.core.library.types.DecimalType; line 26, column 20, length 68 in Testswitch

Edit: Got it :slight_smile:

val output = sendHttpGetRequest("https://api.fuelstation.oilcompany.de/api/v3/stations/12345/prices", 1000)
val rawprice = transform("JSONPATH", "$.data.prices.RON102", output)
val Number price = transform("JS", "divide100.js", rawprice)

Transformations always return a String. You need to parse that into a Number and then divide.

This line is likely to cause problems later as just because you declared the variable as val Number price, you’ve assigned it a String so Rules DSL just ignores the Number.

It’s one of the many reasons I recommend against the use of Rules DSL for new development or rules. It’s no longer easier to use than the alternatives and it has a lot of problems around type handling that make it a poor choice.

A better approach in Rules DSL would be:

val output = sendHttpGetRequest("https://api.fuelstation.oilcompany.de/api/v3/stations/12345/prices", 1000)
val rawprice = new BigDecimal(transform("JSONPATH", "$.data.prices.RON102", output))
val price = rawPrice/100

In JS and jRuby you don’t even need the call to JSONPATH because both can handle JSON natively.

JS

val price = JSON.parse(sendHttpGetRequest("https://api.fuelstation.oilcompany.de/api/v3/stations/12345/prices", 1000)).data.prices.RON102 / 100

Assuming the value in the JSON isn’t in quotes, the JSON parser will know it’s a number, so no additional parsing to get at the number is required.

I’ve shown it as a one-liner, but you’d probably want to separate it so you can add some error checking and meaningful error log statements when things go wrong.

1 Like

You may know my answer, i´m no developer and happy to achieve my rules in openhab :smiley:

Next problem, i can´t use postUpdate to get the result stored into an item.
Script execution of rule with UID 'Testswitch-1' failed: An error occurred during the script execution: Could not invoke method: org.openhab.core.model.script.actions.BusEvent.postUpdate(org.openhab.core.items.Item,java.lang.Number) on instance: null in Testswitch

val output = sendHttpGetRequest("https://api.fuelstation.oilcompany.de/api/v3/stations/12345/prices", 1000)
val rawprice = transform("JSONPATH", "$.data.prices.RON102", output)
val Number price = transform("JS", "divide100.js", rawprice)
ManualStation1.postUpdate(price)

That error usually means Rules DSL can’t figure out how to cast something into a format that it understands. Often the solution is to convert what ever you are passing to postUpdate to a string by calling toString

ManualStation1.postUpdate(price.toString)

I suspect by trying to force the price to be a Number but actually giving it a String Rules DSL is getting confused. Again, showing problems with Rules DSL and why it’s no longer fit for use.

1 Like

Thank you Rich!
I was trying a few things but not to use toString when i want to update a item of the type Number :-o

So it´s finally working and i´m writing a rule to update the prices.

val output = sendHttpGetRequest("https://api.fuelstation.oilcompany.de/api/v3/stations/12345/prices", 1000)
val rawprice = transform("JSONPATH", "$.data.prices.RON102", output)
val Number price = transform("JS", "divide100.js", rawprice)
ManualStation1.postUpdate(price.toString)

I know we´re drifting away from the initial question of this thread.

Currently i got the price extraction part working but i´ve got some problems with the notification.

I got two items per gas station and three gas stations in total.

Gas1_ID = Holds the ID of the gas station
Gas1_Price = Receives the price of the gas station based on the ID
Gas1_Name = User filled name of the gas station

The price items are part of the group and a change of the group will trigger a rule to check the current price against my target price.
Now i want to extract the name from the state of the _Name item.
I already did something similiar for my Alexa speakers to send the answer to the correct device.

val SourceStation = triggeringItem.name.split("_").get(0)
val BuildStation = SourceStation + "_Name"
val pStation = BuildStation.state

This works for sendCommand but not for the .state to get the state of this item.

Script execution of rule with UID 'tankAlarm-2' failed: 'state' is not a member of 'item'; line 56, column 69, length 18 in tankAlarm

Nope, and this is yet another area that ll the other options for rules languages work better than Rules DSL.

See Design Pattern: Associated Items for how to import the ItemRegistry so you can pull the Item Object into your rule with just the name. The only way to get the state of the Item is to have the actual Item Object. You can’t get the state using just the name.

In the other rules languages, the ItemRegistry is already imported ,and it’s very straight forward to get at an Item even if all you have is its name.

In JS:

var buildStation = event.itemName.replace("ID", "Name");
var pStation = items[buildStation].state

Thanks for the hint.

import org.eclipse.smarthome.model.script.ScriptServiceUtil // Outside the rule as the first line of the .rules file

val SourceStation = triggeringItem.name.split("_").get(0)
var buildStation = SourceStation + "_Name"
var pStation = ScriptServiceUtil.getItemRegistry.getItem(buildStation).state

Script execution of rule with UID 'tankAlarm-2' failed: The name 'import' cannot be resolved to an item or type; line 39, column 5, length 6 in tankAlarm

Edit: Found the correct import :slight_smile:
import org.openhab.core.model.script.ScriptServiceUtil

To update and finish this thread with a solution.
This is my final rule to solve the “problem”.

I thought it would be a better approach to use Channels instead of rules but that wasn´t correct.
Thanks to Rich for the help and identify the XY problem :slight_smile:

GasStations.items

Group GasStations
Group StationNames
Group StationIDs

Station1_Name (StationNames)
Station1_ID (StationIDs)
Station1_Price (GasStations)

Station2_Name (StationNames)
Station2_ID (StationIDs)
Station2_Price (GasStations)

Station3_Name (StationNames)
Station3_ID (StationIDs)
Station3_Price (GasStations)

FuelPrices.rules

import org.openhab.core.model.script.ScriptServiceUtil
val String ruleId = "RON102Preise"

rule "Update of dynamic fuel prices"
when
    Time cron "0 0/1 * * * ?" // every minute
then

    StationIDs.members.filter[i | i.state != NULL].forEach[ i | 
        val String stationPriceItem = i.name.split("_").get(0) + "_Preis"
        val priceItem = ScriptServiceUtil.getItemRegistry.getItem(stationPriceItem)

        try {
            val String url = "https://api.fuelstation.oilcompany.de/api/v3/stations/" + i.state.toString + "/prices"
            val String output = sendHttpGetRequest(url, 1000)
            if (output === null) {
                logError(ruleId, "Could not receive price from fuel station " + i.state + "\nOutput: " + output)
                return;
            }
            val rawprice = transform("JSONPATH", "$.data.prices.RON102", output)

            if (rawprice != NULL) {
                val Number price = transform("JS", "divide100.js", rawprice)
                val formattedPrice = Double::parseDouble(price)

                if (price != NULL && formattedPrice >= 1.000) {
                    priceItem.postUpdate(price.toString)
                } else {
                    logInfo(ruleId, "Error: Could not calculate price for gas station with ID " + i.state)                }
            } else {
                logError(ruleId, "Error: Could not receive a price for gas station with ID " + i.state)
            }
        } catch (Exception e) {
            logError(ruleId, "Error while receiving the price for gas station with ID " + i.state + ": " + e.message)
        }
    ]

end