[JSR223-Jython] Read/add/remove Item metadata in rules

In the Rules DSL, I used HashMaps, but now in JSR223, I use dictionaries to store data for particular Items. I use this data for setting the light levels based on the current time of day and the outdoor lux level.

areaLightLevels = {
    "US_DiningRoom_Dimmer" : {
        "Morning"    : {"Low_Lux_Trigger" : 20, "Level" : 30},
        "Day"        : {"Low_Lux_Trigger" : 90, "Level" : 30},
        "Evening"    : {"Low_Lux_Trigger" : 90, "Level" : 30},
        "Night"      : {"Low_Lux_Trigger" : 90, "Level" : 30},
        "Late"       : {"Low_Lux_Trigger" : 90, "Level" : 1}
    },

Using the openhab2-Jython modules, you can access the MetadataRegistry. This lets you read, add and remove metadata from Items. Eventually, metadata may become editable in a UI, but I may create a HABpanel widget for this too. Here is an example script to show you how it’s done:

from org.slf4j import Logger, LoggerFactory
log = LoggerFactory.getLogger("org.eclipse.smarthome.model.script.Rules")

from openhab import osgi
from org.eclipse.smarthome.core.items import Metadata
from org.eclipse.smarthome.core.items import MetadataKey
MetadataRegistry = osgi.get_service("org.eclipse.smarthome.core.items.MetadataRegistry")

# read metadata
testReadMetadata = MetadataRegistry.get(MetadataKey("Test1","Virtual_Switch_1"))# returns None if namespace does not exist
if hasattr(testReadMetadata, 'configuration'):# just checking to be safe
    log.debug("JSR223: testReadMetadata.configuration=[{}], now removing it".format(testReadMetadata.configuration))
    # remove metadata
    MetadataRegistry.remove(MetadataKey("Test1","Virtual_Switch_1"))
else:
    log.debug("JSR223: namespace \"Test1\" does not exist, so adding it")
    # add metadata
    MetadataRegistry.add(Metadata(MetadataKey("Test1","Virtual_Switch_1"), "TestValue", {"TestConfig1":5, "TestConfig2":55}))

# read metadata
testReadMetadata = MetadataRegistry.get(MetadataKey("Test1","Virtual_Switch_1"))# returns None if namespace does not exist
if hasattr(testReadMetadata, 'configuration'):# just checking to be safe
    log.debug("JSR223: testReadMetadata.configuration=[{}], now removing it".format(testReadMetadata.configuration))
    # remove metadata
    MetadataRegistry.remove(MetadataKey("Test1","Virtual_Switch_1"))
else:
    log.debug("JSR223: namespace \"Test1\" does not exist, so adding it")
    # add metadata
    MetadataRegistry.add(Metadata(MetadataKey("Test1","Virtual_Switch_1"), "TestValue", {"TestConfig1":5, "TestConfig2":55}))

test = MetadataRegistry.get(MetadataKey("Test","Virtual_Switch_1"))# returns None if namespace does not exist
lightLevels = MetadataRegistry.get(MetadataKey("Morning","Virtual_Switch_1"))

if hasattr(lightLevels, 'configuration'):
    log.debug("JSR223: lightLevels.configuration=[{}]".format(lightLevels.configuration))
    log.debug("JSR223: lightLevels.configuration[\"Trigger\"]=[{}], lightLevels.configuration[\"Level\"]=[{}]".format(lightLevels.configuration["Trigger"], lightLevels.configuration["Level"]))

You can set metadata in your managed Items by doing this…

Switch      Virtual_Switch_1        "Virtual Switch 1 [%s]"         <switch>      (gVirtual,gTest)      ["Switchable"]      { Morning="LightLevels"[Trigger=1, Level=10], Day="LightLevels"[Trigger=2, Level=20], Evening="LightLevels"[Trigger=3, Level=30], Night="LightLevels"[Trigger=4, Level=40], Late="LightLevels"[Trigger=5, Level=50] }

… but if you do, you can’t modify it. You can still add metadata and then remove it though. To populate and verify my metadata, I used this script, which pulled the data from a dictionary like the above example…

# imports from above
for lightLevel in areaLightLevels:
    for mode in areaLightLevels[lightLevel]:
        if lightLevel != "Default":
            log.debug("JSR223: lightLevel=[{}], mode=[{}], Low_Lux_Trigger=[{}], Level=[{}]".format(lightLevel, mode, areaLightLevels[lightLevel][mode]["Low_Lux_Trigger"], areaLightLevels[lightLevel][mode]["Level"]))
            MetadataRegistry.add(Metadata(MetadataKey(mode,lightLevel), "LightLevels", {"Low_Lux_Trigger":areaLightLevels[lightLevel][mode]["Low_Lux_Trigger"], "Level":areaLightLevels[lightLevel][mode]["Level"]}))
            testReadMetadata = MetadataRegistry.get(MetadataKey(mode,lightLevel))
            log.debug("JSR223: testReadMetadata.configuration=[{}]".format(testReadMetadata.configuration))
5 Likes

Can I adds metadata and channel links or binding configs on the same item?

Yes. Which looks odd, since there are two sets of {} when manually configuring them and metadata.

Scott, I have started utilizing metadata in my jython scripts - its working great. Much better than using globals and solves problems with persistence for non item information.

Mike

1 Like

I put together a class to assist with read/writing of metadata. Still working on some details but starting out with the code below.

from org.eclipse.smarthome.core.items import Metadata
from org.eclipse.smarthome.core.items import MetadataKey
from openhab import osgi
MetadataRegistry = osgi.get_service("org.eclipse.smarthome.core.items.MetadataRegistry")


class Metadata:
    def __init__(self,item_name,namespace):
        self.item_name = item_name
        self.namespace = namespace

    def __str__(self):
        return 'Item: {}, Namespace = {}, Value {}, Configuration {}'.format(self.item_name,self.namespace,self.get_value(),self.get_configuration())

    def does_namespace_exist(self):
        if self.read_metadata() is not None:
            return True
        else:
            return False

    def read_metadata(self):
        return MetadataRegistry.get(MetadataKey(self.namespace,self.item_name)) # returns None if namespace does not exist

    def get_value(self):
        metadata = self.read_metadata()
        return metadata and str(metadata.value) or None

    def get_configuration(self):
        metadata=self.read_metadata() 
        md_configuration = hasattr(metadata, 'configuration') and metadata.configuration or {}

        configuration = {} # process any configuration values here
        for key,value in md_configuration.iteritems():
            configuration [str(key)] = str(value)

        return configuration

    def get_configuration_value_for_key(self,key):
        configuration = self.get_configuration()
        return configuration.has_key(key) and configuration [key] or None 

    def write(self,value='',configuration={}):
        MetadataRegistry.add(Metadata(MetadataKey(self.namespace,self.item_name),str(value),configuration))
    

3 Likes

Excellent! I’m thinking a services.py or interfaces.py might be a good place for it? Then we could have the MetadataRegistry, RuleEngine, LocationProvider, AudioManager, etc. all in one place. Your class looks good, but I think I would rename the read_metadata() method to just read() to stay consistent with write(). I’ll try it out tonight.

Do you what data types are allowed for metadata? I tend to have to do a lot of conversions, which wouldn’t be needed if the metadata could store DecimalType, PercentType, etc. If you don’t know, I’ll explore this tonight as well. I think it may all be stored as primitives.

Code definitely needs some iterating. In terms of reading configuration or value data I am just converting everything to string. Since I am only accessing the data from Jython and I know what the type it should be, I convert it to a number if required. This might be short sighted though - but not sure. In the future they maybe a use case where other bindings are access the same data and we need respect the different java types.

@mjcumming

I am planing to use your class. I have some questions:

  1. I saw that the REST API return config and not configuration (2.4.0-1)
  2. Do u have more updated class?
  3. I fail to import osgi, any idea why (probably missing module that I need to look for)?

2018-12-30 21:41:45.572 [ERROR] [ipt.internal.ScriptEngineManagerImpl] - Error during evaluation of script 'file:/etc/openhab2/automation/jsr223/core/001_initialize_items.py': ImportError: No module named openhab in &lt;script&gt; at line number 5

Thx!

[EDITED]

Answers I found:

  1. Should be configuration (although I see config in REST API)
  2. from core import osgi

hi,

few minor changes are here https://github.com/OH-Jython-Scripters/openhab2-jython/pull/61/commits/2f0e7558f6e8ac113d6825bf86e9745779288493

comments welcome. if @5iver accepts, we can merge this and have it as a common module.

I know I an test this right now but if someone knows off the top of their head it would save me a lot of time.

What if I have a situation where I have a namespace with some keys that are static (e.g. defined in .items file) and other keys in the same namespace that I want to set in Rules.

The specific use case I’m exploring is transferring my offline alerting rule that sends me an alert when one of my sensors or services goes offline. In the alert message I currently use a MAP transform to convert the Item name to a friendly name. This would be the static key. Whether or not I’ve already alerted that it’s offline (I don’t want repeats of the alert if OH reboots while a device is offline) would be the dynamic key.

Can I put these two keys into the same namespace or do they need to be in separate namespaces (or build some Rule to populate them the first time)?

I don’t think you can, because the configuration dictionary of the namespace would then differ from what’s in the item file. OH can write back to the items file, so I think it treats the entire namespace as read-only, not just the keys that are in it. Though I don’t think we tested this specific use case.

I’ve tested creating md from an .items file and changing it in a script and IIRC it can be changed and is reset after restart. Rich, the metadata libraries have been added for both Jython and JS, so better doc in the repo.

I found the library but not docs. Are there docs beyond https://openhab-scripters.github.io/openhab-helper-libraries/Guides/But%20How%20Do%20I.html#read-add-remove-item-metadata? The docs at that link just points to this thread in the forum. I had to hunt around a bit to actually find whether Michael C’s class was what I should have been using or if it was added to the core (I eventually found it in the core).

I’ve opened an Issue already to update the published docs.

The docs are partially built from docstrings, so check https://openhab-scripters.github.io/openhab-helper-libraries/Python/Core/Packages%20and%20Modules/metadata.html. I suggest using the module for metadata. But How Do I is still a valid example, but should also point to the module.

The How Do I is a valid example, but the How Do I should be the canonical example. That should be an example that uses the library right?

At first I was thinking both, but now I see the light. The doc should show usage of the module. If someone wants to use the interface directly, they can look at the code in the module or Java.