Please check my first tcp plugin for any conceptual errors

Do you have questions about writing plugins or scripts in Python? Meet the coders here.
Post Reply
etc6849
Posts: 21
Joined: Fri Dec 28, 2012 5:12 pm

Please check my first tcp plugin for any conceptual errors

Post by etc6849 » Sun Jan 13, 2013 9:38 pm

The code below works great and I've been through it several times now (and fixed many things I initially had wrong), but this is my first attempt at Python and EventGhost (and I'm self taught and not a professional programmer).

The plugin acts as a TCP server, listening for a client. The client is my home automation system (Premise Home Control Server, aka SYS). I've implemented a special protocol for each to talk to one another.

There's also a module that goes into my home automation system that I'm still finalizing it before release. The Premise module and documentation will get posted for all to use, and Premise is free too. Any help in finding any errors is appreciated!

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 Control Server acting as a client. Download Premise for free at http://www.cocoontech.com/wiki/Premise",
)

import eg
import select
import Queue
import socket
import threading

# define the plugin that will send/receive actions and also send events to a connected SYS server that is acting as a client
# the eventghost PC will act as the server, using the port defined in the config panel
# note 1: SYS server refers to the Premise home automation server.
# code uses the following prefixes for variants (for ease of read):
# o = object (unless it's a function), ol = object list, s = string var, i = integer var, b = boolean, f = floating pt var, t = tuple
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: initialized." 
    
    def __start__(self, sPort):
        print "Premise plugin: started."
        self.oSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
        self.iPort = int(sPort)
        self.sData = ""
        self.bError = False
        try:
            # bind the socket object to the address
            # use socket.gethostname method since we want socket to be seen by outside world (e.g. Premise Home Control)
            self.oSocket.bind((socket.gethostname(), self.iPort))
        except:
            self.bError = True
            print "Premise plugin: SOCKET ERROR! Check that port " + str(self.iPort) + " is accessible."
        # if we don't get a socket error, proceed
        if not self.bError:
            # listen for connection from SYS server (where SYS server is acting as client)
            self.oSocket.listen(5)
            print "Premise plugin is bound to port " + str(self.iPort) + "."
            self.oSocketInput = [self.oSocket]
            self.oSocketOutput = []
            
            # create a thread event for each thread  
            self.oMainThreadEvent = threading.Event()
            self.oEventHandlerThreadEvent = threading.Event()
            
            # define each thread
            oMainThread = threading.Thread(target=self.Main, args=(self.oMainThreadEvent,))
            oEventHandler = threading.Thread(target=self.EventHandler, args=(self.oEventHandlerThreadEvent,))
            
            # start each thread
            oMainThread.start()
            oEventHandler.start()

    def __stop__(self):
        print "Premise plugin: stopped."
        print "Premise plugin: attempting to stop event and main threads..."
        try:
            self.oMainThreadEvent.set()
        except:
            print "Premise plugin: main thread was never started."
            
        try:
            self.oEventHandlerThreadEvent.set()
        except:
            print "Premise plugin: event thread was never started."       
            
    # define a second thread for event monitoring
    def EventHandler(self,oEventHandlerThreadEvent):
        print "Premise plugin: event thread started."
        # initialize last event time to something impossible
        fLastEventTime = -1
        # while thread is set, continuously monitor for new events
        # if a new event occurs add it to qSend queue
        while not self.oEventHandlerThreadEvent.isSet():
            # listen for event and print if it is new
            # get floating point event time
			self.oEventHandlerThreadEvent.wait(0.01)
			fEventTime = eg.event.time
			sEvent = "<EVENT>" + "<" + str(eg.event.prefix) + "." + str(eg.event.suffix) + "><" + str(eg.event.payload) + ">"
			
			# do nothing if floating point event time is equal, e.g. must be same event
			if not fEventTime == fLastEventTime:
				self.qSend.put(sEvent)
				fLastEventTime = fEventTime
        print "Premise plugin: event thread has ended."

    # main loop
    def Main(self,oMainThreadEvent):
        print "Premise plugin: started main thread."
        # set client ready object to none
        self.oClientReady = None
        while not self.oMainThreadEvent.isSet():
            olClients_InputReady,olClients_OutputReady, olSockets_InError = select.select(self.oSocketInput,self.oSocketOutput,[],1)
            
            # first check send que and send anything in the queue
            # get data from queue even if no client connection exists
            while not self.qSend.empty():
                # get string data from que
                sData = self.qSend.get()
                
                # send data out to all client socket connections
                self.SendDataToAllClients(sData)
                  
            for oClientReady in olClients_InputReady:
                if oClientReady == self.oSocket:
                    # 
                    self.oClientReady, tAddress = self.oSocket.accept()
                    print "Premise plugin: client added: ", self.oClientReady, tAddress
                    
                    # trigger an event so SYS knows it's connected to EventGhost
                    # event will automatically be sent by the event handler thread
                    self.TriggerEvent("Connected")
                    
                    # if client connection is not already in the input list, add it
                    if not self.oSocketInput.count(self.oClientReady)>0:
                        self.oSocketInput.append(self.oClientReady)
                else:
                    
                    # receive upto 4096 bytes of data from the client connection that's ready
                    sData = oClientReady.recv(4096)
                    
                    # if data is received do something with it
                    if sData:
                        # get rid of any line terminators from the packet
                        sData = self.StripNoPrint(sData)
                        
                        # process <ACTION> packet received from the SYS server
                        if "<ACTION>" in sData:
                            self.ProcessCmd_Action(sData)

                        # process <EVENT> packet received from the SYS server
                        if "<EVENT>" in sData:
                            self.ProcessCmd_Event(sData)
                        
                        # echo back data to all connected client socket connections
                        self.SendDataToAllClients(sData)
                            
                    else:
                        if self.oSocketInput.count(oClientReady) > 0:
                            self.oSocketInput.remove(oClientReady)
                            print "Client removed: ", oClientReady
        
        # if there's any client connections, remove them
        for oClientReady in olClients_InputReady:
            if self.oSocketInput.count(oClientReady) > 0:
                self.oSocketInput.remove(oClientReady)
                print "Client removed: ", oClientReady
        
        # now re-initialize socketinput in case user edits Configuration without disable/re-enable
        self.oSocketInput =  None
        
        # close socket
        self.oSocket.close()
        
        print "Main thread has ended."
        
    # define Configure panel    
    def Configure(self, sPort="1024"):
        oPanel = eg.ConfigPanel()
        sPortControl = oPanel.TextCtrl(sPort)
        oPanel.AddLine("EventGhost will serve the port below over the network so that Premise can connect as a client.")
        oPanel.AddLine("Port: ", sPortControl)
        while oPanel.Affirmed():
            oPanel.SetResult(sPortControl.GetValue())
                     
    def StripNoPrint(self, str):
        sResults = ""
        # iterate through and remove all non-printable ascii characters
        for char in str:
            if ord(char) > 31 and ord(char) < 127:
                sResults += char
        return sResults
        
    # method to handle sending data out.  sole entry point for accessing send() on client socket.
    def SendDataToAllClients(self, sCommand):
        try:
            olClientConnections = self.oSocketInput[1:len(self.oSocketInput)]
            for oClientConnection in olClientConnections:
                oClientConnection.send(sCommand + "\r")
                print "Premise: sending " + sCommand    
        except:
            print "Premise plugin: socket is not valid or in error.  Could not send: " + sCommand
        return ""
        
    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] == "":
            self.TriggerEvent(sCommand[0])
        else:
            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
        self.SendDataToAllClients(sCommand)

    # 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
        self.SendDataToAllClients(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)
FREE Premise home automation software: http://www.cocoontech.com/wiki/Premise
Download: http://www.mediapcforums.com/node/7
Open-source VRC0P (Z-Wave) driver:
https://code.google.com/p/zwave-driver- ... %2FViziaRF

etc6849
Posts: 21
Joined: Fri Dec 28, 2012 5:12 pm

Re: Please check my first tcp plugin for any conceptual erro

Post by etc6849 » Mon Jan 14, 2013 4:02 am

Ok, I found another bug, but I'm not sure if it's my fault (for once)!?!

If you run the plugin and examine the event log (also add a print statement as shown below), you'll see that ~1/5 events are dropped! This goes away if I comment out "self.oEventHandlerThreadEvent.wait(0.01)," but this is bad practice as it sets takes 25% of my quad core i5 processor!

Is there a another way to capture ALL events?

I did find that using my SendEvent action from the plugin inside a macro with an event with string *, works 100% of the time.

Code: Select all

# define a second thread for event monitoring
    def EventHandler(self,oEventHandlerThreadEvent):
        print "Premise plugin: event thread started."
        # initialize last event time to something impossible
        fLastEventTime = -1
        # while thread is set, continuously monitor for new events
        # if a new event occurs add it to qSend queue
        while not self.oEventHandlerThreadEvent.isSet():
            # listen for event and print if it is new
            # get floating point event time
         self.oEventHandlerThreadEvent.wait(0.01)
         fEventTime = eg.event.time
         sEvent = "<EVENT>" + "<" + str(eg.event.prefix) + "." + str(eg.event.suffix) + "><" + str(eg.event.payload) + ">"
         
         # do nothing if floating point event time is equal, e.g. must be same event
         if not fEventTime == fLastEventTime:
            print sEvent
            self.qSend.put(sEvent)
            fLastEventTime = fEventTime
        print "Premise plugin: event thread has ended."
FREE Premise home automation software: http://www.cocoontech.com/wiki/Premise
Download: http://www.mediapcforums.com/node/7
Open-source VRC0P (Z-Wave) driver:
https://code.google.com/p/zwave-driver- ... %2FViziaRF

krambriw
Plugin Developer
Posts: 2570
Joined: Sat Jun 30, 2007 2:51 pm
Location: Stockholm, Sweden
Contact:

Re: Please check my first tcp plugin for any conceptual erro

Post by krambriw » Mon Jan 14, 2013 7:04 am

First of all, I would recommend that you clean up your code according to the guidelines found here:
http://www.eventghost.net/docs/codingstyle.htm

In general, line spacing, line length, indentations

It's good that have included a guid. Did you generate it yourself?

BestR Walter

etc6849
Posts: 21
Joined: Fri Dec 28, 2012 5:12 pm

Re: Please check my first tcp plugin for any conceptual erro

Post by etc6849 » Tue Jan 15, 2013 4:51 am

It's unique, I used an online tool. Premise also generates GUID's automatically when you copy/paste objects. I guess like Premise duplicate GUIDs would be an easy way to crash EventGhost...
FREE Premise home automation software: http://www.cocoontech.com/wiki/Premise
Download: http://www.mediapcforums.com/node/7
Open-source VRC0P (Z-Wave) driver:
https://code.google.com/p/zwave-driver- ... %2FViziaRF

Post Reply