Journey to JSR223 Python 4 of 9

This post is a conversion of all of my lights control Rules. Compared to some people’s lighting controls Rules these are relatively simple.

Turn on the Master Bedroom Lights when someone comes home and it’s dark

Rules DSL

rule "Turn on the master bedroom lights when someone comes home"
when
  Item vPresent changed from OFF to ON
then
  if(aMBR_Lights.state != ON && vMBR_Light.state > 100){
    logInfo(logName, "Welcome home, it's dark, turning on the MBR lights.")
    aMBR_Lights.sendCommand(ON)
  }
end

When someone comes home and it’s dark (note the sensor is just a potoresistor so the number here isn’t using any units like lux) turn on the lights in the master bedroom.

Python

from core.rules import rule
from core.triggers import when

@rule("MBR Lights Ctrl", description="Turns on the MBR lights when we come home and it's dark", tags=["lights"])
@when("Item vPresent changed from OFF to ON")
def mbr_lights(event):
    if items["aMBR_Lights"] != ON and items["vMBR_Light"] > DecimalType(100):
        mbr_lights.log.info("Welcome home, it's dark, turning on the MBR lights")
        events.sendCommand("aMBR_Lights", "ON")

Turn off and on lights based on time of day

Rules DSL

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

When the time of day changes, turn OFF the lights that are supposed to be OFF and ON the lights that are supposed to be ON. The Rule is entirely Group driven using Associated Items Design Pattern. Each time of day has an ON Group and OFF Group named with the time of day (e.g gLights_ON_MORNING and gLights_OFF_MORNING). Add the lights that should be controlled at those times to the appropriate Group. For example, a light that should come ON at AFTERNOON and turn OFF at BED time (two time periods later), add it to gLights_ON_AFTERNOON and gLights_OFF_BED.

Python

from core.rules import rule
from core.triggers import when

@rule("ToD Lights", description="Sets the lights based on the time of day", tags=["lights"])
@when("Item vTimeOfDay changed")
def tod_lights(event):
    offGroupName = "gLights_OFF_{}".format(items["vTimeOfDay"])
    onGroupName  = "gLights_ON_{}".format(items["vTimeOfDay"])

    tod_lights.log.info("Turning off the lights for {}".format(offGroupName))
    for light in filter(lambda light: light.state != OFF, ir.getItem(offGroupName).members): events.sendCommand(light, OFF)

    tod_lights.log.info("Turning on the lights for {}".format(onGroupName))
    for light in filter(lambda light: light.state != ON, ir.getItem(onGroupName).members): events.sendCommand(light, ON)

There are some improvements that could be made to the Rules DSL version, but I find this Python version significantly simpler than the already simple Rule.

Note, I’ve moved the override reset to another Rule

Weather Lights Control

The following are a set of Rules that work together. These Rules turn on lights when the weather says it is cloudy and off when it is no longer cloudy. However, this behavior can be overridden by a user manually changing a light’s state. Once overridden, the cloudiness will no longer control that light until the next day.

Rules DSL

// Thoery of operation: If it is day time, turn on/off the weather lights when cloudy conditions
// change. Trigger the rule when it first becomes day so we can apply cloudy to lights then as well.
rule "Turn on lights when it is cloudy"
when
  Item vIsCloudy changed or
  Item vTimeOfDay changed to "DAY"
then
  // We only care about daytime and vIsCloudy isn't NULL
  if(vTimeOfDay.state != "DAY" || vIsCloudy.state == NULL) return;

  // give the side effects of time of day time to complete
  if(triggeringItem.name == "vTimeOfDay") Thread::sleep(500)

  logInfo(logName, "It is " + vTimeOfDay.state.toString + " and cloudy changed: " + vIsCloudy.state.toString +", adjusting lighting")

  // Apply the cloudy state to all the lights in the weather group
  gLights_ON_WEATHER.members.forEach[ SwitchItem l |

    val overrideName = l.name+"_Override"
    val override = gLights_WEATHER_OVERRIDE.members.findFirst[ o | o.name == overrideName ]

    if(override.state != ON && l.state != vIsCloudy.state) l.sendCommand(vIsCloudy.state as OnOffType)

    if(override.state == ON) logInfo(logName, l.name + " is overridden")
  ]
end


// Theory of operation: any change in the relevant lights that occur more than five seconds after
// the change to DAY or after a change caused by cloudy is an override
rule "Watch for overrides"
when
  Member of gLights_ON_DAY changed
then
  // wait a minute before reacting after vTimeOfDay changes, ignore all other times of day
  if(vTimeOfDay.state != "DAY" || vTimeOfDay.lastUpdate("mapdb").isAfter(now.minusMinutes(1).millis)) return;

  // Assume any change to a light that occurs more than n seconds after time of day or cloudy is a manual override
  val n = 5
  val causedByClouds = vIsCloudy.lastUpdate("mapdb").isAfter(now.minusSeconds(n).millis)
  val causedByTime = vTimeOfDay.lastUpdate("mapdb").isAfter(now.minusSeconds(n).millis)

  if(!causedByClouds && !causedByTime) {
    logInfo(logName, "Manual light trigger detected, overriding cloudy control for " + triggeringItem.name)
    postUpdate(triggeringItem.name+"_Override", "ON")
  }
end

The first Rule triggers when the time of day changes or cloudiness changes. We only activate this Rule when it’s DAY time and we have a real state for cloudiness.

If the Rule were triggered because of the time of day changing, sleep for a bit to allow all the lights to turn ON or OFF based on the previous Rule above.

Loop through all the members of gLights_ON_Weather and turn ON/OFF the light it it isn’t overridden. The override flag is stored in an Associated Item.

The second Rule watches for manual overrides. First ignore any changes that occur during any time period other than day or any light changes that occur in the minute after vTimeOfDay changed to “DAY”. (Note: a minute is way too long).

Next check to see if the light changes were caused by cloudiness changing or time of day changing. (Note: the check on time of day is probably unnecessary, we’ve already filtered that out in the first check). If the change wasn’t caused by either cloudiness or time of day changing, set the override flag.

In the previous section, the Rules DSL Rule that triggers when the time of day changes resets the override flag when the time of day changes.

Python

from core.rules import rule
from core.triggers import when
from time import sleep
from core.metadata import get_key_value, set_metadata
from core.actions import PersistenceExtensions
from org.joda.time import DateTime

@rule("Cloudy Lights", description="Turns ON or OFF some lights when it is cloudy during the day", tags=["lights"])
@when("Item vIsCloudy changed")
@when("Item vTimeOfDay changed")
def cloudy_lights(event):
    if items["vTimeOfDay"] != StringType("DAY") or isinstance(items["vIsCloudy"], UnDefType): return

    if event.itemName == "vTimeOfDay": sleep(0.5)

    cloudy_lights.log.info("It is {} and cloudy changed: {}".format(items["vTimeOfDay"], items["vIsCloudy"]))

    for light in filter(lambda light: get_key_value(light.name, "Flags", "override") != "ON", ir.getItem("gLights_ON_WEATHER").members):
        cloudy_lights.log.info("checking light {}".format(light.name))
        if items[light.name] != items["vIsCloudy"]: events.sendCommand(light, items["vIsCloudy"])

@rule("Lights Override", description="Sets the Override flag when a light is manually changed during the day", tags=["lights"])
@when("Member of gLights_ON_WEATHER changed")
def override_lights(event):
    sleep(0.5) # Give pesistence a chance to catch up

    # Wait a minute before reacting after vTimeOfDay changes, ignore all other times of day
    if items["vTimeOfDay"] != StringType("DAY") or PersistenceExtensions.lastUpdate(ir.getItem("vTimeOfDay"), "mapdb").isAfter(DateTime.now().minusSecond
s(10)):
        return

    if not PersistenceExtensions.lastUpdate(ir.getItem("vIsCloudy"), "mapdb").isAfter(DateTime.now().minusSeconds(5)):
        override_lights.log.info("Manual light trigger detected, overriding the light for auto control for {}.".format(event.itemName))
        set_metadata(event.itemName, "Flags", { "override" : "ON" }, overwrite=False)

@rule("Reset Override", description="Change override flag when time of day changes", tags=["lights"])
@when("Item vTimeOfDay changed")
def reset_overrides(event):
    for light in ir.getItem("gLights_ON_WEATHER").members:
        set_metadata(light.name, "Flags", { "override" : "OFF" }, overwrite=False)

I’ve replaced the two Rules DSL rules above with three. I’ve decided to use subfolders and more .py files than I did with my Rules DSL Rules. Consequently I’ve put the regular lights controlling Rules into one file and these three weather controlling Rules into another. To support this I had to create a new Rule in the weather rules file to reset the overrides. This puts all the logic that sets, resets, and checks the override into one file.

I think there was a bug in the Rules DSL version as well as I don’t wait to give persistence a chance to save the change to vTimeOfDay before checking the lastUpdate on the Item as the first line of the Rule.

Lessons Learned

Not much new here.

  • When I tried to use changedSince in the manual override Rule it never worked right. I think I’ve seen that reported for Rules DSL as well so it may be a bug. So I’m still using lastUpdate.

  • It probably has more to do with familiarity, but I find the Python code easier to read if I break the Rules up into more files. The problem with access to global vars isn’t there anymore so there isn’t anything pushing to keep the Rules in the same file. And with the ability to put them into subfolders for organization it further makes sense to break them up more. My Rule of thumb will be one Rule per file unless there are a few Rules that work together in concert like the weather lights controlling Rules above.

  • When you transition and you start to eliminate the need for some of your Items, go ahead and delete those Items as you go. I’ve not been doing this and now I have around 50 Items that are no longer needed because of the move to using metadata.

Previous: Journey to JSR223 Python 3 of 9
Next: Journey to JSR223 Python 5 of?

3 Likes

You’re probably thinking of storing variables in modules, but you can also store data in scriptExtensions, which could be used in not only other Python scripts, but in rules built in JS, JSON, etc. It’s already on my todos, but ping me if you’re really interested and I’ll write something up.

Your post was another reminder for me to get my MOMOALR posted, which would combine your rules to into two, and eliminate the need for any other lighting rules. I put the Jython addon down and get that polished up!

I’m actually thinking of the limitations in Rules DSL. In Rules DSL there is a push to consolidate all the Rules that address a specific function into the same file because:

  • you can’t use subfolders to organize the files
  • those Rules may share the same global vars and vals, though when I wrote the above I was mostly thinking of vals (i.e. constants) which in Python properly get defined in configuration.py.

I’m definitely interested but I think I can wait. It don’t have a super compelling need for it right now and my use of metadata in these posts is already starting to get a bit more advanced than I intended. I worry that if I use too many advanced features like these which are not available in Rules DSL I might lose some of the users who would use them to translate their own Rules but are not good coders.

With your and Michael’s help I’m already introducing a lot of new concepts for many of these users.

2 Likes