Adding preference option to existing driver/app

So as part of expanding the capabilities of some of my apps and drivers, I need to add new preference options. But it seems that they don't get 'fully added' until I manually click on "Save Preferences" on each app or driver.

For example, I added a new preference, with a default value:

But before I clicked on "Save Preferences" on the driver, it was throwing null errors:

Clicking on Save Preferences resolved it. But that means clicking on that on each and every driver that uses the new preference.

It has a default specified. The user doesn't need to select/enter anything, it should just get the default value, but it doesn't and instead it gets null until "Save Preferences" is clicked.

Is there some way to trigger this to actually save via code?

This is the expected behavior. There is no way to "trigger" this manually. However, you can either set the value of a preference in your installed() method or use your default value if the value is null (often via the ternary conditional operator), among other ways of addressing the issue.

4 Likes

Thanks, I'll do that. When you explain it, it's extremely obvious I could do that... but I totally did not think of doing that.

I've literally got a few other places I'm already using updateSetting()... still didn't think of just checking for null and setting defaults in my code.

I think I'll just write a few 'getter' methods to wrap up accessing the settings values, and if they're accessed and null, then set them to the default and return the default. That should work just fine.

2 Likes
  • Always do null checking.
  • Always think of the stupidest stuff a user could do, and account for it
3 Likes

You wouldn't happen to know if there's a proper way to get the current hub-wide setting for default TTS voice via code, while I have you here?

Asked in this post: Getting current default TTS voice

I have a hack that works, but it's a total hack.

The "Speak" command that's auto-added via the SpeechSynthesis capability seems to have a way to get the default, so it must exist, but I didn't see it documented anywhere.

Have a look at this code, and search for 'Migration'.

Challenge accepted! :wink:

2 Likes

I am not aware of any way this is exposed (including intentionally undocumented location properties I did a quick look at to verify :smiley: ).

Assuming you mean on the device detail page as the pre-selected value, I assume that's just coming from the platform itself (which does, of course, know -- and at some point needs to know -- the value) when displaying the UI.

1 Like

From experience...

Some user, some where. Is guaranteed to out-stupid the stupidest thing you can think of... :rofl::rofl::rofl:

Occasionally, you end up being that user. :open_mouth:

1 Like

I ended up just writing a bunch of quick 'getter' methods for everything:

Handles returning a default if they're null, and in the case of things that should be an integer, returning them as an integer.

Added benefit is they're not Object like the preferences themselves are, so I can use these within @CompileStatic annotations without it complaining.

Wouldn't a hub reboot cause an initialize to run? maybe that would globally reload?

Initialize runs on reboot, yes. But initialize doesn't do whatever "Save Preferences" does to actually write the preferences out to the actual device object state.

From what I can tell Hubitat does something before it does "updated()" when you click on that button.

I just wrote the above 'getters' (now a bit modified from that screenshot to avoid naming conflicts). It works well, and it allows for more flexibility like setting things to Integer on 'get' as well.

1 Like

Adding to the above, initialize() is only called on system start if the driver implements capability "Initialize". I'm guessing this one does (this is pretty much what you'd need to maintain a webscoket connection), but some authors write an initialize() method that they call from both installed() and updated() to more easily share code between the two (even though there is no requirement to use this name for that case alone). There is nothing special about that case.

1 Like

Look again at the code I sent you the link for. Several of the Migration items address that specific issue. It's designed for on-the-fly migration of settings.

FWIW, I would do the migrations in-line rather than with initialize(). In-line will handle deploy of new code in a running hub and does not require a reboot.

A more in-depth fix is to call a function to get each value. For instance:

def get_brightnessStartLevel(){
      if(!settings['brightnessStartLevel']) return settings['device'].currentLevel()
      if(!settings['brightnessStartLevel'] instanceof Integer) settings['brightnessStartLevel'] = settings['brightnessStartLevel'] as Integer // not sure syntax is correct
      if(settings['brightnessStartLevel'] > 100) return 100
      if(settings['brightnessStartLevel'] < 1) return 1
}

Yeah, I'm not using initialize to do it.

Like I said above, I just replaced ALL references in my code to a preference value to instead use a "getter" for them. In that getter it checks for null, and returns a default if it's null. I don't bother setting the preference if it's null since it's not really needed. Once the user selects a preference or clicks on "Save Preferences" it'll no longer be null.

The getter method has the added benefit of being able to do casting, etc. No need to litter my code with "as Integer" because the preference comes in as a String or BigDecimal or whatever and I need Integer. The getter can handle that as well.

And it makes it easy to get the preference from one device in another, since I can just call the getter on the other device.

Yea, maybe it's overkill for your use.

I don't bother setting the preference if it's null since it's not really needed.
No need to litter my code with "as Integer"

But, if you have these sorts of validations for some values, then at some point it becomes easier to do a wrapper for all of them, even if many/most literally just returns the value. The upside is having the same syntax for every variable - as per the example, "get_settingName". You don't need to keep track of if it has been checked, or where. Every single settings variable reference goes through the wrapper. And "get_settingName()" isn't much worse than "settingName" (and I prefer using "settings['settingName'] over just "settingName"). edit: Or _settingsName works too, just more easily confused with non-settings variables. /edit

But yes, overkill to solve just the one null issue, even tho one null issue suggests you'll have another sooner or later :wink:

1 Like

if(!settings['brightnessStartLevel'])

You should explicitly check for null. This will fail in weird ways on Boolean values, and other cases too.

So I have a getter like this:

It explicitly checks for null.

If I wrote it like you're doing, like this:

Where it's checking for Groovy "truth" instead then it will always return true. If disableTrackDataEvents is null, then it returns the 'else' on the ternary, which in this case is true. If it's not null and is set to false, well, that's also a Groovy truth false, so it returns true. If I want to have it as "if this is null, return the else, otherwise return its value" then I need to explicitly check for null.

Or to use your example, if settings['brightnessStartLevel'] = false or null, then it would return settings['device'].currentLevel(). If it's 0 then it'll also return settings['device'].currentLevel() since 0 is 'false' and you're checking for 'not false' in that case.

And since you're using a chain of plain if rather than if-else if, you'll never get down to if(settings['brightnessStartLevel'] < 1) return 1 in the case of a zero.

https://docs.groovy-lang.org/latest/html/documentation/#the-groovy-truth

My bad. I didn't even notice it being "return". :roll_eyes:

And since you're using a chain of plain if rather than if-else if, you'll never get down to if(settings['brightnessStartLevel'] < 1) return 1 in the case of a zero.

Yes, that was by design. My typical assumption is zero is not user-entered (tho not always true) , therefore I'd treat it the same as a null. For the <1 check, I was thinking decimals and/or negative, even not plausible (as a made-up example) since input type would prevent. However, if zero might be either valid, or user-entered, just add a check for zero prior to null, and/or explicitly check "== null". No nesting required, since they're all mutually exclusive.

Personally, I hate inline if's, but to each there own. It's on me for not noticing they're "return".

Your example has a bunch of inline ifs. Or are you referring to the ternary operator?

Personally, I'd just do what you're doing like this:

Integer getBrightnessStartLevel(){
  if(settings['brightnessStartLevel'] == null) { return settings['device'].currentLevel() }
  return Math.max(Math.min(settings['brightnessStartLevel'] as Integer, 100), 0)
}

or this, with a helper function:

Integer getBrightnessStartLevel(){
  if(settings['brightnessStartLevel'] == null) { return settings['device'].currentLevel() }
  return boundedInt(settings['brightnessStartLevel'] as Integer, 100, 0)
}

Integer boundedInt(Integer value, Integer lowBound, Integer hiBound) {
  return Math.max(Math.min(value, hiBound), lowBound)
}