[Deprecated] Design Pattern: Time Of Day

To fit your existing DP, one solution for Daylight Savings would be to work from the end of the day, in order to stay away from 2am. So instead of:

var long Sunday_Morning     = now.withTimeAtStartOfDay.plusHours(6).millis

it would be:

var long Sunday_Morning     = now.plusDays(1).withTimeAtStartOfDay.minusHours(18).millis
1 Like

DST has so many edge cases.

I think that will work as long as we don’t use 2am to 3am as a boundary for a time period, which I think would be rare in a home automation context.

Here’s another option using org.joda.time.Interval, but it doesn’t quite fit into the current DP. Intervals are half-open (start inclusive, end exclusive). I’ve added some other examples in too, for people who use Astro.

import java.util.LinkedHashMap

rule "Mode: Update Mode"
when
	System started
    or
    Time cron "0 0 7 * * ?"
    or
    Time cron "0 0 9 * * ?"
    or
    Time cron "0 55 17 * * ?"
    or
    Time cron "0 0 21 * * ?"
then
    val LinkedHashMap<String,Interval> modeIntervals = newLinkedHashMap(
        "Morning"   -> new Interval(now.withTime(7,0,0,0),now.withTime(9,0,0,0)),
        "Day"       -> new Interval(now.withTime(9,0,0,0),now.withTime(17,55,0,0)),
        //"Day"       -> new Interval(now.withTime(9,0,0,0),new DateTime((nauticDuskStart.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)),
        //"Day"       -> new Interval(new Duration(java.util.concurrent.TimeUnit.HOURS.toMillis(5)),new DateTime((nauticDuskStart.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)),
        "Evening"   -> new Interval(now.withTime(17,55,0,0),now.withTime(21,0,0,0))
        //"Evening"   -> new Interval(new DateTime((nauticDuskStart.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli),now.withTime(21,0,0,0))
    )
    var String newMode = "Night"// it was easiest to do it this way, since this Mode wraps around midnight
    for (mode : modeIntervals.keySet) {
        if (modeIntervals.get(mode).contains(now)) {
            newMode = mode
        }
        logDebug("Rules", "Update Mode: interval=[{}], name=[{}]",modeIntervals.get(mode),mode)
    }
    if (newMode != Mode.state.toString) {
        Mode.sendCommand(newMode)
        logDebug("Rules", "Update Mode: Mode changed to [{}]",newMode)
    }
end

I use something similar for audio alerts. An Interval can be Interval(Instant, Instant), or Interval(Duration, Instant). When our Mode changes, I play a status alert which includes these…

    val String tempTotalSolarEclipse = if (new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(7)),new DateTime((Sun_Eclipse_Total.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)).contains(now)) "\nSpecial Event! There is a Total Solar Eclipse this week on " + Sun_Eclipse_Total.state.format("%1$tA at %1$tl:%1$tM%1$tp.") else ""
    val String tempPartialSolarEclipse = if (new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(7)),new DateTime((Sun_Eclipse_Partial.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)).contains(now)) "\nSpecial Event! There is a Partial Solar Eclipse this week on " + Sun_Eclipse_Partial.state.format("%1$tA at %1$tl:%1$tM%1$tp.") else ""
    val String tempRingSolarEclipse = if (new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(7)),new DateTime((Sun_Eclipse_Ring.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)).contains(now)) "\nSpecial Event! There is a Ring Solar Eclipse this week on " + Sun_Eclipse_Ring.state.format("%1$tA at %1$tl:%1$tM%1$tp.") else ""
    val String tempTotalLunarEclipse = if (new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(7)),new DateTime((Moon_Eclipse_Total.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)).contains(now)) "\nSpecial Event! There is a Total Lunar Eclipse this week on " + Moon_Eclipse_Total.state.format("%1$tA at %1$tl:%1$tM%1$tp.") else ""
    val String tempPartialLunarEclipse = if (new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(7)),new DateTime((Moon_Eclipse_Partial.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)).contains(now)) "\nSpecial Event! There is a Total Lunar Eclipse this week on " + Moon_Eclipse_Partial.state.format("%1$tA at %1$tl:%1$tM%1$tp.") else ""
    val String tempSeasonSpring = if (new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(7)),new DateTime((Season_Spring.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)).contains(now)) "\nSpecial Event! Spring starts this week on " + Season_Spring.state.format("%1$tA at %1$tl:%1$tM%1$tp.") else ""
    val String tempSeasonSummer = if (new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(7)),new DateTime((Season_Summer.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)).contains(now)) "\nSpecial Event! Summer starts this week on " + Season_Summer.state.format("%1$tA at %1$tl:%1$tM%1$tp.") else ""
    val String tempSeasonAutumn = if (new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(7)),new DateTime((Season_Autumn.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)).contains(now)) "\nSpecial Event! Autumn starts this week on " + Season_Autumn.state.format("%1$tA at %1$tl:%1$tM%1$tp.") else ""
    val String tempSeasonWinter = if (new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(7)),new DateTime((Season_Winter.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)).contains(now)) "\nSpecial Event! Winter starts this week on " + Season_Winter.state.format("%1$tA at %1$tl:%1$tM%1$tp.") else ""
    val String tempStartDST = if (new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(7)),parse(nthDayWithinMonth.apply(2, 7, 3) + "T00:00")).contains(now)) "\nSpecial Event! Daylight Savings Time starts this week." else ""
    val String tempEndDST = if (new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(7)),parse(nthDayWithinMonth.apply(1, 7, 11) + "T00:00")).contains(now)) "\nSpecial Event! Daylight Savings Time ends this week." else ""
    val String tempSapFlow = if (now.getMonthOfYear < 5 && ((Weather_Temp_Max_F_0.state > 32 && Weather_Temp_Min_F_0.state <= 32) || (Weather_Temp_Max_F_1.state > 32 && Weather_Temp_Min_F_1.state <= 32) || (Weather_Temp_Max_F_2.state > 32 && Weather_Temp_Min_F_2.state <= 32) || (Weather_Temp_Max_F_3.state > 32 && Weather_Temp_Min_F_3.state <= 32) || (Weather_Temp_Max_F_4.state > 32 && Weather_Temp_Min_F_4.state <= 32) || (Weather_Temp_Max_F_5.state > 32 && Weather_Temp_Min_F_5.state <= 32) || (Weather_Temp_Max_F_6.state > 32 && Weather_Temp_Min_F_6.state <= 32) || (Weather_Temp_Max_F_7.state > 32 && Weather_Temp_Min_F_7.state <= 32)) && Weather_Temp_F.maximumSince(now.minusDays(7)).state <= 32) "\nSpecial Event! Sap will be flowing this week." else ""
    val String tempGymnastics = if (Mode.state.toString == "Night" && new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(1)),new DateTime((Calendar_Upcoming_Gymnastics.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)).contains(now)) "\nReminder: Anya has gymnastics tomorrow at " + Calendar_Upcoming_Gymnastics.state.format("%1$tl:%1$tM%1$tp.") else ""
    val String tempSchool = if (Mode.state.toString == "Night" && new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(1)),new DateTime((Calendar_Upcoming_School.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)).contains(now)) "\nReminder: Anya has school tomorrow at " + Calendar_Upcoming_School.state.format("%1$tl:%1$tM%1$tp.") else ""

Straight milliseconds can be used instead of java.util.concurrent.TimeUnit.DAYS.toMillis(1), but it is MUCH easier to see it this way when looking at the rule. Intervals are pretty handy!

3 Likes

I think I have something that is a little easier. Like I said, I’m not a developer so I’m just doing my best with what little coding experience I do have.

Looks like .withTime is a good fit? You can feed it a timestamp and it will feed back your timestamp with today’s date attached. This way you are hard coding the time instead of calculating it.

So I did some testing and substituting the following format:

now.withTime(6,0,0,0).millis

for:

now.withTimeAtStartOfDay.plusHours(6).millis

works. I of course could not test against DST but I don’t see why it wouldn’t work? It’s also easier to do minutes than the previous method. Before I had to calculate the hour/minute by .plusMinutes(xxx). With this it would just be

now.withTime(6,30,0,0)

for 6:30AM.

I have updated my rules and will monitor for any anomalies over the next week.

Open to thoughts and counter points!!!

4 Likes

This is great… thank you! I had done a long search trying to find an easy way to manually set a DateTime to a specific time, and this is slick. I’ve updated my rules and the interval example above to use .withTime. This looks to me like it will not have any issue with Daylight Savings.

Unrelated to .withTime, I started to wonder how Quartz handles Daylight Savings and found this… http://www.quartz-scheduler.org/documentation/best-practices.html#daylight-savings-time. I don’t have any rule triggers that would be affected, but good to know!

This is great stuff. When I get a chance I will update the DP to use withTime. I really like it and it might open some opportunities in other areas as well.

1 Like

Here is my version of this rule. I have it based on string representation of current time. This makes (imo) the rule more readable and works well with DS.
I also observed that sometimes Astro events don’t trigger my rule, so I made the rule triggered by cron expression - every minute.
Another improvement is using civil dusk and dawn for indicating day start and end which works quite well.
The rest is “stolen” from Rich :slight_smile:

rule "Calculate time of day state"
when
    Time cron "0 * * * * ? *"
then
    val String curr_time = now.toString("HH:mm")
    val String night_start = "00:00"
    var String morning_start = "06:00"
    val String day_start = (Sun_Dawn_Start.state as DateTimeType).format("%1$tH:%1$tM")
    val String evening_start = (Sun_Dusk_End.state as DateTimeType).format("%1$tH:%1$tM")
    val String midnight_time = "24:00"

    var new_val = "UNKNOWN"

    if (day_start < morning_start) {
        morning_start = day_start
    }
    switch true {
        case night_start <= curr_time && curr_time < morning_start:   new_val = "NIGHT"
        case morning_start <= curr_time && curr_time < day_start:     new_val = "MORNING"
        case day_start <= curr_time && curr_time < evening_start:     new_val = "DAY"
        case evening_start <= curr_time && curr_time < midnight_time: new_val = "EVENING"
    }
//  logInfo("Time_Of_Day", "curr_time=" + curr_time + ", night_start=" + night_start + ", morning_start=" + morning_start
//      + ", day_start=" + day_start + ", evening_start=" + evening_start + ", midnight_time=" + midnight_time
//      + ", new_val=" + new_val)

    if (Time_Of_Day.state.toString != new_val) {
        logInfo("Time_Of_Day", "Current time of day is now " + new_val)
        Time_Of_Day.sendCommand(new_val)
    }

end

Items:

DateTime    Sun_Dawn_Start      "Dawn [%1$tR]"              <time>      { channel="astro:sun:home:civilDawn#start" }
DateTime    Sun_Dusk_End        "Dusk [%1$tR]"          <time>      { channel="astro:sun:home:civilDusk#end" }
String      Time_Of_Day         "Time of day [%s]"

Edit: added morning start correction in case day starts before morning hour.
Edit: fixed val to var for morning_start

2 Likes

I really liked this approach. When I rewrite the op might adopt it. I certainly will do so in my own implementation. I may not above because I often reference this often when helping others with time conversion and comparison problems. I’ll have to think on it.

As far as I’m aware all of those problems have been fixed. I’m usually not in favor of polling in rules and go to great lengths to avoid it. But it does avoid problems in this case when Astro has a bug.

Thanks for posting!

Well it happened to me recently, I am currently on January snapshot. Not sure if this is a bug in Astro or OH2, I just want to have it bulletproof, event at cost of running the rule every minute.

If it occurred in a recent snapshot you should file an issue.

1 Like

I have been following the threads for Time Of Day adding / removing code from a useful set of periods
yet when I save the rule and let it run, I get the initial messages then nothing no errors just no update
what else can i monitor. is there a way to enable detailed debug/errors from the code you are working on currently

Added lot is logging to your rule and look in events.log to verify that the events that indicate a change from one time period to another are occuring.

It appears any error created in rules are not being reported and dies silently, I must have turned off errors for
the rule processor if thats possible ?

Unlikely and do you know it is an error? The rule doesn’t do anything if it doesn’t think the time of day had changed.

I have an issue using vTimeOfDay.

2018-07-07 01:16:31.591 [INFO ] [clipse.smarthome.model.script.lights] - Calculating time of day...
2018-07-07 01:16:31.807 [INFO ] [clipse.smarthome.model.script.lights] -  morning_start is 2018-07-07T06:00:00.000-07:00
2018-07-07 01:16:31.820 [INFO ] [clipse.smarthome.model.script.lights] -  night_start is 2018-07-07T23:00:00.000-07:00
2018-07-07 01:16:31.835 [INFO ] [clipse.smarthome.model.script.lights] -  bed_start is 2018-07-07T00:00:00.000-07:00
2018-07-07 01:16:31.840 [INFO ] [clipse.smarthome.model.script.lights] - vSunrise_Time.state is NULL
2018-07-07 01:16:31.858 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Error during the execution of startup rule 'Calculate time of day state': Could not
cast NULL to org.eclipse.smarthome.core.library.types.DateTimeType; line 35, column 33, length 35

The fact that vSunrise_Time.state is NULL makes me think that my binding does not work well.

Any idea of what I can do to progress in my debug?

Thank you by advance.

Do you have the astro building installed? Do you have a Thing defined? Is the right channel from the Thing linked to the Item?

Thank you for your reply.

You asked the good question.

channel="astro:sun:local:set#start"

I had home instead of local.

Can anyone post the weather.map file that is mentioned in the original post by rlkoshak?

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

Trying to figure out what that map looks like so that I can better follow this DP.

@fifo

From:Comprehensive Wunderground using HTTP Binding Example

With a search for weather.map in the forum…

weather.map

NULL=unknown

# Pressure: Note, if you have uninitialzed Items they may be transformed to "falling"
-=falling
+=rising
0=steady

# Cloudy or not
Light\ Drizzle=true
Heavy\ Drizzle=true
Drizzle=true

Light\ Rain=true
Heavy\ Rain=true
Rain=true

Light\ Snow=true
Heavy\ Snow=true
Snow=true

Light\ Snow\ Grains=true
Heavy\ Snow\ Grains=true
Snow\ Grains=true

Light\ Ice\ Crystals=true
Heavy\ Ice\ Crystals=true
Ice\ Crystals=true

Light\ Ice\ Pellets=true
Heavy\ Ice\ Pellets=true
Ice\ Pellets=true

Light\ Hail=true
Heavy\ Hail=true
Hail=true

Light\ Mist=true
Heavy\ Mist=true
Mist=true

Light\ Fog=true
Heavy\ Fog=true
Fog=true

Light\ Fog\ Patches=true
Heavy\ Fog\ Patches=true
Fog\ Patches=true

Light\ Smoke=false
Heavy\ Smoke=true
Smoke=true

Light\ Volcanic\ Ash=false
Heavy\ Volcanic\ Ash=true
Volcanic\ Ash=true

Light\ Widespread\ Dust=false
Heavy\ Widespread\ Dust=true
Widespread\ Dust=true

Light\ Sand=false
Heavy\ Sand=true
Sand=true

Light\ Haze=false
Heavy\ Haze=true
Haze=false

Light\ Spray=false
Heavy\ Spray=false
Spray=false

Light\ Dust\ Whirls=false
Heavy\ Dust\ Whirls=true
Dust\ Whirls=false

Light\ Sandstorm=false
Heavy\ Sandstorm=true
Sandstorm=true

Light\ Low\ Drifting\ Snow=true
Heavy\ Low\ Drifting\ Snow=true
Low\ Drifting\ Snow= true

Light\ Low\ Drifting\ Widespread\ Dust=true
Heavy\ Low\ Drifting\ Widespread\ Dust=true
Low\ Drifting\ Widespread\ Dust=true

Light\ Low\ Drifting\ Sand=true
Heavy\ Low\ Drifting\ Sand=true
Low\ Drifting\ Sand=true

Light\ Blowing\ Snow=true
Heavy\ Blowing\ Snow=true
Blowing\ Snow=true

Light\ Blowing\ Widespread\ Dust=true
Heavy\ Blowing\ Widespread\ Dust=true
Blowing\ Widespread\ Dust=true

Light\ Blowing\ Sand=true
Heavy\ Blowing\ Sand=true
Blowing\ Sand=true

Light\ Rain\ Mist=true
Heavy\ Rain\ Mist=true
Rain\ Mist=true

Light\ Rain\ Showers=true
Heavy\ Rain\ Showers=true
Rain\ Showers=true

Light\ Snow\ Showers=true
Heavy\ Snow\ Showers=true
Snow\ Showers=true

Light\ Snow\ Blowing\ Snow\ Mist=true
Heavy\ Snow\ Blowing\ Snow\ Mist=true
Snow\ Blowing\ Snow\ Mist=true

Light\ Ice\ Pellet\ Showers=true
Heavy\ Ice\ Pellet\ Showers=true
Ice\ Pellet\ Showers=true

Light\ Hail\ Showers=true
Heavy\ Hail\ Showers=true
Hail\ Showers=true

Light\ Small\ Hail\ Showers=true
Heavy\ Small\ Hail\ Showers=true
Small\ Hail\ Showers=true

Light\ Thunderstorm=true
Heavy\ Thunderstorm=true
Thunderstorm=true

Light\ Thunderstorms\ and\ Rain=true
Heavy\ Thunderstorms\ and\ Rain=true
Thunderstorms\ and\ Rain=true

Light\ Thunderstorms\ and\ Snow=true
Heavy\ Thunderstorms\ and\ Snow=true
Thunderstorms\ and\ Snow=true

Light\ Thunderstorms\ and\ Ice\ Pellets=true
Heavy\ Thunderstorms\ and\ Ice\ Pellets=true
Thunderstorms\ and\ Ice\ Pellets=true

Light\ Thunderstorms\ with\ Hail=true
Heavy\ Thunderstorms\ with\ Hail=true
Thunderstorms\ with\ Hail=true

Light\ Thunderstorms\ with\ Small\ Hail=true
Heavy\ Thunderstorms\ with\ Small\ Hail=true
Thunderstorms\ with\ Small\ Hail=true

Light\ Freezing\ Drizzle=true
Heavy\ Freezing\ Drizzle=true
Freezing\ Drizzle=true

Light\ Freezing\ Rain=true
Heavy\ Freezing\ Rain=true
Freezing\ Rain=true

Light\ Freezing\ Fog=true
Heavy\ Freezing\ Fog=true
Freezing\ Fog=true

Patches\ of\ Fog=true
Shallow\ Fog=true
Partial\ Fog=true
Overcast=true
Clear=false
Partly\ Cloudy=true
Mostly\ Cloudy=true
Scattered\ Clouds=false
Small\ Hail=true
Squalls=true
Funnel\ Cloud=true
Unknown\ Precipitation=true
Unknown=false
1 Like

@vzorglub Thank you, I had tried searching only on this post and didn’t find anything here but didn’t think to check the forum since I assumed the map was specific to the Time Of Day design pattern. I appreciate the quick response though! Thank You