Is it possible to make metadata editable in the app?

  • Platform information:
    • Hardware: Raspberry Pi4 4GB
    • OS: Openhabian

I have an expiration timer on an item that I would like to have a widget for in the app if possible, just to make it easier to adjust if needed. I have poked around some and I can’t seem to figure out how to accomplish this.

Yes, it is possible; I have set up a similar system. It is not a trivial task, because it cannot be done just within a widget. You need a widget which uses a dedicated item to trigger and send information to a rule.
Then you can modify metadata, including the expire metadata within that rule. With the expire metadata, there’s the extra slight complication of making the information itself easily editable because it takes the form of 1h30h0s which isn’t very user friendly.

I’ve not cleaned this up for general use yet, but here are the pieces I use.

String Item named Rule_ExpireModify

Widget code:

uid: widget_expire_all
tags:
  - settings
props:
  parameters: []
  parameterGroups: []
component: oh-list-card
config:
  key: =Math.random() + items.Rule_ExpireModify.state
  title: Expire Timers
slots:
  default:
    - component: oh-repeater
      config:
        fetchMetadata: expire
        filter: loop.expireItem.metadata
        for: expireItem
        fragment: true
        itemTags: ","
        sourceType: itemsWithTags
      slots:
        default:
          - component: oh-list-item
            config:
              title: =loop.expireItem.label.replace(' Timer','')
            slots:
              after:
                - component: f7-row
                  slots:
                    default:
                      - component: oh-repeater
                        config:
                          for: timeSeg
                          fragment: true
                          in:
                            - index: 1
                              title: H
                            - index: 2
                              title: M
                            - index: 3
                              title: S
                          sourceType: array
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  padding:
                                    - 5px 0 1px 15px
                                text: "=loop.timeSeg.title + ': '"
                            - component: Label
                              config:
                                style:
                                  padding:
                                    - 5px 0 1px 0
                                text: =loop.expireItem.metadata.expire.value.split(',')[0].match('([0-9]*)h([0-9]*)m([0-9]*)s')[loop.timeSeg.index]
                                visible: =!vars[loop.expireItem.name + "vis"]
                            - component: oh-input
                              config:
                                defaultValue: =loop.expireItem.metadata.expire.value.split(',')[0].match('([0-9]*)h([0-9]*)m([0-9]*)s')[loop.timeSeg.index]
                                inputmode: numeric
                                outline: true
                                style:
                                  padding:
                                    - 0 5px 0 5px
                                  width: 20px
                                type: text
                                variable: =loop.expireItem.name + loop.timeSeg.title
                                visible: =!!vars[loop.expireItem.name + "vis"]
                            - component: Label
                              config:
                                text: Bug Workaround
                                visible: false
                      - component: oh-link
                        config:
                          action: command
                          actionCommand: =loop.expireItem.name + ',' + (vars[loop.expireItem.name + 'H'] || loop.expireItem.metadata.expire.value.split(',')[0].match('([0-9]*)h([0-9]*)m([0-9]*)s')[1]) + ',' + (vars[loop.expireItem.name + 'M'] || loop.expireItem.metadata.expire.value.split(',')[0].match('([0-9]*)h([0-9]*)m([0-9]*)s')[2]) + ',' + (vars[loop.expireItem.name + 'S'] || loop.expireItem.metadata.expire.value.split(',')[0].match('([0-9]*)h([0-9]*)m([0-9]*)s')[3])
                          actionItem: Rule_ExpireModify
                          clearVariable: =loop.expireItem.name + "vis"
                          iconF7: checkmark_circle_fill
                          style:
                            padding:
                              - 3px 0 1px 15px
                          visible: =!!vars[loop.expireItem.name + "vis"]
                      - component: oh-link
                        config:
                          action: variable
                          actionVariable: =loop.expireItem.name + "vis"
                          actionVariableValue: =!vars[loop.expireItem.name + "vis"]
                          iconF7: "=(vars[loop.expireItem.name + 'vis']) ? 'xmark_circle_fill' : 'pencil'"
                          style:
                            padding:
                              - 3px 0 1px 15px

Rule:

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: Rule_ExpireModify
    type: core.ItemStateChangeTrigger
conditions:
  - inputs: {}
    id: "2"
    configuration:
      itemName: Rule_ExpireModify
      state: None
      operator: "!="
    type: core.ItemStateCondition
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        /*

        This rule enacts changes to an item's expire metadata as set by the portal timers widget

        */


        //Set Logger

        var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.ModifyExpire');


        //Access MetadataRegistry

        var FrameworkUtil = Java.type("org.osgi.framework.FrameworkUtil");

        this.ScriptHandler = Java.type("org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler");

        var _bundle = FrameworkUtil.getBundle(ScriptHandler.class);

        var bundle_context = _bundle.getBundleContext()

        var classname = "org.openhab.core.items.MetadataRegistry"

        var MetadataRegistry_Ref = bundle_context.getServiceReference(classname);

        var MetadataRegistry = bundle_context.getService(MetadataRegistry_Ref);

        var Metadata = Java.type("org.openhab.core.items.Metadata");

        var MetadataKey = Java.type("org.openhab.core.items.MetadataKey");


        //Collect all info from expire modify item state and existing metadata

        var modifyInfo = items.getItem(event.itemName).state.split(',');

        var timerName = modifyInfo[0];

        var newHours = parseInt(modifyInfo[1]);

        var newMinutes = parseInt(modifyInfo[2]);

        var newSeconds = parseInt(modifyInfo[3]);

        var metadata = MetadataRegistry.get(new MetadataKey("expire",timerName));


        //Validate new time values and existing metadata

        if (newHours < 0 || 23 < newHours) {
          logger.warn('Hours outside of range: expire metadata not set')
        } else if (newMinutes < 0 || 59 < newMinutes) {
          logger.warn('Minutes outside of range: expire metadata not set')  
        } else if (newSeconds < 0 || 59 < newSeconds) {
          logger.warn('Seconds outside of range: expire metadata not set')
        } else if (metadata === null) {
          logger.warn(['No expire metadata in',event.itemName,': Please configure metadata first.'].join(' '));
        } else {
          
          //Write metadata
          var oldExpire = metadata.value;
          var newExpire = [newHours,'h',newMinutes,'m',newSeconds,'s,',oldExpire.split(',')[1]].join('')
          MetadataRegistry.update(new Metadata(new MetadataKey('expire', timerName), newExpire, metadata.configuration));
          logger.info(['Expire changed from ',oldExpire,'to',newExpire,'for',timerName].join(' '));
        }


        //Cleanup

        items.getItem(event.itemName).postUpdate('None');
    type: script.ScriptAction
1 Like

Out of curiosity, what happens if you fiddle the metadata while an expire period is in progress? I assume nothing, i.e. timing continues with “old” value.

Looks like a good candidate for the Market Place.

But I wonder, since you have to use a rule anyway whether it makes more sense to just implement the expire within the rule(s) using a timer. That gives you more control over the side cases like rossko57 brings up.

Note, in JS Scripting the code would be something like

var logger = log('org.openhab.rule.ModifyExpire');

// Since we are only updating the value we don't need to directly get at the MetadataRegistry

var modifyInfo = items.getItem(event.itemName).state.split(',');

var timerItem = items.getItem(modifyInfo[0]);
var newHours = parseInt(modifyInfo[1]);
var newMinutes = parseInt(modifyInfo[2]);
var newSeconds = parseInt(modifyInfo[3]);

// Validate new time values and existing metadata
if(newHours < 0 || 23 < newHours) {
  logger.warn('Hours outside of range: expire metadata not set');
} else if(newMinutes < 0 || 59 < newMinutes) {
  logger.warn('Minutes outside of range: expire metadata not set');
} else if(newSeconds < 0 || 59 < newSeconds) {
  logger.warn('Minutes outside of range: expire metadata not set');
} else {
  let newExpire = [newHours, 'h', newMinutes, 'm', newSeconds, 's, ', metadata.split(',')[1]].join(' ');
  timerItem.upsertMetadataValue('expire', newExpire); // use upsertMetadataValue if you want to create it if it doesn't exist
}

items.getItem(event.itemName).postUpdate('None');

An alternative, you could use the js-joda Duration (the same would work in Nashorn but you’d have to use java.time.Duration instead). It uses the same overall String structure as Expire except the duration starts with ‘P’. So, if the widget can be modified to return something like 1H2M3S or even better P1H2M3S the rule could become (assuming the ‘P’ can’t be done in the widget):

var logger = log('org.openhab.rule.ModifyExpire');

var modifyInfo = items.getItem(event.itemName).state;
var timerItem = items.getItem(modifyInfo.split(',')[0]);
var newExpireStr = modifyInfo.replace(timerItem+',', ''); // would be better if some other separator than ',' could be used between the timerName and the expire string

try {
  var newExpireDur = Duration.parse('P'+newExpireStr); // if it parses it's valid
  // if you wanted you could test to see if the new value is different from the old by 
  // parsing the existing Duration and calling .equals instead of always updating
  timerItem.upsertMetadataValue(event.itemName, newExpireStr);
  items.getItem(event.itemName, 'None'); // put this here if you don't want to reset a bad value, put it after the catch if you always want to reset it
} catch(DateTimeParseException e) {
  logger.warn(newExpireStr + " is not a valid expire duration.");
}

Obviously I just typed in the above. There might be typos. For the widget, I’d probably just use a text field with an expected format. You can configure the widget to parse the entry for validity before submitting it. Though then you couldn’t use number selectors for each field.

I haven’t done any extensive controlled testing, but from the couple of times I’ve inadvertently changed the metadata while an expire was timing it appears to just cancel the currently running expire timer.

Yeah, it’s on the todo list. As I said, there numerous improvements and tweaks that it needs first, but it’s been a busy summer. I don’t even use this version of the widget that shows all the expire timers it was just the first development version. I use a smaller version that only shows the timings for all of my door and window alarms.

The use of upsert in the code is one area I’d already identified, but before it’s ready for for general distribution I was planning to also add the ability to add new expire metadata to an item that doesn’t have it which is a slightly larger undertaking.

The widget parses metadata and give separate fields for the times which, for me, gives better readability and fairly easy editing, but there is definitely more that can be done for input validation.

There’s also a two step edit process to avoid accidental modifications. You first have to click the edit button:


Then you get the input fields and can accept or cancel any changes you’ve made:

Interesting - feels like editing an Item is causing some kind of “reset”, which may or may not be desirable in any given circumstance. I wonder if there are other side effects.

That’s what upsert does. If the metadata isn’t there it adds it. If it is there it updates it. But I’ve only see the use of upsert in openhab-js so it might be something not part of the MetadataRegistry but implemented in the library so you’d have to move to JS Scripting instead of Nashorn.

I think your post is a good candidate for a feature request/

I’ve did some custom work around this topic: openhab_metadata_edit2.gif

Its a custom UI build which is checking if there is a registered config descriptor with metadata: prefix. The Config forms/framework is being used in several places of UI and with a bit of adjustments it was possible to embed it.

Yeah, upsert is not from the core registry, I think it’s just implemented in the js library. That’s not a huge issue, at this point I don’t have a problem with posting rules that depend on the JSScripting add-on.

The difficulty with novel expire settings is on the widget side not the rules side. The widget’s repeater filters for all the items that already have expire metatdata, so there has to be a completely separate mechanism to access items that don’t yet have expire metadata. I’ve got the framework to do this in other widgets, just haven’t integrated it into this one yet.