Automatic time based lighting control

To control a bunch of our lights and sockets to switch ON/OFF at a defined time I have found a simple solution which can control as much as lights/sockets I need with one simple rule.

My requirements are simple:

  • control lights by time and time events like sunset, sunrise
  • support different devices (I have Homematic and Sonoff devices), can be basically all which Switch items
  • support multiple schedules for a device per day, e.g. ON 4am, OFF sunrise & ON 7pm, OFF 11pm
  • schedule can be changed without coding aka. without change items or .rules file

Particularly important the last one because I have different modes and schedules for the outdoor devices in summer and winter.
A quick search in the OpenHab community already revealed a few interesting approaches. But these were all too complex and integrated addition triggers like contact or motion sensors.

My old approach was based on number items holding the on/off times and a ton of setpoint controls in the sitemap. But this got cumbersome with every new device and every new schedule.

So here is my approach: For each switchable item I want to control I have one additional String item which stores the schedule.

Devices:

Switch Switch_Outdoor_Pond_1            "Teich Luft"          <flow>          (gHouse,gPond,gSwitches)    [ "Switchable" ]   {channel="homematic:HG-HM-LC-Sw1-DR:ccu:xxxxxx:1#STATE"}
Switch Switch_Outdoor_Pond_2            "Teich Wasserfall"    <flow>          (gHouse,gPond,gSwitches)    [ "Switchable" ]   {channel="homematic:HG-HM-LC-Sw1-DR:ccu:xxxxxx:1#STATE"}
Switch Switch_Outdoor_Terrace_Light     "Terrassenlicht"      <light>         (gOutdoor,gHouse,gSwitches) [ "Lighting" ]     {mqtt=">[mosquitto:cmnd/sonoff-terrassenlicht/power:command:*:default],<[mosquitto:stat/sonoff-terrassenlicht/POWER1:state:default]"}
Switch Switch_Outdoor_Terrace_Lounge    "Terrassenlounge"     <light>         (gOutdoor,gHouse,gSwitches) [ "Lighting" ]     {mqtt=">[mosquitto:cmnd/sonoff-terrassenlounge/power:command:*:default],<[mosquitto:stat/sonoff-terrassenlounge/POWER1:state:default]"}

Automation rules:

String Auto_Switch_Outdoor_Pond_1             "Teichprogram Luft [%s]"          <flow>              (gAutomatic)            {mqtt="<[mosquitto:openhab/config/SwitchOutdoorPond1:state:default]"}
String Auto_Switch_Outdoor_Pond_2             "Teichprogram Wasserfall [%s]"    <flow>              (gAutomatic)            {mqtt="<[mosquitto:openhab/config/SwitchOutdoorPond2:state:default]"}
String Auto_Switch_Outdoor_Terrace_Light      "Teressaenlichtprogramm [%s]"     <light>             (gAutomatic)            {mqtt="<[mosquitto:openhab/config/SwitchOutdoorTerraceLight:state:default]"}
String Auto_Switch_Outdoor_Terrace_Lounge     "Lounge Lichtprogramm [%s]"       <light>             (gAutomatic)            {mqtt="<[mosquitto:openhab/config/SwitchOutdoorTerraceLounge:state:default]"}

The pattern for the schedules is as follows:

  • multiple schedules are separated by |
  • a schedule matches the <on>-<off> pattern
  • either <on> or <off> is required, the second is optional
  • SUN_RISE & SUN_SET are supported as well

I cannot change the schedule via Basic UI directly, since there is no input option for that. For some schedules I change often I have configured a “Selection” in the sitemap with all used options. For example home.sitemap snippet:

Selection item=Auto_Switch_Outdoor_Tree_Light label="Pflaumenbaum" mappings=[""="aus", "SUN_SET-20"="bis 20Uhr", "SUN_SET-21"="bis 21Uhr", "SUN_SET-22"="bis 22Uhr", "SUN_SET-23"="bis 23Uhr", "SUN_SET-0"="bis 24Uhr"]
Selection item=Auto_Switch_Outdoor_Terrace_Light label="Terassenlicht" mappings=[""="aus", "-20"="bis 20Uhr", "-21"="bis 21Uhr", "-22"="bis 22Uhr", "-23"="bis 23Uhr", "-0"="bis 24Uhr"]

Examples:

  • 8-10 = ON at 8am, OFF at 10am
  • -23 = no automatic on, but if on the OFF at 11pm
  • 4-SUN_RISE = ON at 4am, OFF at sunrise

All schedules also can simply be updated via MQTT message, which is totally enough since I not change them that often.

The heart of the control login is in one generic rule:

rule "Automatic program timer"
when
    Time cron "0 0 0/1 * * ?"
then
    // iterate all automatic programs we have in gAutomatic group
    gAutomatic.members.forEach(program| {
        // get switch by program name from gSwitches
        val switchName = program.name.substring(5, program.name.length)        
        val switchItem = gSwitches.members.findFirst[SwitchItem ctrl | ctrl.name == switchName ]
        if (null === switchItem) {
            logInfo(filename, "No switch found for " + program.name)
            return
        }

        // get all schedules
        logInfo(filename, "Schdeule for " + switchItem.name + ": " + program.state.toString)
        val programsForSwitch = program.state.toString.split("\\|")
        programsForSwitch.forEach(item | {
            var schedule = item
            if (item.startsWith("-")) schedule = "x" + item // x is placeholder for do nothing
            if (item.endsWith("-")) schedule = item + "x"
            // get ON & OFF time for schedule
            val schedulesForSwitch = schedule.split("-")
            if (schedulesForSwitch.length != 2) {
                logInfo(filename, "Schedule " + schedule + " for " + switchItem.name + " is not valid, must be <on>-<off>")
                return
            }

            // switch ON if not already
            if (schedulesForSwitch.get(0) == now.getHourOfDay.toString && switchItem.state == OFF) {
                logInfo(filename, "Change switch " + switchItem.name + " to ON (time)")
                switchItem.sendCommand(ON)
            }

            // switch OFF if not already
            if (schedulesForSwitch.get(1) == now.getHourOfDay.toString && switchItem.state == ON) {
                logInfo(filename, "Change switch " + switchItem.name + " to OFF (time)")
                switchItem.sendCommand(OFF)
            }
        })
    })
end

The rule is triggered by cron job every full hour. It gets all schedules for the member of the gAutomatic group, finds the corresponding switchable item and evaluates the configured schedule. I have a second rule which does the same but triggered by sunrise/sunset events:

rule "Automatic program sun"
when
    Channel 'astro:sun:home:rise#event'    triggered START or
    Channel 'astro:sun:home:set#event'     triggered START
then
    Thread::sleep(5000) // make sure Day_Phase was already updated

    // iterate all automatic programs we have in gAutomatic group
    gAutomatic.members.filter[program|program.state.toString.contains("SUN")].forEach(program| {
        // get switch by program name from gSwitches
        val switchName = program.name.substring(5, program.name.length)        
        val switchItem = gSwitches.members.findFirst[SwitchItem ctrl | ctrl.name == switchName ]
        if (null === switchItem) {
            logInfo(filename, "No switch found for " + program.name)
            return
        }

        // get all schedules
        logInfo(filename, "Schdeule for " + switchItem.name + ": " + program.state.toString)
        val programsForSwitch = program.state.toString.split("\\|")
        programsForSwitch.forEach(item | {
            var schedule = item
            if (item.startsWith("-")) schedule = "x" + item // x is placeholder for do nothing
            if (item.endsWith("-")) schedule = item + "x"
            // get ON & OFF time for schedule
            val schedulesForSwitch = schedule.split("-")
            if (schedulesForSwitch.length != 2) {
                logInfo(filename, "Schedule " + schedule + " for " + switchItem.name + " is not valid, must be <on>-<off>")
                return
            }

            // switch ON if not already
            if (switchItem.state == OFF && schedulesForSwitch.get(0) == Day_Phase.state.toString) {            
                logInfo(filename, "Change switch " + switchItem.name + " to ON (" + Day_Phase.state.toString + ")")
                switchItem.sendCommand(ON)
            }

            // switch OFF if not already
            if (switchItem.state == ON && schedulesForSwitch.get(1) == Day_Phase.state.toString) {
                logInfo(filename, "Change switch " + switchItem.name + " to OFF (" + Day_Phase.state.toString + ")")
                switchItem.sendCommand(OFF)
            }
        })
    })
end

I have this running for one week now, currently I have 10 devices controlled with this pattern. In a few weeks when the Christmas season starts I will add a lot more for all the Christmas lights.

Let me know thoughts :slight_smile:

3 Likes

Excellent idea to store the schedule in a simple string!

Always nice to hear about new solutions and sorry to disturb the party, but you asked for thoughts. So: don’t you think you’re overengineering things ?
Many people use the generic Time of Day pattern for lighting, usually using static transition times (i.e. coded in rules). Most of the time this is sufficiently flexible since you can also use sunrise/sunset +/- X minutes to transition, too.
Wherever it is not sufficient (for example, I want to turn my garden lights until 23:00 on only in summer), you can still deploy specific rules for them. These rules I need to change a couple of times a year - at most. Far less work than to code a framework to allow for that.
Then again, that allows me to be even more specific than any solution with a purely time-driven schedule can ever become. I can decide based on other information than just time such as presence information or sensor data or other input such as weather forecast.
Yeah we all aim to come up with clever solutions as that sort of gives us a satisfying feeling because it’s us who invented it, and it that’s what you’re after - ok. But in most cases it is overengineering. We’re not programmers of a product to offer scheduling capabilities to end users that can only select from what the programmers had thought of earlier. We DO have access and can change the code at any time. And we just need to program that very one schedule to match our own need, and I see no advantage in going miles just in order to NOT having to code that in rules.

I do!
Scheduling is a sorely missing feature in OH and there are many way to get around the problem.
Different rules (Like you do), External tolls (Node-Red for example), Bindings like gCal or CalDav.

But many people have flexible schedules with for instance work rotas changing on a weekly basis and a simple scheduler without having to go in the code is a very good idea indeed.

1 Like

No, since with these 2 simple and generic rules one can control as much as lights/switches needed. Adding a new is just one addition item containing the schedule and you are done. No additional rules needed.

Actually, as already stated in my intro, I think multiple people have different requirements. I also use other patterns where it makes sense, for example my floor light is triggered by contact & motion sensors, has dedicated off times etc. These rules are as you wrote very static and do not change.

I know, but somethings I only want to change things quickly and temporally. Here is some example: we plan to have a pool party … I want to run the pump longer and to pool light should not switch of at 11pm but at 3 o’clock in the morning.
Would you do coding for that? I won’t.

This are just 2 simple rules which maybe even could be combined into one and simplified. For me this really simplified things much, before I had multiple rules for different lights and scenarios. So in my case this approach means even less miles.

1 Like

Coding your pool party is easy.
Adding an exception for tonight is, too.
Manual switching is, too.
Don’t get me wrong - if you feel your approach is less work than to use rules or manually switch ok do so.
But you’re publishing this and thus sort of encourage others to use this approach as well, and that’s where I object.
Automating your pool pump based on say weather data and/or motion detection is not as easy but still better.
Good automation means everything’s working automatically. You don’t even have to remember and think about it. And that’s what you can not do if you have to keep using the UI in time to change parameters (and back to normal when you’re done with your phase or exception - you easily forget about that if it’s not automated).
With (well-done) rules, you can forget about.

Of course for this simple illustration use case manual switching would obviously be the most easy solution :wink:

I do not, I asked for feedback :wink:
I also do not change schedules every week :slight_smile: For simple light switches like the > 20 outlets I add every Christmas season around the house I don’t need complex rules & sensors. On/off in the morning & evening is fine here. Such things can easily be achieved with only one additional item line containing the schedule. No coding needed, no manual switching needed.

I think I have done something similar. A drop-down in HabPanel sets my roller shutters in different modes. Random/Away, Night, Manual, Sun controlled, …
I will definitely have a look in yoir solution when I have the time. Thank you! Bookmarked.:grinning:

If someone is sleeping in the guest room, I want THEM to control everything in their room. One tip sets the room to guests mode.

@mstormi Even so I too think, that good automation means everything’s working automatically, I think the pool party is good example. Nothing I am aware of could possibly check for a spontaneous pool party to keep the lights on. Even motion detection. The last few guests will sit in the dark when they don’t move in the pool enough?
One switch in the UI or even “Alexa, set Pool to Party Mode” could do that.

Yes, and that’s what I use and propose, too: code a set of predefined scenes you can switch among using cron (by default, if you don’t do anything), UI or Mycroft (Alexa is evil !). Can be applied globally (e.g. all lights and roller shutters in the house) or to a group of devices (such as your heaters/thermostats or your Christmas lights).
But I wouldn’t set a UI-configurable timer like the OP proposed. And for any single item, manual switching is the most simple way to go.

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.

It makes sense if you want to access 3rd party information beyond your control (such as say your wife’s calendar) or want others (to not have access to the rules) such as your family to be able to change it, too.
And if you perfectly automate it, it’s worth going those miles. That I don’t object to.
But those are not the use cases of the OP and to widely automate these will be incompatible with OP’s approach or will supercede it (and it’s then not needed any more).

I even have a physical scene switch that I triple-click on in order to transition all of my house to 'nighttime` scene.
I tried automating that transition using fixed cron times or dynamic triggers (motion detection and other sensors and a number of computations) but it never felt to switch at the right time, seems my AI is not I enough.
It’s obviously an everyday ritual but this use case wouldn’t work with your approach to use fixed times or a selection of these, and you also would quite often forget to re-set the proper night time for the next day before it’s due.
This lack of granularity (or preciseness) will apply to the OP’s solution in general.

Thx @rlkoshak for the detailed feedback.

Yes like that, maybe I will switch to that approach. The MQTT part is actually a goody to change a schedule to anything, even times which are not predefined at all.

I’m also not happy with that. Will try to get rid of it, I initially added the sleep make sure Day_Phase is correctly set and have no race conditions.

Your example looks interesting, will give that a try.
I will also look into using a Lambda to avoid the duplicated code a little. Have not looked into other devices or supporting dimmers etc. most lights/outlets I switch are simple Sonoff devices (of course not for the pool pump :wink: )