Design Pattern for 'Currently Important' items

I would like to have a list of ‘things that I may care about due to current circumstances/environment’ on my sitemap. These would be things like:

  • Devices with batteries that are running low / or are strugging with connectivity / or some other ‘warning’
  • Doors or windows that have been left open (potentially for a long time)
  • Anything else that is unusual or may warrant my attention (although not necessary urgent)

I have begun to implement this myself using jsr223, by creating an API so that items can be specified as ‘alerted’ and will then show up at the top of my sitemap.

I thought that I’d ask here though as to whether there is an existing pattern that people have adopted to solve this problem? If there isn’t, or if there’s interest regardless, I can document/publish my solution once it’s complete.

1 Like

You could make use of sitemap entries visibility=[] feature; this does not have to be linked to the Item of the widget, but can be an unrelated controlling Item

Text item=BellRinger visibility=[BurlgarAlarm==ON , FireAlarm==ON]

The bell line only appears if either alarm is on

I have lots of stuff written for this but not for the sitemap part. For that I’d probably use what rossko57 suggests and use visibility tag. What I actually do is use subframes and Group aggregation. For example, my door status looks like:

image

which opens up to

So at the top of my sitemap I see a summary and based on that summary I can drill down as necessary.

The sitemap section is:

                Text item=gDoorSensors {
                        Frame label="Front" {
                                Text item=vFrontDoor
                                Text item=vFrontDoor_LastUpdate
                        Switch item=aFrontDoor_Lock label="Front Door" icon="lockclosed" mappings=[ON=Open]  visibility=[aFrontDoor_Lock == ON]
                                Switch item=aFrontDoor_Lock label="Front Door" icon="lockopen"   mappings=[OFF=Lock] visibility=[aFrontDoor_Lock == OFF]
                                Text item=vFrontDoor_Alarm
                                Text item=vFrontDoor_User visibility=[vFrontDoor_User!="0"]
                        }
                        Frame label="Back" {
                                Text item=vBackDoor
                                Text item=vBackDoor_LastUpdate
                                Switch item=aBackDoor_Lock  label="Back Door"  icon="lockclosed" mappings=[ON=Open]  visibility=[aBackDoor_Lock == ON]
                                Switch item=aBackDoor_Lock  label="Back Door"  icon="lockopen"   mappings=[OFF=Lock] visibility=[aBackDoor_Lock == OFF]
                                Text item=vBackDoor_Alarm
                                Text item=vBackDoor_User visibility=[vBackDoor_User != 0]
                        }
                        Frame label="Garage" {
                                Text item=vGarageOpener1
                                Text item=vGarageOpener1_LastUpdate
                                Text item=vGarageOpener2
                    Text item=vGarageOpener2_LastUpdate
                                Text item=vGarageDoor
                                Text item=vGarageDoor_LastUpdate
                        }
                }

And the gDoorSensors Group is defined as

Group:Contact:OR(OPEN,CLOSED) gDoorSensors "The doors are [MAP(en.map):%s]" <door>

I use this overall pattern for my batteries, doors, and Items that monitor the online status of various services that openHAB may care about.

As for the alerting, I have written an alerted approach (API seems too strong a term) using metadata. Actually I have two approaches, depending on what I’m alerting. Here is one of my door Items:

Contact vFrontDoor "Front Door is [MAP(en.map):%s]"
  <door> (gDoorSensors,gDoorCounts,vHydra_Items)
  { channel="mqtt:topic:frontdoor:state",
    name="front door", rem_mins="120" }

As you see, there is a name and rem_mins metadata. The name is basically a way to achieve Design Pattern: Human Readable Names in Messages so my alert messages have reasonable names for what is being alerted on. rem_mins is the number of minutes the door needs to be open before sending the alert message.

Here is the code (Jython). It’s kind of complex because I have two layers of timers, one for antiflapping/debounce (I’ve a couple of sensors that freak out when it gets too cold) and the other a reminder. TimerMgr is a library class that I’ve submitted to the Helper Library.

"""
Rules and functions that keep track of the open/closed states of the doors
and issues an alert if it's left open for too long.

Author: Rich Koshak

Functions:
    - get_mins: Gets the timeout from metadata.
    - reminder_timer: Called when the door has been open for too long.
    - not_flapping: Called after the anit-fapping period.
    - flapping: Called if a door is determined to be flapping.
    - door_changed: Called when a door changes state, sets an anti-flapping
    Timer which in turn sets a reminder Timer.
    - reschedule_door_timers: Called at System started to recreate Timers for
    doors that are open.
"""
from __future__ import division
from core.rules import rule
from core.triggers import when
from core.actions import ScriptExecution
from core.log import log_traceback
from core.metadata import get_value
from org.joda.time import DateTime
from personal.util import get_name, send_alert, send_info
from personal.timer_mgr import TimerMgr

flapping_timers = TimerMgr()
reminder_timers = TimerMgr()

def is_night():
    """Returns True if it is night time."""
    return (items["vTimeOfDay"] == StringType("NIGHT")
                or items["vTimeOfDay"] == StringType("BED"))

@log_traceback
def get_mins(itemName):
    """Returns the rem_mins metadata value or 60 if there isn't one.
    Arguments:
        - itemName: Name of the Item to retrieve the metadata from.
    """
    value = get_value(itemName, "rem_mins")
    return 60 if not value else int(value)

@log_traceback
def sched_reminder(itemName, name, log):
    """Schedules a reminder timer for the given door.

    Arguments:
        - itemName: Item representing the door's state.
        - name: Human friendly name for the door.
        - log: Logger from the triggering Rule.
    """
    mins = get_mins(itemName)
    reminder_timers.check(itemName,
                          int(mins*60*1000),
                          lambda: reminder_timer(itemName, name, mins, log),
                          reschedule=True)

@log_traceback
def reminder_timer(itemName, name, mins, log):
    """
    Called when the reminder timer expires. Issue an alert and if it is night
    time, reschedule the Timer.

    Arguments:
        - itemName: Item representing the door's state.
        - name: Human friendly name for the door.
        - mins: Number of minutes to wait until alerting.
        - log: Logger from the triggering Rule.
    """

    # Door is not open, this should not happen.
    if items[itemName] != OPEN:
        log.warning("{} open timer expired but door is not open!".format(name))
        return

    # Convert mins to a human readable format for the message and send the alert.
    hours = mins // 60
    minutes = mins % 60
    timeStr = "{} hours".format(hours) if hours > 0 else ""
    timeStr = "{} and ".format(timeStr) if hours > 0 and minutes > 0 else timeStr
    timeStr = "{}{} minutes".format(timeStr, minutes) if minutes > 0 else timeStr
    send_alert("{} has been open for {}.".format(name, timeStr), log)

    # Reschedule if it is night.
    if is_night():
        log.info("Rescheduling timer because it is night")
        sched_reminder(itemName, name, log)

@log_traceback
def not_flapping(itemName, origState, name, log):
    """
    Called when we determine the door's sensor is not flapping. Mark the
    time, generate an alert if necessary (no one is home, it's night time), and
    schedule a reminder.

    Arguments:
        - itemName: Name of the door Item that was opened.
        - origState: The state the door originally changed to that caused the
        creation of the anti-flapping timer in the first place.
        - name: Human friendly name for the door.
        - log: Logger for the Rule that created the Timer.
    """
    # If the door isn't still in the origState the sensor was flapping, do
    # nothing.
    if items[itemName] != origState:
        log.warning("{} was flapping, false alarm!".format(name))
        return

    # Update the timestamp
    events.postUpdate(itemName+"_LastUpdate", DateTime.now().toString())

    # Set a reminder timer
    if items[itemName] == OPEN:
        sched_reminder(itemName, name, log)
    elif items[itemName] == CLOSED:
        reminder_timers.cancel(itemName)

    # Create the message to log and maybe alert
    alert = False
    change = "opened" if origState == OPEN else "closed"
    time = ""
    present = ""
    if is_night():
        time = " and it is night"
        alert = True
    if items["vPresent"] == OFF:
        present = " and no one is home"
        alert = True
    msg = "The {} was {}{}{}!".format(name, change, time, present)

    if alert: send_info(msg, log)
    else: log.info(msg)

def flapping(name, state, log):
    """
    Called when the door sensor is found to be flapping.

    Arguments:
        - name: Human firendly name for the door.
        - state: State of the door that kicked off the flaping.
        - log: Logger from the Rule.
    """
    log.warning("{} appears to be flapping, it is now {}!".format(name, state))

@rule("Door reminder",
      description=("Track when doors change state and set an alert if they "
                   "remain open for too long"),
      tags=["entry"])
@when("Member of gDoorSensors changed")
def door_changed(event):
    """Rule triggered when a member of gDoorSensors changes."""

    # We don't care if the door changed from an UnDefType.
    if isinstance(event.oldItemState, UnDefType): return

    name = get_name(event.itemName)
    state = event.itemState

    # We don't care if the door changed to UNDEF, but log a warning.
    if state == UNDEF:
        door_changed.log.warning("{} is in an unkown state!".format(name))
        return

    # If a door changes within a second we treat it as flapping.
    flapping_timers.check(event.itemName,
                          1000,
                          lambda: not_flapping(event.itemName,
                                               state,
                                               name,
                                               door_changed.log),
                          lambda: flapping(name, state, door_changed.log),
                          True)

@rule("Reset timers for OPEN doors",
      description="Reschedules the Timers for doors that are open on restart",
      tags=["entry"])
@when("System started")
def reschedule_door_timers(event):
    """
    Rule triggered at System started to recreate reminder timers for any
    doors that are still open.
    """
    for d in [d for d in ir.getItem("gDoorSensors").members if d.state == OPEN]:
        name = get_name(d.name)
        sched_reminder(d.name, name, reschedule_door_timers.log)

def scriptUnloaded():
    """ Cancel all timers on script unload. """
    global flapping_timers
    global reminder_timers
    flapping_timers.cancel_all()
    reminder_timers.cancel_all()

My entry doors are different because of the antiflapping logic and the fact that I want the alert every time.

My sensor and services online status alerting works a little differently. In this case I don’t need a reminder at some point in the future, I want an alert immediately. And a lot of these are using the Network binding so I’m forced to use Switches.

Sitemap:

image

Items:

Group:Switch:AND(ON, OFF) gSensorStatus "Sensor's Status [MAP(admin.map):%s]"
  <network>

Switch vNetwork_cerberos "cerberos Network [MAP(admin.map):%s]"
  <network> (gSensorStatus, gResetExpire)
  { channel="network:servicedevice:cerberos:online",
    expire="2m",
      name="cerberos" }

// Garage Camera
Switch vCerberos_Camera_Online "Cerberos Camera [MAP(admin.map):%s]"
	<network> (gSensorStatus, gResetExpire)
	{ channel="network:servicedevice:garagecamera:online",
      expire="2m",
      name="Garage Camera" }

// cerberos sensorReporter
Switch vCerberos_SensorReporter_Online "Cerberos sensorReporter [MAP(admin.map):%s]"
    <network> (gSensorStatus, gResetExpire, sensorReporters, DeviceControllers)
    { expire="2m,command=OFF",
      name="cerberos sensorReporter" }

As with above, I use name to provide a nice name for each Item to use in alerts.

The Rules, as with the entry Rules, have some antiflapping logic as well.

"""
Rules to keep track of whether or not a device has gone offline or not and
generate an alert message when it goes offline or return online.

Author: Rich Koshak

Functions:
    - alert_timer_expired: Called when a device changes state and stays that way
    for enough time that it's not flapping.
    - alert_timer_flapping: Called when a device changes state too rapidly
    indicating it's flapping.
    - status_alert: Rule called when a sensor changes state and sends an alert
    if necessary.
    - status_reminder: Rule triggered at 8am every morning to issue a report
    with all the known offline devices.
    - pm_online: Called when the Zwave power meter Thing changes state.
    - heartbeat: Called when a member of SensorEvents receives an update
    indicating the device is online.
"""
from threading import Timer
from core.rules import rule
from core.triggers import when
from core.metadata import get_key_value, set_metadata
from core.actions import Transformation
from core.log import log_traceback
from personal.util import send_info, get_name
from personal.timer_mgr import TimerMgr

timers = TimerMgr()

@log_traceback
def alert_timer_expired(itemName, name, origState, log):
    """
    Called when we determine that a sensor's online state is not flapping.

    Arguments:
        - itemName: Name of the sensor's Item.
        - name: Human friendly name of the sensor.
        - origState: The state that originally triggered the Timer to check for
        flapping.
        - log: Logger from the triggering Rule.
    """
    on_off_map = { ON: 'online', OFF: 'offline' }
    alerted = get_key_value(itemName, "Alert", "alerted") or "OFF"

    if items[itemName] != origState:
        log.warning("In alert_timer_expired and {}'s current state of {} is "
                    "different from it's original state of {}."
                    .format(name, items[itemName], origState))

    # If our alerted flag equals the Item's state we need to generate an alert
    if str(items[itemName]) == alerted:
        send_info("{} is now {}".format(name, on_off_map[items[itemName]]), log)
        set_metadata(itemName,
                     "Alert",
                     { "alerted": 'OFF' if alerted == 'ON'else 'ON' },
                     overwrite=False)
    else:
        log.warning("Alert timer expired but curr state doesn't match alert {} "
                    "!= {}".format(name, items[itemName], alerted))

def alert_timer_flapping(itemName, name, log):
    """
    Called when a sensor's online state appears to be flapping.
    Arguments:
        - itemName: Name of the sensor Item.
        - name: Human friendly name of the sensor.
        - log: Logger from the triggering Rule.
    """

    alerted = get_key_value(itemName, "Alert", "alerted") or "OFF"
    log.warning("{} is flapping! Alerted = {} and current state = {}"
                .format(name, alerted, items[itemName]))

@rule("Device online/offline",
      description="A device we track it's online/offline status changed state",
      tags=["admin"])
@when("Member of gSensorStatus changed")
def status_alert(event):
    """
    Triggered when a member of gSensorStatus changes. We don't care if the
    sensor changed from a UnDefType. Set a Timer to see if the device is
    flapping.
    """

    name = get_name(event.itemName)

    if isinstance(event.oldItemState, UnDefType):
        status_alert.log.warning("{} is in an undef type, canceling any running "
                                 "timers".format(name))
        timers.cancel(event.itemName)
        return

    timers.check(event.itemName,
                 60000,
                 lambda: alert_timer_expired(event.itemName,
                                             name,
                                             event.itemState,
                                             status_alert.log),
                lambda: alert_timer_flapping(event.itemName,
                                             name,
                                             status_alert.log),
                reschedule=True)

@rule("System status reminder",
      description=("Send a message with a list of offline sensors at 08:00 and "
                   "System start"),
      tags=["admin"])
@when("Time cron 0 0 8 * * ?")
@when("System started")
def status_reminder(event):
    """
    Called at system start and at 8 AM and generates a report of the known
    offline sensors
    """

    #numNull = len(filter(lambda item: isinstance(item.state, UnDefType), ir.getItem("gSensorStatus").members))
    numNull = len([i for i in ir.getItem("gSensorStatus").members
                   if isinstance(i.state, UnDefType)])
    if numNull > 0:
        status_reminder.log.warning("There are {} sensors in an unknown state!"
                                    .format(numNull))

    #offline = filter(lambda item: item.state == OFF, ir.getItem("gSensorStatus").members)
    offline = [i for i in ir.getItem("gSensorStatus").members if i.state == OFF]
    offline.sort()
    if len(offline) == 0:
        status_reminder.log.info("All sensors are online")
        return

    #offlineMessage = ("The following sensors are known to be offline: {}".format(", ".join(map(lambda sensor: "{}".format(get_name(sensor.name)), sorted(sensor for sensor in offline))))
    offline_str = ", ".join(["{}".format(get_name(s.name)) for s in offline ])
    offline_message = ("The following sensors are known to be offline: {}"
                       .format(offline_str))

    for sensor in offline:
        set_metadata(sensor.name, "Alert", { "alerted" : "ON"}, overwrite=False)
    send_info(offline_message, status_reminder.log)

@rule("Power Meter online status",
      description="Zwave has marked the power meter as offline",
      tags=["admin"])
@when("Thing zwave:device:dongle:node7 changed")
def pm_online(event):
    """Called when the power meter Thing changes it's state."""

    pm_online.log.info("Power meter Thing changed status {}"
                       .format(event.statusInfo.status))
    if str(event.statusInfo.status) == "ONLINE":
        events.sendCommand("vPowerMeter_Online", "ON")
    else:
        events.sendCommand("vPowerMeter_Online", "OFF")

@rule("Process heartbeat",
      description=("Process an uptime heartbeat message to ping the online "
                   "status of a sensor"),
      tags=["admin"])
@when("Member of SensorEvents received update")
def heartbeat(event):
    """Called when a member of SensorEvents receives an update and commands that
    sensor's Online Item to reset the expire binding timer.
    """
    events.sendCommand(event.itemName.replace("Uptime", "Online"), "ON")

def scriptUnloaded():
    """ Called at script unload, cancel all the latent timers. """

    global timers
    timers.cancel_all()

In these Rules I use metadata to keep track of when I’ve alerted that a device went offline. Only if I’ve actually alerted that a device went offline will I alert when the device comes back online. At 8 AM I also generate an alert with a list of all the sensors that are known to be offline.

Honestly, both of these two sets of Rules are probably more complex than they need to be and I’m not 100% sure this last one is working properly (I get some offline alerts but not the online alert sometimes). It is on my list to see how to harmonize these two sets of Rules and create a submission to the Helper Library. These were among the first Rules I migrated from my Rules DSL and they need to be rewritten at some point. I think I can/should extract the antiflapping into it’s own library and I think I can probably merge a goo portion of the alerting code into it’s own. The creation of TimerMgr was my first attempt at that.

So I don’t really have a library or a DP yet for this but it’s on my list.

Code to turn into library:
- generic is alive/ offline alerting (not sure if this is two separate libraries or not)
- manual trigger detection
- sensor aggregation?
- cycle states https://community.openhab.org/t/solved-same-when-different-then/81399/18
- sequence detection (e.g. keypad entries)

In work:
- Human readable names (using metadata, implemented)

Done:
- one timer per Item
- gatekeeper
- hysteresis
- generic presence detection
- countdown timer
- rate limit
- eventbus
- expire binding
- deferred command
- time of day
- item init
- cascading timers (use Gatekeeper instead of a separate implementation)

Implemented by someone else:
- state machine

Skip for now:
- motion sensor timer

I look forward to what you come up with.

Wow, thanks for the detailed reply, loads of info there!

It seems that your approach is less about a generic ‘alerted item’ and more about something that needs your attention based on the context of what the actual item is. This is probably a better approach, although requires a little more work & planning overall.

After getting through a little of my implementation, I realised that the simplest way for me to actually do this (using a more generic approach) would be:

  • Create a group, maybe gAlerted
  • Add this group to the top of the sitemap
  • Create a script API to allow items to be set to alerted of not…
  • …which just adds or removes them from the gAlerted group

But I do think this will result in an inferior solution to yours. Whatever way I go (most likely more similar to yours I now think), if there’s anything useful I create (an antiflapping lib is a likelihood) I’ll be sure to post it.