[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
Migrating DSL Rules to JSR223/Javascript with a case example
Event Scheduler
Time related heating depending on presence
Whole house lighting
Sun (raised / set) indicator
Design Pattern: Using Item Metadata as an Alternative to Several DPs
[SOLVED] Light window lights 30 minutes before sunset
Time Of Day - allowing for comparisons
[SOLVED] Turn on light in the morning, if sun is down
Design Pattern: Manual Trigger Detection
Design Pattern: Working with Groups in Rules
Day long script
[OH2] [Astro Binding] [Feature Request] Between Sunset and Rise
Design Pattern: Rule Refresh
Variable Astro Sunset event offset
[SOLVED] Time related rule using cron
Another rule not triggering with astro
Trigger rule on DateTime item
Trigger rule on DateTime item
Astro binding triggering twice
Another rule not triggering with astro
Simple time Selection for Shutter movement
Location mode equivalent
[Solved] Postponed rule execution? Do rules get put on stack?
More efficient method for rule
Astro Thing offset not working in Item
Security Lights on with Motion between Astro Events
Time Of Day Rule
Sunrise/Sunset Rule
Fatek Plc
Help! Code clean up - Light Ramping, string concatenation, val or var, member of group
Motion sensor During the Day and socket timmer Triggered from Light switch
Motion sensor During the Day and socket timmer Triggered from Light switch
Event Scheduling
Time Of Day not working on Sunday
Advanced rules in jython
EspMilightHub new binding for milight limitlessLED and easybulb
Using Astro Action from jsr223 library
My presence detection in combination with time of day
DP - Heating Boilerplate
A better strategy for cron only if someone is home (but could come home after the cron time)
[SOLVED] Switching light by daytime and presence
Dimm the light during nighttime
Scénarion en fonction de l'heure sur fuseau horaire
Simple timer rule not working
Astro binding fires events at 00:00:30
[Scripted Automation] Multi-day Time of Day Library
Astro binding provides sunrise events but the rule fires hours later? Odd
[SOLVED] Getting error when using timestamp for managing lights on and off with motion sensor
[SOLVED] How to create a rule
[SOLVED] Timer rule drives me crazy :/ Help welcome
Design Pattern: Motion Sensor Timer
Problem in tutorials: Time of Day Events. "Could not cast NULL to org.eclipse.smarthome.core. "
Extended Motion Sensor Rule: any Suggestions?
Sun state fail randomally
Yet another Heating Setup
Complex Rule optimization
[SOLVED] Grater Equal with DateTime Item in Rule does not work
Lights on Rule With Weather Conditions
Journey to JSR223 Python 9 of 9
Area Triggers --> Area Actions
[SOLVED] Trigger Names with Javascript in JSR223 - Without optional Params
Adapting brightness of Hue lights after physical switch button press
Journey to JSR223 Python 8 of 9
Set criteria to time of day rather than
Depreciated Constructor - What is a better way to do this
Best hardware platform
Time Of Day not working on Sunday
Variable (var) does not update
Design Pattern: State Machine Driven Groups
Doing it smarter - help with rule code rollershutter automation
Struggling with turning off the lights in my rules
Thermostat rules with manual override timeout
How to assign lastUpdate to an item?
Issue with time comparing rule
Global (file wide) variables in Rules DSL
Tutorial on Telegram and Node Red - Interact with OpenHab
[SOLVED] Rule at a specific time
[SOLVED] Astro offset earliest not correct after DST change
Arguments to createTimer
Setting up Astro, Cron, Presence Rules
Compare two TimeDate types
To set Hours, Minutes and Second from HabPanel
[SOLVED] [Advice Needed] Announcement Rule
[SOLVED] AstroBinding - If night then do stuff
Create rule based on sun events and between a time periode
Different temperature sensor for heating trigger depending on time of day
[SOLVED] Daytimes rule not working
TOD - tying to Alarm SLEEP + time
[SOLVED] Turn on light in the morning, if sun is down
Heating weekly schedule - more efficient way to implement this
[SOLVED] Sending a message at a specific time
Sunrise-related logic
[SOLVED] Check the time after Sunset
[SOLVED] Check the time after Sunset
Design Pattern: DRY, How Not to Repeat Yourself in Rules DSL
Writing a DAY/NIGHT rule using Astro
[SOLVED] Help creating timeslots
Lights ON at sunset / Arriving home @ Night lights on
My Christmas Rule
Send Mail some Time before Calendar entry
[SOLVED] Check day of the week / between two hours
My diy HVAC zoning setup
[SOLVED] RULE QUESTION: After sunset do this
[SOLVED] Lambda calling other lambda? (JSR223/JYTHON)
Presence rule with reed switch state & time
[SOLVED] Switch light on when motion detected but only after a certain time
[SOLVED] Switch light on when motion detected but only after a certain time
Help with turning lights on when garage door opens
Iconset: Animated Climacons
How to use system time in rules?
[SOLVED] Morning Routine: Rule with 2 conditions is not working consistently
[SOLVED] Create a 'flexible' workweek
Event/Switch toggles
Switch Relays Based on TIme
Is it possible to......... relating to time
[SOLVED] DateTimeType is deprecated
Rule to check status and change it if necessary (SOLVED)
Rule logic failing
Good Morning, Afternoon, and Evening
Thermostat bathroom - on/off based on sensor value
Help with a loop (Runs too fast / Ignores timers)
Astro set#event used in two .rules files only executes one
Dates in rules - have I got this right
Create timer for sonoff pow tasmota
DateTime, time, and Astro binding questions
Design Pattern: Manual Trigger Detection
[SOLVED] Simple Rule - but i am rusty
NodeRED Flow for "Time of Day" Design Pattern
Rule for Roller Shutter via Astro Binding an Paper UI/rule engine
How to calculate DateTime
Rule on specific months
Design Pattern: Associated Items
Cron expression
Automation/Orchestration Design Patterns
File Include
Design Pattern: Looping Timers
Thread:Sleep apparently shutting down OH2 execution randomly
Rule only acting between two hours
Compare an epoch with now's epoch
Node-RED as Alternative Rule Engine
Fire rule when item changed between hours of midnight and 6
OpenHAB or HAAS
[SOLVED] Openhab 2 Contact Sensor Time Rule
[SOLVED] Help wanted to get Astro rules working
GDPR Compliance and NEW WEBSITE!
How I have automated my lights
Cron expression in if statement
What are your top 3 automations
How to compare DateTimeType of the AstroBinding with "now"
Rule for Rollershutters - with proxy items and reed-contacts
Lambda in rule fails with 'Error during the execution of rule '{RuleName}': null'
Time between x and y to do something
[SOLVED] Help to create a rule "turn on a lamp when motion is detected but only after sunset until sunrise"
My openHab Setup
Average of 1 item during 'sun'
Weird crap! with my HVAC and Openhab
DateTime calculations in rules for Daylight switch
DateTime calculations in rules for Daylight switch
Stop timer and reset to null. Structure of rule okay?
[Solved] iCloud Presence Turn on outside light
Rules if then flag [solved]
Timeclock
Motion detection timer rule
Has someone an Idea of making a rule that checks the time in an presence management heating rule
GCAL in combination with Sunrise/-set?
[HELP] Have I missed something in a Rule?
Weather Underground widget with forecast
Compare DateTime to Static DateTime
Two trigger with a condition each in one single rule
[HELP] Have I missed something in a Rule?
Help simplifying rule
How to compre time?
How to compre time?
Changing cron by profile: design pattern around?
Assumed timing issues with rule
Create a datetime item
Can I get a Phone location within my house?
Astro binding to move switches at sunset/sunrise
Compare Hours/Minutes (NTP Binding)
Rule Push Message to iOS App when Window longer Open
How to make a rule active for only a certain time interval within the day?
Sunset: Light On
Error during the execution of rule: org.eclipse.smarthome.core.library.types.DateTimeType
PaperUI Rules vs HABmin
plusMinutes in rule - how to?
Integrate landscape lighting
Get Time Period on System Start Failing due to getCalendar()
Time of Day Events
Setting light themes in rules
Easy Away Switch / Ruleengine
Configuring Insteon Items to use Astro
Openhab.log goes crazy
Need some guidance on series of time based rules
Making Decisions Based on Time of Day
JSR223 Jython Openhab Imports Erroring?
Announce the time and weather via tts
Switching lights on at dark and of at specific times
Scheduler
Switch value changing to Null. Why!?
Send command with .map with MQTT
TimeSetup in openHAB
Astro binding on snapshot #1031 delayed 30s compared to #1025
Central Heating system control - rules the only option?
Design Pattern: Proxy Item
Astro binding to move switches at sunset/sunrise
OpenHAB or not? (KNX, HUE...)
Rule: Sunrise & motion sensor in order to open rollershutter
How to create a complex light status check rule?
Create daily lighting
Notifications in group design pattern
Compare string variables with equals
Check, whether sun is set in if statement in a rule
Set item by timebound rules
DateTime issue again
[SHARE] MY RULE : The Goal is turn on my garage Lights ON when the garage door is open and it nite time for 10MIN
Light controlled by Zwave + Hue + Kodi
Temperature check for pool pump
I need help with making a rule using astro
Cron rules
Turn Lights On Only if After Sunset and If Presence is Detected
Dimming lights based on time of day
Turn on light an hour after sunset
Example of a Lambda Function for timing the turning on and off of Lights (or switches)
Combine cron with item update rule
Looking for design pattern to handle rules based on presence state
Rule that executes only after a given time
Automation/Orchestration Design Patterns
The method getLocalMillis() is not visible, error
OnSunrise not before 6 but latest at 8
Compairing times
Contact vs Switch items?
[OH2] Rule help to play a random sound file
OH2 AstroBinding in Rule
Help, I'm ready to give up
Set Variable with OH2 Astro Binding
Rules issue in OH2. Is it my rules or OH2?
Set last tripped/alarm date, or last action date to an item - best practice?
A State Machine Primer with HABlladin, the openHAB Genie
How can I get the initial state of an MQTT switch?
'Tis The Season (Almost)
Rule: if month=1,2,3 then
Some newb questions I could not find answer for
Direction and focus
When Time cron not firing
OH2: Need help on rules that require multiple conditions to be true
Creating Capabilities with Rule Templates: Time of Day
Conditional Switch Turn on - OH3
Upgrade 2 to 3 strategy/planning
Upgrade 2 to 3 strategy/planning
Design Pattern implementation: Javascript Time Of Day
Shelly H&T Sensor make lastUpdate Date&Time good readable
Am I doing lights the right way? (Showcase + Question)
OH3 Rule check for improvement
Day/Night state item: Cron vs. system started
Help with rule from OH2.5 to OH3.1
Delay on Rules
(Solved) OH3 - Rule trigger but only if after sunset
Create an own Overview in the Panel for my family to create own scheduled rules for shutters (but without touching rules in the OH3 Section itself)
Cron rule controled by the user
Dynamic Temperature Rule
Ephemeris shedule negation or extension for "workdays"
[SOLVED] Another JavaTime conversion Thread... DateTime
How do others open and close their blinds by schedule?
UI Widget: Weather
Sun Elevation rule to control outside wall lights
Luminance settings for lights
Rule if condition
I need help configuring my heating control
Please advice me about running a time schedule multiple, start & end time
Perfom "reboot" command by openhab
Cloudiness from OpenWeatherMap binding

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