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.

1 Like

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.

This is a very interesting discussion, so I tried to make this example working.
I made the widget and the string item Rule_ExpireModify, i see my timer and can change the value’s.

The i made the rule with the following code:

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
      script: >+
        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
        }  finally {
              logger.warn(newExpireStr + " finaly state rule");
           }

    type: script.ScriptAction

If the rule is triggerd i get the following error:
2022-10-25 22:44:49.482 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID ‘1519412e0e’ failed: ReferenceError: “log” is not defined in at line number 1

How can I make this code working? My programming skils are not so high :roll_eyes:

You are using the old version of javascript that comes pre-installed with OH. The code that you copied requires the a newer version and the helper libraries that come with it. You’ll need to go to the settings page and under Addons click on Automation there you can search for and install the JSScripting Addon. This will add a new option to your rule scripts that looks like this

image

You will need to delete the script you have already created, and add a new script using the newer javascript version and then paste the code into that.

Thanks, I installed the add-on and copied your original code. It works! :slightly_smiling_face:
If I tried also the code from Rich I see some programming errors.

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');

Error in the log
2022-10-26 15:59:08.293 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID ‘1519412e0e’ failed: org.graalvm.polyglot.PolyglotException: TypeError: (intermediate value).split is not a function

Last question, how can I put the variables in de logfile?
Something like logger.warn(timerItem.toSting) ??

Some debugging wilI help me to find the problem.

The .split() method applies to string variables. The error is saying that it can’t apply this method which presumably means than the variable it’s being used on isn’t a string type. The problem is this line:

let newExpire = [newHours, 'h', newMinutes, 'm', newSeconds, 's, ', metadata.split(',')[1]].join(' ');

When Rich quick typed up his example, he skimmed over defining the variable metadata. You’ll have to add that back in. The code at the top my the old version of the rule can be greatly simplified these days though. So you just need to put the following after the declaration of the timerItem variable:

var MetadataRegistry = osgi.getService('org.openhab.core.items.MetadataRegistry');
var MetadataKey = Java.type('org.openhab.core.items.MetadataKey');
var metadata = MetadataRegistry.get(new MetadataKey("expire",timerItem));

Close. In this case, you don’t need to convert timerItem to anything, it is already a string variable (the state of the item that caused the rule to run is a string, and timerItem has just been defined as one piece of that string, so it is also just a string). So you can just:

logger.info(timerItem)

In ECMAScript 2021, the Item class supports setting Item metadata, but it only works with the value, not the config part of metadata. So timerItem.upsertMetadataValue('expire', newExpire); should work without importing the registry.

So that metadata variable isn’t the registry. I’m looking at the code and trying to figure out exactly what it’s supposed to be.

Ah, I think I have it. It’s what state you want to expire to. To get that you’d either want to replace the metadata variable with a hard coded state, or

let metadata = timerItem.getMetadataValue('expire');

That will get the current expire and the let newExpire line will extract the expire state from that.

} else {
  let newExpire = [newHours, 'h', newMinutes, 'm', newSeconds, 's', ', ON'].join(' ');
  timerItem.upsertMetadataValue('expire', newExpire); // use upsertMetadataValue if you want to create it if it doesn't exist
}

or

} else {
  let metadata = timerItem.getMetadataValue('expire');
  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
}

Of course, if the Item doesn’t already have expire metadata the second one will fail.

I can change the expire value of the timer with the following code,

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: >-
        
        var logger = log('org.openhab.rule.ModifyExpire');

        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]);

        logger.info(newHours +' '+ newMinutes + ' '+ newSeconds)


        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', ', OFF'].join(' ');
          timerItem.upsertMetadataValue('expire', newExpire); 
        }

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

I have one problem left… after updating the timer the widget is reading his new value’s and al the fields H: M: S: are filled with the error code:
TypeError: Cannot read properties of null (reading ‘1’)
TypeError: Cannot read properties of null (reading ‘2’)
TypeError: Cannot read properties of null (reading ‘3’)

In edit mode I can remove this message and fill in my new timer value and save.
The value is correct written in timer problem in the widget is the same.

When I go to my items in de UI, and change the expire timer the widget is showing the new values I entered without errors.

In the logfile I don’t see any error’s or warnings.

I suspect that this problem is with this line in the rule:

There is a space between the single quotes in the join method, but there shouldn’t be. It should be:

let newExpire = [newHours, 'h', newMinutes, 'm', newSeconds, 's', ', OFF'].join('');

It seems that the OH core can properly parse the non-standard expire time code with the extra spaces, but the widget code (which I haven’t had the chance to clean up) isn’t as flexible.

Thanks, now its working without error’s.

Rich, I’ve only just moved over to Openhab v3 (took a week or two migrating DSL rules and learning new bits etc). I’ve bumped in a few new features, one of which is new lighting rules with expire timers, however, I’m sticking with the BasicUI for now, so cant use this widget above. As such, I was thinking I could create say 5x sliders or setpoints to change the minutes value only of my expire timers (so 1 to 60 values).

This widget works great, but I cant use it in the BasicUI. And try as I might (for a good many hours) to understand the JS code with reading forums or trying to do something in Blocky, I’m getting nowhere, bar confused with errors in my logs.

Obviously my items with expires set are On/Off switches, so for each item with an expire, Id need a matching item to use as the slider (as far as I’m aware or would do in DSL rules). To put that in simple terms:

Items

Switch PresenceDetect1
Switch PresenceDetect2
Switch Pre........etc

Number PresenceDetectExpireChanger1
Number PresenceDetectExpireChanger2
Number Prese......etc

Sitemap

Slider item=PresenceDetectExpireChanger1 minValue=1 maxValue=60
Slider item=PresenceDetectExpireChanger2 minValue=1 maxValue=60
Slider item=PresenceDetectExpir......etc

I read your Design Pattern here and many other forum posts, but I cannot seem to figure the way to make the right code that does what I’m asking for. Somewhere I’m getting lost between JS’s use of pulling in/using Item names, and I’ve tried with Blocky to re-create something, but struggling without a metadata option in there, and using the inline code block to affect the metadata of an item.

Do you know of, or would you be kind enough to show me an example of Javascript code, where it takes a value from ITEM-A and updates/changes the metadata value of ITEM-B?

As an addition perhaps I could even store in the Item that the Number Item is going to affect, in a custom metadata Namespace, so that the code can just look up there all the bits it needs… but again, so far this is beyond my understanding.

Thanks in advance if you manage to find time to help me with this.

You cannot access nor change Item metadata in Blockly. There are no blocks to do it.

Similarly you cannot do it in Rules DSL because there is no easy access to the MetadataRegistry.

I’ve not updated that post in a long time and it doesn’t have JS Scripting examples which is what is being used above.

In JS Scripting it’s simple.

var mins = items.getItem('PresenceDetectExpireChanger2').state;
items.getItem('PresenceDetect2').upsertMetadataValue('expire', mins+'m,command=OFF');

It always pays to look at the reference docs first when you want to know how to do something: JavaScript Scripting - Automation | openHAB

1 Like

Thanks!! Ill give that a go later. Very appreciated!