[Deprecated] Design Pattern: Time Of Day

You don’t have to abandon the Rules DSL code. The advantage of the the JavaScript and Jython versions is that you never have to even look at the code. You just import them to your OH and configure your Items. So you can import those that are useful to you and use them and write your own stuff in Rules DSL if that suits you.

You define a DateTime Item for the start time of each time of day. If you want a different set of times for different types of day (as defined in Ephemeris: Actions | openHAB) you define a set of DateTime Items for each type of day. The example in the top post shows a set of DateTime Items for deafult, weekend, a custom day set called trash, holiday, and a custom holiday defined in the /openhab/conf/services/custom1.xml. See the Ephemeris docs (link above) for how to configure Ephemeris and define a custom dayset or custom holidays.

Each Item is a member of the TimesOfDay.

Each Item has an etod metadata. The etod metadata defines the name of the time of day that starts at that time, and what type of day type it applies to. If using a custom dayset or custom holiday there are additional parameters.

Everything is defined at the Item. The start time is the state of the Item. The rest is metadata defined on the Item.

You would need to bootstrap the state of the Item if it doesn’t have a state and doesn’t get a state from somewhere else (e.g. the Astro binding). Somehow the Item needs to get a valid date time value.

To get a ZonedDateTime from a DateTimeType just call zonedDateTime. For example MyDateTime.state.zonedDateTime. You might need to cast it to DateTimeType first. (MyDateTime.state as DateTimeType).zonedDateTime.

But you don’t have to mess with any of this with the JavaScript or Jython version. It’s already implemented for you.

Everything is defined at the Item. The start time is the state of the Item. The rest is metadata defined on the Item.

Yes, that’s how I understood it. So for those times that do not get updated automatically (like Astro) I still need a rule of some sort that updates the state of the (fixed) items, preferably once at the start of the day. I suppose that has to run daily to update the states with the correct date/time. But I think I can figure that out with ZonedDateTime, as I already did with the original rule.

I can see that the use of Ephemeris expands the usefulness of this to different types of days (weekends, holidays, even trash days).

Thanks!

Only the time has to be right. The JavaScript and Python rule will automatically move the date time to today for you.

If you are running OH 3, see OH 3 Examples: How to boot strap the state of an Item. You can define a card that lets you enter the DateTime from the UI. Then you can change it later without messing with Rules.

Of course you can do it with a rule too, but that’s not the only nor even the best way.

Even better. That way it could even be static. But yes, defining a card (or sitemap section) to set these times from the UI has a neat touch to it, too.

I’m running OH3 Milestone 4, btw.

Ultimately, I decided against using the JavaScript. It really seemed to be a lot of hassle to get it working, not all of it applicable to everybody:

  • no real proficiency with JavaScript
  • Necessity to install and test script plus three helper routines
  • Necessity to dive into the use of metadata for items and go away from file-based items/modify them for metadata
  • Find a way to populate/initialize the items (bootstrapping with metadata again)

TBH, that seemed to be a lot of issues just to get the TimeOfDay. I do realize that the new method gives much more possibilities (weekends/holidays and such), though.
And as I was able to McGyver the DSL script with ZonedDateTime and get the Astro items in the right format (not pretty, but works), I guess I’ll stick with the old script for now.
But thanks for the explanation.

The whole point of a library like this is you don’t need to know JavaScript. You should never have to even look at the code.

There is no test script required. The readme should be clear on that. The helper libraries are required though.

??? You don’t have move away from file based .items files at all. You can define metadata using .items files. It goes in between the { }, just like the metadata required for Google Assistant and Alexa goes.

If it works for you that’s great. But I’m eventually going to be unable/unwilling to support Rules DSL at some point in the not too distant future. At that time I’ll probably be removing them Rules DSL examples from all of my Design Patterns.

I might think about it.
For me, a rather simple DSL rule plus a few items is a lot simpler and easier to understand what it does. Maybe that’s just me :wink:

I understand that the DSL rules might not be supported anymore in the future since you’re focused on a different approach.

2 Likes

Hi Rich,

Ok, so I gave it another shot and seem to come back to my conclusion that while I do understand some JavaScript, obviously not enough to troubleshoot OH3. Or maybe it’s a bug in M4 I can’t do anything about.

I tried to get your time_utils as one of the required libraries to run.
While I understand what the JS testscript is trying to do, it fails halfway:

18:51:01.753 [INFO ] [org.openhab.model.script.Rules.Tests ] - parsing the duration
18:51:02.175 [INFO ] [org.openhab.model.script.Rules.Tests ] - PT26H10S
18:51:02.325 [INFO ] [org.openhab.model.script.Rules.Tests ] - 2020-12-03T20:51:12.206312-06:00[America/Chicago]
18:51:02.413 [INFO ] [org.openhab.model.script.Rules.Tests ] - PT243H0.2S
18:51:02.449 [INFO ] [org.openhab.model.script.Rules.Tests ] - It's ISO8601!
18:51:02.462 [INFO ] [org.openhab.model.script.Rules.Tests ] - It's not ISO8601!
18:51:02.477 [ERROR] [.internal.handler.ScriptActionHandler] - Script execution of rule with UID 'timetest' failed: ReferenceError: "ZonedDateTime" is not defined in <eval> at line number 21

So it gets to the point of “dt = ZonedDateTime.now()” where the current date/time is written into the variable dt, but I really don’t get why it thinks that ZonedDateTime.now() would need to be defined.

commenting out those two lines:
//dt = ZonedDateTime.now()
//logger.info("Already ZDT: " + toDateTime(dt));

lets the script complete.

I usually don’t give up easy, but I feel that as this is probably just the first hiccup getting this to run, I really cannot expect your time to help me limp along :wink:

(And out of curiosity, isn’t that line 21 missing its semicolon at the end?)

Well, again, why mess with the test scripts in the first place? Those are for testing and indeed this is something I need to look into (are you on the latest milestone? it makes a difference as the way you trigger a rule on earlier versions of OH 3 can cause certain things to not be available). But it’s not something you need to worry about. The Time of Day rule doesn’t use that part of the library anyway. It only uses the toToday function. The TimerMgr class uses the toDateTime function.

The tests may be failing but they are irrelevant to getting the rule running. And I know the library code and the rule work because I’m using the timeUtils.js library in about half a dozen of my currently running rules, the timerMgr class in four rules, and of course I’m using the Time of Day rule itself.

All you need to do is (assuming you’ve cloned the repo successfully and created the Items):

  1. (from the time_utils folder) cp automation/* /etc/openhab2/automation
  2. from the timer_mgr folder) cp automation/* /etc/openhab2/automation
  3. Create a rule and paste the contents of the YAML file into the code tab of the new rule

You may be running into a hickup, but it’s not a hickup that is related to anything that you should be messing with in the first place.

Thanks.
Being the cautious type, I learned that if part of a system doesn’t run, chances are good that the whole thing itself doesn’t run, either. So, understandably, I was worried when the test didn’t complete…
It was the latest Milestone (M4), btw.
(so $OPENHAB_CONF points to /etc/openhab with out a “2”)

Hmm, seems to work partly. It switched to the fixed-time items, but failed on the dynamic ones from the astro binding. Also, the logs show:

10:20:46.301 [ERROR] [re.automation.internal.RuleEngineImpl] - Failed to execute rule 'timesofdayjs': Fail to execute action: 3

Failed how?

If you know how, put the TimeOfDay logger into DEBUG mode. If not, you can do a find and replace all instances of “logger.debug” with “logger.info” in the rule itself.

And just to make sure, this is OH 3 M4 or later, right? There were some issues with earlier OH 3 versions and the JS rule won’t work in OH 2.5 at all.

I downgraded to M3 because some other isse (which didn’t work with M3, either, so I’m currently on the latest snapshot, which seems to run quite stable).

With “failed”, I mean that the astro-based items are populated, show the correct time, but the script does not change the TimeOfDay item when they happen. When I manually run the script, it does, but not upon the astro trigger. This is what I have, for example:

DateTime Default_Evening "Default Evening [%s]"<time> (TimesOfDay) { channel="astro:sun:local:set#end", etod="EVENING"[type="default"] }
That is initialized correctly to 16:58h.
Openhab correctly triggered the sunset:
Log:
16:58:00.004 [INFO ] [smarthome.event.ChannelTriggeredEvent] - astro:sun:local:set#event triggered END

But no log entry from the JavaScript. I will set that into debug and see if I can find anything.

Running the script manually gives me:
17:06:20.794 [INFO ] [.openhab.model.script.Rules.TimeOfDay] - Today is a default day.
17:06:21.814 [INFO ] [.openhab.model.script.Rules.TimeOfDay] - The current time of day is EVENING
17:06:21.819 [INFO ] [smarthome.event.ItemCommandEvent ] - Item 'TimeOfDay' received command EVENING
17:06:21.821 [INFO ] [smarthome.event.ItemStateChangedEvent] - Item 'TimeOfDay' changed from DAY to EVENING

Edit: Just confirmed that fixed items work and update TimeOfDay correctly.

Very interesting. Can you look in events.log and see if those Items are updating (it usually occurs around 30 seconds after midnight). Do you see any evidence of the rule running more than once at around that time or at all? Or does the rule not run until one minute after midnight?

The way the rule works is it triggers when ever one of the members of TimesOfDay changes (which should occur when Astro updates the Items), then at one minute after midnight (to handle those users who do not use Astro for any of their Items). Finally it runs at openHAB startup. And you can manually run it if you want by pressing the play button on the rule’s page in MainUI. Though this latter method might not work on M3. It’ll complain about some openHAB Actions not existing.

In my system this is what is looks like in events.log

2020-12-04 00:00:30.356 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Holiday_Day' changed from 2020-12-03T07:03:00.000-0700 to 2020-12-04T07:04:00.000-0700
2020-12-04 00:00:30.991 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Default_Day' changed from 2020-12-03T07:03:00.000-0700 to 2020-12-04T07:04:00.000-0700
2020-12-04 00:00:31.022 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Weekend_Day' changed from 2020-12-03T07:03:00.000-0700 to 2020-12-04T07:04:00.000-0700
2020-12-04 00:00:31.101 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Default_Evening' changed from 2020-12-03T16:05:00.000-0700 to 2020-12-04T16:05:00.000-0700
2020-12-04 00:00:31.112 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Holiday_Evening' changed from 2020-12-03T16:05:00.000-0700 to 2020-12-04T16:05:00.000-0700
2020-12-04 00:00:31.114 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Weekend_Evening' changed from 2020-12-03T16:05:00.000-0700 to 2020-12-04T16:05:00.000-0700
2020-12-04 00:00:31.125 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Weekend_Afternoon' changed from 2020-12-03T15:41:00.000-0700 to 2020-12-04T15:41:00.000-0700
2020-12-04 00:00:31.125 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Holiday_Afternoon' changed from 2020-12-03T15:41:00.000-0700 to 2020-12-04T15:41:00.000-0700
2020-12-04 00:00:31.129 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Default_Afternoon' changed from 2020-12-03T15:41:00.000-0700 to 2020-12-04T15:41:00.000-0700
2020-12-04 00:00:31.190 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Default_Bed' changed from 2020-12-03T23:46:00.000-0700 to 2020-12-04T23:46:00.000-0700
2020-12-04 00:00:31.261 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Default_Night' changed from 2020-12-03T23:00:00.000-0700 to 2020-12-04T23:00:00.000-0700
2020-12-04 00:00:46.220 [INFO ] [marthome.event.ChannelTriggeredEvent] - astro:sun:local:morningNight#event triggered START

and this is what it looks like in openhab.log

2020-12-04 00:00:30.835 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Today is a default day.
2020-12-04 00:00:30.932 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Moved Default_Bed to today.
2020-12-04 00:00:30.977 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Moved Default_Night to today.
2020-12-04 00:00:30.979 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Moved Default_Afternoon to today.
2020-12-04 00:00:30.980 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Moved Default_Evening to today.
2020-12-04 00:00:31.989 [INFO ] [openhab.model.script.Rules.TimeOfDay] - The current time of day is EVENING
2020-12-04 00:01:00.810 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Today is a default day.
2020-12-04 00:01:01.829 [INFO ] [openhab.model.script.Rules.TimeOfDay] - The current time of day is EVENING

You can see Astro start updating the Items at 30 seconds after midnight. And 30 seconds after midnight you can see the rule trigger in response to one of those changes and see it moving the static Items to today and then finally figuring out what the current time of day is. Then you can see the rule trigger again at one minute after midnight where all it does is recalculate the current time of day as all the other Items are already forwarded.

Your Item definition looks right. The Item is a member of the TimesOfDay Group and the metadata looks correct. When you run it manually I don’t see it trying to adjust the Astro times of day so they must have a date of today. So the question is whether the Group membership is wrong or the Astro binding is not doing something.

Note that this rule does not depend on the Astro events. It sets it’s own timers to trigger when the time in the DateTime Item occurs. So the fact that the Channel event triggers isn’t informative in this case. We need to see if the Item is changing 30 seconds after midnight to reflect the new time for today.

Hmm, it looks like it’s doing exactly what it should. The TimeOfDay script as well as the Astro binding. This is the events.log:

2020-12-04 00:00:30.193 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Sunrise_Time' changed from 2020-12-03T06:48:00.000-0600 to 2020-12-04T06:49:00.000-0600
2020-12-04 00:00:30.194 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Default_Day' changed from 2020-12-03T06:48:00.000-0600 to 2020-12-04T06:49:00.000-0600
2020-12-04 00:00:30.194 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Sunset_Time' changed from 2020-12-03T16:58:00.000-0600 to 2020-12-04T16:58:00.000-0600
2020-12-04 00:00:30.194 [INFO ] [marthome.event.ChannelTriggeredEvent] - astro:sun:local:morningNight#event triggered START
2020-12-04 00:00:30.202 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Default_Evening' changed from 2020-12-03T16:58:00.000-0600 to 2020-12-04T16:58:00.000-0600
2020-12-04 00:00:30.220 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Default_Bed' changed from 2020-12-03T22:00:00.000-0600 to 2020-12-04T22:00:00.000-0600
2020-12-04 00:00:30.221 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Default_Morning' changed from 2020-12-03T05:15:00.000-0600 to 2020-12-04T05:15:00.000-0600
2020-12-04 00:00:30.221 [INFO ] [marthome.event.ItemStateChangedEvent] - Item 'Default_Night' changed from 2020-12-03T21:00:00.000-0600 to 2020-12-04T21:00:00.000-0600

And this the openhab.log:

2020-12-04 00:00:30.212 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Today is a default day.
2020-12-04 00:00:30.215 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Moved Default_Bed to today.
2020-12-04 00:00:30.217 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Moved Default_Morning to today.
2020-12-04 00:00:30.220 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Moved Default_Night to today.
2020-12-04 00:00:31.228 [INFO ] [openhab.model.script.Rules.TimeOfDay] - The current time of day is NIGHT
2020-12-04 00:01:00.326 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Today is a default day.
2020-12-04 00:01:01.345 [INFO ] [openhab.model.script.Rules.TimeOfDay] - The current time of day is NIGHT

Looks identical to me. And yes, the items are part of the TimesOfDay group.

Still no change. For testing I added triggers to run the script at the astro times, but strangely at sunset it transitioned from “DAY” straight to “BED”, missing out “EVENING” and “NIGHT” entirely.
When I ran it manually some time after that, it transitioned correctly to “EVENING”.

ok, I’m going to need debug logs so I can see which toners it’s creating for those items.

You can configure the logger used by the rule.rule, put everything into debug level logging, or do a find and replace on the rule relaxing replacing the logger.info with logger.debug.

I’ve tried a few things and so far can’t reproduce the problem on my setup.

Will do. And thanks for the support, I really appreciate it.
With the script in DEBUG and running it manually, this is what it logs:

13:03:32.623 [INFO ] [.openhab.model.script.Rules.TimeOfDay] - Today is a default day.                                                                                                                   
13:03:34.127 [DEBUG] [.openhab.model.script.Rules.TimeOfDay] - NOW:    MORNING start time 2020-12-06T05:15-06:00 is in the past  after 2020-12-05T13:03:33.994282-06:00[America/Chicago]
13:03:34.148 [DEBUG] [.openhab.model.script.Rules.TimeOfDay] - NOW:    DAY start time 2020-12-06T06:50-06:00 is in the past  after 2020-12-06T05:15-06:00
13:03:34.152 [DEBUG] [.openhab.model.script.Rules.TimeOfDay] - FUTURE: EVENING scheduleing timer for 2020-12-06T16:58-06:00
13:03:34.211 [DEBUG] [.openhab.model.script.Rules.TimeOfDay] - FUTURE: NIGHT scheduleing timer for 2020-12-06T21:00-06:00
13:03:34.224 [DEBUG] [.openhab.model.script.Rules.TimeOfDay] - FUTURE: BED scheduleing timer for 2020-12-06T22:00-06:00
13:03:34.230 [DEBUG] [.openhab.model.script.Rules.TimeOfDay] - Created 3 time of day timers
13:03:34.232 [INFO ] [.openhab.model.script.Rules.TimeOfDay] - The current time of day is DAY
13:03:34.254 [INFO ] [smarthome.event.ItemCommandEvent     ] - Item 'TimeOfDay' received command DAY  

So the timer for astro/EVENING gets scheduled. I’ll post the logs of what happens at 16:58, aka sunset.

Ok, so at sunset it went straight from DAY to BED, skipping EVENING (set by astro) and NIGHT (set to fixed time):

16:58:00.041 [INFO ] [.openhab.model.script.Rules.TimeOfDay] - Transitioning Time fo Day from DAY to BED                                                                                                                                                                                
16:58:00.053 [INFO ] [smarthome.event.ItemCommandEvent     ] - Item 'TimeOfDay' received command BED                                                                                                     
16:58:00.058 [INFO ] [smarthome.event.ItemStateChangedEvent] - Item 'TimeOfDay' changed from DAY to BED 

No further DEBUG info in the logs. This is my .items file, just for reference:

Group:DateTime TimesOfDay
String TimeOfDay “Current time of day [%s]”

DateTime Default_Morning "Default Morning [%s]" <time> (TimesOfDay) { etod="MORNING"[type="default"] }
DateTime Default_Day "Default Day [%s]" <time> (TimesOfDay) { channel="astro:sun:local:rise#start", etod="DAY"[type="default"] }
DateTime Default_Evening "Default Evening [%s]"<time> (TimesOfDay) { channel="astro:sun:local:set#end", etod="EVENING"[type="default"] }
DateTime Default_Night "Default Night [%s]" <time> (TimesOfDay) { etod="NIGHT"[type="default"] }
DateTime Default_Bed "Default Bed [%s]" <time> (TimesOfDay) { etod="BED"[type="default"] }

Does that help?

Weird. That doesn’t make much sense and it’s double weird that it’s working for me.

There is one thing that might be going on though, but I would expect slightly different behavior. Nashorn JavaScript is really tetchy about scope. I’m guessing that I’m not preserving the scope properly in the timer function that gets set.

Does it run again at 21:00 and say that it’s BED again instead of NIGHT, and then again at 22:00 and finally move to BED correctly?

Thank you for identifying this bug and helping me figure it out. It helps a lot!

Try this version of the Rule and see if that works better.

triggers:
  - id: "1"
    configuration:
      groupName: TimesOfDay
    type: core.GroupStateChangeTrigger
  - id: "2"
    configuration:
      startlevel: 20
    type: core.SystemStartlevelTrigger
  - id: "4"
    label: One minute after midnight
    configuration:
      time: 00:01
    type: timer.TimeOfDayTrigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript
      script: >
        // Imports

        var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.TimeOfDay");

        scriptExtension.importPreset("default");

        this.Ephemeris = (this.Ephemeris === undefined) ? Java.type("org.openhab.core.model.script.actions.Ephemeris") : this.Ephemeris;

        this.ZonedDateTime = (this.ZonedDateTime === undefined) ? Java.type("java.time.ZonedDateTime") : this.ZonedDateTime;


        //   Get Metadata query stuff

        this.FrameworkUtil = (this.FrameworkUtil === undefined) ? Java.type("org.osgi.framework.FrameworkUtil") : this.FrameworkUtil;

        this._bundle = (this._bundle === undefined) ? FrameworkUtil.getBundle(scriptExtension.class) : this._bundle;

        this.bundle_context = (this.bundle_context === undefined) ? this._bundle.getBundleContext() : this.bundle_context;

        this.MetadataRegistry_Ref = (this.MetadataRegistry_Ref === undefined) ? bundle_context.getServiceReference("org.openhab.core.items.MetadataRegistry") : this.MetadataRegistry_Ref;

        this.MetadataRegistry = (this.MetadataRegistry === undefined) ? bundle_context.getService(MetadataRegistry_Ref) : this.MetadataRegistry;

        this.Metadata = (this.Metadata === undefined) ? Java.type("org.openhab.core.items.Metadata") : this.Metadata;

        this.MetadataKey = (this.MetadataKey === undefined) ? Java.type("org.openhab.core.items.MetadataKey") : this.MetadataKey;


        // Constants

        var ETOD_ITEM = "TimeOfDay";

        var ETOD_GROUP = "TimesOfDay";

        var DAY_TYPES = ["default", "weekday", "weekend", "dayset", "holiday", "custom"];

        var EXPECTED = "Invalid metadata for Item! "
                     + "Expected metadata in the form of etod=\"STATE\"[type=\"daytype\", set=\"dayset\", file=\"uri\"] "
                     + "where set is required if type is dayset and file is required if type is custom.";
        var ETOD_NAMESPACE = "etod";


        // Load TimerMgr

        this.OPENHAB_CONF = (this.OPENHAB_CONF === undefined) ? java.lang.System.getenv("OPENHAB_CONF") : this.OPENHAB_CONF;

        load(OPENHAB_CONF+'/automation/lib/javascript/community/timerMgr.js');

        load(OPENHAB_CONF+'/automation/lib/javascript/community/timeUtils.js');


        /**
         * Return the value or a key value from the Item's metadata
         * @param {string} item name of the item
         * @param {string} namespace metadata namespace to pull
         * @param {string} key index into the configuration dict for the value
         * @return {string} value assocaited with key or null if it doesn't exist.
         */
        var getValue = function(item, namespace, key) {
          var md = MetadataRegistry.get(new MetadataKey(namespace, item));
          if(md === null || md === undefined) {
            return null;
          }
          else if(key === undefined) {
            return md.value;
          }
          else {
            return md.configuration[key];
          }
        }


        /**
         * Verify Item and Item metadata
         * @param {string} item name of the Item
         * return {string} error string or null if the metadata checks out
         */
        var verifyMetadata = function(item) {
          if(items[item].class == UnDefType.class) {
            return "Item's state is " + items[items];
          }
          if(getValue(item, ETOD_NAMESPACE) === null) {
            return "Item lacks metadata or metadata value.";
          }
          var type = getValue(item, ETOD_NAMESPACE, "type");
          if(type === null) {
            return "Item lacks a type key."
          }
          
          if(DAY_TYPES.indexOf(type) < 0) {
            return type + " is not a valid day type, expected one of " + DAY_TYPES + ".";
          }
          
          if(type == "dayset" && getValue(item, ETOD_NAMESPACE, "set") === null) {
            return type + " requires a 'set' value to be defined.";
          }
          
          if(type == "custom" && getValue(item, ETOD_NAMESPACE, "file")) {
            return type + " requires a 'file' value to be defined.";
          }
          
          return null;
        }


        /**
         * Get a list of all the Items that have ephem metadata with type
         * @param {java.util.List} etodItems collection of all the ETOD Items
         * @param {string} type the type of day 
         * @return {java.util.List} those Items with a type metadata matching type
         */
        var getType = function(etodItems, type){
          return etodItems.stream()
                          .filter(function(item){ 
                              return getValue(item.name, ETOD_NAMESPACE, "type") == type;
                           })
                          .toArray();
        }


        /**
         * Pull the set of Items for today based on Ephemeris
         * @param {java.util.List} etodItems collection of all ETOD Items
         * @return {java.util.List} only those Items defined for today's daytype
         */
        var getTodayItems = function(etodItems) {
          /** 
          Get the Items for today. Hierarchy is:
            - custom
            - holiday
            - dayset
            - weekend
            - weekday
            - default
          */
          var startTimes = {"default": getType(etodItems, "default"),
                            "weekday": (!Ephemeris.isWeekend()) ? getType(etodItems, "weekday") : [],
                            "weekend": (Ephemeris.isWeekend()) ? getType(etodItems, "weekend") : [],
                            "dayset": etodItems.stream()
                                               .filter(function(item) {
                                                  return getValue(item.name, ETOD_NAMESPACE, "type") == "dayset"
                                                         && Ephemeris.isInDayset(getValue(item.name, ETOD_NAMESPACE, "set"));
                                               })
                                               .toArray(),
                            "holiday": (Ephemeris.isBankHoliday()) ? getType(etodItems, "holiday") : [],
                            "custom": etodItems.stream()
                                               .filter(function(item) {
                                                  return getValue(item.name, ETOD_NAMESPACE, "type") == "custom"
                                                         && Ephemeris.isBankHoliday(0, getValue(item.name, ETOD_NAMESPACE, "file"));
                                               })
                                               .toArray()
                           };

          var dayType = null;
          if(startTimes["custom"].length > 0) {
            dayType = "custom";
          }
          else if(startTimes["holiday"].length > 0) {
            dayType = "holiday";
          }
          else if(startTimes["dayset"].length > 0) {
            dayType = "dayset";
          }
          else if(startTimes["weekend"].length > 0) {
            dayType = "weekend";
          }
          else if(startTimes["weekday"].length > 0) {
            dayType = "weekday";
          }
          else if(startTimes["default"].length > 0) {
            dayType = "default";
          }

          logger.info("Today is a " + dayType + " day.");
          return (dayType === null) ? null : startTimes[dayType];

          
        }


        /**
         * Update Items to today
         * @param {java.util.List} times list of all the ETOD Items for today
         */
        var moveTimes = function(times) {
          var now = ZonedDateTime.now();
          for each(var time in times) {
            if(time.state.zonedDateTime.isBefore(now.withHour(0).withMinute(0).withSecond(0))) {
              events.postUpdate(time.name, toToday(items[time.name]).toString());
              logger.info("Moved " + time.name + " to today.");
            }
          }
        }


        /**
         * Create timers for all Items with a time in the future
         * @param {java.util.List} times list of all the ETOD Items for todayu
         */
        var createTimers = function(times) {
          var now = ZonedDateTime.now();
          var mostRecentTime = now.minusDays(1);
          var mostRecentState = items[ETOD_ITEM];
          
          for each (var time in times) {
            var name = time.name;
            var dt = time.state.zonedDateTime
            var state = getValue(name, ETOD_NAMESPACE);
            
            if(dt.isBefore(now) && dt.isAfter(mostRecentTime)) {
              logger.debug("NOW:    " + state + " start time " + dt + " is in the past " 
                           + " after " + mostRecentTime);
              mostRecentTime = dt;
              mostRecentState = state;
            }
            
            else if(dt.isAfter(now)) {
              logger.debug("FUTURE: " + state + " scheduleing timer for " + dt);
              timers.check(state, dt, etodTransitionGenerator(state));
            }
            
            else {
              logger.debug("PAST  : " + state + " start time of " + dt + " is before " 
                           + now + " and before " + mostRecentState + " " + mostRecentTime);
                          
            }
          }
          
          logger.debug("Created " + (Object.keys(this.timers.timers).length - 1) + " time of day timers");
          logger.info("The current time of day is " + mostRecentState);
          if(items[ETOD_ITEM] != mostRecentState) {
            events.sendCommand(ETOD_ITEM, mostRecentState);
          }
        }


        /**
         * Transition to a new Time of Day
         * @TODO look into moving this to another rule we can call so it shows up in schedule
         * @param {string} state the new time of day state
         */
        var etodTransitionGenerator = function(state) {
          return function() {
            logger.info("Transitioning Time fo Day from " + items[ETOD_ITEM] + " to " + state);
            events.sendCommand(ETOD_ITEM, state);
          }
        }


        //--------------------------------------------

        // Main body of rule

        this.timers = (this.timers === undefined) ? new TimerMgr() : this.timers;


        // Skip if we have a flapping timer set

        if(!this.timers.hasTimer("ephem_tod_rule")) {

          // Check that all the required Items and Groups exist
          if(items[ETOD_ITEM] === undefined) {
            throw "The " + ETOD_ITEM + " Item is not defined!";
          }
          if(items[ETOD_GROUP] === undefined) {
            throw "The " + ETOD_GROUP + " Group is not defined!";
          }

          var etodItems = ir.getItem("TimesOfDay").getMembers();
          
          if(etodItems.size() == 0) {
            throw ETOD_GROUP + " has no members!";
          }

          // Check the metadata for all the relevant Items
          for each (var item in etodItems) {
            var verify = verifyMetadata(item.name);
            if(verify !== null) {
              throw "Item " + item.name + ". " + EXPECTED + " " + verify;
            }
          }

          // Get the time Items for today
          var times = getTodayItems(etodItems);
          if(times === null){
            throw "No set of date times were found for today! Do you have a default set of date times?";
          }
          
          // Update the Items to today
          moveTimes(times);

          // Schedule a timer to wait for all the Items to update before creating the Timers.
          this.timers.check("ephem_tod_rule", 
                            "1s", 
                            function() { createTimers(times);}, 
                            true, 
                            function() { logger.info("Flapping timer, waiting before creating timers for time of day"); });

        }
    type: script.ScriptAction

The changes are minor. I changed the etodTransition function to return a function instead of creating an anonymous function when I create the function to call etodTransition. This will preserve the variable holding the state in the function’s context instead of relying on the global context.

Don’t worry, I don’t expect you to understand any of that. I’m mainly leaving it as a breadcrumb for future readers, including myself. :wink: