Wake-up light with LMS and openHAB

Since a long time I wanted to build a wake-up light with my Logitech Squeezebox radio and the ceiling light – a Philips Hue bulb.
All components are connected through an openHAB server where such a functionality can be easily implemented.
For usability reasons and to consider the WAF (woman acceptance factor) the alarms should also be configured/managed by the Squeezebox radio – respectively the LMS in the background.

This sound not like a big deal since there is a Squeezebox binding to integrate the LMS into openHAB! Unfortunately the binding is only supporting a limited set of functions. And controlling the alarms is not part of it. So I looked for an easy way to implement this. The LMS-CLI, that is also used by the binding, is a very simple TCP protokoll. And the alarms can be read and configured with it!
The effort to modify the plugin was to big for me at this point. And you will not believe it, but there is a TCP binding in openHAB already that can be used 🙂
So I installed the TCP binding and wrote a small rule to communicate with the LMS. And voilà, I was able to read and write the alarms.

Together with the great code snippet of the universal dimmer my wake-up light works like a charm. And I can still use the Squeezebox radio to configure everything when I’m already in bed and my smartphone or tablet is out of reach.

Here a short outlook what the resulting sitemap looks like:

Sitemap view of the alarms

The code can be easily extended, e.g. to show more alarms or to edit the days the alarm will occur. It’s also possible to implement other LMS-CLI commands. But for me it works like this.

There is one string item to send and receive the command to/from the LMS. This item is triggered every 5 minutes to query the current alarms form the server and to update the other items. If the alarm state is changed, the alarm will be updated in the server. This behavior could be extended to update the alarm when the time items are changes… Feel free 🙂

The item file:

// Communication item
String lms_out “LMS Cmd [%s]” { tcp=“>[:9090:’REGEX((.*))’]” }

// LMS alarm
Number alarm_fade_in_before_alarm “Fade in before alarm [%d min]”
Number alarm_fade_in_after_alarm “Fade in after alarm [%d min]”
Number alarm_dim_target_value “Target Brigthness [%d %%]”

Switch alarm1_state “Alarm 1”
String alarm1_label “Alarm 1 [%s]”
Number alarm1_hour “Hour [%d]”
Number alarm1_minute “Minute [%d]”
String alarm1_dow “Days [%s]”

Switch alarm2_state “Alarm 2”
String alarm2_label “Alarm 2 [%s]”
Number alarm2_hour “Hour [%d]”
Number alarm2_minute “Minute [%d]”
String alarm2_dow “Days [%s]”

The sitemap:

Text label=“Alarms” icon=“time”{
Group item=alarm1_label icon=“time” {
Default item=alarm1_state
Setpoint item=alarm1_hour
Setpoint item=alarm1_minute
Default item=alarm1_dow
}

Group item=alarm2_label icon=“time” {
Default item=alarm2_state
Setpoint item=alarm2_hour
Setpoint item=alarm2_minute
Default item=alarm2_dow
}

Text label=“Settings” icon=“settings” {
Setpoint item=alarm_fade_in_before_alarm
Setpoint item=alarm_fade_in_after_alarm
Setpoint item=alarm_dim_target_value
}
}

The rule file:

import java.net.URLDecoder
import org.eclipse.smarthome.model.script.ScriptServiceUtil
import org.eclipse.xtext.xbase.lib.Procedures
import java.util.ArrayList
import java.util.LinkedHashMap
import java.util.concurrent.locks.ReentrantLock


var ArrayList<LinkedHashMap<String,String>> alarms = null
var Timer timer_alarm = null

val ReentrantLock mutex = new ReentrantLock
val String player_id = “<your player mac>”
val num_alarm_items = 2


val alarms_clear = [|
logInfo(“lms”, “clear all alarms”)
newArrayList()
]

val Procedures$Procedure1<LinkedHashMap<String,String>> update_alarm = [ alarm |
// get Items
logInfo(“test”, “update_alarm: “ + alarm.get(“nr”))
val Integer alarm_nr = Integer::parseInt(alarm.get(“nr”)) + 1
logInfo(“test”, “alarm_nr=” + alarm_nr.toString)
val item_hour = ScriptServiceUtil.getItemRegistry.getItem(“alarm” + alarm_nr.toString + “_hour”)
val item_min = ScriptServiceUtil.getItemRegistry.getItem(“alarm” + alarm_nr.toString + “_minute”)
val item_dow = ScriptServiceUtil.getItemRegistry.getItem(“alarm” + alarm_nr.toString + “_dow”)
val item_state = ScriptServiceUtil.getItemRegistry.getItem(“alarm” + alarm_nr.toString + “_state”)
var String dow = item_dow.state.toString
var hour = (item_hour.state as DecimalType).intValue
var min = (item_min.state as DecimalType).intValue
var state = item_state.state.toString
if(state == “ON”)
state = “1”
else
state = “0”
val time = (hour * 60 + min)*60

val String cmd = alarm.get(“player”) + ” alarm update id:” + alarm.get(“id”) + ” time:” + time.toString + ” enabled:” + state + ” dow:” + dow
logInfo(“test”, “Set Alarm – “ + cmd)
cmd
]

val Procedures$Procedure2<ArrayList<LinkedHashMap<String,String>>, Integer> update_items = [ alarms, num_alarm_items |
for(var n=0; n < num_alarm_items; n++)
{
var String dow = “-“
var hour = 0
var min = 0
var state = OFF
var String label = “-“

if( n < alarms.length)
{
val current_alarm = alarms.get(n)
dow = current_alarm.get(“dow”)
dow = dow.replace(“0”, “So”).replace(“1”, “Mo”).replace(“2”, “Di”).replace(“3”, “Mi”).replace(“4”, “Do”).replace(“5”, “Fr”).replace(“6”, “Sa”)
val t = Integer::parseInt(current_alarm.get(“time”)) / 60 // time in minutes since midnight
hour = t / 60
min = t % 60
if( current_alarm.get(“enabled”) == “1” )
state = ON
label = ” Time: “ + hour.toString + “:” + min.toString + ” Days: “ + dow + ” – “+ state.toString
}

if( n <= num_alarm_items)
{
var Integer alarm_nr = n
alarm_nr += 1
val item_hour = ScriptServiceUtil.getItemRegistry.getItem(“alarm” + alarm_nr.toString + “_hour”)
item_hour.postUpdate(hour)
val item_min = ScriptServiceUtil.getItemRegistry.getItem(“alarm” + alarm_nr.toString + “_minute”)
item_min.postUpdate(min)
val item_dow = ScriptServiceUtil.getItemRegistry.getItem(“alarm” + alarm_nr.toString + “_dow”)
item_dow.postUpdate(dow)

val item_state = ScriptServiceUtil.getItemRegistry.getItem(“alarm” + alarm_nr.toString + “_state”)
item_state.postUpdate(state)

val item_label = ScriptServiceUtil.getItemRegistry.getItem(“alarm” + alarm_nr.toString + “_label”)
item_label.postUpdate(label)
}
}
0
]

val Procedures$Procedure2<String, String> get_next_offset_day = [ dow, time |
val today = now.getDayOfWeek % 7
val days_str = dow.split(“,”)
var days = newArrayList()
for(var i=0; i<days_str.length; i++)
{
var diff = Integer::parseInt(days_str.get(i)) – today
if(diff < 0)
diff += 7
days.add(diff)
}
val sorted = days.sort

val t = Integer::parseInt(time) / 60 // time in minutes since midnight
var alarm_time = now.withTimeAtStartOfDay.plusHours(t / 60).plusMinutes(t % 60)
var doff = sorted.get(0)
if(alarm_time.isBefore(now))
{
if(doff == 0 && sorted.length > 1)
doff = sorted.get(1) // get next difference
else
doff = 7 // postpone by one week
}
doff
]

val Procedures$Procedure1<ArrayList<LinkedHashMap<String,String>>> get_next_alarm = [ alarms |
var next_alarm = –1
var next_time = now.plusDays(7)

for(var n=0; n < alarms.length; n++)
{
if( alarms.get(n).get(“enabled”) == “1” )
{
val t = Integer::parseInt(alarms.get(n).get(“time”)) / 60 // time in minutes since midnight
var alarm_time = now.withTimeAtStartOfDay.plusHours(t / 60).plusMinutes(t % 60)

var days_str = alarms.get(n).get(“dow”).split(“,”)
var days = newArrayList()
var today = now.getDayOfWeek % 7
for(var i=0; i<days_str.length; i++)
{
var diff = Integer::parseInt(days_str.get(i)) – today
if(diff < 0)
diff += 7
days.add(diff)
}
val sorted = days.sort
var doff = sorted.get(0)
if(alarm_time.isBefore(now))
{
if(doff == 0 && sorted.length > 1)
doff = sorted.get(1) // get next difference
else
doff = 7 // postpone by one week
}

alarm_time = alarm_time.plusDays(doff)
if( alarm_time.isBefore(next_time) && alarm_time.isAfter(now) )
{
next_alarm = n
next_time = alarm_time
}
}
}
// return next alarm
next_alarm
]


rule “alarm polling”
when
Time cron “0 0/5 * * * ?” // every 5 minutes
then
logInfo(“lms”, “Send command”)
lms_out.sendCommand( player_id + ” alarms 0 “ + num_alarm_items.toString + ” filter:all \rexit\r\n )
end


rule “receive and parse alarm”
when
Item lms_out received update
then
mutex.lock()
try
{
var String in = URLDecoder::decode(lms_out.state.toString, ‘UTF-8’)
if( in.contains(‘fade:’) )
{
logInfo(“lms”, “cmd: “ + in) // output correctly encoded command
var str_split = in.split(” id:”)
if(str_split.length>1)
{
// clear alarms
alarms = alarms_clear.apply()

for(var n=0; n < str_split.length-1; n++)
{
var s = str_split.get(n+1).split(” “)

var dublicate = false
for(var l=0; l<alarms.length; l++)
{
if(alarms.get(l).get(“id”) == s.get(0))
dublicate = true
}
if( dublicate == false )
{
var current_alarm = newLinkedHashMap();

current_alarm.put(“nr”, n.toString)
current_alarm.put(“player”, player_id)
current_alarm.put(“id”, s.get(0))
logInfo(“lms”, “Alarm “ + current_alarm.get(“nr”) + ” ID: “ + current_alarm.get(“id”))

for(var i=1; i<s.length; i++)
{
var ss = s.get(i).split(“:”)
var val_name = ss.get(0)
var val_value = “”
if( ss.length > 1 )
val_value = ss.get(1)
if( ss.length > 2 )
val_value = ss.get(1) + “:” + ss.get(2)

current_alarm.put(val_name, val_value)
//logInfo(“lms”, “name: ” + val_name + “, value: ” + val_value)
}

alarms.add(current_alarm)
}
}
}
// Update items
update_items.apply( alarms, num_alarm_items )

// get next alarm
var DateTime alarm_time
val Integer na = get_next_alarm.apply(alarms)
if( na >= 0 )
{
//logInfo(“test”, “next alarm: ” + na.toString)
val d = get_next_offset_day.apply(alarms.get(na).get(“dow”), alarms.get(na).get(“time”))
val t = Integer::parseInt(alarms.get(na).get(“time”)) / 60 // time in minutes since midnight
val fade_before_alarm = (alarm_fade_in_before_alarm.state as DecimalType).intValue
alarm_time = now.withTimeAtStartOfDay.plusHours(t / 60).plusMinutes(t % 60).plusDays(d).minusMinutes(fade_before_alarm)
}

// schedule timer
if(alarm_time !== null)
{
logInfo(“test”, “Schedule Light for “ + alarm_time.toString)
if(timer_alarm === null)
{
val fade_time = ((alarm_fade_in_before_alarm.state as DecimalType).intValue + (alarm_fade_in_after_alarm.state as DecimalType).intValue) * 60000
val step_size = 1
val String dim_cmd = “Dimmer,”+ (alarm_dim_target_value.state as DecimalType).intValue.toString + “,” + fade_time.toString + “,” + step_size.toString + “,BedDimmer,0”
timer_alarm = createTimer(alarm_time, [|
timer_alarm = null
universaldimmer.sendCommand(dim_cmd)
timer_alarm = null
])
}
else
{
timer_alarm.reschedule(alarm_time)
}
}
}
}
catch(Throwable t)
{
mutex.unlock()
logError(“lms”, t.toString)
}
finally {
mutex.unlock()
}
end


rule “alarm state”
when
Item alarm1_state received command or
Item alarm2_state received command
then
val str_n = triggeringItem.name.replace(“alarm”, “”).replace(“_state”,“”)
val n = Integer::parseInt(str_n)-1

if( alarms.length > n )
{
val String cmd = update_alarm.apply(alarms.get(n))
logInfo(“lms”, “Update alarm: “ + cmd)
lms_out.sendCommand( cmd + \rexit” )
}
else
logInfo(“lms”, “Update: alarm ‘” + n.toString + “‘ not found”)
end