Open
Description
Your Environment
- Version used: (e.g., openHAB and add-on versions) latest stable
- Environment name and version (e.g. Chrome 111, Java 17, Node.js 18.15, ...): N/A
- Operating System and version (desktop or mobile, Windows 11, Raspbian Bullseye, ...):Ubuntu
It would be great if one can use the triggers.GenericEventTrigger
in the Javascript library to also trigger when an items metadata gets updated - or by using ItemUpdatedEvent
or a new event like ItemMetadataUpdatedEvent
on a topic of openhab/items/*/metadata/updated
or openhab/items/*/updated/metadata
As to why, I am storing information inside metadata to configure my rules - this way it's cached but also accessible from the UI instead of relying on the caching functionality. (See below rule, compiled from Typescript to Javascript for an example, it uses an item to store configuration for the Rule as I couldn't think of another way to do this and have it available from the UI for editing)
Example Rule: Typescript
import { Item } from "openhab/types/items/items";
type ConfigurationMetadataConfiguration = {
tags: string[];
groups: string[];
};
type ConfigurationMetadata = {
value: string;
configuration: ConfigurationMetadataConfiguration;
};
//#jsvariables
const javaList = Java.type('java.util.List');
const javaArrayList = Java.type('java.util.ArrayList');
const javaSet = Java.type("java.util.Set");
const configurationItemName = 'Group_Items_By_Tag_Configuration';
const metadataKey = 'config:%{JSRULE:ID}%'.replace('Typescript:', '').replace(/:/g, '-').toLowerCase();
const baseMetadata: ConfigurationMetadataConfiguration = {
tags: utils.jsArrayToJavaList(['Configuration']) as string[],
groups: utils.jsArrayToJavaList([]) as string[]
};
let configurationItem: Item | null;
let configuration: ConfigurationMetadata;
//#jsvariablesend
//#jsrule
const jsRuleName = "Smart | Create groups from tags and group items"
//#jsrule
// const jsRuleNamespace = null;
//#jsrule
const jsRuleTags = [];
//#jsrule
const jsRuleTriggers = [
triggers.GenericEventTrigger( //TODO: Implement when last item is removed from a group, remove the group
/* eventTopic */ `openhab/items/*/updated`,
/* eventSource */ '',
/* eventTypes */ 'ItemUpdatedEvent'
),
triggers.GenericEventTrigger(
/* eventTopic */ `openhab/items/*/added`,
/* eventSource */ '',
/* eventTypes */ 'ItemAddedEvent'
),
triggers.GenericEventTrigger( //TODO: Implement this and also, when last item is removed from a group, remove the group
/* eventTopic */ `openhab/items/*/removed`,
/* eventSource */ '',
/* eventTypes */ 'ItemRemovedEvent'
),
triggers.SystemStartlevelTrigger(40),
];
function ensureArray(obj: typeof javaList | Array<any>): Array<any> {
const isJavaObject = Java.isJavaObject(obj);
if (isJavaObject == false) {
return obj;
}
switch (Java.typeName(obj.getClass())) {
case 'java.util.ArrayList': return utils.javaListToJsArray(obj);
case 'java.util.List': return utils.javaListToJsArray(obj);
default: throw new Error(`Unsupported Java object type: ${Java.typeName(obj.getClass())}`);
}
}
function loadConfiguration(): [Item | null, ConfigurationMetadata] {
let configurationItem = items.getItem(configurationItemName, true);
if (configurationItem == null) {
console.warn('Configuration item not found, creating new item');
configurationItem = items.addItem({
name: configurationItemName,
type: 'String',
label: 'Group Items By Tag Configuration',
category: 'Configuration',
tags: ['Configuration'],
metadata: {
[metadataKey]: {
value: '',
config: baseMetadata
}
}
});
}
let configurationMetadata = configurationItem.getMetadata(metadataKey) as { value: string; configuration: typeof baseMetadata; };
if (configurationMetadata == null) {
console.warn('Configuration metadata not found, creating new metadata');
configurationItem.replaceMetadata(metadataKey, '', baseMetadata);
configurationMetadata = configurationItem.getMetadata(metadataKey) as { value: string; configuration: typeof baseMetadata; };
}
return [
configurationItem,
{
value: configurationMetadata.value,
configuration: {
tags: ensureArray(configurationMetadata.configuration.tags),
groups: ensureArray(configurationMetadata.configuration.groups)
}
}
];
}
function setup() {
loadConfiguration();
}
function getGroupForTag(tag: string): Item | null;
function getGroupForTag(tag: string, allowCreation: true): Item;
function getGroupForTag(tag: string, allowCreation: false): Item | null;
function getGroupForTag(tag: string, allowCreation: boolean = false): Item | null {
const groupName = `SMART_GROUP__${tag.replace(/[^a-zA-Z0-9]/g, '_')}`;
const groupItem = items.getItem(groupName, true);
if (allowCreation == true && groupItem == null) {
return items.addItem({
name: groupName,
type: 'Group',
label: `Smart Group for tag '${tag}'`,
category: 'Group',
tags: ['Smart', `GROUP:${tag}`]
});
}
return groupItem;
}
function onUpdatedConfiguration() {
for (const tag of configuration.configuration.tags) {
const groupItems = items.getItemsByTag(tag);
if (groupItems.length === 0) {
console.warn(`No items found for tag '${tag}'`);
continue;
}
const groupItem = getGroupForTag(tag, true);
if (groupItem == null) {
console.warn(`No group item found for tag '${tag}'`);
continue;
}
if (configuration.configuration.groups.includes(groupItem.name) === false) {
configuration.configuration.groups.push(groupItem.name);
}
for (const item of groupItems) {
if (item.name === groupItem.name) {
continue;
}
if (Array.from(item.groupNames).includes(groupItem.name) === false) {
item.addGroups(groupItem.name);
}
}
}
const newConfiguration = {
tags: utils.jsArrayToJavaList(configuration.configuration.tags),
groups: utils.jsArrayToJavaList(configuration.configuration.groups)
};
configurationItem?.replaceMetadata(metadataKey, configuration.value, newConfiguration);
}
[configurationItem, configuration] = loadConfiguration();
switch (event.eventClass.split('.').pop()) {
case 'ExecutionEvent':
case 'StartlevelEvent': {
onUpdatedConfiguration();
break;
}
case 'ItemUpdatedEvent': {
const [newPayload, oldPayload] = event.payload;
if (newPayload.name === configurationItemName) {
console.info('Configuration updated');
onUpdatedConfiguration();
break;
}
let tagsChanged = false;
if (
newPayload.tags.length != oldPayload.tags.length
|| newPayload.tags.some((tag: string) => oldPayload.tags.includes(tag) == false)
|| oldPayload.tags.some((tag: string) => newPayload.tags.includes(tag) == false)
) {
tagsChanged = true;
}
let groupsChanged = false;
if (
newPayload.groupNames.length != oldPayload.groupNames.length
|| newPayload.groupNames.some((groupName: string) => oldPayload.groupNames.includes(groupName) == false)
|| oldPayload.groupNames.some((groupName: string) => newPayload.groupNames.includes(groupName) == false)
) {
groupsChanged = true;
}
if (tagsChanged === false && groupsChanged === false) {
console.info(`Item[${newPayload.name}] => No changes detected`);
break;
}
const tagsAdded = tagsChanged
? newPayload.tags.filter((tag: string) => oldPayload.tags.includes(tag) == false)
: [];
const tagsRemoved = tagsChanged
? oldPayload.tags.filter((tag: string) => newPayload.tags.includes(tag) == false)
: [];
const groupsAdded = groupsChanged
? newPayload.groupNames.filter((groupName: string) => oldPayload.groupNames.includes(groupName) == false)
: [];
const groupsRemoved = groupsChanged
? oldPayload.groupNames.filter((groupName: string) => newPayload.groupNames.includes(groupName) == false)
: [];
console.info(`Item[${newPayload.name}] => Tags added: ${tagsAdded.join(', ')}`);
console.info(`Item[${newPayload.name}] => Tags removed: ${tagsRemoved.join(', ')}`);
console.info(`Item[${newPayload.name}] => Groups added: ${groupsAdded.join(', ')}`);
console.info(`Item[${newPayload.name}] => Groups removed: ${groupsRemoved.join(', ')}`);
const groupsToAdd = [];
const groupsToRemove = [];
for (const tag of tagsAdded) {
if (configuration.configuration.tags.includes(tag) === false) {
continue;
}
const groupItem: Item | null = getGroupForTag(tag);
if (groupItem == null) {
console.warn(`Item[${newPayload.name}] => No group item found for tag '${tag}' when adding item '${newPayload.name}'`);
continue;
}
if (groupsAdded.includes(groupItem.name) === false) {
console.info(`Item[${newPayload.name}] => Adding group '${groupItem.name}' for tag '${tag}'`);
groupsToAdd.push(groupItem);
}
}
for (const tag of tagsRemoved) {
if (configuration.configuration.tags.includes(tag) === false) {
continue;
}
const groupItem: Item | null = getGroupForTag(tag);
if (groupItem == null) {
console.warn(`Item[${newPayload.name}] => No group item found for tag '${tag}' when removing item '${newPayload.name}'`);
continue;
}
if (groupsRemoved.includes(groupItem.name) === false) {
console.info(`Item[${newPayload.name}] => Removing group '${groupItem.name}' for tag '${tag}'`);
groupsToRemove.push(groupItem);
}
}
if (groupsToAdd.length === 0 && groupsToRemove.length === 0) {
console.info(`Item[${newPayload.name}] => No groups to add or remove`);
break;
}
const item = items.getItem(newPayload.name) as Item;
item.addGroups(...groupsToAdd);
item.removeGroups(...groupsToRemove);
console.info(`Item[${newPayload.name}] => Added groups: ${groupsToAdd.map((group: Item) => group.name).join(', ')}`);
break;
}
case 'ItemAddedEvent': {
const item = items.getItem(event.payload.name) as Item;
for (const tag of event.payload.tags) {
if (configuration.configuration.tags.includes(tag) === false) {
continue;
}
const groupItem: Item | null = getGroupForTag(tag);
if (groupItem == null) {
console.warn(`No group item found for tag '${tag}' when adding item '${event.payload.name}'`);
continue;
}
if (Array.from(item.groupNames).includes(groupItem.name) === false) {
item.addGroups(groupItem.name);
}
}
break;
}
default:
console.error('Unsupported event type', event);
}
Example Rule: Compiled Javascript (EsBuild with custom plugin)
/* Global variables -- @preserve */
const javaList = Java.type("java.util.List");
const javaArrayList = Java.type("java.util.ArrayList");
const javaSet = Java.type("java.util.Set");
const configurationItemName = "Group_Items_By_Tag_Configuration";
const metadataKey = "config:TypeScript:smart:group-items-by-tag".replace("Typescript:", "").replace(/:/g, "-").toLowerCase();
const baseMetadata = {
tags: utils.jsArrayToJavaList(["Configuration"]),
groups: utils.jsArrayToJavaList([])
};
let configurationItem;
let configuration;
/* Setup script -- @preserve */
function setup() {
loadConfiguration();
}
setup();
/* Rule functions -- @preserve */
function ensureArray(obj) {
const isJavaObject = Java.isJavaObject(obj);
if (isJavaObject == false) {
return obj;
}
switch (Java.typeName(obj.getClass())) {
case "java.util.ArrayList":
return utils.javaListToJsArray(obj);
case "java.util.List":
return utils.javaListToJsArray(obj);
default:
throw new Error(`Unsupported Java object type: ${Java.typeName(obj.getClass())}`);
}
}
function loadConfiguration() {
let configurationItem2 = items.getItem(configurationItemName, true);
if (configurationItem2 == null) {
console.warn("Configuration item not found, creating new item");
configurationItem2 = items.addItem({
name: configurationItemName,
type: "String",
label: "Group Items By Tag Configuration",
category: "Configuration",
tags: ["Configuration"],
metadata: {
[metadataKey]: {
value: "",
config: baseMetadata
}
}
});
}
let configurationMetadata = configurationItem2.getMetadata(metadataKey);
if (configurationMetadata == null) {
console.warn("Configuration metadata not found, creating new metadata");
configurationItem2.replaceMetadata(metadataKey, "", baseMetadata);
configurationMetadata = configurationItem2.getMetadata(metadataKey);
}
return [
configurationItem2,
{
value: configurationMetadata.value,
configuration: {
tags: ensureArray(configurationMetadata.configuration.tags),
groups: ensureArray(configurationMetadata.configuration.groups)
}
}
];
}
function getGroupForTag(tag, allowCreation = false) {
const groupName = `SMART_GROUP__${tag.replace(/[^a-zA-Z0-9]/g, "_")}`;
const groupItem = items.getItem(groupName, true);
if (allowCreation == true && groupItem == null) {
return items.addItem({
name: groupName,
type: "Group",
label: `Smart Group for tag '${tag}'`,
category: "Group",
tags: ["Smart", `GROUP:${tag}`]
});
}
return groupItem;
}
function onUpdatedConfiguration() {
for (const tag of configuration.configuration.tags) {
const groupItems = items.getItemsByTag(tag);
if (groupItems.length === 0) {
console.warn(`No items found for tag '${tag}'`);
continue;
}
const groupItem = getGroupForTag(tag, true);
if (groupItem == null) {
console.warn(`No group item found for tag '${tag}'`);
continue;
}
if (configuration.configuration.groups.includes(groupItem.name) === false) {
configuration.configuration.groups.push(groupItem.name);
}
for (const item of groupItems) {
if (item.name === groupItem.name) {
continue;
}
if (Array.from(item.groupNames).includes(groupItem.name) === false) {
item.addGroups(groupItem.name);
}
}
}
const newConfiguration = {
tags: utils.jsArrayToJavaList(configuration.configuration.tags),
groups: utils.jsArrayToJavaList(configuration.configuration.groups)
};
configurationItem?.replaceMetadata(metadataKey, configuration.value, newConfiguration);
}
/* Rule definition -- @preserve */
rules.JSRule({
id: "TypeScript:smart:group-items-by-tag",
name: "Smart | Create groups from tags and group items",
triggers: [
triggers.GenericEventTrigger(
/* eventTopic */
`openhab/items/*/updated`,
/* eventSource */
"",
/* eventTypes */
"ItemUpdatedEvent"
),
triggers.GenericEventTrigger(
/* eventTopic */
`openhab/items/*/added`,
/* eventSource */
"",
/* eventTypes */
"ItemAddedEvent"
),
triggers.SystemStartlevelTrigger(40)
],
tags: [],
execute(event) {
[configurationItem, configuration] = loadConfiguration();
switch (event.eventClass.split(".").pop()) {
case "ExecutionEvent":
case "StartlevelEvent": {
onUpdatedConfiguration();
break;
}
case "ItemUpdatedEvent": {
const [newPayload, oldPayload] = event.payload;
if (newPayload.name === configurationItemName) {
console.info("Configuration updated");
onUpdatedConfiguration();
break;
}
let tagsChanged = false;
if (newPayload.tags.length != oldPayload.tags.length || newPayload.tags.some((tag) => oldPayload.tags.includes(tag) == false) || oldPayload.tags.some((tag) => newPayload.tags.includes(tag) == false)) {
tagsChanged = true;
}
let groupsChanged = false;
if (newPayload.groupNames.length != oldPayload.groupNames.length || newPayload.groupNames.some((groupName) => oldPayload.groupNames.includes(groupName) == false) || oldPayload.groupNames.some((groupName) => newPayload.groupNames.includes(groupName) == false)) {
groupsChanged = true;
}
if (tagsChanged === false && groupsChanged === false) {
console.info(`Item[${newPayload.name}] => No changes detected`);
break;
}
const tagsAdded = tagsChanged ? newPayload.tags.filter((tag) => oldPayload.tags.includes(tag) == false) : [];
const tagsRemoved = tagsChanged ? oldPayload.tags.filter((tag) => newPayload.tags.includes(tag) == false) : [];
const groupsAdded = groupsChanged ? newPayload.groupNames.filter((groupName) => oldPayload.groupNames.includes(groupName) == false) : [];
const groupsRemoved = groupsChanged ? oldPayload.groupNames.filter((groupName) => newPayload.groupNames.includes(groupName) == false) : [];
console.info(`Item[${newPayload.name}] => Tags added: ${tagsAdded.join(", ")}`);
console.info(`Item[${newPayload.name}] => Tags removed: ${tagsRemoved.join(", ")}`);
console.info(`Item[${newPayload.name}] => Groups added: ${groupsAdded.join(", ")}`);
console.info(`Item[${newPayload.name}] => Groups removed: ${groupsRemoved.join(", ")}`);
const groupsToAdd = [];
const groupsToRemove = [];
for (const tag of tagsAdded) {
if (configuration.configuration.tags.includes(tag) === false) {
continue;
}
const groupItem = getGroupForTag(tag);
if (groupItem == null) {
console.warn(`Item[${newPayload.name}] => No group item found for tag '${tag}' when adding item '${newPayload.name}'`);
continue;
}
if (groupsAdded.includes(groupItem.name) === false) {
console.info(`Item[${newPayload.name}] => Adding group '${groupItem.name}' for tag '${tag}'`);
groupsToAdd.push(groupItem);
}
}
for (const tag of tagsRemoved) {
if (configuration.configuration.tags.includes(tag) === false) {
continue;
}
const groupItem = getGroupForTag(tag);
if (groupItem == null) {
console.warn(`Item[${newPayload.name}] => No group item found for tag '${tag}' when removing item '${newPayload.name}'`);
continue;
}
if (groupsRemoved.includes(groupItem.name) === false) {
console.info(`Item[${newPayload.name}] => Removing group '${groupItem.name}' for tag '${tag}'`);
groupsToRemove.push(groupItem);
}
}
if (groupsToAdd.length === 0 && groupsToRemove.length === 0) {
console.info(`Item[${newPayload.name}] => No groups to add or remove`);
break;
}
const item = items.getItem(newPayload.name);
item.addGroups(...groupsToAdd);
item.removeGroups(...groupsToRemove);
console.info(`Item[${newPayload.name}] => Added groups: ${groupsToAdd.map((group) => group.name).join(", ")}`);
break;
}
case "ItemAddedEvent": {
const item = items.getItem(event.payload.name);
for (const tag of event.payload.tags) {
if (configuration.configuration.tags.includes(tag) === false) {
continue;
}
const groupItem = getGroupForTag(tag);
if (groupItem == null) {
console.warn(`No group item found for tag '${tag}' when adding item '${event.payload.name}'`);
continue;
}
if (Array.from(item.groupNames).includes(groupItem.name) === false) {
item.addGroups(groupItem.name);
}
}
break;
}
default:
console.error("Unsupported event type", event);
}
}
});