Blinds - Auto Open - Auto Close

I thought I would share my latest automation for two reasons:

  1. Feedback on how it can be improved
  2. 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:

  1. Open or close based on an offset
  2. Don’t open before or after a certain time.
  3. 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

Very nice rule you have made.
Looks pretty solid and good working to me (unfortunately I don’t have controllable blinds at my house for testing :frowning:)

A little idea for improvement
I don’t know what kind of blinds you have, but maybe you can connect the Astro binding and a weather binding for calculating when bright sunlight is on a specific window to close the blinds enough for letting light through but not completly close the blinds.

A few months ago I made that in a house with blinds on all sides except north.
The results where great and the blinds were closed only that far to keep direct sunlight out and enough light in.

Hopes you understand what i mean, and like my idea :wink:

Greetings,
Jordo

thanks for the comments.

There is another thread here somewhere where they are working on the sun’s position. One of the guys / gals there is talking about also lowering the blinds to compensate.

Living in the UK, I want every drop of sun I can get through the window :slight_smile: