Dynamic UI Elements with Dynamic Input Variables - How to Access Variable

In my app I would like to set dimmer level by mode. I allow mode multiselection. How can I generate the UI in a dynamic fashion to allow for the allotted number of modes selected?

For example, if Morning, Day and Night are selected, I would like to then have 3 inputs appear to set the level for each mode.

Any help is appreciated.

Just like elsewhere in your code, you can use ifs (or switch or any decision-making structure) around blocks of UI code, so something enclosed in such a conditional will appear only if the condition is met. You will want to declare the page as a dynamicPage rather than just a page (or at least that's what's required in ST; Hubitat doesn't have any official docs on that yet as far as I can see) and use submitOnRefresh: true on elements where you want the page to reload (thus evaluating your ifs or whatever) when that input is changed.

Here's a minimal-ish example:

dynamicPage(name: "pageMain", title: "Dimmer Button Controller", uninstall: true, install: false, nextPage: "pageFinal") {
    section("My First Section") {
        input(name: "buttonDevice", type: "capability.pushableButton", title: "Select button device", multiple: false, required: true, submitOnChange: true)
    }
   section("My Second Section") {
      if (buttonDevice) {
              input(name: "myInput", type: "bool", title: "My example input")
      } else {
              paragraph("Please choose a device")
      }
   }
}

I did not run this through Hubitat's app code/parser in any form, so I might have a mismatched bracket or something silly I'm missing, but it should give you an idea of what's possible. Hope this helps!

Thanks for the response. But that doesn't really solve the problem. I know how to wrap a selection of code in an if to show or hide. But how do I generate an infinite number of variables to handle a dynamic number of inputs. Assuming here I would do a each loop probably on the collection of modes to generate a dynamic number of inputs, I just don't know how to handle the name properties of the input to get those values later.

Ah, sorry, I misinterpreted. You can use a loop such as classic for loop or Groovy's .each. Here's a modified example from one of my apps where I iterate over each button on a button device, then link to a page (pageButtonConfig is a function that defines a dynamicPage that takes the passed parameters and shows a page; you can use variables as input names on that page--or anywhere; I can show if needed--which I need to do to here to keep things straight between different button numbers):

 section("Configure buttons") {
     (1..getNumberOfButtons()).each {
 	    def num = it
 	    href(name: "pageButtonConfigHref",
 	    page: "pageButtonConfig",
 	    params: [btnNum: num, action: "pushed"],
            title: "Button ${num} - Pressed",
 	    description: getButtonConfigDescription(num, "pushed"))
     }
}

I'm iterating through a bunch of numbers, but you could do the same with items in any collection (e.g., location mode, as I imagine Motion Lighting and Rule Machine do when relevant options are chosen).

Ok, I still don't understand how to get a value back out of a dynamic input.

For example

 Modes.each{
                ifDebug("Mode: ${it}")
                input "WHAT HERE", "Number", title: "${it} Level", required: true, multiple: false, submitOnChange: true 
            }

How do I access the value put into an input that is dynamic like this?

If the input is inside a loop, you'll need to use a variable (or anything to differentiate, I guess) where you have "WHAT HERE". As an example, you could do:

input "myVariable_${it}", "Number", title: "${it} Level", required: true, multiple: false, submitOnChange: true

Then, each one of your inputs generated by the loop will have a unique name you can reference later.

Thanks for baring with me. I'm having trouble articulating the question.

How do I assemble a call to acquire the value?
Say, like this, once the variable is named, I don't know how to get back at that variable in another iteration.

Modes.each{
                input "${it}Level", "Number", title: "${it} Level", required: true, multiple: false, submitOnChange: true 
            }


Modes.each{
             log.debug "${it} - Level ${WHAT HERE}")
            }

I cannot dynamically assemble a variable name is my problem, for accessing that value later.

You've got the right idea, and that part is indeed tricky. One option is to re-iterate over the same list you used to generate the variable names in the first place (in my case, numbers; in yours, perhaps the list of location modes). Keep in mind that the variable name you're looking for may not exist (e.g., if the user didn't set it or if the user's location modes changed between then and now); therefore, you'll likely want to verify that the variable name exists (or that the value isn't null; the "classic" syntax where you literally check for that still works, but see also Groovy's ? operator) before you attempt to use it.

Exactly where and how you'd do this depends on when you plan on actually retrieving the value of this variable in your app, but hopefully this is somewhat helpful in general. There may be better ways depending on your specific use.

So I still don't know how to create a variable reference dynamically, meaning, I still don't know what goes in the WHAT HERE position

log.debug "${it} - Level ${**WHAT HERE**}")

to get the value from a variable whose name I do not know until runtime.

${${it}Level} embedded tricks like this don't work, as I expected they wouldn't.

So what is exactly the syntax for access a variable by a dynamic name?

Say I've got the first part of the name in $it , how do I construct the variable reference to the *Level variable in that example?

If you're not aware, it is a special variable that Groovy automagically creates when you're inside an .each. The variable name in Hubitat is a string, so you can use a double-quote with a reference to ${it} inside and have it automatically expanded to the correct string. You may have that much figured out (creating the variable/setting).

You can reference it the same way later. Say you have a mode name (or whatever) stored in modeName; then settings["level_${modeName}"] would be one way to access an input (setting) you have stored in this manner. Sometimes it's easier to assemble in pieces, like settingName = "level" + modeName and then accessing settings[settingName], but that's up to you.

2 Likes

Boom!!! There it is. Thanks so much!

1 Like

If you need another example I just did this in an App UI:

def deviceSelectPage() {
  page(name: "deviceSelectPage", title: "Configure Foobar", install: true, uninstall: true) {
    section {
      input("notificationDevices", "capability.notification", title: "Select Notification Devices", multiple: true, required: true, submitOnChange: true)
    }
    section {
      if (notificationDevices) {
        // Render a Presence selector for each notification device
        notificationDevices.each{ notificationDevice ->
          input(getPresenceDeviceSettingName(notificationDevice.getId()),
            "capability.presenceSensor", title: "Presence Sensor for ${notificationDevice}:", description: "", required: true)
        }
      }
    }
  }
}

Full code: hubitat/pan-app.groovy at master ยท edalquist/hubitat ยท GitHub

Note that the submitOnChange flag just reloads the page but doesn't call updated. So stick that on fields you want to use to drive the dynamic display of other fields on the page.

2 Likes

When I try to do this, I get an error stating that I must specify a non-null value for the to index in a Range. So, how do you get around that?
I'm able to get this to work:

    section(){
        (1..4).each{
            def num = it
            input name:"time${num}", type: "time", title: "Daily check #${num}", width: 2, submitOnChange: true
        }

But when I sub in a variable or function in place of 4, i get this error:
Must specify a non-null value for the 'to' index in a Range on line 47

These are some of the things I tried on that line:

(1..getNumber()).each{
(1..checkNumber).each{
where checkNumber is a variable that is defined earlier.
Literally anything I put in there except for a number gives me that error. So, how did you get around this?

I have been banging my head against the wall for the last 5 hours trying to figure this out. Any held would be greatly appreciated.

I didn't have to do anything special, so my suspicion is that whatever function you're using isn't returning a numeric value (so doesn't jive with the range you're trying to create). Not sure what would be going on with the variable, but maybe something similar (null value or incompatible type). Have you verified (e.g., with a log.debug before--about the only tool we have for this) that the variable or function actually has or returns the value you're expecting?

Also, if you always expect a certain type, you could also explicitly declare the return type of the function or the type of the variable--something totally possible in Groovy but seldom seen in ST or Hubitat code.

For example:

Integer myFunction() {
    return 5
}

instead of def myFunction()..., though this is a bad example because I'm pretty sure the compiler would already infer this ahead of time. Presumably your actual use is more complicated, and explicit types like this can sometimes help catch errors sooner. Similar with variables.

Not sure--just a few guesses. :slight_smile:

But how will it know if I don't have an instance of the app installed yet? Every input is going to be null before you install the app.

I also tried having a call to another function in preferences and that didn't work either.

Now I'm really confused....this works:

def number = getInputs()
(1..number).each{
    def num = it
    input name:"time${num}", type: "time", title: "Daily check #${num}", width: 2, submitOnChange: false
}


def getInputs(){
return 5
}

But if I substitute the other variable (checkNumber) in for "5" I get the null error. I also tried checkNumber.toInteger()
That gives me "cannot invoke toInteger on a null object. But if I remove the reference to checkNumber in the range, that error goes away!!!! WTF is going on?!? LMAO

I didn't see that the value of this variable or return value of the function depended on the value of an app input. In that case, you'll have to use a dynamic page (if you aren't already) and find some way to get that input value committed to settings before you end up needing to use it, likely with a submitOnChange: true on that input. The value of inputs will be stored to settings any time the page is submitted, not just after the entire app is installed.

Here's a minimal-ish example adapted from one of my apps where I need to make sure two previous inputs are defined before displaying remaining preferences. The getNumberOfButtons() function, not shown, returns a value based on the buttonDevice input:

def pageMain() {
    dynamicPage(name: "pageMain", title: "My Page") {
    input(name: "buttonDevice", type: "capability.pushableButton", title: "Select button controller:", multiple: false, required: true, submitOnChange: true)
    input(name: "bulbs", type: "capability.switch", title: "Select lights:", multiple: true, required: true, submitOnChange: true)
    if(settings["buttonDevice"] && settings["bulbs"]) {
        (1..getNumberOfButtons()).each {
	          // more settings here
        }
    }
}

I have it set that way...actually, it is committed as soon as you click away from the input.

What I am trying to do is to get a number of inputs equal to another input. This is what I have today and it works, it is just really long and not particularly clean.

section() {
            input name: "checkNumber", type:"NUMBER", title: "Number of checks to perform each day",description: "Must be a whole number between 1 and 6", range: "1..6", required: true, submitOnChange: true, width:4
        }
section(){
            input(name: "time1", type: "time", title: "First daily check", width: 2, submitOnChange: false)
            if(checkNumber >=2){
                input(name: "time2", type: "time", title: "Second daily check", width: 2, submitOnChange: false)
            }else{
                app.removeSetting("time2")
            }
            if(checkNumber >=3) {
                input(name: "time3", type: "time", title: "Third daily check", width: 2, submitOnChange: false)
            }else{
                app.removeSetting("time3")
            }
            if(checkNumber >= 4){
                input(name: "time4", type: "time", title: "Fourth daily check", width: 2, submitOnChange: false)        
            }else{
                app.removeSetting("time4")
            }
            if(checkNumber >= 5){
                input(name: "time5", type: "time", title: "Fifth daily check", width: 2, submitOnChange: false)        
            }else{
                app.removeSetting("time5")
            }
            if(checkNumber >= 6){
                input(name: "time6", type: "time", title: "Sixth daily check", width: 2, submitOnChange: false)        
            }else{
                app.removeSetting("time6")
            }
        }

So, that's why I was trying to do it the way I saw in yours. I'd been wrestling with it for hours when I found this thread. The submitOnChange gets the page to dynamically update correctly, so I know it is committing to the settings. Maybe it has to be on another page and that's why it's unable to do it. Maybe I'll give that a try, maybe not. Right now I have to walk away from my computer before I throw it out the window!! lol Thanks for trying to help.
:rage: :boom: :face_with_symbols_over_mouth:

So, just for laughs I went through all the trouble of changing everything over to separate dynamic pages. And everything saved!!! But then the page pops the EXACT same error when you try and load the page. And I know 100% sure that the value isn't null because it's defined and saved. You must not be able to define individual inputs this way., only pages.

For fun, I just tried out the code above on my test hub (all on one page, almost exactly what you posted except I removed the width because it displayed poorly on my screen--can't imagine that matters) in a "skeleton" app that does nothing other than show this page. I don't get any errors when loading the page:

definition(
    name: "Test App",
    namespace: "Blah",
    author: "Blah",
    description: "Test app",
    category: "Convenience",
    iconUrl: "",
    iconX2Url: "",
    iconX3Url: ""
)

preferences {
    page(name: "pageMainPage", content: "pageMainPage")
}

def pageMainPage() {
    dynamicPage(name: "pageMainPage") {  
        section() {
            input name: "checkNumber", type:"NUMBER", title: "Number of checks to perform each day",description: "Must be a whole number between 1 and 6", range: "1..6", required: true, submitOnChange: true, width:4
        }
        section(){
            input(name: "time1", type: "time", title: "First daily check", submitOnChange: false)
            if(checkNumber >=2){
                input(name: "time2", type: "time", title: "Second daily check", submitOnChange: false)
            }else{
                app.removeSetting("time2")
            }
            if(checkNumber >=3) {
                input(name: "time3", type: "time", title: "Third daily check", submitOnChange: false)
            }else{
                app.removeSetting("time3")
            }
            if(checkNumber >= 4){
                input(name: "time4", type: "time", title: "Fourth daily check", submitOnChange: false)        
            }else{
                app.removeSetting("time4")
            }
            if(checkNumber >= 5){
                input(name: "time5", type: "time", title: "Fifth daily check", submitOnChange: false)        
            }else{
                app.removeSetting("time5")
            }
            if(checkNumber >= 6){
                input(name: "time6", type: "time", title: "Sixth daily check", submitOnChange: false)        
            }else{
                app.removeSetting("time6")
            }
        }
    }
}

Maybe there is something else somewhere in the app causing this problem? Also, you can do something like this (see the first line in my snippet below) just about anywhere to see if checkNumber is really what you think it is:

        log.debug "checkNumber = $checkNumber"
        if(checkNumber >=2){
            input(name: "time2", type: "time", title: "Second daily check", submitOnChange: false)
        }else{
            app.removeSetting("time2")
        }