Automatic time based lighting control

Nice approach if, as Markus indicates, the ToD DP won’t work in your situation.

  • An alternative to sending MQTT messages can be to use the MAP transform and put the schedule String in a .map file. Then you don’t even need the schedule Items, just a call to the transform with the name of the Light and it will return the schedule string. So you would basically iterate through the Switches/Dimmers, pull the schedule for the light from the map, parse it and apply it.

  • “Thread::sleep(5000)” I really don’t like to see sleeps this long. What exactly is it waiting on? Can you trigger the Rule based on the change in Day_Phase instead of using the sleep?

  • Rather than a polling rule that runs all the time, I’d probably work something out using Timers. So I’d have a Rule that triggers once a day and at System started, loop through the lights and create Timers to turn ON/OFF the lights based on the times in the schedule.

  • At least use a lambda instead of duplicating all of that code. If you decided to change something about how your schedule String works you would have to change it in two places which is always a big no no in coding. I see no reason why they can’t both become just one Rule with three different triggers.

  • I can see this approach potentially being useful for scheduling other things like irrigation and HVAC.

  • Have you given any thoughts into how you would expand this to support dimmers or color lighting?

I’ve even made relatively generic way to control my lights based off of those events and Associated Items DP:

val logName = "lights"

// Theory of operation: Turn off the light that are members of gLights_OFF_<TOD> and
// then turn on the lights that are members of gLights_ON_<TOD>. Reset the overrides.
rule "Set lights based on Time of Day"
when
  Item vTimeOfDay changed
then
  // reset overrides
  gLights_WEATHER_OVERRIDE.postUpdate(OFF)

  val offGroupName = "gLights_OFF_"+vTimeOfDay.state.toString
  val onGroupName = "gLights_ON_"+vTimeOfDay.state.toString

  logInfo(logName, "Turning off lights for " + offGroupName)
  val GroupItem offItems = gLights_OFF.members.filter[ g | g.name == offGroupName ].head as GroupItem
  offItems.members.filter[ l | l.state != OFF ].forEach[ SwitchItem l | l.sendCommand(OFF) ]

  logInfo(logName, "Turning on lights for " + onGroupName)
  val GroupItem onItems = gLights_ON.members.filter[ g| g.name == onGroupName ].head as GroupItem
  onItems.members.filter[ l | l.state != ON].forEach[ SwitchItem l | l.sendCommand(ON) ]

end

But even this is a little over engineered, especially since I only have three lights. :blush: There are companion Rules that control the lights based on how cloudy it is that can be overridden by manually toggling the light which is what that first line is all about. Anyway, sometimes we learn a lot from the over engineering.

One thing I do like about OP’s approach is it would mesh relatively well with @job’s scenes using Scripts approach.

The same can be said for Marku’s approach. But instead of having the granularity of 24 separate stop and start times for each individual light, it supports N named start and stop times (Time of Day) and, in the case of my code above, N lights can start or stop based on those start and stop times based on their Group membership. In my Rule’s case, I just need to add/remove an Item to an ON and OFF Group and it is now controlled on that schedule.

I guess my point is this approach is not the only approach that doesn’t require modifying Rules to add/remove lights or change schedule.

It depends. If I had a pool party more than a couple times a year then probably yes, I’d probably write a temporary Rule for that that I’d just disable or comment out most of the time. It would be less work in the long run then maintaining a complex set of Rules and developing a custom scheduling language. It’s all a balancing act and dealing with the problems that actually hurt or annoy. I can do that sort of edits to my Rules on my phone easy enough, even remotely if I had to through an ssh tunnel.

However, if I had maybe 20% of my on/off controls falling outside of the TimeOfDay defined time periods I might consider something like this. As with everything YMMV.

They can indeed. At a minimum put the shared code in a lambda. That being said, the Rules really are not all that simple. That’s not a knock against them. My Rule above, while shorter, isn’t significantly simpler either. Scheduling is complex in OH unfortunately. Just don’t underestimate the complexity of this Rule. I would expect a new user to have significant challenges understanding and customizing these Rules for their own setup.

Thanks for posting! I like to see new approaches to solve problems.

For the record, my suggestions would result in a Rule along the lines of the following (I’m just typing this in, there are likely typos):

import java.util.Map

val Map<String, Timer> timers = newHashMap

val scheduleTimer = [ SwitchItem ctrl, String timeStr, String cmd, Map<String, Timer> timers, String key |
    if(timeStr == "x") return;

    val DateTime startTime = null
    switch timeStr {
        case "SUN_RISE": startTime = new DateTime(Sunrise.state.toString)
        case "SUN_SET":  startTime = new DateTime(Sunset.state.toString)
        default: startTime = now.plusDays(1).minusHours(24-Integer::parseInt(timeStr)) // plus day minus hours to deal with daylight savings
    }

    timers.put(key, createTimer(startTime, [ | if(ctrl.state.toString != cmd) ctrl.sendCommand(cmd) ] )
]

rule "Automatic program"
when
    System started or
    Item Sunrise changed // trigger every night after Astro updates the sunrise/sunset times
then
    timers.values.forEach[ timer | timer.cancel ] // clear out any existing timers

    gSwitches.members.forEach[ ctrl | 
        val schedules = transform("MAP", "schedules.map", ctrl.name)  // you might need to escape the = in the map file
        if(schedules !== "") { 
            val programsForSwitch = schedule.split("\\|")
        
            programsForSwitch.forEach[ sched |
                val parts = sched.split("-|=")
                val startStr = if(parts.size == 2 && sched.startsWith("-")) "x" else parts.get(0)
                val endStr = if(parts.size == 2 && sched.endsWith("-")) "x" else parts.get(1)
                val cmd = if(parts.size == 2) parts.get(1) else parts.get(2)

                scheduleTimer.apply(ctrl, startStr, cmd, timers, ctrl.name+" "+sched+" start")
                scheduleTimer.apply(ctrl, endStr, cmd, timers, ctrl.name+ " "+sched+" end")
            ]
        }
        else {
            logInfo(filename, ctrl + " doesn't have a schedule, skipping")
        }
    ]

end

When OH starts and when Astro runs to recalculate the sunrise and sunset times the Automatic program Rule runs. It loops through all the switches and pulls that switch’s schedule from the map file, skipping those that lack a schedule. It parses the schedule string and schedules a timer to issue the indicated command at the indicated time.

The big advantage is there is no duplicated code. Other advantages include less code and the rule spends far fewer cycles doing nothing when there is nothing to do. You have even fewer Items since you no longer need separate Items to store the schedules.

Anyway, it’s just another slight twist on your original approach.