[RELEASE] Scrolling Event Notifier Driver

Back in April '22, I was looking for a way of displaying multiple events sequentially on a dashboard tile. Following my post on the forum, @thebearmay came up with his 'Message Rotator Tile Device' driver and I've been using that on my HD+ dashboards ever since. I wondered whether it was possible to do something similar but have the events scroll rather than display sequentially. Using the original driver code and ChatGPT, I've cobbled together 'Scrolling Event Notifier'. Some information:

What is it?

A device driver based heavily on Message Rotator Tile Device by @thebearmay. It can be used to display one or more messages (entered as strings) on a dashboard tile.

Features

  • Add, remove, or change single message
  • Remove all messages
  • Selectable modes - Standard(cycle mode) or Scrolling. Scrolling is the default option
  • In scrolling mode a single message is displayed statically, scrolls when multiple messages added
  • User can choose any valid character/symbol/emoji to use as a separator between scrolling messages and select the number of spaces padding either side
  • Adjustable scroll speed for Scrolling mode, adjustable pause time for cycle mode
  • Adjustable font size*
  • Notification capability for ease of adding, removing, clearing all messages from Rule Machine (Actuator capability with custom command still available)

How To Add The Driver

  • Go to Developers > Drivers Code
  • Click 'Add Driver'
  • Paste the code from below and Save

Add The Device

  • Go to Devices > Add Device > Virtual
  • Select 'Scrolling Event Notifier' from the type dropdown

How To Use/Example

To add, remove, change a message or remove all messages as an action in Rule Machine:

  • Create a new action and Select Action Type To Add > Set Variable, Mode or File, Run Custom Action > Run Custom Action
  • Select capability of action device > Actuator and select your new 'device'
  • addMessage > Add a Parameter > Parameter Type - String
  • enter a unique number in the string value field and save
  • Repeat Add a Parameter > Parameter Type - String
  • Now enter your message in the string value field and save

The number added is paired with the message. Removing a message is done the same as above, but by selecting remMessage and entering the corresponding number in the string value field.

In most of my rules the message is added to the tile as an action by the rule and also removed once the condition has cleared . I have a virtual button next to the notifier tile on my dashboards that can be used to send the 'clear' command manually where a rule is not being used to remove messages automatically.

The Code

Version 1.9.8 beta
/*
 * Scrolling Event Notifier - v1.9.8-beta
 * Based on 1.9.7-stable with Message Count attribute
 */

static String version() { return '1.9.8-beta' }

metadata {
    definition (
        name: "Scrolling Event Notifier",
        namespace: "John Williamson",
        author: "John Williamson with ChatGPT",
        singleThreaded: true
    ) {
        capability "Actuator"
        capability "Notification"

        attribute "html", "string"
        attribute "version", "string"
        attribute "messageCount", "number"

        command "deviceNotification", [[
            name:"msg",
            type:"STRING",
            description:"'MsgID, Message' or 'p, MsgID, Message' (for priority)"
        ]]
        command "addMessage", [
            [name:"msgID*", type:"STRING"],
            [name:"msgContent*", type:"STRING"]
        ]
        command "remMessage", [
            [name:"msgID*", type:"STRING"]
        ]
        command "clear", [[
            name:"info",
            type:"STRING",
            description:"No input needed, click 'Run' to clear all messages"
        ]]
    }
}

preferences {
    input("debugEnabled", "bool", title: "Enable debug logging?")
    input("fontSizePx", "number", title: "Font size (px)", defaultValue:30)
    input("speedPxPerSec", "number", title: "Scroll Speed (pixels/sec)", defaultValue:50)

    input("separatorChoice", "text", title: "Message Separator",
          description: "Enter character, symbol, emoji to separate messages. Leave blank for default ('|')")

    input("separatorPadding", "number", title: "Separator Padding",
          description: "Number of blank spaces to add either side of the separator",
          defaultValue:3)

    input("mode", "enum", title: "Mode",
          options:["Scroll Message","Standard (Cycle Message)"],
          defaultValue:"Scroll Message")

    input("pauseTime", "number", title: "Pause Time (seconds)", defaultValue:5)

    input("noMessageText", "text", title: "No Message Display",
          description: "text to show when no message present, leave blank for empty tile")

    input("priorityColorPreset", "enum", title: "Priority Message Colour",
          options:["Hex Value","red","orange","yellow","green","blue","indigo","violet",
                   "purple","pink","brown","gray","black","white","teal","lime",
                   "cyan","magenta"],
          required:true)

    input("priorityColorHex", "text", title: "Hex Color",
          description: "Enter a hex color (e.g., #FF00FF) if 'Hex Value' is selected",
          required:false)

    input("priorityStyle", "enum", title: "Priority Message Style",
          options:["Standard","Bold","Italic","Underline","Bold & Italic","Bold & Underline",
                   "Italic & Underline","Bold, Italic, & Underline"],
          defaultValue:"Standard")
}

/* ---------------- Core ---------------- */

def installed() {
    state.messages = [:]
    state.priority = [:]
    state.standardIndex = 0
    showEmpty()
    sendEvent(name:"version", value:version())
    sendEvent(name:"messageCount", value:0)
}

def updated() {
    unschedule()
    if(state.messages == null) state.messages = [:]
    if(state.priority == null) state.priority = [:]
    sendEvent(name:"version", value:version())
    sendEvent(name:"messageCount", value: state.messages.size())
}

/* ---------------- Notification Parser ---------------- */

void deviceNotification(String msg) {

    if(state.messages == null) state.messages = [:]
    if(state.priority == null) state.priority = [:]

    if(!msg) return
    msg = msg.trim()

    if(msg.equalsIgnoreCase("clear")) {
        clear()
        return
    }

    if(msg.contains(",")) {

        if(msg.toLowerCase().startsWith("p,")) {
            def parts = msg.split(",",3)
            if(parts.size()==3){
                def id = parts[1]?.trim()
                def content = parts[2]?.trim()
                state.messages[id]=content
                state.priority[id]=true
                sendEvent(name:"messageCount", value: state.messages.size())
                displayTicker()
                return
            }
        }

        if(msg.toLowerCase().startsWith("rem,")) {
            def parts = msg.split(",",2)
            if(parts.size()==2){
                remMessage(parts[1]?.trim())
                return
            }
        }

        def parts = msg.split(",",2)
        if(parts.size()==2){
            def id = parts[0]?.trim()
            def content = parts[1]?.trim()
            state.messages[id]=content
            state.priority[id]=false
            sendEvent(name:"messageCount", value: state.messages.size())
            displayTicker()
            return
        }
    }
}

/* ---------------- Manual Commands ---------------- */

void addMessage(id, content){
    if(state.messages == null) state.messages = [:]
    if(state.priority == null) state.priority = [:]

    state.messages[id]=content
    state.priority[id]=false
    sendEvent(name:"messageCount", value: state.messages.size())
    displayTicker()
}

void remMessage(id){
    if(state.messages == null) state.messages = [:]
    if(state.priority == null) state.priority = [:]

    state.messages.remove(id)
    state.priority.remove(id)
    sendEvent(name:"messageCount", value: state.messages.size())

    if(state.messages.size()==0) showEmpty()
    else displayTicker()
}

void clear(){
    state.messages=[:]
    state.priority=[:]
    sendEvent(name:"messageCount", value:0)
    showEmpty()
}

/* ---------------- Display Logic ---------------- */

private void showEmpty(){
    def html = noMessageText ?
        "<div style='font-size:${fontSizePx}px;text-align:center;'>${noMessageText}</div>" :
        "<div>&nbsp;</div>"
    sendEvent(name:"html", value:html)
}

private String getPriorityStyle(String key){
    if(state.priority[key]){
        def color = (priorityColorPreset=="Hex Value" && priorityColorHex) ?
            priorityColorHex : priorityColorPreset
        def style = ""
        switch(priorityStyle){
            case "Bold": style="font-weight:bold;"; break
            case "Italic": style="font-style:italic;"; break
            case "Underline": style="text-decoration:underline;"; break
            case "Bold & Italic": style="font-weight:bold;font-style:italic;"; break
            case "Bold & Underline": style="font-weight:bold;text-decoration:underline;"; break
            case "Italic & Underline": style="font-style:italic;text-decoration:underline;"; break
            case "Bold, Italic, & Underline": style="font-weight:bold;font-style:italic;text-decoration:underline;"; break
            default: style=""
        }
        return "color:${color};${style}"
    }
    return ""
}

void displayTicker(){

    if(state.messages.size()==0){
        showEmpty()
        return
    }

    def fontSize = fontSizePx ?: 20
    def speed = speedPxPerSec ?: 50
    def padCount = separatorPadding ?: 3

    def visualPad = "&nbsp;" * padCount
    def separatorSymbol = separatorChoice?.trim() ?: "|"
    def separator = visualPad + separatorSymbol + visualPad

    def styled = state.messages.collect{ k,v ->
        "<span style='${getPriorityStyle(k)}'>${v}</span>"
    }

    if(mode=="Standard (Cycle Message)"){
        if(state.messages.size()==1){
            sendEvent(name:"html",
                value:"<div style='font-size:${fontSize}px;text-align:center;'>${styled[0]}</div>")
            return
        }

        state.standardIndex = state.standardIndex ?: 0
        cycleStandardMessage()
        return
    }

    if(state.messages.size()==1){
        sendEvent(name:"html",
            value:"<div style='font-size:${fontSize}px;text-align:center;'>${styled[0]}</div>")
        return
    }

    def combined = styled.join(separator)

    int approxWidthPx = state.messages.collect { k,v ->
        v.length() * fontSize * 0.6
    }.sum()

    int logicalSeparatorLength = (separatorSymbol.length() + (padCount * 2))
    approxWidthPx += (logicalSeparatorLength * (state.messages.size()-1) * fontSize * 0.6)

    int durationSec = Math.max(5,(approxWidthPx/speed).toInteger())

    def html =
        "<div style='width:100%;overflow:hidden;font-size:${fontSize}px;white-space:nowrap;'>" +
        "<div style='display:inline-block;white-space:nowrap;animation:scroll ${durationSec}s linear infinite;'>" +
        combined +
        "</div>" +
        "<style>@keyframes scroll{0%{transform:translateX(100%);}100%{transform:translateX(-100%);}}</style>" +
        "</div>"

    sendEvent(name:"html", value:html)
}

/* ---------------- Standard Cycling ---------------- */

void cycleStandardMessage(){
    if(state.messages.size()<2){
        displayTicker()
        return
    }

    def keys = state.messages.keySet().toList()
    if(state.standardIndex==null || state.standardIndex >= keys.size()) state.standardIndex = 0
    String key = keys[state.standardIndex]
    String msg = state.messages[key]
    String style = getPriorityStyle(key)

    sendEvent(name:"html", value:"<div style='font-size:${fontSizePx}px;text-align:center;${style}'>${msg}</div>")

    state.standardIndex++
    if(state.standardIndex >= keys.size()) state.standardIndex = 0

    runIn(pauseTime ?: 3, "cycleStandardMessage")
}

Driver Version 1.9.1:

  • Changed scrolling message separator. Was a drop down choice of 5 characters. Now you can select any valid character, symbol or emoji. '|' will be used if no input made.

  • Added a preference for padding around the scrolling message separator

  • Added a preference for what to show on the tile if there are no messages. Blank by default, but you can add anything.

Driver version 1.8.3:

Notification Capability Added - On the initial version you had to use the custom command on the actuator capability of the device to add, change, remove messages. This method can still be used.

The notification capability makes it far simpler to action adding messages in Rule Machine. Simply use the Send Message > Send Speak a Message. Then in the 'Message to Send' box:

  • add message: Enter a message ID and message separated by comma. For example, '123, laundry complete' or 'weather, rain at times'

  • remove message: Enter 'rem' and the message ID separated by comma. For example, 'rem, 123' (removes 'laundry complete' in the example) or 'rem, weather' (removes 'rain at times' in the example)

  • clear all messages: Enter 'clear'

Selectable Mode - You can now select between 'Scroll Message' and 'Standard (Cycle Message)'.

The default is to 'Scroll Message'. When this is selected the scroll speed can be adjusted (to suit your tile size) and there's a choice of 5 seperators to use between the messages.

'Standard (Cycle Message)' works similarly to how 'Message Rotator Tile' device worked. When more than one message is present, they are cycled/swapped on the tile. A 'Pause Time' preference is used to dictate how long each message should persist in the tile.

Known issues:

  • I cannot code - the driver is a result of me shouting at ChatGPT over a number of beers.

  • I'd rather that the text size was dictated by the dashboard tile. However I couldn't get things to play nice with the scrolling text so had to leave it with the text size being set by a preference.

  • I'm using HD+ as my dashboard not Hubitat's. When displaying on HD+, I have to edit the tile from 'HTML' to 'Custom' then in 'manage display items, select 'html'. I'm not sure where the issue is there - possibly that 'html' is needed to see the text formatting from the driver.

To Follow:

  • Tidy up the initial post with better instructions!

  • Improve formatting. Add text wrapping for longer messages (cycle mode only)

Could you include a picture of the working solution in the dashboard?

I've taken some boring photo's in my time but that would be a winner! It would be a photo of a tile with some static text in. There's no way of uploading video on the forum without linking to elsewhere so I've added two GIFs below that show messages being sent/removed in the device command window. The current states panel showing what would display on the tile.

The purpose of this driver for me was that I wanted a notification tile on my dashboard that could show multiple messages. When nothing is happening it will be blank, but my attention is drawn when one or more messages appear. It's nothing fancy but something I find useful as a standard way of sending messages to the dashboard. Hope this gives you a general idea:

Standard (cycle message) mode:
Standard Mode - Cycle Message

Scroll message mode:
Scrolling Mode Message

Thanks for sharing, I had misunderstood. I thought this was a generic solution that was aggregating messages across a number of devices. Kind of like the Log, but filtered.

Another update - 1.9.7 (code will be updated shortly in the top post)

This is getting close to how I wanted it to work.

Changes:

  • Added 'Priority Message' - A standard message would be sent as "msgID, someMessage" (e.g - "1 - lovely day") whereas a Priority Message would be sent as "p, msgID, some message" (e.g - "2 - oh dear here comes WW3")...

  • Added 'Priority Message Colour' preference - A dropdown list of standard colours. When a 'Priority Message' is added, that individual message will display in the selected colour, or...

  • Added 'Hex Colour' preference. When 'Hex Colour' is selected in the 'Priority Message Colour' dropdown, this can be used to enter the colour want using it's Hex value

  • Added 'Priority Message Style' preference - A dropdown list of text styles to be applied to Priority Messages. It's Standard by default, but any combination of Italic, Underlined, or Bold can be selected.

Trying this out and I have a question:

Is it possible to take a Weather Alert (ie OpenWeather) and use a device descriptor from the device like "alertDescr" to pass to the message notification?

I tried to create a simple rule that used this value as a trigger, but i could get it to pass what ever value that was in this field to the message notifiction..just some nulls scrolling by

I guess as long as what you're trying to send is a string it should display without issue.

Could you add a Message Count as a Current State value? I'd like to show tne message count next to the message on my dsshboard.

I'll see what I can do. I'm doing all of this with ChatGPT so it's a bit hit and miss (but I like tinkering with this much to my wife's displeasure!)

On this driver I have the preference to choose whether to have the message scroll or switch. I quickly hit issues with html formatting on my dashboards in that I couldn't have the messages scroll while the dashboard handled text size. In the end I decided to split it into two separate drivers; one for scrolling where text size, colour, effects are handled by the driver and one for switching where there's no formatting and the dashboard will size the text.

@JimB - Added messageCount in 1.9.8 beta. I've updated the code in the top post. It seems to be working...

Added your driver. Getting a "No signature of method" error when sending a message from a rule in format (nn,message)

Hmm - I can't replicate that. I've just tried both methods of adding a message from RM and both worked:

Method 1 (the original long winded way) - Custom Action > Actuator > Select Device > Add String for message ID, then add another for the message. You must do this as two string parameters - one for the ID and then another for the associated message. The message ID 'number' must be sent as type string (not number)

Method 2 (the easy way) - Send Message.... > Send/Speak a message > enter the ID and message in the format - ID, message or - p, ID, message to tag it 'Priority'. Examples:

'1, standard message' (will use the standard text)
'p, 2, important message' (will apply the priority formatting from the preferences)
'rem, 1' or 'rem, 2' would remove the message (no need to include the 'p' when removing a priority message)

@JohnWill1

Thanks for this driver. I am using it in consort with @thebearmay "Dashboard Variable Device driver" to display Temp, RH and Dew Point readings from multiple sensors. I'm just getting my feet wet with both drivers. At this point I am just focused on understanding very basic functionality. I have Standard(Cycle Message) working. I did note this in the log files. It does not appear to be interfering with what I have accomplished thus far. Is there anything I should check on my end?

error

groovy.lang.MissingMethodException: No signature of method: user_driver_John_Williamson_Scrolling_Event_Notifier_2618.addMessage() is applicable for argument types: (java.lang.String) values: [Outdoor: Temp 70Âş - Humidity 82% - Dew Point 59Âş] Possible solutions: addMessage(java.lang.Object, java.lang.Object), remMessage(java.lang.Object) on line 6504 (method addMessage)

Thank You

I'm not sure how you're adding your message to the driver. It seems from the error that you're adding the message directly without an ID in front of it (according to ChatGPT). If you look at the example in the GIFs further up you'll see that it should be added as 'msgID, message' for example '1234, this is my message' (the message ID is not displayed, it is used so that the specific message can also be removed).

I've since stopped using that driver. Having one driver with the option for scrolling or cycling caused issues. I made another driver with only the cycling option. This works without needing a message ID:

/*
 * Notification Tile Sequencer - v1.11
 * Split from 'Scrolling Event Notifier' for sequencing events only
 */

static String version() { return '1.11' }

metadata {
    definition (
        name: "Notification Tile Sequencer",
        namespace: "John Williamson",
        author: "John Williamson with ChatGPT",
        singleThreaded: true
    ) {
        capability "Actuator"
        capability "Notification"
        attribute "html", "string"
        attribute "version", "string"

        command "deviceNotification", [[
            name:"msg",
            type:"STRING",
            description:"'MsgID, Message' or 'p, MsgID, Message' (for priority)"
        ]]
        command "addMessage", [
            [name:"msgID*", type:"STRING"],
            [name:"msgContent*", type:"STRING"]
        ]
        command "remMessage", [
            [name:"msgID*", type:"STRING"]
        ]
        command "clear", [[
            name:"info",
            type:"STRING",
            description:"No input needed, click 'Run' to clear all messages"
        ]]
    }
}

preferences {
    input("debugEnabled", "bool", title: "Enable debug logging?")
    input("displayTime", "number", title: "Display Time (seconds)", defaultValue:5)
    input("blankTime", "enum", title: "Blank Time (seconds)",
          options:["none","1","2","3"],
          defaultValue:"none",
          description: "Tile will be empty for this many seconds between messages; 'none' disables")
    input("noMessageText", "text", title: "No Message Display",
          description: "Text to show when no message present, leave blank for empty tile")
    input("priorityIndicator", "enum", title: "Priority Message Indicator",
          options:["⚠️","🔔","✋","‼️","⛔️","🚨","💩"],
          defaultValue:"⚠️")
}

/* ---------------- Core ---------------- */

def installed() {
    state.messages = [:]
    state.priority = [:]
    state.standardIndex = 0
    showEmpty()
    sendEvent(name:"version", value:version())
}

def updated() {
    unschedule()
    sendEvent(name:"version", value:version())
}

/* ---------------- Notification Parser ---------------- */

void deviceNotification(String msg) {
    if(!msg) return
    msg = msg.trim()

    if(msg.equalsIgnoreCase("clear")) {
        clear()
        return
    }

    if(!msg.contains(",")) {
        def id = "auto_${now()}"
        state.messages[id] = msg
        state.priority[id] = false
        displayTicker()
        return
    }

    if(msg.contains(",")) {
        def parts = msg.split(",",3)
        def first = parts[0]?.trim()

        if(first.equalsIgnoreCase("p") && parts.size()==3) {
            def id = parts[1]?.trim()
            def content = parts[2]?.trim()
            def indicator = priorityIndicator ?: "⚠️"
            state.messages[id] = "${indicator}${content}${indicator}"
            state.priority[id]=true
            displayTicker()
            return
        }

        if(first.equalsIgnoreCase("rem") && parts.size()>=2) {
            remMessage(parts[1]?.trim())
            return
        }

        if(parts.size()>=2) {
            def id = parts[0]?.trim()
            def content = parts[1]?.trim()
            state.messages[id]=content
            state.priority[id]=false
            displayTicker()
            return
        }
    }
}

/* ---------------- Manual Commands ---------------- */

void addMessage(id, content){
    state.messages[id]=content
    state.priority[id]=false
    displayTicker()
}

void remMessage(id){
    state.messages.remove(id)
    state.priority.remove(id)
    if(state.messages.size()==0) showEmpty()
    else displayTicker()
}

void clear(){
    state.messages=[:]
    state.priority=[:]
    showEmpty()
}

/* ---------------- Display Logic ---------------- */

private void showEmpty(){
    def html = noMessageText ? noMessageText : " "
    sendEvent(name:"html", value:html)
}

void displayTicker(){
    if(state.messages.size()==0){
        showEmpty()
        return
    }

    if(state.messages.size()==1){
        def key = state.messages.keySet().first()
        def msg = state.messages[key]
        sendEvent(name:"html", value:msg)
        return
    }

    state.standardIndex = state.standardIndex ?: 0
    cycleStandardMessage()
}

/* ---------------- Standard Cycling ---------------- */

void cycleStandardMessage(){
    if(state.messages.size()<2){
        displayTicker()
        return
    }

    if(state.showingBlank == null) state.showingBlank = false

    def keys = state.messages.keySet().toList()
    if(state.standardIndex==null || state.standardIndex >= keys.size()) state.standardIndex = 0

    if(state.showingBlank && blankTime != "none"){
        sendEvent(name:"html", value:" ")
        state.showingBlank = false
        runIn(blankTime.toInteger(), "cycleStandardMessage")
    } else {
        String key = keys[state.standardIndex]
        String msg = state.messages[key]
        sendEvent(name:"html", value:msg)
        state.showingBlank = (blankTime != "none")

        state.standardIndex++
        if(state.standardIndex >= keys.size()) state.standardIndex = 0

        runIn(displayTime ?: 3, "cycleStandardMessage")
    }
}

Thanks for your response. I was just going to edit my post as I discovered it was likely cause by an oversight :slight_smile: a.k.a ERROR on my part. Upon review I realized that I was in fact not using your device in the tile. The tile was still using @thebearmay Rotator Driver.

This is screen shot of work in progress. My goal is to eventually scroll data (temp,RH,DewPoint) from each sensor and eliminate the individual tiles. At the moment I am attempting to keep it simple and feed data to individual sensor tiles.

Regardless of whether or not I end up going this route, I am enjoying the learning exercise as your driver and @thebearmay drivers offer many possibilities.

Thank You
Charlie

@johwill1

I'm back. I installed the driver you posted. I have only used the device interface thus far to test. That interface requires a msgID in order to proceed.

Once msgID is added to the mix all works as expected.

There are two methods for adding a message:

1 - As you've shown. An ID and message are required. If you were to use this in Rule Machine you'd need to use the Custom Command option, pick actuator as device type and the select your device.

2 - Use the 'Device Notification' option. That works with and without a message ID. In Rule Machine you just use the Send a Message, Notify option and select this device as the target (much easier) It's shown in the first GIF up above - in that I use a message ID but it will work without - just type something and click run to test. Without a message ID you'll only have the option to remove ALL messages using the 'clear' command:

Light dawns on Marblehead.

Thank you, once again.

@johnwill1

I managed to get things up and running using both cycle and scrolling. I used both msgID and notify (as you stated - way slicker). This leads me to have another question. I had envisioned that as soon as msg3 was completed, msg1 would re-appear. Is the 20 second break before restart the expected behavior? If yes, is there anything I might tweak in the driver code to reduce it? Inquiring minds want to know.

scrolling

Thanks yet again.