Complex lighting controls, rules, logic etc

  • Platform information:
    • Hardware: Raspberry Pi 5
    • OS: Raspberry Pi OS 64 bit
    • Java Runtime Environment: OpenJDK 17.0.12
    • openHAB version: 4.2.1

I’ve been working on a network of lighting controllers that I have built, which will be ultimately controlled through OpenHAB. Each lighting panel group consists of a Switch, Dimmer and Colour Temperature (which uses a Dimmer item).

These Items are linked through Channels to a generic MQTT Thing which then sends out the MQTT commands to control the physical controllers.

Currently I have everything working under manual control, through both external buttons as well as through the web based UI.

The last thing I want to implement is an Auto mode. Each light group has an additional Switch called Auto mode, where the lights are controlled through automated means, ie motion sensors, scenes and time of day effects etc.

Generally, I would like the lights to function like this:

  • When auto mode if OFF, ignore the automated commands for the switch and dimmers
  • When the switch or dimmers are manually altered either through physical buttons or through the UI, then turn off auto mode.

I can achieve the 2nd point by creating a rule which turns off the auto mode switch when either the light switch or dimmers receive a Command. I was hoping that I could then use the “Update Item” control method to send values to the items through automated means without it turning off auto mode. Unfortunately Updating an item doesn’t send an associated channel link command, and so it doesn’t output any MQTT messages.

The next quick and dirty attempt consisted of sending an Auto mode ON command after any command sent by an automated means. However this only occasionally worked, as I think that there was no guarantee whether that ON command would be processed before or after the rule that turned the auto mode off.

I have a few other ideas which consist of using Items to store global variables. Or some way to distinguish different custom commands (ie, auto mode turns off if light switch receives an ON command but not an AUTO_ON, for example). I’m still unsure if these are possible, or how to go about it.

What would really be helpful is being able to pass arguments into scripts, but that still doesn’t seem to be possible either.

Any ideas?

You can send Dimmer and Switch commands to a Color Item. Depending on how the MQTT communications work you may only need the Color Channel and Item.

That is the difference between a command and an update in OH. An update stays within OH. A command goes out to the binding and eventually out to the device.

Correct, the processing of the command is handled in parallel by the Item and the rule so it is not guaranteed that the command has caused the Item to change state when the rule is running. And really, there is no guarantee that the Item will ever change state in response to a command.

Pass arguments from where? Scripts that call scripts can certainly pass arguments to each other, even in Blockly.

What hasn’t been said is how comprehensive is this automation and where is it implemented? For example, if one maunally changes one light, does that disable the automation for just that one light, some of the lights, all of the lights? Is the automation managed by rules or on the devices themselve?

This is really a case of Design Pattern: Manual Trigger Detection which documents a couple of approaches for detecting where a command or event is comming from. The proxy Item is the most reliable.

My automation is simple so I personally just disable the rule that detects manual triggers when the automation is changing the lights. Then I reenable it. That way the manual detection rule can treate any event as a manually triggered event and set a flag to indicate the automation has been overridden.

Once you can distinguish between automated commands/updates from manual commands/updates you have some options. You can set an Item to act as a flag and then use a rule condition (for managted rules) or if statement to cause your rules not to process the event when automation is disabled. You can also disable the automation rules entirely (not available in Rules DSL) so for all intents and purposes the rule doesn’t exist so can’t be triggered. When returning to auto mode reenable the rules.

My automation involves controlling the lights based on a calculated ambient light level. If one manually controls a light, automation is turned off for that light until the next day. The rules are here for an example:

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: Outdoors_Weighted_Brightness
    type: core.ItemStateChangeTrigger
  - id: "2"
    configuration:
      itemName: TimeOfDay
      state: DAY
    type: core.ItemStateChangeTrigger
conditions:
  - inputs: {}
    id: "3"
    configuration:
      itemName: TimeOfDay
      state: DAY
      operator: =
    type: core.ItemStateCondition
  - inputs: {}
    id: "6"
    label: Outdoors_Weighted_Brightness isn't undefined
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: |
        !items.getItem('Outdoors_Weighted_Brightness').isUninitialized;
    type: script.ScriptCondition
actions:
  - inputs: {}
    id: "4"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var lightState =
        (items.Outdoors_Weighted_Brightness.quantityState.greaterThanOrEqual('25000
        lx')) ? 'OFF' : 'ON';

        console.debug('New light state is ' + lightState + ', adjusting the
        lights accordingly');


        console.debug('Disabling the override rule');

        rules.setEnabled('weather_lights_override', false);


        items.TOD_Lights_ON_WEATHER
             .members
             .filter(light => light.getMetadata('LightsOverride').value == 'false' || !light.getMetadata('LightsOverride').value)
             .forEach(light => {
               console.debug('Commanding ' + light.label + ' to ' + lightState + ' do to weather');
               light.sendCommandIfDifferent(lightState);
             });

        setTimeout(() => {
          console.debug('Reenabling override rule');
          rules.setEnabled('weather_lights_override', true);
        }, 250);
    type: script.ScriptAction

  • The manual trigger detection rule is relatively simple, treating any event as a manual event, setting the override flag (I use Item metadata as you can see from above).
configuration: {}
triggers:
  - id: "1"
    configuration:
      groupName: TOD_Lights_ON_WEATHER
    type: core.GroupStateChangeTrigger
conditions:
  - inputs: {}
    id: "3"
    configuration:
      itemName: TimeOfDay
      state: DAY
      operator: =
    type: core.ItemStateCondition
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        console.info('Manual light trigger detected, overriding the light for
        {}', event.itemName);

        items[event.itemName].replaceMetadata('LightsOverride', 'true', []);
    type: script.ScriptAction
  • Finally, I have a rule to reset the override flags so the automation runs as expected the next day.
configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: TimeOfDay
      previousState: DAY
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: "console.loggerName = 'org.openhab.automation.rules_tools.Reset Lights
        Override';

        console.info('Resetting the LightsOverride metadata value to false');


        console.debug('Looping through the lights');

        items.TOD_Lights_ON_WEATHER.members.forEach( light => {

        \  console.debug('Current override for ' + light.name + ' is ' +
        light.getMetadata('LightsOverride').value);

        \  light.replaceMetadata('LightsOverride', 'false');

        })

        \    "
    type: script.ScriptAction

Note: I’m noticing that there are some updates I can make to these rules to take advantage of changes since OH 3.x when I last looked at them.

2 Likes

Alright thanks for the info. I have made some progress today.

Automation will be toggled on a per light group basis, a group consisting of a switch, dimmer and colour temp slider.

I’ve made a solution having similarities of both solutions you’ve linked in your manual detection writeup. I have created a proxy item for the switch, and 2x dimmer items, along with a number item that stores a 1 or 0 representing an auto command flag.

For any automated command, the general sequence of events would go as such (example using the switch):

  • A command is sent to the proxy switch
  • A rule is triggered when a command is received by the proxy switch
  • If Auto Mode == ON, send a command of “1” to the Flag item
  • Wait 50ms
  • Send the value of the proxy switch to the “real” item
  • When the real switch receives a command, it triggers a script which checks whether the auto flag is “1”, and then does not turn off the auto mode switch.
  • Lastly a rule is triggered by the auto flag receiving a command, which has a timeout function.

Currently it is working quite reliably, the only potential pitfalls are the timings are based on chance rather than certainty. For example, the 50ms wait is required to reasonably be sure that the auto flag command = 1 is processed before the real switch rule checks that value. Without a slight delay, automated commands would eventually self-cancel the auto mode. Sometimes within seconds, sometimes within minutes.

Likewise, the auto flag item requires a timeout function. Ideally whichever function was reading the value should also then set it to “0” to clear the flag, but again it was unreliable with timings.

Currently, any automated command will hold the auto flag at a value of “1” for one second, as that seems to be the lowest value accepted by the plusSeconds function in the ZonedDateTime class. Ie, a value of 0.9 may as well be interpreted as 0. I read that the plusNanos function may work in its place, however I could not get that to work. Ideally a few hundred milliseconds should be plenty, even if the precision isn’t great. I don’t suppose that is possible?

Do you have further reading on passing arguments into or between scripts? Is that using the cache you mentioned?

If you use the shared cache for this instead of an Item you won’t need the wait. However the cache doesn’t survive a reboot like an Item with restoreOnStartup would.

You don’t say what language but assuming you are using the Java ZDT class (i.e. Rules DSL): ZonedDateTime (Java SE 17 & JDK 17). What didn’t work? What was the error?

If JS Scripting joda-js ZonedDateTime supports plusMillis or you can just use time.toZDT(1000) (for example). Plain numbers are treated as plus milliseconds by time.toZDT().

But again, if you use the cache instead of an Item there’s no need to wait.

Not really. It’s not a common operation, it’s not avaialble in all rules languages, and I don’t know what language you are using. But in general the API has a runRule method that takes the UID of the rule, a dict of arguments, and a boolean indicating whether the rule conditions should be followed or not. But how you get to that method varies based on the language being used.

It cannot be done in Rules DSL. In Blockly there’s a block and in JS Scripting it’s rules.runRule('uid', { }, false);.

But you don’t have a situation where you have one rule call another here so that whole conversation is moot.

No, but the cache is a way to share data between rules/scripts. For Rules DSL see Textual Rules | openHAB. For JS Scripting see JavaScript Scripting - Automation | openHAB. For Blockly see Rules Blockly - Value Storage | openHAB

1 Like

OK, I’ve been experimenting now for a few days, and I think I’ve got a stable solution.

In terms of coding language, I’ve been prototyping in blockly or modifying the ECMAScript directly. I did manage to experiment with reading/writing private/shared cache methods, and I did get the time.toZDT() function to work, so I’ll keep that in mind for future use.

However in this case I think any timing/timer based approaches are simply too unreliable, especially when the rate of commands/events increases. Any way that I tried to set an auto command flag preceding other events was around ~97% successful at best, but any constant stream of automated commands would quickly result in a self-cancellation of the auto mode. My best guess is that any sequence of event that required an auto command flag could have that flag reset by a previous event, before the latest one could complete it’s operation.

I’ve ditched that idea in favour of purely proxy items. Take a single light switch for example, it will now be represented by the Switch, Switch Proxy and Swtich Proxy Output.

Any manual commands, ie UI interaction or external physical buttons, directly control the Switch. Any manual commands also cancel auto mode.

Any automated commands are sent to the Switch Proxy. It then checks for conditions, eg whether auto mode is enabled, then sends it’s state to the Switch Proxy Output, which sends MQTT to the physical hardware. It also updates the state of the Switch, so the UI will correspond to the correct state.

I’ve had this method working reliably even with a high rate of events. BUT, there’s still timing issues which I’ve found. These are 2 versions of the same script which is called when there’s a command sent to the Switch Proxy:

In the first version, the get state of item function is called twice. However it would very often return 2 different values.

For example, if the command ON was sent to the Switch Proxy, this script would be called. However the first get state of item function call would often return the previous state of OFF, even though the script is being called in response to an ON command!

In the second example, the get state of item function is only called once, and the value is stored as a private cache value. This means that the value sent to the Switch and Switch Proxy Output are always the same…BUT if we omit the 50ms wait, the same problem as before is encountered. Many times the item state is not updated before the script is called (many times during the script).

So perhaps once the item state is reliably stable, copying it to a private cache is not necessary, but maybe reading the private cache is a faster operation than a get item state function call, I don’t know. Reading from cache did narrow down the issue though.

Lastly, even though I wanted to avoid creating many extra proxy items, it does also reduce the number of commands/events/scripts taking place per automated command.

Thanks again for the help. There’s obviously a lot to consider in how OpenHAB processes various events, in no guaranteed order.

Could you click on the “show code” icon and post the generated JS. Use code fences.

```
code goes here
```

One of the challenges with Blockly is it’s hard to read the screen shots from a phone.

How is this rule triggered? When using the state of an Item in a rule, it’s always better to get the state of the Item that triggered the rule using the context block. This block, among other values, has the state or command that triggered the rule fixed so if the Item changed in the mean time you know what state caused the rule to trigger. that can save a lot. and if you can build the logic off of that instead of relying on the state of the Item it doesn’t matter what happens to the Item while the rule is running. Those changes will be handled in subsequent runs of the rule.

As a general rule, you cannot rely on the state of an Item immediately after that Item was updated or commanded.

You’ve got multiple rules operating on these Items so it might be the case you can’t rely on the states at all as other rules might be updating the same Items at the same time. Adding a delay to all the rules doesn’t help. It’s hard to tell if that’s happening here.

You also have local variables you can use. The cache is mainly to share variables between rules or to save a value between runs of the same rule (e.g. so you can reschedule a timer if one already exists).

You don’t need the cache for this specific use.

Both of these scripts are triggered in response to int_00_led_controller_ch_0_sw_proxy receiving any command:

First version:

if (items.getItem('int_00_led_controller_ch_0_automode').state == 'ON') {
  items.getItem('int_00_led_controller_ch_0_sw_proxy_output').sendCommand(items.getItem('int_00_led_controller_ch_0_sw_proxy').state);
  items.getItem('int_00_led_controller_ch_0_sw').postUpdate(items.getItem('int_00_led_controller_ch_0_sw_proxy').state);
}

Second version:

var thread = Java.type('java.lang.Thread')


if (items.getItem('int_00_led_controller_ch_0_automode').state == 'ON') {
  thread.sleep(50);
  cache.private.put('temp_item_state', items.getItem('int_00_led_controller_ch_0_sw_proxy').state);
  items.getItem('int_00_led_controller_ch_0_sw_proxy_output').sendCommand((cache.private.get('temp_item_state')));
  items.getItem('int_00_led_controller_ch_0_sw').postUpdate((cache.private.get('temp_item_state')));
}

I’ll experiment later with the context block

Actually it wasn’t too hard, I guess this is the best practice way to do it?

if (items.getItem('int_00_led_controller_ch_0_automode').state == 'ON') {
  items.getItem('int_00_led_controller_ch_0_sw_proxy_output').sendCommand(event.itemCommand?.toString());
  items.getItem('int_00_led_controller_ch_0_sw').postUpdate(event.itemCommand?.toString());
}

I assume the downside is that this will pass along any command, even if it’s invalid. Whereas reading the state of an item means it has gone through error checking processes.

You can never count on the state of an Item having changed after a command in a rule triggered by received command on that Item.

However, you can get the command sent that triggered the rule using

the JS version is event.itemCommand.

A command doesn’t change the state of an Item. A command can (but is not guaranteed to) result in an update to the Item. But while OH is deciding whether to update the Item and issuing that update, the rule has already triggered based on the command.

So always use received command in a rule triggered by a command to know what command it was. You cannot rely on the state of the commanded Item to know that. For some commands and some configurations the state may not ever reflect the command (e.g. autoupdate is turned off and you commanded an device that’s offline).

Yes, for all command triggered rules you want to use this instead of the state of the Item. As an added bonus, because rule triggers are queued up and worked off in order, if you bombard an Item with commands, those commands will be worked off in order.

If it’s an invalid command for that Item type the command event will never occur and the rule will never trigger (e.g. sending “FOO” to a Switch Item). This error checking occurs before the event ever makes it to the event bus.

If it is a valid command for a given Item type, you’ll have to add logic to your rule to determine if it’s valid in context or not. But at least you’ll know that it’s a valid command for that type of Item.

1 Like

This is a tangential question, but I assume that scripts are compiled when first run?

Firstly the proxy items have been working almost flawlessly, not to mention much faster than any script that needs to create a thread and run a timer.

There’s usually some lag and unexpected behaviour when first run, ie missed commands etc, but run fine after that.

Performance is still good even scaling up the model, for example broadcasting / batch commands being sent to 15-20 light panels

I think it depends on the language.

I believe Rules DSL gets parsed and “compiled” at load time. JS Scripting used to work the same but a recent change to the add-on moved that to load time instead of first run.

Of course, if you change a rule it needs to be reloaded.