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

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?

Additionally, it’s best to display the “raw” code on Github and copy that. This prevents errors from the wrong type of linefeeds and the like.

@rlkoshak & @Tron , thanks for your help! I think i am getting closer now! :slight_smile:

I copied the code again in raw mode and i think this already solved a few problem.

Now i copied TimerMgr.js and timeUtils.js to the expected path and this also solved a few problems.

Thanks, this was another issue: i just had created one astro thing and just added the set120 thing.

I only configured the Ephemeris at MainUI. And this still seems to be a problem. When i trigger the rule i get the following error message:

10:44:11.318 [WARN ] [hemeris.internal.EphemerisManagerImpl] - This dayset is not configured : trash
10:44:11.347 [WARN ] [hemeris.internal.EphemerisManagerImpl] - This dayset is not configured : trash
10:44:11.352 [WARN ] [hemeris.internal.EphemerisManagerImpl] - This dayset is not configured : trash
10:44:11.363 [WARN ] [hemeris.internal.EphemerisManagerImpl] - This dayset is not configured : trash
10:44:11.380 [WARN ] [hemeris.internal.EphemerisManagerImpl] - This dayset is not configured : trash
10:44:11.385 [WARN ] [hemeris.internal.EphemerisManagerImpl] - This dayset is not configured : trash
10:44:11.552 [ERROR] [b.core.model.script.actions.Ephemeris] - Error reading holiday user file /openhab/conf/services/custom1.xml : /openhab/conf/services/custom1.xml
10:44:11.573 [ERROR] [b.core.model.script.actions.Ephemeris] - Error reading holiday user file /openhab/conf/services/custom1.xml : /openhab/conf/services/custom1.xml
10:44:11.585 [ERROR] [b.core.model.script.actions.Ephemeris] - Error reading holiday user file /openhab/conf/services/custom1.xml : /openhab/conf/services/custom1.xml
10:44:11.633 [INFO ] [.openhab.model.script.Rules.TimeOfDay] - Today is a default day.

I created /etc/openhab/conf/services/custom1.xml, but it seems not to work:
(also tried at first with /etc/openhab/conf/custom1.xml)

country=de
region=ni
dayset-workday=[MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY]
dayset-weekend=[SATURDAY,SUNDAY]
dayset-trash=[MONDAY]
dayset-holiday=[]

Can i deactivate trash and holiday somehow? I think actually i wont need them.

This also was an issue that i fixed!

Added ephemeris.cfg with content of custom1.xml and now i only get:

12:49:44.599 [ERROR] [b.core.model.script.actions.Ephemeris] - Error reading holiday user file /openhab/conf/services/custom1.xml : /openhab/conf/services/custom1.xml
12:49:44.607 [ERROR] [b.core.model.script.actions.Ephemeris] - Error reading holiday user file /openhab/conf/services/custom1.xml : /openhab/conf/services/custom1.xml
12:49:44.610 [ERROR] [b.core.model.script.actions.Ephemeris] - Error reading holiday user file /openhab/conf/services/custom1.xml : /openhab/conf/services/custom1.xml
12:49:44.613 [INFO ] [.openhab.model.script.Rules.TimeOfDay] - Today is a default day.

First of all, don’t bother with the trash and custom holiday stuff unless you plan on using them. As I said, those Items are there to make a comprehensive example. You are not required to use them at all. I personally do not.

I’ve not done a whole lot with Ephemeris custom configs beyond testing I did way back when to write the docs and write these rules. It might be the case you need to restart openHAB to pick up the changes to the Ephemeris config files.

Make sure that the permissions on your custom1.xml file are such that user openhab can read it. Make sure that path is correct. I can say right off the bat that a path like that would only be vaild for a manually installed openHAB or openHAB running in Docker.

Can’t look right now, but that path does not seem correct. At least not for most automatic installations.
I think mine’s at /etc/openhab/services
I added a “ephemeris.cfg” config file there, but only because OH3 would keep trashing my Ephemeris settings on each restart. No idea why, but the config file fixed that.

It just contains the bare minimum:

country=xx
region=yy
dayset-workday=Monday,Tuesday,Wednesday,Thursday,Friday
dayset-weekend=Saturday,Sunday

First of all, don’t bother with the trash and custom holiday stuff unless you plan on using them. As I said, those Items are there to make a comprehensive example. You are not required to use them at all. I personally do not.

I’ve not done a whole lot with Ephemeris custom configs beyond testing I did way back when to wrote the docs and wrote these rules. It might be the case you need to restart openHAB to pick up the changes to the Ephemeris config files.

Make sure that the permissions on your custom1.xml file are such that user openhab can read it. Make sure that path is correct. I can say right off the bat that a path like that would only be vaild for a manually installed openHAB or openHAB running in Docker.

Thanks, i copied the custom1.xml to /etc/openhab/services/, too - but no success. I still get the same error message. The ephemeris.cfg i created the, too.

I even thought about permissions, but all my config files i create with vscode on my local device and push them with git to my oh3 vm. So here should be no difference to all other config files.
The file attributes look like they are all the same:

I just saw, that at my runtime.cfg are already settings for ephemeris
 do i have to add it maybe there?

################### EPHEMERIS ###################

# This parameter defines the default list of usual non workable days for the Ephemeris service.
# The value has to be surrounded by square brackets ('[' and ']') and optionally contain value delimiters - a comma ',' to be interpreted as a list of values.
# Example: [SATURDAY,SUNDAY]
#
org.openhab.ephemeris:dayset-weekend=[SATURDAY,SUNDAY]

# This parameter defines the default list of usual workable days for the Ephemeris service.
# The value has to be surrounded by square brackets ('[' and ']') and optionally contain value delimiters - a comma ',' to be interpreted as a list of values.
#
org.openhab.ephemeris:dayset-school=[MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY]

OK, so the actual file is located in /etc/openhab/services. Then the metadata on the Items needs to be /etc/openhab/services/custom1.xml.

If the daysets are working now, don’t touch anything. If the trash dayset disappears in the future, add that to the cfg file.

The custom holidays file is used on a case by case basis. There is no setting for them. When you use a custom file, you must pass the full path to the location of that file. That’s what is wrong here. You are not using the correct path to the file where it’s being used.

The custom1.xml is already placed at this path. Nevertheless i get the error message.
But i checked the meta data of the items again and i actually just got the manual added meta date for the default input widget in it.
Could this be another point? I added the items above with the import of textual definition. Could it happen, that the link to etod got lost at import of the items?
Can i check this / add the link manually now?

I’ll say it again.

Edit the Items metadata to use that path.

Custom metadata doesn’t show up in the list. But you can still get to it by clicking on “Add custom metadata” and using “etod” as the namespace. That will bring up the current etod metadata.

The path to the file needs to be correct in the Item metadata.