[semantic,ui] Using enriched semantic to ease UI creation

Hi Folks,
I’d like to share the idea of improving custom widget configuration by leveraging yet-existing information which can be derived from the semantics model.

Currently, this is not yet a turn-key solution but more a proof of concept to be discussed and improved. So yes: feel invited to adopt and suggest improvements … :wink:

Let’s start…

The Problem

In a setup, we may have multiple equipment with similar points. So all battery-powered room sensors may have channels for

  • temperature
  • humidity
  • battery low

When linking them to items, what *label* should we use?

  • “Temperature Kitchen Ground Floor”
  • “Humidity Kitchen Ground Floor”
  • “Battery Low Kitchen Ground Floor”

or simply:

  • “Temperature”
  • “Humidity”
  • “Battery Low”

With openHAB3 the latter would be sufficent since we have the semantic model.
We put the relevant items into the hierarchy. So from the model’s perspective it’s clear to what equipment in which room a temperature reading belongs to.

But what if we query “all temperatures” throught the models’s temperature property? We’ll get something like that:

Wouldn’t it be nice to leverage the model’s semantic relations here too?
Shouldn’t we be able to show the room and the equipment where these readings came from in our custom widgets?

Answer: Not out of the box (yet).

Solution idea

We already have all the needed information, the association to a equipment and the position in a location in the semantics metadata:

  • isPointOf
  • hasLocation

Unfortunately, in custom widgets we can’t follow these “pointers” to derive information from the parent groups.

But in rules: We can!
So why not collecting the label information from the parent groups (equipment and location) and simply store them as config date in an additional metadata namespace?

Let us start…

Example

We want to create a UI widget which shows us empty batteries which shoul be replaced. Since all items tagged with LowBattery have the same label, we like to add the Information to our widget where these batteries belong to.

As said above, we introduce a custom namespace “uiSemantics” and put that information there:

"metadata": {
    "uiSemantics": {
      "config": {
        "preposition": " im ",
        "equipment": "Bewegungsmelder",
        "location": "Büro"
      }
    },
    "semantics": {
      "value": "Point_Status_LowBattery",
      "config": {
        "relatesTo": "Property_Energy",
        "isPointOf": "ZIGTIO1PhilipsSML001"
      }

As you see, I even opted for putting the correct German preposition ("im" vs. "in der") to form correct expressions later in the UI like:

"Bewegungsmelder im Büro"
"Kühlschrank in der Küche"

Since we don’t want to add all those metadata by hand, I’ve created a tiny script to derive the needed parent labels and put them into “uiSemantics”. This script may run daily (with adopted to MetadataRegistry.update… :wink: )

var UI_NAMESPACE = "uiSemantics";    
var enrichMetadata = function(item) {
      logger.info("Item enriched: "+item)
      var prepositionFor = ["Küche", "Werkstatt", "Waschküche", "Bibliothek", "Garage"]
      var uiSemanticsKeys = { "equipment" : "" , "location" : "", "prepositon" : ""}
      var equipmentItem=ir.getItem(getValue(item, "semantics", "isPointOf"));
      var locationItem=ir.getItem(getValue(equipmentItem.name, "semantics", "hasLocation"));
  
  uiSemanticsKeys.equipment = equipmentItem.label;
  uiSemanticsKeys.location = locationItem.label;
  uiSemanticsKeys.preposition = (prepositionFor.indexOf( uiSemanticsKeys.location) > -1 ) ? " in der " : " im ";
  
  logger.info( "Item: "+ item +" Equipment: "+ uiSemanticsKeys.equipment  + uiSemanticsKeys.preposition + uiSemanticsKeys.location );
  MetadataRegistry.add(new Metadata(new MetadataKey( UI_NAMESPACE, item), null , uiSemanticsKeys ));
  return null;
}

logger.info("Starting: Collecting semantics ...")
var lowBattItems = ir.getItem("gLowBattery").getMembers();

  // Check the metadata for all the relevant Items
  for each (var item in lowBattItems) {
    enrichMetadata(item.name);
  }

Now that we have have the labels of the parents in the metadata, we can simply use them e.g. in the beloved oh-repeater (A couple of simple oh-repeater examples):

- component: oh-repeater
      config:
        for: i
        sourceType: itemsWithTags
        itemTags: LowBattery
        fetchMetadata: semantics,widgetOrder,uiSemantics
        filter: loop.i.state != "OFF"
      slots:
        default:
          - component: oh-label-card
            config:
              icon: f7:battery_25
              title: =loop.i.label
              footer: =loop.i.metadata.uiSemantics.config.equipment + loop.i.metadata.uiSemantics.config.preposition + loop.i.metadata.uiSemantics.config.location
              item: =loop.i.name

This snippet iterates through all items tagged with LowBattery if the state not “OFF”. Notice the fetchMetadata line: There we query our label information to be used in the footer below.

This gives:
semanticsUI

As I said: The equipment information as well as the location is available in the model and accessible from the rules - but currently simply not accessible from within widget code eg. via

// NOT WORKING!!!
= items[loop.i.metadata.semantics.config.isPointOf].label

Therefore I came up with this concept … :wink:

6 Likes

Clever :slight_smile:
And when I get around to implementing my idea here: [MainUI] See and edit Item metadata on the Item page · Issue #397 · openhab/openhab-webui · GitHub you’ll be able to describe your own metadata namespaces in the developer tools, so they will appear in the available namespaces in the UI (you won’t have to type it) and you’ll get a nice config sheet like the widget props to set values.

3 Likes

BTW: Is there another way in widgets than just oh-repeater 's fetchMetadata to access item’s metadata?

If so, this could be leveraged to derive the presentation of an item (e.g. choose a matching icon based on tags)…

hmm sounds very interesting but I can’t get the script to work…
I’ve placed it in the script folder and named it *.script
Did you add the script via the UI?

Crreate a rule of type javascript …

Run it once or periodically :wink:

hmm already tried that but it throws a lot of errors:

Script execution of rule with UID '6244517efe' failed: ReferenceError: "logger" is not defined in <eval> at line number 18

and if I remove the logger part another error etc.

I think I miss something else

This is what I use, ofc you need to adjust the parts you are after …

var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.Experiments");

var FrameworkUtil = Java.type("org.osgi.framework.FrameworkUtil");
var _bundle = FrameworkUtil.getBundle(scriptExtension.class);
var bundle_context = _bundle.getBundleContext()
var MetadataRegistry_Ref = bundle_context.getServiceReference("org.openhab.core.items.MetadataRegistry");
var MetadataRegistry = bundle_context.getService(MetadataRegistry_Ref);
var MetadataKey = Java.type("org.openhab.core.items.MetadataKey");
var Metadata = Java.type("org.openhab.core.items.Metadata");

function getMetadata(itemName, namespace) {
  return MetadataRegistry.get(new MetadataKey(namespace, itemName));
}

function enrichMetadata(item) {
  logger.info("Item enriched: " + item)

  var semantics = getMetadata(item, "semantics");
  var isPointOf = semantics.configuration["isPointOf"]
  var isPointOfItem = ir.getItem(isPointOf)
  
  var uiSemantics = { equipment: { label: isPointOfItem.getLabel() } };
  
  logger.info("Item: "+ item + " uiSemantics = " + uiSemantics.equipment.label);
  var key = new MetadataKey("uiSemantics", item);
  var metadata = new Metadata(key, null , uiSemantics)
  if (MetadataRegistry.get(key)) {
    MetadataRegistry.update(metadata);
  } else {
    MetadataRegistry.add(metadata);    
  }
}

function enrichMetadataForGroup(group) {
  var items = ir.getItem(group).getMembers();
  for each (var item in items) {
    enrichMetadata(item.name);
  }
}

logger.info("Starting: Collecting semantics ...")

enrichMetadataForGroup("gFF_Lights");
enrichMetadataForGroup("gGF_Lights");
enrichMetadataForGroup("gBasement_Lights");
enrichMetadataForGroup("gOutdoor_Lights");

hmm ok thx but still same error

logger" is not defined

Miss-formatted my post so first line was missing, sorry :slight_smile:

Fixed.

Nice thx that was the solution

1 Like

Hi mates,
I’m happy that you gave the concept a try :slight_smile:
I’ve just evolved the above a tiny bit even more …

Some of you may have seen my oh-repeater stuff for timestamps here:

To not spread related content via multiple threads, I’d like to share it here:

Dynamic warning timeouts

In the thread linked above, I’ve created a list which hilights outstanding heartbeats from wireles sensors:

The initial attempt used a fixed timeout (1h) for warnings and another (1day) for errors.

This leads to some false negativ presentation:
While most wireless sensors report something every some seconds/minutes, I do have some sensors too which need quite very long periods of deep sleep to reach an acceptable battery lifetime:

  • Xiaomi Lumi Zigbee temp/hum/pressure sensors
  • Shelly Flood water sensors
  • Shelly Door Window sensors

See them in the above picture shown with orange or red badges…

To overcome this limitation, I’ve extended my uiSemantics namespace by additional keys for warning/failure timeouts:

  "metadata": {
    "uiSemantics": {
      "config": {
        "warn": -1440,
        "fail": -2160,
        "icon": "f7:waveform_path_ecg",
        "preposition": " im ",
        "equipment": "Wassermelder",
        "location": "Technikraum"
      }

Notice the warn/fail keys here. There I’ve added the specific timeout in minutes for that item.

Sure, you’ll certainly ask for the negative values there - right?
Answer: in the dayjs() calculation in the widget I was using negative offsets. So putting negatives here already saved me some rewrite in my widgets. :wink:

As in the OP, I don’t populate the key’s by hand. It’s again in a supporting script:

var enrichMetadata = function(item, icon) {
  
  logger.info("Item tested: "+item)
  var prepositionFor = ["Küche", "Werkstatt", "Waschküche", "Bibliothek", "Garage"]
  var uiSemanticsKeys = { "equipment" : "" , "location" : "", "prepositon" : "" , "icon" : "" , "warn" : -30, "fail" : -45}
  var isPointOf = getValue(item, "semantics", "isPointOf");
  logger.info("Item isPointOf: "+ isPointOf );
  
  var equipmentItem = isPointOf === undefined ? ir.getItem(item) : ir.getItem( isPointOf ) ;
  var locationItem=ir.getItem(getValue(equipmentItem.name, "semantics", "hasLocation"));
  
  uiSemanticsKeys.equipment = equipmentItem.label;
  uiSemanticsKeys.location = locationItem.label;
  uiSemanticsKeys.preposition = (prepositionFor.indexOf( uiSemanticsKeys.location) > -1 ) ? " in der " : " im ";
  uiSemanticsKeys.icon = icon;
  
  var warnTimeoutFor = {"LUMITH" : -720 , "SHELLYDW" : -240 , "SHELLYFLOOD" : -1440 };
  for (var s in warnTimeoutFor) {
    if (item.toUpperCase().indexOf(s) > -1 ) { 
      uiSemanticsKeys.warn = warnTimeoutFor[s];
      break;
    }
  }
  uiSemanticsKeys.fail= uiSemanticsKeys.warn * 1.5;
  
  if ( getValue(item, UI_NAMESPACE, "location" ) !== null ) {
        logger.info( "Item: "+ item +" UPDATE Metadata in "+ UI_NAMESPACE + ": " + uiSemanticsKeys.equipment  + uiSemanticsKeys.preposition + uiSemanticsKeys.location );
        MetadataRegistry.update(new Metadata(new MetadataKey( UI_NAMESPACE, item), null , uiSemanticsKeys ));
  } else {
        logger.info( "Item: "+ item +" ADD Metadata in "+ UI_NAMESPACE + ": " + uiSemanticsKeys.equipment  + uiSemanticsKeys.preposition + uiSemanticsKeys.location );
        MetadataRegistry.add(new Metadata(new MetadataKey( UI_NAMESPACE, item), null , uiSemanticsKeys ));
  }
  return null;
}

The scripts makes some assumption on the item name to filter the long-sleeping deivices (items). I did this because I didn’t found a way to determine, what thing type an item is linked to from within a rule.
IF SOMEONE HAS GOT AN IDEA ON THIS, please let me know :wink:

Once we have the additional timeout set in our metadata, we can change the oh-repeater loop to use the specific timeouts instead of the fixed for filtering and coloring:

  - component: oh-repeater
    config:
      for: i
      sourceType: itemsInGroup
      groupItem: =props.mainItem
      fetchMetadata: semantics,widgetOrder,uiSemantics
      filter: '( loop.i.state < dayjs().add(loop.i.metadata.uiSemantics.config.warn,"m").format() ) ? true : vars.detailsOn '
      style:
        highlightColor: blue
    slots:
      default:
        - component: oh-list-item
          config:
            icon: ="f7:"+props.icon
            iconColor: '=dayjs(items[loop.i.name].state).isAfter(dayjs().add(loop.i.metadata.uiSemantics.config.warn,"m")) ? "green" : dayjs(items[loop.i.name].state).isAfter(dayjs().add(loop.i.metadata.uiSemantics.config.fail,"m")) ? "orange" : "red"'
            footer: =loop.i.metadata.uiSemantics.config.preposition + loop.i.metadata.uiSemantics.config.location
            title: =loop.i.metadata.uiSemantics.config.equipment
            item: =loop.i.name
            badge: =dayjs(items[loop.i.name].state).fromNow()
            badgeColor: '=dayjs(items[loop.i.name].state).isAfter(dayjs().add(loop.i.metadata.uiSemantics.config.warn,"m")) ? "green" : dayjs(items[loop.i.name].state).isAfter(dayjs().add(loop.i.metadata.uiSemantics.config.fail,"m")) ? "orange" : "red"'

This brings us:

Notice the Shelly Flood “Wassermelder im Technikraum”. This guy was marked orange formerly. BUT: Since it is absolutely fine, if it sleeps for a whole day if no water alarm is to be issued, the widget should not complain about it…

Have fun … :wink:

1 Like

thx for all the infos.
Now I have a question to your model:
You’re model must look something like this regarding the Feuchtigkeit example:
image
So each SensorType (e.g. Feuchtigkeit, Temperatur) has its own group even though they might come from the same Device/Sensor that measures both values, is this correct?

I ask because currently I have combined them in one Sensor like this:
image
And therefore your script gives me wired results.

That’s like my model is organized:

I see, that you have a kind of “sub-equipment” for battery inside of “Raumfühler”.
If this is how you really want it to be, you certainly have to adopt the script to gather the information (e.g. the location) not just from the direct parent but form the parents parent in case of your equipment-in-equipment. :wink:

I’m still figuring out what the best way is but sure I will change it.

Hmm strange you’re model looks very similar to mine but still I gate strange results.
So in the script when you call the function “enrichMetadata” would get you “Raumfühler Büro im Büro” or not?

Yes. This deserves some cleanup - indeed! :wink:

ok got it.
And for a custom UI Widget that describes the temperature of a sensor like “Temperatur im Büro” you use a different “enrichMetadata” script?

No, not neccessarily. For now, I’ve just apply the metadata to items belonging to certain groups or have certain tags (e.g. LowBattery) attached.

In general, this just makes sense for items appearing in the model. So I had to limit the scope of the script to the items I#m currently sure they have the needed semantic metadata stuff set.

Potentially, you could evolve the script to make it runover all your items and just populate the uiSemantics for items, where the information is available from the parent groups.

Once you did this, feel free to share the result here, so tht others can benefit from it :wink:

One suggestion I would make to your script is to have the option to remove metadata from those Items that have been removed from the model. Managing Item metadata is a little awkward (though will get better with the solution to issue 397) but given that the script can potentially generate a massive amount of metadata, it would be nice if it could clean up after itself too.

Beyond that this is a really nice use of metadata. Thanks for posting!

1 Like

Exactly!
Currently I limit the scope to specific groups of items but already had the need for cleanup in mind … :wink:

Happy to hear that :slight_smile:

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.