Code question about enum

So I have a code question about enum. I am configuring a list of variables to be selected in an enum list in an app. What's the best way to determine if they are selected? I'm using integers to make the enumerations easier to code logic for later.

input "whenToUnlock", "enum", title: "When to unlock?  Default: '(Prevent unlocking under any circumstances)'", 
options: [1:"${state.whenToUnlock1}",  2:"${state.whenToUnlock2}", 3:"${state.whenToUnlock3}", 4:"${state.whenToUnlock4}",
          5:"${state.whenToUnlock5}", 6:"${state.whenToUnlock6}"],defaultValue: 6, required: false, multiple: true 

def whenToUnlockOptions() {
state.whenToUnlock1 = "Bolt/frame strike protection"
state.whenToUnlock2 = "Presence unlock"
state.whenToUnlock3 = "Fire/medical panic unlock"
state.whenToUnlock4 = "Switch triggered unlock"
state.whenToUnlock5 = "State sync fix"
state.whenToUnlock6 = "Prevent unlocking under any circumstances"

image

image

Options 1 through 5 are selected, and 6 is omitted. Enumerating for each I can see it outputs the ones that are selected so I'm almost done. Just need a little help with converting this to the simplest possible testable logic:

image
image

Pretty sure something like this would work but seems way more work than should be necessary to accomplish this. Maybe there is a better way?

whenToUnlock.each { it ->
whenToUnlockEachSelected = it
    if (whenToUnlockEachSelected == "1") {option1 = true} else {option1 = false}
    if (whenToUnlockEachSelected == "2") {option2 = true} else {option2 = false}
    if (whenToUnlockEachSelected == "3") {option3 = true} else {option3 = false}
    if (whenToUnlockEachSelected == "4") {option4 = true} else {option4 = false}
    if (whenToUnlockEachSelected == "5") {option5 = true} else {option5 = false}
    if (whenToUnlockEachSelected == "6") {option6 = true} else {option6 = false}

If you have multiple: true set in an input, as you do, then that selection will be stored as a List. You may be interested in how to work with lists in Groovy beyond .each(), which I see you've already discovered. The Groovy 2.4 docs on this may help: Working with collections

If you're just looking for a simple way to tell "is '1' selected?" without having to do what you did in your last example, then, for example, "1" in settings.whenToUnlock will return true or false (depending, on course, on whether it is). Note that in Hubitat, your keys are (as it seems you've also discovered) Strings, even though you're using Integer literals. Alternatively, you could do something like settings.whenToUnlock?.contains("1"), which should be logically equivalent (I'm using the null-safe ? here just in case this preference hasn't been saved yet and is null; you can omit that if you don't think that would happen at this point in your code).

You'll see there are even more things you can do, like settings.whenToUnlock?.containsAll(["1","2"]) , which will return true if (at least) both of those are selected. Depending on what you want, you should have a lot of options with what Groovy provides for collections. If you are trying to do something specific and want to figure out how, feel free to ask if this general guidance isn't enough!

PS - While this is a matter of style, I wouldn't personally use state to store your "whenToUnlockOptions" options, unless you have a reason to do so, like if they change based on user input and you need to store those values. If you just want something more or less equivalent to constants you can reference at any point in your code, I'd recommend a static, @Field-annotated script variable (field, now) instead. Also, I believe your options for that input should be a list of Maps, not just a Map. Something like:

@Field static List<Map<String,String>> whenToUnlockOptions = [
    ["1": "Bolt/frame strike protection"],
    ["2": "Presence unlock"],
    // ...
]

Then you can use options: whenToUnlockOptions for that input and reference the values in the same way. You could also get a list of all possible options ("1", "2", etc.) for that input with something like:

whenToUnlockOptions.collect { it.keySet()[0] }

(there might be a more elegant way to do that...just something I thought of now) ... which will give you a List of ["1", "2", /* etc. */ ], so you could further use that to enumerate over all possible options:

whenToUnlockOptions.collect { it.keySet()[0] }.each { /* do things here */ }

Just some other ideas. :smiley:

4 Likes

Great! This was exactly what I was looking for. I confirmed that my assumptions about how I was going about it would work, but this definitely gets me back on the right track. Bookmarking this!

I was using state as an easy way of tracking the variables on the app settings page while piddling with the logic. Easy to pop it up on a second tab and hit refresh. I plan on pulling them out and going with static. User input will affect what sensors and options are displayed, but the list should remain pretty static unless someone wants me to add more stuff in there for whatever reason.

Combining logic from these two I am guessing that this would work for contains any of the following?

settings.whenToUnlock?contains(["1","3","5"])

Unfortunately, no; that will look for a List (your ["1", "3", "5"] argument to contains()) inside the whenToUnlock field's value, but whenToUnlock is a list of Strings, not a list containing other lists. (In other words, [["1", "2", "3"]].contains(["1", "2", "3"]) is true, but ["1", "2", "3"].contains(["1", "2", "3"] is false, and you've got something like the latter--and it should also be noted that [["1", "2", "3" "4"]].contains(["1", "2", "3"]) is false).

But the good news is that there is a .containsAll() method that does what you want. :slight_smile: So, ["1", "2", "3"].containsAll(["1", "2", "3"] is true. Logically, this would be equivalent to something like settings.whenToUnlock?.contains("1") || settings.whenToUnlock?.contains("2") || settings.whenToUnlock?.contains("3"), which looks less cool/elegant but would also work.

1 Like

So putting all this together does this look correct?

input "ifLevel","enum", title: "Logging level",required: true, options: logLevelOptions, defaultValue : "1"

@Field static List<Map<String,String>> logLevelOptions = [
["0": "None"],
["1": "Info"],
["2": "Debug"],
["3": "Trace"]
]

def ifInfo(msg) {
    if ((!settings.ifLevel?.containsAll(["1","2","3"])) && (isInfo != true)) {return}//bail
    else if (settings.ifLevel?.containsAll(["1","2","3"])) {log.info "${state.thisName}: ${msg}"}
}

def ifDebug(msg) {
    if ((!settings.ifLevel?.containsAll(["2","3"])) && (isDebug != true)) {return}//bail
    else if (settings.ifLevel?.containsAll(["2","3"])) {log.debug "${state.thisName}: ${msg}"}
}

def ifTrace(msg) {
    if ((!settings.ifLevel?.contains("3")) && (isTrace != true)) {return}//bail
    else if (settings.ifLevel?.contains("3")) {log.trace "${state.thisName}: ${msg}"}
}

def turnOffLoggingTogglesIn30() {
    if (!isDebug) {
    app.updateSetting("isDebug", false)
    }
    if (isTrace == true) {
        runIn(1800, traceOff)
    }
    if (isDebug == true) {
        runIn(1800, debugOff)
    }
    if (isTrace == true) {
        runIn(1800, traceOff)
    }
    if (isHSM == true) {
        runIn(1800, hsmOff)
    }
}

The isTrace, isDebug, isInfo, and isHSM are toggles to turn on logging just for a little while.

1 Like

I can't speak to the logic in the conditionals (since I don't know the desired outcome or what some of the referenced values are), but from a Groovy List-querying perspective (which I assume is the scope of the question here), everything looks good to me!

Specifically around when a user selects a logging level from a drop-down list of the logs they want to enable (single selection option). If they select info they get only info logs. If they select debug, they get info and debug. If they select trace, then they get all 3. The isInfo and isDebug and isTrace are not done yet but just need to add them to the else if statement as an or, but not relevant to this.

I have some other code where I could make use of this as well and it makes it a lot cleaner if I can smoosh it down into one, which is why the question came up.

I get this error when I try the code posted above.

I'm guessing that could be related to your defaultValue, which is "3", a String and not a List. Does it work if you specify that as ["3"] instead? (In any case, the error is clear that the value it's reading from settings at that point is a String--but it should be a List.)

PS - This discussion on defaultValue may be of interest (some people find its behavior unexpected): Preferences: defaultValue. When are they loaded?

Let me try it and see.

Looks like that got it! Thanks.

I'll check it out. I really appreciate all the help. @Kings

3 Likes

Ah, looking at this again, I don't see multiple: true specified here like I assumed from the original discussion. In that case, your result will be a String (per the key in your options Map), but perhaps you meant to to allow multiple selections? That would be another explanation for this problem--it will get you a List as the value for this preference, which you'd want for the above to work. (I don't remember if defaultValue is smart enough to make a single provided value a list for you, but I'd guess no, so that could still be an issue, too.)

1 Like

Good to keep in mind for the other lists I have.