The passage of time

In the post Scheduling events, we were responding to this desire: "it would be more convenient if the pico could just wake up every morning, check for ...".

The problem is that a pico isn't aware of the passing of time. It is not until some event comes for it — from the world outside of it — that the pico will do a computation in reaction to that event. After that reaction, it becomes quiescent again, until the next event or query.

In the earlier post, we showed how a ruleset can schedule future events, like we might set an alarm clock. The schedule is maintained by the pico engine, and when a future time arrives the pico engine will send the event to the pico to wake it up. This allowed the pico to do something every day at the set time.

This post will explore the idea of a simple ruleset that acts like an old-time town crier. It will send an event at the top of each hour of the day (and night). Like the crier shouting out "Ten o'clock and all's well!" Rules can then select on this event so that the pico can do things as often as every hour.

We will call the ruleset town_crier, and arrange for it to raise an event each hour. The scheduled event belongs to the ruleset, but the event that is raised is seen by all of the rulesets installed in the pico.

Ruleset code

The boilerplate for this new app is shown here

ruleset town_crier {
  meta {
    name "times"
    use module io.picolabs.plan.apps alias app
    shares time
  }
  global {
    time = function(_headers){
      app:html_page("manage counts", "",
<<
<h1>Manage times</h1>
>>, _headers)
    }
  }
}

and we'll have it display

  • the time is was installed
  • the last time it raised a top-of-the-hour event
  • the current time
  • all of the scheduled events for this ruleset (there should be just one)

The time function generates a web page that will be a kind of dashboard, but will likely not be used very often. It is for our convenience to be sure it is functioning as designed. Here is the time function definition

    time = function(_headers){
      app:html_page("manage counts", "",
<<
<h1>Manage times</h1>
<dl>
  <dt>Time installed</dt>
  <dd>#{ent:time_installed}</dd>
  <dt>Time last hour</dt>
  <dd>#{ent:time_last_hour}</dd>
  <dt>Time currently</dt>
  <dd>#{time:now()}</dd>
</dl>
<h2>Technical</h2>
<p>ent:id = #{ent:id.encode()}</p>
#{schedule:list().map(function(v){
  <<<pre>#{v.encode()}</pre>
>>}).join("")}
>>, _headers)
    }

using an HTML description list to display the desired information.

All that is left now is to schedule the event, and assign the correct values to two of the entity variables. This will be done as part of our reaction to the factory_reset event (which is raised by the apps ruleset whenever we install an application):

  rule start {
    select when town_crier factory_reset
    if ent:id.isnull() then noop()
    fired {
      ent:time_installed := time:now()
      schedule time event "top_of_the_hour"
        repeat << 0 * * * * >>  attributes { } setting(id)
      ent:id := id.typeof()=="Map" => id{"id"} | id
    }
  }

Finally, we will want to react to the event in the ruleset itself, just to record the time the event was raised in the ent:time_last_hour entity variable.

  rule hearTheCry {
    select when time top_of_the_hour
    fired {
      ent:time_last_hour := time:now()
    }
  }

The complete ruleset can be see here

Usage

In the previous post, in phase two, we assumed that the pico would be subjected to a timer:top_of_the_hour event, and this is how that event gets raised. Any ruleset (or app) we install in our pico can now select on that event, rather than having to schedule an event of its own.

Answer to exercise

In the previous post it was "left as an exercise to actually send the email message." That is accomplished as shown in this commit (cleaned up by a couple of subsequent commits). 

 The only tricky part is dealing with the pico owner's email address. The one thing we don't want to do is to hard-code an email address in a GitHub repository. In the code shown in the commit, we elected to have the UI — the dashboard — ask for the desired email address. The HTML widget to do that is shown here:

<h3>Email Setup</h3>
<form action="#{app:event_url(meta:rid,"new_settings")}">
To <input name="email" value="#{ent:email.defaultsTo("")}">
<button type="submit">Save changes</button>
</form>

Aside. The URL for the form is built by a function defined and provided by the io.picolabs.plan.apps ruleset, which we use as a module, and known within this ruleset by the alias app. The event_url function takes two arguments: the ruleset identifier of this ruleset (which is munged to become the event domain) and the event type (in this case "new_settings"). Aside over.

Below are the two rules that could react to the event that is the value of the action attribute of the form tag, which is io_picolabs_plan_wovyn_monitor:new_settings:

  rule saveSettings {
    select when io_picolabs_plan_wovyn_monitor new_settings
      email re#(.+@.+)# setting(to)
    fired {
      ent:email := to
    }
  }
  rule removeEmailAddress {
    select when io_picolabs_plan_wovyn_monitor new_settings
      email re#^ *$#
    fired {
      clear ent:email
      clear ent:last_response
    }
  }

Aside. Both rules select on the same event! This means that both might be selected for evaluation, and if both were selected, they would be evaluated in the order written. However, their event expressions are different. The first, the saveSettings rule will only be selected if there is an attribute named email that matches the regular expression shown (at least one character before an at sign followed by at least one character). The second, the removeEmailAddress rule will only be selected if the entire email attribute value consists of one or more space characters. Clearly, at most one of these will be the case. A value matching neither of these will mean that neither rule will be selected for evaluation. Aside over.

When the pico owner supplies an email address and clicks on the "Save changes" button, that address (which notice is not validated in any way (on neither the client nor the server side)) will be stored as the value in the ent:email entity variable.

The actual sending of the email message is accomplished by the action clause — highlighted in blue — of this rule:

  rule notifyFailedCheck {
    select when io_picolabs_plan_wovyn_monitor check_failed
      where not ent:email.isnull()
    pre {
      subject = <<Alert: #{meta:rid}: #{ent:last_alert_sent}>>
      body = <<At least one sensor has not reported in over an hour:
#{display_counts(ent:last_alert_counts).replace(re#<br>#g,"")}>>
    }
    email:send_text(ent:email,subject,body) setting(response)
    fired {
      ent:last_response := response
    }
  }

Aside. The rule will not be selected if the ent:email entity variable is null. The subject and body are computed in the rule's prelude. The unconditional action of the rule invokes a defaction defined and provided by the com.mailjet.sdk ruleset which we are using as a module. The response supplied by that defaction is bound to the name response and — since a rule with an unconditional action aways fires — saved as the value of the ent:last_response entity variable. Aside over.

No comments:

Post a Comment