Design Pattern: Simple State Machine (e.g. Time Of Day)

Ok, so you can use Minutes to define the time. A bed start of 22:30 would be:
.plusDays(1).minusHours(2).plusMinutes(30) or even
.plusDays(1).minusHours(1).minusMinutes(30)

I think you can also use seconds, if necessary.

However, I found that the design pattern defines bedtime to start at 00:01. If you want to stretch it over the day change at midnight, as I did, I found that you need to define an additional variable for the end of the day. That’s because at midnight the day changes to the next one and the before/after statements don’t work correctly. Without it, the TimeOfDay changes to “UNKNOWN” at midnight.

I’ve already transitioned to the ZonedDateTime class as I am on Openhab3 and the older classes are not supported anymore.

val  ZonedDateTime      zdt = ZonedDateTime.now()
val  ZonedDateTime      start_of_day = zdt.toLocalDate().atStartOfDay(zdt.getOffset())
val  ZonedDateTime      end_of_day = start_of_day.plusDays(1).minusMinutes(1)

That puts start_of_day at 0:00, and end_of_day at 23:59.
The case part of the rule now looks like this:

case now.isAfter(bed_start)       && now.isBefore(end_of_day):      curr = "BED"
case now.isAfter(start_of_day)    && now.isBefore(morning_start):   curr = "BED"

Hope that helps.

1 Like

Just wanted to bring an update. Curiouser and curiouser to quote Alice in Wonderland. Some days it works and some days it doesn’t and I can’t find a pattern between the two. Still doing more tests. I’ve a bunch of logs to look through later today. Hopefully that will reveal something useful.

If it helps, I’m now on OH3 Milestone 5, and the script continues to run twice at night (00:00:30 and again 00:01:00), moves the date and schedules the timers, but no transitions take place.

That at least tells me that it’s not changed behavior. That’s useful information too. Thanks!

1 Like

Do you see the log “Creating timer manager” at any time other than when OH first starts?

If you insert another trigger to the rule to run, let’s say at noon, does it work correctly? That could be a work around until I get to the bottom of this.

No, nothing about a “timer manager” in the logs.
Do you mean noon as driven by a time or by astro?

I currently have five different times scheduled, some by a fixed time and some by astro. Currently, none of them run.

I’ve inserted a NOON time by astro, let’s see what it does. The script did run automatically on the change of the TimeOfDay group, so at least that works. It did re-create all timers, so they really seem to have been killed somehow between the last run at midnight and now (11:30).

It doesn’t matter. I had a trigger at 1:35 pm that I forgot about over the weekend and discovered that since adding that trigger to test with all the timers went off as they were supposed to (and really messed up my data for testing :frowning: ). That extra trigger might be enough to keep the timers from being garbage collected (if that is in fact what is happening.).

But the lack of that “created timer manager” log is evidence against the problem being garbage collection. If the variables are not saved than when the rule triggers ten seconds from midnight, it will have to recreate the TimeMgr and we would see that log statement. Since we don’t that means the TimerMgr is still there.

It always cancels and recreates all the timers. Doing so is way simpler to implement than to check whih timers already exist, match them up to the new timers, and making sure any timers that don’t need to be rescheduled are cancelled. So I just cancel them all and recreate them all every time the rule runs.

Understood.
I created a NOON item, but it didn’t fire when it should have.
As that was supposed to be only like half an hour from the time it was scheduled, that may be another indication that it’s not some kind of garbage disposal algorithm at work.

Well, if the rule didn’t fire that’s a problem with the rule trigger and not anything related to the rule’s code.

What I meant is that my TimeOfDay item did not transition (DAY to NOON) when it should have.

Maybe we are talking cross purposes. I didn’t mean to create a new Item with ephem_tod metadata. I meant to add a new trigger to the rule so it runs at noon(ish). Your Items would stay the same and there would be no new state to transition to. The Rule would just run more than just at midnight.

Remember, the way this rule works is the Rule should run just once a day (around midnight, though because of the timing of updates from Astro it ends up running twice around midnight). I having you add a new trigger so the rule runs twice a day, 12 hours apart.

Sorry, then I misunderstood you.
I can certainly add another trigger, but since the script ran at 11:30 when I added the NOON-item (verified that in the logs), and the transition to NOON was scheduled to occur only 20 minutes later (and didn’t happen), I’m not sure that would help.

There could be other reasons why that didn’t happen. I can’t verify that it is related to the current problem or something else without spending time looking at the Item definition and a bunch of other stuff. But that does not fit the pattern that is consistent between both your setup and my setup where the times of day that are farther from midnight are the ones that are skipped.

I think it’s a conincidence that those just happen to be the ones linked to Astro for you. For me they are half Astro and half statically set times.

Currently, no transitions take place at all, astro or not. That’s with the latest version you posted here a couple days back. I don’t see a version number in the script, so I can’t tell which version it is exactly.

Really, that us different. Most days I’m getting all the transitions as expected. There were two days where it didn’t transition from BED to DAY. I’ve never had it not transition when it was run again though.

Post your Items as they currently are defined.

Post the logs from the last run or two.

Here is the rule again in case there is something went wrong in the copy and paste.

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
  - id: "5"
    configuration:
      time: 13:43
    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 item + " has " + type + " which is not a valid day type, expected one of " + DAY_TYPES + ".";
          }
          
          if(type == "dayset" && getValue(item, ETOD_NAMESPACE, "set") === null) {
            return item + " has type " + type + " which requires a 'set' value to be defined.";
          }
          
          if(type == "custom" && getValue(item, ETOD_NAMESPACE, "file")) {
            return item + " has type " + type + " which 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 createTimersGenerator = function(times, timers) {
          return function() {
            var now = ZonedDateTime.now();
            var mostRecentTime = now.minusDays(1);
            var mostRecentState = items[ETOD_ITEM];

            logger.info("Cancelling any existing timers");
            timers.cancelAll();
            logger.info("Existing timers have been cancelled");
            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.info("NOW:    " + state + " start time " + dt + " is in the past " 
                             + " after " + mostRecentTime);
                mostRecentTime = dt;
                mostRecentState = state;
              }

              else if(dt.isAfter(now)) {
                logger.info("FUTURE: " + state + " scheduleing timer for " + dt);
                timers.check(state, dt, etodTransitionGenerator(state));
              }

              else {
                logger.info("PAST  : " + state + " start time of " + dt + " is before " 
                             + now + " and before " + mostRecentState + " " + mostRecentTime);

              }
            }

            logger.info("Created " + (Object.keys(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 of Day from " + items[ETOD_ITEM] + " to " + state);
            events.sendCommand(ETOD_ITEM, state);
          }
        }


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

        // Main body of rule

        if(this.timers === undefined){
          logger.info("Creating timer manager");
        }

        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 verify + "\n" + EXPECTED;
            }
          }

          // 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", 
                            "30s", 
                            createTimersGenerator(times, this.timers), 
                            true, 
                            function() { logger.info("Flapping timer, waiting before creating timers for time of day"); });

        }
    type: script.ScriptAction

Notice this one has an additional trigger for around 1:45 pm.

Ok, I used the script you posted, just to be sure.

items:

Group:DateTime TimesOfDay <time>
String TimeOfDay "Current time of day [%s]" <time>
DateTime Default_Morning "MORNING [%s]" <time> (TimesOfDay) { etod="MORNING"[type="default"] }
DateTime Default_Day "DAY [%s]" <time> (TimesOfDay) { channel="astro:sun:local:rise#start", etod="DAY"[type="default"] }
DateTime Default_Evening "EVENING [%s]"<time> (TimesOfDay) { channel="astro:sun:local:set#end", etod="EVENING"[type="default"] }
DateTime Default_Night "NIGHT [%s]" <time> (TimesOfDay) { etod="NIGHT"[type="default"] }
DateTime Default_Bed "BED [%s]" <time> (TimesOfDay) { etod="BED"[type="default"] }

Logs from last run (I had the NOON still in there this night, but took it out now):
Previous run looks the same.

I just noticed that even when it’s run at midnight, it does not get the current time of day right.

2020-12-15 00:00:31.980 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Today is a default day.
2020-12-15 00:00:32.146 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Moved Default_Morning to today.
2020-12-15 00:00:32.153 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Moved Default_Night to today.
2020-12-15 00:00:32.155 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Moved Default_Bed to today.
2020-12-15 00:00:33.603 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - FUTURE: MORNING scheduleing timer for 2020-12-15T05:15-06:00
2020-12-15 00:00:33.677 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - FUTURE: DAY scheduleing timer for 2020-12-15T06:57-06:00
2020-12-15 00:00:33.686 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - FUTURE: EVENING scheduleing timer for 2020-12-15T17:00-06:00
2020-12-15 00:00:33.688 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - FUTURE: NIGHT scheduleing timer for 2020-12-15T21:00-06:00
2020-12-15 00:00:33.689 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - FUTURE: BED scheduleing timer for 2020-12-15T22:00-06:00
2020-12-15 00:00:33.690 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - FUTURE: NOON scheduleing timer for 2020-12-15T11:58-06:00
2020-12-15 00:00:33.695 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - Created 6 time of day timers
2020-12-15 00:00:33.696 [INFO ] [openhab.model.script.Rules.TimeOfDay] - The current time of day is DAY
2020-12-15 00:01:01.031 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Today is a default day.
2020-12-15 00:01:02.060 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - FUTURE: MORNING scheduleing timer for 2020-12-15T05:15-06:00
2020-12-15 00:01:02.070 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - FUTURE: DAY scheduleing timer for 2020-12-15T06:57-06:00
2020-12-15 00:01:02.071 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - FUTURE: EVENING scheduleing timer for 2020-12-15T17:00-06:00
2020-12-15 00:01:02.072 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - FUTURE: NIGHT scheduleing timer for 2020-12-15T21:00-06:00
2020-12-15 00:01:02.073 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - FUTURE: BED scheduleing timer for 2020-12-15T22:00-06:00
2020-12-15 00:01:02.074 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - FUTURE: NOON scheduleing timer for 2020-12-15T11:58-06:00
2020-12-15 00:01:02.076 [DEBUG] [openhab.model.script.Rules.TimeOfDay] - Created 0 time of day timers
2020-12-15 00:01:02.077 [INFO ] [openhab.model.script.Rules.TimeOfDay] - The current time of day is DAY

Running the new script manually, I get:

2020-12-15 10:17:49.496 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Creating timer manager
2020-12-15 10:17:49.909 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Today is a default day.

I’ll see what this does later on…

OK, some weirdness I see.

  1. It’s coming up with the wrong time of day. It should be saying “The current time of day is BED”

  2. I’m not seeing the “Cancelling any existing timers” log statement which makes me think maybe you forgot to click save after copying the YAML over from the most recent version of the rule. That could mean one of the bugs I’ve already fixed is not in your version as of midnight last night.

  3. No timers were created the second time the rule ran at 00:01. That explains why you are not seeing any time of day transitions. And if you are missing the cancelling of the timers (see 2.) that would explain the behavior. I call a method on TimerMgr called “check” which checks to see if a Timer exists and if not creates it. That method has a reschedule argument that defaults to false. So if the Timer exists the second time the rule runs, which it does, then it gets cancelled and not rescheduled. The same would occur when you run it again manually.

So I think that problem (2. and 3.) is already solved at least. Just to make sure, double check that you have a log line of code that mentions cancelling the timers followed by a call to timers.cancelAll() in the createTimersGenerator function.

But I am concerned about it coming up with the wrong time of day at midnight. There might be a bug there I need to look into.

Regarding 1: yes, probably

But an interesting behavior is: Right now it is DAY, so when I manually set the TimeOfDay variable to something else, it does not get corrected on the first run of the script (Only log output I see is “Today is a default day” and that’s it.
However, if I run the script again, I see the full log and the variable gets update.

For 2 & 3, I had to re-set the logging level, but on a manual run I now see the “Canceling any existing timers”, followed by re-creating them.

Pay special attention to the “Created x time of day timers” log statement. X should be the same numbers as lines that say ‘FUTURE’

Now that you mention it, it is always one timer less than FUTURE statements.
But it now did transition to NOON at the correct time. Twice, actually, within a few milliseconds of each other.