binding a callback to an event

Do you have questions about writing plugins or scripts in Python? Meet the coders here.
User avatar
kgschlosser
Site Admin
Posts: 5498
Joined: Fri Jun 05, 2015 5:43 am
Location: Rocky Mountains, Colorado USA

binding a callback to an event

Post by kgschlosser » Tue Dec 24, 2019 6:27 am

OK I am going to provide a coding example of how to take care of some possible quirks with devices and EG.
This topic has come up more then a single time in various forms so I am going to post some code that is going to be a very powerful tool in your arsenal


take this scenario..

say you have an audio device that is attached to your PC.. say it's a TV. as these have audio over HDMI. you want to react to an event gor the audio device getting attached which would occur when powering the TV on.. You want it to set the volume or some such thing..

Then say some other event that you react to changes the resolution on the TV... But that resolution change for some reason causes a disconnect and then a reconnect event moments later for the audio device.. you could do the whole enable disable kind of thing.. But there is a single issue with using that solution.. It is possible for it to bounce out of sync and the whole thing stops working or it starts acting in a manner you do not want it to..


This is how you can get around that problem and not have to use the enable/disable routine.

Code: Select all

def event_callback(event):
    # we unbind the events until the next time
    # this script gets run again.. It is effectively doing
    # the same as the enable disable but without any 
    # sync issues
    
    if event.string == 'Audio.DeviceAttached':
        eg.Unbind('Audio.DeviceAttached', event_callback)

    elif event.string == 'Audio.DeviceRemoved':
        eg.Unbind('Audio.DeviceRemoved', event_callback)

    return True  # returning True stops the event from running any macros..


eg.Bind('Audio.DeviceAttached', event_callback)
eg.Bind('Audio.DeviceRemoved', event_callback)

this you would place before the action to change the resolution.. It is going to consume the the events for the device getting attached and detached. it is only going to consume only a single occurrence of each of the events and then thing go back to normal. This cannot get out of sync for some issue.

you also have the option of running other code inside of that callback if you wanted to.

this is also a good way to consume duplicate IR codes if the duplicates do not always happen a simple timer to do the unbind would be easy enough to put into place...
If you like the work I have been doing then feel free to Image

Crowley
Posts: 27
Joined: Sat Dec 21, 2019 6:05 am

Re: binding a callback to an event

Post by Crowley » Tue Dec 24, 2019 10:46 am

I'm new to eventghost so I'm probably asking a lot of stupid questions but here goes.

Do I add this as a python script? When I do I get this error:

12:42:37 PM Error compiling script.
12:42:37 PM Traceback (most recent call last):
12:42:37 PM IndentationError: unindent does not match any outer indentation level (37, line 10)

If I try to modify the part that seems to throw the error it just throws a different error. I have no clue how to use python at all.

User avatar
kgschlosser
Site Admin
Posts: 5498
Joined: Fri Jun 05, 2015 5:43 am
Location: Rocky Mountains, Colorado USA

Re: binding a callback to an event

Post by kgschlosser » Tue Dec 24, 2019 4:32 pm

the indents are off.. I will fix it...


also it is not going to work "out of the box" you need to change the strings in the script to the events you get.
If you like the work I have been doing then feel free to Image

Crowley
Posts: 27
Joined: Sat Dec 21, 2019 6:05 am

Re: binding a callback to an event

Post by Crowley » Tue Dec 24, 2019 4:34 pm

Yeah, I got that much atleast (:D ) , but have no idea what the indents should look like.

User avatar
kgschlosser
Site Admin
Posts: 5498
Joined: Fri Jun 05, 2015 5:43 am
Location: Rocky Mountains, Colorado USA

Re: binding a callback to an event

Post by kgschlosser » Tue Dec 24, 2019 4:35 pm

ok i redid the indents. if you still get the error then you are going to have to redo the indents after you paste it into a script.

the indents are 4 spaces.. so you will need to delete the current indents and make new ones that are 4 spaces..
If you like the work I have been doing then feel free to Image

Crowley
Posts: 27
Joined: Sat Dec 21, 2019 6:05 am

Re: binding a callback to an event

Post by Crowley » Tue Dec 24, 2019 4:49 pm

Ok, no more indent errors but now it gives me a different one. My code:

Code: Select all

def event_callback(event):
    # we unbind the events until the next time
    # this script gets run again.. It is effectively doing
    # the same as the enable disable but without any 
    # sync issues
    
    if event.string == 'System.Device.Attached.SoundDevice.NVIDIA High Definition Audio':
        eg.Unbind('System.Device.Attached.SoundDevice.NVIDIA High Definition Audio', callback)

    elif event.string == 'System.Device.Removed.SoundDevice.NVIDIA High Definition Audio':
        eg.Unbind('System.Device.Removed.SoundDevice.NVIDIA High Definition Audio', callback)

    return True  # returning True stops the event from running any macros..


eg.Bind('System.Device.Attached.SoundDevice.NVIDIA High Definition Audio', callback)
eg.Bind('System.Device.Removed.SoundDevice.NVIDIA High Definition Audio', callback)
and error:

6:42:29 PM Python Script
6:42:29 PM Traceback (most recent call last):
6:42:29 PM Python script "5", line 16, in <module>
6:42:29 PM eg.Bind('System.Device.Attached.SoundDevice.NVIDIA High Definition Audio', callback)
6:42:29 PM NameError: name 'callback' is not defined

Do I need to add something else?

User avatar
kgschlosser
Site Admin
Posts: 5498
Joined: Fri Jun 05, 2015 5:43 am
Location: Rocky Mountains, Colorado USA

Re: binding a callback to an event

Post by kgschlosser » Tue Dec 24, 2019 9:30 pm

my bad.. brain must have been elsewhere

it's fixed
If you like the work I have been doing then feel free to Image

Crowley
Posts: 27
Joined: Sat Dec 21, 2019 6:05 am

Re: binding a callback to an event

Post by Crowley » Wed Dec 25, 2019 4:44 am

Seems to work perfectly now and really could be useful for many other things as well. Thank you so much for the support and merry Christmas!

User avatar
kgschlosser
Site Admin
Posts: 5498
Joined: Fri Jun 05, 2015 5:43 am
Location: Rocky Mountains, Colorado USA

Re: binding a callback to an event

Post by kgschlosser » Wed Dec 25, 2019 10:59 am

no worries m8..

glad it solved your issue.
If you like the work I have been doing then feel free to Image

piert
Experienced User
Posts: 322
Joined: Tue Jun 14, 2011 2:53 pm

Re: binding a callback to an event

Post by piert » Fri Dec 27, 2019 2:27 pm

This solution is just in time, as I am trying to create actions that capture the 0-9 inputs from my remote. The IRreceiver creates multiple times the same input (i.e. if I press ' 1' , I receive up to 7x 1. This is not workable, because it means selecting channel 12 would result in '1111111122222222' being selected.

You mentioned inserting a timer, how would that work? One must also be able to select channel 11....
Note that I normally use the 'wait' action in Eventghost, but I know this is not preferable, as it halts the entire process, correct?

User avatar
kgschlosser
Site Admin
Posts: 5498
Joined: Fri Jun 05, 2015 5:43 am
Location: Rocky Mountains, Colorado USA

Re: binding a callback to an event

Post by kgschlosser » Sat Dec 28, 2019 9:06 am

that is correct as far as using the wait.. if you can avoid using it then you should. you will have to use waits when emulating mouse movements because it takes time for the pointer to move from point a to point b. So there are use examples where they are required.. If you MUST use a wait or multiple waits in a single macro the total duration of all of the waits in a macro should not exceed 3.0 seconds. The reason why is because there is a possibility of a windows notification coming in and if causing a lengthy stall in the window notification system occurs there can and will be undesirable consequences from doing this. any time it is going to take a macro longer then 3 seconds to run then you either need to create a script and carry out the actions inside of a thread.. Or if the macro can be broken in multiple sections that do not have to be carried out exactly one after another then create multiple macros. and at the end of each of the macros trigger an event that will run the next "stage" this will ensure that the windows notification system will not be held up for some reason.

in your use case we do not need a timer.. there is a constant that you provided. the repeat happens exactly 7 times.. This macro will get run when the first event comes in, so we will absorb the next 6 by counting the number of times the callback gets used.

This is the most important piece.. you need to make sure that this script gets run before any macro containing the event.. so you make a folder at the top of your config tree just under AutoStart and add the macro to that folder... EG runs macros in a specific order.. it looks for macros starting from the top of the tree to the bottom. each branch of the tree (folder) is also evaluated from the top down as well. we want to make sure that this script is always run first..

since you are dealing with only a single bound event per script and not trying to absorb an event that did not run this script we can capture the event string that ran the script and use that in the bind and unbind calls. Unfortunately the current versions of EG do not evaluate the use of wildcards for binding to an event. so you MUST have this script in a macro for each of the buttons even if all of your buttons experience this issue. I know it is a pain in the ass to do, and I am sorry for that. This and I do plan on changing the code in EG to check for wildcards for the event notifications.

I do also plan on adding an action that will perform this simple operation without needing to use a python script. I may make a plugin to handle it
and then add it in a future version of EG. I have not yet decided on what I want to do. It is a really handy feature i will say that. it is better to consume an event then to wait and then clear the event queue. there are to many unknowns when going that route, like when is the repeat event going to take place?.. and are there any other events will get removed when the queue gets cleared? and also the use of the wait that can be avoided.

Code: Select all

EVENT_STRING = eg.event.string

count = 0

def event_callback(_):
    # we have to make the variable count a global so we are able to change it.
    global count
    count += 1

    # if 6 events have occurred after the original event then we want to unbind
    # the callback so we can start over again.
    # If the number of events that can occur does not remain constant
    # 100% of the time we can address that issue as well by using a timer to unbind
    # this callback after a period of time has elapsed. let me know
    # if that is the case

    if count == 6:
        eg.Unbind(EVENT_STRING, event_callback)

    return True  # returning True stops the event from running any macros..

eg.Bind(EVENT_STRING, event_callback)
I also wanted to to point out that this binding to the event feature can be used in place of using the jump if action to an empty macro without returning in order to not carry out the rest of a macro ... you can evaluate the conditions that need to be met just like you would do before the jump if action inside of the event_callback function and if the conditions are met then return False if the conditions are not met then return True. just remember you always have to unbind the event in the callback at some point or another.



I decided to throw in the timer example i just read over your post again and you used the words "up to" so it is not always 7 times.

read the comments in the script there are some things you may have to adjust. It should automatically dial into the best possible timeout for unbinding the event. we want to keep this as close as possible to the duration it takes between the events taking place.. reason why is..

say you want to enter 11 as a number... events for a repeat should happen faster then you pressing the 1 button 2 times but that margin is going to be really skinny. so I added a padding that you can adjust to dial this in better.

Code: Select all

import threading
import time

# this is a padding that gets added to the unbind timer.
# you may have to adjust this value slightly if you find 
# that you are still getting repeat events.. it is set
# so it will pad the timeout by 50 milliseconds. the fastest
# ir protocol is 45 milliseconds between loops and the slowest
# can be close to 250 milliseconds. I wrote in some code that 
# measures the duration between repeat events and then averages
# them. it averages the last 20 events. and uses this
# when setting the unbind timeout... so before tinkering with 
# this number run the event a few times and see if the repeat 
# events go away. if they do not then make an adjustment as needed
# you have to remember this is going to reset it's self each time 
# you restart EG.
UNBIND_PADDING = 0.05


# If you set this to True it will print out the calculated unbind timeout
# once you have gotten everything dialed in and working correctly
# you can set this to True. tell me what the results are and we can hard
# code that number as the start point. It shopuld work without needing
# us to do this.. but I felt that a fallback scenario should be in place
# just in case.
DEBUG = False

count = 0

EVENT_STRING = eg.event.string

try:
    UNBIND_TIMEOUT
except NameError:
    UNBIND_TIMEOUT = 0.5
    UNBIND_HISTORY = []


def unbind_function():
    eg.Unbind()


start_time = time.time()

def event_callback(_):
    # we have to make the variable count a global so we are able to change it.
    global count
    global start_time
    global UNBIND_TIMEOUT
    global timer

    timer.cancel()
    
    count += 1
    stop_time = time.time()

    UNBIND_HISTORY.append((stop_time - start_time) * 1000)
    
    if len(UNBIND_HISTORY) > 20:
        UNBIND_HISTORY.pop(0)

    # get an average so we can dial in when to unbind.
    # it then gets padded to ensure another event does not come in.
    UNBIND_TIMEOUT = ((sum(UNBIND_HISTORY) / len(UNBIND_HISTORY)) / 1000.0) + UNBIND_PADDING
    
    if DEBUG:
        print UNBIND_TIMEOUT, ':', len(UNBIND_HISTORY)

    # if 6 events have occurred after the original event then we want to unbind

    if count == 6:
        eg.Unbind(EVENT_STRING, event_callback)
    else:
        timer = threading.Timer(UNBIND_TIMEOUT, eg.UnBind, EVENT_STRING, event_callback)
        timer.start()

    return True  # returning True stops the event from running any macros..


timer = threading.Timer(UNBIND_TIMEOUT, eg.UnBind, EVENT_STRING, event_callback)
timer.start()
eg.Bind(EVENT_STRING, event_callback)
If you like the work I have been doing then feel free to Image

User avatar
kgschlosser
Site Admin
Posts: 5498
Joined: Fri Jun 05, 2015 5:43 am
Location: Rocky Mountains, Colorado USA

Re: binding a callback to an event

Post by kgschlosser » Sat Dec 28, 2019 9:11 am

sorry for the typos.. my arthritis is really giving me a bugger of a time today and my hands are swelled up.. I have sausage fingers!!!

the grammar is because I am to damned lazy to reread the thing and edit the post.. i fixed some of my typos and change the wording in some places.. and forgot to go back and remove some words that needed to be deleted.. I am sure you can figure it out tho.
If you like the work I have been doing then feel free to Image

piert
Experienced User
Posts: 322
Joined: Tue Jun 14, 2011 2:53 pm

Re: binding a callback to an event

Post by piert » Sun Dec 29, 2019 10:30 am

Thanks Kevin. I am on the road today but will try this out when I am back home and will let you know how it went.

Ps: hope your arthritis problems improve for you!

piert
Experienced User
Posts: 322
Joined: Tue Jun 14, 2011 2:53 pm

Re: binding a callback to an event

Post by piert » Mon Dec 30, 2019 1:06 pm

Getting the following error:

Code: Select all

         Traceback (most recent call last):
           Python script "1", line 80, in <module>
             timer = threading.Timer(UNBIND_TIMEOUT, eg.UnBind, EVENT_STRING, event_callback)
           File "C:\Program Files (x86)\EventGhost\eg\__init__.py", line 45, in __getattr__
             mod = __import__("eg.Classes." + name, None, None, [name], 0)
         ImportError: No module named UnBind
         
I should let you know that I have solved my IR code repetition problem meanwhile by using a 'disable' action of a folder containing each of the 0-9 button commands, of which each event/action also contains a 0.2 second timer with an associated Event generated after the timer completes. This event I have used to re-enable the entire folder with all the separate 0-9 button command actions again.
It works like a charm, so I don't really need a solution anymore, but I am still intrigued by the bind/unbind solution you provided, so if the above error can be solved, I would be interested in re-testing.

User avatar
kgschlosser
Site Admin
Posts: 5498
Joined: Fri Jun 05, 2015 5:43 am
Location: Rocky Mountains, Colorado USA

Re: binding a callback to an event

Post by kgschlosser » Mon Dec 30, 2019 2:10 pm

change UnBind to Unbind.. that's my bad.
If you like the work I have been doing then feel free to Image

Post Reply