Page 1 of 1

How to send all EG events over tcp w/o creating a macro??

Posted: Thu Jan 10, 2013 1:10 am
by etc6849
I'm working on a tcp plug-in that will connect to my home automation (HA) server (called SYS or Premise) and allow for two-way communications. My HA server can then trigger actions in EventGhost (such as send OSD messages), send events with payload (to trigger EG macros) , and receive events from EventGhost (to trigger things in the HA server).

Everything works (code is below), but the only way I can get EventGhost events sent to my home automation server is to include the Premise: SendEvent action in a macro along with whatever events I want to receive. This is a manual task of dragging all events that I want into a macro, then adding the "SendEvent" action.

I'd like to have all EventGhost events automatically be sent. I'm hoping there's a way to do this with a plug-in? As implied from above, whatever solution must work over the network via a tcp listener. Please help!

PS: I'm new to EventGhost, so please do not assume I know anything (beyond what the plug-in does below), but I did do a lot of reading so that I could write the plug-in...

Here's the code I have now for SendEvent:

Code: Select all

# this action will send the string and payload of an EventGhost event to the SYS server. 
class SendEvent(eg.ActionClass):
    description = "Sends to the connected Premise server the EventGhost event string and payload (as a string) that initiated the action."
    
    def __call__(self):
        sEventString = str(eg.event.string)
        sEventPayload = str(eg.event.payload)
        self.plugin.SendCmd_Event(sEventString, sEventPayload)
Here's the entire plug-in:

Code: Select all

eg.RegisterPlugin(
    name = "Premise",
    author = "etc6849", 
    version = "1.0",
    guid = '{85f83859-9bb5-4112-9be6-04c5ff5e6ce8}',
    canMultiLoad = False,
    description = "Send and receive events or actions to and from a Premise home automation server. Download Premise for free at http://www.cocoontech.com/wiki/Premise",
)

import eg
import select, time
import asynchat
import xml.dom.minidom as xmldom
from socket import *
from threading import Event, Thread

# define the plugin  that will send/receive actions and also send events to a connect SYS server
# note 1: SYS server refers to the Premise home automation server.  
# note 2: this plugin is disigned to handle more than one connection.
class Premise(eg.PluginBase):
    def __init__(self):
        self.AddAction(SetValue)
        self.AddAction(SendEvent)
        print "Init..." 
    
    def __start__(self, host, port):
        self.theSock = socket(AF_INET, SOCK_STREAM)
        self.host = host
        self.port = port
        self.data = ''
        bound = 0
        while bound == 0 :
            bound = 1
            try :
                addr = (self.host, self.port)
                self.theSock.bind(addr)
            except :
                bound = 0
                print "Socket error"
        self.theSock.listen(5)
        print "Premise plugin bound to port " + str(self.port)
        self.input = [self.theSock]
        self.output = []
        self.errored = []
        self.mainThreadEvent = Event()
        mainThread = Thread(target=self.main, args=(self.mainThreadEvent,))
        mainThread.start()

    def __stop__(self):
        print "Stopping...please wait..."
        self.mainThreadEvent.set()

    # main loop
    def main(self,mainThreadEvent):
        self.client = None
        while not self.mainThreadEvent.isSet():
            inputready,outputready,errored = select.select(self.input,self.output,self.errored,1)
            
            for s in inputready:
                if s == self.theSock: 
                    self.client, address = self.theSock.accept()
                    print "Client added: ", self.client, address
                    if not self.input.count(self.client)>0:
                        self.input.append(self.client)
                else:
                    # receive upto 4096 bytes of data
                    data = s.recv(4096)
                    
                    tempConn = self.input[1:len(self.input)]
                    
                    # if data is received do something with it
                    if data:
                        # get rid of any line terminators from the packet
                        data = self.StripNoPrint(data)
                        print "Processing data: ", data
                        
                        # process <ACTION> packet received from the SYS server
                        if '<ACTION>' in data:
                            self.ProcessCmd_Action(data)

                        # process <EVENT> packet received from the SYS server
                        if '<EVENT>' in data:
                            self.ProcessCmd_Event(data)
                            
                        # echo back data to each connection
                        for i in tempConn:
                            # echo the data back along with a CR
                            i.send(data + "\r")
                        
                        # set an event trigger in EventGhost
                        eg.TriggerEvent(str(s.getpeername()) + ": " + data)
                    else:
                        if self.input.count(s) > 0:
                            self.input.remove(s)
                            print "Client removed: ", s
        self.theSock.close()
        print "Main thread has ended"

    # define Configure panel
    def Configure(self, host = '0.0.0.0', port = 1024):
        panel = eg.ConfigPanel(self)
        mySizer = wx.GridBagSizer(5, 5)

        hostControl = wx.TextCtrl(panel, -1, host)
        mySizer.Add(wx.StaticText(panel,-1,"Host ip or blank: "),(2,0))
        mySizer.Add(hostControl,(2,1))

        portControl = panel.SpinIntCtrl(port, 1024, 1024)
        portControl.SetInitialSize((60,-1))
        mySizer.Add(wx.StaticText(panel,-1,"Port number: "),(3,0))
        mySizer.Add(portControl,(3,1))
        panel.sizer.Add(mySizer, 0, flag = wx.EXPAND)

        while panel.Affirmed():
            panel.SetResult(hostControl.GetValue(),portControl.GetValue())
                     
    def StripNoPrint(self, str):
        results = ""
        # iterate through and remove all non-printable ascii characters
        for char in str:
            if ord(char) > 31 and ord(char) < 127:
                results += char
        return results

    def ProcessCmd_Action(self, sCommand):
        # replace the first occurance of the tag with nothing
        sCommand = sCommand.replace("<ACTION>", "", 1)
        print "Processing action: " + sCommand
        
        # this will invoke an action based on received python code from the SYS server
        eg.plugins.EventGhost.PythonCommand(sCommand)
        return ''

    def ProcessCmd_Event(self, sCommand):
        # replace the first occurance of the tag with nothing
        sCommand = sCommand.replace("<EVENT>", "", 1)
        
        # remove all "<" from array
        sCommand = sCommand.replace("<", "")
        
		# split the packet into a list
        sCommand = sCommand.split(">")
        
        # trigger event in EG, process payload if it's present
        if sCommand[1] == "":
            print "Processing event: " + sCommand[0]
            self.TriggerEvent(sCommand[0])
        else:
            print "Processing event and payload: " + sCommand[0] + "\'" + sCommand[1] + "\'"
            self.TriggerEvent(sCommand[0], payload = sCommand[1])
        return ''
        
    # send the event string and event payload (as a string) to the SYS server
    def SendCmd_Event(self, sEventString, sEventPaload):
        sCommand = "<EVENT>" + "<" + sEventString + "><" + sEventPaload + ">"
    
        # send EVENT packet to all connected SYS servers
        tempConn = self.input[1:len(self.input)]
        for i in tempConn:
            i.send(sCommand + "\r")
            print "Sending event: " + sCommand
        return ''

    # send a setvalue request to the SYS server in order to change some object's property    
    def SendCmd_SetValue(self, sObjPath, sPropName, sPropValue, bForceStateChange):
        sCommand = "<SETVAL>"
        if bForceStateChange:
            sCommand = "<SETVALFORCE>"
        sCommand = sCommand + "<" + sObjPath + "><" + sPropName + "><" + sPropValue + ">"
    
        # send SetValue packet to all connected SYS servers
        tempConn = self.input[1:len(self.input)]
        for i in tempConn:
            i.send(sCommand + "\r")
            print "Sending setvalue: " + sCommand
        return ''
            
            
# this action will invoke the SetValue and SetValueForce methods within SYS using the passed parameters. 
class SetValue(eg.ActionClass):	
    description = "Calls the SetValue method on the connected Premise server using the supplied parameters."
    
    def __call__(self, sObjPath, sPropName, sPropValue, bForceStateChange):
        self.plugin.SendCmd_SetValue(sObjPath, sPropName, sPropValue, bForceStateChange)
     
    # build the configuration panel    
    def Configure(self, sObjPath="Home.Living.Light", sPropName="PowerState", sPropValue="True", bForceStateChange=0 ):
        panel = eg.ConfigPanel()

        sObjPathControl = panel.TextCtrl(sObjPath)
        sPropNameControl = panel.TextCtrl(sPropName)
        sPropValueControl = panel.TextCtrl(sPropValue)
        bForceStateChangeControl = panel.CheckBox(bForceStateChange)
        
        panel.AddLine("Use options below to define the desired Premise server action.  Values are not case sensitive.")
        panel.AddLine("SYS Object Path: ", sObjPathControl)
        panel.AddLine("Property Name: ", sPropNameControl)
        panel.AddLine("Property Value: ", sPropValueControl)
        panel.AddLine("Force State Change: ", bForceStateChangeControl)
        
        while panel.Affirmed():
            panel.SetResult(sObjPathControl.GetValue(),sPropNameControl.GetValue(),sPropValueControl.GetValue(), bForceStateChangeControl.GetValue())
            
           
# this action will send the string and payload of an EventGhost event to the SYS server. 
class SendEvent(eg.ActionClass):
    description = "Sends to the connected Premise server the EventGhost event string and payload (as a string) that initiated the action."
    
    def __call__(self):
        sEventString = str(eg.event.string)
        sEventPayload = str(eg.event.payload)
        self.plugin.SendCmd_Event(sEventString, sEventPayload)

Re: How to send all EG events over tcp w/o creating a macro?

Posted: Thu Jan 10, 2013 7:18 am
by Pako
Just for clarification:
I understand correctly, that you want to capture completely all events (from all plugins) ?

Pako

Re: How to send all EG events over tcp w/o creating a macro?

Posted: Thu Jan 10, 2013 12:32 pm
by krambriw
Actually, instead of dragging all events to the macro, you could just add one event with the name "*"

If you have a plugin already you could also add a thread to it that has a while loop listening to all events and then just forwards them to your HA server. Vice versa, the plugin would listen to commands from your HA server and generate events in EG for what ever reason you need.

Re: How to send all EG events over tcp w/o creating a macro?

Posted: Thu Jan 10, 2013 10:22 pm
by etc6849
Correct, I want to capture completely all events (from all plugins) and this needs to run as a secondary thread within the plug-in from my original post.

If there's a Python example of how one listens for all events from a plug-in, I'd be interested in studying it. Also, the threading part sounds complicated as a resource (the socket) will need to be used across two different threads.

An example of sharing a socket across multiple threads would really help as I'm not a programmer (although I'm very good at writing "linear" HA programs that run in a single thread). I'm willing to study Python more and threading, but I honestly have no idea how to share the socket across two threads... My guess is I'd need to use some sort of shared queue, but I'm new to Python.

Re: How to send all EG events over tcp w/o creating a macro?

Posted: Fri Jan 11, 2013 5:08 am
by etc6849
Ok, the good news is the multiple threading thing was easy and actually works! I did have to use a queue as I'd expected.

However, the while loop that waits for an event ghost event could use some help (please). To keep it from running forever, I simply test if a new event string equals an old event string. However, this is not ideal as it means I will not receive consecutive identical events into my home automation server. I'm thinking that one should use the event.time property as a better test as no other unique identifiers are defined? http://www.eventghost.org/docs/eg/eg.Ev ... GhostEvent

Any ideas on how to make the code below a better event listener? Is it always acceptable to compare the time property of two events to know if they are the same event?

Code: Select all

    def eventloop(self,eventLoopThreadEvent):
        sEventOld = ""
        while not self.eventLoopThreadEvent.isSet():
            # listen for event and print if it is new
            sEventNew = "<EVENT>" + "<" + str(eg.event.prefix) + "." + str(eg.event.suffix) + "><" + str(eg.event.payload) + ">"

            if not sEventNew == sEventOld:
                print sEventNew
                self.qSend.put(sEventNew)
                sEventOld = sEventNew

Full plug-in code:

Code: Select all

# -*- coding: utf-8 -*-


eg.RegisterPlugin(
    name = "Premise",
    author = "etc6849", 
    version = "1.0",
    guid = '{85f83859-9bb5-4112-9be6-04c5ff5e6ce8}',
    canMultiLoad = False,
    description = "Send and receive events or actions to and from a Premise home automation server. Download Premise for free at http://www.cocoontech.com/wiki/Premise",
)

import eg
import select, time
import asynchat
import Queue
import xml.dom.minidom as xmldom
from socket import *
from threading import Event, Thread

# define the plugin  that will send/receive actions and also send events to a connect SYS server
# note 1: SYS server refers to the Premise home automation server.  
# note 2: this plugin is disigned to handle more than one connection.
class Premise(eg.PluginBase):
    def __init__(self, qSend=None):
        # initialize the send queue
        self.qSend = Queue.Queue()
        self.AddAction(SetValue)
        self.AddAction(SendEvent)
        print "Premise plugin is initialized..." 
    
    def __start__(self, host, port):
        self.theSock = socket(AF_INET, SOCK_STREAM)
        self.host = host
        self.port = port
        self.data = ''
        bound = 0
        while bound == 0 :
            bound = 1
            try :
                addr = (self.host, self.port)
                self.theSock.bind(addr)
            except :
                bound = 0
                print "Socket error"
        self.theSock.listen(5)
        print "Premise plugin bound to port " + str(self.port)
        self.input = [self.theSock]
        self.output = []
        self.errored = []
        
        self.mainThreadEvent = Event()
        mainThread = Thread(target=self.main, args=(self.mainThreadEvent,))
        mainThread.start()
        self.eventLoopThreadEvent = Event()
        eventLoopThread = Thread(target=self.eventloop, args=(self.eventLoopThreadEvent,))
        eventLoopThread.start()
        
    def __stop__(self):
        print "Stopping...please wait..."
        self.mainThreadEvent.set()

    def eventloop(self,eventLoopThreadEvent):
        sEventOld = ""
        while not self.eventLoopThreadEvent.isSet():
            # listen for event and print if it is new
            sEventNew = "<EVENT>" + "<" + str(eg.event.prefix) + "." + str(eg.event.suffix) + "><" + str(eg.event.payload) + ">"

            if not sEventNew == sEventOld:
                print sEventNew
                self.qSend.put(sEventNew)
                sEventOld = sEventNew

    # main loop
    def main(self,mainThreadEvent):
        self.client = None
        while not self.mainThreadEvent.isSet():
            inputready,outputready,errored = select.select(self.input,self.output,self.errored,1)
            
            # first check send que and send anything in the queue
            while not self.qSend.empty():
                tempConn = self.input[1:len(self.input)]
                qdata = self.qSend.get()
                for i in tempConn:
                    i.send(qdata + "\r")
                  
            for s in inputready:
                print "looping input ready"
                if s == self.theSock: 
                    self.client, address = self.theSock.accept()
                    print "Client added: ", self.client, address
                    if not self.input.count(self.client)>0:
                        self.input.append(self.client)
                else:
                    
                    # receive upto 4096 bytes of data
                    data = s.recv(4096)
                    
                    # if data is received do something with it
                    if data:
                        # get rid of any line terminators from the packet
                        data = self.StripNoPrint(data)
                        #print "Processing data: ", data
                        
                        # process <ACTION> packet received from the SYS server
                        if '<ACTION>' in data:
                            self.ProcessCmd_Action(data)

                        # process <EVENT> packet received from the SYS server
                        if '<EVENT>' in data:
                            self.ProcessCmd_Event(data)
                            
                        # echo back data to each connection
                        tempConn = self.input[1:len(self.input)]
                        for i in tempConn:
                            # echo the data back along with a CR
                            i.send(data + "\r")
                            
                    else:
                        if self.input.count(s) > 0:
                            self.input.remove(s)
                            print "Client removed: ", s
        self.theSock.close()
        print "Main thread has ended"

    # define Configure panel
    def Configure(self, host = '0.0.0.0', port = 1024):
        panel = eg.ConfigPanel(self)
        mySizer = wx.GridBagSizer(5, 5)

        hostControl = wx.TextCtrl(panel, -1, host)
        mySizer.Add(wx.StaticText(panel,-1,"Host ip or blank: "),(2,0))
        mySizer.Add(hostControl,(2,1))

        portControl = panel.SpinIntCtrl(port, 1024, 1024)
        portControl.SetInitialSize((60,-1))
        mySizer.Add(wx.StaticText(panel,-1,"Port number: "),(3,0))
        mySizer.Add(portControl,(3,1))
        panel.sizer.Add(mySizer, 0, flag = wx.EXPAND)

        while panel.Affirmed():
            panel.SetResult(hostControl.GetValue(),portControl.GetValue())
                     
    def StripNoPrint(self, str):
        results = ""
        # iterate through and remove all non-printable ascii characters
        for char in str:
            if ord(char) > 31 and ord(char) < 127:
                results += char
        return results

    def ProcessCmd_Action(self, sCommand):
        # replace the first occurance of the tag with nothing
        sCommand = sCommand.replace("<ACTION>", "", 1)
        print "Premise: processing action " + sCommand
        
        # this will invoke an action based on received python code from the SYS server
        eg.plugins.EventGhost.PythonCommand(sCommand)
        return ''

    def ProcessCmd_Event(self, sCommand):
        # replace the first occurance of the tag with nothing
        sCommand = sCommand.replace("<EVENT>", "", 1)
        
        # remove all "<" from array
        sCommand = sCommand.replace("<", "")
        
		# split the packet into a list
        sCommand = sCommand.split(">")
        
        # trigger event in EG, process payload if it's present
        if sCommand[1] == "":
            #print "Processing event: " + sCommand[0]
            self.TriggerEvent(sCommand[0])
        else:
            #print "Processing event and payload: " + sCommand[0] + "\'" + sCommand[1] + "\'"
            self.TriggerEvent(sCommand[0], payload = sCommand[1])
        return ''
        
    # send the event string and event payload (as a string) to the SYS server
    def SendCmd_Event(self, sEventString, sEventPaload):
        sCommand = "<EVENT>" + "<" + sEventString + "><" + sEventPaload + ">"
    
        # send EVENT packet to all connected SYS servers
        tempConn = self.input[1:len(self.input)]
        for i in tempConn:
            i.send(sCommand + "\r")
            print "Premise: sending event " + sCommand
        return ''

    # send a setvalue request to the SYS server in order to change some object's property    
    def SendCmd_SetValue(self, sObjPath, sPropName, sPropValue, bForceStateChange):
        sCommand = "<SETVAL>"
        if bForceStateChange:
            sCommand = "<SETVALFORCE>"
        sCommand = sCommand + "<" + sObjPath + "><" + sPropName + "><" + sPropValue + ">"
    
        # send SetValue packet to all connected SYS servers
        tempConn = self.input[1:len(self.input)]
        for i in tempConn:
            i.send(sCommand + "\r")
            print "Premise: sending setvalue " + sCommand
        return ''
 
            
# this action will invoke the SetValue and SetValueForce methods within SYS using the passed parameters. 
class SetValue(eg.ActionClass):	
    description = "Calls the SetValue method on the connected Premise server using the supplied parameters."
    
    def __call__(self, sObjPath, sPropName, sPropValue, bForceStateChange):
        self.plugin.SendCmd_SetValue(sObjPath, sPropName, sPropValue, bForceStateChange)
     
    # build the configuration panel    
    def Configure(self, sObjPath="Home.Living.Light", sPropName="PowerState", sPropValue="True", bForceStateChange=0 ):
        panel = eg.ConfigPanel()

        sObjPathControl = panel.TextCtrl(sObjPath)
        sPropNameControl = panel.TextCtrl(sPropName)
        sPropValueControl = panel.TextCtrl(sPropValue)
        bForceStateChangeControl = panel.CheckBox(bForceStateChange)
        
        panel.AddLine("Use options below to define the desired Premise server action.  Values are not case sensitive.")
        panel.AddLine("SYS Object Path: ", sObjPathControl)
        panel.AddLine("Property Name: ", sPropNameControl)
        panel.AddLine("Property Value: ", sPropValueControl)
        panel.AddLine("Force State Change: ", bForceStateChangeControl)
        
        while panel.Affirmed():
            panel.SetResult(sObjPathControl.GetValue(),sPropNameControl.GetValue(),sPropValueControl.GetValue(), bForceStateChangeControl.GetValue())
            
           
# this action will send the string and payload of an EventGhost event to the SYS server. 
class SendEvent(eg.ActionClass):
    description = "Sends to the connected Premise server the EventGhost event string and payload (as a string) that initiated the action."
    
    def __call__(self):
        sEventString = str(eg.event.string)
        sEventPayload = str(eg.event.payload)
        self.plugin.SendCmd_Event(sEventString, sEventPayload)

Re: How to send all EG events over tcp w/o creating a macro?

Posted: Fri Jan 11, 2013 7:21 am
by krambriw
Is it always acceptable to compare the time property of two events to know if they are the same event?
Have you tried? If it works I would say yes, this is a way of doing it.

Also, I would add a very short wait in all while loops to give back cpu. In the while loop I would add something like below (maybe 10 ms is not enough, you can try to increase if needed but the cpu load for EG should be zero most of the time)

Code: Select all

self.eventLoopThreadEvent.wait(0.01)

Re: How to send all EG events over tcp w/o creating a macro?

Posted: Fri Jan 11, 2013 2:23 pm
by etc6849
kambriw,

I haven't tried using the time property yet. I thought of it after I made my post, then went back and edited it ;)

Thanks for the pointer about CPU loading. This is my first Python script (and my first time using threads in any language) so any other pointers are appreciated.

PS: Is there an advantage over using the wait method of the event object and not the time.sleep method?
Is this the correct reasoning for using the .wait() method: eventloop gets passed the event object "eventLoopThreadEvent" and so eventloop can use event object to signal the thread it's running in to temporarily stop until the timeout period ends at which point the event flag will be set to true and the thread will start again?!?

"This is one of the simplest mechanisms for communication between threads: one thread signals an event and other threads wait for it. An event object manages an internal flag that can be set to true with the set() method and reset to false with the clear() method. The wait() method blocks until the flag is true.
...
wait([timeout])
Block until the internal flag is true. If the internal flag is true on entry, return immediately. Otherwise, block until another thread calls set() to set the flag to true, or until the optional timeout occurs. When the timeout argument is present and not None, it should be a floating point number specifying a timeout for the operation in seconds (or fractions thereof). This method returns the internal flag on exit, so it will always return True except if a timeout is given and the operation times out."

Re: How to send all EG events over tcp w/o creating a macro?

Posted: Fri Jan 11, 2013 3:06 pm
by krambriw
If you use time.sleep the whole EG will sleep, otherwise just the thread

Re: How to send all EG events over tcp w/o creating a macro?

Posted: Fri Jan 11, 2013 3:14 pm
by etc6849
Thanks, this is what I was wondering about.

Re: How to send all EG events over tcp w/o creating a macro?

Posted: Fri Jan 11, 2013 3:17 pm
by krambriw
Premise looks powerful. Interesting history...
Combing with EG will bring some interesting possibilities.

BestR

Re: How to send all EG events over tcp w/o creating a macro?

Posted: Fri Jan 11, 2013 4:12 pm
by etc6849
Thanks!

Definitely a love/love relationship with Premise and EventGhost. EG has a lot of neat plugin actions that Premise can now access by passing a python command line over tcp (along with triggering EG events via TCP), and Premise can easily do stuff that you would never want to write EG plugin for... For example look at this module: http://cocoontech.com/forums/files/file ... r-premise/

If you love EventGhost, you'll probably love Premise too. Premise's architecture is very very well thought out. You can change lighting systems completely, and never have to touch any home specific scripts you've written (e.g. timeouts, etc)! I'm not going to dis on Homeseer and other options such as CQC, but from what I've seen Premise is far more well thought out (and free). The help files in Premise Builder are very comprehensive too.

Best of all Premise has 1000's of predefined classes to make building new modules very easy (you can inherit,extend, contain existing classes into new ones) and a world class IDE that requires only knowledge of vbscript (however, C++ and vb.net sdk's are available)!

Details of what is to come:
The Premise side of the things is hopefully logically laid out (I'll post it once I get the documentation finished). I've defined new Premise classes for system objects (e.g. a computer's display) that send python commands based on property state changes of those system objects (e.g. a computer display has several properties like powerstate, screensaver, etc...), but I've also made a method to where you can send any generic python command you want too.

The idea behind the system objects is that a Premise end user can be shielded from the python code (they would simply initiate a property change for a system object), but can still send python commands in Premise scripts if needed. The module will be open-source too so it will be easy to define new system objects if desired.

Since Premise is class based, you can also have as many EG modules running in Premise as needed, one for each EG PC. However, a unique port will need to be selected for each instance. Also implemented on the Premise side are send/receive event objects to help make the module very useful for performing things in Premise when EG events happen on a PC or for triggering EG events on a PC.