I thought I would share my latest automation for two reasons:
- Feedback on how it can be improved
- Maybe someone else finds it useful.
This is still a WIP but mostly done. Letting it dry-run to check it works properly.
What
I have electric blinds (powered by SOMA blinds)
I want them to open and close automatically.
I do not want them to open and close with the sunset and sunrise times as those wildly in the UK
How does it work
Everything is based arouond Astro binding getting sunset and sunrise times.
I have two parameters for each of those:
- Open or close based on an offset
- Don’t open before or after a certain time.
- Everything is group based. Put the blind item into the control group and the magic happens.
Items files. There are multiple, grouped by base and location.
blinds.items
Group g_BlindsCollection
Group:String g_Blinds_ALL "Blinds - All"
Group:Number g_BlindsTarget_All "Blinds - All - Target"
Switch BlindsGetProperties "Blinds - Get Properties"
Switch BlindsUpSwitch_All "Front room blinds UP" {expire="2s,command=OFF"}
Switch BlindsDownSwitch_All "Front room blinds DOWN" {expire="2s,command=OFF"}
Number BlindsSunsetAdjustMinutes "Blinds Sunset adjust minutes" (g_persist_change)
Number BlindsSunriseAdjustMinutes "Blinds Sunrise adjust minutes" (g_persist_change)
DateTime BlindsSunset
DateTime BlindsClosedBy "Blinds must be closed by this time."
DateTime BlindsSunrise
DateTime BlindsOpenUntil
DateTime BlindsSunRise
DateTime BlindsOpeningAt
DateTime BlindsClosingAt
String BlindsClosedByAdjust
String BlindsClosedByAdjustMinutes
String BlindsOpenUntilAdjust
String BlindsOpenUntilAdjustMinutes
Switch BlindsSupressAutoOpen
Switch BlindsSurpessAutoClose
Number TestBlinds
rooms.frontroom.blinds.items
This is specific items for the front room blinds.
Group:String g_Blinds_FrontRoom "Front room blinds" (g_Blinds_ALL)
Group:Number g_BlindsTarget_FrontRoom "Front room blinds - Target" (g_BlindsTarget_All)
Switch BlindsUpSwitch_FrontRoom "Front room blinds UP" {expire="2s,command=OFF"}
Switch BlindsDownSwitch_FrontRoom "Front room blinds DOWN" {expire="2s,command=OFF"}
Number BlindsBattery_FrontRoomRight "Blinds - Front Room - Right - Battery [%.0f %]" (g_persist_change, g_persist_5minute)
Number BlindsPosition_FrontRoomRight "Blinds - Front Room - Right - Position [%.0f %]" (g_persist_change)
Number BlindsTarget_FrontRoomRight "Blinds - Front Room - Right - Target" (g_persist_change, g_BlindsTarget_FrontRoom, g_BlindsCollection)
String BlindsMotorControl_FrontRoomRight "Blinds - Front Room - Right - Motor" (g_persist_change, g_Blinds_FrontRoom, g_BlindsCollection)
Number BlindsBattery_FrontRoomLeft "Blinds - Front Room - Left - Battery [%.0f %]" (g_persist_change, g_persist_5minute)
Number BlindsPosition_FrontRoomLeft "Blinds - Front Room - Left - Position [%.0f %]" (g_persist_change)
Number BlindsTarget_FrontRoomLeft "Blinds - Front Room - Left - Target" (g_persist_change, g_BlindsTarget_FrontRoom, g_BlindsCollection)
String BlindsMotorControl_FrontRoomLeft "Blinds - Front Room - Left - Motor" (g_persist_change, g_Blinds_FrontRoom, g_BlindsCollection)
Number BlindsCloseAfterSunSet_FrontRoom "Close front room blinds minutes after sunset" (g_persist_change)
Number BlindsOpenAfterSunrise_FrontRoom "Open front room blinds minutes before sunrise" (g_persist_change)
DateTime BlindsOpeningAt_FrontRoom
DateTime BlindsClosingAt_FrontRoom
The rules files. Again, split by specific use.
blinds.rules
rule "Blinds - Up - All Switch changed to off"
when Item BlindsUpSwitch_All changed to ON
then
g_Blinds_ALL?.allMembers.forEach[item |
logWarn("Blinds - All", "Opening " + item.name + " from UP switch")
item.sendCommand("UP")
]
createTimer(now.plusSeconds(5)) [|
BlindsUpSwitch_All.sendCommand(OFF)
]
end
rule "Blinds - Down - All Switch changed to off"
when Item BlindsDownSwitch_All changed to ON
then
g_Blinds_ALL?.allMembers.forEach[item |
logWarn("Blinds - All", "Closing " + item.name + " from DOWN switch")
item.sendCommand("DOWN")
]
createTimer(now.plusSeconds(5)) [|
BlindsDownSwitch_All.sendCommand(OFF)
]
end
rule "Blinds - Adjust Blinds Close By"
when Item BlindsClosedByAdjust changed
then
var curTime = new DateTime()
if (BlindsClosedBy.state != NULL){
curTime = new DateTime(BlindsClosedBy.state.toString("yyyy-MM-dd'T'HH:mm:00.000Z"))
}
val cmd = BlindsClosedByAdjust.state
if (cmd == "MUP")
BlindsClosedBy.sendCommand(curTime.plusMinutes(1).toString)
else if (cmd == "MUD")
BlindsClosedBy.sendCommand(curTime.plusMinutes(-1).toString)
else if (cmd == "HUP")
BlindsClosedBy.sendCommand(curTime.plusMinutes(60).toString)
else if (cmd == "HUD")
BlindsClosedBy.sendCommand(curTime.plusMinutes(-60).toString)
logWarn("Blinds - Adjust Blinds Close By", curTime.toString("HH:mm"));
BlindsClosedByAdjust.sendCommand("--")
end
rule "Blinds - Adjust Blinds Open Until"
when Item BlindsOpenUntilAdjust changed
then
var curTime = new DateTime()
if (BlindsOpenUntil.state != NULL){
curTime = new DateTime(BlindsOpenUntil.state.toString("yyyy-MM-dd'T'HH:mm:00.000Z"))
}
val cmd = BlindsOpenUntilAdjust.state
if (cmd == "MUP")
BlindsOpenUntil.sendCommand(curTime.plusMinutes(1).toString)
else if (cmd == "MUD")
BlindsOpenUntil.sendCommand(curTime.plusMinutes(-1).toString)
else if (cmd == "HUP")
BlindsOpenUntil.sendCommand(curTime.plusMinutes(60).toString)
else if (cmd == "HUD")
BlindsOpenUntil.sendCommand(curTime.plusMinutes(-60).toString)
logWarn("Blinds - Adjust Blinds Open Until", curTime.toString("HH:mm"));
BlindsOpenUntilAdjust.sendCommand("--")
end
rule "Blinds - Adjust Blinds Adjust Minutes"
when Item BlindsClosedByAdjustMinutes changed
then
var int adjustBy = 0
if (BlindsSunsetAdjustMinutes.state != NULL){
adjustBy = (BlindsSunsetAdjustMinutes.state as Number).intValue
}
val cmd = BlindsClosedByAdjustMinutes.state
if (cmd == "UP")
adjustBy = adjustBy + 10
else if (cmd == "DOWN")
adjustBy = adjustBy - 10
else if (cmd == "--")
return;
BlindsClosedByAdjustMinutes.sendCommand("--")
BlindsSunsetAdjustMinutes.sendCommand(adjustBy)
logWarn("Blinds - Adjust Blinds Adjust Minutes", adjustBy.toString);
end
rule "Blinds - Adjust Blinds Open Until Adjust Minutes"
when Item BlindsOpenUntilAdjustMinutes changed
then
var int adjustBy = 0
if (BlindsSunriseAdjustMinutes.state != NULL){
adjustBy = (BlindsSunriseAdjustMinutes.state as Number).intValue
}
val cmd = BlindsOpenUntilAdjustMinutes.state
if (cmd == "UP")
adjustBy = adjustBy + 10
else if (cmd == "DOWN")
adjustBy = adjustBy - 10
else if (cmd == "--")
return;
BlindsOpenUntilAdjustMinutes.sendCommand("--")
BlindsSunriseAdjustMinutes.sendCommand(adjustBy)
logWarn("Blinds - Adjust Blinds Adjust Minutes", adjustBy.toString);
end
blinds.auto.open.close.rules
This is the file which works out when blinds must open and close.
rule "Auto Close Blinds"
when
// Item MinsToSunset changed
// or Item FrontRoomBlindsCloseAfterSunSet changed
// or Item BlindsCloseByHour changed
Item TestBlinds changed
or Item SunriseTime changed
or Item BlindsClosedBy changed
or Item BlindsSunsetAdjustMinutes changed
or Item BlindsOpenUntil changed
or Item BlindsSunriseAdjustMinutes changed
then
//logic:
//blindSunSetTime = sunsettime + blindSunSetAdjustMinutes
//if blindscloseby < blindSunSetTme then blindSunsetTime = blindscloseby
val dNow = new DateTime("1900-01-01").plusMinutes(now.getMinuteOfDay)
//closing logic
var int blindSunSetAdjustMinutes = 0
if (BlindsSunsetAdjustMinutes.state != NULL) blindSunSetAdjustMinutes = (BlindsSunsetAdjustMinutes.state as Number).intValue
var blindsClosedBy = 1200 //8pm
if (BlindsClosedBy.state != NULL) blindsClosedBy = new DateTime(BlindsClosedBy.state.toString("yyyy-MM-dd'T'HH:mm:00.000Z")).getMinuteOfDay
var blindSunsetTime = new DateTime(SunsetTime.state.toString("yyyy-MM-dd'T'HH:mm:00.000Z")).plusMinutes(blindSunSetAdjustMinutes).getMinuteOfDay
if (blindsClosedBy < blindSunsetTime){
logDebug("Blinds Auto OpenClose", "Blind Close by is less than the sunset time. Resetting")
blindSunsetTime = blindsClosedBy
}
var blindClosingTime = new DateTime("1900-01-01").plusMinutes(blindSunsetTime)
logWarn("Blinds Auto Open Close", "Blinds Closing At:" + blindClosingTime.toString("HH:mm"))
BlindsClosingAt.sendCommand(blindClosingTime.toString("yyyy-MM-dd'T'HH:mm:00.000Z"))
//end closing logic
//opening logic
var int blindSunRiseAdjustMinutes = 0
if (BlindsSunriseAdjustMinutes.state != NULL) blindSunRiseAdjustMinutes = (BlindsSunriseAdjustMinutes.state as Number).intValue
var blindsOpenUntil = 360 //6am
if (BlindsOpenUntil.state != NULL) blindsOpenUntil = new DateTime(BlindsOpenUntil.state.toString("yyyy-MM-dd'T'HH:mm:00.000Z")).getMinuteOfDay
var blindSunriseTime = new DateTime(SunriseTime.state.toString("yyyy-MM-dd'T'HH:mm:00.000Z")).plusMinutes(blindSunRiseAdjustMinutes).getMinuteOfDay
if (blindsOpenUntil > blindSunriseTime){
logDebug("Blinds Auto OpenClose", "Blind Open by is after than the sunset time. Resetting")
blindSunriseTime = blindsOpenUntil
}
var blindOpenTime = new DateTime("1900-01-01").plusMinutes(blindSunriseTime)
logWarn("Blinds Auto Open Close", "Blinds Opening At:" + blindOpenTime.toString("HH:mm"))
BlindsOpeningAt.sendCommand(blindOpenTime.toString("yyyy-MM-dd'T'HH:mm:00.000Z"))
//end opening logic
//do we need to close the blinds as the trigger point as tripped
if (dNow.equals(BlindsClosingAt) && BlindsSurpessAutoClose.state != ON){
logWarn("Auto Close Blinds", "Closing blinds")
}
//TODO
//Put in the opening logic
end
blinds.http.rules
This is the file which talks to the SOMA blinds api.
This still has some work to do. I want to change bits of it to use lamdas instead of duplicate code.
import java.util.concurrent.locks.ReentrantLock
val BLIND_MOVE_UP = 105
val BLIND_MOVE_DOWN = 150
val ReentrantLock blindOperateLock = new ReentrantLock
val _URL = "http://$controller$.home:8080/"
var blindRequest = newArrayList()
rule "Blinds - Operate"
when Member of g_BlindsCollection changed
then
val item = triggeringItem
logWarn("Blinds - Operate", "Name:" + item.name + " fired")
if (item.state.toString == "-1"){
return;
}
val blindLocation = item.name.replace("BlindsMotorControl_", "").split("_").get(1)
val blindDet = transform("MAP", "blinds.mac.map", blindLocation).split(",")
val mac = blindDet.get(0)
val baseURL =_URL.replace("$controller$", blindDet.get(1))
//what are we doing.
var String blindFunc = "UNKN"
var String urlOpt = ""
} else if (item.name.contains("Target")){
blindFunc = "setposition"
urlOpt = "/" + item.state.toString
} else if (item.name.contains("MotorControl")){
blindFunc = transform("MAP", "blinds.motion.map", item.state.toString)
}
var url = baseURL + blindFunc + "/" + mac + urlOpt
try{
blindOperateLock.lock();
var String value = null
var int counter = 0
while (value === null && counter < 5){
counter++;
if (counter > 1){
Thread::sleep(250)
logInfo("Blinds - Operate", "Counter is: " + counter)
}
value = sendHttpGetRequest(url)
if(value !== null && !value.contains("OK")){
logWarn("Blinds - Operate", "Failed. Value: " + value)
}
logWarn("Blinds - Operate", "Value: " + value)
}
}
catch(Exception t) {
logError("Blinds - Operate", "Broken. Exception is:" + t.getMessage())
}
finally {
blindOperateLock.unlock()
sendCommand(item.name, "-1")
}
end
rule "Blinds - Get Properties"
when Item BlindsGetProperties changed to ON
then
g_Blinds_ALL?.allMembers.forEach[item |
// logWarn("Blinds - Get Properties", "Checking properties for :" + item.name + " - " + item.type)
val blindLocation = item.name.split("_").get(1)
val blindDet = transform("MAP", "blinds.mac.map", blindLocation).split(",")
val mac = blindDet.get(0)
val baseURL =_URL.replace("$controller$", blindDet.get(1))
val operations = newArrayList("Battery", "Position")
operations.forEach[operation |
// logWarn("Blinds - Get Properties", "Checking properties for:" + blindLocation)
var url = baseURL + "get" + operation.toLowerCase() + "/" + mac
// logWarn("Blinds - Get Properties", "URL:" + url)
try{
blindOperateLock.lock();
var String value = null
var int counter = 0
while (value === null && counter < 5){
counter++;
if (counter > 1){
Thread::sleep(250)
logInfo("Blinds - Get Properties", "Counter is: " + counter)
}
value = sendHttpGetRequest(url)
if(value === null){
logWarn("Blinds - Get Properties", "Failed. Value is null")
}
//logWarn("Blinds - Get Properties", "Value: " + value)
val iVal = Integer.parseInt(transform("JS", "blindsExtractValue.js", value))
if (iVal < 0){
logInfo("Blinds - Get Properties", "Value returned is less than zero for blind " + blindLocation);
value = null; //do this so it ticks over once more.
}
else {
logWarn("Blinds - Get Properties", "Setting: " + "Blinds" + operation + "_" + blindLocation + " to " + iVal.toString)
sendCommand("Blinds" + operation + "_" + blindLocation, iVal.toString)
}
}
}
catch(Exception t) {
logError("Blinds - Operate", "Broken. Exception is:" + t.getMessage())
}
finally {
blindOperateLock.unlock()
}
]
]
BlindsGetProperties.sendCommand(OFF)
end
mac addresses map file
This file simply maps a named item and location to the mac address of the blind controller.
The second part in the string is the controller name. I (will have) have multiple controllers around the house as the BLE range is not that great and struggles for some of them.
FrontRoomRight=F4:21:18:4C:58:1B,blindsfrontroom
FrontRoomLeft=FD:9B:70:91:33:BA,blindsfrontroom
Kitchen=F3:95:30:3E:DD:4A,blindsfrontroom
// Left - "F3:95:30:3E:DD:4A"
// Right - "C1:03:74:91:05:6C"
What it looks like in HabPanel
So, you can see that I don’t want the blinds to open until 6am even though the current sunrise time is 5:53 where I am.
I don’t mind when they close though so they currently set to close 4 minutes after sunset.
If either value goes negative then it will simply add it to the time so you can easily do it before or after sunset.
TODO
Allow different rooms to have a different offset.
The Kitchen for example can open as early as it wants. It views out onto a private garden. The front of the house though, that must only open after 7:45 when everyone is nearly ready for work / school.
I’ll update the code as I make tweaks to it. Also appreciate some feedback on what and how I have done it.
All of these blinds are controlled with the Soma Smart Shades. I’ve no affiliation with them but they’re decent pieces of kit and the guys and girls at the end of the phone are very helpful (including in with the API and what I have been doing with them)
C