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

Geesh. I must have skimmed right over it in the State Transformation section 20 times times.

Thanks!

I wanted to use this method to set a reminder to feed my animals:

rule "Food reminder"
when
    Time cron "0 0 18 1/1 * ? *"
then {
  val DT_Feed_Next = DT_Feed_Last.plusDays(2)
  if (now.isAfter(DT_Feed_Next)) {
    sendBroadcastNotification("Feed!")
    }
  }
end

but I get the error:

[ntime.internal.engine.ExecuteRuleJob] - Error during the execution of rule 'Food reminder': 'plusDays' is not a member of 'org.eclipse.smarthome.core.library.items.DateTimeItem'; line 43, column 33, length 31

Not sure why since similar operations (plusMinutes, plusDays) work in other rules e.g. when setting up timers.

Where is DT_Feed_Last defined?

Try something like this:

       var day2 = new DateTime(localLastMeasurement_00.state.toString).plusDays(2).toString("yyMMdd")
       var day3 = new DateTime(localLastMeasurement_00.state.toString).plusDays(2)

It is a DateTime item linked to the “Last Updated” channel from a Zigbee switch.

DateTime Items can’t be used as DateTime vars. If referring to the Item state, you have to use

DT_Feed_Last.state

and there is no plusDays method for IDateTime Items either. Instead, you have to build a DateTime var which is the same as the Item state. see

1 Like

Dear all,
I try to use the script reduced to some minimum but cannot find the bug. It crashes after debug login #4:

2019-11-19 22:36:04.737 [INFO ] [e.smarthome.model.script.Time Of Day] - Calculating time of day…
2019-11-19 22:36:04.743 [INFO ] [e.smarthome.model.script.Time Of Day] - Calculating time of day… 2
2019-11-19 22:36:04.748 [INFO ] [e.smarthome.model.script.Time Of Day] - Calculating time of day… 3
2019-11-19 22:36:04.753 [INFO ] [e.smarthome.model.script.Time Of Day] - Calculating time of day… 4
2019-11-19 22:36:04.757 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Error during the execution of startup rule ‘Calculate time of day state’: Invalid format: “NULL”

Items

Group gTimeofTheDay
String vTimeOfDay "Aktuelle Tageszeit: [%s]" <tod> (gTimeofTheDay)
DateTime vMorning_Time "Morgen [%1$tH:%1$tM]" <sunrise> (gTimeofTheDay) { channel="astro:sun:Lohhof:civilDawn#start" }
DateTime vDay_Time "Tag [%1$tH:%1$tM]" <sun> (gTimeofTheDay)         { channel="astro:sun:Lohhof:civilDawn#end" }
DateTime vAfternoon_Time "Nachmittag [%1$tH:%1$tM]" <sunset> (gTimeofTheDay) { channel="astro:sun:Lohhof:civilDusk#start" }
DateTime vEvening_Time "Abend [%1$tH:%1$tM]" <bedroom_blue> (gTimeofTheDay) { channel="astro:sun:Lohhof:civilDusk#end" }
DateTime vNight_Time "Nacht [%1$tH:%1$tM]" <moon> (gTimeofTheDay)

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:Lohhof:civilDawn#event'    triggered START or
  Channel 'astro:sun:Lohhof:civilDawn#event'    triggered END or
  Channel 'astro:sun:Lohhof:civilDusk#event'  	triggered END or
  Channel 'astro:sun:Lohhof:civilDusk#event'     triggered START or
  Item test_button received command 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) // 6 Uhr in der Früh
  //vMorning_Time.postUpdate(morning_start.toString) 

  //val bed_start = now.withTimeAtStartOfDay
  //vBed_Time.postUpdate(bed_start.toString)
    // Convert the Astro Items to Joda DateTime
  val morning_start = new DateTime(vMorning_Time.state.toString)
  logInfo(logName, "Calculating time of day...   2")

  val day_start = new DateTime(vDay_Time.state.toString) 
  logInfo(logName, "Calculating time of day...   3")

  val afternoon_start = new DateTime(vAfternoon_Time.state.toString)
  logInfo(logName, "Calculating time of day...   4")

  val evening_start = new DateTime(vEvening_Time.state.toString)
  //val evening_start = new DateTime(vEvening_Time.state.toString)
  logInfo(logName, "Calculating time of day...   5")

  val night_start = now.withTimeAtStartOfDay.plusDays(1).minusHours(1) // 23 Uhr abends
  logInfo(logName, "Calculating time of day...   6")

  vNight_Time.postUpdate(night_start.toString)
  logInfo(logName, "Calculating time of day...   7")

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

  }


  // Publish the current state
  logInfo(logName, "Calculated time of day is " + curr)
  vTimeOfDay.sendCommand(curr)
  sendMail("xxx@gmail.com", "OpenHAB Time of The Day: "+vTimeOfDay.state.toString+"", "OpenHAB Time of The Day: "+vTimeOfDay.state.toString+"")
end

I hope somebody can give me a hint.
Thanks and best
MagtzeMuc86

Is this populating? It appears it’s a NULL value so failing the rule…
Add it to the BASIC UI to see what value it gives, it is NULL then check the binding / item…

It failed yesterday but was able to update the status to the new value “Day”.
How can I check if it is populated, not sure what this means.
Thanks

So, it works through the case statement and the fact it can go through MORNING and DAY and then fails on AFTERNOON suggests that particular case statement is the issue:

case now.isAfter(afternoon_start) && now.isBefore(evening_start):
    
    {
      curr = "AFTERNOON"
    }

Looking at your rule values afternoon_start and evening_start are filled by:

val afternoon_start = new DateTime(vAfternoon_Time.state.toString)
val evening_start = new DateTime(vEvening_Time.state.toString)

Which are populated by the channels:

DateTime vAfternoon_Time "Nachmittag [%1$tH:%1$tM]" <sunset> (gTimeofTheDay) { channel="astro:sun:Lohhof:civilDusk#start" }
DateTime vEvening_Time "Abend [%1$tH:%1$tM]" <bedroom_blue> (gTimeofTheDay) { channel="astro:sun:Lohhof:civilDusk#end" }

So, either astro:sun:Lohhof:civilDusk#start or astro:sun:Lohhof:civilDusk#end aren’t being filled with a value so it is NULL.

I would check the astro settings to make sure they are linked etc. If you add it to your default site map and see if they have a value, that’s the way I test things, you might find someone else has a different / better way…

Thanks. Actually the astro binding seems to work as I could see the time on Basic UI.
What I do not understand: tod was NOT working but without changes during the day it started to work. Is there anything that needs to run a while or how will astro update?

I just performed a reboot of my raspberry an both astro and tod are fine, no issue anymore but also no changes. I can not explain.

This is my astro items file. Should not be relevant as the tod itemds uses the channels directly (correct)?

Group gSonneLohhof
DateTime           AstronomischeSonnendatenAstroDawnStart         "AstroDawnStart"                      (gSonneLohhof) {channel="astro:sun:Lohhof:astroDawn#start"}
DateTime           AstronomischeSonnendatenAstroDawnEnd           "AstroDawnEnd"                        (gSonneLohhof) {channel="astro:sun:Lohhof:astroDawn#end"}
DateTime           AstronomischeSonnendatenNauticDawnStart        "NauticDawnSart"                      (gSonneLohhof) {channel="astro:sun:Lohhof:nauticDawn#start"}
DateTime           AstronomischeSonnendatenNauticDawnEnd          "NauticDawnEnd"                        (gSonneLohhof) {channel="astro:sun:Lohhof:nauticDawn#end"}
DateTime           AstronomischeSonnendatenCivilDawnStart         "CivilDawnStart"                      (gSonneLohhof) {channel="astro:sun:Lohhof:civilDawn#start"}
DateTime           AstronomischeSonnendatenCivilDawnEnd           "CivilDawnEnd"                        (gSonneLohhof) {channel="astro:sun:Lohhof:civilDawn#end"}
DateTime           AstronomischeSonnendatenAstroDuskStart         "AstroDuskStart"                      (gSonneLohhof) {channel="astro:sun:Lohhof:astroDusk#start"}
DateTime           AstronomischeSonnendatenAstroDuskEnd           "AstroDuskEnd"                        (gSonneLohhof) {channel="astro:sun:Lohhof:astroDusk#end"}
DateTime           AstronomischeSonnendatenNauticDuskStart        "NauticDuskStart"                      (gSonneLohhof) {channel="astro:sun:Lohhof:nauticDusk#start"}
DateTime           AstronomischeSonnendatenNauticDuskEnd          "NauticDuskEnd "                        (gSonneLohhof) {channel="astro:sun:Lohhof:nauticDusk#end"}
DateTime           AstronomischeSonnendatenCivilDuskStart         "CivilDuskStart"                      (gSonneLohhof) {channel="astro:sun:Lohhof:civilDusk#start"}
DateTime           AstronomischeSonnendatenCivilDuskEnd           "CivilDuskEnd "                        (gSonneLohhof) {channel="astro:sun:Lohhof:civilDusk#end"}
DateTime           AstronomischeSonnendatenSetStart               "SetStart "                      (gSonneLohhof) {channel="astro:sun:Lohhof:set#start"}
DateTime           AstronomischeSonnendatenSetEnd                 "SetEnd"                        (gSonneLohhof) {channel="astro:sun:Lohhof:set#end"}
Number:Time        AstronomischeSonnendatenSetDuration            "SetDuration  "                          (gSonneLohhof) {channel="astro:sun:Lohhof:set#duration"}

Astro Things look like this

astro:sun:Lohhof  [ geolocation="48.287041, 11.584295,475", interval=60 ]

Channels are users to trigger the rule, but the items are used to calculate the tod.

Correct. But my specific astro items are - please correct me if I am wrong - irrelevant for the tod. Only the tod specific items are used.

The Items that appear in the Rule are the only ones that matter to the Time of Day.

Matthias, if I understand you right, the error occurs only after a “System started” ! Is that correct ? And when the other triggers are fired (Channel ‘astro:sun:Lohhof:civilDawn#event’, …) everything’s fine. Isn’t it ?

From my point of view it seems to be a still pending OH-Problem (or better, Linux Problem).

When OH is started/restarted it can happen that the rules are triggered even if some item-files are not loaded/initialized. In this case the Error occurs.

You can check this point easily with your “Test-Switch” (test_button) . If you switch it ON/OFF, your rule will trigger again and all items should be filled correctly and then triggered again via CRON-Triggers.

Maybe there’s a solution with createTimer and/or checking states for NULL. So maybe someone can help or give a hint in that case.

I use the "original from Rich, but I didn’t observe a problem yet. (I fairly have to say that I use it only for “information”. I have no depending rules).

Cheers,
Peter

Items begin life at system boot with a NULL state.
Item states can be reset to NULL during edits of xxx.items files, if you use them.

Astro binding will populate (assign a meaningful value to) Items via its state channels usually only once a day, just after midnight. Populating say, sunrise and sunset for the day ahead.
It should normally do that also when the binding initializes at boot time, but should hopefully complete that task before rules run. But it’s not totally guaranteed.

So you can see if an Item state gets reset to NULL it may not get updated until next midnight.

When you have doubts about an Item state inside a rule execution, log it out.

logInfo("test", "My Item state - " + someItem.state.toString)

Do that before it causes an error, probably at the top of the rule. Even if it is NULL state the log will work.

Thank you, looks like I’m getting closer :wink:

Now I have two DateTime variables, lastFeeding and nextFeeding, and the first rule to set the first one when I press a button:

rule "Set feeding time"
when
  Channel 'deconz:switch:openhabian:SW_Xia2:buttonevent' triggered 1002
then
  val lastFeeding = new DateTime(DT_Feeding.state.toString)
  logInfo(filename,lastFeeding.toString)
end

…with DT_Feeding being the last_updated channel of the button.
The loginfo ist just there for the moment and returns:

2019-11-21 15:16:48.872 [INFO ] [marthome.model.script.Schildis.rules] - 2019-11-21T15:16:47.000+01:00

Then, I have a second rule to remind me if more than two days have passed (right now it checks if one minute has passed):

rule "Food reminder"
when
    Time cron "40 18 15 1/1 * ? *"
then {
  val nextFeeding = lastFeeding.plusMinutes(1)
  if (now.isAfter(nextFeeding)) {
    sendBroadcastNotification("Feeding")
    }
  }
end

…which returns an error:

2019-11-21 15:18:40.013 [ERROR] [ntime.internal.engine.ExecuteRuleJob] - Error during the execution of rule 'Food reminder': cannot invoke method public org.joda.time.DateTime org.joda.time.DateTime.plusMinutes(int) on null

To me it looks like I have the DateTime var lastFeeding which is not null but plusMinutes seems to think otherwise. Where am I going wrong now?

Val creates a new variable within a rule.
When the rule ends, the variable disappears.

I’ve just had this conversation, see the first part of

1 Like

Use a global var instead (or test against the item)

// always define global vars at top of the file
var DateTime lastFeeding = null

rule "Set feeding time"
when
    Channel 'deconz:switch:openhabian:SW_Xia2:buttonevent' triggered 1002
then
    lastFeeding = new DateTime(DT_Feeding.state.toString)
    logInfo(filename,lastFeeding.toString)
end

rule "Food reminder"
when
    Time cron "40 18 15 1/1 * ? *" // at 15:18:40, every day
    // a more common quartz cron expression would be
    // Time cron "40 18 15 * * ?" // at 15:18:40, every day (year is optional)
then
    //  lastFeeding = new DateTime(DT_Feeding.state.toString) // update var when rule is triggered

    val nextFeeding = lastFeeding.plusMinutes(1)
    if (now.isAfter(nextFeeding)) {
        sendBroadcastNotification("Feeding")
    }
end

Dear all, thank you all very much for the support. Acually maybe something else went wrong. I could not re-create the problem. I rebooted the raspberry and the tod shows the right status even after reboot (giving it some time). Besides that you confirmed how to work and debug with openhab. Thank you all a lot!

1 Like