The 2.2 example seems to be missing declarations of the following 3 items:
- vMorning_Time
- vNight_Time
- vBed_Time
I was getting errors whenever the rule was triggered. After adding them to my items list, everything works as intended.
The 2.2 example seems to be missing declarations of the following 3 items:
I was getting errors whenever the rule was triggered. After adding them to my items list, everything works as intended.
hi guys,
i implemented the script and everything works fine except that he never has the status “AFTERNOON”.
could you please tell me why ?
Post your code. The most likely problem is your times do not line up or you have a typo.
As much as I try, I do not understand it. Above all, I do not understand how I can intervene.
How can I move events?
For example: I do not want to take the sunrise as start MORNING but dusk end, so {channel = “astro: sun: home: astroDawn # end”}
and if I let them show me these values are empty, why?
DateTime vNight_Time (Astro)
DateTime vBed_Time (Astro)
And why does he never reach the status “AFTERNOON”
Can anyone explain to me?
Thank you in advance
here we go. I use OH 2.2
import org.joda.time.* // I don't think this should be required but I get errors when I don't include it
val String filename = "Tageszeit.rules"
val logName = "Tageszeit"
rule "Calculate time of day state"
when
System started or
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
Channel 'astro:sun:minus120:set#event' triggered START or
Channel 'astro:sun:minus150:set#event' triggered START or
Time cron "0 1 0 * * ? *" or
Time cron "0 0 6 * * ? *" or
Time cron "0 0 23 * * ? *"
then
logInfo(logName, "berechne Tageszeit...")
val long morning_start = now.withTimeAtStartOfDay.plusHours(6).millis
val long day_start = (vSunrise_Time.state as DateTimeType).getZonedDateTime.toInstant.toEpochMilli
val long afternoon_start = (Evening_Time.state as DateTimeType).calendar.timeInMillis
val long evening_start = (vSunset_Time.state as DateTimeType).getZonedDateTime.toInstant.toEpochMilli
val long night_start = now.withTimeAtStartOfDay.plusHours(23).millis
val long bed_start = now.withTimeAtStartOfDay.millis
// Update when changing the cron triggers above change
vMorning_Time.postUpdate(now.withTimeAtStartOfDay.plusHours(6).toString)
vNight_Time.postUpdate(now.withTimeAtStartOfDay.plusHours(23).toString)
vBed_Time.postUpdate(now.withTimeAtStartOfDay.toString)
//val long afternoon_start = (vEvening_Time.state as DateTimeType).getZonedDateTime.toInstant.toEpochMilli
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"
}
logInfo(logName, "Calculated time of day is " + curr)
vTimeOfDay.sendCommand(curr)
end
In order to make changes to code like this you must understand how it works.
I see that this DP was written before I included a Theory of Operation section, so here is a high level theory of operation:
Prerequisites
The Rule triggers at System started and for ALL events that can change the time of day state. If for some reason you want to include weather or presence or something else in your time of day calculations then changes to those Items must also trigger the rule to run.
The first step of the rule is to get the start time for every time of day. If the time starts based on a static time we use now.withTimeAtStartOfDay.plusHours(h).plusMinutes(m) to get the millis for that time today. If it is an Astro event, we use .getZonedDateTime.toInstant.toEpochMilli to get the millis for that time today.
The next step is a switch statement that compares the millis from state 2 to determine between which to start times the current time is.
It depends on the event. If you want to change the Astro event, you need to change the Rule trigger and change the event that is linked to the Item that event represents. If you want MORNING to start at dusk end, you need to change the trigger on the rule to the dusk end event and change the vSunrise_Time to link to the dusk end channel.
Because they don’t have a value. Look for errors in your logs.
Because the there is never a time during the day where it is between the start of afternoon and the start of evening. Look at your Rule triggers and Items to make sure that you have defined the start and end times appropriatly.
For example, if the sun sets before whatever Evening_Time is set to, or you do not have a trigger to run the Rule when Evening_Time starts, you will never see AFTERNOON.
Since this has not been addressed by now, just a short push @rikoshak
Also maybe it makes sense to give the first post a bit more structure by outsourcing code for older OpenHab versions to different threads and only keep the most current one in this thread.
Please see the introduction to Design Patterns I’ve just added to all the DPs I’ve written.
DPs are intended to be complete and self-contained solutions and to successfully use them one should be able to read and understand the example code. Part of that is recognizing when there are references to Items that are included in the DP itself.
The examples in a DP almost always reference other DPs and it is too much of a maintenance nightmare to duplicate that across all the DPs. So there is almost always Items or other segments of code that will be in the DP examples that are not shown or fully explained.
So, while the missing Items in the 2.2 example do not fit this, I do not think it is unreasonable for the DP users to see and recognize Items being used in the Rule code and understand those Items need to be defined or the lines removed if those lines are not relevant to your particular case.
Alright, understood. Did not know that there are multiple patterns that sometimes rely on each other, but thought that part had something to do with the functionality of this approach.
The Items in question are just used so I can see on my sitemap what time the various times of days start since not all of them depend on Astro events. I’ve removed those lines from the 2.2 example because they are not relevant to the DP.
You will note at the bottom of all of my DPs there will be a “Related Design Patterns” section with links to the other DPs that are used by that DP or used by the example in that DP. There are probably a few older DPs where I’ve not gone back to add that but most of them, particularly the popular DPs, will have this section. I also try to reference in the comments of the examples and in the Theory of Operation section the other DPs that are referenced.
I’m thinking daylight savings breaks my DP. I have these bits:
When
Time cron "0 0 6 ? * 1" or //6:00AM every sunday - sunday morning period
Then
var long Sunday_Morning = now.withTimeAtStartOfDay.plusHours(6).millis
...
The rule triggered fine at 6am but still thought it was NIGHT’s time period. I’m suspecting that the .plusHours calculation would come back as 5 since technically only 5 hours had passed since 12 midnight crossed over.
If this is the case, the rest of my periods will fail for the rest of this day and again on Fall’s DST change.
Any good work arounds to this? Otherwise, I have really enjoyed this DP.
Add some loving to verify that your guess is correct. If so I need to think on it. Dealing with daylight savings makes my want to move to Arizona. It’s never easy.
I think the way to deal with it will be too create DateTime Items with the constant times and abandon the now.plusHours stuff.
I agree. DST should just go away… everybody knows it, not sure why we don’t just shut it down.
My Sunday evening time also evaluated improperly. It evaluated as the 6 am period as I suspected it would because it is all off by an hour.
I found in the jodatime documents that this is expected behavior: http://www.joda.org/joda-time/apidocs/org/joda/time/DateTime.html#withTimeAtStartOfDay-- It should return with 1 hour missing as it counts real hours that have passed since start of day.
Also found this thread that seems to have confirmed same pain except in fall. Surprised they just passed it off and moved on. Once I figured it out, I realized it was going to therefore break my heating all day twice a year. That’s no good.
I’ll have to do some reading on your suggestion. I’m not much of a programmer unfortunately.
OK, now that we have confirmed it something does need to change. That is pretty annoying that it works like that.
At a high level, we need to create DateTime Items for each of the absloute time periods (i.e. the ones with cron triggers). Unfortunately, the easy way to do that is to use now, which we can’t do because of this DST behavior. I think what needs to be done is to construct a date time String that we pass to the new DateTime Items.
I’ve a few ideas but it will take a day or two before I get a chance to work though them.
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
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!
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!!!
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.
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
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