[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.7
/*
 * Scrolling Event Notifier - v1.9.7-stable
 * Based on 1.9.6-stable with Priority Message Style preference
 */

static String version() { return '1.9.7-stable' }

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"

        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())
}

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 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()
            state.messages[id]=content
            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 ?
        "<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="" // Standard
        }
        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

    // Visual padding
    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>"
    }

    // Standard (Cycle Message) mode
    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
        }

        // Multiple messages: start cycling
        state.standardIndex = state.standardIndex ?: 0
        cycleStandardMessage()
        return
    }

    // Scroll Message mode
    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)

    // Scroll width calculation (raw text length, logical separator length)
    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")
}
4 Likes

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?

2 Likes

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

1 Like

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.

1 Like

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.

1 Like

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.