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

jsr223
jython
metadata
Tags: #<Tag:0x00007f51db7b9880> #<Tag:0x00007f51db7b95d8> #<Tag:0x00007f51db7b9498>

(Scott Rushworth) #1

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

How can item metadata be accessed in rules?
How to Setup Jython
Additional key/value attributes field for GenericItem
Roadmap to Happiness - What is missing in the core framework
Next generation design: A Paper UI replacement proposal
(Rich Koshak) #2

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


(Scott Rushworth) #3

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


(Michael Cumming) #4

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


(Michael Cumming) #5

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


Jython: Rule to run when script file is loaded
(Scott Rushworth) #6

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.


(Michael Cumming) #7

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.


(Eyal Cohen) #8

@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

(Michael Cumming) #9

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.