Is there a better way to test enum objects? (such as Astro binding moon phase)

Summary

The enum object definitions used by the Astro binding for moon phase#name seem to require conversion toString for logical tests. The value definitions are not imported like ON or CLOSED are. I have working rule code, but would really appreciate your thoughts on if there is A Better Way!

Use Case

After upgrading to OpenHAB 2.4.0 with the v2 Astro binding, I wanted to recreate the moon information message spoken at sunset by a previous MisterHouse set of scripts.

The Astro binding has VERY comprehensive calculation abilities, however testing the resulting Items isn’t simple as values such as Moon Phase are Java objects based on enumerated types.

After looking for MoonPhaseNames in the binding documentation, I went looking in the binding source for the values used by the phase#name channel.

The OH1 binding defines this as public enum MoonPhaseName in MoonPhaseName.java

The OH2 binding looks to have moved the code internally, with the same definition, just in a different MoonPhaseName.java location.

Both define a public enum type with definitions of NEW, WAXING_CRESCENT, FIRST_QUARTER, WAXING_GIBBOUS, FULL, WANING_GIBBOUS, THIRD_QUARTER, WANING_CRESCENT.

I suspect an internal map is used to convert the enum type toString, but I couldn’t find the code.

Could these enum definitions be used directly like other more common state objects, such as ‘ON’ or ‘CLOSED’, and what’s the best way to use these for comparisons?

Testing

To investigate, I added debug to a test rule using the Astro binding.

astro.items file defines:

String	stMoonPhase "Moon Name [%s]"	{channel="astro:moon:local:phase#name"}
Number	nmMoonAgePc "Moon Age % [%.1f]"	{channel="astro:moon:local:phase#agePercent"}

The results below suggest the binding Item object is turned into a text string with the same value as the binding enum.

Test astro.rules code snippets with the corresponding log output

logInfo("AstronomSunset", "stMoonPhase                <" + stMoonPhase + ">")
RESULT: stMoonPhase                <stMoonPhase (Type=StringItem, State=WANING_GIBBOUS, Label=Moon Name, Category=null)>

logInfo("AstronomSunset", "stMoonPhase.state          <" + stMoonPhase.state + ">")
RESULT: stMoonPhase.state          <WANING_GIBBOUS>

logInfo("AstronomSunset", "stMoonPhase.state.toString <" + stMoonPhase.state.toString + ">")
RESULT: stMoonPhase.state.toString <WANING_GIBBOUS>

logInfo("AstronomSunset", "nmMoonAgePc                <" + nmMoonAgePc + ">")
RESULT: nmMoonAgePc                <nmMoonAgePc (Type=NumberItem, State=72.5926333006597, Label=Moon Age %, Category=null)>

logInfo("AstronomSunset", "nmMoonAgePc.state          <" + nmMoonAgePc.state + ">")
RESULT: nmMoonAgePc.state          <72.5926333006597>

Testing of different comparison code showed direct comparison using enum fails, and I could only get working tests using string conversions. This makes some sense, but doesn’t feel elegant as hard coding ‘magic strings’ in tests might risk failure if the binding changes, or if local language text is used.

// attempt to use the enum defines directly
if (stMoonPhase.state == FULL) {<code>}
  RESULT: name 'FULL' cannot be resolved to an item or type;

This suggests the enum type defined in MoonPhaseName.java is not imported (like ON or CLOSED are).

I found two comparisons using conversion to String types worked, but would really appreciate your thoughts on a better/ neater method:

if (stMoonPhase.state.toString == "WANING_GIBBOUS") { <code> }
if (stMoonPhase.state.equals("WANING_GIBBOUS")) { <code> }
  RESULT: Both comparisons work fine.

Working example rule giving a message at sunset with moon information (uses the above astro.items definitions):

rule "AstronomSunset"
when
    Channel 'astro:sun:local:set#event' triggered END
then
    logInfo("AstronomSunset", "Rule entry...")

    say("Notice, the sun is now setting at " + new DateTimeType().format("%1$tl %1$tM") + ".")

    if (stMoonPhase.state.equals("FULL")) {
        logInfo("AstronomSunset", "Full Moon")
        say("Notice, tonight is a full moon.")
    }

    if (stMoonPhase.state.equals("NEW")) {
        logInfo("AstronomSunset", "New Moon")
        say("Notice, tonight is a new moon.")
    }

    logInfo("AstronomSunset", "Rule exit...")
end

A rather ugly and hacky attempt at another calculation might also work (note the use of explicit float values for the percent moon age):

    if ((nmMoonAgePc.state > 49.0) && (nmMoonAgePc.state < 51.0)) {
        logInfo("AstronomSunset", "Alternate Full Moon calc")
        say("Moon Age Percent test says this is a FULL MOON.")
    }

I’d suggest the Astro binding documentation could also benefit from adding the enumerated type text, and example tests such as the above code (that is if there’s not a better way! :wink: )

Thanks for reading my detailed query!

I’m not sure what your question is exactly, but just to confirm:
astro:moon:local:phase#name is a String, as described in the documentation you linked to yourself, so if you want to compare to a desired value, that desired value should also be a string.
So this:

Fails because FULL is not a string, "FULL" is though, so this would work:
if (stMoonPhase.state == "FULL") {<code>}.

So as you suggest, you could indeed define constants

val FULL = "FULL"
val WANING_GIBBOUS = "WANING_GIBBOUS"

but you’d need to maintain these constants in the same way as you would need to do in inline code.
Only benefit would be if you use these a lot, and small changes would require you to only change it in a central place.

1 Like

Hi @RolfV,

Thanks - I didn’t know the difference between val and var so had to dig into the Xtend documentation to find an explanation:

var sStringExample = "Variable"    // variable, can be changed
val STRING_EXAMPLE = "Constant"    // constant, can not be changed

I’m a Unix greybeard, so searching for the differences between a C/C++ const and Java static or final qualifiers started to make by head hurt, but var and val make sense.

After many years of defensive programming, defining ‘magic numbers’ as constants (val) feels a bit bad, but I’m not seeing a way to import the actual definition of the enum type from the original binding Java.

Thanks for the tip!