Turning an on/off-script into a switch

I have worked out a Python script for moving my beamer screen up/down using the 433MHz RF remote control codes. I do this on a Raspberry Pi 4 through the GPIO with OpenHABian 1.5/OpenHAB 2.5.

I call it like so to move it down:

./beamer_screen_controller on

or to move it up:

./beamer_screen_controller off

I would now like to “wrap” this into switch, so that I can turn it on/off by flipping a switch (from GUI or from rules).
What is the best way of achieving this? SInce I will use Jython for rules, I am not sure, if I should use the Exec binding or use Jython directly.

Things to note:

  • The movement of the screen takes long (~16 seconds). I also want to run the switch from rules (e.g. turn things on, when I start playing on my Chromecast). I therefore would like to run this asynchronously, since I do not want to block the whole rule, while the movement is executing (since I will also open other things that will take long (turn on the beamer and AVR)). I guess this is a more general question: Is there a design pattern for executing code asynchronously in OpenHAB?
  • How would I make the current state of the beamer screen (e.g. on/off) available to OpenHAB so, that I can use it in rules?

Thanks,
Michael

exec binding is an example

You will need to decide yourself based on personal preference. From my experience and from what I’ve heard from people in the forum, rules are much easier to implement than using the exec binding.

I use thread.start_new_thread to get things started up without holding up execution of other stuff. For example, I use this when queuing up audio alerts so that more alerts can be added to the queue while an alert is already playing. I’m not so sure you will be needing this though.

Beamer == projector? If so, I would think it would fit well with a rollershutter Item. You’d then have a rule that triggers when the Item changes or receives a command, and then executes your script. Going back to your question of async calls, if each of the actions that you are performing in your “Start movie” scene is tied to an Item, then there is nothing to worry about, since Commands to Items are asynchronous. Meaning, if your screen, AVR, media server, lights, popcorn maker, etc. are all represented by Items in OH, then you can send them commands or change their state without holding up the execution of the rest of your rule.

from core.rules import rule
from core.triggers import when

@rule("Raise/lower projector screen")
@when("Item Projector_Screen received command")
def control_projector_screen(event):
    # call your script

To call your script, you could:

  • Copy the script into the rule (not reusable, but simplest and appears to be most fitting for your use case)
  • Convert your script to a function, copy it into the Jython script, and call the function from the rule (best if function would be called by other ules in the script file, which does not appear to be the case)
  • Convert your script to a function, create a package and/or a module, copy your function into the module, and call the function from the rule (best if function is shared by multiple rules in separate script files or scripts outside of OH, which does not appear to be the case)
  • Use executeCommandLine, if it is written in Python 3 and uses a module that is not compatible with Jython

Thanks for the reply. So, if I understand correctly I would do the following:

  • Define an item as switch or rollershutter.
  • When the item changes I would then run the rule, like you defined, which would execute my code depending on the new value of the switch or rollershutter.
  • And since items execute asynchronously, the corresponding code of the rule will run asynchronously.

Is this correct? If so, I have stupid question:

  • How do I define an “empty” Switch or Rollershutter item. Meaning one, that is not tied to a channel as such, but only has a state? Up until now, I have only created to channels. I have tried to define a proxy-switch like so (I found the suggestion of a proxy switch here), but it does not show up under controls in my PaperUI (unlike the other switches, that I have defined):
Switch projector_screen "Projector Screen"

An example of how to define the needed items would be really helpful.

Regarding the rollershutter:
Would I gain something from using this item instead of the switch given that my projector screen only has two positions - close position and viewing position? It seems to me like rollershutter is something like blinds, that can stop somewhere in-between.

Thanks,
Michael

How did you create it? If you use xxx.items files, they generally don’t show up in PaperUI control.
You can alternatively create Items with PaperUI > Configuration > Items and the big blue + button.
I think that still doesn’t appear in ‘control’ because there is no channel to be controlled.

A quick way to set Item states e.g. for rule testing is to use the REST API self-documenting tool.

Clearly, this one is meant to be used from your user-facing UI anyway, so you’d probably include it in your sitemap to begin with.

2 Likes

I suggested RollerShutterItem based on the assumption that you were also reporting transition states. If the device only reports ON/OFF, then SwitchItem seems a better fit.

Ok. Makes sense. Thanks for the suggestion.

I have figured it out. I had an issue in my sitemap file, which I copied from a post in this forum. I then had the ticks " formatted incorrectly, which messed up reading the sitemap. This was immediately obvious after looking at the logs.

I have come up with a solution (see code below), that works only partially, after I figured out the issue with the proxy switch and the sitemap (see previous comment).

Now I have the issue, that my rule sometimes executes the code correctly and sometime not. However, I always get the output of:

            self.log.info("Turning projector screen ON");

or

            self.log.info("Turning projector screen OFF");

I suspect this issue might have to do with the class StatePersistingInterlockedOnOffController, which keeps internal state using the files .lock and .current_state. For instance I cannot even find, where .lock is created.

I would be very grateful, if you (and whoever else wants to) could look over it and suggest improvements. I suspect there will be many, e.g.:

  • Where to put the codesend binary?
  • Where to put the files .lock and .current_state? (if I should continue using them)
  • Naming conventions.
  • etc.

I suspect the internal state-keeping is not necessary, now that I use the Jython engine?! I wrote it this way, because I thought I would use the Exec binding. But doing away with them would probably require using persistence, which I have not yet touched.

Any suggestions are much appreciated!

Here is my current code for the rule, which I put in /etc/openhab2/automation/jsr223/python/personal/project_screen_controller_rule.py:

from core.rules import rule
from core.triggers import ItemStateUpdateTrigger

import sys
import os.path
from time import sleep
import os


class StatePersistingInterlockedOnOffController(object):
    def __init__(self, on_command, off_command, interlock_timeout):
        self.on_command = on_command
        self.off_command = off_command
        self.interlock_timeout = interlock_timeout  # lock timeout in [s]; after this time the execution is aborted
        self.lock_file_path = "/etc/openhab2/automation/jsr223/python/personal/.lock"
        self.state_file_path = "/etc/openhab2/automation/jsr223/python/personal/.current_state"
        self.lock_wait_time = 0.5  # wait time in [s] between checking lock

    def turn_on(self):
        self.execute("ON", lambda: (self.on_command()))

    def turn_off(self):
        self.execute("OFF", lambda: (self.off_command()))

    def execute(self, target_state, command):
        self.wait_for_lock_file_release()
        self.create_lock_file()
        if self.already_in_state(target_state):
            print("Already in target state: " + target_state)
        else:
            command()
            self.update_state(target_state)
        self.delete_lock_file()

    def already_in_state(self, target_state):
        if not os.path.isfile(self.state_file_path):
            # no state_file_path file exists: create it with desired target_state, return False to move into that state
            self.update_state(target_state)
            return False
        state_file = open(self.state_file_path, "r+")
        current_state = state_file.read()
        if current_state == target_state:
            return True

    def update_state(self, state):
        state_file = open(self.state_file_path, "w")
        state_file.write(state)

    def wait_for_lock_file_release(self):
        """
        Checks if the script is already running in another instance by checking if the .lock file exists.
        If so, waits until it is removed or timeout is hit.
        """

        if not os.path.isfile(self.lock_file_path):
            return  # no lock file exists, so return directly

        print("Waiting for previous execution to finish.")

        total_wait_time = 0
        while (os.path.isfile(self.lock_file_path)):
            sleep(self.lock_wait_time)
            total_wait_time += self.lock_wait_time

            if total_wait_time > self.interlock_timeout:
                raise Exception(
                    "Timeout hit, while waiting for previous run to finish. Please check, if the lock file was not removed during previous execution.")

    def create_lock_file(self):
        try:
            lock_file = open(self.lock_file_path, "w+")
            lock_file.close()
        except IOError:
            print("Error: Could not create lock-file.")
            # EXIT AND RETURN ERROR

    def delete_lock_file(self):
        try:
            os.remove(self.lock_file_path)
        except IOError:
            print("Could not delete lock file.")
            # EXIT AND RETURN ERROR


# controller setup: define commands and interlock timeout and initialize controller
def down():
	os.system("/etc/openhab2/automation/jsr223/python/personal/codesend 14486692")

def up():
	os.system("/etc/openhab2/automation/jsr223/python/personal/codesend 14486690")

def stop():
	os.system("/etc/openhab2/automation/jsr223/python/personal/codesend 14486696")

def on_command():
	down(), sleep(15.65), stop()

def off_command():
	up(), sleep(17), stop()

@rule("Projector screen trigger rule", description="This rule triggers the projector screen controller, when its switch is activated.", tags=["Projector screen rule"])
class ExampleExtensionRule(object):
    def __init__(self):
        self.triggers = [ItemStateUpdateTrigger("projector_screen").trigger]
        self.first_switch = True
        lock_timeout = 20
        self.controller = StatePersistingInterlockedOnOffController(on_command, off_command, lock_timeout)

    def execute(self, module, inputs):
	item_state = str(inputs['state'])
        if(item_state in "ON"):
            self.log.info("Turning projector screen ON");
            self.controller.turn_on()
        if(item_state in "OFF"):
            self.log.info("Turning projector screen OFF");
            self.controller.turn_off()

In my opinion, any device that needs time to complete received command should be handled in a similar way, using a proxy virtual item as you did, but in a NON-BLOCKING fashion. The proxy item should accept and process ANY COMMAND AT ANYTIME and “take care of the magic” to set the device to the correct state when the real device is ready to accept commands.

Your beamer_switch should handle the ON and OFF commands sent at anytime. To achieve this, you can store the “target state” for later processing if the beamer is currently in a state that does not allow receiving/processing commands immediately. Then the beamer_switch state should be updated to transitional states (HEATING/COOLING) if needed and to the final state (ON/OFF) once ready. On my side, I use Number items to handle the transitonal state that Switch items cannot handle. Rollershutter items are a good idea if only 2 transitional states (but for A/V devices with input switching, this is not enough to handle all possible transitions).

I believe that blocking patterns (waiting or debouncing or similar ) are nonsense : life is asynchroneous and we spent years to take computers from sequential processing to asynchroneous mutithreaded processing for good reasons : processing get smooths and user-friendly. If you block or debounce, you slow down processing or even jeopardize the states expected by the triggering items or rules when the logic of processing is complex.

var Timer vp_task = null //timer that will take care of device delays
var Integer vp_on_delay = 20 //delay for switching on the device (or getting to the )
var Integer vp_off_delay = 40 // delay for switching off the device
var Integer vp_state_target = null


rule "Projector command processor (command)"
when
    Item AV_mRDC_Salon_vp received command
then
//AV_mRDC_Salon_vp states : 0=OFF, 10=Preheating, 90=Shutting-down, 100=0N
//IR_mRDC_Salon_command is a virtual item that control IR sending and IR queue
    if(AV_mRDC_Salon_testmode.state == ON) { // when in test mode: reduce delays
            vp_on_delay = 5
            vp_off_delay = 2
    }

    switch(AV_mRDC_Salon_vp.state.toString + ">" + receivedCommand.toString){
        //cases when ON command received
        case "0>100": { 
			vp_state_target = 100 // ON
            AV_mRDC_Salon_vp.postUpdate(10) //Preheating
            IR_mRDC_Salon_command.sendCommand("Jvc_vp,poweron")
            vp_task?.cancel()
            vp_task = createTimer(now.plusSeconds(vp_on_delay)) [|
                AV_mRDC_Salon_vp.postUpdate(100) //ON
                vp_task = null
                ]
        }
        //case "10>100": case "100>100": { }//Preheating>ON or ON>ON: nothing to do
        case "90>100": { //Shutting-down>ON: wait for state to change to OFF before sending the ON command
			vp_state_target = 100 // ON
            AV_mRDC_Salon_vp.postUpdate(10) //Preheating
        }
        //cases when OFF command received
        //case "0>0": case "90>0": {  }//Shutting-down>OFF or OFF>OFF: nothing to do
        case "100>0": { 
			vp_state_target = 0 //OFF
            AV_mRDC_Salon_vp.postUpdate(90) //Shutting-down
			IR_mRDC_Salon_command.sendCommand("Jvc_vp,poweroff")
            vp_task?.cancel()
            vp_task = createTimer(now.plusSeconds(vp_off_delay)) [|
                AV_mRDC_Salon_vp.postUpdate(0) //OFF
                vp_task = null
                ]
        }
        case "10>100": { //Preheating>OFF: wait for state to change to ON before sending the OFF command
			vp_state_target = 0 //OFF
            AV_mRDC_Salon_vp.postUpdate(90) //Shutting-down
        }
    }
end

rule "Projector command processor (state)"
when
    Item AV_mRDC_Salon_vp changed
then
    if (AV_mRDC_Salon_vp.state != vp_state_target) { AV_mRDC_Salon_vp.sendCommand(vp_state_target.toString)}
end

For the screen, processing the logic is different. When receiving an UP command while rolling-down, I store the elapsed time for the rolling-down so I can calculate when the screen will actually be in the UP position and call a postUpdate() at the right time. In this case, “the magic” it to send a STOP command to the screen, followed by an UP command (but this depends on behavior of your device).

My examples are DSL-style. I just installed JSR and I am still investigating the benefits of Jython over DSL (except speed which is obvious) to take advantage of it in upcomng reimplementation.