My story:
I have a zoo of actors and sensors for Illumination at home. Lamps controlled through homematic actors, Lightify and Tradfri ones. The lights are a mix of dimming, switched (ON/OFF), white temperature and colour ones . Sensors (switches) from HomeMatic traditional and IP with motion detection and without. Motion sensors have and environment Illumination sensor included.
Expectation:
- As much as possibly uniformity of operation across all type of lights (lamps) and controls (switches, movement sensors).
- Keep it simple and understandable
- Support day time scenarios e.g. bed time, tv time, day time
Topics I will touch:
- Unified switch paradigm (toggle, dimm)
- Movement initiated lights
- Day time scenarios
- Light parameters (min, max, timeouts,…)
- Implementation
Unified switch paradigm
My switches support following channel actions:
- SHORT_PRESSED initiates a toggle light action, between ON/OFF or 0 and max dim value for current day time scenario. Switches dimmed light off and if motion light is active turns it on permanent.
- LONG_PRESSED initiates a dim light action, from 0% to min dim value and increments of dim steps to 100%
- DOUBLE_PRESSED used for special purposes
Movement initiated lights
Following starting requirements are set:
- All my movement sensors are set to send a movement ON and timeout after 15s to movement OFF is no movement detected.
- Managing of movement light timeout is individual per light and done by the rule only
- Each individual control takes care about illumination thresholds
- Each individual control adapts light between min/max dim value based on environmental illumination, the darker the lower the dim value
- Each individual control takes care about day time scenarios (daytime, bedtime, tv time, etc) with corresponding on/off/dim values
Day time scenarios
- Day Time is the default scenario if no other scenario active
- Bed Time is a schedule and manual overridden scenario which indicates sleeping or at most visiting the bathroom and being sure that no flashes of motion operated lights would happen therefore no motion light.
- TV Time is the time when it should be Bed Time, the TV though is still running, getting popcorn or visiting bath room should operate motion lights at lowest possible level, if at all
Light parameters
There are global parameters which are used if no parameter is set on light individual level.
- Illumination Threshold Value - Maximum value where motion operated light react on movement
- All Illumination - Illumination is ignored, motion always activates light
- **All Day Light ** - Day time scenarios are ignored motion always activates light
- Timeout OFF Time - Time in seconds for a motion operated light to go off if no more motion detected
- Timeout Forgot Light - Time in seconds a manually toggled/dimmed should go off
- Minimum Dim Value - Lowest daytime dim values for a dimmed and motion light with lowest illumination
- Maximum Dim Value - Highest daytime dim values for a dimmed and motion light with highest illumination
- Bed Time Dim Value - Bed time dim value for a dimmed and motion light with all day enabled or when tv scenario on
- Dimm Step - Incremental step for dimmed lights for subsequent LONG_PRESSED
- Double Click Timeout - maximum time between subsequent press to be recognised as double press action
Implementation
Rules do not support library function, therefore I decided to extensively use lambdas. This has some constraints which lambdas imply:
- Parameters a call by value
- Only Items are callable as global variables
- Everything has to be passed as parameter including lambdas calling lambdas
- Synchronisation of threads not possible by Java locks as try catch final not guaranteed
as a result the implementation is:
- Using of thread safe hashMap for passing parameters back and forth including thread sync
- Passing all lambdas as parameters
Items
The isBedTime and isTVOn are managed by Rules which are not listed here, just simple time schedule routines. The light items are just listed for completeness.
import java.util.Map
// Hasmap Key PostFixes not usable in lambdas
/*
val String ITEM = "Light Item (used in force/cleanup routine)"
val String ITST = "Item State (used in force/cleanup routine)"
val String ITSF = "Item State Force, not yet used"
val String ITIL = "Ilumination Item"
val String ITCT = "Color Temp Item"
val String ITMO = "Motion Item"
val String ILUT = "Illumination Threshold"
val String ALLD = "All Day Light, No Time Limit"
val String ALIL = "All Illumination, No ilumination Limit "
val String TIMR = "Timer"
val String TIEX = "Timer Expiring At"
val String TIMO = "Timeout Motion Light OFF"
val String TOFL = "Timeout Forgot Light"
val String MIDI = "Minimum Dim Value"
val String MADI = "Maximum Dim Value"
val String BTDI = "Bed Time Dimm Value"
val String DIST = "Dimm Step"
val String DCTO = "Double Click Timeout"
val String DCLC = "Double Click Lock" //0=idle, 1=short_press_waiting, 2=in_execution
val String LAST = "Last Action" // 0=off/idle 1=toggle, 2=dimm, 3=motion
*/
val java.util.concurrent.ConcurrentHashMap<String, Object> lightParams = new java.util.concurrent.ConcurrentHashMap()
val hashMapMgr = [ Map<String, Object> lightParams, GenericItem light, String postFix, Object value |
try{
// no value supplied, return stored value
if (value === null){
// if not value stored for item, return global default value
return if (lightParams.containsKey(light.label+postFix)) lightParams.get(light.label+postFix) else lightParams.get(postFix)
} else {
// set the supplied value to hashMap
lightParams.put(light.label+postFix, value)
}
} catch(Exception e) {
logInfo("hashMapMgr", " Exception : " + e.getLocalizedMessage)
}
return null
]
val lightManager = [ GenericItem light,
Number level,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object> hashMapMgr,
Map<String, Object> lightParams|
try{
// is it a dimmer color type, hadle it through % values
if (light.acceptedCommandTypes.contains(PercentType) ) {
val calcLevel = if (level > 0 && isBedTime.state == ON)
hashMapMgr.apply(lightParams, light, "BTDI", null) as Number
else
if (level > 0 && level < hashMapMgr.apply(lightParams, light, "MIDI", null) as Number)
hashMapMgr.apply(lightParams, light, "MIDI", null) as Number
else
if (level > hashMapMgr.apply(lightParams, light, "MADI", null) as Number)
hashMapMgr.apply(lightParams, light, "MADI", null) as Number
else
level
logInfo("lightManager", " <"+light.label + "> : Percent Type - Current Level: " + light.state + " Target Level: " + calcLevel.toString)
// send target level to dimm/color item
sendCommand(light, calcLevel)
}
else {
logInfo("lightManager", " <"+light.label + "> : Switch Type - Current Level: " + light.state + " Target Level: "+ level)
// send ON/OFF to switch item
sendCommand(light, if (level > 0) ON else OFF)
}
// store target level for cleanup and enforcement
hashMapMgr.apply(lightParams, light, "ITST", level)
hashMapMgr.apply(lightParams, light, "ITEM", light)
} catch(Exception e) {
logInfo("lightManager", " Exception : " + e.getLocalizedMessage)
sendCommand(light, "0")
}
return true
]
val lightTimerManager= [ GenericItem light,
Number level,
Functions$Function4<GenericItem,
Number,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object>,
Map<String, Object>,
Boolean> lightMgr,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object> hashMapMgr,
Map<String, Object> lightParams |
try{
// value for motion or forgot light timeout
val Number timeout = if (hashMapMgr.apply(lightParams, light, "LAST", null) as Number == 3)
hashMapMgr.apply(lightParams, light, "TIMO", null) as Number // motion timeout
else
hashMapMgr.apply(lightParams, light, "TOFL", null) as Number // forgot light timeout
val GenericItem motion = hashMapMgr.apply(lightParams, light, "ITMO", null) as GenericItem
// is the light currently on
if (level != 0){
// Is there a timer already running for item (e.g. motion) and timers expiration > now
if ((hashMapMgr.apply(lightParams, light, "TIMR", null)!== null ) && (hashMapMgr.apply(lightParams, light, "TIEX", null) as DateTime > now )){
// Is the current running timer timeout shorter that new needed
if ((hashMapMgr.apply(lightParams, light, "TIEX", null) as DateTime) < now.plusSeconds(timeout.intValue)){
// yes, so extend timer and store new timeout in hash table
logInfo("lightTimerManager", " <"+light.label + "> : Extend Timer for " + timeout.toString + "s")
hashMapMgr.apply(lightParams, light, "TIEX", now.plusSeconds(timeout.intValue))
(hashMapMgr.apply(lightParams, light, "TIMR", null) as Timer).reschedule(now.plusSeconds(timeout.intValue))
}
}
else {
// either no timer runnimg or expired, create a new one and store new timeout in hash table
logInfo("lightTimerManager", " <"+light.label + "> : Create Timeout for "+ timeout.toString + "s")
hashMapMgr.apply(lightParams, light, "TIEX", now.plusSeconds(timeout.intValue))
hashMapMgr.apply(lightParams, light, "TIMR",
createTimer(now.plusSeconds(timeout.intValue), [|
if (motion !== null && motion.state == ON) {
// timer expired motion still active, extend time again, again, ....
logInfo("lightTimerManager", "<"+light.label + "> : Timer Expired, Though Motion Still Active")
(hashMapMgr.apply(lightParams, light, "TIMR", null) as Timer).reschedule(now.plusSeconds(timeout.intValue))
hashMapMgr.apply(lightParams, light, "TIEX", now.plusSeconds(timeout.intValue))
}
else {
// timer expired turn light off
logInfo("lightTimerManager", "<"+light.label + "> : Timer Expired")
lightMgr.apply(light, 0, hashMapMgr, lightParams)
}
]))
}
} else if (hashMapMgr.apply(lightParams, light, "TIMR", null)!== null) {
// light turned off externally, button, update/cancel timer update hash table
logInfo("lightTimerManager", " <"+light.label + "> : Cancel Timer")
(hashMapMgr.apply(lightParams, light, "TIMR", null) as Timer).cancel()
hashMapMgr.apply(lightParams, light, "TIEX", now)
}
} catch(Exception e) {
logInfo("lightTimerManager", " Exception : " + e.getLocalizedMessage)
sendCommand(light, "0")
}
return true
]
val dimmLight= [ GenericItem light,
Functions$Function4<GenericItem,
Number,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object>,
Map<String, Object>,
Boolean> lightMgr,
Functions$Function5<GenericItem,
Number,
Functions$Function4<GenericItem,
Number,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object>,
Map<String, Object>, Boolean>,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object>,
Map<String, Object>,
Boolean> lightTimerMgr,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object> hashMapMgr,
Map<String, Object> lightParams|
try{
val Number lastOp = hashMapMgr.apply(lightParams, light, "LAST", null) as Number
// store dimming als last operation, used by other lamdas
hashMapMgr.apply(lightParams, light, "LAST", 2)
val Number level =
if (light.getStateAs(OnOffType) == OFF)
// is off set minum value, lightmanager takes care about bed time, etc
hashMapMgr.apply(lightParams, light, "MIDI", null) as Number
else
// is on, if prevois movement set to max else increment by dimm increment, light manager takes care about bed time limits, etc
if ( light.getStateAs(OnOffType) == OFF || lastOp == 3)
100
else
hashMapMgr.apply(lightParams, light, "ITST", null) as Number + hashMapMgr.apply(lightParams, light, "DIST", null) as Number
lightMgr.apply(light, level , hashMapMgr, lightParams)
// notify time manager to take care about auto off timers
lightTimerMgr.apply(light, level, lightMgr, hashMapMgr, lightParams)
} catch(Exception e) {
logInfo("toggleLight", " Exception : " + e.getLocalizedMessage)
sendCommand(light, "0")
}
return true
]
val toggleLight= [ GenericItem light,
Functions$Function4<GenericItem,
Number,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object>,
Map<String, Object>,
Boolean> lightMgr,
Functions$Function5<GenericItem,
Number,
Functions$Function4<GenericItem,
Number,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object>,
Map<String, Object>, Boolean>,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object>,
Map<String, Object>,
Boolean> lightTimerMgr,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object> hashMapMgr,
Map<String, Object> lightParams |
// retvalue is used to reset timer if light switched off
try{
hashMapMgr.apply(lightParams, light, "LAST", 1)
// manage double_pressed event
if (hashMapMgr.apply(lightParams, light, "DCLC", null) !== null){
// double click enabled for item
if (hashMapMgr.apply(lightParams, light, "DCLC", null) == 0){
// no 1st click inaction, lock it a 1st click and create timer to wait for 2nd click
hashMapMgr.apply(lightParams, light, "DCLC", 1)
createTimer(now.plusSeconds(hashMapMgr.apply(lightParams, light, "DCTO", null) as Integer), [|
// timer expired, did a 2nd click came in, if no lock the 1st click and execute
if (hashMapMgr.apply(lightParams, light, "DCLC", null) == 1) {
hashMapMgr.apply(lightParams, light, "DCLC", 2)
lightMgr.apply(light, if ( light.getStateAs(OnOffType) == OFF) 100 else 0, hashMapMgr, lightParams)
}
// reset to initial state
hashMapMgr.apply(lightParams, light, "DCLC", 0)
])
return true
}
else
// 2nd click, check if 1st click still waiting, if so lock it as 2nd click,
// else get out and leave it to 1st click to complete
if (hashMapMgr.apply(lightParams, light, "DCLC", null) == 1)
hashMapMgr.apply(lightParams, light, "DCLC", 2)
else
return true
}
// only reached when no double click enabled or 2nd click came in before 1st click expires
lightMgr.apply(light, if ( light.getStateAs(OnOffType) == OFF) 100 else 0, hashMapMgr, lightParams)
// notify time manager to take care about auto off timers
lightTimerMgr.apply(light, if ( light.getStateAs(OnOffType) == OFF) 100 else 0 ,lightMgr, hashMapMgr, lightParams)
} catch(Exception e) {
logInfo("toggleLight", " Exception : " + e.getLocalizedMessage)
lightMgr.apply(light, 0, hashMapMgr, lightParams)
}
return true
]
val motionOnLight= [ GenericItem light,
Functions$Function4<GenericItem,
Number,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object>,
Map<String, Object>,
Boolean> lightMgr,
Functions$Function5<GenericItem,
Number,
Functions$Function4<GenericItem,
Number,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object>,
Map<String, Object>, Boolean>,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object>,
Map<String, Object>,
Boolean> lightTimerMgr,
Functions$Function4<Map<String, Object>, GenericItem, String, Object, Object> hashMapMgr,
Map<String, Object> lightParams|
try{
logInfo("motionOnLight", " <"+light.label )
// enable to override motion on by button on (higher density + longer timeout)
hashMapMgr.apply(lightParams, light, "LAST", 3)
val Number iluItemState = (hashMapMgr.apply(lightParams, light, "ITIL", null) as GenericItem).state as Number
val Number iluLimit = hashMapMgr.apply(lightParams, light, "ILUT", null) as Number
val Boolean allDay = hashMapMgr.apply(lightParams, light, "ALLD", null) as Boolean
val Boolean allIll = hashMapMgr.apply(lightParams, light, "ALIL", null) as Boolean
var Number targetLevel = 0
// Is is not bed time or light night TV or all time light and it is dark enough or ignoring illumination
if ((isBedTime.state == OFF || isTVOn.state == ON || allDay) && (allIll || (iluItemState) <= iluLimit)) {
// out of the threshold calculate min 5 and max 100 values
targetLevel = 5 + ((iluItemState + 5) * 95 / iluLimit)
// only set value if current light power is lower, timeout extension managed by time manager
if (light.getStateAs(DecimalType)*100 < targetLevel) lightMgr.apply(light, targetLevel, hashMapMgr, lightParams)
}
// notify time manager to take care about auto off timers
lightTimerMgr.apply(light, targetLevel, lightMgr, hashMapMgr, lightParams)
} catch(Exception e) {
logInfo("motionOnLight", " Exception : " + e.getLocalizedMessage)
sendCommand(light, "0")
}
return true
]
rule "Initiate lightControl.rules"
when
System started
then
sendCommand(gColTemp, 100)
sendCommand(ToiletLightWallHM, ON)
sendCommand(BathRoomLightTopHM, ON)
lightParams.put("ITST", 0)
lightParams.put("TIMO", 60)
lightParams.put("TOFL", 60*60*12)
lightParams.put("MIDI", 5)
lightParams.put("BTDI", 5)
lightParams.put("MADI", 100)
lightParams.put("DIST", 25)
lightParams.put("ALLD", false)
lightParams.put("ALIL", false)
lightParams.put("ITSF", false)
lightParams.put("ILUT", 100)
lightParams.put("LAST", 0) // idle
lightParams.put("DCTO", 1)
logInfo("Initiate lightControl.rules", "finished")
end
// ***************************************************************
// ************************ Rules Start **************************
// ***************************************************************
// ***************************************************************
// ************************ Rules Start **************************
// ***************************************************************
rule "lightControl Clean-Up .rules"
when
Time cron " 0 0/5 * 1/1 * ? *"
then
/*
val lightParamsKeys = lightParams.keySet()
lightParamsKeys.forEach[ key |
if (key.contains("ITST")){
var String itemKey = key.replace("ITST", "ITEM")
itemKey.replace("ITST", "ITEM")
if ((lightParams.get(itemKey) as GenericItem).getStateAs(OnOffType) == ON && lightParams.get(key) as Number == 0){
lightManager.apply(lightParams.get(itemKey) as GenericItem, 0, hashMapMgr, lightParams)
loggInfo("lightControl Clean-Up.rules", " mismatch light: "+ key.replace("ITST", "")+" value: "+ lightParams.get(key).toString)
logggggDebug("loggerName", "message")
}
}
]
*/
logInfo("lightControl Clean-Up .rules", "finished")
end
// ***************************************************************
// ************************ Entry/Outside **************************
// ***************************************************************
rule "Initiate Entry-Outside.rules"
when
System started
then
lightParams.put(EntryLight.label+"MIDI", 30)
lightParams.put(EntryLight.label+"ITIL", EntryIlumination)
lightParams.put(EntryLight.label+"ITMO", EntryMotion)
lightParams.put(EntryOutsideLight.label+"ALLD", true)
lightParams.put(EntryOutsideLight.label+"ITIL", EntryOutsideIlumination)
lightParams.put(EntryOutsideLight.label+"ITMO", EntryOutsideMotion)
logInfo("Initiate Entry-Outside.rules", "finished")
end
rule "Entry toggle light"
when
Channel "homematic:HMIP-WRC2:3014F711A0001F58A992FB22:00019709AAA0C6:1#BUTTON" triggered SHORT_PRESSED or
Channel "homematic:HMIP-WRC2:3014F711A0001F58A992FB22:00019709AAA0C6:1#BUTTON" triggered DOUBLE_PRESSED
then
toggleLight.apply(EntryLight, lightManager, lightTimerManager, hashMapMgr, lightParams)
end
rule "Entry Outside toggle light"
when
Channel "homematic:HMIP-WRC2:3014F711A0001F58A992FB22:00019709AAA0C6:2#BUTTON" triggered SHORT_PRESSED or
Channel "homematic:HMIP-WRC2:3014F711A0001F58A992FB22:00019709AAA0C6:2#BUTTON" triggered DOUBLE_PRESSED
then
toggleLight.apply(EntryOutsideLight, lightManager, lightTimerManager, hashMapMgr, lightParams)
end
rule "Entry dimm light"
when
Channel "homematic:HMIP-WRC2:3014F711A0001F58A992FB22:00019709AAA0C6:1#BUTTON" triggered LONG_PRESSED or
Channel "homematic:HMIP-WRC2:3014F711A0001F58A992FB22:00019709AAA0C6:2#BUTTON" triggered LONG_PRESSED or
Channel "homematic:HmIP-SMI55:3014F711A0001F58A992FB22:0014D709AEF787:1#BUTTON" triggered CONT or
Channel "homematic:HmIP-SMI55:3014F711A0001F58A992FB22:0014D709AEF787:2#BUTTON" triggered CONT
then
dimmLight.apply(EntryLight, lightManager, lightTimerManager, hashMapMgr, lightParams)
end
rule "Entry motion light "
when
Item EntryMotion changed to ON
then
motionOnLight.apply(EntryLight, lightManager, lightTimerManager, hashMapMgr, lightParams)
end
Ported to Python NGRE
I invested some time in order to “lift-and-shift” the code from DSL to NGRE.
As the paradigms and setup were ok I stuck to the external definition of rules. Metadata seemed to me no inflexible for the job.
lightControl.py (reduced to my test lights, I have a few dozens:
from core.log import logging, LOG_PREFIX
from core.rules import rule
import imp
import personal.LightControlLib
imp.reload(personal.LightControlLib)
import personal.kitchenCookingVentilationLib
imp.reload(personal.kitchenCookingVentilationLib)
from personal.LightControlLib import lightItemManager
log = logging.getLogger(LOG_PREFIX + ".light.control.log")
'''
Light Parameter Abbreviations
ILUT Ilumination Threshold
ALLD all day light, ignore daytime and other schedule (0/1)
NTOL nightime only light, on only when bedtime (0/1)
ALIL no ilimination limit, ignore ilumination threshold (0/1)
TIMO timeout for motion events
TOFL timeout forgot light
MIDI minimum dim value
MADI maximum dim value
BTDI bed time dim value
DIST dim step
ONOF set light type to on/of only (0/1)
DOCL cleanup and enforce light state (0/1)
CLUT cleanup and enforce state timer
Trigger Type Abreviations
CRON CronTrigger** - fires based on cron expression
ISCT ItemStateChangeTrigger** - fires when the specified Item's state changes
ISUT ItemStateUpdateTrigger** - fires when the specified Item's state is updated
ICOT ItemCommandTrigger** - fires when the specified Item receives a Command
GEVT GenericEventTrigger** - fires when the specified occurs
IEVT ItemEventTrigger** - fires when am Item reports an event (based on ``GenericEventTrigger``)
TEVT ThingEventTrigger** - fires when a Thing reports an event (based on ``GenericEventTrigger``)
TSCT ThingStatusChangeTrigger** - fires when the specified Thing's status changes **(requires S1636, 2.5M2 or newer)**
TSUT ThingStatusUpdateTrigger** - fires when the specified Thing's status is updated **(requires S1636, 2.5M2 or newer)**
CEVT ChannelEventTrigger** - fires when a Channel reports an event
DEVT DirectoryEventTrigger** - fires when a directory's contents changes
IRET ItemRegistryTrigger** - fires when the specified Item registry event occurs
IADT ItemAddedTrigger** - fires when an Item is added (based on ``ItemRegistryTrigger``)
IRET ItemRemovedTrigger** - fires when an Item is removed (based on ``ItemRegistryTrigger``)
IUPT ItemUpdatedTrigger** - fires when an Item is updated (based on ``ItemRegistryTrigger``)
STUT StartupTrigger** - fires when the rule is activated **(implemented in Jython and requires S1566, 2.5M2 or newer)**
'''
LIGHT_ATTRIBUTES='''
{
"TestGroup":
[
{
"ONOF" : 0,
"MIDI" : 1,
"TIMO" : 10,
"ITIL" : "iluminationTest"
}
,
{
"TOG" : [
["CET", "homematic:HmIP-SMI55:3014F711A061A7D5698CE994:0014D709AEF7C0:1#BUTTON", "SHORT_PRESSED", ""],
["CET", "homematic:HmIP-SMI55:3014F711A061A7D5698CE994:0014D709AEF7C0:2#BUTTON", "SHORT_PRESSED", ""]
],
"DIM" : [
["CET", "homematic:HmIP-SMI55:3014F711A061A7D5698CE994:0014D709AEF7C0:1#BUTTON", "LONG_PRESSED", ""],
["CET", "homematic:HmIP-SMI55:3014F711A061A7D5698CE994:0014D709AEF7C0:2#BUTTON", "LONG_PRESSED", ""]
]
}
],
"TestGroupSceneMix":
[
{
"ONOF" : 0,
"MIDI" : 1,
"TIMO" : 10,
"ITIL" : "iluminationTest"
}
,
{
"MOV" : [
["ISCT", "motionTest", "OFF", "ON"]
]
}
],
"testGU10L":
[
{
"ONOF" : 1,
"MIDI" : 7,
"ALLD" : 1,
"ILUT" : 100,
"ITIL" : "iluminationTest"
}
,
{
"TOG" : [
["CET", "deconz:switch:d6dfbf93:ccccccfffe51c077011000:buttoneven", "2002", ""]
],
"DIM" : [
["CET", "deconz:switch:d6dfbf93:ccccccfffe51c077011000:buttoneven", "2001", ""]
],
"MOV" : [
["ISCT", "motionTestOFF", "OFF", "ON"]
]
}
],
"testGU10M":
[
{
"ONOF" : 0,
"MIDI" : 7,
"ITIL" : "iluminationTest"
}
,
{
"TOG" : [
["CET", "deconz:switch:d6dfbf93:ccccccfffe51c077011000:buttoneven", "1002", ""]
],
"DIM" : [
["CET", "deconz:switch:d6dfbf93:ccccccfffe51c077011000:buttoneven", "1001", ""]
],
"MOV" : [
["ISCT", "motionTestOFF", "OFF", "ON"]
]
}
]
}
'''
lightsMgr = lightItemManager(LIGHT_ATTRIBUTES)
@rule("Light Manager Rule", description="This is an rule for controlling lights")
class LightManagerRule(object):
def __init__(self):
self.triggers = lightsMgr.getAllTriggers()
def execute(self, module, inputs):
lightsMgr.execute(module, inputs)
def scriptUnloaded():
log.info("************* Unload ********* ")
# lightsMgr.cleanUpTimer()
lightControlLib.py located in lib/personal
# edit 23-03
from core.triggers import ChannelEventTrigger, ItemEventTrigger, ItemStateChangeTrigger
#from core.actions import ScriptExecution
#from org.joda.time import DateTime
from core.log import logging, LOG_PREFIX
from threading import Timer
import yaml
from core.jsr223.scope import events, items, itemRegistry, PercentType#, OnOffType, IncreaseDecreaseType, RefreshType
import inspect
import traceback
'''
Light Parameter Abbreviations
ILUT Ilumination Threshold
ALLD all day light, ignore daytime and other schedule (0/1)
NTOL nightime only light, on only when bedtime (0/1)
ALIL no ilimination limit, ignore ilumination threshold (0/1)
TIMO timeout for motion events
TOFL timeout forgot light
MIDI minimum dim value
MADI maximum dim value
BTDI bed time dim value
UDDI up/down dimm enable
DIST dim step
ONOF set light type to on/of only (0/1)
DOCL cleanup and enforce light state (0/1)
CLUT cleanup and enforce state timer
CTON color temperature on
CTVL color temperature value
ITCT item color temperature
ITIL item ilumination
ITTR triggering item
Trigger Type Abreviations
CRON CronTrigger** - fires based on cron expression
ISCT ItemStateChangeTrigger** - fires when the specified Item's state changes
ISUT ItemStateUpdateTrigger** - fires when the specified Item's state is updated
ICOT ItemCommandTrigger** - fires when the specified Item receives a Command
GEVT GenericEventTrigger** - fires when the specified occurs
IEVT ItemEventTrigger** - fires when am Item reports an event (based on ``GenericEventTrigger``)
TEVT ThingEventTrigger** - fires when a Thing reports an event (based on ``GenericEventTrigger``)
TSCT ThingStatusChangeTrigger** - fires when the specified Thing's status changes **(requires S1636, 2.5M2 or newer)**
TSUT ThingStatusUpdateTrigger** - fires when the specified Thing's status is updated **(requires S1636, 2.5M2 or newer)**
CEVT ChannelEventTrigger** - fires when a Channel reports an event
DEVT DirectoryEventTrigger** - fires when a directory's contents changes
IRET ItemRegistryTrigger** - fires when the specified Item registry event occurs
IADT ItemAddedTrigger** - fires when an Item is added (based on ``ItemRegistryTrigger``)
IRET ItemRemovedTrigger** - fires when an Item is removed (based on ``ItemRegistryTrigger``)
IUPT ItemUpdatedTrigger** - fires when an Item is updated (based on ``ItemRegistryTrigger``)
STUT StartupTrigger** - fires when the rule is activated **(implemented in Jython and requires S1566, 2.5M2 or newer)**
'''
log = logging.getLogger(LOG_PREFIX + ".light.control.log")
def log_trace_back():
frame = inspect.currentframe()
stack_trace = traceback.format_stack(frame)
log.info(''.join(stack_trace))
LIGHT_GLOB_SETTING = '''
{
"ILUT" : 120,
"ALLD" : 0,
"NTOL" : 0,
"ALIL" : 0,
"TIMO" : 60,
"TOFL" : 86400,
"UDDI" : 0,
"MIDI" : 5,
"MADI" : 100,
"BTDI" : 5,
"DIST" : 15,
"ONOF" : 0,
"DOCL" : 1,
"CLUT" : 120,
"CTVL" : 100,
"CTON" : 0,
"ITCT" : "None",
"ITIL" : "None",
"ITTR" : "None"
}
'''
class lightTimer(object):
"""
Class for managing the item (light) timeouts:
- item specific timeouts
- celanup of running timers
"""
def __init__(self):
self.timer = None
def start(self, time_out, lambda_fnc):
"""
start timeout for item (light):
- toggle (TOG) dimming (DIM) -> timeout forgot light (TOFL)
- movement light -> timeout movemet (TIMO)
"""
#if a time already running, cancel it
if self.timer is not None:
self.timer.cancel()
# set and start timer
self.timer = Timer(time_out, lambda_fnc)
self.timer.start()
def cancel(self):
if self.timer != None:
self.timer.cancel()
class lightAttributes(object):
"""
Class for managing the item (light) attributes:
- item specific attributes
- global attributes
- gui changable attributes
"""
def __init__(self, attr):
"""
initiate item specific and global attributes
"""
self.attr = attr
# set default attr values from LIGHT_DEFAULTS group members
self.global_attr = yaml.safe_load(LIGHT_GLOB_SETTING)
# attributes managed trhough UI
self.ui_default_attr = {}
def getGlobAttr(self, attr_name):
"""
return globaly defined attribute
"""
return int(str(self.global_attr[attr_name]))
def getItemAttr(self, attr_name):
"""
return item specific attribute if exist
else
return the globaly defined attribute
"""
if str(attr_name) in self.attr:
return int(self.attr[attr_name]) if str(self.attr[attr_name]).isnumeric() else self.attr[attr_name]
else:
return int(self.global_attr[attr_name]) if str(self.global_attr[attr_name]).isnumeric() else self.global_attr[attr_name]
def setItemAttr(self, attr_name, value):
"""
set item specific attribute
else
return the globaly defined attribute
"""
self.attr[attr_name] = value
def loadUIDefaults(self):
"""
load UI defined attributes over global attributes
"""
self.ui_default_attr = {str(item.name).split("_")[2] :item.state for item in itemRegistry.getItem("LIGHT_DEFAULTS").members}
class lightItem(object):
"""
Class for managing the item (light) states
- reaction on trigger comming from the basic rule
- cleanup of lazy/not reacting lights
- respecting daytime schedules
- respecting environmental ilimination
- implementing timeouts for movement events and forgot light
"""
def __init__(self, item_name, attr):
"""
"""
self.item_name = item_name
self.light_attrs = lightAttributes(attr)
# stored ligt states (0..100, ON/OFF)
self.stored_items_state = 0
# item retry counter
self.item_retry_count = 0
# item act type (None/DIM/TOG/MOV)
self.item_act_type = "None"
# item dimm direction (UP/DOWN)
self.item_dim_dir = "UP"
# item on/off type
self.is_on_off = self.light_attrs.getItemAttr("ONOF") == 1 or PercentType not in itemRegistry.getItem(self.item_name).acceptedCommandTypes
# initiale timers for all lights
self.light_tmr = lightTimer()
self.light_dim_toggle_tmr = lightTimer()
def enforceON(self, init):
# cancel all running timers
if init:
self.light_tmr.cancel()
self.item_retry_count = 0
if self.stored_items_state != self.getState():
events.sendCommand(self.item_name, "ON" if self.is_on_off else str(self.stored_items_state))
self.item_retry_count = self.item_retry_count + 1
if self.item_retry_count < 10:
self.light_tmr.start(3, lambda: self.enforceON(False))
return
# timeout value based on action/trigger type
time_out = self.light_attrs.getItemAttr("TIMO") if self.item_act_type == "MOV" else self.light_attrs.getItemAttr("TOFL")
self.light_tmr.start(time_out, lambda: self.setState(0))
def enforceOFF(self, init):
# cancel all running timers
try:
if init:
self.light_tmr.cancel()
self.item_retry_count = 0
# if triggering item still on, leave the light on
if self.light_attrs.getItemAttr("ITTR") != "None" and str(items[self.light_attrs.getItemAttr("ITTR")]) == "ON" and self.item_act_type == "MOV" :
self.light_tmr.start(self.light_attrs.getItemAttr("TIMO"), lambda: self.enforceOFF(True))
return
if self.stored_items_state != self.getState():
events.sendCommand(self.item_name, "OFF" if self.is_on_off else "0")
self.item_retry_count = self.item_retry_count + 1
if self.item_retry_count < 10:
self.light_tmr.start(self.item_retry_count * 5, lambda: self.enforceOFF(False))
except:
self.item_act_type = "None"
def setState(self, state):
"""
set state of openhab item (itm)
and
set state of item in local class db
"""
#limit the state to 0/100 regardless of type
state = max(0, min(100, int(state)))
# store item state in class db if is on/off type 0/100 are possible values
if self.is_on_off and state > 0 :
self.stored_items_state = 100
else:
self.stored_items_state = state
if state > 0:
# set color temperature is enabled
if self.light_attrs.getItemAttr("CTON") == 1 :
events.sendCommand(self.light_attrs.getItemAttr("ITCT"), str(self.light_attrs.getItemAttr("CTVL")))
self.enforceON(True)
else:
self.enforceOFF(True)
def getState(self):
"""
get state of the openhab item (itm)
"""
try:
if self.is_on_off:
return 100 if str(items[self.item_name]) == "ON" or str(items[self.item_name]) == "100" else 0
else:
return items[self.item_name].intValue()
except:
return 0
def isLightOnSchedule(self):
"""
Dtermine if the light should be switch on based on:
- daytime schedule (BedTime)
"""
if self.light_attrs.getItemAttr("NTOL") == 1:
return True if str(items["isBedTime"]) == "ON" else False
else:
return self.light_attrs.getItemAttr("ALLD") == 1 or str(items["isBedTime"]) == "OFF"
def isLightRequired(self):
"""
Determine if the light should be switch on:
- brightnes/illumination of the environment.
"""
return True if self.light_attrs.getItemAttr("ALIL") == 1 else items[self.light_attrs.getItemAttr("ITIL")].floatValue() < float(self.light_attrs.getItemAttr("ILUT"))
def getLightLevel(self, ilumin_aware):
"""
Determine if the light level based on:
- brightness of environment
- daytime schedule.
"""
#if ilumination aware, set value based of environment ilumination
if ilumin_aware:
target_level = 100 if self.light_attrs.getItemAttr("ALIL") == 1 else 5 + ((items[self.light_attrs.getItemAttr("ITIL")].floatValue() + 5 )*95 / self.light_attrs.getItemAttr("ILUT"))
# if it is bedtime, return the bedtine level
if str(items["isBedTime"]) == "ON":
target_level = self.light_attrs.getItemAttr("BTDI")
# make sure max dim value is maintained
if self.light_attrs.getItemAttr("MADI") < target_level:
target_level = self.light_attrs.getItemAttr("MADI")
return target_level
def setDimDir(self, dir):
self.item_dim_dir = dir
def getDimLevel(self):
if self.light_attrs.getItemAttr("UDDI") == "1":
self.light_dim_toggle_tmr.cancel()
if self.item_dim_dir == "UP":
self.light_dim_toggle_tmr.start(1.5, lambda: self.setDimDir("DOWN"))
return str(self.getState() + self.light_attrs.getItemAttr("DIST"))
else:
self.light_dim_toggle_tmr.start(1.5, lambda: self.setDimDir("UP"))
return str(self.getState() - self.light_attrs.getItemAttr("DIST"))
else:
return self.light_attrs.getItemAttr("MIDI") if self.getState() == 0 else self.getState() + self.light_attrs.getItemAttr("DIST")
def togLight(self):
"""
React on toggle light trigger and toggle:
- 100
- 0
light level
"""
self.item_act_type = "TOG"
if self.getState() == 0:
self.setState(self.getLightLevel(False))
else:
self.setState(0)
def dimLight(self):
"""
React on dim light trigger an increase light level:
- by dim step (DIST) defined for light
- by global dim step if not defined for light
"""
self.item_act_type = "DIM"
self.setState(self.getDimLevel())
def movLight(self, trig_itm):
"""
React on moving light trigger:
- turn on light for defined time
- extend the time is movement reocures
"""
if self.item_act_type != "MOV" and self.getState() != 0:
return
self.item_act_type = "MOV"
self.light_attrs.setItemAttr("ITTR", trig_itm)
if self.isLightOnSchedule() and self.isLightRequired():
self.setState(self.getLightLevel(True))
def cleanUpTimer(self):
"""
Cleanup all running timers by caling "cancel()"
"""
self.light_tmr.cleanUpTimer()
class lightItemManager(object):
"""
Class for managing the items (lights):
- forwarding the events
- creating the trigger definition for the rule class
- cleanup timers by calling involved classes
"""
def __init__(self, attrJsn):
# initiate light items
all_attr = yaml.safe_load(attrJsn)
all_items = [item for (item, atr) in yaml.safe_load(attrJsn).items() ]
self.light_items = { itm: lightItem(itm, all_attr[itm][0]) for itm in all_items }
# set trigger values for lights
self.all_trig = { item : attributes[1] for (item, attributes) in yaml.safe_load(attrJsn).items() }
def execute(self, module, inputs):
act = str(inputs["module"]).split("_")[0]
itm = str(inputs["module"]).split("_")[1]
if act == "TOG":
self.light_items[itm].togLight()
if act == "DIM":
self.light_items[itm].dimLight()
if act == "MOV":
self.light_items[itm].movLight(inputs['event'].getItemName())
if act == "GLOB":
self.light_attrs.loadUIDefaults()
def cleanUpTimer(self):
self.light_state.cleanUpTimer()
def getAllTriggers(self):
# get triggers from json defintion
raw_trigger = [[trigger[0], trigger[1], trigger[2], trigger[3], key_item_mode+"_"+key_item]for (key_item, item_all_triggers) in self.all_trig.items() for (key_item_mode, item_mode_triggers) in item_all_triggers.items() for trigger in item_mode_triggers]
all_trig = []
for trigger in raw_trigger:
if trigger[0] == "CET":
all_trig.append(ChannelEventTrigger(trigger[1], trigger[2], trigger[4]).trigger)
elif trigger[0] == "ISCT":
all_trig.append(ItemStateChangeTrigger(trigger[1], trigger[2], trigger[3], trigger[4]).trigger)
else:
log.error("********* {} ********* is not implemented yet".format(trigger[0]))
# add trigger for global defaults
all_trig.append(ItemEventTrigger("LIGHT_DEFAULTS", "GroupItemStateChangedEvent", "", "GLOB_XXX").trigger)
return all_trig
Feel free to comment.