Get list of unlinked items

While I’ve got you all here, let me describe how you too could come up with code like this. All the breadcrumbs are there and I’m going to show you how to follow them.

Always with a problem like this we have three steps:

  1. load the data needed
  2. parse/manipulate that data into a form we can work with
  3. do something with the result.

This is no different.

Load the data needed

The first problem is “load the data needed”. In our case part of the data needed is a list of all the Items (or their names) and a list of all the links. The first thing I did was look at the docs for JS Scripting to see if there is a way to get at the links through the API provided by openhab-js. I’m not surprised but sadly it’s not available.

After hitting that dead end I looked at the REST API through the API explorer. That would work but then I’d have to mess with creating an API token and making HTTP calls and all that mess. But what if I can load the file itself instead of using the API?

I honestly wasn’t positive that would work due to permissions (and I’m not sure yet if there isn’t a security problem here). But it’s not hard to try it out. So how to load a file? There are all sorts of ways and tutorials on the forum. A quick search brought up JS Scripting - loading config file - #5 by tose and I remembered the executeCommandLine cat trick. Note, if you are on Windows there is an equivalent command that should be just a short search away.

And of course getting the list of all Items is documented in the JS Scripting docs.

So I wrote the executeCommandLine and logged out the result to verify that it successfully loads the file and puts it into a String variable. That worked, no permission problems like I half expected there to be. :tada:

Next, I know it’s a lot easier to work with a parsed JSON Object than it is to try to make something work using the JSONPATH transformation so I parse the JSON I loaded. The parse succeeded, further validating that the executeCommandLine worked.

Finally I call items.getItems() as documented in the reference docs to get all the Items. All the data I need is now loaded into the rule in three lines of code (could be two).

var linksStr = actions.Exec.executeCommandLine(time.Duration.ofSeconds(5), 'cat', '/openhab/userdata/jsondb/org.openhab.core.thing.link.ItemChannelLink.json');
console.info(linksStr);
var linksParsed = JSON.parse(linksStr);
var allItemNames = items.getItems()

Manipulate the data until it’s something we can use

First I looked at the JSON closely. I expected it to be an array but to my dismay it’s a map. The key is a combination of the Item name and the Channel UID. An example entry looks like this:

  "AdGuard_Protection -\u003e http:url:adguard:protection_status": {
    "class": "org.openhab.core.thing.link.ItemChannelLink",
    "value": {
      "channelUID": {
        "segments": [
          "http",
          "url",
          "adguard",
          "protection_status"
        ],
        "uid": "http:url:adguard:protection_status"
      },
      "configuration": {
        "properties": {}
      },
      "itemName": "AdGuard_Protection"
    }
  },

What I need to do is extract just the Item name from each link.

Based on experience I know there has to be a way to get all the keys from a map like this. A quick search on the internet for “JavaScript get all keys from a map” which among others brings up Map.prototype.keys() - JavaScript | MDN. Awesome! Lets try that!

var linksStr = actions.Exec.executeCommandLine(time.Duration.ofSeconds(5), 'cat', '/openhab/userdata/jsondb/org.openhab.core.thing.link.ItemChannelLink.json');
var linksParsed = JSON.parse(linksStr);
var allItemNames = items.getItems()
console.info(linkesParsed.keys);

Disaster! :firecracker: This generates an error. The parsed JSON isn’t a Map like I assumed (and probably should have known but there are still a lot of things I don’t know about how JS Scripting works).

I am undaunted. Lets try to modify the search to “JavaScript get all keys from a JSON Object” and in the top results is Object.keys() - JavaScript | MDN. OK, let’s try that.

var linksStr = actions.Exec.executeCommandLine(time.Duration.ofSeconds(5), 'cat', '/openhab/userdata/jsondb/org.openhab.core.thing.link.ItemChannelLink.json');
var linksParsed = JSON.parse(linksStr);
var allItemNames = items.getItems()
console.info(Object.keys(linkesParsed));

Success! :tada:

OK, now I know about it because I wrote it but everyone should know about the Design Pattern postings. There’s one that shows how to manipulate the members of a Group in lots of different ways: Design Pattern: Working with Groups in Rules.

Let me tell you a secret. Everything you see there works for messing with any array, not just members of a Group. Most often when you have an array it’ll be members of a Group, but the examples shown there extend beyond that.

We have an array of keys to a JSON Object now and thanks to that Design Pattern (or our general knowledge of JavaScript or your language of choice) we know about the map operation on lists/arrays. That takes an array of Objects and returns a new array of objects modified in some way. Usually it’s used as a way to extract a value from a larger Object.

I could have use a .split on the key to pull out the Item name but I decided to pull it from the itemName property of the parsed JSON instead. Check that it’s working as we go.

var linksStr = actions.Exec.executeCommandLine(time.Duration.ofSeconds(5), 'cat', '/openhab/userdata/jsondb/org.openhab.core.thing.link.ItemChannelLink.json');
var linksParsed = JSON.parse(linksStr);
console.info(Object.keys(linksParsed).map(key => linksParsed[key].value.itemName));
var allItemNames = items.getItems()

The structure of the parsed JSON is as you see in the example above. So I use map to iterate over the keys of linksParsed and use that key to get the record and the value and finally the itemName which I knew to do by looking at the hierarchy of the JSON loaded from the file.

Success! :tada:

Now all I have are the names of the Items but that’s OK. Using the same approach I can get the names of all the Items.

var linksStr = actions.Exec.executeCommandLine(time.Duration.ofSeconds(5), 'cat', '/openhab/userdata/jsondb/org.openhab.core.thing.link.ItemChannelLink.json');
var linksParsed = JSON.parse(linksStr);
var linkedItemNames = Object.keys(linksParsed).map(key => linksParsed[key].value.itemName);
var allItemNames = items.getItems().map(item => item.name);

I now have two lists, one with all the Items that have a Link and a list of all the Items whether or not they have a link. The last step is we need to do what in set theory would be a “difference” operation. Yes, I looked it up so I could word my next search better.

Not knowing if this is something built into JavaScript I searched for “JavaScript difference arrays” and How to Get the Difference Between Two Arrays in JavaScript ? - GeeksforGeeks was among the top results. Even by looking at the table of contents I can see that I can use filter which I learned about on the Working with Groups in Rules post to do the task and the includes() method. So that’s what I did, I filtered allItemNames for those Items that are not included in linkedItemNames.

var linksStr = actions.Exec.executeCommandLine(time.Duration.ofSeconds(5), 'cat', '/openhab/userdata/jsondb/org.openhab.core.thing.link.ItemChannelLink.json');
var linksParsed = JSON.parse(linksStr);
var linkedItemNames = Object.keys(linksParsed).map(key => linksParsed[key].value.itemName);
var allItemNames = items.getItems().map(item => item.name);
var unlinkedItemNames = allItemNames.filter(itemName => !linkedItemNames.includes(itemName));
console.info(unlinkedItemNames);

I noticed that most of the Items listed come from the semantic model (Locations and Equipment) so I added to the filter to remove the Groups too. I had to look at the JS Scripting reference to know how to get the type of an Item though.

var linksStr = actions.Exec.executeCommandLine(time.Duration.ofSeconds(5), 'cat', '/openhab/userdata/jsondb/org.openhab.core.thing.link.ItemChannelLink.json');
var linksParsed = JSON.parse(linksStr);
var linkedItemNames = Object.keys(linksParsed).map(key => linksParsed[key].value.itemName);
var allItemNames = items.getItems().map(item => item.name);
var unlinkedItemNames = allItemNames.filter(itemName => !linkedItemNames.includes(itemName) && items[itemName].type != 'Group');
console.info(unlinkedItemNames);

:tada:

Do something with the result

In a normal rule the “do something” is usually creating a timer or sending a command to an Item or Items or the like. I didn’t know what @Oliver2 wanted to do so I just left it as is.

So that’s how I did it. It only took me about 15 minutes from start to finish mainly because I have a lot of experience with this sort of thing. But there was very little above that I didn’t have to look up to figure out. If I had to look it up, you could look it up too! Hopefully seeing how I went about solving this will give you the confidence and a map (pun intended) for how to research to solve similar problems too.

And just see how much you can accomplish in just a few lines of code.

Finally, I’ll close by saying that using find, filter, map, reduce and forEach (the later two were not needed here) are key to writing generic rules that work for any number of similar Items. They are super powerful.

Good luck and happy coding!

3 Likes