[Deprecated] Design Pattern: Time Of Day

It does. At both 2100 and 2200 it transitions from “BED” to “BED”. However, it did not run at the scheduled times this morning when it should have transitioned from “BED” to “MORNING” and ultimately to “DAY”. So as of now (1130) it is still “BED”.

I guess the script just likes staying in bed and sleeping in. :wink:

I’ll implement the script tonight and report back on what it does.

Thanks!

If it doesn’t work post the debug logs too. NOTE: I didn’t convert my debug level statements to info in the above script.

I did not change the script but set the log level for the script to DEBUG within OH3. So that’s fine.

1 Like

I’m not sure if this is related or not, but my modified TOD rule stopped working right a few versions back (2.5.7). I think I’ve narrowed it down to the Astro binding. The sun phase string stops updating after a few days of runtime. At midnight the TRACE logs show the Astro binding setting up the events and phase string changes, but they never fire. Hence screwing up my TOD rule. I’ll keep investigating and following this thread.

If you are right than the problem is going to be with the Astro binding and you should probable open a new thread to reach a wider audience.

The astro binding is not at fault (at least not here), it updates its items correctly.

But some good news here. With the new script, the sunset-transition worked:

2020-12-07 16:58:00.040 [INFO ] [openhab.model.script.Rules.TimeOfDay] - Transitioning Time fo Day from DAY to EVENING

Let’s see what will happen to the other transitions.

Ok, so it got the transitions to NIGHT and BED correctly, but nothing since then. The item’s times are correct.
DEBUG log shows it scheduling the timers for MORNING (fixed time) and DAY (astro) at midnight, but they did not fire. Nothing in the logs.

BUT:
It did, strangely, transition from EVENING to NIGHT at the set time, but then again from NIGHT to NIGHT a few milliseconds later. An hour later, it transitioned from NIGHT to BED also twice, both with the same timestamp.

There was a typo in the script (Transitioning Time fo Day), which I corrected, and one of those two transitions shows “of”, and the other “fo”. I only have one copy of the script in my rules. So maybe the old one is still lurking around in memory somewhere. I’ll restart OpenHAB and see what happens.

Yes indeed, timers don’t get cleaned up so the old ones likely still work hanging around. I’m still trying to figure out a way around that. It’s easy in Python text based rules but not so easy in JSONDB rules.

I know about the typo in the log statement, I just haven’t fixed it yet.

I’m going to post another version of the rule in a bit. There is one more place that could have the same problem I fixed previously. Though I wouldn’t expect it to prevent all the toners from being scheduled.

Sorry for the tardy posting.

Here is another version of the rule that applies the same fix that is applied above to the creation of the flapping timer (I create a timer to wait until all the Astro Items or all the moved Items are updated before running the rule. Otherwise the rule could run a dozen times in a row, once for each DateTimeItem. While that wouldn’t be a problem, I don’t want to spam the logs.

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 createTimersGenerator = function(times, timers) {
          return function() {
            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(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

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

        }
    type: script.ScriptAction
1 Like

Thanks, I’ll try it and see what happens.

No change with the new version.
The evening transitions went flawlessly, but the morning transitions didn’t fire. The script is still in BED (probably nursing a hangover by now).

It did run twice last night, moving dates to today and scheduling all 5 timers at 00:00:30, and then again listing the times and scheduling 0 timers at 00:01:00.

It’s really interesting that it seems to work correctly in your configuration.

I’m going to try using your Item definitions. Those in post 486 are still the ones you are using?

I don’t see any items definitions in #486, but I posted them in #479. Those are the ones I use.

1 Like

Hmmmm. As of yesterday I’m now seeing the same thing. I wonder if there was a change in core that is causing problems. This is going to require a good deal more debugging and because it appears to be time based (if I run the rule manually in the morning it works just fine) it’s going to take some time. It’s almost as if the timers are going away or something like that. It’s clearly creating them.

I agree, at least the logging lines for creating the timers are run, so I would suppose that the if/else if statements run, too.

The latest version didn’t do any transitions, though, but it updates TimeOfDay to the correct value if run manually. So yes, it’s probably somewhere in the main part of the rule where the timers are set. I was thinking that I didn’t implement your TimerMgr correctly (which I think is run , but if you are seeing the same thing, that can’t be it.

That is probably why the DSL script has run conditions for every transition time, so it is run every time it needs to do something. Not going to complain, but that rule runs fine since I rewrote it to ZonedDateTime.

The JavaScript version works exactly like the Jython version which is what drives my production OH 2.5 system (I’m still transitioning so I have both running at the same time. The DSL version has the run conditions because there is no way to access Item metadata from Rules DSL Rules. Consequently there is no way to implement it any other way. But that approach is not suitable for making a reusable library. A reusable library should be usable without modifying the code.

Something changed recently in the core that might be a bug that needs to be filed here. I need more experimentation to figure out if that’s the case though.

At this point I’ve confirmed that the timers are being created. But it seems like they are silently being destroyed before they get a chance to run. I wish there were a way I could reproduce the behavior without waiting a whole day.

1 Like

But that approach is not suitable for making a reusable library. A reusable library should be usable without modifying the code.

I agree, the new version is also much more versatile (thinking about weekends/holidays).

I wish there were a way I could reproduce the behavior without waiting a whole day.

For that purpose I’ve created a test item, put it in the group along with the proper metadata, and set it to a time 10 minutes from now. When the script is run manually, that gets updated along with the others and the timer is scheduled. Or is that too easy?

It’s not a matter of triggering the rule, because when you manually run it, it works. When OH restarts it works. It seems to be only when the rule is triggered at midnight that the timers disappear. And the question is do they disappear immediately, after a few hours, or when?

My first thought was maybe the context is different depending on how the rule is triggered meaning when it’s cron triggered it has a different set of variables from when it’s triggered based on a change or system started. I think I’ve eliminated that as the root cause (one more test is needed to verify).

So now my thought is that the timers are being garbage collected over night. But I need a stretch of time to give the system time to garbage collect them. So far six hours doesn’t seem to be long enough. If that is what’s happening I’ll file a bug in the core because it shouldn’t do that and it didn’t used to do that.

1 Like

Ah, I see, thanks for the clarification.
In that case patience is probably necessary :wink:

Hello,

Just a question about being able to change some of the times (using Rules DSL). In the example, it lists that following TODs are as follows:

  • MORNING: 06:00 to sunrise; Note during some times of year sunrise is before 06:00
  • DAY: Sunrise to 90 minutes before sunset
  • EVENING: 90 minutes before sunset to 23:00
  • NIGHT: 23:00 to 00:100:
  • BED: 00:00 to 06:00

However, if I wanted to change BED to be between 22:30 and 06:00, how would I change that? I understand that the value of ‘now.withTimeAtStartOfDay’ gives a value of midnight (or 00:00), hence the adding/subtracting of days and hours, but how would I get the minutes? Can I give the value of ‘minusHours()’ as a decimal, like this:

val bed_start = now.withTimeAtStartOfDay.plusDays(1).minusHours(2.5)

to get a value of 22:30?

(NOTE: I realize if I change the BED value, I will have to adjust other values like EVENING and NIGHT, but those should be fairly easy once I know if I can use decimals in my ‘minusHours()’ string)