Plain rules allow a full customization of a rule with specific needs by means of setting the final EPL statement used by the Esper engine inside perseo-core. In order to work with perseo (front-end) properly, the EPL statement must fulfill several conventions for the rule to be able to operate on the incoming events and trigger adequate actions.
The “anatomy” of a rule is as follows
{
"name":"blood_rule_update",
"text":"select *,\"blood_rule_update\" as ruleName, *, ev.BloodPressure? as Pressure, ev.id? as Meter from pattern [every ev=iotEvent(cast(cast(BloodPressure?,String),float)>1.5 and type=\"BloodMeter\")]",
"action":{
"type":"update",
"parameters":{
"name":"abnormal",
"value":"true",
"type":"boolean"
}
}
}
The fields (all must be present) are
- name: name of the rule, used as identifier
- text: EPL statment for the rule engine in perseo-core
- action: action to be performed by perseo if the rule is fired from the core
The rule name must consist of the ASCII characters from A to Z, from a to z, digits (0-9), underscore (_) and dash (-). It can have a maximum length of 50 characters.
The field action
can be also an array of "actions", objects with the same structure than the single action described in the rest of the documentation. Each of those actions will be executed when the rule is fired, avoiding to duplicate a rule only for getting several actions executed. For practical purposes, it is the same result that would be obtained with multiple rules with the same condition.
The field text
of the rule must be a valid EPL statement and additionally must honor several restrictions to match expectations of perseo and perseo-core.
EPL is documented in [Esper web] (http://www.espertech.com/esper/documentation.php)
A EPL statement to use with perseo could be:
select *, "blood_rule_update" as ruleName,
ev.BloodPressure? as Pressure, ev.id? as Meter
from pattern
[every ev=iotEvent(cast(cast(BloodPressure?,String),float)>1.5 and type="BloodMeter")]
- The rule name must be present with ruleName alias. It must be equal to the ‘name’ field of the rule object
- The from pattern must name the event as ev and the event stream from which take events must be iotEvent
- A type= condition must be concatenated for avoiding mixing different kinds of entities
The used entity's attributes must be cast to float
in case of being numeric (like in the example). Alphanumeric
values must be cast to String
. Nested cast to string and to float is something we are analyzing, and could be
unnecessary in a future version. Use it by now. All the attributes in the notification from Orion are available in the
event object, ev, like ev.BlodPressure? and ev.id?. A question mark is necessary for EPL referring ‘dynamic’
values. Metadata is also available as explained in Metadata and object values.
When a rule is fired, the "complex event" generated by the select clause is sent to perseo (front-end) which executes the action using the generated event as parameter to the action.
There are a predefined set of actions: sending a SMS, sending a email, updating an attribute of the entity that fired the rule and making a HTTP POST to a provided URL.
The action must be provided in the action
field of rule. An example:
"action":{
"type":"update",
"parameters":{
"name":"abnormal",
"value":"true",
"type":"boolean"
},
"interval" : "30e3"
}
The type
field is mandatory and must be one of
update
: update an entity's attributesms
: send a SMSemail
: send an emailpost
: make an HTTP POSTtwitter
: send a twitter
An action can optionally have a field interval
for limiting the frequency of the action execution (for the rule and
entity which fired it). The value is expressed in milliseconds and is the minimum period between executions. Once the action
is executed successfully, it won't be executed again until that period has elapsed. All the request from core to execute
it are silently discarded. This way, the rate of executions for an entity and rule can be limited. (Strictly, the minimum
time between executions)
Some of the fields of an action
(see detailed list below) can include a reference to one of the fields of the
notification/event. This allows include information as the received "pressure" value, the id of the device, etc. For
example, the actions sms
, email
, post
include a field template
used to build the body of message/request. This
text can include placeholders for attributes of the generated event. That placeholder has the form ${X}
where X
may be:
id
for the id of the entity that triggers the rule.type
for the type of the entity that triggers the rule- Any other value is interpreted as the name of an attribute in the entity which triggers the rule and the placeholder is substituted by the value of that attribute.
All alias for simple event attributes or "complex" calculated values can be directly used in the placeholder with their
name. And any of the original event attributes (with the special cases for id
and type
meaning entity ID and type,
respectively) can be referred too.
This substitution can be used in the the following fields:
template
,from
,to
andsubject
foremail
actiontemplate
,url
,qs
,headers
,json
forpost
actiontemplate
forsms
actiontemplate
fortwitter
actionid
,type
,name
,value
,ìsPattern
forupdate
action
Sends a SMS to a number set as an action parameter with the body of the message built from the template
"action": {
"type": "sms",
"template": "Meter ${Meter} has pressure ${Pressure}.",
"parameters": {
"to": "123456789"
}
}
The field parameters
include a field to
with the number to send the message to.
The template
and to
fields perform attribute substitution.
Sends an email to the recipient set in the action parameters, with the body mail build from the template
field. A field to
in parameters
sets the recipient and a field from
sets the sender's email address. Also the subject of the email can be set in the field subject
in parameters
.
"action": {
"type": "email",
"template": "Meter ${Meter} has pressure ${Pressure} (GEN RULE)",
"parameters": {
"to": "someone@telefonica.com",
"from": "cep@system.org",
"subject": "It's The End Of The World As We Know It (And I Feel Fine)"
}
}
The template
, from
, to
and subject
fields perform string substitution.
Updates one or more attributes of a given entity (in the Context Broker instance specified in the Perseo configuration).
The parameters
map includes the following fields:
- id: optional, the id of the entity which attribute is to be updated (by default the id of the entity that triggers the rule is used, i.e.
${id}
) - type: optional, the type of the entity which attribute is to be updated (by default the type of the entity that triggers the rule is usedi.e.
${type}
) - isPattern: optional,
false
by default - attributes: mandatory, array of target attributes to update. Each element of the array must contain the fields
- name: mandatory, attribute name to set
- value: mandatory, attribute value to set
- type: optional, type of the attribute to set. By default, not set (in which case, only the attribute value is changed).
- trust: optional, trust token for getting an access token from Auth Server which can be used to get to a Context Broker behind a PEP.
"action":{
"type":"update",
"parameters":{
"id":"${id}_mirror",
"attributes": [
{
"name":"abnormal",
"type":"boolean",
"value":"true"
}
]
}
}
The name
parameter cannot take id
or type
as a value, as that would refer to the entity's id and the entity's type (which are not updatable) and not to an attribute with any of those names. Trying to create such action will return an error.
The id
, type
, name
, value
, ìsPattern
fields perform string substitution.
First time an update action using trust token is triggered, Perseo interacts with Keystone to get the temporal auth token corresponding to that trust token. This auth token is cached and used in every new update associated to the same action. Eventually, Perseo can receive a 401 Not Authorized due to auth token expiration. In that case, Perseo interacts again with Keystone to get a fresh auth token, then retries the update that causes the 401 (and the cache is refreshed with the new auth token for next updates).
It could happen (in theory) that a just got auth token also produce a 401 Not authorized, however this would be an abnormal situation: Perseo logs the problem with the update but doesn't try to get a new one from Keystone. Next time Perseo triggers the action, the process may repeat, i.e. first update attemp fails with 401, Perseo requests a fresh auth token to Keystone, the second update attemp fails with 401, Perseo logs the problem and doesn't retry again.
Makes an HTTP request to an URL specified in url
inside parameters
, sending a body built from template
.
The parameters
field can specify
- method: optional, HTTP method to use, POST by default
- url: mandatory, URL target of the HTTP method
- headers: optional, an object with fields and values for the HTTP header
- qs: optional, an object with fields and values to build the query string of the URL
- json: optional, an object that will be sent as JSON. String substitution will be performed in the keys and
values of the object's fields. If present, it overrides
template
fromaction
"action":{
"type":"post",
"template":"BloodPressure is ${BloodPressure}",
"parameters":{
"url": "http://localhost:9182/${type}/${id}",
"method": "PUT",
"headers": {
"Content-type": "text/plain",
"X-${type}-pressure": "${BloodPressure}"
},
"qs": {
"${id}": "${BloodPressure}"
}
}
}
Note that you can encode a JSON in the template
field:
"action": {
"type": "post",
"template": "{\"meter\":\"${Meter}\", \"pressure\": ${Pressure}}",
"parameters": {
"url": "http://${target_host}:${target_port}/myapp/${id}",
"headers": {
"Content-type": "application/json",
"X-${type}-pressure": "${BloodPressure}"
},
"qs": {
"${id}": "${BloodPressure}"
}
}
}
or use the json
parameter
"action": {
"type": "post",
"parameters": {
"url": "http://${target_host}:${target_port}/myapp/${id}",
"headers": {
"Content-type": "application/json",
"X-${type}-pressure": "${BloodPressure}"
},
"qs": {
"${id}": "${BloodPressure}"
},
"json": {
"meter": "${meter}",
"${id}": "${type}",
"pressure": "${pressure}"
}
}
}
The template
and url
fields and both the field names and the field values of qs
and headers
and json
perform string substitution.
Updates the status of a twitter account, with the text build from the template
field. The field parameters
must contain the values for the consumer key and secret and the access token key and access token secret of the pre-provisioned application associated to the twitter user.
"action": {
"type": "twitter",
"template": "Meter ${Meter} has pressure ${Pressure} (GEN RULE)",
"parameters": {
"consumer_key": "xvz1evFS4wEEPTGEFPHBog",
"consumer_secret": "L8qq9PZyRg6ieKGEKhZolGC0vJWLw8iEJ88DRdyOg",
"access_token_key": "xvz1evFS4wEEPTGEFPHBog",
"access_token_secret": "L8qq9PZyRg6ieKGEKhZolGC0vJWLw8iEJ88DRdyOg"
}
}
The template
field performs string substitution.
Metadata values can be accessed by adding the suffix __metadata__x
to the attribute name, being x
the name of the
metadata attribute. This name can be used in the EPL text of the rule and in the parameters of the action which accept
string substitution. If the value of the metadata item is an object itself, nested fields can be referred by additional
suffixes beginning with double underscore and the hierarchy can be walked down by adding more suffixes, like
__metadata__x__subf1__subf12
.
For example: The metadata in an event/notice like
{
"subscriptionId" : "51c04a21d714fb3b37d7d5a7",
"originator" : "localhost",
"contextResponses" : [
{
"contextElement" : {
"attributes" : [
{
"name" : "BloodPressure",
"type" : "centigrade",
"value" : "2",
"metadatas": [{
"crs": {
"value": {"system": "WGS84"}
}]
}
},
{
"name" : "TimeInstant",
"type" : "urn:x-ogc:def:trs:IDAS:1.0:ISO8601",
"value" : "2014-04-29T13:18:05Z"
}
],
"type" : "BloodMeter",
"isPattern" : "false",
"id" : "bloodm1"
},
"statusCode" : {
"code" : "200",
"reasonPhrase" : "OK"
}
}
]
}
could be used by a rule so
{
"name": "blood_rule_email_md",
"text": "select *,\"blood_rule_email_md\" as ruleName, *,ev.BloodPressure? as Pression, ev.id? as Meter from pattern [every ev=iotEvent(cast(BloodPressure__metadata__crs__system?,String)=\"WGS84\" and type=\"BloodMeter\")]",
"action": {
"type": "email",
"template": "Meter ${Meter} has pression ${Pression} (GEN RULE) and system is ${BloodPressure__metadata__crs__system}",
"parameters": {
"to": "someone@org.com",
"from": "perseo_cep@telefonica.com",
"subject": "MD VALUE: ${BloodPressure__metadata__crs__system}"
}
}
}
Generally, fields of attribute values which are objects themselves are accessible by adding to the name of the field a
double underscore prefix, so an attribute x
with fields a
, b
, c
, will allow these fields to be referred as
x__a
, x__b
and x__c
.
Note: be aware of the difference between the key metadatas
used in the context broker notificacions (v1), ending in s
and the infix metadata
, without the final s
, used to access fields from EPL and actions.
Fields with geolocation info with the formats recognized by NGSI v1, are parsed and generate two pairs of
pseudo-attributes, the first pair is for the latitude and the longitude and the second pair is for the x and y
UTMC coordinates for the point. These pseudo-attributes ease the use of the position in the EPL sentence of the rule.
These derived attributes have the same name of the attribute with a suffix of __lat
and __lon
, and __x
and
__y
respectively.
The formats are
So, a notification in the deprecated format like
{
"subscriptionId":"57f73930e0e2c975a712b8fd",
"originator":"localhost",
"contextResponses":[
{
"contextElement":{
"type":"Vehicle",
"isPattern":"false",
"id":"Car1",
"attributes":[
{
"name":"position",
"type":"coords",
"value":"40.418889, -3.691944",
"metadatas":[
{
"name":"location",
"type":"string",
"value":"WGS84"
}
]
}
]
}
}
]
}
will propagate to the core, (and so making available to the EPL sentence) the fields position__lat
, position__lon
,
position__x
, position__y
{
"noticeId":"169b0920-8edb-11e6-838d-0b633312661c",
"id":"Car1",
"type":"Vehicle",
"isPattern":"false",
"subservice":"/",
"service":"unknownt",
"position":"40.418889, -3.691944",
"position__type":"coords",
"position__metadata__location":"WGS84",
"position__metadata__location__type":"string",
"position__lat":40.418889,
"position__lon":-3.691944,
"position__x":657577.4234800448,
"position__y":9591797.935076647
}
Analogously, a notification in "geopoint" format, like
{
"subscriptionId":"57f73930e0e2c975a712b8fd",
"originator":"localhost",
"contextResponses":[
{
"contextElement":{
"type":"Vehicle",
"isPattern":"false",
"id":"Car1",
"attributes":[
{
"name":"position",
"type":"geo:point",
"value":"40.418889, -3.691944"
}
]
},
"statusCode":{
"code":"200",
"reasonPhrase":"OK"
}
}
]
}
will send to core an event with the fields position__lat
, position__lon
, position__x
, position__y
also
{
"noticeId":"7b8f1c50-8eda-11e6-838d-0b633312661c",
"id":"Car1",
"type":"Vehicle",
"isPattern":"false",
"subservice":"/",
"service":"unknownt",
"position":"40.418889, -3.691944",
"position__type":"geo:point",
"position__lat":40.418889,
"position__lon":-3.691944,
"position__x":657577.4234800448,
"position__y":9591797.935076647
An example of rule taking advantage of these derived attributes could be:
{
"name": "rule_distance",
"text": "select *, \"rule_distance\" as ruleName from pattern [every ev=iotEvent(Math.pow((cast(cast(position__x?,String),float) - 618618.8286057833), 2) + Math.pow((cast(cast(position__y?,String),float) - 9764160.736945232), 2) < Math.pow(5e3,2))]",
"action": {
"type": "email",
"template": "${id} (${type}) is at ${position__lat}, ${position__lon} (${position__x}, ${position__y})",
"parameters": {
"to": "someone@tid.es",
"from": "system@iot.tid.es",
"subject": "${id} is coming"
}
}
}
that will send an email when the entity with attribute position
is less than 5 km far away from Cuenca. It uses the
circle equation, (x - a)^2 + (y - b)^2 = d^2
, being (a, b)
618618.8286057833 and 9764160.736945232 the UTMC coordinates
of Cuenca and d
the distance of 5 000 m.
Note: for long distances the precision of the computations and the distortion of the projection can introduce some degree of inaccuracy.
Some attributes and metadata, supposed to contain a time in ISO8601 format, will generate a pseudo-attribute with the same name as the attribute (or metadata field) and a suffix "__ts", with the parsed value as milliseconds for Unix epoch. This value makes easier to write the EPL text which involves time comparisons. The fields (attribute or metadata) supposed to convey time information are
- Fields named
TimeInstant
- Fields of type
DateTime
- Fields of type
urn:x-ogc:def:trs:IDAS:1.0:ISO8601
Additionally, some derived pseudo-attributes are included also
x__day
: the day of the month (1-31) for the specified date according to local time.x__month
: the month (1-12) in the specified date according to local time.x__year
: the year (4 digits) of the specified date according to local time.x__hour
: the hour (0-23) in the specified date according to local time.x__minute
: the minutes (0-59) in the specified date according to local time.x__second
: the seconds (0-59) in the specified date according to local time.x__millisecond
: the milliseconds (0-999) in the specified date according to local time.x__dayUTC
: the day of the month (1-31) in the specified date according to universal time.x__monthUTC
: the month (1-12) in the specified date according to universal time.x__yearUTC
: the year (4 digits) of the specified date according to universal time.x__hourUTC
: the hour (0-23) in the specified date according to universal time.x__minuteUTC
: the minutes (0-59) in the specified date according to universal time.x__secondUTC
: the seconds (0-59) in the specified date according to universal time.x__millisecondUTC
: the milliseconds (0-999) in the specified date according to universal time.
So, an incoming notification like
{
"subscriptionId": "51c04a21d714fb3b37d7d5a7",
"originator": "localhost",
"contextResponses": [
{
"contextElement": {
"attributes": [
{
"name": "TimeInstant",
"value": "2014-04-29T13:18:05Z"
},
{
"name": "birthdate",
"type": "urn:x-ogc:def:trs:IDAS:1.0:ISO8601",
"value": "2014-04-29T13:18:05Z"
},
{
"name": "hire",
"type": "DateTime",
"value": "2016-10-13T12:10:44.149Z"
},
{
"name": "role",
"value": "benevolent dictator for life",
"metadatas": [{
"name": "when",
"value": "2014-04-29T13:18:05Z",
"type": "DateTime"
}]
}
],
"type": "employee",
"isPattern": "false",
"id": "John Doe"
},
"statusCode": {
"code": "200",
"reasonPhrase": "OK"
}
}
]
}
will send to core the "event"
{
"noticeId":"799635b0-914f-11e6-836b-bf1691c99768",
"noticeTS":1476368120971,
"id":"John Doe",
"type":"employee",
"isPattern":"false",
"subservice":"/",
"service":"unknownt",
"TimeInstant":"2014-04-29T13:18:05Z",
"TimeInstant__ts":1398777485000,
"TimeInstant__day":29,
"TimeInstant__month":4,
"TimeInstant__year":2014,
"TimeInstant__hour":15,
"TimeInstant__minute":18,
"TimeInstant__second":5,
"TimeInstant__millisecond":0,
"TimeInstant__dayUTC":29,
"TimeInstant__monthUTC":4,
"TimeInstant__yearUTC":2014,
"TimeInstant__hourUTC":13,
"TimeInstant__minuteUTC":18,
"TimeInstant__secondUTC":5,
"TimeInstant__millisecondUTC":0,
"birthdate":"2014-04-29T13:18:05Z",
"birthdate__type":"urn:x-ogc:def:trs:IDAS:1.0:ISO8601",
"birthdate__ts":1398777485000,
"birthdate__day":29,
"birthdate__month":4,
"birthdate__year":2014,
"birthdate__hour":15,
"birthdate__minute":18,
"birthdate__second":5,
"birthdate__millisecond":0,
"birthdate__dayUTC":29,
"birthdate__monthUTC":4,
"birthdate__yearUTC":2014,
"birthdate__hourUTC":13,
"birthdate__minuteUTC":18,
"birthdate__secondUTC":5,
"birthdate__millisecondUTC":0,
"hire":"2014-04-29T13:18:05Z",
"hire__type":"DateTime",
"hire__ts":1398777485000,
"hire__day":29,
"hire__month":4,
"hire__year":2014,
"hire__hour":15,
"hire__minute":18,
"hire__second":5,
"hire__millisecond":0,
"hire__dayUTC":29,
"hire__monthUTC":4,
"hire__yearUTC":2014,
"hire__hourUTC":13,
"hire__minuteUTC":18,
"hire__secondUTC":5,
"hire__millisecondUTC":0,
"role":"benevolent dictator for life",
"role__metadata__when":"2014-04-29T13:18:05Z",
"role__metadata__when__type":"DateTime",
"role__metadata__when__ts":1398777485000,
"role__metadata__when__day":29,
"role__metadata__when__month":4,
"role__metadata__when__year":2014,
"role__metadata__when__hour":15,
"role__metadata__when__minute":18,
"role__metadata__when__second":5,
"role__metadata__when__millisecond":0,
"role__metadata__when__dayUTC":29,
"role__metadata__when__monthUTC":4,
"role__metadata__when__yearUTC":2014,
"role__metadata__when__hourUTC":13,
"role__metadata__when__minuteUTC":18,
"role__metadata__when__secondUTC":5,
"role__metadata__when__millisecondUTC":0
}
A rule that will check if the employee has been hired in the last half hour, could be
{
"name": "rule_time",
"text": "select *, \"rule_time\" as ruleName from pattern [every ev=iotEvent(cast(cast(hire__ts?,String),float) > current_timestamp - 30*60*1000)]",
"action": {
"type": "email",
"template": "So glad with our new ${role}, ${id}!",
"parameters": {
"to": "someone@tid.es",
"from": "system@iot.tid.es",
"subject": "Welcome ${id}!"
}
}
}