Best Practice for defining constants?

I haven't bothered with this up until now, but I just started using the VSCode Hubitat plugin, and using linting on my groovy code. The linter doesn't like repeated string constants. Here's an example:

sendEvent(name: "switch", value: "on")

If I have multiple lines of code that can set the switch, the linter raises warnings. And I'd also like to avoid typos, so I'd like to use string constants of some form.

With some googling, I found that in general in Groovy, you can do something like:

public enum ConstantValues {
ON('on'),
OFF('off')
}

However, when I try to save this on my hub, the variety of groovy/framework/etc that is running on the hub throws an error:

Importing [ConstantValues] is not allowed
Expression [MethodCallExpression] is not allowed: org.codehaus.groovy.runtime.InvokerHelper.runScript(Script1, args) at line number -1

Another way I found, that DID work, is to do something like:

def cSWITCH() { 'switch' }

def cON() { 'on' }
def cOFF() { 'off' }

sendEvent(name: cSWITCH(), value: cON(), isStateChange: true)

However, this forces the constants to be functions, which is not my favorite.

Anybody using a better notation for this? Thanks!

1 Like

Ooh, in the Inovelli drivers, I found code like:

@Field static Integer shortDelay = 500		//default delay to use for zwave commands (in milliseconds)
@Field static Integer longDelay = 1000		//long delay to use for changing modes (in milliseconds)
@Field static Integer defaultQuickLevel=50	//default startup level for QuickStart emulation
@Field static List ledNotificationEndpoints = [99]

@Field static Map configParams = [
    parameter001 : [
        number: 1,
        name: "Dimming Speed - Up (Remote)",
        description: "Sets the rate that the light dims up when controlled from the hub. A setting of 'instant' turns the light immediately on.<br>Default=2.5s",
        range: ["0":"instant","5":"500ms","6":"600ms","7":"700ms","8":"800ms","9":"900ms","10":"1.0s","11":"1.1s","12":"1.2s","13":"1.3s","14":"1.4s","15":"1.5s","16":"1.6s","17":"1.7s","18":"1.8s","19":"1.9s","20":"2.0s","21":"2.1s","22":"2.2s","23":"2.3s","24":"2.4s","25":"2.5s (default)","26":"2.6s","27":"2.7s","28":"2.8s","29":"2.9s","30":"3.0s","31":"3.1s","32":"3.2s","33":"3.3s","34":"3.4s","35":"3.5s","36":"3.6s","37":"3.7s","38":"3.8s","39":"3.9s","40":"4.0s","41":"4.1s","42":"4.2s","43":"4.3s","44":"4.4s","45":"4.5s","46":"4.6s","47":"4.7s","48":"4.8s","49":"4.9s","50":"5.0s","51":"5.1s","52":"5.2s","53":"5.3s","54":"5.4s","55":"5.5s","56":"5.6s","57":"5.7s","58":"5.8s","59":"5.9s","60":"6.0s","61":"6.1s","62":"6.2s","63":"6.3s","64":"6.4s","65":"6.5s","66":"6.6s","67":"6.7s","68":"6.8s","69":"6.9s","70":"7.0s","71":"7.1s","72":"7.2s","73":"7.3s","74":"7.4s","75":"7.5s","76":"7.6s","77":"7.7s","78":"7.8s","79":"7.9s","80":"8.0s","81":"8.1s","82":"8.2s","83":"8.3s","84":"8.4s","85":"8.5s","86":"8.6s","87":"8.7s","88":"8.8s","89":"8.9s","90":"9.0s","91":"9.1s","92":"9.2s","93":"9.3s","94":"9.4s","95":"9.5s","96":"9.6s","97":"9.7s","98":"9.8s","99":"9.9s","100":"10.0s","101":"1s","102":"2s","103":"3s","104":"4s","105":"5s","106":"6s","107":"7s","108":"8s","109":"9s","110":"10s","111":"11s","112":"12s","113":"13s","114":"14s","115":"15s","116":"16s","117":"17s","118":"18s","119":"19s","120":"20s","121":"21s","122":"22s","123":"23s","124":"24s","125":"25s","126":"26s","127":"27s","128":"28s","129":"29s","130":"30s","131":"31s","132":"32s","133":"33s","134":"34s","135":"35","136":"36s","137":"37s","138":"38s","139":"39s","140":"40s","141":"41s","142":"42s","143":"43s","144":"44s","145":"45s","146":"46s","147":"47s","148":"48s","149":"49s","150":"50s","151":"51s","152":"52s","153":"53s","154":"54s","155":"55s","156":"56s","157":"57s","158":"58s","159":"59s","160":"60s","161":"1m","162":"2m","163":"3m","164":"4m","165":"5m","166":"6m","167":"7m","168":"8m","169":"9m","170":"10m","171":"11m","172":"12m","173":"13m","174":"14m","175":"15m","176":"16m","177":"17m","178":"18m","179":"19m","180":"20m","181":"21m","182":"22m","183":"23m","184":"24m","185":"25m","186":"26m","187":"27m","188":"28m","189":"29m","190":"30m","191":"31m","192":"32m","193":"33m","194":"34m","195":"35m","196":"36m","197":"37m","198":"38m","199":"39m","200":"40m","201":"41m","202":"42m","203":"43m","204":"44m","205":"45m","206":"46m","207":"47m","208":"48m","209":"49m","210":"50m","211":"51m","212":"52m","213":"53m","214":"54m","215":"55m","216":"56m","217":"57m","218":"58m","219":"59m","220":"60m","221":"61m","222":"62m","223":"63m","224":"64m","225":"65m","226":"66m","227":"67m","228":"68m","229":"69m","230":"70m","231":"71m","232":"72m","233":"73m","234":"74m","235":"75m","236":"76m","237":"77m","238":"78m","239":"79m","240":"80m","241":"81m","242":"82m","243":"83m","244":"84m","245":"85m","246":"86m","247":"87m","248":"88m","249":"89m","250":"90m","251":"91m","252":"92m","253":"93m","254":"94m"],
        default: 25,
        size: 1,
        type: "enum",
        value: null
        ],
    parameter002 : [
        number: 2,

etc...

Are @Fields the preferred way to do this?

1 Like

Fields are the way I do them.

2 Likes

Thats what i do, but i also use global maps for lookups.

2 Likes

I agree that defining a whole method just for this seems like an odd choice (unless maybe you need to compute the value each time)...but maybe there's some advantage I don't see. Otherwise, seems like more overhead than you really need.

Me too. I add in the final modifier but likely mostly for fun, as I don't really think it does much in Groovy in this case (but at least maybe indicates to someone that it's not intended to be changed).

But I don't really think you have to do any of this--Hubitat apps and drivers are normally pretty simple, and I don't see anything wrong with just writing "on" or whatnot if that's what you really mean. I suppose it can help avoid typos, but Groovy will hardly complain if you make a typo in the name of (what you thought was) a variable, anyway, at least not until runtime, so it's not a guarantee either way. :smiley:

1 Like

That's why I was interested in starting to use a linter for this. I've got a lot of apps and drivers now, and I've had less time for fiddling with them recently, and especially less time for testing, so I've gotten into a "don't touch it and you won't break it" rut.

(Now, if I can just figure out how to make useful unit tests for these. I had a go at it a couple years ago. Had an app with complicated interactions between different events in sequence over time. Really wanted to have coded tests, so I wouldn't have to manually test it. Even tried making a version that would run against virtual devices, so that it would truly run inside the hubitat environment. Still, never got it to the point that I felt it was saving me time and headache.)

At one point I entertained the idea of running a Groovy linter on some of my code, but some of the complaints it listed had fixes that a Hubitat app/driver doesn’t even allow you to do. Or silly things like complaining about ‘switch’ being repeated in code.

The couple of things I do is try to make anything that’s more than 30 or so characters a static final constant. And adding @CompileStatic on everything I can, as it provides a lot of resource use reduction from what I can tell. And keep everything that can be async as async. But for things like ‘on’, ‘switch’, etc? It’s silly to make those contestants. Now, if I’ve got code that does a bunch of “sendEvent(name: ‘switch’…)” over and over, I’ll make a method like “sendSwitchOnEvent(String desc)” and “sendSwitchOnEvent(String desc)”, which eliminates a lot more repetition than just the word ‘switch’.

1 Like

I new someone has explained to me why this is important.... Took a while to find it...

One, I do believe @CompileStatic causes this to act like Java, and normalize any duplicate literals. Additionally, @CompileStatic has a ton of other benefits, so it's definitely a better way to spend time than running around assigning duplicate literals to variables. If you're going to spend extra time optimizing code, I think moving as much as possible into @CompileStatic methods is the first thing you should do. Handling repetitive literals should be secondary effort.

Second, unless there's an absolutely absurd number of literals, we're talking bytes of memory here. It really feels like a premature optimization to worry about reducing RAM usage by a fraction of a KB on a device with 1GB of RAM (and usually about 400+ MB free). Sure, allocating that RAM also has a cost, but it's tiny since we're only talking about a few bytes here.

Good to know Groovy doesn't normalize duplicate tho. Almost all my driver/app code is as close to Java as I can get it, as I can't stand when I get runtimes errors because "def" isn't the object type I thought it was. Far too many times do I get "Cannot cast object..." errors in my logs. I just wish the Hubitat Dev docs were better, as more often than not the only way I can even figure out what actual object type I'm dealing with is via getObjectClassName() calls.

3 Likes

I'm not really worried about optimizing ram usage. It's more that I want an IDE that will YELL_AT_ME_IN_RED_SQUIGGLES_AND_ALL_CAPS if I make a typo.

The linter I've been trying out (Groovy Lint, Format and Fix - Visual Studio Marketplace) allows me to disable rules across the entire project, so I've disabled the ones that are asking for a change that wouldn't work on Hubitat. (and I disabled a couple that were asking for changes I just don't want to do.)

One disappointment I have with it is that it doesn't seem to be highlighting if I try to use a variable that doesn't exist. (dynamic language, I know. But I've seen javascript linters handle this.)

Interesting to know about the CompileStatic option. Yeah, I wasn't expecting the memory thing to be a major deal in the vast majority of cases here, was just raising it as why it may be suggested as a good practice in a more general sense.

The other reasons I have seen are things like ease of re-factoring or potential for error if values are not the same, but those are also minor points in many situations.

I have tried, though not as consistently as I perhaps should, to try and specify the class of an object wherever possible, both in method returns and variables I define. It can be a double-edge sword to have such a flexible language sometimes...

1 Like

That's the same linter I had installed. For whatever reason it REFUSED to stop linting literally every file in my workspace. Running on an M2 MacBook it'd chug away for a solid minute (I've got quite a few Hubitat apps/drivers I've written for my own use). Does yours do this? It also complained about the #include statements and would barf and fail to do anything unless I commented them out so it could lint the files. I've got 5 or so libraries I use in almost everything, so needing to comment out the #include statements was a show stopper for me.

Yes, I have found it periodically crashing or chugging.

Just remember that def is synonymous to Object. Loosely typed languages are less expressive, but they have the burden of runtime exception checking for type casting, whereas strictly typed languages can warn you about type casting exceptions and some other runtime exceptions at compile time. We don’t compile groovy, but the linter does provide these warnings for us.

I will look into compile static. I was not aware of that. I’m not a groovy developer, I just see it as a dialect of Java, and treat it as a way to do Java-esque coding in ways that Java doesn’t support out of the box. I try to avoid loose typing. I strongly type all my variable and return types where possible.

1 Like