Use multiple Group state aggregation functions of same type - for example multiple `OR`

  • Platform information:
    • Hardware: Docker container
    • OS: Linux/6.2.0-33-generic (amd64) - Ubuntu
    • Java Runtime Environment: 17.0.7 (undefined)
    • openHAB version: openHAB 4.1.0 Build #3684

Currently one can only specify a single GROUP aggregation function or only supply two arguments without using rules. What I want to do is have my GROUP’s state equal the state with highest priority - my group contains multiple items which can have one of three string based states: heat, cool and none - you can probably guess this is for.

I want the group’s state to be equal to one of these states based on order - so as follows:

  1. If one of the items state equals heat - then the group state is also heat
  2. If one of the items state equals cool but none equal heat - then the group state is also cool
  3. If none of the above is true, then the group state is none

Currently I got step 1 and 3 working using the OR function - sadly, it only allows two parameters - so I cannot set it to cool without using a rule - why not use a rule? Simple, I’ve got many items to do this for and making a rule per item with the exact same code inside (using this.event.itemName) will be really cumbersome and as the member of group changes does not seem to work through all layers of the group, I will have to create a group containing all items or use tags to have this function - I rather define it on the item itself, this way I can easily change the logic per item if needed (for example if I want to switch the order of heat and cool.

Proposed solutions:

  1. Allow multiple params for the OR function, checking in order, can look like:
function:
  name: OR
  params:
    - heat // Wil be set when one of the members equals `heat`
    - cool // Wil be set when one of the members equals `cool` but none equal `heat`
    - none // Wil be set when none of the members equal `heat` and none of the members equal `cool`
  1. Allow the use of multiple functions, perhaps combine different ones, for example, top layer OR with two AND inside, this can look like:
function:
  name: OR
  params:
    - function:
      name: AND
      params:
       - ValueA
       - DefaultValue
    - function:
      name: AND
      params:
       - ValueB
       - DefaultValue

There is no reason this cannot be implemented with a single rule for all Items.

There are ways around this. There is a generic trigger which you can set up almost any way you want to.

This is usually done using Item metadata.

This sounds like a great candidate for a rule template. I can think of several ways to implement it using a single rule. One approach:

  1. create a summary Item for each Group, this Item will hold the result of the aggregation

  2. put all your status Items into a single Group

  3. add metadata to the Items that are a member of the single Group containing at a minimum the name of the summary Item it belongs to

  4. optionally add metadata to the aggregation Item with the configuration for how to do the aggregation.

  5. trigger the rule when any member of the single Group changes

  6. based on the Item that triggered the rule find the summary Item and all the other Items that belong to that summary Item. Pull the metadata from the summary Item to configure the aggregation function and calculate and update the summary Item. The code in JS Scripting would look something like:

var triggeringItem = items[event.itemName];
var summaryItem = items[triggeringItem.getMetadata('aggregation').value()]
var aggregationItems = items.getItems().filter( item => item.getMetadata('aggregation') == summaryItem.name);

var aggregationConfig = summaryItem.getMetadata('aggregation').???? // depends on how the config is represented.

// Do the aggregation on aggregationItems, can probably use a map/reduce

summaryItem.postUpdate(result);

Similar proposals have been made in the past. Unless you are volunteering to implement it I wouldn’t hold much hope that it gets implemented. Implementing something like this is going to hit a lot of different repos and be a whole lot of work that so far no one has volunteered to take on.

Also, Rules are there to handle these more complicated cases. And hopefully you can see, it’s not that much to implement in a single rule.

How would one trigger a rule for all items - except creating a huge group? If we could somehow create a rule to trigger based on a item with tag it could be easily done - or maybe I am thinking to complicated

Which generic trigger do you mean? The cron and trigger every second?

I am already using metadata for a rule which sets these status items based on the measured temperature and the setpoint temperature

If I knew where to start I would volunteer myself to implement this - or maybe open it up to allow a custom expression to set the state

Yup, easily use a rule for this.

You can create a group gThermostats and add all those items as members. Then trigger on member-of-group. It’s more efficient than filtering by tags.

Then rule could look like this (done in JRuby)

ORDER = %w[none cool heat]

rule "Update global thermostat summary" do
  changed gThermostats.members
  run do |event|
    summary = event.group.members
                   .map(&:state)
                   .compact
                   .map(&:to_s)
                   .max { |a, b| ORDER.index(a) <=> ORDER.index(b) }
    event.group.update(summary)
  end
end

Thank you @JimT - I’ll look into using more rules then - wish we could order them in some kind of hierachy though - cause the list can get long :rofl:

Going to look to use metadata too - wishing one had auto-complete in custom metadata though, especially for item names :rofl:

What do you mean by hierarchy? It could always be implemented in a rule, just need to understand what needs to be done :slight_smile:

I already got a rule for setting the status on based on temperature and setpoint - any improvements I can make?

configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: Radiators_Setpoints
    type: core.GroupStateChangeTrigger
  - id: "2"
    configuration:
      groupName: Radiators_Actual_Temps
    type: core.GroupStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript
      script: >-
        function getRadiator(itemName) {
            var radiator = items.getItem(itemName, true);

            if (!radiator) return [null, null];

            var metadata = radiator.getMetadata('thermostat') ?? radiator.getMetadata('heating') ?? radiator.getMetadata('cooling');

            if (!metadata) return [radiator, null];

            if (metadata.value?.trim().length > 0) return getRadiator(metadata.value);

            return [radiator, metadata];
        }


        function handleItem(itemName) {
            const MEASUREMENT_MARGIN = 0.1; // Number.EPSILON is too small
          
            var name, setpointItemName, setpointItem, setpoitMetadata, actualTempItem, actualTempItemName, actualTempMetadata, requestItem, requestItemName, requestMetadata;

            if (itemName.endsWith('_Setpoint_Temperature')) {
                name = itemName.slice(0, -"_Setpoint_Temperature".length);

                setpointItemName = itemName;
                [setpointItem, setpoitMetadata] = getRadiator(setpointItemName);
                
                actualTempItemName = setpoitMetadata?.configuration?.actualTempItem ?? `${name}_Actual_Temp`;
                [actualTempItem, actualTempMetadata] = getRadiator(actualTempItemName);

                requestItemName = setpoitMetadata?.configuration?.requestItem ?? `${name}_Requests`;
                [requestItem, requestMetadata] = getRadiator(requestItemName);

            } else if (itemName.endsWith('_Actual_Temp')) {
                name = itemName.slice(0, -"_Actual_Temp".length);

                actualTempItemName = itemName;
                [actualTempItem, actualTempMetadata] = getRadiator(actualTempItemName);

                setpointItemName = actualTempMetadata?.configuration?.setpointItem ?? `${name}_Setpoint_Temperature`;
                [setpointItem, setpoitMetadata] = getRadiator(setpointItemName);

                requestItemName = actualTempMetadata?.configuration?.requestItem ?? `${name}_Requests`;
                [requestItem, requestMetadata] = getRadiator(requestItemName);
            } else {
                var [radiator, metadata] = getRadiator(itemName);

                setpointItemName = metadata.configuration.actualTempItem ? metadata.configuration.actualTempItem : itemName;
                [setpointItem, setpoitMetadata] = metadata.configuration.setpointItem ? getRadiator(setpointItemName) : [radiator, metadata];
                
                actualTempItemName = metadata.configuration.setpointItem ? metadata.configuration.setpointItem : itemName;
                [actualTempItem, actualTempMetadata] = metadata.configuration.actualTempItem ? getRadiator(actualTempItemName) : [radiator, metadata];
                
                requestItemName = metadata.configuration.requestItem ? metadata.configuration.requestItem : itemName;
                [requestItem, requestMetadata] = metadata.configuration.requestItem ? getRadiator(requestItemName) : [radiator, metadata];
            }

            if (!actualTempItem) {
                console.error(`Radiator '${name}' cannot determine actual temp item`, { actualTempItemName, setpointItemName, requestItemName, metadata: actualTempMetadata });
                return;
            }

            if (!setpointItem) {
                console.error(`Radiator '${name}' cannot determine setpoint item`, { actualTempItemName, setpointItemName, requestItemName, metadata: setpoitMetadata });
                return;
            }

            if (!requestItem) {
                console.error(`Radiator '${name}' cannot determine request item`, { actualTempItemName, setpointItemName, requestItemName, metadata: requestMetadata });
                return;
            }

            var temperature = actualTempItem.numericState;
            var setpoint = setpointItem.numericState;

            switch (requestItem.state) {
                case 'none':
                    if (temperature > setpoint) {
                        console.info(`Radiator '${name}' changing from '${requestItem.state}' to 'cool' as ${temperature} > ${setpoint}`);
                        requestItem.sendCommandIfDifferent("cool");
                    } else if (temperature < setpoint) {
                        console.info(`Radiator '${name}' changing from '${requestItem.state}' to 'heat' as ${temperature} < ${setpoint}`);
                        requestItem.sendCommandIfDifferent("heat");
                    } else {
                        console.info(`Radiator '${name}' not changing`);
                        requestItem.sendCommandIfDifferent("none");
                    }
                    break;
                case 'cool':
                    if (temperature >= (setpoint - MEASUREMENT_MARGIN) && temperature <= (setpoint + MEASUREMENT_MARGIN)) {
                        console.info(`Radiator '${name}' changing from '${requestItem.state}' to 'none' as ${temperature} == ${setpoint}`);
                        requestItem.sendCommandIfDifferent("none");
                    } else if (temperature < setpoint) {
                        console.info(`Radiator '${name}' changing from '${requestItem.state}' to 'heat' as ${temperature} < ${setpoint}`);
                        requestItem.sendCommandIfDifferent("heat");
                    } else {
                        console.info(`Radiator '${name}' not changing`);
                        requestItem.sendCommandIfDifferent("cool");
                    }
                    break;
                case 'heat':
                    if (temperature >= (setpoint - MEASUREMENT_MARGIN) && temperature <= (setpoint + MEASUREMENT_MARGIN)) {
                        console.info(`Radiator '${name}' changing from '${requestItem.state}' to 'none' as ${temperature} == ${setpoint}`);
                        requestItem.sendCommandIfDifferent("none");
                    } else if (temperature > setpoint) {
                        console.info(`Radiator '${name}' changing from '${requestItem.state}' to 'cool' as ${temperature} > ${setpoint}`);
                        requestItem.sendCommandIfDifferent("cool");
                    } else {
                        console.info(`Radiator '${name}' not changing`);
                        requestItem.sendCommandIfDifferent("heat");
                    }
                    break;
            }
        }


        if (this.event.itemName?.trim()?.length > 0) {
            handleItem(this.event.itemName);
        } else {
            for (var item of items.getItem('Radiators_Actual_Temps')?.descendents ?? []) {
                console.info(`Manual trigger for '${item.name}'`);
                handleItem(item.name);
            }

            for (var item of items.getItem('Radiators_Setpoints')?.descendents ?? []) {
                console.info(`Manual trigger for '${item.name}'`);
                handleItem(item.name);
            }
        }
    type: script.ScriptAction

Also quick question - why do the buttons for specific code fences use PHP and not the corresponding language like yaml?

Hierarchy as in grouping rules into folders/groups, as this list can get really long - currently using some fancy label naming to sort of group them:

Use file-based rules would be my suggestion, but others may have better suggestions.

There is a generic trigger that supports filters. But I don’t think thats warranted in this case. A Group or tag would be a better choice, particularly on lower power machines.

No. GenericEventTrigger. I’ve used this in my Thing Status Reporting [4.0.0.0;4.9.9.9] rule template to trigger the rule on all changes to a Thing’s status. It’s not available in Rules DSL and it requires hand coding it in the UI from the Code tab. But that’s OK, most users day-to-day shouldn’t be using this trigger anyway.

You can add tags to rules and use the search bar to find all the rules with a given tag. You can also use the Developer Sidebar to pin what you are currently working on.

The list can get long on any OH Settings page which is why there is a fairly robust search capability.

The nice thing about the developer sidebar is you can pin everything: Items, Things, Rules, etc. so everything is right there.

The code fences buttons of the forum software have not been updated since before OH 3. At that time there was no YAML in OH anywhere. The buttons use the languages closest to the custom formats OH defines for those entities. In the rules case, PHP gave the best results for Rules DSL.

I tried that - but for some reason I cannot navigate to the pinned items - like open the corresponding page

Which version of OH are you running? In OH 4.1 M1 (I think, maybe M2) a bug was introduced that eliminated the little pencil icon. An issue was filed and I think it’s been fixed. I’m still on the milestone though so am not positive it’s fixed yet or awaiting merging.

afbeelding
I am on one of the latest snapshot: 4.1.0 Build #3684

When I click it - I get the following:
afbeelding

I guess the fix hasn’t been merged then. It should be fixed before OH 4.1 release for sure.

The issue is OH4.1.0M2: Developer Sidebar edit and unpin icon missing on pinned items · Issue #2133 · openhab/openhab-webui · GitHub and PR is Fix missing footer element for Item Vue component by florian-h05 · Pull Request #2135 · openhab/openhab-webui · GitHub and indeed it hasn’t been merged yet.