Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend Event-Bus - ItemUpdatedEvent to also trigger on metadata changes - or add new event like ItemMetadataUpdatedEvent #4409

Open
ThaDaVos opened this issue Oct 7, 2024 · 11 comments
Labels
enhancement An enhancement or new feature of the Core

Comments

@ThaDaVos
Copy link

ThaDaVos commented Oct 7, 2024

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);
    }
  }
});
@ThaDaVos ThaDaVos added the enhancement An enhancement or new feature of the Core label Oct 7, 2024
@rkoshak
Copy link

rkoshak commented Oct 8, 2024

If it's configuration data for the rule, why do you need to trigger the rule to run when the configuration changes? You need to load the metadata when the rule runs anyway so the next time the rule runs it will get the new version of the metadata.

I use this in a couple of my published rule templates on the marketplace.

I don't quite understand your need for an event to trigger a rule in this case.

@ThaDaVos
Copy link
Author

ThaDaVos commented Oct 9, 2024

If you check my rule - you can see it has a special coding path when the Configuration changes, in this case it prepares the groups and cleans up the groups it no longer watches. This is just one scenario where triggering a rule based on Metadata changes of an item can be useful.

The request is not specific to configuration containing items, but just items in general and responding to Metadata changes - perhaps even go a little further and also allow subfiltering to the namespace you want - in my opinion this is a valid request in general, my example was just about a configuration item for a rule - but it can also be used for metadata on items which are used to set stuff like temperature etc.

@rkoshak
Copy link

rkoshak commented Oct 9, 2024

Adding a whole new set of events has performance and load impacts on the whole OH system. One needs to be careful about the impacts. Given that the vast majorioty of users would not use this event, is the overall impact low enought that it doesn't matter that the load is increased on their system? I can't answer that but I do want to make sure that it does get answered.

If this is going to slow down people's MainUI or something like that the use case needs to be pretty compelling. Given there are other ways to accomplish what you've done without an event I personally don't find the use case to be that compelling, especially since there are several other approches to accomplish the same thing.

But if the impact is overall really small then the use case doesn't have to be all that compelling.

I'm not a developer so it's up to whom ever volunteers to implement it to decide if the use case is compelling enough to justify the amount of work it will take to implement.

Some of the alternative approaches include:

  • cron based rule to check for changes to the metadata and do what ever needs to be done
  • check for changes when ever the rule changes to detect when the metadata changed (given the triggers of your rule this rule is almost constantly running on even a modest configuration so there won't really be any significant latency)

I'm not saying this isn't a valid request. I'm just cautious that we don't go down some path that causes a huge impact to all users to solve a pretty niche problem with alternative solutions.

@spacemanspiff2007
Copy link
Contributor

This is a duplicate of #3281 and #2877 .
From a logical point of view it makes sense that metadata is bound to an item, but unfortunately it is not and it seems there are no plans to change this.

@ThaDaVos
Copy link
Author

It's not really a duplicate - as my feature request is about sending an event on the Event Bus when metadata changes - this can have all sorts of uses.

But maybe, we need to introduce something new to OpenHAB as currently an item and it's metadata are misused for rule configuration - maybe Rule Metadata should be implemented or a Configuration Registry which is separate from everything and can be used to store configurations for all sorts of things, not only Rules, but also a central place for Addon configuration etc.

In the case of the Rule metadata a special trigger can be added in the rule to trigger it when the Rule metadata changes, just like how the Item state trigger works.

If a Configuration Registry is implemented, same kind of rule can be added but then the configuration is specified, just like how the Item state trigger works

@rkoshak
Copy link

rkoshak commented Oct 10, 2024

I agree I don't think it's a duplicate. The Item metadata doesn't need to be bound to the Item in those ways to generate an event on the bus when the metadata changes.

But maybe, we need to introduce something new to OpenHAB as currently an item and it's metadata are misused for rule configuration

The term "misused" is a little loaded. What do you mean by misused?

Most of us who use Item metadata for rule configuration do so such that the rule behaves different for each Item it processes. For example, I use Item metadata to encode the proxy Item and how long to debounce an Item's state for my Debounce rule tempalte. Moving that to a Rule Metadata doesn't make sense because it controls how that specific Item is processed.

Any configuration of the rule over all is encoded in the rule itself or, in the case of rule tempaltes, through the properties filled out when instantiating the rule from the template. Soemtimes configuration can be done through libraries or loading a configuration file when the rule is loaded (mainly an option for file based rules).

What are some of the use cases where the most appropriate way to configure a rule is to use Item metadata where the metadata isn't on a per Item configuration but for the whole rule? I'm not arguing against this, mind you, I just don't see it and I've done configuration of rules like this in OH for many years so have some experience to bring to bear.

I would like to see a way to create and use templates that does not require one to publish it on the marketplace to install and use it. Not only would that improve the experience for the developers of templates but it is also quite useful in cases like this.

@ThaDaVos
Copy link
Author

ThaDaVos commented Oct 10, 2024

Let me state misuse correctly - I misuse it as I create an item which purpose is to hold configuration for a rule which changes what it is run for.

In my example I have a run, which automatically creates groups based on the tags an item is tagged with - instead of doing it for all tags, I filter out the tags I want it to look for - as my rule comes from a file - I want this configuration to be inside the UI side somehow, so I let the rule create and manage an Item which contains it's configuration - but if I change this items configuration (metadata) I want the rule to run, for example when I remove a tag from the configuration the rule needs to be triggered to clean up the group it created for that tag.

Maybe using a rule for this is wrong, I don't know, but I don't know of a different way to add logic to OpenHAB except perhaps creating an addon.

Maybe the solution I am looking for is what Rule Templates already have, but then for file-based rules - this way the rules configuration can be decoupled from the rule itself. If one can create file-based rule templates, which automatically updates when the rule updates that would solve a lot already as it brings both options together: Allow one to define properties to be configured in the file based rule and have them configurable from the interface - only thing then needed is an easy way to update these from the rule if needed (like I do with keeping track of a list of groups created) and perhaps a trigger for the rule when the configuration changes (as an optional thing to add if needed, in my case, if a tag gets removed, remove the corresponding group)

But still, adding a way to listen for Metadata changes could also help - as my next project I am gonna try to create something like https://community.openhab.org/t/timeline-picker-to-setup-heating-light-and-so-on/55564 - but then store the schedule per item in its metadata - would be great if the metadata gets updated that also a rule could be triggered to for example change the temperature if needed.

The documentation states one can define extra EventTypes - so maybe I could take a look to see if I can listen for metadata changes on the MetadataRegistry and create an addon which sends the events I want - that way it's opt-in
https://www.openhab.org/docs/developer/utils/events.html#define-new-event-types

@rkoshak
Copy link

rkoshak commented Oct 15, 2024

In my example I have a run, which automatically creates groups based on the tags an item is tagged with - instead of doing it for all tags, I filter out the tags I want it to look for - as my rule comes from a file - I want this configuration to be inside the UI side somehow, so I let the rule create and manage an Item which contains it's configuration - but if I change this items configuration (metadata) I want the rule to run, for example when I remove a tag from the configuration the rule needs to be triggered to clean up the group it created for that tag.

What are these Groups used for?

I would use the tags themselves probably instead of Groups, though without knowing how the Groups are used 🤷‍♂️ .

For an example, in the past I've had rule generators which trigger based on system started and a switch Item. If I change the configuration (e.g. add some tags to some Items) I'd command the switch which triggers the rule which deletes the old rule and recreated it anew using the tag to identify those Items for which triggers should be created. Today that might not even need to be done; there might be a way to adjust the triggers of a rule dynamically.

Sure, you need to remember to manually trigger the rule to process the changes, but if these changes are somewhat disruptive you'd probably want to do that once for many changes anyway, instead of processing the changes one at a time.

Maybe the solution I am looking for is what Rule Templates already have, but then for file-based rules - this way the rules configuration can be decoupled from the rule itself.

That's simple enough. Create a function that creates a rule based on the arguments passed to the function. There is nothing really special about the data one passes to JSRule. For example,

function(ruleUID, ruleName, ruleDescription, tag, prop1, prop2) {
  rules.removeRule(ruleUID);
  const triggerItems = items.getItemsByTag(tag);
  const triggers = triggerItems.map( item => triggers.ItemStateChangeTrigger(item) );

  const action = (event) => {
    const one = prop1;
    const two = prop2;
    // rest of the code that uses one/two to control it's behavior
  }

  JSRule( {
    name: ruleName,
    description: ruleDescription,
    triggers = triggers,
    execute: action,
    tags: [],
    id: ruleID
  }};
}

Put this in a personal library and there's little you can't do along these lines.

All UI rule tempaltes do is have a "find and replace" operation to replace {{ prop1 }} with the value set for prop1 from the form when instantiating the rule. You actually have a lot more power in a file based rule to create something akin to rule templates by using rule generators like the above.

It's up to you to figure out when to call the above rule, in response to an event or a manually run rule or based on a cron.

Again, I'm not arguing against a MetadataChanged event, but from what I can see what's driving this request is a little niche as well as potentially an XY Problem.

The documentation states one can define extra EventTypes

I'm pretty sure that's for creating Event Channels on a Thing, not creating new core events. You'll need to modify core to add a new event and modify the Item Metadata Registry to emit this event.

@ThaDaVos
Copy link
Author

ThaDaVos commented Oct 16, 2024

What are these Groups used for?

These are used to setup the GroupCommandTrigger, GroupStateUpdateTrigger and GroupStateChangeTrigger triggers cause as far as I know there is no trigger for items based on tags - unless one uses the GenericEventTrigger and filters manually - so this rule just creates and manages the Groups I'll use for that.

For an example, in the past I've had rule generators which trigger based on system started and a switch Item. If I change the configuration (e.g. add some tags to some Items) I'd command the switch which triggers the rule which deletes the old rule and recreated it anew using the tag to identify those Items for which triggers should be created. Today that might not even need to be done; there might be a way to adjust the triggers of a rule dynamically.

So you've got a rule which creates other rules when triggered? How did you setup the triggers? Did you manually add each item as trigger to the rule? Or create a rule per item?

Again, I'm not arguing against a MetadataChanged event, but from what I can see what's driving this request is a little niche as well as potentially an XY Problem.

You're perhaps right, but I just mentioned one thing, of course there are more examples out there - for example, if I store the schedule for an item inside the metadata, if I change this schedule having the item update accordingly to the new schedule would also need MetadataChanged event - or like you said before, manually trigger a rule or script to update all items based on their schedule.

But thanks for the insight into the Rule Generators - this is something I am going to explore more and can help a lot!

@rkoshak
Copy link

rkoshak commented Oct 16, 2024

So you've got a rule which creates other rules when triggered? How did you setup the triggers? Did you manually add each item as trigger to the rule? Or create a rule per item?

See the example code above. I create all the triggers for the one rule.

or example, if I store the schedule for an item inside the metadata, if I change this schedule having the item update accordingly to the new schedule would also need MetadataChanged event - or like you said before, manually trigger a rule or script to update all items based on their schedule.

Given that there is a Time is <item> trigger, wouldn't it be better to store the schedule in Item states? Then not only do you not need to do anything to the rule (e.g. adding a new trigger) you can have a nice date picker widget to change it.

Now one thing that would be nice in that case would be to do something like a Time is member of <group> that triggers based on the times stored by any member of that Group. That would probably be less work than creating an Item metadata event.

@ThaDaVos
Copy link
Author

Now one thing that would be nice in that case would be to do something like a Time is member of that triggers based on the times stored by any member of that Group. That would probably be less work than creating an Item metadata event.

This is a great suggestion, then there's no need to store it inside the Metadata but you can create a hierarchy of items/groups to store it instead - also I forgot about the Time is <item> trigger - good suggestion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement An enhancement or new feature of the Core
Projects
None yet
Development

No branches or pull requests

3 participants