Table of Contents
Dynamische Szenen sind das Highlight smarter Glühbirnen. Wenn sie farbig sind, lassen sich eindrucksvolle Effekte erzeugen. Mit OpenHAB lässt sich dieser Effekt einfach hervorrufen, der sonst nur kompliziert über die Hue App zu starten ist.
Der Grundgedanke: OpenHAB startet eine Schleife, die unter bestimmten Bedingungen läuft und laufend Aktualisierungen an einer Gruppe oder einzelner Lampen vornimmt.
Da manchmal verschiedene Parameter an die Lampe übergeben werden sollen, wird dafür der Lampe ein Interface-Item hinzugefügt, welches im Zweifel notwendige Übersetzungen vornimmt (falls man eben nicht bei Hue bleiben will, Geräte anderer Hersteller hat oder sich mal was ändern sollte) und diese passend für die Lampe sendet, ohne dabei die bestehenden Items für die übrigen Eigenschaften anzusprechen. Letzteres ist notwendig, um die Szenen auch wieder unterbrechen zu können.
Um Szenen sowohl für Gruppen, als auch für einzelne Lampen innerhalb einer Gruppe, welche sich unterschiedlich verhalten sollen, Szenen erstellen zu können, müssen sowohl die Gruppen, als auch die Lampen angelegt werden.
1. Zigbee2MQTT
Verbinde deine Lampen mit zigbee2mqtt. Ich verwende dafür ein bestimmtes Namens-Konzept für die Verwendung von MQTT.
2. OpenHAB Things
Auch dazu habe ich bereits einen Artikel verfasst, welcher generell darauf eingeht, wie Hue mit zigbee2mqtt in OpenHAB eingebunden werden kann.
Dieser Konzept muss noch erweitert werden, sodass das Interface geschaffen werden kann. Außerdem müssen die einzelnen Lampen zumindest ein Interface erhalten. Ein Beispiel in meiner things/mqtt_hue.things :
Bridge mqtt:broker:myMosquitto "Mosquitto" [ host="XXXX", port=1883, secure=false, clientID="XXXX", username="XXXX", password="XXXX" ] {
}
Thing mqtt:topic:myMosquitto:HueBulbOfficeAlex "Büro Alex Licht" (mqtt:broker:myMosquitto) @ "Büro Alex" {
Channels:
Type switch : switch [ stateTopic="+/+/officeAlex/+/lightBulb/group", commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/group/set", transformationPattern="REGEX:(.*state.*(ON|OFF).*)∩JSONPATH:$.state", transformationPatternOut="JS:switch2zigbee2mqtt.js"]
Type dimmer : brightness [ stateTopic="+/+/officeAlex/+/lightBulb/group", commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/group/set", min=0, max=100, step=1, transformationPatternOut="JS:openhabDimmer2zigbeebridge.js", transformationPattern="JS:hueWhiteDimmer2openhab.js" ]
Type dimmer : color_temperature [ stateTopic="+/+/officeAlex/+/lightBulb/group", commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/group/set", min=0, max=100, step=1, transformationPatternOut="JS:openhabColorTemperature2zigbeebridge.js", transformationPattern="JS:hueWhiteAndColorTemperature2openhab.js" ]
Type color : color [ stateTopic="+/+/officeAlex/+/lightBulb/group", commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/group/set", transformationPatternOut="JS:openhabColor2zigbeebridge.js", transformationPattern="JS:hueWhiteAndColorColor2openhab.js" ]
Type string : interface [ commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/group/set", transformationPatternOut="JS:openhabJSON2zigbeebridgeHueInterface.js" ]
}
Thing mqtt:topic:myMosquitto:0x00178801XXXXXXXX "Büro Alex Licht 1" (mqtt:broker:myMosquitto) @ "Büro Alex" {
Channels:
Type string : interface [ commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/Hue/WC/E27/0x0017880XXXXXXXX/set", transformationPatternOut="JS:openhabJSON2zigbeebridgeHueInterface.js" ]
}
Thing mqtt:topic:myMosquitto:0x0017880XXXXXXXX "Büro Alex Licht 2" (mqtt:broker:myMosquitto) @ "Büro Alex" {
Channels:
Type string : interface [ commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/Hue/WC/E27/0x0017880XXXXXXXX/set", transformationPatternOut="JS:openhabJSON2zigbeebridgeHueInterface.js" ]
}
Thing mqtt:topic:myMosquitto:0x0017880XXXXXXXX "Büro Alex Licht 3" (mqtt:broker:myMosquitto) @ "Büro Alex" {
Channels:
Type string : interface [ commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/Hue/WC/E27/0x0017880XXXXXXXX/set", transformationPatternOut="JS:openhabJSON2zigbeebridgeHueInterface.js" ]
}Hinweis zu OpenHAB 4:
OpenHAB 4 verwendet Java 17 statt Java 11 und die JS-Transformationen sind so nicht mehr verfügbar. Um sie wieder verwenden zu können, muss das Javascript-Binding installiert werden und die nachfolgenden Transformations mit *.script enden statt *.js.
Außerdem verändert sich die Thing-Syntax (nur in der Version 4.0.0, nicht mehr in Version 4.0.3) wie folgt:
Thing mqtt:topic:myMosquitto:HueBulbOfficeAlex "Büro Alex Licht" (mqtt:broker:myMosquitto) @ "Büro Alex" {
Channels:
Type switch : switch [ stateTopic="+/+/officeAlex/+/lightBulb/group", commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/group/set", transformationPattern="REGEX:(.*state.*(ON|OFF).*)∩JSONPATH:$.state", transformationPatternOut="SCRIPT:js:switch2zigbee2mqtt.script"]
Type dimmer : brightness [ stateTopic="+/+/officeAlex/+/lightBulb/group", commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/group/set", min=0, max=100, step=1, transformationPatternOut="SCRIPT:js:openhabDimmer2zigbeebridge.script", transformationPattern="SCRIPT:js:hueWhiteDimmer2openhab.script" ]
Type dimmer : color_temperature [ stateTopic="+/+/officeAlex/+/lightBulb/group", commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/group/set", min=0, max=100, step=1, transformationPatternOut="SCRIPT:js:openhabColorTemperature2zigbeebridge.script", transformationPattern="SCRIPT:js:hueWhiteAndColorTemperature2openhab.script" ]
Type color : color [ stateTopic="+/+/officeAlex/+/lightBulb/group", commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/group/set", transformationPatternOut="SCRIPT:js:openhabColor2zigbeebridge.script", transformationPattern="SCRIPT:js:hueWhiteAndColorColor2openhab.script" ]
Type string : interface [ commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/group/set", transformationPatternOut="SCRIPT:js:openhabJSON2zigbeebridgeHueInterface.script" ]
}
Thing mqtt:topic:myMosquitto:0x0017880XXXXXXXX "Büro Alex Licht 1" (mqtt:broker:myMosquitto) @ "Büro Alex" {
Channels:
Type string : interface [ commandTopic="zigbee2mqtt/ug/officeAlex/undef/lightBulb/Hue/WC/E27/0x0017880XXXXXXXX/set", transformationPatternOut="SCRIPT:js:openhabJSON2zigbeebridgeHueInterface.script" ]
}3. OpenHAB Transformations
Die Transformations verwende ich wie in dem allgemeinen Beispiel beschrieben. Dazu kommt jetzt noch das transform/openhabJSON2zigbeebridgeHueInterface.js.
Das stellt nur sicher, dass bestimmte Konventionen erhalten bleiben und Übersetzungen stattfinden können. Möglicherweise unterstützt das Ziel keinen RGB-Befehl. So können alle Farbbefehle sorglos in einem Format (hier HSB) übergeben werden.
(function(input) {
var json_in = JSON.parse(input);
var json_out = {};
if(json_in.hasOwnProperty('state')){
json_out.state = json_in.state;
}
if(json_in.hasOwnProperty('color')){
if(json_in.color.hasOwnProperty('hsb')){
hsb = json_in.color.hsb.split(",");
json_in.color.h = hsb[0];
json_in.color.s = hsb[1];
json_in.color.b = hsb[2];
}
else if(json_in.color.hasOwnProperty('rgb')){
rgb = json_in.color.rgb.split(",");
json_in.color.r = rgb[0];
json_in.color.g = rgb[1];
json_in.color.b = rgb[2];
}
else if(json_in.color.hasOwnProperty('x') && json_in.color.hasOwnProperty('y')){
}
else if(json_in.color.hasOwnProperty('hex')){
}
else if(json_in.color.hasOwnProperty('hue') && json_in.color.hasOwnProperty('saturation')){
json_in.color.h = json_out.color.hue;
json_in.color.s = json_out.color.saturation;
}
if(json_in.color.hasOwnProperty('h') && json_in.color.hasOwnProperty('s')){
json_out.color = {hue:json_in.color.h,saturation:json_in.color.s};
if(json_in.color.hasOwnProperty('b')){
json_out.color = {
h:json_in.color.h,
s:json_in.color.s
};
json_out.brightness_percent = json_in.color.b;
}
else if(json_in.color.hasOwnProperty('v')){
json_out.color = {
h:json_in.color.h,
s:json_in.color.s,
v:json_in.color.v
};
}
else if(json_in.color.hasOwnProperty('l')){
json_out.color = {
h:json_in.color.h,
s:json_in.color.s,
l:json_in.color.l
};
}
}
else if(json_in.color.hasOwnProperty('r') && json_in.color.hasOwnProperty('g') && json_in.color.hasOwnProperty('b')){
var r = json_in.color.r;
var g = json_in.color.g;
var b = json_in.color.b;
var l = Math.max(r, g, b);
var s = l - Math.min(r, g, b);
var h = s ?
l === r ?
(g - b) / s :
l === g ?
2 + (b - r) / s : 4 + (r - g) / s
: 0;
json_out.color = {};
json_out.color.h = 60 * h < 0 ? 60 * h + 360 : 60 * h;
json_out.color.s = 100 * (s ? (l <= 0.5 ? s / (2 * l - s) : s / (2 - (2 * l - s))) : 0);
json_out.color.l = (100 * (2 * l - s)) / 2;
}
}
if(json_in.hasOwnProperty('brightness_percent')){
json_out.brightness_percent = json_in.brightness_percent;
}
if(json_in.hasOwnProperty('transition')){
json_out.transition = json_in.transition;
}
if(json_in.hasOwnProperty('color_temp')){
json_out.color_temp = Math.round(json_in.color_temp*3.47+153);
}
if(json_in.hasOwnProperty('effect')){
json_out.effect = json_in.effect;
}
if(json_out.brightness_percent == 0){
return JSON.stringify({state:'OFF'});
}
return JSON.stringify(json_out);
})(input)4. OpenHAB Items
Für die Items müssen bestimmte Strukturen eingehalten werden, die ich bereits in einem anderen Artikel allgemein empfohlen habe. Alle Items eines Zigbee-Gruppen-Things werden wie folgt definiert und in einer Gruppe mit dem “ig”-Präfix (Item-Group) zusammengefasst. So weiß OpenHAB, welche Eigenschaften zusammengehören. Außerdem werden die Interfaces der Glühbirnen dieser Gruppe hinzugefügt. So weiß OpenHAB, welche Birnen dazugehören, wenn es sie einzeln ansprechen will.
Darüber hinaus gibt es für jede Gruppe einen Speed-Parameter, welcher bei 50% die Szene normal, bei 0% die Szene mit 1/10 Geschwindigkeit und bei 100% 10-fache Geschwindigkeit anwenden soll.
Ein Beispiel aus meiner items/hue.items:
Group igBueroAlexLight "Büro Alex" (sgOfficeAlex)["Lightbulb", "Group"]
Color BueroAlex_Farbe "Farbe" <ColorLight> (igBueroAlexLight,sgOfficeAlex,tgLSchange)["Control", "Light"] { autoupdate="false", channel="mqtt:topic:myMosquitto:HueBulbOfficeAlex:color", alexa="Color" }
Dimmer BueroAlex_Farbtemperatur "Farbtemperatur" <ColorLight> (igBueroAlexLight,tgLSinterrupt,sgOfficeAlex)["Control", "ColorTemperature"] { autoupdate="false", channel="mqtt:topic:myMosquitto:HueBulbOfficeAlex:color_temperature", alexa="ColorTemperature" }
Dimmer BueroAlex_Helligkeit "Licht" <DimmableLight> (igBueroAlexLight,tgLSinterrupt,sgOfficeAlex)["Control", "Brightness"] { autoupdate="false", channel="mqtt:topic:myMosquitto:HueBulbOfficeAlex:brightness", alexa="Brightness,PowerState" }
String BueroAlex_Lightshow "Lightshow" (igBueroAlexLight,tgLightshow,sgOfficeAlex)["Control", "DynamicScene"] { alexa="Mode" [capabilityNames="@Setting.Mode",supportedModes="Normal,Kerzenlicht,Polarlicht,Farbwechsel,Zufallsfarben,Gewitter,TV-Simulation"] }
Dimmer BueroAlex_LightshowSpeed "Geschwindigkeit" (igBueroAlexLight,sgOfficeAlex)["Control", "DynamicSpeed"] { alexa="RangeValue" [capabilityNames="Geschwindigkeit,Speed", supportedRange="0:100:10", presets="10=@Value.Low:@Value.Minimum,50=@Value.Medium:Normal,100=@Value.High:@Value.Maximum"] }
String BueroAlex_Interface "Interface" (igBueroAlexLight)["Control", "Interface"] { channel="mqtt:topic:myMosquitto:HueBulbOfficeAlex:interface" }
String x001788010b9b7b15_Interface (igBueroAlexLight)["Lightbulb", "Entity"] { channel="mqtt:topic:myMosquitto:0x001788010b9b7b15:interface" }
String x001788010b9b7c33_Interface (igBueroAlexLight)["Lightbulb", "Entity"] { channel="mqtt:topic:myMosquitto:0x001788010b9b7c33:interface" }
String x0017880104370a3d_Interface (igBueroAlexLight)["Lightbulb", "Entity"] { channel="mqtt:topic:myMosquitto:0x0017880104370a3d:interface" }5. OpenHAB Automation
Jetzt müssen nur noch die Szenen definiert werden. Diese müssen nun so generisch wie möglich agieren. In meinem Beispiel in einer rules/dynamic-scenes.rules habe ich eine Vielzahl von Szenen bereits einmal vorgeschlagen. Zum triggeringItem wird die zugehörige ItemGroup ermittelt und daraus dann die notwendigen Members abgeleitet und angesprochen. Ändert sich eines der üblichen Steuer-Items wird die Szene unterbrochen.
import org.openhab.core.model.script.ScriptServiceUtil
import java.util.List
import java.util.Random
import java.util.Map
var Map<String, HSBType> colors = newHashMap
var Map<String, String> mode = newHashMap
val getSpeedFactorFromDimmer = [ DimmerItem speedItem |
//logInfo("Test", "speedItem: {}", speedItem)
if( speedItem.state != NULL ){
// x10 ... x1 ... x0.1
// 0 ... 50 ... 100
10 * Math.exp( -0.04605 * (speedItem.state as Number).intValue )
} else {
1.0
}
]
rule "Lightshow"
when
Member of tgLightshow changed
then
var double speed = 1.0
val Random rand = new Random()
//Get the Bulb-Group of the trigger
val String bulbGroupName = triggeringItem.getGroupNames.findFirst[ String groupName | groupName.startsWith("ig") ]
val GroupItem bulbGroup = ScriptServiceUtil.getItemRegistry.getItem(bulbGroupName) as GroupItem
//detect specific of the group
//val ColorItem colorItem = bulbGroup.members.findFirst[ a | a.tags.contains("Control") && a.tags.contains("Light")]
//val DimmerItem colorTemperatureItem = bulbGroup.members.findFirst[ a | a.tags.contains("Control") && a.tags.contains("ColorTemperature")]
val StringItem interfaceItem = bulbGroup.members.findFirst[ a | a.tags.contains("Control") && a.tags.contains("Interface")]
val DimmerItem speedItem = bulbGroup.members.findFirst[ a | a.tags.contains("Control") && a.tags.contains("DynamicSpeed")]
//break conditions
mode.put(bulbGroupName,triggeringItem.state.toString()) //um bei neuem Kommando nicht mit altem Thread weiterzumachen
//Lampen in Gruppe ansteuern
if(mode.get(bulbGroupName) == "Gewitter") {
createTimer(now, [ |
var String lightCmdStandby = '{"brightness_percent":40,"transition":0}' // Grundhelligkeit 0 - 254: 40
interfaceItem.sendCommand(lightCmdStandby)
while(mode.get(bulbGroupName) == "Gewitter" && triggeringItem.state.toString() == mode.get(bulbGroupName)) {
speed = getSpeedFactorFromDimmer.apply(speedItem)
var sleep = (1000 * speed).intValue // Schleifen-Durchlaufzeit
var float randomnumber = rand.nextFloat() // 0.0 - 1.0
if(randomnumber > 0.95){
var B = 100 // Helligkeit
interfaceItem.sendCommand('{"brightness_percent":' + B + ',"transition":0}')
Thread::sleep(10)
interfaceItem.sendCommand(lightCmdStandby)
}
else if(randomnumber > 0.9){
var B = 60 // Helligkeit
interfaceItem.sendCommand('{"brightness_percent":' + B + ',"transition":0}')
Thread::sleep(10)
interfaceItem.sendCommand(lightCmdStandby)
Thread::sleep(100)
interfaceItem.sendCommand('{"brightness_percent":' + B + ',"transition":0}')
Thread::sleep(10)
interfaceItem.sendCommand(lightCmdStandby)
}
//to break the loop immediately while waiting for the nex iteration too
for (var i = 0; triggeringItem.state.toString() == mode.get(bulbGroupName) && i < sleep; i = i + 100){
Thread::sleep(100)
//sometimes the commands are not received in the correct order, so:
interfaceItem.sendCommand(lightCmdStandby)
}
}
])
}
else if(mode.get(bulbGroupName) == "Alarm") {
createTimer(now, [ |
var sleep = 1400
while(mode.get(bulbGroupName) == "Alarm" && triggeringItem.state.toString() == mode.get(bulbGroupName)){
//logInfo("Test", "triggeringItem1: {}", mode.get(bulbGroupName))
interfaceItem.sendCommand('{"color":{"hsb":"18,100,100"},"transition":1}') //red
for (var i = 0; triggeringItem.state.toString() == mode.get(bulbGroupName) && i < sleep; i = i + 100){
Thread::sleep(100)
}
if(triggeringItem.state.toString() == mode.get(bulbGroupName)){
interfaceItem.sendCommand('{"color":{"hsb":"18,100,30"},"transition":1}')
}
for (var i = 0; triggeringItem.state.toString() == mode.get(bulbGroupName) && i < sleep; i = i + 100){
Thread::sleep(100)
}
}
interfaceItem.sendCommand('{"brightness_percent":100}')
])
}
else if(mode.get(bulbGroupName) == "TV-Simulation") {
createTimer(now, [ |
while(mode.get(bulbGroupName) == "TV-Simulation" && triggeringItem.state.toString() == mode.get(bulbGroupName)) {
var led_bright = "255"
var led_half = "128"
var led_min = "60"
var List<String> transtions = newArrayList("0.0","0.0","0.0","0.0","0.0","0.0","0.4","0.4","1.0","2.0")
var List<String> pictures = newArrayList(
led_half + "," + led_half + "," + led_half,
led_min + "," + led_min + "," + led_min,
led_bright + "," + led_bright + "," + led_half,
led_bright + "," + led_half + "," + led_bright,
led_half + "," + led_bright + "," + led_bright,
led_bright + "," + led_half + "," + led_min,
led_half + "," + led_min + "," + led_bright,
led_min + "," + led_half + "," + led_bright,
led_half + "," + led_bright + "," + led_min,
led_bright + "," + led_min + "," + led_half,
led_min + "," + led_bright + "," + led_half,
led_half + "," + led_half + "," + led_bright,
led_half + "," + led_bright + "," + led_half,
led_bright + "," + led_half + "," + led_half,
led_half + "," + led_half + "," + led_min,
led_half + "," + led_min + "," + led_half,
led_min + "," + led_half + "," + led_half,
led_half + "," + led_min + "," + led_min,
led_min + "," + led_half + "," + led_min,
led_min + "," + led_min + "," + led_half
)
speed = getSpeedFactorFromDimmer.apply(speedItem)
var sleep = ((500 + Math::random * 5000.0) * speed).intValue // Wechselzeit: 500 - 5500ms
interfaceItem.sendCommand( '{"color":{"rgb":"' + pictures.get(rand.nextInt(pictures.size())) + '"},"brightness_percent":' + rand.nextInt(100).toString + ',"transition":' + transtions.get(rand.nextInt(transtions.size())) + '}' )
//to break the loop immediately while waiting for the nex iteration too
for (var i = 0; triggeringItem.state.toString() == mode.get(bulbGroupName) && i < sleep; i = i + 100){
Thread::sleep(100)
}
}
])
}
//Lampen einzeln ansteuern
else{
bulbGroup.members.forEach[ StringItem entityInterfaceItem |
if(entityInterfaceItem.tags.contains("Lightbulb") && entityInterfaceItem.tags.contains("Entity")){
createTimer(now, [ |
while(mode.get(bulbGroupName) == "Kerzenlicht" && triggeringItem.state.toString() == mode.get(bulbGroupName)) {
var H = 18 + (Math::random * 20.0).intValue // Color: 18 - 38
var B = 50 + (Math::random * 50.0).intValue // Helligkeit: 50 - 100
speed = getSpeedFactorFromDimmer.apply(speedItem)
var sleep = ((750 + Math::random * 1250.0) * speed).intValue // Wechselzeit: 750 - 2000
entityInterfaceItem.sendCommand('{"color":{"hsb":"' + H.toString + ',100,' + B.toString + '"},"transition":0.1}')
//to break the loop immediately while waiting for the nex iteration too
for (var i = 0; triggeringItem.state.toString() == mode.get(bulbGroupName) && i < sleep; i = i + 100){
Thread::sleep(100)
}
}
if(mode.get(bulbGroupName) == "Polarlicht"){
colors.put(bulbGroupName,new HSBType("210,100,100"))
}
while(mode.get(bulbGroupName) == "Polarlicht" && triggeringItem.state.toString() == mode.get(bulbGroupName)) {
var H = (colors.get(bulbGroupName).getHue().intValue() +310) % 360 + (Math::random * 100.0).intValue // Color: 160 - 260
var B = 60 + (Math::random * 40.0).intValue // Helligkeit: 60 - 100
speed = getSpeedFactorFromDimmer.apply(speedItem)
var sleep = ((1000.0 + Math::random * 5000.0) * speed).intValue // Wechselzeit: 1000 - 6000
entityInterfaceItem.sendCommand('{"color":{"hsb":"' + H.toString + ',100,' + B.toString + '"},"transition":' + (sleep/1000.0).floatValue + '}')
//to break the loop immediately while waiting for the nex iteration too
for (var i = 0; triggeringItem.state.toString() == mode.get(bulbGroupName) && i < (sleep + 500); i = i + 100){
Thread::sleep(100)
}
}
while(mode.get(bulbGroupName) == "Zufallsfarben" && triggeringItem.state.toString() == mode.get(bulbGroupName)) {
var H = (Math::random * 360.0).intValue + 1 // Color: +5
speed = getSpeedFactorFromDimmer.apply(speedItem)
var sleep = ((750 + Math::random * 1250.0) * speed).intValue // Wechselzeit: 750 - 2000
entityInterfaceItem.sendCommand('{"color":{"hue":' + H.toString + ',"saturation":100},"transition":' + (sleep/1000.0).floatValue + '}')
//to break the loop immediately while waiting for the nex iteration too
for (var i = 0; triggeringItem.state.toString() == mode.get(bulbGroupName) && i < sleep; i = i + 100){
Thread::sleep(100)
}
}
])
Thread::sleep(100)
}
]
}
end
rule "Lightshow Change"
when
Member of tgLSchange received command
then
val String bulbGroupName = triggeringItem.getGroupNames.findFirst[ String groupName | groupName.startsWith("ig") ]
if(receivedCommand == OFF || receivedCommand == ON){
colors.put(bulbGroupName, HSBType::WHITE )
} else {
colors.put(bulbGroupName, (receivedCommand as HSBType) )
}
//logInfo("Test", "receivedCommand: {}", receivedCommand)
end
rule "Lightshow Interrupt"
when
Member of tgLSinterrupt received command
then
val String bulbGroupName = triggeringItem.getGroupNames.findFirst[ String groupName | groupName.startsWith("ig") ]
val GroupItem bulbGroup = ScriptServiceUtil.getItemRegistry.getItem(bulbGroupName) as GroupItem
val StringItem lightshowItem = bulbGroup.members.findFirst[ a | a.tags.contains("Control") && a.tags.contains("DynamicScene")]
lightshowItem.sendCommand("Normal")
end
rule "Set default values"
when
Time cron "0 0 * * * ?"
or Time cron "0 * * * * ?"
or System started
then {
var items = ScriptServiceUtil.getItemRegistry.getItemsByTag("DynamicScene")
items.forEach[ GenericItem item |
if(item.state == NULL || item.state == UNDEF){
item.sendCommand("Normal")
}
]
items = ScriptServiceUtil.getItemRegistry.getItemsByTag("DynamicSpeed")
items.forEach[ GenericItem item |
if(item.state == NULL || item.state == UNDEF){
item.sendCommand(50)
}
]
}
end/**
* Lightshow Automation for openHAB (JS Scripting Edition)
*/
const { rules, triggers, items, time } = require("openhab");
// Global state storage (replaces the global maps)
// cache.private stores data only within this script, but keeps it between executions.
const CACHE = cache.private;
// Initialization of storage structures, if not already present
if (!CACHE.get("activeTimers")) CACHE.put("activeTimers", {});
if (!CACHE.get("colors")) CACHE.put("colors", {});
// Constants
const NEUTRAL_WHITE = '{"brightness":254,"color":{"hue":31,"saturation":84,"x":0.4682,"y":0.4123},"color_mode":"color_temp","color_temp":384,"state":"ON"}';
// --- HELPER FUNCTIONS ---
/**
* Calculates the speed factor based on the dimmer value.
* Logic: 10 * exp(-0.04605 * value)
*/
function getSpeedFactor(speedItem) {
if (!speedItem || speedItem.state === "NULL" || speedItem.state === "UNDEF") {
return 1.0;
}
const val = parseFloat(speedItem.state);
return 10 * Math.exp(-0.04605 * val);
}
/**
* Stops all running animations for a group.
* Important: Replaces the "break conditions" and Thread::sleep loops from the DSL.
*/
function stopAnimation(groupName) {
const timers = CACHE.get("activeTimers");
if (timers[groupName]) {
// It can be individual timers or arrays of timers (for individual modes)
if (Array.isArray(timers[groupName])) {
timers[groupName].forEach(id => clearTimeout(id));
} else {
clearTimeout(timers[groupName]);
}
delete timers[groupName];
}
}
/**
* Saves a timer handle to be able to cancel it later.
*/
function registerTimer(groupName, timerId, isArray = false) {
const timers = CACHE.get("activeTimers");
if (isArray) {
if (!timers[groupName] || !Array.isArray(timers[groupName])) {
timers[groupName] = [];
}
timers[groupName].push(timerId);
} else {
timers[groupName] = timerId;
}
}
// --- MAIN RULE: Lightshow ---
rules.JSRule({
name: "Lightshow Logic",
description: "Steuert komplexe Licht-Szenen und Effekte",
triggers: [triggers.GroupStateChangeTrigger("tgLightshow")],
execute: (event) => {
const triggeringItem = items.getItem(event.itemName);
const newState = event.newState;
// 1. Identify group
// Searches for group names starting with "ig"
const bulbGroupName = triggeringItem.groupNames.find(g => g.startsWith("ig"));
if (!bulbGroupName) return; // No matching group found
const bulbGroup = items.getItem(bulbGroupName);
// 2. Stop existing animations for this group (Clean slate)
stopAnimation(bulbGroupName);
// If the new state is "NORMAL" or "OFF", we stop here (timers are already stopped)
if (newState === "NORMAL" || newState === "OFF" || newState === "NULL" || newState === "UNDEF") {
return;
}
// 3. Get items
const members = bulbGroup.members;
const interfaceItem = members.find(i => i.tags.includes("Control") && i.tags.includes("Interface"));
const speedItem = members.find(i => i.tags.includes("Control") && i.tags.includes("DynamicSpeed"));
// Safety check
if (!interfaceItem) {
console.warn(`Lightshow: Kein Interface-Item in Gruppe ${bulbGroupName} gefunden.`);
return;
}
// 4. Scene logic
// We define recursive functions for the loops.
// === SCENES ===
if (newState === "GD_SCENES") {
// Define list (excerpt, rest can easily be added)
let gradientList = [
["0000FF", "0000FF", "0000FF", "FF0000", "FF0000", "FF0000", "FF0000", "0000FF", "0000FF"], // blue-red
["FFFF00", "FFFF00", "FFFF00", "FFFF00", "00FF00", "00FF00", "FFFF00", "FFFF00", "FFFF00"], // green-yellow
["FF0000", "FF0000", "FF0000", "FFFFFF", "FFFFFF", "FFFFFF", "FF0000", "FF0000", "FF0000"], // red-white
["00FFFF", "00FFFF", "00FFFF", "FF00FF", "FF00FF", "FF00FF", "FF00FF", "00FFFF", "00FFFF"], // Cyberpunk
["000000", "000000", "000000", "00FF00", "00FF00", "00FF00", "00FF00", "000000", "000000"], // Matrix
// ... insert more arrays here ...
];
const runScene = () => {
// Rotation of the list: First element to the end
const currentGradient = gradientList.shift();
gradientList.push(currentGradient);
const speed = getSpeedFactor(speedItem);
const sleepTime = Math.floor(60000 * speed);
const gradientJson = JSON.stringify({ gradient: currentGradient });
interfaceItem.sendCommand(gradientJson);
// Plan next step
const tId = setTimeout(runScene, sleepTime);
registerTimer(bulbGroupName, tId);
};
runScene(); // Start
}
// === FLOATING ===
else if (newState === "GD_FLOATING") {
let colors = ["0000FF","0000FF","0000FF","FF0000","FF0000","FF0000","FF0000","0000FF","0000FF"];
const runFloating = () => {
const speed = getSpeedFactor(speedItem);
const sleepTime = Math.floor(4000 * speed);
const transition = Math.floor(3000 * speed);
const json = JSON.stringify({
gradient: colors,
transition: transition
});
interfaceItem.sendCommand(json);
// Rotate colors
colors.push(colors.shift());
const tId = setTimeout(runFloating, sleepTime);
registerTimer(bulbGroupName, tId);
};
runFloating();
}
// === THUNDERSTORM ===
else if (newState === "THUNDERSTORM") {
const lightCmdStandby = JSON.stringify({brightness_percent: 40, transition: 0});
interfaceItem.sendCommand(lightCmdStandby);
const runThunder = () => {
const speed = getSpeedFactor(speedItem);
const sleepTime = Math.floor(1000 * speed);
const randomVal = Math.random();
// Lightning logic
if (randomVal > 0.95) {
interfaceItem.sendCommand(JSON.stringify({brightness_percent: 100, transition: 0}));
setTimeout(() => interfaceItem.sendCommand(lightCmdStandby), 20); // Short lightning
} else if (randomVal > 0.9) {
// Double lightning
interfaceItem.sendCommand(JSON.stringify({brightness_percent: 60, transition: 0}));
setTimeout(() => {
interfaceItem.sendCommand(lightCmdStandby);
setTimeout(() => {
interfaceItem.sendCommand(JSON.stringify({brightness_percent: 60, transition: 0}));
setTimeout(() => interfaceItem.sendCommand(lightCmdStandby), 20);
}, 100);
}, 20);
} else {
// Ensure base light is on (in case packets were lost)
interfaceItem.sendCommand(lightCmdStandby);
}
const tId = setTimeout(runThunder, sleepTime);
registerTimer(bulbGroupName, tId);
};
runThunder();
}
// === ALARM ===
else if (newState === "ALARM") {
let stateToggle = true; // true = red, false = dark red
const runAlarm = () => {
if (stateToggle) {
interfaceItem.sendCommand(JSON.stringify({color: {hsb: "18,100,100"}, transition: 1}));
} else {
interfaceItem.sendCommand(JSON.stringify({color: {hsb: "18,100,30"}, transition: 1}));
}
stateToggle = !stateToggle;
const tId = setTimeout(runAlarm, 1500); // Sleep was 1400 + 100 Loop
registerTimer(bulbGroupName, tId);
};
runAlarm();
}
// === REG_PULSATE ===
else if (newState === "REG_PULSATE") {
let stateToggle = false;
const runPulsate = () => {
const speed = getSpeedFactor(speedItem);
const transition = 3.0 * speed;
const sleepTime = Math.floor(transition * 1000) + 200; // sleep + buffer
if (!stateToggle) {
interfaceItem.sendCommand(JSON.stringify({brightness_percent: 20, transition: transition}));
} else {
interfaceItem.sendCommand(JSON.stringify({brightness_percent: 100, transition: transition}));
}
stateToggle = !stateToggle;
const tId = setTimeout(runPulsate, sleepTime);
registerTimer(bulbGroupName, tId);
};
runPulsate();
}
// === UNREG_PULSATE ===
else if (newState === "UNREG_PULSATE") {
let stateToggle = false;
const runUnregPulsate = () => {
const speed = getSpeedFactor(speedItem);
if (!stateToggle) {
// Dim down phase
const transition = (0.5 + Math.random() * 5.0) * speed;
const sleepTime = Math.floor(transition * 1000) + 500;
interfaceItem.sendCommand(JSON.stringify({brightness_percent: 20, transition: transition}));
stateToggle = true;
const tId = setTimeout(runUnregPulsate, sleepTime);
registerTimer(bulbGroupName, tId);
} else {
// Dim up phase
// Here transition was reused, but recalculated in the original loop?
// In the original DSL, transition is calculated at the beginning of the loop and used for both phases.
// But sleep is recalculated for the Up-Phase.
// We recalculate here to be close to the original, or cache the transition.
// Simplification: Recalculate for dynamics
const transition = (0.5 + Math.random() * 5.0) * speed;
const sleepTime = Math.floor((500 + Math.random() * 5000.0) * speed) + 500;
interfaceItem.sendCommand(JSON.stringify({brightness_percent: 100, transition: transition}));
stateToggle = false;
const tId = setTimeout(runUnregPulsate, sleepTime);
registerTimer(bulbGroupName, tId);
}
};
runUnregPulsate();
}
// === CANDLE ===
else if (newState === "CANDLE") {
interfaceItem.sendCommand('{"color":{"h":30,"s":100},"brightness_percent":100,"transition":0.1}');
const runCandle = () => {
const speed = getSpeedFactor(speedItem); // Optional, was commented out in the original, but used here in formulas
// High Phase
const hi_sleep = Math.floor((100.0 + Math.random() * 40.0) * speed);
const hi_power = Math.floor(60.0 + Math.random() * 22.0);
interfaceItem.sendCommand(JSON.stringify({brightness_percent: hi_power, transition: 0.1}));
// We use nested timeouts for the "High then Low" sequence
const tId = setTimeout(() => {
// Low Phase
const lo_sleep = Math.floor((100.0 + Math.random() * 40.0) * speed);
const lo_power = Math.floor(50.0 + Math.random() * 10.0);
interfaceItem.sendCommand(JSON.stringify({brightness_percent: lo_power, transition: 0.1}));
// Next cycle after Low-Wait
const tIdNext = setTimeout(runCandle, lo_sleep);
registerTimer(bulbGroupName, tIdNext);
}, hi_sleep);
registerTimer(bulbGroupName, tId);
};
runCandle();
}
// === TV ===
else if (newState === "TV") {
const led_bright = "255";
const led_half = "128";
const led_min = "60";
const transitions = [0.0, 0.0, 0.0, 0.0, 0.4, 0.4, 1.0, 2.0];
const pictures = [
`${led_half},${led_half},${led_half}`,
`${led_min},${led_min},${led_min}`,
`${led_bright},${led_bright},${led_half}`,
// ... (Insert remaining combinations here) ...
`${led_min},${led_min},${led_half}`
];
const runTV = () => {
const speed = getSpeedFactor(speedItem);
const transition = transitions[Math.floor(Math.random() * transitions.length)];
const sleepTime = transition + Math.floor((500 + Math.random() * 6000.0) * speed);
const picture = pictures[Math.floor(Math.random() * pictures.length)];
const brightness = Math.floor(Math.random() * 100);
const json = JSON.stringify({
color: { rgb: picture },
brightness_percent: brightness,
transition: transition
});
interfaceItem.sendCommand(json);
const tId = setTimeout(runTV, sleepTime);
registerTimer(bulbGroupName, tId);
};
runTV();
}
// === INDIVIDUAL CONTROL (AURORA / RANDOM) ===
else {
// Here it gets special: In the original, one timer per lamp is started.
// We must use array timers here.
members.forEach(entity => {
if (entity.tags.includes("Lightbulb") && entity.tags.includes("Entity")) {
// Set initial color for Aurora
if (newState === "AURORA") {
const colorsMap = CACHE.get("colors");
// We just store a dummy object here or use HSB string
// JS has no native HSBType, we use strings or small objects
if (!colorsMap[bulbGroupName]) colorsMap[bulbGroupName] = {h:210, s:100, b:100};
}
const runIndividual = () => {
const speed = getSpeedFactor(speedItem);
if (newState === "AURORA") {
const colorsMap = CACHE.get("colors");
let baseH = 210;
if(colorsMap[bulbGroupName] && colorsMap[bulbGroupName].h) baseH = colorsMap[bulbGroupName].h;
const H = (baseH + 310) % 360 + Math.floor(Math.random() * 100);
const B = 60 + Math.floor(Math.random() * 40);
const sleep = Math.floor((1000.0 + Math.random() * 5000.0) * speed);
const transition = sleep / 1000.0;
entity.sendCommand(JSON.stringify({
color: { hsb: `${H},100,${B}` },
transition: transition
}));
const tId = setTimeout(runIndividual, sleep + 500);
registerTimer(bulbGroupName, tId, true); // true = append to array
}
else if (newState === "RANDOM") {
const H = Math.floor(Math.random() * 360) + 1;
const sleep = Math.floor((750 + Math.random() * 1250.0) * speed);
const transition = sleep / 1000.0;
entity.sendCommand(JSON.stringify({
color: { hue: H },
transition: transition
}));
const tId = setTimeout(runIndividual, sleep);
registerTimer(bulbGroupName, tId, true);
}
};
// Start the loop for this specific lamp
// Small delay to start so they don't all start exactly at the same time (Thread::sleep(100) in the original)
setTimeout(runIndividual, Math.random() * 100);
}
});
}
}
});
// --- RULE: Lightshow Change ---
rules.JSRule({
name: "Lightshow Change Color",
triggers: [triggers.GroupCommandTrigger("tgLSchange")],
execute: (event) => {
const triggeringItem = items.getItem(event.itemName);
const cmd = event.receivedCommand;
const bulbGroupName = triggeringItem.groupNames.find(g => g.startsWith("ig"));
if (bulbGroupName) {
const colorsMap = CACHE.get("colors");
if (cmd === "ON" || cmd === "OFF") {
// White preset
colorsMap[bulbGroupName] = {h:0, s:0, b:100};
} else {
// We assume that cmd is an HSB string "H,S,B"
const parts = cmd.toString().split(",");
if (parts.length === 3) {
colorsMap[bulbGroupName] = {
h: parseFloat(parts[0]),
s: parseFloat(parts[1]),
b: parseFloat(parts[2])
};
}
}
}
}
});
// --- RULE: Lightshow Interrupt ---
rules.JSRule({
name: "Lightshow Interrupt",
triggers: [triggers.GroupCommandTrigger("tgLSinterrupt")],
execute: (event) => {
const triggeringItem = items.getItem(event.itemName);
const bulbGroupName = triggeringItem.groupNames.find(g => g.startsWith("ig"));
if (bulbGroupName) {
const bulbGroup = items.getItem(bulbGroupName);
const lightshowItem = bulbGroup.members.find(i => i.tags.includes("Control") && i.tags.includes("DynamicScene"));
if (lightshowItem) {
lightshowItem.sendCommand("NORMAL");
}
}
}
});
// --- RULE: Set Default Values ---
rules.JSRule({
name: "Lightshow Defaults",
triggers: [
triggers.SystemStartlevelTrigger(100),
//triggers.GenericCronTrigger("0 0 * * * ?"), // Hourly
//triggers.GenericCronTrigger("0 * * * * ?") // Every minute
],
execute: (event) => {
// Search items by tags
// In JS Scripting we iterate over items.getItems() and filter
items.getItems().forEach(item => {
if (item.tags.includes("DynamicScene")) {
if (item.state === "NULL" || item.state === "UNDEF") {
item.sendCommand("NORMAL");
}
}
if (item.tags.includes("DynamicSpeed")) {
if (item.state === "NULL" || item.state === "UNDEF") {
item.sendCommand("50");
}
}
});
}
});
Pingback: Alarmanlage und Anwesenheitssimulator mit OpenHAB - Smarthome DIY - Heimautomatisierung selbst gemacht
Pingback: Interaktiver Floorplan in OpenHAB mit Effekten