Code review wanted: notification suite with @recipients, urgency levels, repeating, and presence-based message queuing

notification

#1

I recently switched to a more convenient and consistent method for sending notifications but it would benefit from a code review and any other feature ideas before sharing it in Examples. I’m looking for feedback on both the concept and implementation. Is this useful for you, is there anything else you would like to see, and is the documentation below easy enough to follow?

Overview

Execute the Smart Notify piston with a single argument, message, to send SMS, push, email, or any other kind of notifications to multiple recipients. Configure the recipients in Smart notify and use that contact info to send the messages in Smart notification sender.

Features

Recipients

After installing the piston, configure it with any number of named recipients. You will message them with standard @Someone notation.

@Hank Bag was left at home

Hank will receive a message with the text “Bag was left at home”

@Divya @Hank Smoke detected in the guest cottage

Both Divya and Hank will receive a message with the text “Smoke detected in the guest cottage”

Urgency

The channel(s) over which a notification is sent is determined by the urgency of the message. For example, while a push notification is fine for a non-essential notification you may want to get an SMS and phone call for critical messages. This is handled with a simple bracket notation:

[low] Open the windows

The washing machine is finished

[critical] Barn may be on fire

The urgencies and notification channels will vary for everyone, so I included a separate piston that is in charge of sending the notifications based on the urgency level and recipient data. It serves as an example but will need to be modified to fit how you want to use notifications. I use the following, though you can use any terminology for the levels and any means of notification for each level:

  • low – send an email
  • normal – send a push notification to the specified contact
  • high – send an SMS to the specified phone
  • critical – use IFTTT to call my phone (only one phone number per account), send an SMS by Verizon’s @vtext.com email (different ringtone), send an email

I have SmartThings saved as a contact in my phone so that I can use specific ringtones for SMS sent from SmartThings and sent from the webcore.co email to reinforce the urgency… the SMS for a critical notification is definitely going to get my attention if I somehow miss the also very alarming ringtone for calls from IFTTT.

I’m not keen on naming this “urgency” or “level” – that’s a perfectly valid use case for it but you could set up any number of channels in Smart notification sender and name them any way you want. Any ideas?

Presence logic

Some messages only need to be sent if the recipient is currently present. In some cases messages should be queued until you get back home, delivered only once you’re home and able to act on them.

@Divya:when-present Empty the dishwasher

This message will be sent to Divya either immediately if she is already home, otherwise as soon as she arrives.

@Hank:when-away Check for missing equipment

This message will be sent to Hank either immediately if he is already away, otherwise as soon as he leaves.

@Hank:if-away @Divya:if-away Open the windows at the office

Both will receive the notification if they are away. If both are home, the message is ignored.

A notification can have any number of recipients and any number of notifications can be queued up until the required presence change.

@Divya:if-present @Hank:if-present @Divya @first The door is open!

The use of @first here will pare the list down to only the first valid recipient. For example, without @first if both Hank and Divya are home the valid recipients list will be “Divya, Hank, Divya”, if Divya is away the recipients list will be “Hank, Divya”, and if both are away the recipients list will be “Divya” – the @first command delivers the message only to the first valid recipient so in these examples Divya, Hank, and Divya respectively.

Is the distinction between the words “when” and “if” clear enough to convey the difference in behavior? If not, maybe something more verbose but clearer like @Divya:wait-until-present?

Repeat until cancelled

I really need to be pestered to remember to switch over a load of laundry; one alert is not enough. The Smart Notify piston allows you to repeat a notification at a predetermined interval. There is one interval running to handle all repeating messages, 5 minutes is a good default for me but you can configure that at the top of the Smart notify piston. Unfortunately the frequency is not configurable on a per-message basis.

Repeating messages require a keyword so that you can cancel the message at a later time. For example, Hank will receive the message “The dishwasher is finished” every 5 minutes if he’s home. If he is away, the message will still be attempted every 5 minutes but the presence logic will cause it to not be sent until he gets back.

@repeat:dishwasher @Hank:if-present The dishwasher is finished

Just be sure to set up a piston to cancel the repeating message. I use a contact sensor on the dishwasher to cancel repeating messages when the door is opened, and power meters on the washer and dryer. Simply send a @cancel message to the Smart notify piston:

@cancel:dishwasher

If you don’t have a way to automatically detect that an action has been completed you could instead use a virtual or physical button that cancels any active repeating messages with:

@cancel:all

Repeating messages can use any features of Smart notify (e.g. multiple recipients and presence logic) with the exception of :when-away and :when-present for which you should use :if-away and :if-present instead.

Configuration

The Smart notify piston has a few variables to configure as well as a section for recording the contact info for each recipient. After installing the piston you will be asked for two presence devices, these are for the two sample contacts which can be added to or removed later. If each person has multiple presence devices, combine their presence value with Maximum.

Look at the comments for cues on how to format the values and where to stop editing. There are also three Execute Piston calls in the bottom half of the code that need to be updated to point to your Smart notify and Smart notification sender pistons.

The Send smart notification piston may require much more creative configuration based on the number of urgency levels you want and the channels through which you want to receive notifications. The example uses IFTTT to place a phone call for urgent messages but note that IFTTT integrations like voice call and SMS do not work with multiple recipients since you can only verify one number per IFTTT account. I also use an SMS notification via email to Verizon’s @vtext.com service in order to have two different ringtones for SMS to reinforce the urgency.

Feel free to build one from scratch if my Send smart notification is not what you want; I think this initial example is too opinionated and complex to distribute but let me know what you think. The messages, subjects, etc. use sprintf formatting for some flexibility, be sure to look for the list of “placeholders” in the comment instructions.

Usage

To send a notification from one of your pistons, create a string variable called message. Assign to that variable the contents of your message then pass the variable as an argument to the Smart notify piston using the Execute piston action. No other arguments are needed since all of the functionality described above is controlled by the message.

Installation

  1. Add a new piston
  2. Restore a piston using a backup code
  3. Name the first piston Smart notify and use the backup code wumg8
  4. Edit the contact information and Execute piston calls as described in Configuration above
  5. Add a new piston
  6. Restore a piston using a backup code
  7. Name this piston Smart notification sender and use the backup code b2hx
  8. Edit the piston or make a copy to notify in ways that make sense to you

Disclaimer Please do not switch all your live notifications to this… it needs more thorough testing and the implementation details may change before it is posted to Examples. Do install it and set up a simple piston to test sending messages with it.

If you decide to switch any live notifications over to Smart notify I highly recommend setting up a third piston that just receives a message argument and forwards it to Smart notify. Reference that abstraction in your pistons rather than directly executing Smart notify so that if you replace Smart notify with a later version you only need to update the single Execute piston call in your abstraction.

&


“Backup Bin Updated” msg while trying to save Piston - Does not save to cloud
#2

Aaaaaaand he goes and builds yet another programming language on top of webCoRE :smiley: :smiley: :smiley: Great! I mean, great job! :smiley:


#3

IMG_3629


#4

Heh, thanks Adrian let’s hope it works :wink:

I encountered a few issues and questions while working through this:

  1. Contact ID: Most importantly, I can’t remember how I got the :240c0c8af7d641d3b213e2e90269f2e9: values for contacts, probably network inspector or from the angular scope. I noticed that they do not match the ID in the URL for the contact in ST developer – is there a convenient place to find these IDs?
  2. Zero index: I had trouble a week or two ago with accessing and setting values at index zero in arrays so I made all the arrays 1-indexed :scream: I need to try this again to see if it’s still acting up.
  3. Replace first occurrence: is there a way to do a regex replace that is non-global? The g flag made it a bit funky to pull out one @recipient at a time
  4. For each w/ arrays: it would be awesome if for each worked with arrays other than device lists but tracking the length and occasionally using a second array to keep track of keys worked fine
  5. Piston return value: is there any sneaky way to execute a piston then get a result into the caller? I wanted to have the contact info in a separate piston to make updating this one easier but since global variables [thankfully] don’t update mid-execution I couldn’t find a reasonable way to do it
  6. Global arrays: Another alternative to configuring the contact info here would have been storing the contact info globally but there aren’t any global arrays – would that be too much of a privacy concern anyway?
  7. Null: Is there a better way to test for null than foo == $args.$null (where $null could be any key that is never defined)? Since message is based on $args.message it can be null rather than a blank string.
  8. For step 0: The for loop defaults to a step of 0. I never actually tried it but does that just increment by 1 or does it operate like a while loop? Seemed like a strange default.

#5

Well that puts my notification handler piston to shame!


#6

I found a few answers in case anyone has similar questions:

  1. Zero index: I had trouble a week or two ago with accessing and setting values at index zero in arrays so I made all the arrays 1-indexed :scream: I need to try this again to see if it’s still acting up.

This turned out to be a glitch specifically affecting the default $index variable in loops, zero-indexed arrays work fine with a custom counter variable (see bug report).

  1. Replace first occurrence: is there a way to do a regex replace that is non-global? The g flag made it a bit funky to pull out one @recipient at a time

I learned that the g flag is automatic in Groovy and trying to disable it with (?-g) tosses an exception. I also forgot that I can use (?s) in the expression in order to let .* pull in line terminators rather than [\\s\\S]* with which this piston is littered.

  1. Null: Is there a better way to test for null than foo == $args.$null (where $null could be any key that is never defined)? Since message is based on $args.message it can be null rather than a blank string.

A cleaner solution is isEmpty(foo) when I want to test that the value is neither an empty string nor null. If you need to explicitly check for null then the $args.$null trick would be better.

  1. For step 0: The for loop defaults to a step of 0. I never actually tried it but does that just increment by 1 or does it operate like a while loop? Seemed like a strange default.

It operates as a step of 1.

Next revision will probably focus on cleaning up the notification sender. Pushover notifications look incredibly useful for this. I’m not going to be able to create a general purpose sender but one example using Pushover and one using just free built-in messaging would be nice.


#7

I can add replaceOnce if that helps?


#8

That would be nice but certainly no urgency, it wasn’t difficult to work around but these are pretty simple expressions.


#9

any way to check if $args.variable is null or was not passed in?

thanks!


#10

I used if $args.variable is $args.$null where $null is an arbitrary thing you know won’t be passed in, or are you saying that a true null value be passed in and that needs to be tested differently?


#11

trying to come up with a piston that gets executed by IFTTT and also runs on a schedule. from IFTTT it passes in args but when run on schedule there are no args.

i tried checking $args.variable != $args.$undefined and that throws this:

+695ms ║An error occurred while executing the event: java.lang.ClassCastException

thanks!


#12

Are you sure that specific test is throwing the exception and not something else in the piston? It works for me.


#13

from the trace it looks like it … may be i am missing something?


#14

Hard to say, the trace just shows that the condition evaluated to false.


Pushbullet Capability?
#15

yeah. its a scratch piston, so going to skip posting the entire piston for now and look at it later.

thank you.


#16

While discussing alternate notification services today I realized that with the current structure of these pistons it would be very difficult to add more properties for each recipient, like a device ID for Pushbullet. You would have to add a new array, populate it for each contact, create a variable to hold the argument value, add it to the argument list when executing the sender piston, then update the sender to make use of that argument.

Instead, contact info will be maintained in the sender piston with simple local variables for each of the datapoints, populated in a switch based on the recipient name. Adding a datapoint is as simple as creating the variable, setting the value for each recipient, then making use of it.

There are several merits to this change:

  • Less to edit when Smart notify is updated
  • “Public API” of the sender (now just level, recipient, message) is less subject to change
  • The sender piston can be executed directly by that API rather than going through Smart notify for simple notifications
  • Contact info does not need to be stored in variables, just set temporarily for the current execution

with a few concessions:

  • Recipients’ presence still needs to be tracked in Smart notify, I don’t think there is any way around this without global arrays
  • Sender piston (which some people will just rewrite from the ground up) is more complicated

#17

Maybe it would be easier to add a specialized task in webCoRE that implements the whole logic?


Laundry monitor with power meter piston - anyone?
Send Pushover notifications
#18

That would be very interesting! Let’s see once this is ironed out and posted in Examples what the response is and what requests come in. I am very near a size limit here, could squeak out a bit more by removing debugging but the debugging is necessary to make it testable.

The latest revision features:

  • much easier to write a custom sender since all the contact info is in local variables in the sender rather than arguments between two pistons
  • custom sender is usable independently since it just needs a recipient name and message
  • can deliver to recipients whose presence is not trackable (just can’t use :if-present et al. with them)
  • fewer arrays and less iteration thanks to help from @bangali over here
  • more thorough debug logging
  • separate piston that runs test cases
  • slightly less ugly regex due to (?s) flag

Smart notify piston

Basic sender piston using built-in webCoRE features

Tests

Some issues I had to work around:

  • arrayItem, count, etc barf when array values have commas in them, so I replaced commas in the repeating and presence messages with a unicode character in the private use subset, reverted when read out of the array
  • attempting to save with 31 chunks broke the entire dashboard temporarily, ISE’s everywhere

The next step for me is to switch over all my pistons to use it for some more thorough testing.


#19

Earlier I had forgotten how I got the contactId values. I haven’t found a good way to do this but I’ve been doing the following:

  1. Open browser’s Developer Tools from the page that shows your piston
  2. Navigate to console
  3. Paste c = $('ng-view').scope().instance.contacts; console.log(Object.keys(c).map(id => `${c[id].f} ${c[id].l}: ${id}`).join('\n')) and hit enter

The console then displays the names and IDs of your contacts. Any easier way to get these IDs or alternate expressions for sending a notification to a contact?


Who pressed the button and notify only those present
#20

you dont have ST setup to support notification to contacts? once you have done that go to webcore settings and check the contacts you want webcore to be able to send notification to.