Not getting error statements when loading/parsing rules file

I’m writing some extensions to my rules to accommodate some new Shelly automations that I have.

Recently I’ve noticed that when I write rules and upload the new rules file, I don’t get what I used to in the openhab log - it used to tell me the rules file had loaded, and complain if it couldn’t parse it (along with errors).

I’m using VSCode, so I get syntax errors that way. But I have rules files that are fine in VSCode, but when I upload them onto my server they don’t run. That’s not “don’t do what I expect”, it’s “none of the rules in that file run at all.” And I’m reduced to commenting out things bit by bit until I find the offending line, which I fix, and then the file is all fine.

I’ve been living with that, figuring it was some misconfiguration somewhere. But now that I’m writing quite a few new rules for this automation it’s actually really painful.

Does Openhab still provide parse errors when loading files, or even tell you it loaded a file? What log levels or configuration controls whether it does that? I don’t see anything obvious in log levels, but it’s not clear to me that log levels are even the thing that controls these messages. I’m happy to post that info if it’s useful.

I can also post my specific problem, but I feel I’d be able to self help in solving that if I could just see the parse errors - it’ll no doubt tell me why it didn’t load the rules.

Yes.

I’m not sure what you mean by this. Do you mean touch the file or otherwise modify it?

I think you should see a line from org.openhab.model.core.internal.ModelRepositoryImpl saying “Loading model ‘nameoffile.rules’” for each .rules file.

I don’t remember but I think the rule engine itself logs under org.openhab.core.automation.

First thing to check is whether something has happened to your log4j2 config to suppress logging from those packages. By default they shoud log at the INFO level IO think.

Couldn’t hurt.

Yes, that’s exactly what I mean. I’d assumed perhaps that wasn’t something it did anymore, but when it started getting really annoying I thought harder, and wondered whether it was instead something in my config.

I don’t see anything obviously turning it off:

openhab> log:list
Logger                                             │ Level
───────────────────────────────────────────────────┼──────
ROOT                                               │ WARN
javax.jmdns                                        │ ERROR
javax.mail                                         │ ERROR
openhab.event                                      │ INFO
openhab.event.AddonEvent                           │ ERROR
openhab.event.ChannelDescriptionChangedEvent       │ ERROR
openhab.event.InboxUpdatedEvent                    │ ERROR
openhab.event.ItemAddedEvent                       │ ERROR
openhab.event.ItemChannelLinkAddedEvent            │ ERROR
openhab.event.ItemChannelLinkRemovedEvent          │ ERROR
openhab.event.ItemRemovedEvent                     │ ERROR
openhab.event.ItemStateEvent                       │ ERROR
openhab.event.RuleAddedEvent                       │ ERROR
openhab.event.RuleRemovedEvent                     │ ERROR
openhab.event.RuleStatusInfoEvent                  │ ERROR
openhab.event.StartlevelEvent                      │ ERROR
openhab.event.ThingAddedEvent                      │ ERROR
openhab.event.ThingRemovedEvent                    │ ERROR
openhab.event.ThingStatusInfoEvent                 │ ERROR
openhab.event.ThingUpdatedEvent                    │ ERROR
org.apache                                         │ ERROR
org.apache.cxf.jaxrs.sse.SseEventSinkImpl          │ ERROR
org.apache.karaf.jaas.modules.audit                │ INFO
org.apache.karaf.kar.internal.KarServiceImpl       │ ERROR
org.apache.karaf.shell.ssh.SshUtils                │ ERROR
org.apache.karaf.shell.support                     │ OFF
org.apache.sshd                                    │ ERROR
org.eclipse.lsp4j                                  │ OFF
org.jupnp                                          │ ERROR
org.openhab                                        │ ERROR
org.openhab.automation.script                      │ TRACE
org.openhab.automation.script.watering             │ DEBUG
org.openhab.binding.homematic                      │ INFO
org.openhab.core.model.script                      │ INFO
org.openhab.core.model.script.Lights               │ DEBUG
org.openhab.core.model.script.boiler               │ INFO
org.openhab.core.model.script.lights               │ DEBUG
org.openhab.core.model.script.watering             │ INFO
org.openhab.core.model.script.zones                │ INFO
org.openhab.model.script.zones                     │ DEBUG
org.openhab.ui                                     │ ERROR
org.openhab.ui.basic                               │ ERROR
org.ops4j.pax.url.mvn.internal.AetherBasedResolver │ ERROR
org.ops4j.pax.web.pax-web-runtime                  │ OFF
su.litvak.chromecast.api.v2.Channel                │ ERROR

Presumably first step would be to create a config for org.openhab.model.core.internal.ModelRepositoryImpl at the INFO level, and then touch some files and see what I get?

I’ve tried that, and touched a file, and get no result. I sometimes find I need to restart openhab to get logging changes to take effect - is that expected? I’m not at home today/tomorrow, and don’t want to restart my production openhab whilst I’m not there - I have some remote access but not full access, and if it doesn’t restart my significant other loses all heating. It’s still winter here, so that could cause me big issues. :-).

Would we expect a restart to be needed?

I did two flavours of the log setting - one for org.openhab.model.core.internal.ModelRepositoryImpl and one for org.openhab.core.model.internal.ModelRepositoryImpl (pattern matching the other settings around it).

openhab> log:list
Logger                                              │ Level
────────────────────────────────────────────────────┼──────
ROOT                                                │ WARN
javax.jmdns                                         │ ERROR
javax.mail                                          │ ERROR
openhab.event                                       │ INFO
openhab.event.AddonEvent                            │ ERROR
openhab.event.ChannelDescriptionChangedEvent        │ ERROR
openhab.event.InboxUpdatedEvent                     │ ERROR
openhab.event.ItemAddedEvent                        │ ERROR
openhab.event.ItemChannelLinkAddedEvent             │ ERROR
openhab.event.ItemChannelLinkRemovedEvent           │ ERROR
openhab.event.ItemRemovedEvent                      │ ERROR
openhab.event.ItemStateEvent                        │ ERROR
openhab.event.RuleAddedEvent                        │ ERROR
openhab.event.RuleRemovedEvent                      │ ERROR
openhab.event.RuleStatusInfoEvent                   │ ERROR
openhab.event.StartlevelEvent                       │ ERROR
openhab.event.ThingAddedEvent                       │ ERROR
openhab.event.ThingRemovedEvent                     │ ERROR
openhab.event.ThingStatusInfoEvent                  │ ERROR
openhab.event.ThingUpdatedEvent                     │ ERROR
org.apache                                          │ ERROR
org.apache.cxf.jaxrs.sse.SseEventSinkImpl           │ ERROR
org.apache.karaf.jaas.modules.audit                 │ INFO
org.apache.karaf.kar.internal.KarServiceImpl        │ ERROR
org.apache.karaf.shell.ssh.SshUtils                 │ ERROR
org.apache.karaf.shell.support                      │ OFF
org.apache.sshd                                     │ ERROR
org.eclipse.lsp4j                                   │ OFF
org.jupnp                                           │ ERROR
org.openhab                                         │ ERROR
org.openhab.automation.script                       │ TRACE
org.openhab.automation.script.watering              │ DEBUG
org.openhab.binding.homematic                       │ INFO
org.openhab.core.model.internal.ModelRepositoryImpl │ INFO
org.openhab.core.model.script                       │ INFO
org.openhab.core.model.script.Lights                │ DEBUG
org.openhab.core.model.script.boiler                │ INFO
org.openhab.core.model.script.lights                │ DEBUG
org.openhab.core.model.script.watering              │ INFO
org.openhab.core.model.script.zones                 │ INFO
org.openhab.model.core.internal.ModelRepositoryImpl │ INFO
org.openhab.model.script.zones                      │ DEBUG
org.openhab.ui                                      │ ERROR
org.openhab.ui.basic                                │ ERROR
org.ops4j.pax.url.mvn.internal.AetherBasedResolver  │ ERROR
org.ops4j.pax.web.pax-web-runtime                   │ OFF
su.litvak.chromecast.api.v2.Channel                 │ ERROR

Now that I have the keyword to use, I see someone with a similar issue in the past, which seems unresolved. Log "loading model" [el.core.internal.ModelRepositoryImpl] is gone?

The default level for the root org.openhab logger is INFO in the standard out-of-the-box config. It there isn’t a explicit override under that that means any logger name starting with org.openhab only logs errors.

I’d recommend setting that back to the default level of INFO and then your scripts and the ModelRepositoryImpl and everything else you might be missing will appear.

Aha. Don’t recall ever changing that.

I made the change, but still no logs. I cycled the openhab service, and that seems to have led to it starting to log.

I do get errors on an unrelated file (which I’ve fixed), but this particular file is not running, but also not giving errors. Sigh.

openhab> log:list
Logger                                              │ Level
────────────────────────────────────────────────────┼──────
ROOT                                                │ WARN
javax.jmdns                                         │ ERROR
javax.mail                                          │ ERROR
openhab.event                                       │ INFO
openhab.event.AddonEvent                            │ ERROR
openhab.event.ChannelDescriptionChangedEvent        │ ERROR
openhab.event.InboxUpdatedEvent                     │ ERROR
openhab.event.ItemAddedEvent                        │ ERROR
openhab.event.ItemChannelLinkAddedEvent             │ ERROR
openhab.event.ItemChannelLinkRemovedEvent           │ ERROR
openhab.event.ItemRemovedEvent                      │ ERROR
openhab.event.ItemStateEvent                        │ ERROR
openhab.event.RuleAddedEvent                        │ ERROR
openhab.event.RuleRemovedEvent                      │ ERROR
openhab.event.RuleStatusInfoEvent                   │ ERROR
openhab.event.StartlevelEvent                       │ ERROR
openhab.event.ThingAddedEvent                       │ ERROR
openhab.event.ThingRemovedEvent                     │ ERROR
openhab.event.ThingStatusInfoEvent                  │ ERROR
openhab.event.ThingUpdatedEvent                     │ ERROR
org.apache                                          │ ERROR
org.apache.cxf.jaxrs.sse.SseEventSinkImpl           │ ERROR
org.apache.karaf.jaas.modules.audit                 │ INFO
org.apache.karaf.kar.internal.KarServiceImpl        │ ERROR
org.apache.karaf.shell.ssh.SshUtils                 │ ERROR
org.apache.karaf.shell.support                      │ OFF
org.apache.sshd                                     │ ERROR
org.eclipse.lsp4j                                   │ OFF
org.jupnp                                           │ ERROR
org.openhab                                         │ INFO
org.openhab.automation.script                       │ TRACE
org.openhab.automation.script.watering              │ DEBUG
org.openhab.binding.homematic                       │ INFO
org.openhab.core.model.internal.ModelRepositoryImpl │ INFO
org.openhab.core.model.script                       │ INFO
org.openhab.core.model.script.Lights                │ DEBUG
org.openhab.core.model.script.boiler                │ INFO
org.openhab.core.model.script.lights                │ DEBUG
org.openhab.core.model.script.watering              │ INFO
org.openhab.core.model.script.zones                 │ INFO
org.openhab.model.core.internal.ModelRepositoryImpl │ INFO
org.openhab.model.script.zones                      │ DEBUG
org.openhab.ui                                      │ ERROR
org.openhab.ui.basic                                │ ERROR
org.ops4j.pax.url.mvn.internal.AetherBasedResolver  │ ERROR
org.ops4j.pax.web.pax-web-runtime                   │ OFF
su.litvak.chromecast.api.v2.Channel                 │ ERROR

OK, so we’ll shift to talking about the exact file I have, and the exact behaviour.

The space I’m in is using a set of Shelly relays to control outdoor lights. These used to be hardwired sensor lights, I’m bypassing the sensors (that are mostly broken) to make them fixed lights, then using Shelly relays to control them. I’m then going to use a set of Shelly Motion sensors to turn them on and off. I have about 10 outdoor lights, I have at the moment 3 motion sensors. The sensors turn on different configurations of lights.

I’ve stripped the logic back to the minimum that gives me the problem - so this isn’t the full logic obviously, but is enough to deal with the specific issue I have. The general plan here is that each motion sensor will call a common function, that common function will use HashMaps for the configuration to determine which lights to turn on and for how long.

import java.util.HashMap
import java.util.List

// Static Config

// list of timers for motion sensors that have been triggered
val HashMap< String, Timer> MotionTimers = newHashMap()

// list of lights that were turned on by each sensor - only turn off lights we turned on (otherwise when you walk past a sensor it will cause lights to turn off)
val HashMap< String, List<String>> MotionTimerLights = newHashMap()

// list of times the motion sensor has been triggered

val HashMap< String, List<String>> MotionTriggereds = newHashMap(
  "MotionBoot"     -> (newArrayList("")),
  "MotionKitchen"  -> (newArrayList("")),
  "MotionDining"   -> (newArrayList("")),
  "MotionStudy"    -> (newArrayList("")),
  "MotionDaybed"   -> (newArrayList(""))
)

// list of timers for motion sensors that have lowered sensitivity
val HashMap< String, Timer> MotionSensitivityTimers = newHashMap()

val Procedures$Procedure1<String> ProcessMotion = [ 
  String motionName | 
  logDebug( 'motion', 'Processing motion event for ' + motionName )
]

rule "Shelly motion boot triggered"
when
  Item swMotionBootActive changed to ON 
then
  logDebug( 'motion', 'boot motion detected')
  ProcessMotion.apply( "MotionBoot" )
end

The problem I currently have is that with this logic when I trigger the swBootMotionActive I get precisely nothing in the log.

If I comment out entirely the procedure “ProcessMotion” I get errors in the log:

import java.util.HashMap
import java.util.List

// Static Config

// list of timers for motion sensors that have been triggered
val HashMap< String, Timer> MotionTimers = newHashMap()

// list of lights that were turned on by each sensor - only turn off lights we turned on (otherwise when you walk past a sensor it will cause lights to turn off)
val HashMap< String, List<String>> MotionTimerLights = newHashMap()

// list of times the motion sensor has been triggered

val HashMap< String, List<String>> MotionTriggereds = newHashMap(
  "MotionBoot"     -> (newArrayList("")),
  "MotionKitchen"  -> (newArrayList("")),
  "MotionDining"   -> (newArrayList("")),
  "MotionStudy"    -> (newArrayList("")),
  "MotionDaybed"   -> (newArrayList(""))
)

// list of timers for motion sensors that have lowered sensitivity
val HashMap< String, Timer> MotionSensitivityTimers = newHashMap()

/*
val Procedures$Procedure1<String> ProcessMotion = [
  String motionName |
  logDebug( 'motion', 'Processing motion event for ' + motionName )
]
*/

rule "Shelly motion boot triggered"
when
  Item swMotionBootActive changed to ON
then
  logDebug( 'motion', 'boot motion detected')
  ProcessMotion.apply( "MotionBoot" )
end

When I trigger the switch I get:

2023-08-23 07:40:45.052 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'motion-1' failed: The name 'ProcessMotion' cannot be resolved to an item or type; line 37, column 3, length 13 in motion

Which is exactly what I’d expect, as the procedure is now undefined.

Ah, crap. Now I see the problem. I haven’t set debug logging for this file. So when it works I get nothing. That was a good waste of a couple hours. Sigh.

This is a sure sign you’ve outgrown Rules DSL.

In Rules DSL don’t force the type. It causes way more problems than it solves. The only time you need to specify the type is when you initialize your variable to null and the parser cannot get the type from that first assignment.

That’s not your problem but it’s something that likely will bite you at some point.

Sometimes you just need to type it out to see the problem. Glad you got it working.

If you ever want help making this code hurt my head less post the full thing and I can maybe help. With what little has been printed, you might be able to use the Scene Control Suit rule templates or Scenes (new in OH 4) to greatly simplify it.

That’s very interesting. I have a plan for how I’m going to do this, but it does really mean I’m torturing the DSL quite a bit - with a lot of static state for configuration. And the Shellys use triggers for some things, which then breaks a lot of generalisation I might otherwise do.

As an example, this is the core logic for the lights when turned on by the light switches on the wall (rather than the motion sensors). It uses momentary switches to turn them on, and distinguishes semantics for a short press, double press, triple press etc.

I know you have views on how to use Rules DSL to do some of this stuff, but I haven’t really found a way to push the configuration into items in a sensible way. I could perhaps do it with strings that I parse to get the list of lights out, and a group of strings to represent the full list. But the use of triggers to start the whole chain kind of breaks the pattern of using the name of the triggering element as an index into that. But it feels like that’s just a different sort of torturing, and it splits the static configuration away from the rules - I still end up with a generic procedure that would need to be triggered whenever a switch is pressed, it’d just use a group of strings to get at the configuration instead of using a hash map. I find the hash map easier to keep up to date than I think I’d find a group of strings to be.

I do note that at some point in the past I started explicitly typing everything, as you say. I think one of the openhab versions things broke if I didn’t, and so I started doing it and have followed the pattern since. I also am fully qualifying some names that probably don’t need to be, again at some point in the past it was breaking but I think I noticed in one of the examples I was looking at that I don’t have to do that any more.

import java.util.HashMap
import java.util.List

// Static Config

val HashMap< String, List<String>> SinglePress = newHashMap(
  "SensorDaybed"  -> (newArrayList ("SensorDaybed", "SensorDining")),
  "SensorStudy"   -> (newArrayList ("SensorStudy", "SensorLaundry", "SensorBoot")),
  "SensorLaundry" -> (newArrayList ("SensorLaundry", "SensorStudy", "SensorBoot")),
  "SensorAtrium"  -> (newArrayList ("SensorAtrium")),
  "SensorBoot"    -> (newArrayList ("SensorBoot", "SensorKitchen", "SensorGarage", "SensorDining")),
  "SensorGarage"  -> (newArrayList ("SensorGarage", "SensorKitchen", "SensorBoot", "SensorDining")),
  "SensorKitchen" -> (newArrayList ("SensorKitchen", "SensorGarage", "SensorDining")),
  "SensorDining"  -> (newArrayList ("SensorDining", "SensorKitchen", "SensorGarage", "SensorDaybed")),
  "FloodLounge"   -> (newArrayList ("FloodLounge")),
  "FloodMaster"   -> (newArrayList ("FloodMaster")),
  "FloodTractor"  -> (newArrayList ("FloodTractor")),
  "LightGarage"   -> (newArrayList ("LightGarage")),
  "LightBootDoor" -> (newArrayList ("LightBootDoor", "LightBootHall")),
  "LightBootHall" -> (newArrayList ("LightBootHall", "LightBootDoor")),
  "Triple"        -> (newArrayList ("SensorDaybed", "SensorDining", "SensorStudy", "SensorAtrium", "SensorLaundry", "SensorBoot", "SensorGarage", "SensorKitchen", "FloodLounge", "FloodDaybed", "FloodTractor"))
)

val HashMap< String, List<String>> DoublePress = newHashMap(
  "SensorDaybed"  -> (newArrayList ("SensorDaybed", "SensorDining", "FloodLounge", "FloodDaybed", "FloodTractor")),
  "SensorStudy"   -> (newArrayList ("SensorStudy", "SensorLaundry", "SensorBoot", "SensorDaybed", "SensorDining", "FloodLounge", "FloodMaster", "FloodTractor")),
  "SensorLaundry" -> (newArrayList ("SensorLaundry", "SensorStudy", "SensorBoot", "SensorAtrium")),
  "SensorAtrium"  -> (newArrayList ("SensorAtrium", "SensorStudy", "SensorLaundry", "SensorBoot")),
  "SensorBoot"    -> (newArrayList ("SensorBoot", "SensorStudy", "SensorLaundry", "SensorKitchen", "SensorGarage", "SensorDining", "FloodLounge", "FloodDaybed", "FloodTractor")),
  "SensorGarage"  -> (newArrayList ("SensorGarage", "SensorKitchen", "SensorDining", "SensorBoot", "LightBootDoor")),
  "SensorKitchen" -> (newArrayList ("SensorKitchen", "SensorGarage", "SensorDining", "FloodLounge")),
  "SensorDining"  -> (newArrayList ("SensorKitchen", "SensorGarage", "SensorDining", "SensorDaybed", "FloodLounge", "FloodMaster", "FloodTractor")),
  "FloodLounge"   -> (newArrayList ("FloodLounge", "FloodMaster", "FloodTractor", "SensorDining", "SensorDaybed")),
  "FloodMaster"   -> (newArrayList ("SensorDaybed", "SensorDining", "SensorStudy", "SensorLaundry", "SensorAtrium", "SensorBoot", "SensorGarage", "SensorKitchen", "FloodLounge", "FloodMaster", "FloodTractor")),
  "FloodTractor"  -> (newArrayList ("FloodTractor", "FloodMaster", "FloodLounge", "SensorDining", "SensorDaybed")),
  "LightGarage"   -> (newArrayList ("LightGarage", "SensorKitchen", "SensorGarage", "SensorBoot", "LightBootDoor")),
  "LightBootDoor" -> (newArrayList ("LightBootDoor", "LightBootHall", "SensorBoot", "SensorKitchen", "SensorGarage")),
  "LightBootHall" -> (newArrayList ("LightBootHall", "LightBootDoor", "SensorBoot", "SensorKitchen", "SensorGarage"))
)

val HashMap< String, Number> LightTime = newHashMap(
  "SensorDaybed"  -> 10*2,
  "SensorStudy"   -> 10*2,
  "SensorLaundry" -> 10*2,
  "SensorAtrium"  -> 5*2,
  "SensorBoot"    -> 5*2,
  "SensorGarage"  -> 10*2,
  "SensorKitchen" -> 10*2,
  "SensorDining"  -> 5*2,
  "FloodLounge"   -> 15*2,
  "FloodMaster"  -> 5*2,
  "FloodTractor"  -> 5*2,
  "LightGarage"   -> 5*2,
  "LightBootDoor" -> 10*2,
  "LightBootHall" -> 10*2,
  "Triple"        -> 10*2
)

val HashMap< String, Timer> LightTimers = newHashMap()


/*
Logic flow for button presses

Light already off:
  Short - turn it and associated short lights on, set a timer for the specified length
  Double - turn it and associated more lights on, set a timer for the specified length
  Long - turn it and associated short lights on, no timer
  Short-Long - turn it and associated long lights on, no timer
  Triple - turn all outside lights on, set a timer

Light already on:
  Short - turn it and associated lights off
  Double - turn on the more lights, reset the timer
  Long - cancel timer, leave lights on
  Short-Long - cancel timer, turn more lights on
  Triple - turn all outside lights on, reset timer

End of timer:
  Turn off all lights that may have been turned on by this switch (i.e. the double press lights)

Timers need to be tied to the button that initiated them, so we can cancel them again. That implies a 
hashmap that holds them all.

*/

val org.eclipse.xtext.xbase.lib.Procedures$Procedure6<String, String, HashMap<String, List<String>>, HashMap<String, List<String>>, HashMap<String, Number>, HashMap<String, Timer> > ProcessButtonPress = [ 
  String lightName,
  String sEvent,
  HashMap< String, List<String>> SinglePress,
  HashMap< String, List<String>> DoublePress,
  HashMap<String, Number> LightTime,
  HashMap<String, Timer> LightTimers |
  logDebug( 'lights', 'Processing button press for ' + lightName + ' event was ' + sEvent )

  /* set a light timer to turn lights off - sub function */
  val org.eclipse.xtext.xbase.lib.Procedures$Procedure4<String, List<String>, HashMap<String, Number>, HashMap<String, Timer> > SetLightTimer = [ 
    String lightName,
    List<String> LightList,
    HashMap<String, Number> LightTime,
    HashMap<String, Timer> LightTimers |

    logDebug( 'lights', 'Setting a timer for ' + lightName)
    LightTimers.put( lightName, createTimer(new DateTimeType(ZonedDateTime.now()).getZonedDateTime().plusSeconds(LightTime.get(lightName))) [|{
      // at end of timer, turn off all lights that may have been turned on
      logDebug( 'lights', 'Timer turning off all lights for ' + lightName)
      strLightLastUpdate.postUpdate("Timer turning all lights off for " + lightName)
      LightList.forEach[ light |
        var SwitchItem swLight = LightOutputs.members.filter( li | li.name.startsWith( 'sw' + light ) ).last as SwitchItem
        swLight.sendCommand( OFF )
      ]
      LightTimers.put( lightName, null )
    }])
  ]

  var SwitchItem swOutput = LightOutputs.members.filter( ou | ou.name.startsWith( 'sw' + SinglePress.get(lightName).get(0))).last as SwitchItem
  logDebug( 'lights', 'Output switch is ' + swOutput )

  // take action depending on what sort of button press we got
  switch sEvent {
    case 'SHORT_PRESSED': {
      // If the light is off, turn on the small set of lights and set a timer. If the light is on, turn all possibly associated lights off.
      logDebug( 'lights', 'We got a short press')
      if( swOutput.state == OFF){
        strLightLastUpdate.postUpdate("Turning timer lights on for " + lightName)
        SinglePress.get(lightName).forEach [ light |
          var SwitchItem swLight = LightOutputs.members.filter( li | li.name.startsWith( 'sw' + light ) ).last as SwitchItem
          swLight.sendCommand( ON )
        ]
       SetLightTimer.apply( lightName, DoublePress.get(lightName), LightTime, LightTimers )
      } else {  // light is currently on, turn off all lights that may have been turned on, and cancel timer
        logDebug( 'lights', 'Light currently on, turning off all double press lights for ' + lightName)
        strLightLastUpdate.postUpdate("Switch turning all lights off for " + lightName)
        DoublePress.get(lightName).forEach[ light |
          var SwitchItem swLight = LightOutputs.members.filter( li | li.name.startsWith( 'sw' + light ) ).last as SwitchItem
          swLight.sendCommand( OFF )
        ]
        if( LightTimers.get(lightName) !== null ){
          LightTimers.get(lightName).cancel()
          LightTimers.put(lightName, null) 
        } else {
          logInfo( 'lights', 'Turning off light ' + lightName + ' and there was no timer, which could be a problem')
        }
      }
    }

    case 'DOUBLE_PRESSED': {
      // cancel a timer if there is one, then turn on all the lights and set a timer
      logDebug( 'lights', 'We got a double press')
      if( swOutput.state == ON){
        if( LightTimers.get(lightName) !== null ){
          LightTimers.get(lightName).cancel()
          LightTimers.put(lightName, null) 
        } else {
          logInfo( 'lights', 'Double press for light thats already on ' + lightName + ' and there was no timer, which could be a problem')
        }
      }
      strLightLastUpdate.postUpdate("Turning timer extra lights on for " + lightName)
      DoublePress.get(lightName).forEach[ light |
        var SwitchItem swLight = LightOutputs.members.filter( li | li.name.startsWith( 'sw' + light ) ).last as SwitchItem
        swLight.sendCommand( ON )
      ]
      SetLightTimer.apply( lightName, DoublePress.get(lightName), LightTime, LightTimers )
    }

    case 'LONG_PRESSED': {
      // cancel a timer if there is one, turn on the small set of lights. If the large set of lights are on, they'll stay on
      logDebug( 'lights', 'We got a long press')
      if( swOutput.state == ON){
        if( LightTimers.get(lightName) !== null ){
          LightTimers.get(lightName).cancel()
          LightTimers.put(lightName, null) 
        } else {
          logInfo( 'lights', 'Long press for light thats already on ' + lightName + ' and there was no timer, which could be a problem')
        }
      }
      strLightLastUpdate.postUpdate("Turning lights on without timer for " + lightName + " if more lights were already on, they'll stay on" )
      SinglePress.get(lightName).forEach[ light |
        var SwitchItem swLight = LightOutputs.members.filter( li | li.name.startsWith( 'sw' + light ) ).last as SwitchItem
        swLight.sendCommand( ON )
      ]
    }

    case 'SHORT_LONG_PRESSED', case "LONG_SHORT_PRESSED": {
      // cancel a timer if there is one, turn on the small set of lights.
      // seems that this isn't a real thing though - shelly 1/2 don't support long-short or short-long
      logDebug( 'lights', 'We got a short-long press')
      if( swOutput.state == ON){
        if( LightTimers.get(lightName) !== null ){
          LightTimers.get(lightName).cancel()
          LightTimers.put(lightName, null) 
        } else {
          logInfo( 'lights', 'Short-long press for light thats already on ' + lightName + ' and there was no timer, which could be a problem')
        }
      }
      strLightLastUpdate.postUpdate("Turning extra lights on without timer for " + lightName)
      DoublePress.get(lightName).forEach[ light |
        var SwitchItem swLight = LightOutputs.members.filter( li | li.name.startsWith( 'sw' + light ) ).last as SwitchItem
        swLight.sendCommand( ON )
      ]
    }

    case 'TRIPLE_PRESSED': {
      // cancel a timer if there is one, turn on all outside lights, set a timer for 10 minutes.
      logDebug( 'lights', 'We got a triple press')
      if( swOutput.state == ON){
        if( LightTimers.get(lightName) !== null ){
          LightTimers.get(lightName).cancel()
          LightTimers.put(lightName, null) 
        } else {
          logInfo( 'lights', 'Triple press for light thats already on ' + lightName + ' and there was no timer, which could be a problem')
        }
      }
      strLightLastUpdate.postUpdate("Turning every outside light on with timer for " + lightName)
      SinglePress.get("Triple").forEach[ light |
        var SwitchItem swLight = LightOutputs.members.filter( li | li.name.startsWith( 'sw' + light ) ).last as SwitchItem
        swLight.sendCommand( ON )
      ]
      SetLightTimer.apply( lightName, SinglePress.get("Triple"), LightTime, LightTimers )
    }

    default: {
      logInfo( 'lights', 'Received unhandled event ' + sEvent + ' on item ' + lightName )
    }
  }
]


rule "Shelly button flood lounge triggered"
when
  Channel "shelly:shellyplus1:a8032aba16f8:relay#button" triggered 
then
  ProcessButtonPress.apply( "FloodLounge", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end

rule "Shelly button sensor dining triggered"
when
  Channel "shelly:shellyplus1:441793aa6bf8:relay#button" triggered 
then
  ProcessButtonPress.apply( "SensorDining", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end

rule "Shelly button sensor study triggered"
when
  Channel "shelly:shellyplus1:a8032aba1704:relay#button" triggered 
then
  ProcessButtonPress.apply( "SensorStudy", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end

rule "Shelly button sensor laundry triggered"
when
  Channel "shelly:shellyplus1:441793a950cc:relay#button" triggered 
then
  ProcessButtonPress.apply( "SensorLaundry", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end

rule "Shelly button sensor atrium triggered"
when
  Channel "shelly:shellyplus1:441793a86d40:relay#button" triggered 
then
  ProcessButtonPress.apply( "SensorAtrium", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end

rule "Shelly button sensor boot triggered"
when
  Channel "shelly:shellyplus1:441793a94fe8:relay#button" triggered 
then
  ProcessButtonPress.apply( "SensorBoot", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end

rule "Shelly button sensor kitchen triggered"
when
  Channel "shelly:shellyplus1:083af2028610:relay#button" triggered 
then
  ProcessButtonPress.apply( "SensorKitchen", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end

rule "Shelly button flood tractor triggered"
when
  Channel "shelly:shellyplus1:441793aa9040:relay#button" triggered 
then
  ProcessButtonPress.apply( "FloodTractor", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end

rule "Shelly button light boot door triggered"
when
  Channel "shelly:shellyplus1:083af202a350:relay#button" triggered 
then
  ProcessButtonPress.apply( "LightBootDoor", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end

rule "Shelly button light boot hall triggered"
when
  Channel "shelly:shellyplus1:083af2029730:relay#button" triggered 
then
  ProcessButtonPress.apply( "LightBootHall", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end

rule "Shelly button sensor daybed triggered"
when
  Channel "shelly:shellyplus2pm-relay:80646fdbe00c:relay1#button" triggered 
then
  ProcessButtonPress.apply( "SensorDaybed", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end

rule "Shelly button flood master triggered"
when
  Channel "shelly:shellyplus2pm-relay:80646fdbe00c:relay2#button" triggered 
then
  ProcessButtonPress.apply( "FloodMaster", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end

rule "Shelly button sensor garage triggered"
when
  Channel "shelly:shellyplus2pm-relay:80646fdb89e0:relay1#button" triggered 
then
  logDebug( 'lights', 'Garage sensor button pressed' )
  ProcessButtonPress.apply( "SensorGarage", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end

rule "Shelly button light garage triggered"
when
  Channel "shelly:shellyplus2pm-relay:80646fdb89e0:relay2#button" triggered 
then
  ProcessButtonPress.apply( "LightGarage", receivedEvent, SinglePress, DoublePress, LightTime, LightTimers )
end





/*
Logic flow for motion sensors triggering

Light off - turn specified lights on
Light on - extend the timer

As a later enhancement, some logic where it's getting triggered too much - probably turn the sensitivity down
*/

In every rules language except Rules DSL (even Blockly) you can access Item metadata and have rules that call other rules.

In managed rules and I think even some of the other files based rules languages there is a generic event trigger you can use to catch all events matching a certain pattern (see Thing Status Reporting [4.0.0.0;4.9.9.9] for and example to catch all Thing status changes).

Your tool box is small with Rules DSL. That’s the real problem here and why I say you’ve probably out grown it.

There are certain operations where Rules DSL fails to determine the type correctly. But you’ll encounter fewer problems if you cast when you hit those than to force the type of the variables.

Meh, that doesn’t bother me. It shouldn’t matter either way. But forcing type can add minutes to how long it takes to load your rules, even on a fast machine, in some circumstances.

And it’s not even reliable to add the type to a variable. Just because it’s var String myString doesn’t mean you can’t assign a BigDecimal to it later and there won’t be a single complaint.

This could probably be captured using Item tags. You could then use the Item registry to pull all Items with a given tag. You could also use Groups but that’s a lot of Groups I think. You can get at the ItemRegistry in Rules DSL.

This is where I would use Item metadata. You can create a namespace, let’s call it “lights”. The value would be set to one of the keys you are using in your hashmaps and you can add a config for the light time and single press/double press. Or maybe put the light Item with the sensor Items. It’s not clear to me yet how the numbers are being use or why the *2.

Much better to use

val ProcessButtonPress = [ String lightName ...

All the types are captured automatically and it’s so much easier to read.

If you use jRuby, they have a lot of timer management stuff built in . If JS Scripting I’ve a library with a TimerMgr which does timer book keeping in cases where you have one timer per Item. One function call handles creation and rescheduleing and all the book keeping and cleanup.

Why not use one rule and the implicit variables to map the event to a “Sensor”?

As an alternative, there is a progfile (it might be on the SmarthomeJ marketplace) that allows you to link an event Channel to an Item. That saves a lot of mapping and it might be an option to apply here.

I’m just typing this in so what I have here is likely going to be error ridden. But what about something like this, as a managed rule since that’s what I use:

uid: lights
label: Lights timers 
description: Turn on and off lights according to a timer
triggers:
  - id: "3"
    label: A Thing Changes Status
    description: Triggers when any Thing changes status
    configuration:
      types: ChannelTriggeredEvent
      payload: ""
      topic: openhab/channels/*/triggered
      source: "shelly"
    type: core.GenericEventTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: |
        var {TimerMgr} = require('openhab_rules_tools');


        var parsed = JSON.parse(event.payload); // the event comes as JSON, log it out to make sure you get the right values and put them into "triggerEvent" below

        var triggerEvent = {};

        triggerEvent['thingID'] = event.topic.split('/')[2];

        triggerEvent['channel'] = parsed[0].channel

        triggerEvent['event'] = parsed[0].event

        var timers = cache.private.get('timers', () => TimerMgr()); // pull the TimerMgr from the cache or create one if there isn't one

        // use a tag here so we can pull the switch by tag instead of name
        var swOutput = items.getItemsByTag(triggerEvent['channel'])[0];

        // get all the Items with the this channel in it's lights metadata, could use tags but we need metadata anyway
        // Could have yet another mapping instead of using the Channel name but this makes the code simpler
        var lightsItems = items.getAllItems().filter( item =>  item.getMetadata('lights') !== undefined);

        var lightsToControl = lightsItems.filter( item => item.getMetadata('lights).value == triggerEvent['channel']);

        lightsToControl.forEach( light => {
            console.debug('Controlling light ' + light + '  based on event ' + triggeringEvent['event']);
            const md = light.getMetadata('lights').configuration['time'];

            // break early if this light doesn't respond to the event
            if(md.configuration['SINGLE'] === undefined && md.configuration['DOUBLE'] === undefined && md.configuration['TRIPLE') === undefined) {
              break;
            } 
            else if(triggeringEvent.event == 'LONG_PRESS' 
                       && (md.configuration['SINGLE'] === undefined || md.configuration['SINGLE'] != 'true')) {
              break;
            }
            else if((triggeringEvent.event == 'SHORT_LONG_PRESS' || triggeringEvent.event == 'LONG_SHORT_PRESSED')
                       && (md.configuration['DOUBLE'] === undefined || md.configuration['DOUBLE'] != 'true')) {
              break;
            }
           else if(triggeringEvent.event == 'TRIPLE_PRESS' 
                        && (md.configuration['TRIPLE'] === undefined || md.configuration['TRIPLE'] != 'true')) {
              break;
           }
      

            // always cancel the timer if the light is on, they will be recreated if the light turns on
            // single press
            if(swOutput.state == 'ON') {
                timers.cancel('light');
            }

            switch(triggeringEvent['event']) {
              case 'SHORT_PRESSED':
                if(swOutput.state == 'ON') {
                  light.sendCommand('ON');
                  timers.check(light.name, md.configuration['time'], () => {
                    light.sendCommand('OFF');
                  }
               }
               else {
                 light.sendCommand('OFF');
                 timers.cancel(light.name);
               }
               break;
              case  'DOUBLE_PRESSED': 
              case 'TRIPLE_PRESSED':
                  light.sendCommand('ON');
                  timers.check(light.name, light.getMetadata('lights').configuration['time'], ()=> light.sendCommand('OFF'));
                break;
              case 'LONG_PRESSED':
              case 'SHORT_LONG_PRESSED':
              case 'LONG_SHORT_PRESSED':
                light.sendCommand('ON');
                break;
                
        }
    type: script.ScriptAction

I believe the generic trigger above will catch and trigger the rule on all ChannelTrigger events that include “shelly” somewhere in the event. The event from this trigger comes over as JSON which can be parsed and the data extracted. You can use the name of the Channel as the mapping between Items and those Item that respond to the switch.

I use my TimerMgr library to handle all the timer stuff. All I have to do is call check to create the timer and cancel to cancel it. We use the cache to save the timers from one run to the next.

Instead of using the Item name to get the swOutput Item, we use a tag that matches the channel that triggered the rule.

Next we get all the Item with “light” metadata defined. We further reduce the list to just those whose “light” metadata values match the Channel ID. That leaves us with the list of Items that respond to only this Channel so we loop through them. We could continue using filters but I’ll switch so you can see another approach.

Now we loop through the Items and skip over any Item that doesn’t have metadata indicating that light responds to this specific event from the Channel.

I notice that in all but SINGLE_PRESS you cancel the timers when swOutput is ON, and we recreate the timers in SINGLE_PRESS anyway in that case so we will just always cancel the timers. That saves a lot of repeated code.

Now we have the switch statement. But because we’ve already narrowed down to the case where we know we need to command the Item, and we’ve already canceled the timers that need to be canceled, all we need to do command the Item and set the timer based on the event.

The Item metadata would look something like this:

.items files
lights="shelly:shellyplus2pm-relay:80646fdb89e0:relay2#button"["DOUBLE"='true', "TRIPLE"='true']

ui
value: "shelly:shellyplus2pm-relay:80646fdb89e0:relay2#button"
config:
  DOUBLE: 'true'
  TRIPLE: 'true'

That’s the metadata you’d use for a given light that responds to DOUBLE or TRIPLE clicks on that shell button.

I tried to show a few different things above that you can leverage that do not exist in Rules DSL.

If I were to use Scenes, you can create an ON and OFF scene for each “room” and button press event (which could be a number of Scenes). Then you’d need a mapping between the button Channel ID and the Scene number (maybe still hard coded as a map perhaps). But once you have that mapping the code just becomes:

// define mappings and timings

var sceneId = sceneName + '-' + triggeringEvent.event;
switch(triggeringEvent['event']) {
  case 'SHORT_PRESSED':
    if(swOutput.state == 'ON') {
      rules.runRule(sceneId+'-ON');
      timers.check(sceneId, timerTime, () => {
        rules.runRule(sceneId+'-OFF');
      }
      else {
        rules.runRule(sceneId+'-OFF');
        timers.cancel(sceneId);
      }
      break;
...                
}

Which lights to turn ON and OFF get captured in the Scenes and you just need to figure out which scene to call and how long to set the timer for.

These are just ideas. There is almost certainly a bug or two in the above. The intent is to show how you can use some of the new tools and features you don’t have access to in Rules DSL to solve these sorts of problems.

Thanks for that, it’s a lot to consume but lots of good ideas in there.

I keep looking at the JSR languages, but I have lots of inertia. And then I’m torn as to which language to use, since I can programme in about 20 languages, but some of them I’m rusty and some of them I don’t like. So my logic tends to go:

  • I wrote a bunch of Javascript back in the day. Unless I’m writing it frequently the syntax hurts my eyes when I read it, it’s a really ugly language. So, yeah, but one of my least preferred
  • I can write typescript, and it’s prettier. But it’s been a while, at least you get some stronger typing
  • I wrote Ruby for a while when I was doing Ruby on Rails (with Javascript front end and Angular). I like Ruby, but it’s kind of niche these days
  • Python in theory is probably a good choice, but I’ve actually never written Python. It doesn’t look hard, but there’s learning curve.

And then I end up doing “just enough” in Rules DSL, usually cursing it all the way because it’s just so damn finicky to work with.

Perhaps it’s time to finally bite the bullet and make the change. I have a lot of rules that I’d need to migrate over time, but a lot of them could do with a rethink and a rearchitect anyway. A bunch of them date back to openHab one and I’ve patched and cobbled them together through the versions, which means they’re pretty ugly compared to what I’d write today if I started again.

Do you have any particular thoughts on which language gives best access to the toolset / integration into openhab, has the best tooling and/or debugging? Or are they really all about the same, and it’s just a case of picking a language that I feel like writing in?

On some specific points, I’m trying to cover some corner cases based on how my significant other uses lights. Other than that if I give her three switches she just turns them all on, and probably when I give her short press for a short time and long press for a long time she’ll always just press the long, one of the tricks is overlapping lights. So if I turn on lights down one end of the house they get a timer, if I turn on a different set of lights down the other end of the house they get a different (and overlapping) timer. The problem is I have some logic to let you turn the lights on and leave them on (say you’re out the back in the garden). And then someone may take the dog out the front to go to the toilet, and that’s on a 5 minute timer. But I don’t want the lights out the back to go off when that timer ends. I could perhaps simplify things a lot by just saying that’s not a real requirement, so when any timer ends I could just turn off every light - a lot of the complexity is keeping track of which light was turned on how, and only turning it off when all the things that turned it on go back to off. Hmmm. which gives me some ideas on how better to factor that - perhaps I could remember the events that turned a light on, and then clear those events one by one, that’d be an interesting pattern.

Tags on items would be super useful. I take it they’re not available in Rules DSL? I need to do more reading on them, I won’t have time for that till the weekend.

On the specific *2 on the times…it’ll be *60 in production, its number of minutes but expressed in seconds. Whilst testing I changed it all to *2 so that I didn’t have to wait so long.

JS Scripting, jRuby, Groovy and JRule are the best built in in terms of exposing all the OH APIs. There’s HABApp if you want to go down the Python path but that’s a separate service that runs alongside openHAB and interacts through the REST API.

Note, if you’ve not looked at the other rules languages in a long while a ton has changed in terms of ease of use. In the case of JS Scripting and jRuby the helper library comes with the add-on and they go to great lengths to make interactions with OH itself match the conventions of the language and make it as simple and easy as possible.

Moving from JSR223 Nashorn JavaScript to JS Scripting let me reduce my line count by more than half. Then of course I built that line count right back up again by adding tons of error checking and meaningful errors (at least for my rule templates published to the marketplace). But now my templates are pretty robust and when users run into problems they can usually figure out what they did wrong on their own.

I have neither a love nor hate relationship with any of the languages (none of them are Lisp so :person_shrugging: ). I use JS Scripting for practical reasons. It’s what Blockly compiles to and it was the first to treat UI rules as first class citizens.

You don’t have to move everything all at once. Maybe explore new rules in another rule. Try a few and see which ones speak to you.

I do a simplified version of this to override the lights automation. During the day my lights turn on and off based on how cloudy it is. If a light is manually controlled I set a flag in that Item’s metadata that the light is overridden and to stay where it is until the next time of day.

Tags are one of the few things you can get from Items and if you pull in the ItemRegistry you can get Items based on their tags.

One cool thing in JS Scripting is there is a tool called time.toZDT(). You can pass it almost anything and it will convert it to a ZonedDateTime suitable to schedule a Timer with. So for example, I can set a timer using my TimerMgr with:

timers.check(event.itemName, 1000, ...) // one second from now
timers.check(event.itemName, 'PT1M5S', ...) // one minute five seconds from now
timers.check(event.itemName, items.MyDateTime, ...) // the date time in the state of that Item

My code is so much easier to read just because of that little feature.

1 Like

Thanks for your time on this @rlkoshak, and in general for the time you put into openhab and the forums. It’s super useful. I won’t have time to properly consume it / engage with it till the weekend, and I’m miles behind on my work and at work now, so I perhaps should do some of that. :slight_smile:

You’ll fall in love with jruby. We’ve made the helper library so nice to work with. Check it out!

Regarding your original question, whenever I made changes to a rulesdsl file, I got this in my log:

13:15:52.345 [INFO ] [del.core.internal.ModelRepositoryImpl] - Loading model 'test.rules'

I have this log level set on my system

org.openhab                                        │ INFO

See, it looks really cool.

But, to be honest, when I read it I go “argh, I’d have to change my whole mental model to take advantage of that.” That’s a few weeks invested, not the few hours it’d take me to torture the Rules DSL to do the thing I want to do right now.

Which is kind of the wrong answer. But I don’t have as much time as I did to play with this, because I’m working full time again. When I’m not working full time, then I can probably learn a new syntax and mental model, and replatform all my stuff.

You could do it gradually. Change one rule at a time for the ones you need right now or just for new rules.

I think learning how to do it in jruby is far easier than trying to torture yourself with rulesdsl. It’s also good for your mental health

I’m partway through the journey - I have it working as I wish it to work using the legacy rules DSL. I’m still considering stepping into the new language, and haven’t decided which language would suit me most. It needs to wait until I’m less busy at work.

In case anyone is having similar issues or otherwise comes to this thread looking for help, I’m posting the full rules file that is now working quite well. Key areas of change were:

  • declare most of my procedures as elements within a hash. This is ugly, but it means that I don’t have to keep passing all the procedures into every procedure call - the rules DSL doesn’t deal with globally declared procedures well and if you want one procedure to call another procedure (lambda really I guess) you have to explicitly pass it in. I still kept a couple outside the hash, because the downside of the hash is calling them directly is ugly - and my event based rules call into these directly - so a couple are outside the hash, a few inside the hash, and some kind of live in both places.
  • similarly, declaring my various config data all within one big hash called “StaticData”, which also holds those procedures above. This avoids passing lots of parameters around
  • removing a lot of types from things. Visual Studio Code (actually the DSL I think) complains that things are untyped, so in the past I’d put types on them. But you can just ignore those warnings, and it’s less likely to tie itself in knots and declare your syntax to be invalid. This means casting many things when you need to use them
  • generally cleaning up the code to generalise some concepts better. In particular I didn’t want to turn outside lights on, then walk past a motion sensor, and have the motion sensor turn that light off (I turned it on for a reason). So I wrote a concept of triggers - a light can be turned on by multiple events, it doesn’t turn off until all those triggers clear. But, I do want to turn lights off by a switch different than the one that turned it on…so the trigger concept applies to all timers and motion sensors, but when I turn a light off with a switch I want it to go off now, not wait for the timers
  • other than syntax, the main thing I cannot easily do is generalise the channel calls - the Shellys trigger on a channel for the button presses rather than being a switch event or similar. That means you can’t use a group to handle them as a job lot. There are ways, but in the end it was easier to just enumerate them as a list of rules than it was to mess with that. Somewhere I’d have had to enumerate them anyway, so I’d really just be shifting that from rules to items. It feels better, but it’s not really less work

All good fun, and other than usual syntax messing around, it works pretty cleanly now. The Shellys are a bit more fussy than I might have expected, losing their config and needing restarting. They seem to have settled down now that I’m not changing them, so we’ll see if they’re stable across events like power cuts.

import java.util.HashMap
import java.util.List

// Static Config
val HashMap StaticData = newHashMap(
  "SinglePress" -> newHashMap(
    "SensorDaybed"  -> (newArrayList ("SensorDaybed", "SensorDining")),
    "SensorStudy"   -> (newArrayList ("SensorStudy", "SensorLaundry", "SensorBoot")),
    "SensorLaundry" -> (newArrayList ("SensorLaundry", "SensorStudy", "SensorBoot")),
    "SensorAtrium"  -> (newArrayList ("SensorAtrium")),
    "SensorBoot"    -> (newArrayList ("SensorBoot", "SensorKitchen", "SensorGarage", "SensorDining")),
    "SensorGarage"  -> (newArrayList ("SensorGarage", "SensorKitchen", "SensorBoot", "SensorDining")),
    "SensorKitchen" -> (newArrayList ("SensorKitchen", "SensorGarage", "SensorDining")),
    "SensorDining"  -> (newArrayList ("SensorDining", "SensorKitchen", "SensorGarage", "SensorDaybed")),
    "FloodLounge"   -> (newArrayList ("FloodLounge")),
    "FloodMaster"   -> (newArrayList ("FloodMaster")),
    "FloodTractor"  -> (newArrayList ("FloodTractor")),
    "LightGarage"   -> (newArrayList ("LightGarage")),
    "LightBootDoor" -> (newArrayList ("LightBootDoor", "LightBootHall")),
    "LightBootHall" -> (newArrayList ("LightBootHall", "LightBootDoor")),
    "Triple"        -> (newArrayList ("SensorDaybed", "SensorDining", "SensorStudy", "SensorAtrium", "SensorLaundry", "SensorBoot", "SensorGarage", "SensorKitchen", "FloodLounge", "FloodMaster", "FloodTractor")),
    "AllOff"        -> (newArrayList ("SensorDaybed", "SensorDining", "SensorStudy", "SensorAtrium", "SensorLaundry", "SensorBoot", "SensorGarage", "SensorKitchen", "FloodLounge", "FloodMaster", "FloodTractor", "LightBootHall", "LightBootDoor"))
  ),

  "DoublePress" -> newHashMap(
    "SensorDaybed"  -> (newArrayList ("SensorDaybed", "SensorDining", "FloodLounge", "FloodMaster", "FloodTractor")),
    "SensorStudy"   -> (newArrayList ("SensorStudy", "SensorLaundry", "SensorBoot", "SensorDaybed", "SensorDining", "FloodLounge", "FloodMaster", "FloodTractor")),
    "SensorLaundry" -> (newArrayList ("SensorLaundry", "SensorStudy", "SensorBoot", "SensorAtrium")),
    "SensorAtrium"  -> (newArrayList ("SensorAtrium", "SensorStudy", "SensorLaundry", "SensorBoot")),
    "SensorBoot"    -> (newArrayList ("SensorBoot", "SensorStudy", "SensorLaundry", "SensorKitchen", "SensorGarage", "SensorDining", "FloodLounge", "FloodMaster", "FloodTractor")),
    "SensorGarage"  -> (newArrayList ("SensorGarage", "SensorKitchen", "SensorDining", "SensorBoot", "LightBootDoor")),
    "SensorKitchen" -> (newArrayList ("SensorKitchen", "SensorGarage", "SensorDining", "FloodLounge")),
    "SensorDining"  -> (newArrayList ("SensorKitchen", "SensorGarage", "SensorDining", "SensorDaybed", "FloodLounge", "FloodMaster", "FloodTractor")),
    "FloodLounge"   -> (newArrayList ("FloodLounge", "FloodMaster", "FloodTractor", "SensorDining", "SensorDaybed")),
    "FloodMaster"   -> (newArrayList ("SensorDaybed", "SensorDining", "SensorStudy", "SensorLaundry", "SensorAtrium", "SensorBoot", "SensorGarage", "SensorKitchen", "FloodLounge", "FloodMaster", "FloodTractor")),
    "FloodTractor"  -> (newArrayList ("FloodTractor", "FloodMaster", "FloodLounge", "SensorDining", "SensorDaybed")),
    "LightGarage"   -> (newArrayList ("LightGarage", "SensorKitchen", "SensorGarage", "SensorBoot", "LightBootDoor")),
    "LightBootDoor" -> (newArrayList ("LightBootDoor", "LightBootHall", "SensorBoot", "SensorKitchen", "SensorGarage")),
    "LightBootHall" -> (newArrayList ("LightBootHall", "LightBootDoor"))
  ),

  "LightTime" -> newHashMap(
    "SensorDaybed"  -> 10,
    "SensorStudy"   -> 10,
    "SensorLaundry" -> 10,
    "SensorAtrium"  -> 5,
    "SensorBoot"    -> 5,
    "SensorGarage"  -> 10,
    "SensorKitchen" -> 10,
    "SensorDining"  -> 5,
    "FloodLounge"   -> 15,
    "FloodMaster"  -> 5,
    "FloodTractor"  -> 5,
    "LightGarage"   -> 5,
    "LightBootDoor" -> 10,
    "LightBootHall" -> 10,
    "Triple"        -> 10
  ),

  "LightTimers" -> newHashMap(),

  "LightTriggers" -> newHashMap(
    "SensorDaybed"  -> (newHashMap ()),
    "SensorStudy"   -> (newHashMap ()),
    "SensorLaundry" -> (newHashMap ()),
    "SensorAtrium"  -> (newHashMap ()),
    "SensorBoot"    -> (newHashMap ()),
    "SensorGarage"  -> (newHashMap ()),
    "SensorKitchen" -> (newHashMap ()),
    "SensorDining"  -> (newHashMap ()),
    "FloodLounge"   -> (newHashMap ()),
    "FloodMaster"   -> (newHashMap ()),
    "FloodTractor"  -> (newHashMap ()),
    "LightGarage"   -> (newHashMap ()),
    "LightBootDoor" -> (newHashMap ()),
    "LightBootHall" -> (newHashMap ())  
  ),

  "MotionLights" -> newHashMap(
    "MotionBoot"     -> (newArrayList("SensorBoot", "SensorGarage", "SensorKitchen", "LightBootDoor", "SensorLaundry", "SensorStudy")),
    "MotionKitchen"  -> (newArrayList("SensorKitchen", "SensorDining", "SensorGarage")),
    "MotionDining"   -> (newArrayList("SensorDining", "SensorDaybed", "SensorKitchen")),
    "MotionStudy"    -> (newArrayList("SensorStudy", "SensorLaundry", "SensorBoot", "SensorDaybed")),
    "MotionDaybed"   -> (newArrayList("SensorDaybed", "SensorDining"))
  ),

  /*
  Logic flow for button presses

  Light already off:
    Short - add this button to the triggers for each of the short lights, set a timer for the specified length to turn them off again
    Double - add this button to the triggers for each of the long lights, set a timer for the specified length to turn them off again
    Long - add this button to the triggers for each of the short lights, no timer
    Triple - add this button to the triggers for all outside lights, set a timer

  Light already on:
    Short - remove triggers from all lights for this button, cancel timer, then recalc lights
    Double - add this button to the triggers for each of the long lights, reset the timer
    Long - cancel timer, leave lights on
    Triple - turn all outside lights off irrespective of how they were turned on, clear timer

  End of timer:
    Turn off all lights that were turned on by this switch (based on the LightTriggers list)

  Timers need to be tied to the button that initiated them, so we can cancel them again. That implies a 
  hashmap that holds them all.
  */


  /* 
    Set a light timer to turn lights off based on the configured timer length for this light switch.
    If there is a pre-existing timer, cancel it first
  */

  "SetLightTimer" -> [ 
    switchName,
    StaticData |

    logDebug( 'lights', 'Setting a timer for ' + switchName)

    val org.eclipse.xtext.xbase.lib.Procedures$Procedure2 LightTimerEnd = (StaticData as HashMap).get("LightTimerEnd")
    val LightTimers = (StaticData as HashMap).get("LightTimers") as HashMap<String, Timer>
    val LightTime = (StaticData as HashMap).get("LightTime") as HashMap<String, Long>

    if( LightTimers.get(switchName) !== null ){
      LightTimers.get(switchName).cancel
      LightTimers.put(switchName, null)
    }

    LightTimers.put( switchName, createTimer(new DateTimeType(ZonedDateTime.now()).getZonedDateTime().plusSeconds(LightTime.get(switchName) * 60)) [|{
      // at end of timer, turn off all lights that may have been turned on
      logDebug( 'lights', 'Timer turning off lights for ' + switchName)
      LightTimerEnd.apply( switchName, StaticData )
    }])
  ],


  /*
    When a light timer ends, remove this trigger from the LightTriggers for each light, then
    recalc light state for each light (i.e. turn off the lights we turned on).
    Note that light triggers can be motion as well as switches, hence sourceName
  */

  "LightTimerEnd" -> [ 
    sourceName,
    StaticData |
    logDebug( 'lights', 'Processing timer end for ' + sourceName )

    val org.eclipse.xtext.xbase.lib.Procedures$Procedure1 RecalcLights = (StaticData as HashMap).get("RecalcLights")
    val LightTriggers = (StaticData as HashMap).get("LightTriggers") as HashMap<String, HashMap<String, String>>
    val LightTimers = (StaticData as HashMap).get("LightTimers") as HashMap<String, Timer>

    LightTriggers.keySet.forEach[ triggerKey | 
      LightTriggers.get(triggerKey).remove(sourceName)
    ]
    if( LightTimers.get(sourceName) !== null ){
      LightTimers.get(sourceName).cancel()
      LightTimers.put(sourceName, null) 
    }
    RecalcLights.apply( StaticData )
    if(sourceName == "Triple"){
      swLightTriplePress.postUpdate(OFF)
    }
    if(sourceName == "FloodLounge"){
      swFloodLights.postUpdate(OFF)
    }
  ],

  /*
    When a switch is used to turn off then we actually turn off every light
    this switch controls, whether this switch turned it on or not. That is
    what we think people expect. Then we recalc light state for each light.
  */

  "SwitchOff" -> [ 
    switchName,
    StaticData |
    logDebug( 'lights', 'Processing switch off (from short press or from UI) for ' + switchName )
    
    val org.eclipse.xtext.xbase.lib.Procedures$Procedure1 RecalcLights = (StaticData as HashMap).get("RecalcLights")
    val LightTriggers = (StaticData as HashMap).get("LightTriggers") as HashMap<String, HashMap<String, String>>
    val LightTimers = (StaticData as HashMap).get("LightTimers") as HashMap<String, Timer>
    val DoublePress = ((StaticData as HashMap).get("DoublePress") as HashMap<String, List<String>>)

    DoublePress.get(switchName).forEach[ triggerKey | 
      LightTriggers.get(triggerKey).clear()
    ]
    if( LightTimers.get(switchName) !== null ){
      LightTimers.get(switchName).cancel()
      LightTimers.put(switchName, null) 
    }
    RecalcLights.apply( StaticData )
  ],

  /*
    Recalc light state for each light (turning it on or off as appropriate based on whether it has any
    triggers remaining open)
  */

  "RecalcLights" -> [
    StaticData |
    logDebug( 'lights', 'Recalculating lights' )

    val LightTriggers = (StaticData as HashMap).get("LightTriggers") as HashMap<String, HashMap<String, String>>

    var String sDebug = ""
    LightTriggers.keySet.forEach[ triggerKey | 
      var SwitchItem swLightOutput = LightOutputs.members.filter( ou | ou.name.startsWith( 'sw' + triggerKey ) ).last as SwitchItem
      var SwitchItem swLightInput = LightInputs.members.filter( in | in.name.startsWith( 'sw' + triggerKey ) ).last as SwitchItem

      sDebug = sDebug.concat( triggerKey + ":" + LightTriggers.get(triggerKey).size() + " ")
      if( LightTriggers.get(triggerKey).size() == 0 ) {
        swLightOutput.sendCommand(OFF)
        swLightInput.postUpdate(OFF)
      } else {
        swLightOutput.sendCommand(ON)
      }
    ]
    logDebug( 'lights', sDebug )
  ]
)

/*
  Calculate what to do based on a light button press
*/
val ProcessButtonPress = [
  switchName,
  sEvent,
  StaticData |
  logDebug( 'lights', 'Processing button press for ' + switchName + ' event was ' + sEvent )

  val org.eclipse.xtext.xbase.lib.Procedures$Procedure2 SetLightTimer = (StaticData as HashMap).get("SetLightTimer")
  val org.eclipse.xtext.xbase.lib.Procedures$Procedure2 SwitchOff = (StaticData as HashMap).get("SwitchOff")
  val org.eclipse.xtext.xbase.lib.Procedures$Procedure1 RecalcLights = (StaticData as HashMap).get("RecalcLights")
  val SinglePress = (StaticData as HashMap).get("SinglePress") as HashMap<String, List<String>>
  val DoublePress = (StaticData as HashMap).get("DoublePress") as HashMap<String, List<String>>
  val LightTriggers = (StaticData as HashMap).get("LightTriggers") as HashMap<String, HashMap<String, String>>
  val LightTimers = (StaticData as HashMap).get("LightTimers") as HashMap<String, Timer>

  var SwitchItem swLightOutput = LightOutputs.members.filter( ou | ou.name.startsWith( 'sw' + SinglePress.get(switchName).get(0))).last as SwitchItem

  // take action depending on what sort of button press we got
  switch sEvent {
    case 'SHORT_PRESSED': {
      // add this button to the triggers for each of the short lights, set a timer for the specified length to turn them off again
      logDebug( 'lights', 'We got a short press')
      if( swLightOutput.state == OFF){
        SinglePress.get(switchName).forEach [ lightOutput |
          LightTriggers.get(lightOutput).put(switchName, switchName)
        ]
        RecalcLights.apply( StaticData )
        SetLightTimer.apply( switchName, StaticData )
      } else {  // remove triggers from all lights for this button, cancel timer, then recalc lights
        logDebug( 'lights', 'Light currently on, turning off all lights turned on by ' + switchName)
        SwitchOff.apply(switchName, StaticData)
      }
    }

    case 'DOUBLE_PRESSED': {
      // turn on all the double press lights and set a timer
      logDebug( 'lights', 'We got a double press')

      DoublePress.get(switchName).forEach[ lightOutput |
        LightTriggers.get(lightOutput).put(switchName, switchName)
      ]
        RecalcLights.apply( StaticData )
      SetLightTimer.apply( switchName, StaticData )
    }

    case 'LONG_PRESSED': {
      // cancel a timer if there is one, turn on the small set of lights. If the large set of lights are on, they'll stay on
      logDebug( 'lights', 'We got a long press')
      if( swLightOutput.state == ON){
        if( LightTimers.get(switchName) !== null ){
          LightTimers.get(switchName).cancel()
          LightTimers.put(switchName, null) 
        } else {
          logInfo( 'lights', 'Long press for light thats already on ' + switchName + ' and there was no timer, which could be a problem')
        }
      }
      SinglePress.get(switchName).forEach [ lightOutput |
        LightTriggers.get(lightOutput).put(switchName, switchName)
      ]
      RecalcLights.apply( StaticData )
    }

    case 'TRIPLE_PRESSED': {
      // If this light isn't on, turn on all outside lights with a timer. If this light is on, turn off all lights and clear all triggers and all timers
      logDebug( 'lights', 'We got a triple press')
      if( swLightOutput.state == ON){
        LightTimers.keySet.forEach[ timerKey | 
          if( LightTimers.get(switchName) !== null ){
            LightTimers.get(switchName).cancel()
            LightTimers.put(switchName, null) 
          }
        ]
        SinglePress.get("AllOff").forEach[ light |
          LightTriggers.get(light).clear()
        ]
        RecalcLights.apply( StaticData )
      } else {
        SinglePress.get("Triple").forEach[ lightOutput |
          LightTriggers.get(lightOutput).put('Triple', switchName)
        ]
        RecalcLights.apply( StaticData )
        SetLightTimer.apply( 'Triple', StaticData )
      }
    }

    default: {
      logInfo( 'lights', 'Received unhandled event ' + sEvent + ' on item ' + switchName )
    }
  }
]


/* ---------------------------------------------------------------------------------------------------------------------------------------------------------------------
  MOTION SENSORS
*/

// Static Config



/*
Logic flow for motion sensors

Timer already active:
  Cancel timer, restart it. Keep the list of lights turned on

Timer not already active:
  For each light
    Light already on - do nothing
    Light not on - turn it on, and remember it was turned on for timer turn off

End of timer:
  Turn off all lights that were turned on by this motion sensor

Sensitivity:
  Keep track of every new turn on. If it happens more than 3 times in a half hour, turn down sensitivity by 20 points, set 
  a timer to adjust it back up by 20 points in 4 hours
  When adjust back up, if it's not at maxSensitivity, then set another timer to adjust it up another 20 points (or to max sensitivity)
*/

val ProcessMotion = [
  motionName,
  StaticData |
  logDebug( 'motion', 'Processing motion event for ' + motionName )

  val org.eclipse.xtext.xbase.lib.Procedures$Procedure1 RecalcLights = (StaticData as HashMap).get("RecalcLights")
  val HashMap< String, List<String>> MotionLights = (StaticData as HashMap).get("MotionLights")
  val LightTriggers = (StaticData as HashMap).get("LightTriggers") as HashMap<String, HashMap<String, String>>

  var StringItem strIllumination = MotionIlluminations.members.filter( il | il.name.startsWith( 'str' + motionName )).last as StringItem

  // turn on lights    
  if(strIllumination.state != "dark") { 
    logDebug( 'motion', "It's not dark out, it's " + strIllumination.state + " don't turn on lights for " + motionName)
  } else {
    logDebug( 'motion', "It's dark out, turn on lights")
    MotionLights.get(motionName).forEach[ lightOutput |
      LightTriggers.get(lightOutput).put(motionName, motionName)
    ]
    RecalcLights.apply( StaticData )
  }

  // still need to write logic to check the list of trigger events and dial down sensitivity, and set timer to dial sensitivity back up

]

// sensor now inactive, turn off each light that was turned on
val EndMotion = [
  String motionName,
  StaticData |

  logDebug( 'motion', 'Processing end of motion event for ' + motionName )

  val org.eclipse.xtext.xbase.lib.Procedures$Procedure2 LightTimerEnd = (StaticData as HashMap).get("LightTimerEnd")
  LightTimerEnd.apply( motionName, StaticData )  
]




rule "Shelly button flood lounge triggered"
when
  Channel "shelly:shellyplus1:a8032aba16f8:relay#button" triggered 
then
  ProcessButtonPress.apply( "FloodLounge", receivedEvent, StaticData )
end

rule "Shelly button sensor dining triggered"
when
  Channel "shelly:shellyplus1:441793aa6bf8:relay#button" triggered 
then
  ProcessButtonPress.apply( "SensorDining", receivedEvent, StaticData )
end

rule "Shelly button sensor study triggered"
when
  Channel "shelly:shellyplus1:a8032aba1704:relay#button" triggered 
then
  ProcessButtonPress.apply( "SensorStudy", receivedEvent, StaticData )
end

rule "Shelly button sensor laundry triggered"
when
  Channel "shelly:shellyplus1:441793a950cc:relay#button" triggered 
then
  ProcessButtonPress.apply( "SensorLaundry", receivedEvent, StaticData )
end

rule "Shelly button sensor atrium triggered"
when
  Channel "shelly:shellyplus1:441793a86d40:relay#button" triggered 
then
  ProcessButtonPress.apply( "SensorAtrium", receivedEvent, StaticData )
end

rule "Shelly button sensor boot triggered"
when
  Channel "shelly:shellyplus1:441793a94fe8:relay#button" triggered 
then
  ProcessButtonPress.apply( "SensorBoot", receivedEvent, StaticData )
end

rule "Shelly button sensor kitchen triggered"
when
  Channel "shelly:shellyplus1:083af2028610:relay#button" triggered 
then
  ProcessButtonPress.apply( "SensorKitchen", receivedEvent, StaticData )
end

rule "Shelly button flood tractor triggered"
when
  Channel "shelly:shellyplus1:441793aa9040:relay#button" triggered 
then
  ProcessButtonPress.apply( "FloodTractor", receivedEvent, StaticData )
end

rule "Shelly button light boot door triggered"
when
  Channel "shelly:shellyplus1:083af202a350:relay#button" triggered 
then
  ProcessButtonPress.apply( "LightBootDoor", receivedEvent, StaticData )
end

rule "Shelly button light boot hall triggered"
when
  Channel "shelly:shellyplus1:083af2029730:relay#button" triggered 
then
  ProcessButtonPress.apply( "LightBootHall", receivedEvent, StaticData )
end

rule "Shelly button sensor daybed triggered"
when
  Channel "shelly:shellyplus2pm-relay:80646fdbe00c:relay1#button" triggered 
then
  ProcessButtonPress.apply( "SensorDaybed", receivedEvent, StaticData )
end

rule "Shelly button flood master triggered"
when
  Channel "shelly:shellyplus2pm-relay:80646fdbe00c:relay2#button" triggered 
then
  ProcessButtonPress.apply( "FloodMaster", receivedEvent, StaticData )
end

rule "Shelly button sensor garage triggered"
when
  Channel "shelly:shellyplus2pm-relay:80646fdb89e0:relay1#button" triggered 
then
  ProcessButtonPress.apply( "SensorGarage", receivedEvent, StaticData )
end

rule "Shelly button light garage triggered"
when
  Channel "shelly:shellyplus2pm-relay:80646fdb89e0:relay2#button" triggered 
then
  ProcessButtonPress.apply( "LightGarage", receivedEvent, StaticData )
end

rule "Open garage door, turn on garage lights with timer"
when
  Item swGarageDoor changed to ON
then
  logDebug( 'lights', 'Garage door opened, considering turning on lights, its currently ' + strMotionBootIllumination.state)
  if( strMotionBootIllumination.state == 'dark'){
    ProcessButtonPress.apply( "LightGarage", "SHORT_PRESSED", StaticData )
  }
end

rule "Close garage door, turn off garage lights"
when
  Item swGarageDoor changed to OFF
then
  logDebug( 'lights', 'Garage door closed, turning off lights' )
  val org.eclipse.xtext.xbase.lib.Procedures$Procedure2 LightTimerEnd = StaticData.get("LightTimerEnd")
  LightTimerEnd.apply( "LightGarage", StaticData )
end

rule "Manual light turn on from UI"
when
  Member of LightInputs received command ON
then
  val LightInput = triggeringItem
  logDebug( 'lights', 'Light manually turned on for ' + LightInput.name )
  
  val switchName = LightInput.name.substring(2, LightInput.name.length() - 5)
  ProcessButtonPress.apply( switchName, "SHORT_PRESSED", StaticData )
end

rule "Manual light turn off from UI"
when
  Member of LightInputs received command OFF
then
  val LightInput = triggeringItem
  logDebug( 'lights', 'Light manually turned off for ' + LightInput.name )
  
  val switchName = LightInput.name.substring(2, LightInput.name.length() - 5)
  val org.eclipse.xtext.xbase.lib.Procedures$Procedure2 SwitchOff = StaticData.get("SwitchOff")

  SwitchOff.apply( switchName, StaticData )
end

rule "Manual all lights from UI"
// we can triple press on and triple press off, so no need to distinguish on vs off
when
  Item swLightTriplePress received command
then
  logDebug( 'lights', 'Toggle all outside lights (triple press from UI)' )
  
  ProcessButtonPress.apply( 'FloodMaster', 'TRIPLE_PRESSED', StaticData )
end

rule "Manual flood lights from UI"
// we double press on, but we want to single press off
when
  Item swFloodLights received command ON
then
  logDebug( 'lights', 'Turn on flood lights from UI' )
  
  ProcessButtonPress.apply( 'FloodLounge', 'DOUBLE_PRESSED', StaticData )
end

rule "Manual flood lights from UI"
// we double press on, but we want to single press off
when
  Item swFloodLights received command OFF
then
  logDebug( 'lights', 'Turn off flood lights from UI' )
  
  ProcessButtonPress.apply( 'FloodLounge', 'SHORT_PRESSED', StaticData )
end



// Motion rules

rule "Shelly motion boot triggered on"
when
  Item swMotionBootActive changed to ON 
then
  ProcessMotion.apply( "MotionBoot", StaticData )
end

rule "Shelly motion boot triggered off"
when
  Item swMotionBootActive changed to OFF 
then
  EndMotion.apply( "MotionBoot", StaticData )
end

rule "Shelly motion dining triggered on"
when
  Item swMotionDiningActive changed to ON 
then
  ProcessMotion.apply( "MotionDining", StaticData )
end

rule "Shelly motion dining triggered off"
when
  Item swMotionDiningActive changed to OFF 
then
  EndMotion.apply( "MotionDining", StaticData )
end

rule "Shelly motion kitchen triggered on"
when
  Item swMotionKitchenActive changed to ON 
then
  ProcessMotion.apply( "MotionKitchen", StaticData )
end

rule "Shelly motion kitchen triggered off"
when
  Item swMotionKitchenActive changed to OFF 
then
  EndMotion.apply( "MotionKitchen", StaticData )
end

And the items:

Group MotionSensors
Group MotionIlluminations
Group MotionBatteryLevels
Group MotionBatteryLows
Group MotionActives
Group MotionLastMotions

Group    MotionDining                 "Motion Sensor Dining"                       (MotionSensors)
String   strMotionDiningIllumination  "Motion Sensor Dining Illumination [%s]"     (MotionDining, MotionIlluminations)    { channel="shelly:shellymotion:2c1165cb0d29:sensors#illumination" }
Number   numMotionDiningBatteryLevel  "Motion Sensor Dining Battery Level [%d%%]"  (MotionDining, MotionBatteryLevels)    { channel="shelly:shellymotion:2c1165cb0d29:battery#batteryLevel" }
Switch   swMotionDiningBatteryLow     "Motion Sensor Dining Battery Low"           (MotionDining, MotionBatteryLows)      { channel="shelly:shellymotion:2c1165cb0d29:battery#lowBattery" }
Switch   swMotionDiningActive         "Motion Sensor Dining Active"                (MotionDining, MotionActives)          { channel="shelly:shellymotion:2c1165cb0d29:sensors#motion" }
DateTime dtMotionDiningLastMotion     "Motion Sensor Dining Last Motion [%1$td %1$tb %1$tr]" <clock>  (MotionDining, MotionLastMotions)  { channel="shelly:shellymotion:2c1165cb0d29:sensors#motionTimestamp"}

Group    MotionKitchen                "Motion Sensor Kitchen"                      (MotionSensors)
String   strMotionKitchenIllumination "Motion Sensor Kitchen Illumination [%s]"    (MotionKitchen, MotionIlluminations)   { channel="shelly:shellymotion:8cf681cd2138:sensors#illumination" }
Number   numMotionKitchenBatteryLevel "Motion Sensor Kitchen Battery Level [%d%%]" (MotionKitchen, MotionBatteryLevels)   { channel="shelly:shellymotion:8cf681cd2138:battery#batteryLevel" }
Switch   swMotionKitchenBatteryLow    "Motion Sensor Kitchen Battery Low"          (MotionKitchen, MotionBatteryLows)     { channel="shelly:shellymotion:8cf681cd2138:battery#lowBattery" }
Switch   swMotionKitchenActive        "Motion Sensor Kitchen Active"               (MotionKitchen, MotionActives)         { channel="shelly:shellymotion:8cf681cd2138:sensors#motion" }
DateTime dtMotionKitchenLastMotion    "Motion Sensor Kitchen Last Motion [%1$td %1$tb %1$tr]" <clock>  (MotionKitchen, MotionLastMotions)  { channel="shelly:shellymotion:8cf681cd2138:sensors#motionTimestamp"}

Group    MotionBoot                   "Motion Sensor Boot"                         (MotionSensors)
String   strMotionBootIllumination    "Motion Sensor Boot Illumination [%s]"       (MotionBoot, MotionIlluminations)      { channel="shelly:shellymotion:2c1165cb07b9:sensors#illumination" }
Number   numMotionBootBatteryLevel    "Motion Sensor Boot Battery Level [%d%%]"    (MotionBoot, MotionBatteryLevels)      { channel="shelly:shellymotion:2c1165cb07b9:battery#batteryLevel" }
Switch   swMotionBootBatteryLow       "Motion Sensor Boot Battery Low"             (MotionBoot, MotionBatteryLows)        { channel="shelly:shellymotion:2c1165cb07b9:battery#lowBattery" }
Switch   swMotionBootActive           "Motion Sensor Boot Active"                  (MotionBoot, MotionActives)            { channel="shelly:shellymotion:2c1165cb07b9:sensors#motion" }
DateTime dtMotionBootLastMotion       "Motion Sensor Boot Last Motion [%1$td %1$tb %1$tr]" <clock>  (MotionBoot, MotionLastMotions)  { channel="shelly:shellymotion:2c1165cb07b9:sensors#motionTimestamp"}


Group  Lights
Group  LightInputs 
Group  LightOutputs 
Group  LightLastEvents

Switch swLightTriplePress           "All outside lights (panic)"       (Lights)
Switch swFloodLights                "Flood lights"             (Lights)
// shelly 2 master - second channel
Group  SensorDaybed                 "Daybed Sensor Light"      (Lights)                     
Switch swSensorDaybedInput          "Daybed Sensor Switch"     (SensorDaybed, LightInputs)     
Switch swSensorDaybedOutput         "Daybed Sensor Output"     (SensorDaybed, LightOutputs)      {channel="shelly:shellyplus2pm-relay:80646fdbe00c:relay1#output"}
String stringSensorDaybedLastEvent  "Daybed Sensor Last Event" (SensorDaybed, LightLastEvents)    {channel="shelly:shellyplus2pm-relay:80646fdbe00c:relay1#lastEvent"}

// shelly 2 master - first channel
Group  FloodMaster                  "Master Flood Light"       (Lights)                     
Switch swFloodMasterInput           "Master Flood Switch"      (FloodMaster, LightInputs)      
Switch swFloodMasterOutput          "Master Flood Output"      (FloodMaster, LightOutputs)       {channel="shelly:shellyplus2pm-relay:80646fdbe00c:relay2#output"}
String stringFloodMasterLastEvent   "Master Flood Last Event"  (FloodMaster, LightLastEvents)    {channel="shelly:shellyplus2pm-relay:80646fdbe00c:relay2#lastEvent"}

Group  SensorStudy                  "Study Sensor Light"       (Lights)                     
Switch swSensorStudyInput           "Study Sensor Switch"      (SensorStudy, LightInputs)      
Switch swSensorStudyOutput          "Study Sensor Output"      (SensorStudy, LightOutputs)       {channel="shelly:shellyplus1:a8032aba1704:relay#output"}
String stringSensorStudyLastEvent   "Study Sensor Last Event"  (SensorStudy, LightLastEvents)    {channel="shelly:shellyplus1:a8032aba1704:relay#lastEvent"}

Group  SensorLaundry                "Laundry Sensor Light"      (Lights)                     
Switch swSensorLaundryInput         "Laundry Sensor Switch"     (SensorLaundry, LightInputs)   
Switch swSensorLaundryOutput        "Laundry Sensor Output"     (SensorLaundry, LightOutputs)    {channel="shelly:shellyplus1:441793a950cc:relay#output"}
String stringSensorLaundryLastEvent "Laundry Sensor Last Event" (SensorLaundry, LightLastEvents) {channel="shelly:shellyplus1:441793a950cc:relay#lastEvent"}

Group  SensorAtrium                 "Atrium Sensor Light"       (Lights)                     
Switch swSensorAtriumInput          "Atrium Sensor Switch"      (SensorAtrium, LightInputs)    
Switch swSensorAtriumOutput         "Atrium Sensor Output"      (SensorAtrium, LightOutputs)     {channel="shelly:shellyplus1:441793a86d40:relay#output"}
String stringSensorAtriumLastEvent  "Atrium Sensor Last Event"  (SensorAtrium, LightLastEvents)  {channel="shelly:shellyplus1:441793a86d40:relay#lastEvent"}

Group  SensorBoot                   "Boot Sensor Light"         (Lights)                     
Switch swSensorBootInput            "Boot Sensor Switch"        (SensorBoot, LightInputs)      
Switch swSensorBootOutput           "Boot Sensor Output"        (SensorBoot, LightOutputs)       {channel="shelly:shellyplus1:441793a94fe8:relay#output"}
String stringSensorBootLastEvent    "Boot Sensor Last Event"    (SensorAtrium, LightLastEvents)  {channel="shelly:shellyplus1:441793a94fe8:relay#lastEvent"}

Group  SensorKitchen                "Kitchen Sensor Light"      (Lights)                     
Switch swSensorKitchenInput         "Kitchen Sensor Switch"     (SensorKitchen, LightInputs)   
Switch swSensorKitchenOutput        "Kitchen Sensor Output"     (SensorKitchen, LightOutputs)    {channel="shelly:shellyplus1:083af2028610:relay#output"}
String stringSensorKitchenLastEvent "Kitchen Sensor Last Event" (SensorKitchen, LightLastEvents) {channel="shelly:shellyplus1:083af2028610:relay#lastEvent"}

Group  SensorDining                 "Dining Sensor Light"       (Lights)                     
Switch swSensorDiningInput          "Dining Sensor Switch"      (SensorDining, LightInputs)    
Switch swSensorDiningOutput         "Dining Sensor Output"      (SensorDining, LightOutputs)     {channel="shelly:shellyplus1:441793aa6bf8:relay#output"}
String stringSensorDiningLastEvent  "Dining Sensor Last Event"  (SensorDining, LightLastEvents)  {channel="shelly:shellyplus1:441793aa6bf8:relay#lastEvent"}

Group  FloodLounge                  "Lounge Flood Light"        (Lights)                     
Switch swFloodLoungeInput           "Lounge Flood Switch"       (FloodLounge, LightInputs)     
Switch swFloodLoungeOutput          "Lounge Flood Output"       (FloodLounge, LightOutputs)      {channel="shelly:shellyplus1:a8032aba16f8:relay#output"}
String stringFloodLoungeLastEvent   "Lounge Flood Last Event"   (FloodLounge, LightLastEvents)   {channel="shelly:shellyplus1:a8032aba16f8:relay#lastEvent"}

Group  FloodTractor                 "Tractor Flood Light"       (Lights)                     
Switch swFloodTractorInput          "Tractor Flood Switch"      (FloodTractor, LightInputs)    
Switch swFloodTractorOutput         "Tractor Flood Output"      (FloodTractor, LightOutputs)     {channel="shelly:shellyplus1:441793aa9040:relay#output"}
String stringFloodTractorLastEvent  "Tractor Flood Last Event"  (FloodTractor, LightLastEvents)  {channel="shelly:shellyplus1:441793aa9040:relay#lastEvent"}

// second channel of shelly 2 in garage
Group  SensorGarage                 "Garage Sensor Light"       (Lights)                     
Switch swSensorGarageInput          "Garage Sensor Switch"      (SensorGarage, LightInputs)    
Switch swSensorGarageOutput         "Garage Sensor Output"      (SensorGarage, LightOutputs)     {channel="shelly:shellyplus2pm-relay:80646fdb89e0:relay2#output"}
String stringSensorGarageLastEvent  "Garage Sensor Last Event"  (SensorGarage, LightLastEvents)  {channel="shelly:shellyplus2pm-relay:80646fdb89e0:relay2#lastEvent"}

// first channel of shelly 2 in garage
Group  LightGarage                  "Garage Internal Light"     (Lights)                     
Switch swLightGarageInput           "Garage Light Switch"       (LightGarage, LightInputs)     
Switch swLightGarageOutput          "Garage Light Output"       (LightGarage, LightOutputs)      {channel="shelly:shellyplus2pm-relay:80646fdb89e0:relay1#output"}
String stringLightGarageLastEvent   "Garage Light Last Event"   (LightGarage, LightLastEvents)   {channel="shelly:shellyplus2pm-relay:80646fdb89e0:relay1#lastEvent"}

Group  LightBootDoor                "Boot Door Light"           (Lights)                     
Switch swLightBootDoorInput         "Boot Door Light Switch"    (LightBootDoor, LightInputs)   
Switch swLightBootDoorOutput        "Boot Door Light Output"    (LightBootDoor, LightOutputs)    {channel="shelly:shellyplus1:083af202a350:relay#output"}
String stringLightBootDoorLastEvent "Boot Door Light Last Event" (LightBootDoor, LightLastEvents)  {channel="shelly:shellyplus1:083af202a350:relay#lastEvent"}

Group  LightBootHall                "Boot Hall Light"           (Lights)                     
Switch swLightBootHallInput         "Boot Hall Light Switch"    (LightBootHall, LightInputs)   
Switch swLightBootHallOutput        "Boot Hall Light Output"    (LightBootHall, LightOutputs)    {channel="shelly:shellyplus1:083af2029730:relay#output"}
String stringLightBootHallLastEvent "Boot Hall Light Last Event" (LightBootHall, LightLastEvents)  {channel="shelly:shellyplus1:083af2029730:relay#lastEvent"}