Voice Control (wit.ai) - My progress/setup + Tips/advice needed

Hi community

I’m hoping this post will generate some discussion regarding setting up openHAB for voice control.

Over the last week I’ve been experimenting with integrating the voice/text processing capabilities from wit.ai with openHAB, and while it works for my simple set up, I’m looking for ways to make it more scalable and robust.

If you’re not already familiar with wit.ai it might be worth taking a few mins to try and get your head around how it works.

Here’s a quick run through my progress so far:

As some of you may know, when you use the voice controls in the mobile apps (or at least on Android), the app sends a transcribed version of what you said to the item “VoiceCommand”. I’ve got a rule set up so that when the VoiceCommand item is updated it sends the new data off to wit.ai to be processed and I receive a JSON formatted message back with with the probable meaning. Depending on what was said, and how you set up your wit.ai rules (intents), you may or maybe not receive things like device, location, state, amount etc. Once the response is received I try and extract the important parts from the message using the JSONPATH transformation tools.

Hopefully you’re still with me! Here’s a quick example.

  1. I say “turn the light on” in the openHAB android app.

  2. The VoiceCommand item is updated to “turn the light on”

  3. The rule that triggers during this change sends the phrase to wit.ai and receives the response. The response looks as follows:

    {
    “msg_id” : “2ca95ad3-610e-4d8c-99c8-b3fc2a9d71a3”,
    “_text” : “turn the light on”,
    “outcomes” : [ {
    “_text” : “turn the light on”,
    “intent” : “command_toggle”,
    “entities” : {
    “device” : [ {
    “suggested” : true,
    “value” : “light”,
    “type” : “value”
    } ],
    “on_off” : [ {
    “value” : “on”
    } ]
    },
    “confidence” : 0.981
    } ]
    }

Using JSONPATH transformation I’m able to extract the intent (“command_toggle”), device (“light”) and the state (“on”) and check how confident the service is that the response is an accurate interpretation of what I said.

  1. Assuming I only have one light item called “light”, I can build a rule easily to handle this to turn the light on or off, something like sendCommand(device, state).

I’m now trying to progress this set up and that’s where I’m looking for some ideas on how best to approach this. Obviously if I have more than one light and perhaps my phrase was “turn the bedroom light on” wit.ai is intelligent enough to respond with the following:

{
  "msg_id" : "c8b73118-590a-4d03-8103-0b60b208a549",
  "_text" : "turn the bedroom light on",
  "outcomes" : [ {
    "_text" : "turn the bedroom light on",
    "intent" : "command_toggle",
    "entities" : {
      "device" : [ {
        "suggested" : true,
        "value" : "light",
        "type" : "value"
      } ],
      "on_off" : [ {
        "value" : "on"
      } ],
      "location" : [ {
        "suggested" : true,
        "value" : "bedroom",
        "type" : "value"
      } ]
    },
    "confidence" : 0.887
  } ]
}

You can see it’s added the location. OK maybe I could change the name of my items to something like “bedroom_light” and it would work for multiple lights if I concatenate the location and item but I’m not convinced this is an elegant solution so tips/ideas on how to structure the rules or items is appreciated.

Has anyone else tried to integrate voice command processing into their setup? What are your experiences or lessons learnt so far?

2 Likes

I’m very new to openHAB and one of my first thoughts is why there wasn’t more functionality/discussions/ideas with voice control. I saw this article about using the Amazon Echo with openHAB. That looked promising but at this point I need to take baby steps with openHAB. I’d be interested in the method and code needed to get a voice command from a microphone connected to the computer, send it off to wit.ai and the receive message back inside of openHAB. Also could you share to code you have in the rule when the “VoiceCommand” item is updated? I wouldn’t mind experimenting with it myself from my android phone to see what I come up with.

Anyway, sorry I don’t have a lot to contribute but I did want to respond to this post so I’m alerted to the discussion as others join in.

I am in the process of writing a rule that does some naturral voice processing and so far the only issue I have found is my own knowledge of the openhab syntax. I am getting better though and I am confident I will be finished soon.

After alot of thought I found there are 2 ways you can go about it - naming convention wise

You can name everything however you want with short names and such - but you will need a hashmap that stores tags or a more explanatory name.
For instance you could name things FF_Lights, and SF_Lights - but you will need a hashmap and some ifs to change those too first_floor_lights and second_floor_lights respectively.

The second option I see is the one I am using, very explanatory names, I believe it is a recommendation in the wiki as well.

Lets say I have a group - upstairs - any groups within that group will be prefixed with upstairs
so that would make upstairs ->upstairs_lights, upstairs_bedroom_one, upstairs_kitchen
then any items or groups within those would would also follow that concept.
As an example my bedroom light is upstairs_bedroom_one_light

The reason I have gone with this convention is the rule I am making for processing.

  1. receive voice command
  2. strip out filler words - mostly for speed - store original if needed later
  3. go through all TOP LEVEL groups for each word in the command, each group gets a point for each word of the voice command it contains
  4. the group, or groups that have the most points get searched the same and given points
  5. ALL the groups from that last search get checked against each word in the command, a point for each word it contains
  • at this point it is searching an item named something like upstairs_bedroom_one_secondary_light
  1. if there is more than one match they are stored in an array list and read back *not sure how ill do that
  • if there is only one match then we have now found the item to perform an action on
  • I then use something like the voice control rule in the wiki to extract the action - off/on/up/down/percent/etc

At the moment my issue is my knowledge of the language, I keep having variables overwritten and null values pop up, otherwise, with simple commands where there are no timing issues it seems to work very well. I figure every time a command is figured it will be stored in a map to make the process faster each and every time. Sorry for the rant, hope it gives you some ideas of how to proceed.

@ct_buffalo - I would love if you could elaborate or share an example of step 3 onwards, I might be able to use that process to solve some of the issues.

@George - the below is a response to a couple of your points/questions but should be useful to all.

wit.ai has some example apps (though you need to build them) that use the microphone to send audio data to be processed. However, speech to text capabilities already exist on Android so I’m not inclined to reinvent the wheel here especially since HABdroid does this part for you.

Here’s my basic item/rule set up. It’s a bit crude at the moment and could probably be done better but it works for now.

voice.items:

String VoiceCommand
String WitAi 

voice.rules:

   rule "Process Speech"
    when
        Item VoiceCommand received update
    then
        var String command = VoiceCommand.state.toString.toLowerCase
        var String scriptCall = String::format("/opt/openhab2/conf/scripts/wit.sh@@%s", command)
        executeCommandLine(scriptCall)
    end

rule "ParseWitAiReponse"
when Item WitAi received update
then
    var Number confidence = new Double(transform("JSONPATH","$.outcomes[0].confidence",WitAi.state.toString))
    if (confidence > 0.5) {
               var String state = transform("JSONPATH","$.outcomes[0].entities.on_off[0].value",WitAi.state.toString)
               var String item = transform("JSONPATH","$.outcomes[0].entities.device[0].value",WitAi.state.toString)
               sendCommand(item,state) 
     }
end

wit.sh (You need to replace the authentication token with the one from your own wit.ai app):

BaseUrl="https://api.wit.ai/message?v=20150906&q="$1
Url=$(echo $BaseUrl | sed 's/ /%20/g' )
Response=$(curl -s -H 'Authorization: Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' $Url )
curl --header "Content-Type: text/plain" --request PUT --data "$Response" http://localhost:8080/rest/items/WitAi/state

So in a nutshell, when VoiceCommand is updated it uses wit.sh to send the command off to wit.ai to be processed and then the response is sent to another item called WitAi where there is a rule to extract bits of information.

As mentioned in the OP I’m still looking at good ways to link the information received to commands and items in openHAB.

1 Like

Here is an example of step 3, please note this specific example may need some tweaks and grammar corrections - I am also, this may still have my issues with variables being null when the loops get to them, it is still untested, but the structure and basics of it will be the same.

//The actual voice command to be processed
var String command = “turn off the light in the upstairs”

//Split the voice command into an array or the different words
var String[] arr = command.trim.split(" ")

//Hashmap that will be used to store the points for likelyhood for each item
val HashMap<String, Object> possibleItems = new HashMap<String, Object>()

//Loop through each word in the voice command word array
for(String item : arr){
    //For each of the words being looped through, search top level group names to see if any contain words from the command
	All?.members.filter(g | g.name.contains(item)).forEach(r,t1 | createTimer(now.plusSeconds(t1)) [|
        //If there is a word found then add that item to the hashMap, or increase that item in the hasmap by 1
        if(possibleItems.get(item.name) != Uninitialized && possibleItems.get(item.name) != null)
            possibleItems.put(item.name, (possibleItems.get(item.name) + 1))
        } else {
            possibleItems.put(item.name, 1)
        }
    ]
    
    //Afterwords you have a hashmap that has items and possible values, you take the most likely ones and do it again

Sorry to flog this posting but here is an updated rule. This rule skips 1 step, as I may have found a bug with either my install or openhab.

basically it splits up the command and searches the root groups for the probable group. Then it searches those probable groups for all members and assigns a probability, then it maps the most probable items and prints it out (for example purposes)

It works because my items are named based on their groups and sub groups upstairs->upstairs_bedroom ->upstairs_bedroom_light

rule "voice control"
    when 
        System started 
    then
        println("started")
        lights_manual_override.members.forEach[lmo |
            var SwitchItem lm = lmo as SwitchItem
            sendCommand(lm, OFF)
        ]
        if(1 == 0) {
        createTimer(now.plusSeconds(5)) [| 
            lights_wink.members.forEach[item, i| createTimer(now.plusSeconds(i)) [| 
                var String name = item.name
                var Number value = item.state
                executeCommandLine("/home/pi/wink/wink-status.sh " + name + " " + value.toString + " -update")
            ]]
            createTimer(now.plusSeconds(15)) [| 
                lights_wink.members.forEach[item2, i2| createTimer(now.plusSeconds(i2)) [| 
                    var String name2 = item2.name
                    var Number value2 = item2.state 
                    winkLightInfo.get(name2).put("last_update", value2)
                ]]
            ]
        ] 
        }
        createTimer(now.plusSeconds(5)) [| 
            var String command = "turn on the light in the upstairs bedroom"
            //Split the voice command into an array or the different words
            var String[] arr = command.trim.replace("in ", "").replace("the ", "").replace("on ", "").split(" ")
        
            //Hashmap that will be used to store the points for likelyhood for each item
            val HashMap<String, LinkedHashMap<String, Object>> possibleItems = new HashMap<String, LinkedHashMap<String, Object>>()
            var aMembers = All?.members.map[it]
            //Loop through each word in the voice command word array
            for(member : aMembers) {
                val LinkedHashMap<String, Object> temp1 = new LinkedHashMap<String, Object>()
                for(String word : arr) {
                    if(member.name.contains(word)) {
                        if(possibleItems.get(member.name) == Uninitialized || possibleItems.get(member.name) == null) {
                            temp1.put("item", member)
                            temp1.put("possibility", 1)
                            possibleItems.put(member.name, temp1)
                        } else {
                            temp1.put("item", member)
                            var Number poss = possibleItems.entrySet().get(member.name).get("possibility")
                            temp1.put("possibility", poss + 1)
                            possibleItems.put(member.name, temp1)
                        }
                        
                    }
                }
            }
            val HashMap<String, Object> possibleItemsB = new HashMap<String, Object>()
            for(memberB : possibleItems.entrySet()) { 
                val GroupItem hh = memberB.getValue().get("item") as GroupItem
                hh.allMembers.forEach[e | 
                    possibleItemsB.put(e.name, e)
                ]
            }
            val HashMap<String, LinkedHashMap<String, Object>> possibleItemsC = new HashMap<String, LinkedHashMap<String, Object>>()
            for(memberC : possibleItemsB.keySet()) {
                val LinkedHashMap<String, Object> temp2 = new LinkedHashMap<String, Object>()
                for(String word2 : arr) {
                    if(memberC.contains(word2)) {
                        if(possibleItemsC.get(memberC) == Uninitialized || possibleItemsC.get(memberC) == null) {
                        
                            temp2.put("item", possibleItemsB.get(memberC))
                            temp2.put("possibility", 1)
                            possibleItemsC.put(memberC, temp2)
                        
                        } else {
                            
                            temp2.put("item", possibleItemsB.get(memberC))
                            val Number poss = possibleItemsC.get(memberC).get("possibility")
                        
                            temp2.put("possibility", (poss + 1))
                            possibleItemsC.put(memberC, temp2)
                            //println(possibleItemsC)
                        }
                    }
                }
            }
            var HashMap<String, String> winnerMap = new HashMap<String, String>()
            val Number highCount = 0
            val String winner = ""
            for(key : possibleItemsC.keySet()) {
                var Number finalPoss= possibleItemsC.get(key).get("possibility")
                if(finalPoss > highCount) {
                    highCount = finalPoss
                    winnerMap = new HashMap<String, String>()
                    winnerMap.put(key, finalPoss.toString)
                } else if(finalPoss == highCount){
                    winnerMap.put(key, finalPoss.toString)
                }
            }
            println("done")
            println("** The Winners **")
            
            for(key : winnerMap.keySet()) {
                println(key + " : " + winnerMap.get(key))	
            }
        ]

end

@ct_buffalo how fast is your search?

I wrote a string similarity scoring function (Levenshtein distance) however it seems to take 1-2 seconds per comparison which causes the rule to fall over when it iterates over multiple groups/items. Even if the rule didn’t fall in a heap I doubt I’ll tolerate a delay like that.

The search isn’t very long actually, maybe a bit longer as the system grows though.
For the test case

voice command = “turn on the light in the bedroom upstairs”
# of items = approx 30
top level groups = 5

Search time from receiving command t outputting the most likely item is approx 2 seconds
Here is the rule, I have made a couple changes since I posted it but this is basically it.

As for your function and hurdles…
That is more the type of function I hoped to work towards, it is a little disconcerning hearing of such a delay.
Also, and sorry or my lack of terminology knowledge but I am not 100% sure I get what you mean.

If I understand correctly it sounds like the same issue I was having when my variables where being declared null when the second and third loop would be fired before the first one ever finished (for example). Am I correct in that assumption of your issue?

Yes your last paragraph seems to describe the behaviour I’m seeing where I’m getting errors of vars/objects being null when I know they shouldn’t be.

I’m still searching for a solution but for now I’m stumped.

Well in my situation the issue was that I was using forEach in a closure.
example…
All.members.forEach[a | ]

If this is the same thing as you, my fix was to instead declare all the members as a hashmap and then for each through them.

var Hashmap nameOfHashMap = All.members.forEach[map.it]
then

for(String x : nameOfHashMap) {
// code goes here
}

This fixed my issues with all the loops being fired at once. It seems like closure for loops are all at the same time but otherwise the for loop runs step by step as you would expect.

I’m very interested on how you are all doing this. I was thinking of using tasker thats got ok google integration. I do it now for direct control of my lights however i’d love to have a voice command for all my rules. I wanted to also do an amazon echo installation as well and it doesn’t seem like it could be to hard. What are you guys using to capture and process. Again I really would like to see how your accomplishing this and how I can possibly help.

@Robert_Burgess

At the moment I’m using the HABdroid app to capture speech and do speech-to-text processing.

The HABdroid app then sends the text to an Item in your openHAB system called VoiceCommand. I’m then sending this text to my wit.ai application in order to process the meaning of the command.

I’m currently working on how to handle the response from wit.ai.

Once my Moto 360 arrives I’m hoping that I can send commands directly to my openHAB system using AutoVoice/AutoWear or something similar.

I have both HABdroid app and 3hou.se installed. I like the 3hou.se interface because on the HABdroid app I have text that just over laps. The 3hou.se atleast formats it to a new line. I’ll have to setup a wit.ai and check it out. I have the AutoVoice app installed from Tasker and was using the AutoHue integration that was created. However i’d like to see about sending it directly to the OpenHAB to control other things. I’ll have to dig further into this wit.ai and see how its sent and coming back to see how I could use this :slight_smile:

I’ve successfully used AutoVoice and Tasker to communicate with OH through the REST API. Of course, if you want to do this away from your home LAN you need expose your OH to the public internet or set up a VPN. I use the HTTP Get action in Tasker.

Server:Port = https://%OHSERVER:%OHPORT
Path = CMD
Attributes = <Switch>=<New State> // for example MySwitch=ON
Trust Any Certificate= checked

I put my username and password in %OHSERVER using user:password@host. Probably not the best security but better than nothing for sure and I’m usually connected via VPN so it doesn’t matter anyway.

1 Like

I was just playing with this a little bit ago. Setup an away mode and my daughter says Bye Bye house and it turns all the lights off and stuff. She thinks its great. Also I usually have my phone VPN in as well.

@rlkoshak Can you use REST API through my.openhab? I’m currently trying to decide whether to go with a free dynamic dns service or my.openhab.

Has anyone got experience using AutoVoice with Android Wear?

I’m currently using Home Remote (iOS app) which has a voice interface. On the Apple watch, the voice interface either executes a predefined command (in the app) or passes it on to a server for processing (as text). The developer had originally intended this to be used by voxcommando (windows only), but after a talk with me he added openhab as an interface.

So now the watch app will send speech converted to text to openhabs rest interface. I use the VoiceCommand item. The speech to text in the app uses fuzzy logic, and is astoundingly accurate.

Now, how do you process this plain text?

I tried natural language processing, but it really doesn’t work.

In the end, keep it simple works best. I basically extract key works (ON, OFF) and percentage values. Then try to identify items - I have a lookup list of items vs real world names. I do a little pre-processing to try to eliminate obvious errors (plurals are a pain - light vs lights for example)

Then if I have a command (ON, OFF, percentage) and an item, I fire off the sendCommand.

Works very well, and it’s fast. it’s all contained in one rule, so no external services required.

Drawback is that you have to maintain “the list” which is items to real world names. and when it goes wrong, it’s hard to find out what it did. ie if you say “turn the landing lights on” and nothing happens - either it did’t parse, or it did something else (for a while the “fuzzy logic” was turning “landing lights” into “london likes”).

I had a lot of problems with nulls, apparently Java doesn’t like comparing things to null.

I haven’t tested it extensively, turning lights on, off, to a percentage works well though.

Here is the full rule.

let me know what I could do better, as Java isn’t really my thing.

rule "VoiceControl"
when
    Item VoiceCommand received command
then
    logInfo("VoiceControl", "Voice Command received: " + receivedCommand.toString.lowerCase)
    val GenericItem myItem = null
    val String stt_org = receivedCommand.toString.lowerCase
    var Command newState = null
    // find new state, toggle otherwise (if possible)


    // fix plural confusion on light vs lights and others
    var stt1=stt_org.replaceAll("night", "light")
    var stt2=stt1.replaceAll("light", "lights")
    var stt3=stt2.replaceAll("degree", "degrees")
    var stt4=stt3.replaceAll("next", "nicks")
    var stt5=stt4.replaceAll("jules", "jills")
    var stt6=stt5.replaceAll("read", "red")
    var stt7=stt6.replaceAll("'", "")
    var stt=stt7.replaceAll("ss", "s")

    logInfo("VoiceControl", "New String is: " + stt)
    
    if (stt.contains("degrees") || stt.contains("percent") || stt.contains("%")) {
        // extract new state (find the digits in the string)
        var Pattern p = Pattern::compile(".* ([0-9]+).*(degree|percent|%).*")
        var Matcher m = p.matcher(stt)
        if (m.matches()) {
            newState = m.group(1).trim()
        }
    }
    else if (stt.contains("out")|| stt.contains("off")) {
        logInfo("VoiceControl", "matching state")
        newState = OFF
    } else if (stt.contains("on")) {
        newState = ON
    } else if (stt.contains("red")) {
        newState = HSBType::RED
    } else if (stt.contains("green")) {
        newState = HSBType::GREEN
    } else if (stt.contains("blue")) {
        newState = HSBType::BLUE
    } else if (stt.contains("down") || stt.contains("decrease")) {
        newState = INCREASE
    } else if (stt.contains("up") || stt.contains("increase")) {
        newState = DECREASE
    }
    logInfo("VoiceControl", "new state will be: " + newState.toString)

    if (stt.contains("family room lights")) {
        myItem = familyMain;
    } else if (stt.contains("family room lamp")) {
        myItem = familyLamp;
    } else if (stt.contains("iris") || stt.contains("fireplace")) {
        myItem = hueIris;
    } else if (stt.contains("bloom")) {
        myItem = hueBloom;
    } else if(stt.contains("dining room pots")) {
        myItem = diningPots;
    } else if(stt.contains("kitchen pots")) {
        myItem = kitchenPots;
    } else if(stt.contains("kitchen pendants")) {
        myItem = kitchenPendants;
    } else if(stt.contains("kitchen under counter")) {
        myItem = kitchenUCSwitch;
    } else if(stt.contains("hallway")) {
        myItem = hallwayLight;
    } else if(stt.contains("porch")) {
        myItem = porchLight;
    } else if(stt.contains("fountain")) {
        myItem = gardenPower;
    } else if(stt.contains("garden power")) {
        myItem = gardenPower;
    } else if(stt.contains("garden lights")) {
        myItem = gardenLight;
    } else if(stt.contains("landing")) {
        myItem = landingMain;
    } else if(stt.contains("bathroom")) {
        myItem = bathroomLight;
    } else if(stt.contains("bedroom")) {
        myItem = masterBRDimmer;
    } else if(stt.contains("nicks bedside")) {
        myItem = NickBedsideDim;
    } else if(stt.contains("jills bedside")) {
        myItem = JillBedsideDim;
    } else if(stt.contains("bedside")) {
        myItem = hueWhites;
    } else if(stt.contains("ensuite")) {
        myItem = ensuiteLight;
    } else if(stt.contains("garden leds")) {
        if(newState == OFF)
            myItem = RGBSAll_pi;
        if(newState == ON) {
            myItem = RGBSFadeRGBW_Pi;
            newState = 7
            }
    } else if(stt.contains("garden leds fade")) {
        myItem = RGBSFadeRGBW_pi;
        newState = 7;
    } else if(stt.contains("bedtime lights off")) {
        myItem = bedtime;
    } else if(stt.contains("all lights off")) {
        myItem = lights;
    } else if(stt.contains("temperature")) {
        myItem = dining_room_target_temperature_c;
    } else if(stt.contains("fan")) {
        myItem = dining_room_fan_timer_active;
    } else if(stt.contains("tv lights")) {
        if(newState == ON) newState=10;
        myItem = RGBWControllerW;
    } else {
        myItem = "";
    }
    
    
    if (myItem!="" && newState!="") {
        if (stt.contains("all ") || stt.contains(" all")) {
            val itemName = myItem
            val finalState = newState
            logInfo("VoiceControl","searching for  *"+itemName+"* items")
            All?.allMembers.filter(s | s.name.contains(itemName) && s.acceptedDataTypes.contains(finalState.class)).forEach[item|
                logInfo("VoiceControl","item  "+item+" found")
                sendCommand(item,finalState.toString)
            ]   
        } else {
            logInfo("VoiceControl", "sending "+newState+" to "+myItem.name)
            sendCommand(myItem,newState.toString)
            }
        }
        else if (myItem!="") {
        if(myItem.state >= 1) {
            logInfo("VoiceControl", "Executing command: " + myItem + ", OFF")
            sendCommand(myItem, OFF)
            }
        else {
            logInfo("VoiceControl", "Executing command: " + myItem + ", ON")
            sendCommand(myItem, ON)
            }
        }
        else {
            logInfo("VoiceControl", "Voice Command not recognized: " + stt)
            }
end

Reading this again, I have INCREASE and DECREASE the wrong way round - never noticed that (and probably never tested it).

@danielwalters86, that is a really good question. I just did a quick test and if it works, it doesn’t work the same was as the native REST API and I can’t find any documentation (in my five minutes of looking) to tell whether it is possible or not. Given that it is a secure connection I suspect that it is non-trivial to get working, if it is possible at all.

Rich

@Nicholas_Waterton, very impressive work. I do have a few suggestions that might make your code a little easier to augment and maintain going forward.

One thing to note is that while everything is running on top of Java, the rules are actually written in a sub-set of the Xtend language which is more terse and more of a functional like language. So if you run into trouble look at the Xtend documentation. But even then, remember that it is a subset of that language and there are some things you can’t do, like defining classes or using arrays.

And now for some ideas:

In general I prefer to separate the data from the business logic. In this case, I would move a bunch of your replaceAll and contains pairings to HashMaps and loop through the hashMap in your business logic.

The set of code where you run replaceAll on the stt_org String you can move your word pairings to a hashMap and collapse the code there into a loop. This way you can add new mappings, new items, in one location without needing to edit and potentially damage the business logic.

// Global val (val means final and should be used when you know it won't be replaced
// Put this at the top of your rules file so it is in an easy place to find and update.
val Map<String, String> replacements = newHashMap( "night" -> "light",
                                                   "light" -> "lights",
                                                   "degree" -> "degrees",
                                                   "next" -> "nicks",
                                                   "jules" -> "jills",
                                                   "read" -> "red",
                                                   "'", "",
                                                   "ss", "s" )

                                                                                   
// fix plural confusion on light vs lights and others
var stt = receivedCommand.toString.lowerCase
replacements.keySet.forEach(key | stt = stt.replaceAll(key, replacements.get(key))

A similar approach can work for states.

val Map<String, Object> states = newHashMap("degrees" -> Pattern::compile(".* ([0-9]+).*(degree|percent|%).*"),
                                            "percent" -> Pattern::compile(".* ([0-9]+).*(degree|percent|%).*"),
                                            "%" -> Pattern::compile(".* ([0-9]+).*(degree|percent|%).*"),
                                            "out" -> OFF,
                                            "off" -> OFF,
                                            "on" -> ON,
                                            "red" -> HSBType::RED,
                                            ...

var newState = null
states.keySet.forEach(key | 
    if(stt.contains(key)) {
        if (states.get(key) instanceof Pattern) {
            val p = states.get(key) as Pattern
            val m = p.matcher(stt)
            if(m.matches) newState = m.group(1).trim
        } else {
            newState = stt.get(key) as Command
        }
    }
)

Getting myItem though can be collapsed similar to above. I do notice there are some special cases which for now will still need to be handled separately unless I can think of an alternative.

// Again, put this map at the top as a global
val Map<String, GenericItem> strToItem = newHashMap( "family room lights" -> familyMain,
                                                     "family room lamp" -> familyLamp,
                                                     "iris" -> hueIris,
                                                     "fireplace"-> hueIris,
                                                     "bloom" -> hueBloom,
                                                     ...
                                                     "garden leds" -> RGBSAll_pi,
                                                     "garden leds fade" -> RGBSFadeRGBW_pi
var myItem = null
strToItem.keySet.forEach(key | if(stt.contains(key)) myItem = strToItem.get(key))

if(myItem == RGBSAll_pi && newState == ON) {
    myItem = RGBSFasdeRGBW_Pi
    newState = 7
}
else if(myItem == RGBSFadeRGBW_pi) {
    newState = 7
}

You can compare things to null in Java and Xtend.

if(myItem != null && newState != null) {
    if(stt.contains("all ") || stt.contains(" all")){
        // Not really sure what is going on here. Up to this point myItem has been treated like a GenericItem for
        // which there can be only one with a given name but here you are treating myItem as a String. It would be
        // best to pick one way and stick to it. Also, you don't need the "?" here because you probably want an error
        // if All is null
        All.allMembers.filter(s | s.name.contains(myItem) && s.acceptedDataTypes.contains(newState)).forEach[item |
sendCommand(item, newState.toString)
        ]
    } else {
        sendCommand(myItem, newState.toString)
    }
}
else if(myItem != null) {
    logInfo("VoiceControl", "Executing command: " + myItem + ", " + if(myItem.state == ON) "OFF" else "ON")
    sendCommand(myItem, if(myItem.state == ON) OFF else ON)
}
else {
    // error log
}

That is what I would do as a start.

Rich