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

Yes please! Thanks for figuring this out!

Thanks, just found that toToday didn’t work correct any more, it just leaves the minutes and overrides everything else with now.

1 Like

I’ve created a PR. https://github.com/rkoshak/openhab-rules-tools/pull/60

1 Like

Merged. Thanks!

OK, I am almost there. Working in the MainUI is a challenge for me, but I am getting where I need to be. I hope you can answer my last questions.

The defined astro channels are base on “astro:sun:set120:set#start” and channel=“astro:sun:local:set#start”. Where and how is channel set120 defined ? I aussume it’s not the same channel as local. Do I need to define this channel somewhere ?

In the example you define times for the holiday.

// Weekend day, notice that not all the states are listed, the unlisted states are skipped
DateTime Weekend_Day (TimesOfDay) { channel="astro:sun:set120:set#start", etod="DAY"[type="weekend"] }
DateTime Weekend_Evening (TimesOfDay) { channel="astro:sun:local:set#start", etod="EVENING"[type="weekend"] }
DateTime Default_Bed (TimesOfDay) { etod="BED"[type="weekend"] }

// Default holiday
DateTime Weekend_Day (TimesOfDay) { channel="astro:sun:local:set#start", etod="DAY"[type="holiday"] }
DateTime Weekend_Evening (TimesOfDay) { channel="astro:sun:local:set#start", etod="EVENING"[type="holiday"] }
DateTime Default_Bed (TimesOfDay) { etod="BED"[type="holiday"] }

I assume that you have to define other items than I defined for the weekend i.e. Holiday_Evening instead of Weekend_Evening. Am I right ?

Thanks for the all the help. A lot of rules inmu OH2 configuration where based on this design, so I want to get it working in OH3 Javascript

It’s a Thing you define in your Astro binding configuration.

You may define several Astro Things, each has a type (sun, moon) and you set a location. Each creates a full set of predefined channels (sunrise, azimuth etc.).

You might choose to put a sun Thing at a location 30 degrees of longitude to the west of you, and call it mePlus120, because the astro calculations will be two hours behind your real location.

Clear, I can do that ! But shouldnt’ it be :rise#start for the xxxx_“DAY” items then ? Why is the :set#start being used ? I am confused.

Probably. It is an example, not a copy/paste solution.

1 Like

Thanks ! I thought it was a good example to start with, which I could tune to my needs afterwards. Now I changed everything to rise it works !

Don’t worry, you were not the only one having this problem! :wink:

1 Like

Hi!

Today i tried out to add this pattern to my oh3 installation.
I added the item and rules from Rules DSL.

When i trigger the rule thats calculates the tod, i get this error in the log:

21:11:56.785 [ERROR] [.internal.handler.ScriptActionHandler] - Script execution of rule with UID 'time_of_day-1' failed: 'withTimeAtStartOfDay' is not a member of 'java.time.ZonedDateTime'; line 20, column 23, length 24 in time_of_day

Here is the complete rule:

val logName = "Time Of Day"

rule "Calculate time of day state" 
when
  System started or // run at system start in case the time changed when OH was offline
  Item vTimeofDay_Calc changed to ON or
  Channel 'astro:sun:local:rise#event'    triggered START or
  Channel 'astro:sun:local:set#event'     triggered START or
  Channel 'astro:sun:minus90:set#event'  triggered START or
  Time cron "0 1 0 * * ? *" or // one minute after midnight so give Astro time to calculate the new day's times
  Time cron "0 0 6 * * ? *" or
  Time cron "0 0 23 * * ? *"
then

  logInfo(logName, "Calculating time of day...")

  // Calculate the times for the static tods and populate the associated Items
  // Update when changing static times
  // Jump to tomorrow and subtract to avoid problems at the change over to/from DST
  val morning_start = now.withTimeAtStartOfDay.plusDays(1).minusHours(18)
  vMorning_Time.postUpdate(morning_start.toString) 

  val night_start = now.withTimeAtStartOfDay.plusDays(1).minusHours(1)
  vNight_Time.postUpdate(night_start.toString)

  val bed_start = now.withTimeAtStartOfDay
  vBed_Time.postUpdate(bed_start.toString)

  // Convert the Astro Items to Joda DateTime
  val day_start = new DateTime(vSunrise_Time.state.toString) 
  val evening_start = new DateTime(vSunset_Time.state.toString)
  val afternoon_start = new DateTime(vEvening_Time.state.toString)

  // Calculate the current time of day
  var curr = "UNKNOWN"
  switch now {
  	case now.isAfter(morning_start)   && now.isBefore(day_start):       curr = "MORNING"
  	case now.isAfter(day_start)       && now.isBefore(afternoon_start): curr = "DAY"
  	case now.isAfter(afternoon_start) && now.isBefore(evening_start):   curr = "AFTERNOON"
  	case now.isAfter(evening_start)   && now.isBefore(night_start):     curr = "EVENING"
  	case now.isAfter(night_start):                                      curr = "NIGHT"
  	case now.isAfter(bed_start)       && now.isBefore(morning_start):   curr = "BED"
  }

  // Publish the current state
  logInfo(logName, "Calculated time of day is " + curr)
  vTimeOfDay.sendCommand(curr)
end

The Rules DSL version has not, and probably will not be updated to work with OH 3 due to the change from Joda to ZonedDateTime. See Design Pattern: Time Of Day - #622 by lamero for a Rules DSL version that works with OH 3.

Or take this as an opportunity to switch to the JavaScript version which does not require any changes to the rule itself to create new times of day periods. And you can have a different set of times of day based on the Ephemeris day type.

Have a look a bit further up in the thread, I think about December timeframe. There was a rule posted that works.

(Oops, Rich just beat me to it)

Thanks for your fast reply!

I deleted the “old” rule and items and took now the items and rule for JS implementation.
I copied the config and just added another trigger (switch) to test the rules, but i get this error now:

22:02:55.481 [ERROR] [.internal.handler.ScriptActionHandler] - Script execution of rule with UID 'TimeOfDay_JS' failed: <eval>:5:59 Missing close quote
 * Return the value or a key value from the Item's metadata
                                                           ^ in <eval> at line number 5 at column number 59

Please don’t post pictures of code. For one thing you didn’t event include all the code.

I deleted the rule and did again copy and paste from:

Without success and the same error message.

The error message indicates that you have a syntax error in your items file. Maybe post that…

Here’s the thing. Lots of people are using this rule. I need to rule out a copy and paste error. Posting a picture of part of the rule and posting where you copied the rule from is useless to me.

Did you follow the instructions at openhab-rules-tools/ephem_tod at main · rkoshak/openhab-rules-tools · GitHub? You installed the timerMgr.js and timeUtils.js? You created the Items with the necessary Item metadata?

Sorry, i thought its not needed because i just copied and pasted it. Here is the exact code from the rule in OH3:

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 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") === null ) {
            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.debug("Cancelling any existing timers");
            timers.cancelAll();
            logger.debug("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.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 if(this.timers === undefined){
          logger.debug("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

No, i did do this manually.

I just copied the items from first post:

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

// Default day, initialization for JavaScript should be done thgrough MainUI. See https://community.openhab.org/t/oh-3-examples-how-to-boot-strap-the-state-of-an-item/108234
DateTime Default_Morning (TimesOfDay) { etod="MORNING"[type="default"] }
DateTime Default_Day (TimesOfDay) { channel="astro:sun:set120:set#start", etod="DAY"[type="default"] }
DateTime Default_Evening (TimesOfDay) { channel="astro:sun:local:set#start", etod="EVENING"[type="default"] }
DateTime Default_Night (TimesOfDay) { etod="NIGHT"[type="default"] }
DateTime Default_Bed (TimesOfDay) { etod="BED"[type="default"] }

// Weekend day, notice that not all the states are listed, the unlisted states are skipped
DateTime Weekend_Day (TimesOfDay) { channel="astro:sun:set120:set#start", etod="DAY"[type="weekend"] }
DateTime Weekend_Evening (TimesOfDay) { channel="astro:sun:local:set#start", etod="EVENING"[type="weekend"] }
DateTime Default_Bed (TimesOfDay) { etod="BED"[type="weekend"] }

// Custom dayset
DateTime Trash_Morning (TimesOfDay) { etod="MORNING"[type="dayset", set="trash"] }
DateTime Trash_Trashtime (TimesOfDay) { etod="TRASH"[type="dayset", set="trash"]}
DateTime Trash_Day (TimesOfDay) { channel="astro:sun:set120:set#start", etod="DAY"[type="dayset", set="trash"] }
DateTime Trash_Evening (TimesOfDay) { channel="astro:sun:local:set#start", etod="EVENING"[type="dayset", set="trash"] }
DateTime Trash_Night (TimesOfDay) { etod="NIGHT"[type="dayset", set="trash"] }
DateTime Trash_Bed (TimesOfDay) { etod="BED"[type="dayset", set="trash"] }

// Default holiday
DateTime Weekend_Day (TimesOfDay) { channel="astro:sun:set120:set#start", etod="DAY"[type="holiday"] }
DateTime Weekend_Evening (TimesOfDay) { channel="astro:sun:local:set#start", etod="EVENING"[type="holiday"] }
DateTime Default_Bed (TimesOfDay) { etod="BED"[type="holiday"] }

// Custom holiday
DateTime Weekend_Day (TimesOfDay) { channel="astro:sun:set120:set#start", etod="DAY"[type="custom", file="/openhab/conf/services/custom1.xml"] }
DateTime Weekend_Evening (TimesOfDay) { channel="astro:sun:local:set#start", etod="EVENING"[type="custom", file="/openhab/conf/services/custom1.xml"] }
DateTime Default_Bed (TimesOfDay) { etod="BED"[type="custom", file="/openhab/conf/services/custom1.xml"] }

Manually is how it’s installed. Copied to automation/lib/javascript/community.

OK, and do you have those Astro Things defined? Notice there are two Astro things in use in those examples. Do you have Ephemeris configured? Did you define a custom Trash dayset and custom Holiday file for Ephemeris. That example is comprehensive to show how to use all aspects of Ephemeris. But they won’t work if you’ve not configure all aspects of Ephemeris.

For the DateTime Items that are not linked to a Channel, did you manually give them a DateTime? If the Items are NULL or UNDEF errors will occur.

Looking at the code itself, it looks like there are a bunch of missing new lines.

This line:

        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'); /**

does not look like what’s at Github:

        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');
        /**

How exactly did you copy and paste the code? Where did you paste it, on the code tab? Did you do it all at once or line by line?