Updated: 2023-09-19 - made it work with jruby helper library 5.x
The idea is simple: turn on some lights for a set duration when triggered by motion sensor(s).
But I want to tweak the conditions, and how the lights are turned on (brightness, color temp) depending on complex conditions, and not have to repeat the timer / reset logic. Also each light / room / sensors have their own peculiarities that writing a generic rule doesn’t work for me.
I want to be able to add more motion trigger rules easily and quickly for new sensors / lights.
Example rules:
require 'personal/motiontrigger_dsl'
# The simplest trigger:
# turn on Hallway_Light3 when ToolCupBoard_Motion is detected
# The default duration is 2 minutes, but when more motion is detected
# the timer is extended by another 2 minutes since last motion
rule 'MT: Tool Cupboard' do
trigger ToolCupBoard_Motion
turn_on Hallway_Light3
end
# Conditional duration, make two rules
rule 'MT: Wardrobe non sleep mode' do
trigger Wardrobe_Motion
not_if { Sleep_Mode.on? }
turn_on Wardrobe_Light
end
# A different duration when Sleep_Mode is on
rule 'MT: Wardrobe sleep mode' do
trigger Wardrobe_Motion
only_if { Sleep_Mode.on? }
turn_on Wardrobe_Light, for: 75.seconds
end
# Multiple sensors can trigger a light
rule 'MT: Front Porch' do
trigger FrontPorch_Motion
trigger FrontPorch_Camera_Motion
trigger Driveway_Camera_Motion
only_if { Sun_Elevation.negative? }
turn_on FrontPorch_Light, for: 20.minutes
end
# Extender: an additional motion sensor that doesn't trigger the light
# but it will extend the timer when motion is detected
# Also when the light is turned on manually (not by motion trigger),
# it will turn off automatically in 1 hour.
rule 'MT: Study Room' do
trigger StudyRoom_Motion2
extender StudyRoom_Motion
turn_on StudyRoom_Light, for: 30.minutes, manual: 1.hour
end
# The hallway is a long L shape, with multiple different sensors in various rooms
# set up to trigger the hallway light
# Demonstrates the use of a block to be executed prior to turning on the light
# to set the colour temperature based on Sun Elevation (i.e. day vs night)
rule 'MT: Hallway Lights NON sleep mode' do
trigger Hallway_Motion
trigger Hallway_Motion2
trigger LaundryRoom_Motion2
trigger LoungeRoom_Motion
turn_on(Hallway_Light_Power, for: 1.hour) do
Hallway_Light_Dimmer << 50
Hallway_Light_CT << (Sun_Elevation.state < 10 ? 100 : 0)
end
end
# automatically turn off lights when it was turned on manually
# No triggers, just extenders
# The extender can be any changes in the TV Volume / app / channel / mute/unmute/play/pause
# gLoungeRoomTV is a group to which all the TV items belong
rule 'MT: LoungeRoom' do
extender LoungeRoom_Motion
extender gLoungeRoomTV.members, to: nil
turn_on LoungeRoom_Light1, manual: 1.hour
turn_on LoungeRoom_Light2, manual: 1.hour
end
Features:
- Have multiple motion sensors (or door contact sensors) to trigger a light (or multiple lights)
- Not limited to a strict item naming convention / pattern. FrontPorch_Motion sensor can trigger the Garage_Light if you wish.
- The default duration is 2.minutes but it can be overridden for each trigger case
- Multiple conditions can be specified (using an actual code / proc) to determine whether the trigger should take place. E.g. Check for lux level, or time of day, sun elevation, etc.
- Pre-action can be specified for each light, e.g. to adjust dimmer level
- When the relevant light(s) were turned on manually (not by the trigger), specify a longer duration (I call it
manual_duration
for the lack of a better name) to turn it off. This avoids the annoying premature darkness when working in the room. - Optional debounce feature, to prevent immediate re-triggering after the light was turned off manually
Comments/suggestions both on the coding (I’m a Ruby beginner, so please don’t hesitate to criticise my coding style) and the general concept / functionality would be most welcome.
The motiontrigger_dsl.rb
is a library to be placed in conf/automation/ruby/lib/personal/
#
# A quick and easy way to build motion trigger rules.
#
# Example rule:
#
# # Turn on the light for the default duration (2 minutes)
# rule 'Simple rule' do
# trigger LivingRoom_Motion
# turn_on LivingRoom_Light
# end
#
# rule 'MT: Laundry Room' do
# trigger LaundryRoom_Motion
# extender LaundryRoom_Motion2 # this sensor picks up motion outside the laundry room too
# only_if { Sun_Elevation.negative? || LaundryRoom_Light.on? || LaundryRoom_Lux.to_f < 15 }
# turn_on LaundryRoom_Light
# end
#
# rule 'MT: Hallway Lights NON sleep mode' do
# trigger Hallway_Motion
# trigger Hallway_Motion2
# trigger LaundryRoom_Motion2
# trigger LoungeRoom_Motion
# only_if { Sun_Elevation < 10 || ToolCupboard_Lux.to_f < 10 }
# not_if Sleep_Mode
# turn_on Hallway_Light_Power, for: 1.hour do
# Hallway_Light_Dimmer << 50
# Hallway_Light_CT << (Sun_Elevation < 10 ? 100 : 0)
# end
# end
#
# rule 'MT: Hallway Lights sleep mode' do
# trigger Hallway_Motion
# trigger Hallway_Motion2
# trigger LaundryRoom_Motion2
# trigger LoungeRoom_Motion
# trigger BedRoom1_Motion
# only_if { Sun_Elevation < 10 }
# only_if Sleep_Mode
# turn_on(Hallway_Light_Power) do
# Hallway_Light_Dimmer << 1
# Hallway_Light_CT << 100
# end
# end
#
#
# TRIGGER DURATION
#
# By default, the light will turn on for 2 minutes
# To specify a different duration, pass a "for" argument to turn_on: e.g.
#
# turn_on LivingRoom_Light, for: 10.minutes
#
#
# WHEN THE LIGHTS WERE MANUALLY TURNED ON
#
# When the lights specified by turn_on gets turned on manually (e.g. from the wall switch)
# and not by the trigger sensors, they will remain on for a default of 1 hour.
# To override this, pass a "manual" argument to turn_on, e.g.
#
# turn_on LivingRoom_Light, manual: 30.minutes
#
# This will automatically turn off the light after 30 minutes, when it was turned on manually (e.g. at the wall switch)
# To prevent any automatic turn off, specify "manual: nil"
#
#
# TRIGGER CONDITIONS / GUARDS
#
# Any of the standard rule trigger guards can be used as well
# Example:
#
# Only automatically turn on the lights after 6pm
# rule 'Turn on light by motion sensor at night' do
# trigger LivingRoom_Motion
# only_if { TimeOfDay.now > '6pm' }
# turn_on LivingRoom_Light, for: 15.minutes
# end
#
#
# Helper functions for motiontrigger_dsl
module MotionTriggers
module DSL
extend(OpenHAB::DSL)
#
# Defines the sensor(s) that will turn on the lights
# When the sensor(s) received an update, turn on the item(s) (e.g. lights)
# If they were already on, their timer will be extended
#
# @param [Item] items A list of items that will trigger the rule
# @param [State] from match for trigger
# @param [State] to match for trigger
#
def trigger(*item, from: nil, to: OPEN)
if from
changed(*item, from: from, to: to, attach: :trigger)
else
updated(*item, to: to, attach: :trigger)
end
end
#
# Defines the sensors that only extend the timers, but not turn on the lights.
# When an extender item received an update, existing trigger timers will be restarted
# so that the controlled lights that were already on continue to stay on.
# An extender will not turn on lights that weren't already on.
#
# @param [Array] items acting as extenders
# @param [State] to match the trigger
#
def extender(*item, to: OPEN)
updated(*item, to: to, attach: :extender)
end
def turn_on(item, for: 2.minutes, manual: 1.hour, debounce: 3.second, &block)
MotionTriggers.add_managed_item(item, manual)
duration = binding.local_variable_get(:for)
run do |event|
if item.on?
logger.debug "#{item} extended by #{event.item}"
# item.ensure.on # force send the command
timers.reschedule(item)
timers.reschedule(MotionTriggers.manual_timer_id(item))
next
end
next unless event.attachment == :trigger
next if MotionTriggers.debounced(item, debounce)
next if block_given? && !yield # Execute block before turning on the item
logger.info("#{item} triggered by #{event.item} for #{duration}")
OpenHAB::DSL.after(duration, id: item) do
item.ensure.off
logger.info("#{item} turned off automatically after being triggered")
MotionTriggers.update_item_time(item)
end
item.on
end
end
OpenHAB::DSL::Rules::BuilderDSL.prepend(self)
end
# Stores the Time when the item was switched off by any means
@turned_off = Hash.new(Time.at(0))
# The last time the item was turned off by a timer
@turned_off_by_timer = Hash.new(Time.at(0))
@managed_items = {}
def self.add_managed_item(item, manual_duration) = @managed_items[item] = manual_duration
def self.cancel_timers(item)
OpenHAB::DSL.timers.cancel(item)
OpenHAB::DSL.timers.cancel(manual_timer_id(item))
end
def self.update_item_time(item)
@turned_off_by_timer[item] = Time.now
end
# Prevents the motion sensors from turning the light back on
# right after it was turned off manually
def self.debounced(item, debounce)
return if debounce.nil?
return if (@turned_off[item] - @turned_off_by_timer[item]) < 1 # Don't debounce if it was turned off by the timer
return if (seconds_since_turned_off = Time.now - @turned_off[item]) > debounce.seconds
logger.debug("Debounced #{item} because it was just turned off #{seconds_since_turned_off} seconds ago")
true
end
def self.create_item_rules
@managed_items.each { |item, duration| create_item_rule(item, duration) }
end
def self.create_item_rule(item, duration)
OpenHAB::DSL.rule "Motion trigger Manual rule #{item.name}" do
changed item, to: [ON, OFF]
run do |event|
if event.on?
create_manual_activation_timer(item, duration)
else
cancel_timers(item)
@turned_off[item] = Time.now if event.was_on?
end
end
end
create_manual_activation_timer(item, duration) if item.on?
end
def self.manual_timer_id(item) = { manual: item }
def self.create_manual_activation_timer(item, duration)
return if OpenHAB::DSL.timers.include?(item) || duration.nil? # skip if automatic timer is active
logger.debug("#{item} was manually turned on. Creating a timer to turn it off after #{duration}")
OpenHAB::DSL.after(duration, id: manual_timer_id(item)) do
item.ensure.off
logger.info("#{item} turned off automatically after manually turned on for #{duration}")
@turned_off_by_timer[item] = Time.now
end
end
OpenHAB::DSL.script_loaded { create_item_rules }
end