openHAB and Tankerkönig gas prices + Telegram integration

Posted by ads' corner on Saturday, 2020-06-27
Posted in [Ansible][Linux][Openhab]

In Germany, every gas station must report gas prices online, to the Markttransparenzstelle für Kraftstoffe. Businesses and users can then fetch this data and provide services. One of these companies is Tankerkönig, and there’s also a Binding for openHAB.

If you are interested in gas prices for certain gas stations, you need to sign up for a (free) API key, and figure out the IDs of the gas stations. For that go to this website, Position the blue marker on the location you are interested in, and then click on all the gas stations you want to include. Finally click on Tankstellen übernehmen - this will open a JSON with the data, from there extract the id (and possibly the other data as well).

Binding

As usual I’m deploying everything using Ansible, which makes my life here so much easier. Let’s start by installing the Binding:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
- name: Get list of available and installed extensions
  uri:
    url: "http://{{ ansible_host }}:8080/rest/extensions"
  register: oh2_extensions
  changed_when: false

- name: Install extensions
  uri:
    url: "http://{{ ansible_host }}:8080/rest/extensions/{{ item }}/install"
    method: POST
  when: "not (oh2_extensions.json|byattr('id', item))[0].installed"
  with_items:
    - binding-tankerkoenig
  register: oh2_install_extensions

This can of course be also done by using the Paper UI, but this way I can reinstall the openHAB system any time I want.

Things

Now that I have a list of IDs, I put them into a list in Ansible, and use a loop to create the tankerkoenig.things file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{% set tankerkoenig = [('Station1', 'Aral 1', 'ab1cdef', '52.000000', '13.000000'),
                       ('Station2', 'Shell 1', 'ab2cdef', '52.000000', '13.000000'),
                       ('Station3', 'Esso 1', 'ab3cdef', '52.000000', '13.000000')
                      ] %}

Bridge tankerkoenig:webservice:Tankpreise "Tankpreise" [ apikey="{{ lookup('file', playbook_dir + '/credentials/tankerkoenig.txt') }}", refresh=10, modeOpeningTime=true ] {
{% for tk in tankerkoenig %}
    // "lat": {{ tk[3] }}, "lon": {{ tk[4] }}
    Thing station {{ tk[0] }} "{{ tk[1] }}" @ "GasStations"[ locationid = "{{ tk[2] }}" ]

{% endfor %}

}

The $tankerkoenig variable holds a list with the data from the JSON. Each list element includes the following data:

  • internal station name as identifier used internally in openHAB (no spaces ect allowed)
  • Station name: the official station name
  • Station ID: the id from the Tankerkönig JSON
  • latitude
  • longitude

I’m storing the coordinates as well, even though I’m not yet using them. Who knows what other ideas I have in the future …

The API key is included using an Ansible Lookup, from a directory on my Ansible host.

The resulting /etc/openhab2/things/tankerkoenig.things:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Bridge tankerkoenig:webservice:Tankpreise "Tankpreise" [ apikey="<your API key goes here>", refresh=10, modeOpeningTime=true ] {
    // "lat": 52.000000, "lon": 13.000000
    Thing station Station1 "Aral 1" @ "GasStations"[ locationid = "ab1cdef" ]

    // "lat": 52.000000, "lon": 13.000000
    Thing station Station2 "Shell 1" @ "GasStations"[ locationid = "ab2cdef" ]

    // "lat": 52.000000, "lon": 13.000000
    Thing station Station3 "Esso 1" @ "GasStations"[ locationid = "ab3cdef" ]
}

Items

The Binding provides one global Item, the holiday channel. That is a Switch which indicates if today is a holiday. For every gas station in the list, there are four different channels: e10, e5, diesel and station_open. The .items file needs a couple new channels, and I’m rolling this out using an Ansible Template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Group GasStationPrices_E10
Group GasStationPrices_E5
Group GasStationPrices_Diesel
Group GasStation_Open

Switch		Station_Holidays		"Today is holiday: [%s]"	{ channel="tankerkoenig:webservice:Tankpreise:holiday"}

{% for tk in tankerkoenig %}
// Station: {{ tk[1] }}
Number		TK_{{ tk[0] }}_E10		"E10 [%.3f €]"		(GasStationPrices_E10)		{ channel="tankerkoenig:station:Tankpreise:{{ tk[0] }}:e10" }
Number		TK_{{ tk[0] }}_E5		"E5 [%.3f €]"		(GasStationPrices_E5)		{ channel="tankerkoenig:station:Tankpreise:{{ tk[0] }}:e5" }
Number		TK_{{ tk[0] }}_Diesel		"Diesel [%.3f €]"	(GasStationPrices_Diesel)	{ channel="tankerkoenig:station:Tankpreise:{{ tk[0] }}:diesel"}
Contact		TK_{{ tk[0] }}_Open		"Station is [%s]"	(GasStation_Open)		{ channel="tankerkoenig:station:Tankpreise:{{ tk[0] }}:station_open"}

{% endfor %}

Using a template makes it easy to make any modifications. The resulting /etc/openhab2/items/tankerkoenig.items file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Group GasStationPrices_E10
Group GasStationPrices_E5
Group GasStationPrices_Diesel
Group GasStation_Open

Switch          Station_Holidays                                "Today is holiday: [%s]"        { channel="tankerkoenig:webservice:Tankpreise:holiday"}

// Station: Aral 1
Number          TK_Aral1_E10                   "E10 [%.3f €]"          (GasStationPrices_E10)          { channel="tankerkoenig:station:Tankpreise:Aral1:e10" }
Number          TK_Aral1_E5                    "E5 [%.3f €]"           (GasStationPrices_E5)           { channel="tankerkoenig:station:Tankpreise:Aral1:e5" }
Number          TK_Aral1_Diesel                        "Diesel [%.3f €]"       (GasStationPrices_Diesel)       { channel="tankerkoenig:station:Tankpreise:Aral1:diesel"}
Contact         TK_Aral1_Open                  "Station is [%s]"       (GasStation_Open)               { channel="tankerkoenig:station:Tankpreise:Aral1:station_open"}

// Station: Shell 1
Number          TK_Shell1_E10                    "E10 [%.3f €]"          (GasStationPrices_E10)          { channel="tankerkoenig:station:Tankpreise:Shell1:e10" }
Number          TK_Shell1_E5                     "E5 [%.3f €]"           (GasStationPrices_E5)           { channel="tankerkoenig:station:Tankpreise:Shell1:e5" }
Number          TK_Shell1_Diesel                 "Diesel [%.3f €]"       (GasStationPrices_Diesel)       { channel="tankerkoenig:station:Tankpreise:Shell1:diesel"}
Contact         TK_Shell1_Open                   "Station is [%s]"       (GasStation_Open)               { channel="tankerkoenig:station:Tankpreise:Shell1:station_open"}

// Station: Esso 1
Number          TK_Esso1_E10                       "E10 [%.3f €]"          (GasStationPrices_E10)          { channel="tankerkoenig:station:Tankpreise:Esso1:e10" }
Number          TK_Esso1_E5                        "E5 [%.3f €]"           (GasStationPrices_E5)           { channel="tankerkoenig:station:Tankpreise:Esso1:e5" }
Number          TK_Esso1_Diesel                    "Diesel [%.3f €]"       (GasStationPrices_Diesel)       { channel="tankerkoenig:station:Tankpreise:Esso1:diesel"}
Contact         TK_Esso1_Open                      "Station is [%s]"       (GasStation_Open)               { channel="tankerkoenig:station:Tankpreise:Esso1:station_open"}

After a couple minutes, the Binding will report ONLINE, and start fetching data.

Telegram

All the data is near worthless when I can’t query it when needed. We already have a Telegram Bot sitting in a channel which reports all kind of events. That bot can also answer queries. On top of that I want the prices sorted, lowest first. That requires a little bit of Java in the .rules file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import java.util.HashMap

rule "Telegram Bot receive tanken"
when
    Item telegramlastMessageDate received update
then
    // no option selected, send back the available options
    if (telegramLastMessage.state.toString.toLowerCase == "/tanken") {
        val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
        telegramAction.sendTelegram(Long::parseLong(telegramLastMessageChatId.state.toString), "Options:\n/tanken e10\n/tanken diesel\n/tanken e5")

    // select and sort Diesel prices, then return the list
    } else if (telegramLastMessage.state.toString.toLowerCase == "/tanken diesel") {
        var reply = "Dieselpreise:"

        val HashMap<String, DecimalType> DieselMap = newHashMap
{% for tk in tankerkoenig %}
        if (TK_{{ tk[0] }}_Diesel.state != NULL && TK_{{ tk[0] }}_Diesel.state != UNDEF && TK_{{ tk[0] }}_Open.state == OPEN) {
            DieselMap.put("{{ tk[1] }}", (TK_{{ tk[0] }}_Diesel.state as DecimalType))
        }
{% endfor %}
        for (Price : DieselMap.entrySet.sortBy[value]) {
            var String key = Price.getKey().toString
            var String value = Price.getValue().toString
            reply = reply + String::format("\n%s: `%s`€", key, value)
        }

        logInfo("Dieselpreise", reply)
        val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
        telegramAction.sendTelegram(Long::parseLong(telegramLastMessageChatId.state.toString), reply)

    // select and sort E10 prices, then return the list
    } else if (telegramLastMessage.state.toString.toLowerCase == "/tanken e10") {
        var reply = "Preise E10:"

        val HashMap<String, DecimalType> E10Map = newHashMap
{% for tk in tankerkoenig %}
        if (TK_{{ tk[0] }}_E10.state != NULL && TK_{{ tk[0] }}_E10.state != UNDEF && TK_{{ tk[0] }}_Open.state == OPEN) {
            logInfo("Tankerkoenig E10 Debug", "{}: {}", "{{ tk[1] }}", TK_{{ tk[0] }}_E10.state.toString)
            E10Map.put("{{ tk[1] }}", (TK_{{ tk[0] }}_E10.state as DecimalType))
        }
{% endfor %}
        for (Price : E10Map.entrySet.sortBy[value]) {
            var String key = Price.getKey().toString
            var String value = Price.getValue().toString
            reply = reply + String::format("\n%s: `%s`€", key, value)
        }

        logInfo("Preise E10", reply)
        val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
        telegramAction.sendTelegram(Long::parseLong(telegramLastMessageChatId.state.toString), reply)

    // select and sort E5 prices, then return the list
    } else if (telegramLastMessage.state.toString.toLowerCase == "/tanken e5") {
        var reply = "Preise E5:"

        val HashMap<String, DecimalType> E5Map = newHashMap
{% for tk in tankerkoenig %}
        if (TK_{{ tk[0] }}_E5.state != NULL && TK_{{ tk[0] }}_E5.state != UNDEF && TK_{{ tk[0] }}_Open.state == OPEN) {
            E5Map.put("{{ tk[1] }}", (TK_{{ tk[0] }}_E5.state as DecimalType))
        }
{% endfor %}
        for (Price : E5Map.entrySet.sortBy[value]) {
            var String key = Price.getKey().toString
            var String value = Price.getValue().toString
            reply = reply + String::format("\n%s: `%s`€", key, value)
        }

        logInfo("Preise E5", reply)
        val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
        telegramAction.sendTelegram(Long::parseLong(telegramLastMessageChatId.state.toString), reply)

    // currently unknown option, send back an error
    } else if (telegramLastMessage.state.toString.length() > 6 && telegramLastMessage.state.toString.toLowerCase.substring(0, 7) == "/tanken") {
        val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
        telegramAction.sendTelegram(Long::parseLong(telegramLastMessageChatId.state.toString), "Unknown option!")
    }
end

Let’s go over the details:

1
import java.util.HashMap

I’m using a hash map in the Rule, need to include the Java library for HashMap.

1
2
3
4
rule "Telegram Bot receive tanken"
when
    Item telegramlastMessageDate received update
then

I’m using the last message date for the trigger, because a user can send the same message multiple times, and that will not trigger an update for the message content.

1
2
3
4
    // no option selected, send back the available options
    if (telegramLastMessage.state.toString.toLowerCase == "/tanken") {
        val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
        telegramAction.sendTelegram(Long::parseLong(telegramLastMessageChatId.state.toString), "Options:\n/tanken e10\n/tanken diesel\n/tanken e5")

If just the /tanken command is sent, send back a list of available gas options.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    // select and sort Diesel prices, then return the list
    } else if (telegramLastMessage.state.toString.toLowerCase == "/tanken diesel") {
        var reply = "Dieselpreise:"

        val HashMap<String, DecimalType> DieselMap = newHashMap
{% for tk in tankerkoenig %}
        if (TK_{{ tk[0] }}_Diesel.state != NULL && TK_{{ tk[0] }}_Diesel.state != UNDEF && TK_{{ tk[0] }}_Open.state == OPEN) {
            DieselMap.put("{{ tk[1] }}", (TK_{{ tk[0] }}_Diesel.state as DecimalType))
        }
{% endfor %}
        for (Price : DieselMap.entrySet.sortBy[value]) {
            var String key = Price.getKey().toString
            var String value = Price.getValue().toString
            reply = reply + String::format("\n%s: `%s`€", key, value)
        }

        logInfo("Dieselpreise", reply)
        val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
        telegramAction.sendTelegram(Long::parseLong(telegramLastMessageChatId.state.toString), reply)

This piece of code is the workhorse for the Diesel prices (and the E10 and E5 code are similar). It’s a mix of Ansible templating for the gas stations and Java code here. I think one can extract the data from the GasStationPrices_Diesel and GasStation_Open group directly in Java, without creating the if statements in Ansible in a loop, but my Java is not good enough for this.

First I’m creating a new empty HashMap. This hash map is populated with data from all currently open gas stations which report a price for the selected fuel. The internal station name is used as key, the fuel price as value. Doesn’t work the other way around: a hash map can only hold one value for a key, and multiple gas stations can have the same price. Closed gas stations are ignored. Finally the reply is populated by going over the by-value sorted list, and adding all station names and prices. The resulting string is send back as reply using the Telegram Bot.

The final version in /etc/openhab2/rules/tankerkoenig.rules:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import java.util.HashMap

rule "Telegram Bot receive tanken"
when
    Item telegramlastMessageDate received update
then
    if (telegramLastMessage.state.toString.toLowerCase == "/tanken") {
        val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
        telegramAction.sendTelegram(Long::parseLong(telegramLastMessageChatId.state.toString), "Options:\n/tanken e10\n/tanken diesel\n/tanken e5")

    } else if (telegramLastMessage.state.toString.toLowerCase == "/tanken diesel") {
        var reply = "Dieselpreise:"

        val HashMap<String, DecimalType> DieselMap = newHashMap
        if (TK_Aral1_Diesel.state != NULL && TK_Aral1_Diesel.state != UNDEF && TK_Aral1_Open.state == OPEN) {
            DieselMap.put("Aral 1", (TK_Aral1_Diesel.state as DecimalType))
        }
        if (TK_Shell1_Diesel.state != NULL && TK_Shell1_Diesel.state != UNDEF && TK_Shell1_Open.state == OPEN) {
            DieselMap.put("Shell 1", (TK_Shell1_Diesel.state as DecimalType))
        }
        if (TK_Esso1_Diesel.state != NULL && TK_Esso1_Diesel.state != UNDEF && TK_Esso1_Open.state == OPEN) {
            DieselMap.put("Esso 1", (TK_Esso1_Diesel.state as DecimalType))
        }
        for (Price : DieselMap.entrySet.sortBy[value]) {
            var String key = Price.getKey().toString
            var String value = Price.getValue().toString
            reply = reply + String::format("\n%s: `%s`€", key, value)
        }

        logInfo("Dieselpreise", reply)
        val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
        telegramAction.sendTelegram(Long::parseLong(telegramLastMessageChatId.state.toString), reply)

    } else if (telegramLastMessage.state.toString.toLowerCase == "/tanken e10") {
        var reply = "Preise E10:\n"

        val HashMap<String, DecimalType> E10Map = newHashMap
        if (TK_Aral1_E10.state != NULL && TK_Aral1_E10.state != UNDEF && TK_Aral1_Open.state == OPEN) {
            E10Map.put("Aral 1", (TK_Aral1_E10.state as DecimalType))
        }
        if (TK_Shell1_E10.state != NULL && TK_Shell1_E10.state != UNDEF && TK_Shell1_Open.state == OPEN) {
            E10Map.put("Shell 1", (TK_Shell1_E10.state as DecimalType))
        }
        if (TK_Esso1_E10.state != NULL && TK_Esso1_E10.state != UNDEF && TK_Esso1_Open.state == OPEN) {
            E10Map.put("Esso 1", (TK_Esso1_E10.state as DecimalType))
        }
        for (Price : E10Map.entrySet.sortBy[value]) {
            var String key = Price.getKey().toString
            var String value = Price.getValue().toString
            reply = reply + String::format("\n%s: `%s`€", key, value)
        }

        logInfo("Preise E10", reply)
        val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
        telegramAction.sendTelegram(Long::parseLong(telegramLastMessageChatId.state.toString), reply)

    } else if (telegramLastMessage.state.toString.toLowerCase == "/tanken e5") {
        var reply = "Preise E5:"

        val HashMap<String, DecimalType> E5Map = newHashMap
        if (TK_Aral1_E5.state != NULL && TK_Aral1_E5.state != UNDEF && TK_Aral1_Open.state == OPEN) {
            E5Map.put("Aral 1", (TK_Aral1_E5.state as DecimalType))
        }
        if (TK_Shell1_E5.state != NULL && TK_Shell1_E5.state != UNDEF && TK_Shell1_Open.state == OPEN) {
            E5Map.put("Shell 1", (TK_Shell1_E5.state as DecimalType))
        }
        if (TK_Esso1_E5.state != NULL && TK_Esso1_E5.state != UNDEF && TK_Esso1_Open.state == OPEN) {
            E5Map.put("Esso 1", (TK_Esso1_E5.state as DecimalType))
        }
        for (Price : E5Map.entrySet.sortBy[value]) {
            var String key = Price.getKey().toString
            var String value = Price.getValue().toString
            reply = reply + String::format("\n%s: `%s`€", key, value)
        }

        logInfo("Preise E5", reply)
        val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
        telegramAction.sendTelegram(Long::parseLong(telegramLastMessageChatId.state.toString), reply)

    } else if (telegramLastMessage.state.toString.length() > 6 && telegramLastMessage.state.toString.toLowerCase.substring(0, 7) == "/tanken") {
        val telegramAction = getActions("telegram","telegram:telegramBot:HA_Bot")
        telegramAction.sendTelegram(Long::parseLong(telegramLastMessageChatId.state.toString), "Unknown option!")
    }
end

Final result:

Telegram Tankerkoenig
Telegram Tankerkoenig


Categories: [Ansible] [Linux] [Openhab]