How to make Openhab understand a string which contains a time?

EDIT: Go here for the latest info: How to make Openhab understand a string which contains a time? - #17 by salexes

Hello,

I am currently trying to setup a rule using the Amazon Echo Control binding and its lastVoiceCommand channel.

This is an example for the channel output:
2019-11-19 15:43:18.923 [vent.ItemStateChangedEvent] - SalexesEchoDot_LastVoiceCommand changed from alexa to set a timer for five minutes

What I am trying to achieve is to use the information of that channel to create a timer in openhab.

The problem is how can I filter the channel output, so I can make sure the user wanted to set a timer and how long the timer needs to be set?

regards,
salexes

Hi

I’m not entirely sure how you would achieve this, but I think this tutorial will help


If I were doing it, I would create a rule that triggered when the Item changed, then look for clues for intentions and values with the text string.

Thankfully Alexa is giving you the “Speech to Text” string, so you “only” need to work out the intention. and harvest the value.

rule "Alexa Speech to Text"

when SalexesEchoDot_LastVoiceCommand changed

then

// and here's the tricky bit...

// What is the intention?
// How do you know if the string starts with (contains) "Set a Timer"

// What do you with it?
// If it is a Timer intention, what is the required time?

end

Good luck, I’m looking forward to seeing how you progress.

Quick search results

https://community.openhab.org/search?q=speech%20intention

Well this is what I have come up with yet.

This is my current code:

import java.util.stream.Stream;
import java.util.regex.Pattern;
import java.util.stream.Collectors
import java.util.regex.Matcher;

var String[] words = newArrayList("One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten")

rule "Alexa Speech to text"

    when 
    	Item SalexesEchoDot_LastVoiceCommand  changed
    then
    	logInfo("Test", "Test")
    	var String example = SalexesEchoDot_LastVoiceCommand.state.toString

    	logInfo("Test2", "Test2")
    	var String regex = Stream.of(words).map(Pattern::quote).collect(Collectors.joining("|", "(?i)\\b(?:", ")\\b"))

    	logInfo("Test3", "Test3")

    	if (Pattern.compile(regex).matcher(example).find() && SalexesEchoDot_LastVoiceCommand.state.toString.contains("Timer")) {
            logInfo("Test4", "Test4")
    	}
    end

But I get this error in openhab logs:

2019-11-20 02:07:39.908 [INFO ] [.eclipse.smarthome.model.script.Test] - Test

2019-11-20 02:07:39.912 [INFO ] [eclipse.smarthome.model.script.Test2] - Test2

2019-11-20 02:07:39.916 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Alexa Speech to text': An error occurred during the script execution: index=0, size=0

I do not understand the error, how can I fix it. What exactly is not working ?

Can I use a String Array like that in openhab ?

It seems to be this line which is causing that weird error but I have no idea why:

var String regex = Stream.of(words).map(Pattern::quote).collect(Collectors.joining("|", "(?i)\\b(?:", ")\\b"))

Any help is greatly appreciated

I would also be willing to pay for help to make it work

Once I manage to get this working I can finally use multi room timers with Alexa.

@rlkoshak @papabrenta Are you guys familar with this error? I found something about it in your thread here: Common Rules errors and their solutions but I wasn’t able to find out what is causing the problem. Because as far as I can tell there is no bracket or something like that missing.

No, Rules DSL does not support arrays. But it does support ArrayLists which is what you’ve created. Get rid of the attempt to force the ArrayList into a String[]. If Stream requires an array, call toArray on words and that should work I think.

val words = newArrayList("One", ...
...
Stream.of(words.toArray).map...
1 Like

Thank you for the quick response. I tried the code changes you suggested but when I try to use .toArray it shows this error:
2019-11-20 15:58:38.342 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Alexa Speech to text': 'toArray' is not a member of 'java.lang.String'; line 17, column 31, length 13

You made both changes?

My bad I did not remove the word “String” in the val words line.
now it is like this: val words = newArrayList("One"...

Still getting the same
Rule 'Alexa Speech to text': An error occurred during the script execution: index=0, size=0
error again with this version with the .toArray
val String regex = Stream.of(words.toArray).map(Pattern::quote).collect(Collectors.joining("|", "(?i)\\b(?:", ")\\b"))
but I also get the same error when just using
val String regex = Stream.of(words).map(Pattern::quote).collect(Collectors.joining("|", "(?i)\\b(?:", ")\\b"))

Did I miss something obvious again ?

I don’t know anything about how to use Stream but that is where the error is coming from. You will need to research how that works and what that error may mean. The only thing that sticks out now is that when calling a static member of a class from Rules DSL you should use :: instead of ., but I don’t think that is strictly required.

Stream::of...

Pattern::compile...

Instead of messing with Stream, you could use the Rules DSL way of doing map/reduce type operations. See Design Pattern: Working with Groups in Rules.

Tried that, sadly did not work.

I am not sure if I can follow.
What I am trying to do is to check if the lastvoicecommand from alexa contains the word timer and one of the specified numbers defined in my Array List.
for example: “Set timer for five minutes”

I can not follow right now how I could use map/reduce to check for that.
Maybe I am understanding the reduce thing wrong. Can I filter a string I filter for single/multiple words with it ?

In the example linked it always uses Groups but I only have a single item?

Sorry to ask so many questions. Quite new to java programming in general that is why I am sometimes slow to understand.

Edit: created a group and put the SalexesEchoDot_LastVoiceCommand item in it.

val words = newArrayList("One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten")

rule "Alexa Speech to text"
when
    Member of AlexaStringFilter changed
then
    val check = AlexaStringFilter .members.filter[ NotSureWhatToPutHereToFilterForAllWordsFromArrayList]
    if (SalexesEchoDot_LastVoiceCommand.state.toString.contains("Timer") && checkContainsANumber) {
        //create timer with amout of minutes mentioned in alexas lastvoicecommand
    }
end

A map/reduce is exactly what you are doing with the Stream line of code. That line of code is looping through all the words and building the regular expression String. And you probably don’t even need the map part, just the reduce.

You can do this without the regular expression at all. Just use a reduce and .contains. The examples on the DP use members of a Group for the examples because that is where users encounter Lists in Rules 99% of the time. But those operations work with all Lists.

    // Doesn't contain the word timer, exit
    // Double check the call to toLower, I'm going from memory and it might be called something else
    // We want to normalize the text so we don't have to deal with all variations of Timer, timer, tImer, etc.
    if(!SalesEchoDot_LastVoiceCommand.state.toString.toLower.contains("timer")) {
        return;
    }

    // Contains one of the words in words
    // reduce loops through all the members of words and executes the reduction calculation for each.
    // In this case, it returns true when one of the words is in the command String and false otherwise.
    val matches = words.reduce[result, word | 
        val r = SalesEchoDot_LastVoiceCommand.state.toString.toLower.contains(word)
        r || result // if r or result are true, returns true
    ]
    if(matches){
        // do something, it contains both "timer" and at least one member of word
    }

If you want to know what members of word are in the String you can use a filter

    // matches will be a list of all the members of words that are in the last command String
    val matches = words.filter[ word | SalesEchoDot_LastVoiceCommand.state.toString.toLower.contains(word) ]

Using filter, you can also just get a true or false like we did with the reduce above.

    val matches = words.filter[ word| SalesEchoDot_LastVoiceCommand.state.toString.toLower.contains(word) ].size > 0
    if(matches){
        ...

Then don’t use advanced Java stuff like Stream and Regular Expressions and stuff. Stick to Rules DSL language constructs which are a little easier to work with and understand.

1 Like

This seems a bit simpler…

var words_to_numbers = newHashMap(
    "eine" -> 1, "zwei" -> 2, "drei" -> 3, "vier" -> 4, "fünf" -> 5, "sechs" -> 6, "sieben" -> 7, "acht" -> 8, "neun" -> 9, "zehn" -> 10, "elf" -> 11, "zwölf" -> 12, "dreizehn" -> 13, "vierzehn" -> 14, "fünfzehn" -> 15, "sechzehn" -> 16, "siebzehn" -> 17, "achtzehn" -> 18, "neunzehn" -> 19, "zwanzig" -> 20
)

rule "Test rules DSL"
when
    Member of gLastVoiceCommand changed
then
    last_voice_command = triggeringItem.state.toString
    if (last_voice_command.contains("set a timer for") || last_voice_command.contains("set timer for")) {
        val response_list = last_voice_command.split(" ")
        for (number : words_to_numbers.keySet) {
            if (response_list.contains(number)) {
                // create timer using words_to_numbers.get(number) as timer length
                return;
            }
        }
    }

Edit: a little less simple, but takes care of some nuances in numbers for the German language pointed out by @salexes in a PM (e.g. zehn and dreizehn).

1 Like

Hello guys,

I have a different problem:
I am getting this error:

Rule 'Alexa Timer Test': Unknown variable or command '*'; line 143, column 26, length 23

remaining is globally defined as
var Number remaining

This is my line 143:
remaining = ((duration)*60) - ((now.getMillis - timerStarted)/1000)

Why doesn’t it recognize the * as the multiply operator?

What is duration?

duration is a number
var Number duration

timerStarted is also a number

Get rid of the extraneous parens.

(duration * 60) - ...

Hello everyone, after a long time I am revisiting this thread once again.

My rule looked something like this in the end:

var words_to_numbers = newHashMap(
    "eine" -> 1, "zwei" -> 2, "drei" -> 3, "vier" -> 4, "fünf" -> 5, "sechs" -> 6, "sieben" -> 7, "acht" -> 8, "neun" -> 9, "zehn" -> 10, "elf" -> 11, "zwölf" -> 12, "dreizehn" -> 13, "vierzehn" -> 14, "fünfzehn" -> 15, "sechzehn" -> 16, "siebzehn" -> 17, "achtzehn" -> 18, "neunzehn" -> 19, "zwanzig" -> 20
)

rule "Alexa Timer"

when

    Member of gLastVoiceCommand changed

then

    val last_voice_command_Kueche = KCheEchoDot_LastVoiceCommand.state.toString

    if (last_voice_command_Kueche.contains("timer") && !last_voice_command_Kueche.contains("stop") && !last_voice_command_Kueche.contains("abbrechen") && !last_voice_command_Kueche.contains("wie")) {

        val response_list = last_voice_command_Kueche.split(" ")

        for (number : words_to_numbers.keySet) {

            if (response_list.contains(number)) {    

                    time_unit_Kueche = response_list.get(response_list.indexOf(number) + 1)

                    timer_duration_Kueche = null

                    durationKueche = words_to_numbers.get(number)

                    switch (time_unit_Kueche) {

                        case "stunde" : {

                            timer_duration_Kueche = now.plusHours(words_to_numbers.get(number))

                        }                        

                        case "minute" : {

                            timer_duration_Kueche = now.plusMinutes(words_to_numbers.get(number))

                        }

                        case "sekunde" : {

                            timer_duration_Kueche = now.plusSeconds(words_to_numbers.get(number))

                        }

                        case "stunden" : {

                            timer_duration_Kueche = now.plusHours(words_to_numbers.get(number))

                        }                        

                        case "minuten" : {

                            timer_duration_Kueche = now.plusMinutes(words_to_numbers.get(number))

                        }

                        case "sekunden" : {

                            timer_duration_Kueche = now.plusSeconds(words_to_numbers.get(number))

                        }                        

                    }          

            }
timerAlexaKueche = createTimer(timer_duration_Kueche, [|
			timerAlexaKueche.cancel()
			timerAlexaKueche = null
            EchoShowWohnzimmer_Announcement.sendCommand('Küchentimer')
			EchoShowWohnzimmer_AlarmSound.sendCommand('ECHO:system_alerts_melodic_02')
			if (stopAlexaTimerSoundKueche === null) {
			    stopAlexaTimerSoundKueche = createTimer(now.plusSeconds(20)) [|
				stopAlexaTimerSoundKueche.cancel()
				stopAlexaTimerSoundKueche = null
				EchoShowWohnzimmer_AlarmSound.sendCommand('')
				KCheEchoDot_AlarmSound.sendCommand('')
				]
			}						
						
			])

The solution which 5iver has posted and has helped me fine tune in private messages back then worked quite good but is quite restrictive. Only for full commands like set timer for xx minutes but not for commands like set timer for 10 minutes and 25 seconds or set timer for 1 hour 20 minutes and 40 seconds.

I am wondering what the best approach would be to support cases like that aswell?

Is a rule like above still a good way/start of realizing it or are there more efficient ways to solve something like this nowadays?

EDIT:
Just to give a short descriptive summary of what I am trying to Achieve.

I have an Alexa Device in my Livingroom and in my Kitchen.
Almost always I set a timer while I am cooking/baking in the Kitchen for x amount of time and then leave the kitchen and go into the living room.
Now I want to have the Alexa device in the Living room to make noise/say something aswell when the timer in which I set on my Kitchen Alexa Device finished.
(Otherwise I often do not hear the timer I have set in the Kitchen)

Any suggestions?

String = set a timer for one hour twentyfive minutes and sixteen seconds

My biggest issue is that I am not sure how to detect which which number was said/in the string before the word hour or before the word minute/seconds.

How is it possible to identify it ?