Thursday, December 25, 2014

(#18) More Esper Tidbits


As I continue to use and explore the capabilities of Esper, I'm going to put down some of the answers to questions that I ran into.

The documentation in Esper is certainly comprehensive but, to me, it's not as approachable for a novice, as I think it could be.

So I'm coding, playing and using the Netbeans debugger to see how things work.

Note, take all of the code snippets with a grain of salt. I'm not claiming them to be correct nor proper nor efficient.  They're just examples of my discoveries.

Recall that I'm using Esper to be the CEP engine for a Home Automation project. My sensors around the house emit MQTT packets periodically. There are door sensors, motion sensors, weather sensors, a Nest Thermostat, a Caller ID box and so on. 

Esper's job is to take the MQTT packet data from the sensors and try to draw conclusions about what's going on in the house.  Did someone just leave? Did someone just come home?  Did my spouse just crank up the thermostat to some hellish level and single-handily impact, adversely, the Natural Gas stores in Colorado?


While the Esper documentation is great about providing examples on the EPL, I've also been struggling with what the handler code should do?  The handler, "listener" in Esper parlance, is the code that's invoked when the EPL finds a match.


Esper Listener Examples


Q: For the EPL "select count(*)..." what does the listener code look like?

A: The attribute to get is named "count(*) and is of type long

For example:
String  subQuery1 = "SELECT COUNT(*) FROM WS2308WeatherStatusEvent wse WHERE wse.oTemp < cast(50.0, double)";

The Listener:
    @Override
    public void update(EventBean[] newData, EventBean[] oldData)
    {
         Long number = (Long) newData[ 0 ].get( "count(*)" );

An easier way would probably have been to modify the select clause:
String  subQuery1 = "SELECT COUNT(*) as numEvents FROM WS2308WeatherStatusEvent wse WHERE wse.oTemp < cast(50.0, double)";

The cast is there because I didn't just default all floating point types in Java to double. I used floats too.  Without the cast, Esper complained, at compile time, about the type mismatch.


Q: The Solution Patterns document, under the section "How do I detect N events in X seconds" has the example:  "select count(*), window(*) from MyEvent(somefield = 10).win:time(3 min) having count(*) >= 5 output first every 3 minutes".   

If I change it to reflect my needs:

anEPLQuery = "SELECT COUNT(*), WINDOW(*) FROM NestStatusEvent( hvacStatus = 'HEATING' ).win:length(5) HAVING COUNT(*) >= 5 OUTPUT FIRST EVERY 5 MINUTES";  

then what does the Listener code need to do?

A: Here's what's working for me.  To serve as a tutorial, the snippet takes no shortcuts. Some of the statements could be combined.

class FurnaceRunningListener implements UpdateListener
{

  @Override
  public void update(EventBean[] newData, EventBean[] oldData)
  {
    logger.info( "FurnaceRunningListener - update called" );
       
    try {
      //
      // When the EPL is simple "SELECT * FROM EventObject" - you 

      // use the getUnderlying() method. For example, 
      // NestStatusEvent  eventOne = 
      // (NestStatusEvent) newData[ 0 ].getUnderlying();
           
      //
      // For this, More complex EPL
      // "SELECT COUNT(*), WINDOW(*) FROM 

      //     NestStatusEvent( hvacStatus = 'HEATING' ).win:time( 5 min )
      //     HAVING COUNT(*) >= 5 OUTPUT FIRST EVERY 5 MINUTES";
      //  Now EventBean[] is coming in with a hashmap
      EventType et = newData[ 0 ].getEventType();
      String  propertyName1 = et.getPropertyNames()[ 0 ]; // "count(*)"
      String  propertyName2 = et.getPropertyNames()[ 1 ]; // "window(*)
      

      // propertyName1 is "count(*)" - get the value
      long  countValue = ((Long) (newData[0].get( propertyName1 ))).longValue();

      
      // propertyName2 is "window(*)" - the events are coming in as an array
      NestStatusEvent[] triggeringEvents = (NestStatusEvent[]) newData[0].get( propertyName2 );


      // How many events? Our EPL asked for (at least) 5
      int                 numEvents = triggeringEvents.length;
 

      // so the first event object is at [0], the last at [4]           
      NestStatusEvent     firstEvent = triggeringEvents[ 0 ];
      NestStatusEvent     lastEvent  = triggeringEvents[ numEvents - 1 ];
      //


And go on from there.

BPM - Getting it off my Chest: Microservices and APIs 

I mentioned in post 17, that I had come back from a conference on Application Architecture.  Hosted by a national vendor with a good reputation for quality.  I'm expecting a call from the account rep asking me how I enjoyed the conference.  I think what I'm simply going to say is that "the coffee was good."


And leave it at that.


My first criticism was simply that the conference was more "bread than meat" -- the topics were only covered in the most cursory manner.  They were all the proverbial 50,000' fly-by.


I get it - the sessions are 45 minutes; you cannot get into too much detail in 45 minutes.  But still, that wasn't long enough for me.  I don't think I'll go back to this conference because of this alone.

But there was something else at this conference. What's a polite synonym for pandering? By the way, BPM, for this post, does not stand for Business Process Management.


Buzzwords de' decade.

Microservices and APIs.  Ugh. Every damned slide, poster, badge, ornament and coffee cup holder was emblazoned with Microservices and APIs.

Really? Really?  Really?  Give me a break.


So just what is an API anyway?

This one was puzzling.  We've been defining, designing, implementing and using APIs for over 40 years.  What was so different about the 2014 definition to warrant all this hype?

What's in shared library or DLL?  An API.  How did the client / server applications we created in the 80s interact? Through APIs.  Apollo Network Computing, OSF DCE, CORBA, SOA -- all laden with APIs.

So what's the difference between a service and an API anyway?  Only one analyst tackled this one head on and answered "Nothing." Thank you!

I'd venture a guess that that vast majority of attendees didn't know or didn't care about this.  I did.  Is it just the malcontent of an old curmudgeon?  I don't think so. I walked out feeling like I was being hyped.

I expect this kind of hype from a vendor.  But I thought it quite beneath the analysts at this conference. 

I found it distasteful.


Microservices

Goodness I don't know where to start.  Maybe I'll start off with a couple of opinions.

Opinion Number 1 - There's nothing new here

Service based design and programming is as old as dirt.  

Ok - as old as computer dirt.  I stumbled across the early trails, then called Network Computing, in the mid 80's with such offerings as Sun's RPC/XDR, and NFS products.  

A few years later and the design approach had gathered enough of a following to warrant OSF's DCE offering.  So it's at least 35 years old, and probably older.


Again - this isn't a rant about the old farts not getting credit for something.  This is a warning. Services, micro or macro, are not new. 

The service-based shoals of application development are littered with the bodies of those who've tried and failed to make these things work.  Which leads to Opinion Number 2.


Opinion Number 2 - There were 54 problems we ran into when designing applications using services.  Microservices takes that number down to 53.

When creating non-trivial applications using services we ran into:
  • language independent service discovery
  • transaction management
  • auditing / logging
  • orchestration / choreography
  • common context (enterprise object models)
  • contractual obligations / promises
  • testing
  • granularity
  • security
  • error handling
And 43 others.  Just Google "SOA implementation problems" for a taste of what we ran into.
With microservices there's a half-hearted attempt to tackle one: granularity.  Make 'em small.  How small?  Can't tell you exactly.  Make 'em small enough but not too small. Make 'em just the right size.

Sweet.  Thanks for the help.
You want to make a real impact - tackle error handling. That should be simple enough.


Bull Hockey

One analyst tossed up a slide showing that "applications of the future will be an assembly of microservices."  It was difficult to remain seated.  I've seen that slide, in one form or another, for 20+ years.




Ok - show me.
Show me one non-trivial application constructed from an assembly of microservices.
Show me one, non-trivial, business relevant, customer facing application:

  • assembled from 6 or more microservices
  • with microservices created by 2+ vendors

Sigh.
Never mind...


So, what?  Give up?

When I was a kid, about 7, I came to the conclusion that we had invented swearing. My generation had invented cursing. I didn't hear it at home (much) but there was this 5th grader, Jeremy, that could make a sailor blush.

Since that was my first real exposure to the art of cussing, I assumed that Jeremy, the 5th grader, had started it all.

Around 2005, give or take, I had a young gun from Microsoft come into work for a presentation (on BizTalk, I think) and state categorically that Microsoft had invented distributed computing.


If this is your first exposure to service based development, you might think you're blazing uncharted territory too.   That's fine.    Please let me repeat: this isn't the malcontent of an old curmudgeon who feels like we didn't get our share of credit. I personally had nothing to do with the science, the design of service based design.  I just hopped on the bandwagon.


If I have seen further it is by standing on ye sholders of Giants.

Do your homework.  Don't make the same mistakes or run needlessly into the same obstacles we did.


Don't give up.  Keep working on it.

But I'll close with a suggestion that you keep a George Santayana quote in mind about remembering the past.



Good luck.
May you have more success that we did.

Tuesday, December 9, 2014

(#17) Well That was Unexpected - CEP Legitimized?


I'm at a Nerd Conference in Vegas. There's about 2000 nerds out here.  Mostly IT nerds. IT nerds from big companies. IT nerds from big companies talking about problems they have, that I used to have, that I no longer have.

And that makes me smile.

Much to my surprise, Event Processing made the bill.  Not as high on the charts as, oh say, Microservices and APIs. But there were three or so sessions dedicated to event processing in business.  The TLA CEP (Complex Event Processing) was scattered on a few slides too.

And that reminded me I hadn't come back to close off the situation presented in post #16.     In that post, I said that I had not been getting the results I had expected, nor wanted from Esper's CEP Engine.  When I posted my question to the Esper discussion groups, there were several responses saying I should abandon my EPL query and switch over to Match Recognize.


The Match Game

I don't know the history of this syntax. You'll find references to "match recognize SQL" in the documentation for Oracle 12c DBMS.  Whether Esper borrowed it or if it's a extension that Oracle came up with or if it's an emerging standard -- I don't know.  What I do know is that it worked.




Here's the new query using the Esper's Match Recognize syntax:

   anEPLQuery = "SELECT * FROM HHBAlarmEvent " +
            " MATCH_RECOGNIZE (" +
            " MEASURES A as a, B as b, C as c" +
            " PATTERN (A B C) " +
            " DEFINE " +
            "  A as A.macAddress = '000000B357' and A.deviceStatus = 'OPEN', "  +
            "  B as B.macAddress = '0000012467' and B.deviceStatus = 'OPEN', "  +
            "  C as C.macAddress = '000007AAAF' and C.deviceStatus = 'MOTION'"  +
                        ")";
   EPStatement arrivalStatement_EPL4 = cepAdm.createEPL( anEPLQuery );

      


Stare at it a bit, and you can kind of tease it out.  The Pattern we're going to recognize is (A B C).  The pattern syntax is regex based.  I'm looking for Event A, then Event B, then Event C. The Measures A as a, B as b means that my listener method can pull out the events using "a", "b" and "c".  Like this:

        HHBAlarmEvent  eventOne = (HHBAlarmEvent) newData[ 0 ].get( "a" );
        HHBAlarmEvent  eventTwo = (HHBAlarmEvent) newData[ 0 ].get( "b" );
        HHBAlarmEvent  eventThree = (HHBAlarmEvent) newData[ 0 ].get( "c" );
 

 And the DEFINE part tells the Engine how to recognize a match.  It defines the condition that triggers a match.  In my case, Event A is when the Garage Door Sensor (MAC Address '0B357') goes to device status OPEN.  Then Event B is when the Door to the Garage (MAC Address '12467' also goes to status OPEN.  And finally, when those two are followed by Event C, the Motion Detector (MAC Address '7AAAF) detects MOTION.

I'll post the results of this change from Esper EPL (A -> B -> C) to Match Recognize later, but it worked!

I started getting the results I thought I should get.

The syntax for Match Recognize looks a bit harder to master, but it's going to be worth mastering.

 

Sunday, October 12, 2014

(#16) Writing the Wrongs - Esper Patterns 

I made a mistake.  After watching my Esper code run for several weeks I was consistently seeing triggers on event patterns that I didn't expect.  It's not that the triggers were wrong -- I'm not asserting a bug in Esper -- it's that I wasn't seeing results that I expected to see.


The ABC's 

Let's simplify what I'm after. If you recall, I'm looking for a Garage Door Open event, a Door Open event and then a Motion event as a pattern that I'm interested in.  

Simplify this to event A, event B then event C, where event A is the Garage Door Open Event, event B is the Door Open event and event C is the motion event.

I'm interested in A, then B, then C.  In Esper's EPL parlance, this is noted as "A -> B -> C".  Which is useful notation to adopt.


The Real World Intrudes

What was happening, in my house, occasionally was this:

  1. Garage Door Opens (e.g. A-1)
  2. Garage Door closes (don't care about this event)
  3. Time passes
  4. Garage Door Opens again (e.g. A-2)
  5. Door Opens (B-1)
  6. Motion Sensor triggers (C-1)
In my psuedo EPL notation I was seeing A1 -> A2 -> B1 -> C1.

What was I expecting?  I was expecting that my Esper would trigger on the last three: A2 -> B1 -> C1.   And that's not what I was seeing.  In my update listener code, I saw that the three events that were coming in were A1, B1, and C1.  I was getting the first A event, not the last as I had thought I'd see.

The Fault, Dear Brutus

I've just finished up an hour of playing with Esper and patterns. As you'd surmise, the results I'm getting are because of the EPL I used.  So let's explore a bit more on the EPL varations I tried and the results I got.


The test cases I used were all variations on the arrivals of A, B and C events with delays in between.  Test case 3, to pick one, is noted as: "A1, A2, d10 A3, B1, C1".



In English this would be: 
  • send A event (A1)
  • send A event (A2)
  • Delay 10 seconds
  • send A event (A3)
  • send B event (B1)
  • send C event (C1)

I created 4 Esper patterns in EPL and ran the application.

EPL-1
SELECT * FROM PATTERN
[ every 
 (eventOne = HHBAlarmEvent( macAddress = '000000B357', deviceStatus = 'OPEN' ) 
-> eventTwo = HHBAlarmEvent( macAddress = '0000012467', deviceStatus = 'OPEN' ) 
-> eventThree = HHBAlarmEvent( macAddress = '000007AAAF', deviceStatus = 'MOTION' )) where timer:within( 2 minutes )];

EPL-1 can be noted in my shorthand as [ every ( A-> B -> C) where timer:within ]


EPL-2
SELECT * FROM PATTERN
(eventOne = HHBAlarmEvent( macAddress = '000000B357', deviceStatus = 'OPEN' ) 
-> eventTwo = HHBAlarmEvent( macAddress = '0000012467', deviceStatus = 'OPEN' ) 
-> eventThree = HHBAlarmEvent( macAddress = '000007AAAF', deviceStatus = 'MOTION' )) where timer:within( 2 minutes )];

EPL-2 can be noted in my shorthand as [  ( A-> B -> C) where timer:within ]



EPL-3
SELECT * FROM PATTERN

( every eventOne = HHBAlarmEvent( macAddress = '000000B357', deviceStatus = 'OPEN' ) 
-> eventTwo = HHBAlarmEvent( macAddress = '0000012467', deviceStatus = 'OPEN' ) 
-> eventThree = HHBAlarmEvent( macAddress = '000007AAAF', deviceStatus = 'MOTION' )) where timer:within( 2 minutes )];



EPL-3 can be noted in my shorthand as [  (  every A-> B -> C) where timer:within ]



EPL-4
SELECT * FROM PATTERN

every eventOne = HHBAlarmEvent( macAddress = '000000B357', deviceStatus = 'OPEN' ) 
-> eventTwo = HHBAlarmEvent( macAddress = '0000012467', deviceStatus = 'OPEN' ) 
-> eventThree = HHBAlarmEvent( macAddress = '000007AAAF', deviceStatus = 'MOTION' ) where timer:within( 2 minutes )];

EPL-1 can be noted in my shorthand as [ every A-> B -> C where timer:within ]


You can see I'm moving the "every" and the parenthesis around and see what results I find.


The Test Cases

With the four EPL patterns ready, I modified the code to read patterns from a file, to add real-world timings. Then I created six test cases:



(Recall "d10" is my short hand for delay (wait) 10 seconds, d123 means delay 123 seconds.)

All four EPL patterns will be run against the test cases.  Next, I thought about the results that I wanted.  For example on test case 3, what I'd like to get is a trigger on the event sequence A3 -> B1 -> C1.

So I added a column to indicate what I was hoping to see from Esper:



Now, we could certainly disagree over what's to be expected or desired. Your needs / expectations could be different from mine.  For example in test case 5, you might possibly want to get notified on A1->B1-> C1 or A1->B3-> C1 or A3->B3-> C3.  What you expect is up to you.  I've just put down what I think I'd want to meet my needs.

The Results - Grouped by EPL

Test Case 1

Recall EPL-1 is [ every ( A-> B -> C) where timer:within ]. Let's look at the results:



Green denotes that I got what I was wanting to get.  Red means that I got something that I didn't want. Please, please, please note - red does not mean the results are wrong. Red means that the EPL I used didn't give me the results I was hoping for.

Think about Test Case 1, where I was hoping to get A3->B1->C1 and instead I got triggered on A1->B1->C1.  It's obviously correct but it's also reasonable to get that response from Esper.  So the chore becomes "what EPL will produce the results I'm after?"

Let me beat this horse a bit more - Esper responded. The EPL worked, my updateListerner method was called.  But when I examined the three events that triggered the listener object, sometimes I got the event objects I wanted (green) and sometimes I did not (red).

Let's keep going


Test Case 2

Recall EPL-2 removes the "every" keyword: [ ( A-> B -> C) where timer:within ]. The results were:




Again - green the results match what I expected. The red, I got something other than what I had wanted and now, yellow, means the trigger did not fire. Yellow means the updateListener method was not called.


The Yellow results on Test Case 5 puzzle me.  Yes, the time delta between A1 and C1 is outside the window. No trigger should fire.  But A3, B3 and all of the C events are within the two minute window and best I can tell, the updateListener did not get called.  That strikes me as odd.


Test Case 3

Recall EPL-3 puts the "every" keyword back but inside the parenthesis: [  (  every A-> B -> C) where timer:within ]The results were:


Interesting,  More cases where there was no trigger fired.  Again not what I was expecting.

Test Case 4

Finally EPL-4 removes the parenthesis: [  every A-> B -> C where timer:within ]The results were:






Conclusions

First I need to read more and get a better understanding of the EPL pattern syntax to see if my errors are obvious.  

The Community Responds!  Switch your EPL to use The Match Recognize Syntax!

Monday, August 4, 2014

(#15) Esper Example Continued - the Event Listener 


We've outlined the Esper CEP main code in post #13, and in post #14, we posted the Java code that backs up an event.  Here we'll cover off on the listener code -- the code that gets called when your event rule triggers.

Recall that we're looking for Departure Events and Arrival Events. Each occur when three other events happen in a specific sequence in a specific amount of time.

Here's the code for the Departure Event Listener:

public class DepartureEventListener implements UpdateListener 
{
    // Esper's using Log4J v1.2.17 - we might as well too
    private final org.apache.log4j.Logger logger = 
                 org.apache.log4j.Logger.getLogger( "DepartureEventListener" );

    private MQTTClient      theMQTTClient = null;
  
    
    // -------------------------------------------------------------------------
    @Override
    public void update(EventBean[] newData, EventBean[] oldData) 

    {
        logger.info( "Departure Event Listener - update");

        //
        // Note the names align to the EPL statement.  We called them 'eventOne'
        //  'eventTwo' and 'eventThree' in the EPL
        HHBStatusEvent  eventOne = (HHBStatusEvent) newData[ 0 ].get( "eventOne" );
        HHBStatusEvent  eventTwo = (HHBStatusEvent) newData[ 0 ].get( "eventTwo" );
        HHBStatusEvent  eventThree = (HHBStatusEvent) newData[ 0 ].get( "eventThree" );

        //
        // Calculate time -- in seconds -- between the three events
        long deltaTime1 = (eventTwo.getDateTime().getTimeInMillis() - 
                           eventOne.getDateTime().getTimeInMillis()) / 1000L;

        long deltaTime2 = (eventThree.getDateTime().getTimeInMillis() -
                           eventTwo.getDateTime().getTimeInMillis()) / 1000L;

        long deltaTime3 = (eventThree.getDateTime().getTimeInMillis() -
                           eventOne.getDateTime().getTimeInMillis()) / 1000L;

        //
        // Create the message payload...
        //  CEP/CONCLUSION | 2014-05-28 15:24:23 -0600 | DEPARTURE | EPL1 | E1:E2 10 | E2:E3 88 | E1:E3 98 |

        StringBuffer    sb = new StringBuffer();
        sb.append( "CEP/CONCLUSION" );        sb.append( " | " );
        sb.append( getCurrentDateTime() );    sb.append( " | " );
        sb.append( "DEPARTURE" );             sb.append( " | " );
        sb.append( "EPL1" );                  sb.append( " | " );
        sb.append( "E1:E2 " );        sb.append( deltaTime1 );            sb.append( " | " );
        sb.append( "E2:E3 " );        sb.append( deltaTime2 );            sb.append( " | " );
        sb.append( "E1:E3 " );        sb.append( deltaTime3 );            sb.append( " | " );
            
        logger.info( "Departure Event Listener - ready to call publish!" );       
        this.theMQTTClient.publishMessage( "CEP/CONCLUSION", sb.toString() );
        logger.info( "Departure Event Listener - publish called!" );       
    }

    // -------------------------------------------------------------------------
    public  void    setTheMQTTClient (MQTTClient theClient)
    {
        this.theMQTTClient = theClient;
    }
    
    //--------------------------------------------------------------------------
    private static  String  getCurrentDateTime()
    {
        Date now = new Date();
        SimpleDateFormat format = new SimpleDateFormat( "YYYY-MM-dd HH:mm:ss Z");
        
        return format.format( now );
    }
}

Things to note:
  1. The class implements the UpdateListener Interface
  2. There's one abstract method to implement, update()
  3. The update() method receives the events that matched the rule
  4. And you can do whatever you want to do when the rule triggers


Reacting to a Departure Event
In my case, I simply create another MQTT message and publish an event. The presumption is that there's another process, elsewhere on the network that will subscribe to these events and that the proper action.

It's very useful to have the events that matched and triggered the conclusion passed in. 


Wednesday, July 30, 2014

(#14) Esper Example Continued - The Event Pojo 

The more time I spend with Esper, the more impressed I am at the low barrier to entry. In other words, getting a CEP platform up and running is easy.  The previous post displayed the code I came up with that instantiates the Esper CEP engine.

The second line of that code:

 cepConfig.addEventType( "HHBStatusEvent", HHBStatusEvent.class.getName() );


shows us informing Esper about the class we'll create to represent an event, HHBStatusEvent.

Now, where's more than one way to represent an event, from your stream, to Esper but I chose what I felt was the most simple, a Plain Old Java Object (POJO).

Without further ado,

HHBStatusEvent.java


import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author patrick.conroy
 */
public class HHBStatusEvent 
{
    java.util.Calendar  dateTime;                   // date/time event was created
    int                 deviceType;                 // HHB device type (eg. 23 = Motion Sensor)
    String              deviceTypeString;           // String of type (eg. "MOTION SENSOR")
    String              deviceName;                 // Sensor name (eg. "FRONT DOOR")
    String              deviceStatus;               // Sensor state (eg. "OPEN", "CLOSED", "MOTION", "NO MOTION")
    int                 statusDuration;             // How long (seconds) the sensor has been in this state
    String              alarmOnSetting1;            // See Note 1
    String              alarmOnSetting2;            // See Note 1
    String              callOnSetting1;             // See Note 2
    String              callOnSetting2;             // See Note 2
    boolean             deviceOnline;               // True or False
    boolean             batteryOK;                  // False if low or empty
    boolean             triggered;                  // True if sensor is in ALARM state
    String              macAddress;                 // MAC Address of this sensor
    
    // ------------
    // Note 1:  The HHB System can be set to 'alarm' the Key FOB based on the sensor type and state/
    //  Eg. You can configure the System to alarm when a Door Sensor is 'OPEN' and you could 
    //  configure the System to alarm when a Door Sensor is 'CLOSED'.  A motion sensor could alarm
    //  on 'MOTION', a Tilt Sensor could alarm on 'TILT' and a power sensor could alarm on 'OFF'
    //  alarmSetting1 will be similar to "ALARM ON OPEN" and alarmSetting2 could be "NO ALARM ON CLOSED"
    //
    // Note 2: The system had (past tense) the capability to do the above but call into the Eaton
    //  servers and, in turn, call you or send a text message.  So everything said above if copied
    //  to these attributes, just replace 'ALARM' with 'CALL'.  For example the Tilt Sensor could be
    //  "CALL ON NO-TILT" for callOnSetting1 and "NO CALL ON TILT" for callOnSetting2
    
    
    //--------------------------------------------------------------------------
    @Override
    public  String  toString()
    {
        return "HHBStatusEvent(" +
                getDateTimeString() + "|" +
                this.deviceTypeString + "|" +
                this.deviceName + "|" +
                this.deviceStatus + "|" +
                this.statusDuration + "|" +
                this.triggered + "|" +
                this.macAddress + 
                ")";
    }
    
    //--------------------------------------------------------------------------
    public String   getDateTimeString() 
    {
        SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss z" );
        return sdf.format( this.dateTime.getTime() );
    }
    
    //--------------------------------------------------------------------------
    public  void    fromEventString (String eventString)
    {
        /*
        * Here are some some examples
HHB/STATUS | 2014-05-15 11:31:50 -0600 | 03 | OPEN-CLOSE SENSOR | Front Door | CLOSED | 14400 | ALARM ON OPEN | NO ALARM ON CLOSE | DO NOT CALL ON OPEN | DO NOT CALL ON CLOSE | ONLINE | BATTERY OK | CLEARED | 0000010228 |
HHB/STATUS | 2014-05-15 11:31:50 -0600 | 05 | WATER LEAK SENSOR | Basement Floor | DRY | 950400 | ALARM ON WET | NO ALARM ON DRY | DO NOT CALL ON WET | DO NOT CALL ON DRY | ONLINE | BATTERY OK | CLEARED | 0000011957 |
HHB/STATUS | 2014-05-15 11:31:50 -0600 | 23 | MOTION SENSOR | Motion Sensor | NO MOTION | 3600 | ALARM ON MOTION | NO ALARM ON NO MOTION | DO NOT CALL ON MOTION | DO NOT CALL ON NO MOTION | ONLINE | BATTERY OK | CLEARED | 000007AAAF |
HHB/STATUS | 2014-05-15 11:31:50 -0600 | 24 | TILT SENSOR | Garage Door Sensor | CLOSED | 3600 | ALARM ON OPEN | NO ALARM ON CLOSE | DO NOT CALL ON OPEN | DO NOT CALL ON CLOSE | ONLINE | BATTERY OK | CLEARED | 000000B357 |        
        */
        String regexDelimiters = "\\|";
        String[] tokens = eventString.split( regexDelimiters );
        
        String  topic           = tokens[ 0 ];
        this.dateTime           = createDateTime( tokens[ 1 ].trim() );
        this.deviceType         = Integer.parseInt( tokens[ 2 ].trim() );
        this.deviceTypeString   = tokens[ 3 ].trim();
        this.deviceName         = tokens[ 4 ].trim();
        this.deviceStatus       = tokens[ 5 ].trim();
        this.statusDuration     = Integer.parseInt( tokens[ 6 ].trim() );
        this.alarmOnSetting1    = tokens[ 7 ].trim();
        this.alarmOnSetting2    = tokens[ 8 ].trim();
        this.callOnSetting1     = tokens[ 9 ].trim();
        this.callOnSetting2     = tokens[ 10 ].trim();
        this.deviceOnline       = (tokens[ 11 ].trim().equalsIgnoreCase( "ONLINE" ) );
        this.batteryOK          = (tokens[ 12 ].trim().equalsIgnoreCase( "BATTERY OK" ) );
        this.triggered          = (tokens[ 13 ].trim().equalsIgnoreCase( "TRIGGERED" ) );
        this.macAddress         = tokens[ 14 ].trim();
    }
    
    //--------------------------------------------------------------------------
    private java.util.Calendar  createDateTime (String dateTimeStr)
    {
        Calendar    aCal = Calendar.getInstance();
        SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss z" );
        try {
            aCal.setTime( sdf.parse( dateTimeStr ) );
        } catch (ParseException ex) {
            //
            //  In the event of an error - lets just set the event time to "now"
            aCal.setTimeInMillis( System.currentTimeMillis() );
        }
        
        return aCal;
    }
    
    //--------------------------------------------------------------------------
    public Calendar getDateTime() {
        return dateTime;
    }

    public void setDateTime (Calendar dateTime) {
        this.dateTime = dateTime;
    }

    public int getDeviceType() {
        return deviceType;
    }

    public void setDeviceType (int deviceType) {
        this.deviceType = deviceType;
    }

    public String getDeviceTypeString() {
        return deviceTypeString;
    }

    public void setDeviceTypeString (String deviceTypeString) {
        this.deviceTypeString = deviceTypeString;
    }

    public String getDeviceName() {
        return deviceName;
    }

    public void setDeviceName(String deviceName) {
        this.deviceName = deviceName;
    }

    public String getDeviceStatus() {
        return deviceStatus;
    }

    public void setDeviceStatus(String deviceStatus) {
        this.deviceStatus = deviceStatus;
    }

    public int getStatusDuration() {
        return statusDuration;
    }

    public void setStatusDuration(int statusDuration) {
        this.statusDuration = statusDuration;
    }

    public String getAlarmOnSetting1() {
        return alarmOnSetting1;
    }

    public void setAlarmOnSetting1(String alarmOnSetting1) {
        this.alarmOnSetting1 = alarmOnSetting1;
    }

    public String getAlarmOnSetting2() {
        return alarmOnSetting2;
    }

    public void setAlarmOnSetting2(String alarmOnSetting2) {
        this.alarmOnSetting2 = alarmOnSetting2;
    }

    public String getCallOnSetting1() {
        return callOnSetting1;
    }

    public void setCallOnSetting1(String callOnSetting1) {
        this.callOnSetting1 = callOnSetting1;
    }

    public String getCallOnSetting2() {
        return callOnSetting2;
    }

    public void setCallOnSetting2(String callOnSetting2) {
        this.callOnSetting2 = callOnSetting2;
    }

    public boolean isDeviceOnline() {
        return deviceOnline;
    }

    public void setDeviceOnline(boolean deviceOnline) {
        this.deviceOnline = deviceOnline;
    }

    public boolean isBatteryOK() {
        return batteryOK;
    }

    public void setBatteryOK(boolean batteryOK) {
        this.batteryOK = batteryOK;
    }

    public boolean isTriggered() {
        return triggered;
    }

    public void setTriggered(boolean triggered) {
        this.triggered = triggered;
    }
    
    public String getMacAddress() {
        return macAddress;
    }

    public void setmacAddress(String macAddress) {
        this.macAddress = macAddress;
    }


}

Digging In

Starting right at the top of the class, there's a direct mapping between a field in the MQTT message that represent an event and an attribute in this class.

Here's the first few fields from an HHB/STATUS event from MQTT:

HHB/STATUS | 2014-05-15 11:31:50 -0600 | 03 | OPEN-CLOSE SENSOR | Front Door | CLOSED | 14400 | <snip>


And the first few attributes in the Java class:

public class HHBStatusEvent 
{
    java.util.Calendar  dateTime;             // date/time event was created
    int                 deviceType;           // HHB device type (eg. 23 = Motion Sensor)
    String              deviceTypeString;     // String of type (eg. "MOTION SENSOR")
    String              deviceName;           // Sensor name (eg. "FRONT DOOR")
    String              deviceStatus;         // Sensor state (eg. "OPEN", "CLOSED", "MOTION", "NO MOTION")
    int                 statusDuration;       // How long (seconds) the sensor has been in this state
    <snip!>

   
Only the getter and setter methods are required. The toString() and createDateTime() methods are there to help me out.

The other not-essential-to-Esper method, fromEventString() is an easy way for me to create an instance of an HHBStatusEvent object from the payload of an MQTT message.

That's enough for now.
We'll look at the Listener class next. This class has the method that Esper will call when it finds a match for your events in the event stream.