[Deprecated] Design Pattern: Time Of Day

[Edit] Adde link to Time of Day State Machine

For years now I’ve let this Design Pattern post get out of hand so I’m going to rein it in a bit now. It’s really expanded away from what it’s originally intended to be (i.e. a way to illustrate how to code a time based state machine) into a thread that is mostly about the example, Time of Day. It’s partially my fault for naming the thread “Time of Day”. But there are better alternatives for Time of Day in specific and time based state machines in general now so I’m going to close this down.

So here’s the way this thread will work going forward.

  1. If you want a simple implementation of the Time of Day example to use in your system see Creating Capabilities with Rule Templates: Time of Day. It is easy to install and instantiate and doesn’t require messing with any code. You can build it all in the UI using the Marketplace (OH 3.2 M2 or later required). Problems or questions should be posted there.

  2. If you want a more capable implementation of Time of Day that uses Ephemeris to know what type of day it is (and therefore have a different state machine for each type of day) see Time Based State Machine which can be installed from the marketplace. See openhab-rules-tools/ephem_tod at main · rkoshak/openhab-rules-tools · GitHub for the raw code, though I don’t plan on supporting those going forward. OH 3 users should look at the JavaScript implementation and OH 2 users should look at the Python implementation. If you have problems or questions please open a new thread.

  3. If you have a Rules DSL version of the code below and need help with it, see 1. As far as I’m concerned the Rules DSL version is deprecated and no longer supported (at least by me). If you need to port the Rules DSL version below to OH 3 or are having some other problem with it, it will be faster to start over and use 1 than it will be to fix the existing code.

  4. If you are looking for a way to set up a time based state machine, see 1. Hopefully that thread both illustrates how to set it up but also makes it clear how you can adjust it to apply to other use cases.

  5. If you want to use the time of day as an example to help you write your own code see below. But be aware that the code below is no longer supported and likely will need to be adjusted to work. However, I recommend looking at 1 also for a more recent example to inspire your own code.

Everything below this point is deprecated as of October 26th, 2021.


Please see Design Pattern: What is a Design Pattern and How Do I Use Them for an explanation of what a DP is and how to use them.

Problem Statement

Often in home automation one has certain rules they want to run at certain times or have the home automation exhibit different behaviors for different times of day. The naive approach would be to do the time comparisons inline in each Rule that cares about the time of day. This DP provides an alternative approach.

This DP is particularly suitable for tracking any sequence of events that are denoted by a starting event, not just time. In fact, this DP is really just a simple state machine and can be applied to any similar problem. Other examples include tracking important dates, seasons of the year, controlling an irrigation system (the end of one zone triggers the next zone), etc.

Concept

The overall concept is to create a String Item that represents the current state in the sequence. Rules will check to determine what that state is to determine their behavior or trigger when the String Item changes. One or more additional Rules are responsible for calculating and populating the String Item based on events.

NOTE: The naive approach to implementing this Design pattern is to use Switches, one for each possible state. However, in practice using Switches makes the code more complicated than it needs to be, causes the proliferation of unnecessary Items, and ultimately results in an implementation that is less able to scale when it comes time to add new times of day.

Simple Example: Tracking the Time Periods in a Day

Library implementations

JavaScript MainUI Rule

There is a MainUI Rule JavaScript implementation located at GitHub - rkoshak/openhab-rules-tools: Library functions, classes, and examples to reuse in the development of new Rules.. You can import it by creating a new rule in MainUI, click on the code tab and paste the contents of ephemTimeOfDay.yml into the form. Switch back to the design tab and set a “Schedule” tag and the rule will show up in the calendar under Schedule.

You just create a TimeOfDay String Item which will hold the current state and create a TimesOfDay Group to hold all the time of day DateTime Items.

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"] }

See the readme in the repo for more details.

Python

There is an implementation of tracking time periods in the day that supports a different set of time periods depending on the type of day (e.g. weekend, holiday, etc.) located at GitHub - rkoshak/openhab-rules-tools: Library functions, classes, and examples to reuse in the development of new Rules.. This is a reusable Rule that lets you define the time periods through Item metadata.

The below examples also includes use of the init_items library (see openhab-rules-tools link above) to boot strap static times not based on something like Astro.

// Default day, notice the use of init_items to initialize the state of the Item.
DateTime Default_Morning { init="2020-06-01T06:00:00", etod="MORNING"[type="default"] }
DateTime Default_Day { channel="astro:sun:set120:set#start", etod="DAY"[type="default"] }
DateTime Default_Evening { channel="astro:sun:local:set#start", etod="EVENING"[type="default"] }
DateTime Default_Night { init="23:00:00", etod="NIGHT"[type="default"] }
DateTime Default_Bed { init="00:02:00", etod="BED"[type="default"] }

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

// Custom dayset, defined in [Ephemeris](https://www.openhab.org/docs/configuration/actions.html#ephemeris).
DateTime Trash_Morning { init="06:00:00", etod="MORNING"[type="dayset", set="trash"] }
DateTime Trash_Trashtime { init="07:00:00", etod="TRASH"[type="dayset", set="trash"]}
DateTime Trash_Day { channel="astro:sun:set120:set#start", etod="DAY"[type="dayset", set="trash"] }
DateTime Trash_Evening { channel="astro:sun:local:set#start", etod="EVENING"[type="dayset", set="trash"] }
DateTime Trash_Night { init="23:00:00", etod="NIGHT"[type="dayset", set="trash"] }
DateTime Trash_Bed { init="00:02:00", etod="BED"[type="dayset", set="trash"] }

// Default holiday from Ephemeris.
DateTime Weekend_Day { channel="astro:sun:set120:set#start", etod="DAY"[type="holiday"] }
DateTime Weekend_Evening { channel="astro:sun:local:set#start", etod="EVENING"[type="holiday"] }
DateTime Default_BEd { init="00:02:00", etod="BED"[type="holiday"] }

// Custom holiday defined in Ephemeris.
DateTime Weekend_Day { channel="astro:sun:set120:set#start", etod="DAY"[type="custom", file="/openhab/conf/services/custom1.xml"] }
DateTime Weekend_Evening { channel="astro:sun:local:set#start", etod="EVENING"[type="custom", file="/openhab/conf/services/custom1.xml"] }
DateTime Default_BEd { init="00:02:00", etod="BED"[type="custom", file="/openhab/conf/services/custom1.xml"] }

The rule will automatically create a TimeOfDay Item if it doesn’t already exist. See the library readme for more details.

Rules DSL

In this example we will be tracking the time of day. There will be six states: MORNING, DAY, AFTERNOON, EVENING, NIGHT and BED. Some of the boundaries are statically defined while others are based on sunrise and sunset.

  • 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

Things

The Astro binding is used to generate the events for sunrise and sunset. The Astro binding supports using an offset but it effects all events for that Thing. For example, if you use an offset of -90 minutes, you cannot get an event at sunset -90 and at sunset from the same Thing so you may need to define more than one Astro Thing depending on where you want to put the boundaries.

There are also two ways to define an offset for the Astro binding. You can define the offset on the Thing or you can add 15 degrees to the latitude to add 60 minutes.

Items

String vTimeOfDay "Current Time of Day [MAP(weather.map):%s]" <tod>

DateTime vMorning_Time "Morning [%1$tH:%1$tM]" <sunrise>

DateTime vSunrise_Time "Day [%1$tH:%1$tM]" <sun> { channel="astro:sun:home:rise#start" }

DateTime vSunset_Time "Evening [%1$tH:%1$tM]" <sunset> { channel="astro:sun:home:set#start" }
    
DateTime vNight_Time "Night [%1$tH:%1$tM]" <moon>
	
DateTime vBed_Time "Bed [%1$tH:%1$tM]" <bedroom_blue>

DateTime vEvening_Time "Afternoon [ %1$tH:%1$tM]" <sunset> { channel="astro:sun:minus90:set#start" }

See Items | openHAB for creating the tod icons applied to vTimeOfDay.

Rules

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
  Channel 'astro:sun:home:rise#event'    triggered START or
  Channel 'astro:sun:home: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

// Examples for use of vTimeOfDay
rule "Day time started"
when
  Item vTimeOfDay changed to "DAY" // does not work prior to OH 2.3 Release
then
  // do stuff when DAY starts
end

rule "Some rule"
when
    // some trigger
then
  if(vTimeOfDay.state != "BED") return;

  // do stuff to do when it isn't BED time
end 

Adding a new time of day is as simple as adding a new trigger to the rule to fire when that time of day starts, calculate the DateTime for today when that time period starts, and then adding a new case to the switch statement to determine whether the current time is between when this new time period starts and the next one starts. The isBefore test for the previous time period would have to be updated as well.

Theory of Operation

There is a DateTime Item for the start of each time of day. In the above those DateTimes are statically populated that are not linked to an Astro channel. This can be expanded using one of the Alarm Clock examples or CalDav binding to populate these DateTime Items if desired.

There is also vTimeOfDay which represents the current state.

There is a Rule that gets triggered at the start of each new time of day and it gets triggered at system start. The Rule calulates the current time and determines what the current state of vTimeOfDay should be based on the current Time.

Advantages and Limitations

The major advantage of this approach is it centralizes all of your time of day calculations into one place. This allows:

  • avoidance of duplicated code
  • avoidance of typos and coding errors scattered through the code
  • ease of debugging
  • simpler Rules logic for Rules that care about Time of Day as now they only need to test vTimeOfDay for the current state

The major limitation of this approach is that it does not support overlapping time periods well. It could be expanded to handle that through the addition of additional Time Of Day Items or setting vTimeOfDay to a list of Strings (e.g. “Evening, Twilight”) but that adds complications to the TimeOfDay rule and any Rule that depends upon TimeOfDay.

Complex Example

In this example, the design pattern is applied to a simple three zone irrigation system. The first trigger is time based and the subsequent triggers and states are defined by the end of watering the zones.

The below also excludes error checking and likely has typos and is intended for illustration only. In particular, if OH restarts or reloads the rules file for some reason, the Irrigation will jump to the next Zone no matter how long the previous zone has been watering and it will not make sure the previous zone turned off. Handling these edge cases is beyond the scope of this writing.

Items

String Irrigation
Group gZones
Switch Zone1 (gZones)
Switch Zone2 (gZones) 
Switch Zone3 (gZones)

Rules:

rule "Irrigation control"
when
    System started or
    Time cron "0 0 6 * * ? *" or
    Member of gZones changed to OFF
then
    val currState = Irrigation.state.toString
    val nextState = "Done"

    if(currState == NULL || currState == "Done") nextState = "Zone1"
    else if(currState == "Zone1")                nextState = "Zone2"
    else if(currState == "Zone2")                nextState = "Zone3"
    // If it is "Zone3" the next state is already set to "Done"

    Irrigation.sendCommand(nextState)
end

rule "Zone management"
when
    Item Irrigation received command
then
    sendCommand(receivedCommand.toString, "ON")
    createTimer(now.plusMinutes(5), [ | sendCommand(receivedCommand.toString, "OFF") ] 
end

Theory of Operation

Each Zone is represented by an Item and there is a controlling String Item to represent the state of the Irrigation.

The Rule that calculates the irrigation state gets triggered based on Time, at System started, and when any member of gZones turns OFF. Based on the current state stored in Irrigation the next state is calculated and commanded.

There is a companion Rule that is driven by the Irrigation state that sends the ON command to the zone represented by the state of Irrigation and sets a Timer to turn it OFF in five minutes.

Other implementations

Python version of the Rules DSL above

See Migrating DSL Rules to JSR223/Javascript with a case example for an example of this DP in JavaScript using JSR223 Rules.

Another version that includes more controls for scenes

See Mode (Time of Day) — openHAB Helper Libraries documentation for another approach to implement this DP using Python Rules.

A Python version that uses Ephemeris, Groups, and Item Metadata

The following is a third version which I’ve written but have since re-implemented with the library linked to above. I leave it here for another example of the DP. As such, it is slightly different in use and configuration. This version is no longer maintained and left for illustrative purposed only. I’ve an attempt to incorporate the new Ephemeris capability to allow for the definition of different start times based on the type of day (e.g. different start times for weekends and holidays from work days).

Configuration

This reusable Rule requires configuration through configuration.py and changes to your Items.

configuration.py

tod_group = "TimeOfDay_StartTimes"
tod_item = "vTimeOfDay"

You define the name of the Group that contains all of the start time Items and the name of the Item which represents the current time of day.

Items

To make this Rule reusable the approach has been adjusted to move away from an event driven approach to an Item based approach. Rather than requiring users to define Rule triggers for each starting event they now only need to define DateTime Items for each starting time. These Items can be populated from any source including Astro, CalDav, HABPanel widgets, Rules, and any other source.

I’ve also implemented a way to initialize Items with a statically defined date time using Item metadata (see Design Pattern: Using Item Metadata as an Alternative to Several DPs for details). Any Item that is NULL or UNDEF when the rule is triggered will be updated with the date time stored in the ToD start_time metadata value. Only those Items that need to be initialized need to have this metadata value.

In addition, each DateTime needs the name of the time of day state that the Item represents the start of. This too is stored in Item metadata, the ToD tod_state metadata value.

String vTimeOfDay "Current Time of Day [MAP(weather.map):%s]" <tod>

Group TimeOfDay_StartTimes

DateTime vMorning_Time "Morning [%1$tH:%1$tM]" <sunrise> (TimeOfDay_StartTimes) 
    { ToD="init"[start_time="06:00",tod_state="MORNING"] }

DateTime vDay_Time "Day [%1$tH:%1$tM]" <sun> (TimeOfDay_StartTimes)
    { channel="astro:sun:local:rise#start", ToD="init"[tod_state="DAY"] }

DateTime vAfternoon_Time "Afternoon [ %1$tH:%1$tM]" <sunset> (TimeOfDay_StartTimes)
    { channel="astro:sun:set120:set#start", ToD="init"[tod_state="AFTERNOON"] }

DateTime vEvening_Time "Evening [%1$tH:%1$tM]" <sunset> (TimeOfDay_StartTimes)
    { channel="astro:sun:local:set#start", ToD="init"[tod_state="EVENING"] }
    
DateTime vNight_Time "Night [%1$tH:%1$tM]" <moon> (TimeOfDay_StartTimes)
    { ToD="init"[start_time="23:00", tod_state="NIGHT"] } 

DateTime vBed_Time "Bed [%1$tH:%1$tM]" <bedroom_blue> (TimeOfDay_StartTimes)
    { ToD="init"[start_time="00:02",tod_state="BED"] }

Take note that those Items above that are not linked to a binding to get initialized and updated with a time have a start_time value defined.

Python

from core.rules import rule
from core.triggers import when
from org.joda.time import DateTime
from core.utils import sendCommandCheckFirst
from core.metadata import get_key_value
from time import sleep
from core.actions import ScriptExecution
from configuration import tod_group, tod_item

tod_timers = {}

def tod_init(log):
    """
    Initializes any member of tod_group with the time in it's metadata if it's
    state is UnDefType. This depends upon the rule loading delay from the Helper
    Libraries to ensure that restoreOnStartup and Astro have finished populating
    the Items prior to this function executing.
    """

    log.info("About to initialize Time of Day Items")
    now = DateTime.now()

    # Loop through the undefined Items.
    init_done = False
    for start_time in [t for t in ir.getItem(tod_group).members
                       if isinstance(t.state, UnDefType)]:
        # extract the initialization string from metadata.
        t = get_key_value(start_time.name, "ToD", "start_time")

        # Log an error if there is no such metadata.
        if t is None: log.error("{} is uninitialized and doesn't have metadata!"
                                .format(start_time.name))

        # Parse the init string to HH:MM:SS:MS (SS:MS are both optional).
        else:
            time_parts = t.split(':')
            num_parts = len(time_parts)
            if num_parts < 2: log.error("{} is malformed metadata to initialize"
                                        " {}".format(t, start_time.name))
            else:
                events.postUpdate(start_time,
                            str(now.withTime(int(time_parts[0]),
                                int(time_parts[1]),
                                int(time_parts[2]) if num_parts > 2 else 0,
                                int(time_parts[3]) if num_parts > 3 else 0)))
                init_done = True

    if init_done: sleep(0.2) # Give the Items a chance to update.

    log.info("Done initializing Time of Day")

@log_traceback
def clear_timers():
    # Clear out all the existing timers.
    for name, timer in tod_timers.items():
        new_tod.log.debug("Cleaning out timer for {}.".format(name))
        if timer is not None and not timer.hasTerminated():
            timer.cancel()
        del tod_timers[name]

@log_traceback
def tod_transition(state, log):
    log.info("Transitioning time of day from {} to {}."
             .format(items[tod_item],state))
    events.sendCommand(tod_item, state)

@rule("New Time of Day",
      description="Generic implementation state machine driven by time",
      tags=["designpattern"])
@when("System started")
@when("Time cron 0 1 0 * * ? *")
@when("Member of {} changed".format(tod_group))
def new_tod(event):
    """
    Time of Day: A time based state machine which commands a state Item
    (tod_item in configuration) with the current time of day as a String.

    To define the state machine, create a DateTime Item which will be populated
    with the start times for each of the time of day states. Each of these Items
    must be made members of the tod_group Group.

    The values stored in the Items can come from anywhere but the two most
    common will be from Astro or statically defined. For Astro, just link the
    Item to the appropriate Astro Channel. For statically defined the time is
    defined and the Item initialized through metadata.

    The expected metadata is : Tod="init"[start_time="HH:MM:SS:MS", tod_state="EXAMPLE"]
    where
        - HH is hours
        - MM is minutes
        - SS is seconds and optional
        - MS is milliseconds and optional
        - EXAMPLE is the State String

    All members of tod_group are required to have a tod_state metadata entry.
    Only static Items require the start_time metadata entry.

    This Rule triggeres at system started, one minute after midnight, and if any
    member of tod_group changes. When the Rule triggers, it creates timers to go
    off at the indicated times.
    """
    tod_init(new_tod.log)
    now = DateTime.now()
    clear_timers()

    # Create timers for all the members of tod_group.
    most_recent_time = now.minusDays(1)
    most_recent_state = str(items[tod_item])
    for start_time in ir.getItem(tod_group).members:

        item_time = DateTime(str(start_time.state))
        trigger_time = now.withTime(item_time.getHourOfDay(),
                                    item_time.getMinuteOfHour(),
                                    item_time.getSecondOfMinute(),
                                    item_time.getMillisOfSecond())

        # Update the Item if it's still got yesterday's date.
        if item_time.isBefore(trigger_time):
            events.postUpdate(start_time, str(trigger_time))

        state = get_key_value(start_time.name, "ToD", "tod_state")
        # If there is no state we can't use this Item.
        if state is None:
            new_tod.log.error("{} does not have tod_state metadata!"
                              .format(start_time.name))

        # If we have already passed this time, keep track of the most recent
        # time and state.
        elif (trigger_time.isBefore(now)
                and trigger_time.isAfter(most_recent_time)):
            most_recent_time = trigger_time
            most_recent_state = state
            new_tod.log.debug("The most recent state is now {}".format(state))

        # Create future timers
        elif trigger_time.isAfter(now):
            new_tod.log.debug("Setting timer for Item {}, Time {}, and State {}"
                              ".".format(start_time.name, trigger_time, state))
            tod_timers[start_time.name] = ScriptExecution.createTimer(
                                            trigger_time,
                                            lambda s=state: tod_transition(s,
                                                                   new_tod.log))

        else:
            new_tod.log.debug("{} is in the past but there is a more recent "
                              "state".format(state))

    # Command the time of day to the current state
    new_tod.log.info("Time of day is now {}".format(most_recent_state))
    sendCommandCheckFirst(tod_item, most_recent_state)

def scriptUnloaded():
    clear_timers()

Theory of Operation

The rule triggers when OH starts, a minute after midnight, and whenever a member of tod_group changes. (TODO: Can I trigger a Rule on the change in membership of tod_group?).

First the rule initializes any UNDEF/NULL Item using it’s metadata. Then it clears out any Timers that may be hanging around. Unlike the Rules DSL version above which is Rule Trigger driven, this Rule is Timer driven.

Next the rule loops through all the members of tod_group.

If the DateTime isn’t for today, we update the Item with today’s date.

Then get the name of the state from metadata.

Log an error if the Item lacks the needed metadata. If the Item’s state is before now, keep track of which ever state occurred most recently. If the Item’s state is in the future, create a Timer to transition the tod_state Item to the new state at that time.

Finally, command the tod_state with the most_recent_state.

When the script is unloaded, all Timers are cancelled to keep stray timers from hanging around.

Related Design Patterns

Design Pattern How It’s Used
Design Pattern: Unbound Item (aka Virtual Item) vTimeOfDay, Irrigation, and other Items are examples of a Virtual Item
Design Pattern: Separation of Behaviors This DP is a specific implementation of Separation of Behaviors
A State Machine Primer with HABlladin, the openHAB Genie - #6 by jswim788 This DP is a simplified implementation of a state machine
Design Pattern: Using Item Metadata as an Alternative to Several DPs The Python versions use metadata to define the times of day.

Edit: A near complete rewrite to match formatting of other DP postings and reflect changes in OH 2.3. Eliminated examples for prior versions of OH.
Edit: Use toString instead of going through the long set of calls to get a millis to convert DateTimeType to DateTime
Edit. Remove call to millis in the switch statement.
Edit: Resorted the headings and added a reference to the openhab-rules-tools implementation.

61 Likes

I tried this exact script (was using your old one, which worked like a treat), and it only switches from Day to Twilight and back.

2016-10-21 16:30:00.195 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Twilight"
2016-10-21 16:34:44.832 [INFO ] [hab.model.script.Weather1.rule] - The Weather_Cloudy condition has changed to: false (34)
2016-10-22 05:09:00.146 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Day"
2016-10-22 05:09:00.163 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Day"
2016-10-22 06:34:44.866 [INFO ] [hab.model.script.Weather1.rule] - The Weather_Cloudy condition has changed to: false (32)
2016-10-22 07:34:44.815 [INFO ] [hab.model.script.Weather1.rule] - The Weather_Cloudy condition has changed to: true (28)
2016-10-22 12:34:45.110 [INFO ] [hab.model.script.Weather1.rule] - The Weather_Cloudy condition has changed to: true (26)
2016-10-22 16:31:00.189 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Twilight"
2016-10-22 16:31:00.213 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Twilight"
2016-10-22 18:34:45.073 [INFO ] [hab.model.script.Weather1.rule] - The Weather_Cloudy condition has changed to: true (11)

Also, the log entry appears twice, and I have no idea why.

    if(TimeOfDay.state.toString != currPeriod) {
      logInfo("ToD.rule1", "Setting TimeOfDay to \"" + currPeriod + "\"")
      TimeOfDay.sendCommand(currPeriod)
    }

Any hints appreciated.

1 Like

Hmmmm. I think I can explain the two events.

When Astro triggers an event it toggles the switch ON and then immediately OFF. Try changing the Astro event triggers to received update ON or (I’ll update the code above accordingly.

I also see a typo in the code above. There should be no “Twilight” period.

This is what happens when I try to distill my more complicated code into simpler examples. I’ll also correct it above.

Unfortunately this doesn’t explain what you are seeing. My working rule which has an extra time period and an extra Item I use to easily keep track of the previous time of day. I’ve pasted it below. It has been working for me reliably.

rule "Get time period for right now"
when
        System started or
        Time cron "0 0 6 * * ? *" or             // Morning start
        Item Sunrise_Event received update ON or // Day start
        Item Twilight_Event received update ON or // Twilight start
        Item Sunset_Event received update ON or  //  Evening start
        Time cron "0 0 23 * * ? *"               // Night start
then
    Thread::sleep(50) // lets make sure we are just a little past the time transition

    val morning = now.withTimeAtStartOfDay.plusHours(6).millis
    val sunrise = new DateTime((Sunrise_Time.state as DateTimeType).calendar.timeInMillis)
    val twilight = new DateTime((Twilight_Time.state as DateTimeType).calendar.timeInMillis)
    val evening = new DateTime((Sunset_Time.state as DateTimeType).calendar.timeInMillis)
    val night = now.withTimeAtStartOfDay.plusHours(23).millis

    var currPeriod = "Night"
    if(now.isAfter(morning) && now.isBefore(sunrise)) currPeriod = "Morning"
    else if(now.isAfter(sunrise) && now.isBefore(twilight)) currPeriod = "Day"
    else if(now.isAfter(twilight) && now.isBefore(evening)) currPeriod = "Twilight"
    else if(now.isAfter(evening) && now.isBefore(night)) currPeriod = "Evening"

    if(TimeOfDay.state.toString != currPeriod) {
        logInfo(logNameWeather, "Updating Time of Day {}, Previous Time of Day {}", TimeOfDay.state.toString, currPeriod)
        PreviousTimeOfDay.sendCommand(TimeOfDay.state.toString)
        TimeOfDay.sendCommand(currPeriod)
    }

end
4 Likes

Hey @rlkoshak, I really like this solution. I just feel it does however not fall into the category of Design Patterns. Would you agree? I would call it a snippet, similar to the two solutions already posted here. In comparison your solution is quite neat! :wink:

I put it as a Design Pattern because it can be augmented beyond just Time of Day tracking. It could also be used for Day tracking, it can be used to develop a rule cascade (e.g. something that can be used for controlling one’s irrigation zones), and I’m currently working through using this approach to develop a generic state machine (one that isn’t as much work or more than just custom coding for one’s particular problem).

Perhaps if I illustrate a different non-time based example or added more text to show its generic nature.

1 Like

Hmm… checked my log, and this is what it does:

2016-10-23 05:08:00.211 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Day"
2016-10-23 05:08:00.221 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Day"
2016-10-23 16:31:01.625 [INFO ] [openhab.model.script.ToD.rule1] - Updating Time of Day Day, Previous Time of Day Twilight
2016-10-23 18:01:00.999 [INFO ] [openhab.model.script.ToD.rule1] - Updating Time of Day Day, Previous Time of Day Evening
2016-10-23 22:00:00.218 [INFO ] [openhab.model.script.ToD.rule1] - Updating Time of Day Day, Previous Time of Day Evening
2016-10-24 16:32:01.522 [INFO ] [openhab.model.script.ToD.rule1] - Updating Time of Day to Day, Previous Time of Day was Twilight
2016-10-24 18:02:01.077 [INFO ] [openhab.model.script.ToD.rule1] - Updating Time of Day to Day, Previous Time of Day was Evening

It is always Day… took your code verbatim, and only changed the wording of the log info…

Edit – added:

2016-10-25 20:57:48.626 [INFO ] [openhab.model.script.ToD.rule1] - TimeOfDay morning...: 1477339200000
2016-10-25 20:57:48.921 [INFO ] [openhab.model.script.ToD.rule1] - TimeOfDay sunrise...: 2016-10-25T05:06:00.000+10:00
2016-10-25 20:57:49.130 [INFO ] [openhab.model.script.ToD.rule1] - TimeOfDay twilight..: 2016-10-25T16:33:00.000+10:00
2016-10-25 20:57:49.377 [INFO ] [openhab.model.script.ToD.rule1] - TimeOfDay evening...: 2016-10-25T18:03:00.000+10:00
2016-10-25 20:57:49.582 [INFO ] [openhab.model.script.ToD.rule1] - TimeOfDay night.....: 1477400400000
2016-10-25 20:57:52.078 [INFO ] [openhab.model.script.ToD.rule1] - Updating TimeOfDay: Day, Previous TimeOfDay: Evening

What I have noticed is that sunrise is before morning… morning and night come up as millis, while the others are date/time stamps. Can OH compare these without casting them first?

I have noticed one error in my setup now that I’ve been home to notice (it was a busy week this past week) in that it is not transitioning to Night for me. I do need to look into that but that does not address what you are seeing. Here is my Time Of Day logs for the past few days:

2016-10-21 06:00:00.053 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Evening, Previous Time of Day Morning
2016-10-21 07:17:00.060 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Day
2016-10-21 16:40:00.079 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Day, Previous Time of Day Twilight
2016-10-21 18:10:00.058 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Twilight, Previous Time of Day Evening
2016-10-22 06:52:52.439 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Evening, Previous Time of Day Morning
2016-10-22 07:18:00.062 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Day
2016-10-22 16:39:00.063 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Day, Previous Time of Day Twilight
2016-10-22 18:09:00.058 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Twilight, Previous Time of Day Evening
2016-10-23 06:00:00.093 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Evening, Previous Time of Day Morning
2016-10-23 07:19:00.116 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Day
2016-10-23 16:38:00.060 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Day, Previous Time of Day Twilight
2016-10-23 18:08:00.060 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Twilight, Previous Time of Day Evening
2016-10-24 06:00:00.059 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Evening, Previous Time of Day Morning
2016-10-24 07:20:00.086 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Day
2016-10-24 16:36:00.064 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Day, Previous Time of Day Twilight
2016-10-24 18:06:00.057 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Twilight, Previous Time of Day Evening
2016-10-25 06:00:00.056 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Evening, Previous Time of Day Morning
2016-10-25 07:21:00.058 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Day

That can happen if your sunrise is before 06:00 which it is indeed for me some parts of the year for me, but not at the moment. There might be a bug there, though I as pretty sure I had accounted for that in this rule. Since the actual time the sun comes up moves around (not to mention the daylight savings nonsense) the rule should account for this. It should just skip over Morning entirely if Sunrise is before Morning starts.

That is indeed an inconsistency. They should all be the same. However it works as is because the Joda DateTime class has two versions on .isAfter and .isBefore, one that takes a long (i.e. milliseconds) and one that takes a DateTime (really a parent of DateTime but let’s not bring OO programming and inheritance into this right now). It really isn’t OH that is doing the comparison, it’s the Joda DateTime class.

Updates to make them consistent.

    val morning = new DateTime(now.withTimeAtStartOfDay.plusHours(6).millis)
    val sunrise = new DateTime((Sunrise_Time.state as DateTimeType).calendar.timeInMillis)
    val twilight = new DateTime((Twilight_Time.state as DateTimeType).calendar.timeInMillis)
    val evening = new DateTime((Sunset_Time.state as DateTimeType).calendar.timeInMillis)
    val night = new DateTime(now.withTimeAtStartOfDay.plusHours(23).millis)

I need to think some more on why when I run I’m failing to transition to night and for you it is stuck as Day. What is weird is the “Previous Time of Day” does seem to be changing some.

Are you certain the rule is copied in verbatim? It almost looks like PreviousTimeOfDay is being updated twice and TimeOfDay is not or something like that.

    if(TimeOfDay.state.toString != currPeriod) {
    	logInfo(logNameWeather, "Updating Time of Day {}, Previous Time of Day {}", TimeOfDay.state.toString, currPeriod)
    	PreviousTimeOfDay.sendCommand(TimeOfDay.state.toString)
    	TimeOfDay.sendCommand(currPeriod)
    }

EDIT: Wait, I am seeing weirdness in the timestamps in my logs, though the behavior of my other rules seems to be correct. Investigating…

EDIT 2: My bad, I had the arguments passed to the log statement backward. doh! It should be:L

logInfo(logNameWeather, "Updating Time of Day {}, Previous Time of Day {}", currPeriod, TimeOfDay.state.toString)

So in your logs, “Previous Time of Day” is actually the current and visa versa.

EDIT 3: I do see that indeed you should never see a morning since your sunrise is well before 06:00. And based on the error I see in my log statement I see that it is your PreviousTimeOfDay that is stuck at Day and your TimeOfDay is flipping between Twilight and Evening. That is still very wrong but another data point.

EDIT 4: The lack of the transition to Night may have something to do with the problem described here:

I’ve changed my trigger to use only one cron trigger:

Time cron "0 0 6,23 * * ? *"

We will see if that fixes that one problem tonight.

1 Like

OK, adjusting the cron trigger as described above seems to have addressed the no Night problem I was seeing.

Here is the log from the past day and the rule as it exists.

2016-10-25 07:21:00.058 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Day
2016-10-25 16:35:00.059 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Twilight, Previous Time of Day Day
2016-10-25 18:05:00.061 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Evening, Previous Time of Day Twilight
2016-10-25 23:00:00.058 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Night, Previous Time of Day Evening
2016-10-26 06:00:00.060 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Night
2016-10-26 07:22:00.101 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Day, Previous Time of Day Morning
rule "Get time period for right now" 
when
	System started or
	Time cron "0 0 6,23 * * ? *" or           // Morning start, Night start
	Item Sunrise_Event received update ON or  // Day start
    Item Twilight_Event received update ON or // Twilight start
	Item Sunset_Event received update ON      // Evening start
then
    Thread::sleep(50) // lets make sure we are just a little past the time transition

    val morning = new DateTime(now.withTimeAtStartOfDay.plusHours(6).millis)
	val sunrise = new DateTime((Sunrise_Time.state as DateTimeType).calendar.timeInMillis)
	val twilight = new DateTime((Twilight_Time.state as DateTimeType).calendar.timeInMillis)
	val evening = new DateTime((Sunset_Time.state as DateTimeType).calendar.timeInMillis)
	val night = new DateTime(now.withTimeAtStartOfDay.plusHours(23).millis)

    var currPeriod = "ERROR"
    if     (now.isAfter(morning)  && now.isBefore(sunrise))  currPeriod = "Morning"
    else if(now.isAfter(sunrise)  && now.isBefore(twilight)) currPeriod = "Day"
    else if(now.isAfter(twilight) && now.isBefore(evening))  currPeriod = "Twilight"
    else if(now.isAfter(evening)  && now.isBefore(night))    currPeriod = "Evening"
    else if(now.isAfter(night))                              currPeriod = "Night"

    if(TimeOfDay.state.toString != currPeriod) {
    	logInfo(logNameWeather, "Updating Time of Day {}, Previous Time of Day {}", currPeriod, TimeOfDay.state.toString)
    	PreviousTimeOfDay.sendCommand(TimeOfDay.state.toString)
    	TimeOfDay.sendCommand(currPeriod)
    }
        
end

Compare your version of the rule to the above. Pay particular attention to the if(TimeOfDay.state.toString != currPeriod) section. Based on your logs I suspect the problem lies there.

1 Like

I added a Complicated Example showing how the design pattern applies to other non-time based sequences, in this case controlling the zones of a sprinkler system. In this case the only time based event is the one that kicks off the watering. The rest of the events occur when the Zones turn off. A separate rule get triggered by the Irrigation Item’s state changes.

2 Likes

… well, it depends… maybe complicated in OH :slight_smile:
No, as always, I appreciate your work, and more so sharing of ideas and patterns… as well as replying to noobs like me…

Good Time of Day Design Pattern example rick… added some extra comments here and there as I learned the rule. Let me know if you want those.

With the latest OH2, I found I needed to encapsulate the information as such:

val long sunset_start = new DateTime((Astro_Sun_Set_Time.state as DateTimeType).calendar.timeInMillis).millis
val long sunrise_start = new DateTime((Astro_Sun_Rise_Time.state as DateTimeType).calendar.timeInMillis).millis

for your sunrise and evening values. Did you have to do this to your rules as well?

Actually I’m my current version, which I thought I had posted here, I don’t use the DateTime objects at all any more and just use the milliseconds. There is no reason to create objects as now.isBefore etc all can handle epoc as well.

Upon looking back I do see that I posted it but only in the Astro 2.0 section.

I tried to follow what you did in the OH2 section, but the only solution that I found that properly set the variables with the milliseconds was that convoluted mess. When I did

(Astro_Sun_Set_Time.state as DateTimeType).calendar.timeInMillis

without the extra stuff around it, it complained about calendar not having a timeInMillis field that it could find. Does it look okay to you?

Also, love the trick of doing the offset by changing the geolocation!

Was Designer complaining or the oh logs?

If Designer, which version?

For the moment, please use Eclipse SmartHome Designer 0.8.

If in the log in not sure what the issue could be. The code above is copied verbatim from my running config.

Your rule calls now more than once. Although every call will only differ some milliseconds, from a design pattern perspective it would be stricter to declare a val at the top of the rule that calls now once and use that value across the rule instead of now.

1 Like

It would but I would consider that a micro optimization which is something I typically do not worry about unless and until I actually experience performance, logic, or timing problems. For a rule that executes five times over the course of the day and even with all the calls to now ends up only taking a hundred milliseconds or so to run I wouldn’t worry.

One could argue that it might make the rule more clear and easier to understand in which case I would consider the change.

However, in this case I do not see how defining a new variable to replace the calls to now would actually clarify anything.

What is the specific purpose of assigning a value and not use

var curr = NULL

instead?

Because I can’t sendCommand(null). It will generate an error. I could use curr = NULL which would set the state of vTimeOfDay to undefined but that would mean that whereever I check vTimeOfDay for some reason I’d need to check if it is NULL in addition to checking if it is “Evening”, for example. By using the String “UNKNOWN” I can avoid those extra checks and since the “UNKNOWN” state doesn’t drive any behavior it will not negatively impact any of my rules.

Honestly, “UNKNOWN” should never actually be sent to to the vTimeOfDay unless there is an error in the rule. I use it mainly to catch errors without breaking my other rules.

2 Likes

I am testing a rule with the clause

rule MyRule
when
    Item TimeOfDay received command "AFTERNOON"
then
    // do stuff
end

but it doesn’t seem to trigger. I know it is possible to use received command ON for switches. Is it possible at all to use a string after the received command? Or is it only possible to use

rule MyRule
when
    Item TimeOfDay received command
then
    switch(receivedCommand) {
        case "AFTERNOON" : // do some stuff
        case "BED" : // do other stuff
    }
end

I use the second method you mentioned. Something like this:

rule MyRule
when
    Item TimeOfDay received command
then
    if(TimeOfDay.state.toString == "AFTERNOON") {
                                                   //Do some stuff                            
    }
end
1 Like