Telegram reminder and re-schedule cron rule based on response

One of the things openHAB lacks is some kind of configurable scheduler… and by that I don’t mean, of course, the cron trigger for rules.

I wanted to send a Telegram reminder, for example to read the electricity and gas meters - this is simple enough - but I wanted to re-schedule the alert. Why? Well, some kind of snooze mechanism… I often forget to read the meters because, when I receive the reminder, I’m not at home…

So, here is what I came up with…

I will post all of my scripts and rules below, but I wanted first to explain a bit WHY I chose to directly modify another rule…

To send a reminder, one could use a cron rule, a Google calendar/CalDAV event - evidently, no problems here. One could process the response from Telegram (there are some threads on how to control OH with Telegram and even a binding, project started by @Belgadon) so, also no problems here…
What I wanted to share is how I managed to re-scheduled another rule based on the response… As I wanted the changes to survive a reboot and even a reinstall of OH, I chose to directly modify the when clause of the alerting rule.
I’m sure that advanced users, @rlkoshak - master of design patters comes to mind :laughing:, could find a more elegant approach like, dunno, HashMaps - never managed to wrap my head around this one, encoding values in StringItems and saving/restoring them after reboot… but my programming abilities are limited (and also my English, sorry :wink:).

So, let’s start:

telegram_response.items:

String TelegramResponse "Telegram Response [%s]" {mqtt="<[mqtt:telegram/response:state:default"}
String TelegramResponse_Reminder (gPersist_MapDB_Change, gPersist_MapDB_Restore)

TelegramResponse is populated by a python script telegram2mqtt.py - inspiration comes from @Dixon’s post which is run in the background (started by /etc/rc.local).
I won’t post the scripts I use to send a Telegram message with a keyboard and how I read Telegram responses mainly because it will make this post too long.
One far better approach is to use @Belgadon Telegram binding and modified Telegram action (post). Maybe his work will be included in official openHAB addons.
This python script will publish any callback (from Inline keyboard) or any texts from any chats is the format <chat_id>@<message>.

The telegram_reminder_meters.rules is the one that send the initial alert and gets modified by the response:

rule "telegram reminder"
when
    Time cron "0 0 18 26-28 12 ? 2018"
then
    val keyboard_postpone2H = '{\"text\":\"Snooze 2 hours\"}'
    val keyboard_postpone4H = '{\"text\":\"Snooze 4 hours\"}'
    val keyboard_postpone1D = '{\"text\":\"Snooze 1 day\"}'
    val keyboard_done = '{\"text\":\"Reading done\"}'
    val keyboard = '{\"keyboard\":[[' + keyboard_postpone2H + ',' + keyboard_postpone4H + ',' + keyboard_postpone1D + '],[' + keyboard_done + ']],"resize_keyboard":true,"one_time_keyboard":true,"selective":true}\"'
    val chatId = "<chat_id>"
    var String message = "*REMINDER!*\n_You should read the electricity and gas meters...\nElectricty meter should be read between_ *26-05 every month* _and the gas meter between_ *24-28 every month*_..._"
    logWarn("DEBUG",executeCommandLine("/etc/openhab2/scripts/my_scripts/sendTelegram_withKeyboard.sh " + chatId + " '" + message + "' '" + keyboard + "'", 30000))
    TelegramResponse_Reminder.postUpdate("telegram_reminder_meters")
end

As you can see, it is initially programmed to run on 18:00 every day between 26 and 28.12.2018.
It uses another external script to send a Telegram message with Markdownsyntax and a reply_markup keyboard.

Whenever the TelegramResponse changes the following rule with read the response and change telegram_reminder_meters.rule cron trigger:

telegram_response.rules:

import org.quartz.CronExpression

rule "telegram response"
when
    Item TelegramResponse changed
then
    Thread::sleep(150)
    if(previousState == "") {
        if (TelegramResponse_Reminder.state != "") {
            // split Telegram response by "@"
            
            // <sender> is transformed from a Telegram chat_id in a bot name to be used with sendTelegram action
            val sender = transform("MAP","telegram_chatId.map",TelegramResponse.state.toString.split("@").get(0))
            // <response> will be the actual response received by Telegram, whether by pressing the keyboard buttons or simple text
            val response = TelegramResponse.state.toString.split("@").get(1)
            switch(TelegramResponse_Reminder.state){
                // the rule that sent the reminder gets modified. Value is kept in TelegramResponse_Reminder
                case "telegram_reminder_meters": {
                    var String new_cron
                    switch(response) {
                        // depending on the response, we will create the new cron trigger
                        // TO-DO: use some REGEX to extact the numbers from string and ditch the switch-case
                        case "Snooze 2 hours": {
                            new_cron = now.plusHours(2).toString("0 m H d M ? yyyy")
                        }
                        case "Snooze 4 hours": {
                            new_cron = now.plusHours(4).toString("0 m H d M ? yyyy")
                        }
                        case "Snooze 1 day": {
                            new_cron = now.plusHours(24).toString("0 m H d M ? yyyy")
                        }
                        case "Reading done": {
                                new_cron = now.withDayOfMonth(26).withTime(18,0,0,0).toString("0 m H d-28 M ? yyyy")
                            }
                        }
                    }
                    // validate the cron trigger
                    if(CronExpression::isValidExpression(new_cron)){
                        sendTelegram(sender,"Scheduling " +TelegramResponse_Reminder.state + ".rules to run on [" + (new DateTime(new CronExpression(new_cron).getNextValidTimeAfter(now.toDate))).toString("dd.MM.yyyy HH:mm") + "]")
                        executeCommandLine("/etc/openhab2/scripts/my_scripts/replace_cron.sh /etc/openhab2/rules/" + TelegramResponse_Reminder.state + ".rules '" + new_cron + "'",30000)
                    } else {
                        sendTelegram(sender,"[telegram_response.rules] The resulting CRON expression is not valid: [" + new_cron + "]")
                    }
                    TelegramResponse_Reminder.postUpdate("")
                }
            }
            TelegramResponse.postUpdate("")
        }
    }
end

replace_cron.sh:

#!/bin/sh

# CALL: replace_cron.sh <quartz_cron_expression>

/bin/sed -i "s/Time cron.*/Time cron $2/g" $1

As I said earlier, I sure there are other, more elegant solutions to re-schedule a rule, but this is what I came up with.
I’m interested to hear form advanced users/programmers what are the downsides of this approach…
Comments welcome! :smiley:

Edit: found some typos…

6 Likes

Hey, a working hack is an useful hack :grin:

It’s not the approach I would have taken but Hakan is right. It works and therefore it’s useful. Thanks for posting!