I’ve tried a few methods for doing automatic lighting control based on motion and contact sensors, and I’ve recently found a pattern for my rules that is working really well. It makes it simpler (but possibly not shorter). Maybe people are already doing this, either way very I’m interested to know thoughts.
The requirements are relatively simple: if certain conditions are met, turn the light on, otherwise turn it off. However evaluating those conditions can get complicated. Ordering in particular is a problem.
So instead my approach is:
- Work out what inputs will affect the light, e.g. darkness, motion, contact
- Rely on a persistence addon to handle cool-off periods (e.g. any motion in last X minutes)
- Express the desired result as a single, clear
if
statement (e.g. isDark && (recent motion || recent door open)) - Wrap it in rule that will evaluate this condition should any input change, plus trigger on a cron to reevaluate coof-off
- Detect manual overrides, store as an item, and use as an additional input
var OnOffType expected_kitchen_table
rule "Kitchen table light control"
when
Item Motion_Kitchen_Table received update or
Item Contact_Kitchen_Door received update or
Item Dark_Indoors received update or
Item Light_Kitchen_Override received update or
Item Mode_Auto_Lights received update or
Time cron "*/10 * * * * ?"
then
// do nothing unless automatic lighting mode is enabled
if (Mode_Auto_Lights.state == ON) {
// if light has been overridden, simply set to overridden value
if (Light_Kitchen_Override.state !== UNDEF && Light_Kitchen_Override.state !== NULL) {
expected_kitchen_table = (Light_Kitchen_Override.state as OnOffType)
} else {
/* heart of the system - evaluate layers to determine if light should be on
* 1. is it dark, AND
* 2. motion detected recently, OR
* 3. door open, or opened recently
*/
if ( (Dark_Indoors.state == ON) &&
(((Motion_Kitchen_Table.state == ON) || (Motion_Kitchen_Table.lastUpdate() as DateTime).plusMinutes(4).isAfter(now)) ||
((Contact_Kitchen_Door.state == OPEN) || (Contact_Kitchen_Door.lastUpdate() as DateTime).plusMinutes(2).isAfter(now))
)) {
expected_kitchen_table = ON
} else {
expected_kitchen_table = OFF
}
}
// update actual item if different from expected
if (Light_Kitchen_Table.state != expected_kitchen_table) {
sendCommand(Light_Kitchen_Table, expected_kitchen_table)
}
}
end
/* Overrides can be detected simply by looking for updates to state that don't match
* expected, previously evaluated value */
rule "Light_Kitchen_Table override detection"
when
Item Light_Kitchen_Table received update
then
if (Light_Kitchen_Table.state != expected_kitchen_table) {
logInfo("override_kitchen", "Override detected using table switch")
Light_Kitchen_Override.postUpdate(Light_Kitchen_Table.state)
}
end
/* reset override item back to UNDEF once a reasonable amount of time has passed
* I've recently switched from using expire binding because a) it works across restarts
* and b) I can have different times depending if it was overridden to ON or OFF */
rule "Light_Kitchen_Table override reset"
when
Time cron "*/10 * * * * ?"
then
// is light overridden?
if (Light_Kitchen_Override.state !== UNDEF && Light_Kitchen_Override.state !== NULL) {
val DateTime lastUpdate = (Light_Kitchen_Override.lastUpdate() as DateTime)
// if turned ON manually, reset to automatic after 10 minutes
if (Light_Kitchen_Override.state == ON && lastUpdate.plusMinutes(10).isBefore(now)) {
logInfo("override_kitchen", "ON override reset")
Light_Kitchen_Override.postUpdate(UNDEF)
}
// if turned OFF manually, reset to automatic after 2 minutes
if (Light_Kitchen_Override.state == OFF && lastUpdate.plusMinutes(2).isBefore(now)) {
logInfo("override_kitchen", "OFF override reset")
Light_Kitchen_Override.postUpdate(UNDEF)
}
}
end
With this system I have lights turning on automatically around my house based on various inputs, with no concern over what order they occur in. I can have multiple lights using the same inputs if required.
One caveat is that the persistence addon that stores the Override
items must support storing UNDEF
values. MapDB, for example, does not. I’m currently using a simple JSON on disk persistence addon I put together.
Let me know thoughts