def initiateCommunications(self, initializeConnect=True): # determine if this device is missing any properties that were added # during device/plugin upgrades propertiesDictUpdateRequired = False pluginPropsCopy = self.indigoDevice.pluginProps for newPropertyDefn in self.upgradedDeviceProperties: if not (newPropertyDefn[0] in pluginPropsCopy): self.hostPlugin.logger.info(u'Triggering property update due to missing device property: ' + RPFrameworkUtils.to_unicode(newPropertyDefn[0])) pluginPropsCopy[newPropertyDefn[0]] = newPropertyDefn[1] propertiesDictUpdateRequired = True if propertiesDictUpdateRequired == True: self.indigoDevice.replacePluginPropsOnServer(pluginPropsCopy) # determine if this device is missing any states that were defined in upgrades stateReloadRequired = False for newStateName in self.upgradedDeviceStates: if not (newStateName in self.indigoDevice.states): self.hostPlugin.logger.info(u'Triggering state reload due to missing device state: ' + RPFrameworkUtils.to_unicode(newStateName)) stateReloadRequired = True if stateReloadRequired == True: self.indigoDevice.stateListOrDisplayStateIdChanged(); # start concurrent processing thread by injecting a placeholder # command to the queue if initializeConnect == True: self.queueDeviceCommand(RPFrameworkCommand.RPFrameworkCommand(RPFrameworkCommand.CMD_INITIALIZE_CONNECTION))
def scheduleReconnectionAttempt(self): self.hostPlugin.logger.debug(u'Scheduling reconnection attempt...') try: self.failedConnectionAttempts = self.failedConnectionAttempts + 1 maxReconnectAttempts = int(self.hostPlugin.getGUIConfigValue(self.indigoDevice.deviceTypeId, RPFrameworkPlugin.GUI_CONFIG_RECONNECTIONATTEMPT_LIMIT, u'0')) if self.failedConnectionAttempts > maxReconnectAttempts: self.hostPlugin.logger.debug(u'Maximum reconnection attempts reached (or not allowed) for device ' + RPFrameworkUtils.to_unicode(self.indigoDevice.id)) else: reconnectAttemptDelay = int(self.hostPlugin.getGUIConfigValue(self.indigoDevice.deviceTypeId, RPFrameworkPlugin.GUI_CONFIG_RECONNECTIONATTEMPT_DELAY, u'60')) reconnectAttemptScheme = self.hostPlugin.getGUIConfigValue(self.indigoDevice.deviceTypeId, RPFrameworkPlugin.GUI_CONFIG_RECONNECTIONATTEMPT_SCHEME, RPFrameworkPlugin.GUI_CONFIG_RECONNECTIONATTEMPT_SCHEME_REGRESS) if reconnectAttemptScheme == RPFrameworkPlugin.GUI_CONFIG_RECONNECTIONATTEMPT_SCHEME_FIXED: reconnectSeconds = reconnectAttemptDelay else: reconnectSeconds = reconnectAttemptDelay * self.failedConnectionAttempts reconnectAttemptTime = time.time() + reconnectSeconds self.hostPlugin.pluginCommandQueue.put(RPFrameworkCommand.RPFrameworkCommand(RPFrameworkCommand.CMD_DEVICE_RECONNECT, commandPayload=(self.indigoDevice.id, self.deviceInstanceIdentifier, reconnectAttemptTime))) self.hostPlugin.logger.debug(u'Reconnection attempt scheduled for ' + RPFrameworkUtils.to_unicode(reconnectSeconds) + u' seconds') except e: self.hostPlugin.logger.error(u'Failed to schedule reconnection attempt to device')
def concurrentCommandProcessingThread(self, commandQueue): try: self.hostPlugin.logger.debug( u'Concurrent Processing Thread started for device ' + RPFrameworkUtils.to_unicode(self.indigoDevice.id)) # obtain the IP or host address that will be used in connecting to the # RESTful service via a function call to allow overrides deviceHTTPAddress = self.getRESTfulDeviceAddress() if deviceHTTPAddress is None: self.hostPlugin.logger.error( u'No IP address specified for device ' + RPFrameworkUtils.to_unicode(self.indigoDevice.id) + u'; ending command processing thread.') return # retrieve any configuration information that may have been setup in the # plugin configuration and/or device configuration updateStatusPollerPropertyName = self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_RESTFULSTATUSPOLL_INTERVALPROPERTY, u'updateInterval') updateStatusPollerInterval = int( self.indigoDevice.pluginProps.get( updateStatusPollerPropertyName, u'90')) updateStatusPollerNextRun = None updateStatusPollerActionId = self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_RESTFULSTATUSPOLL_ACTIONID, u'') emptyQueueReducedWaitCycles = int( self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_RESTFULDEV_EMPTYQUEUE_SPEEDUPCYCLES, u'80')) # spin up the database connection, if this plugin supports databases self.dbConn = self.hostPlugin.openDatabaseConnection( self.indigoDevice.deviceTypeId) # begin the infinite loop which will run as long as the queue contains commands # and we have not received an explicit shutdown request continueProcessingCommands = True lastQueuedCommandCompleted = 0 while continueProcessingCommands == True: # process pending commands now... while not commandQueue.empty(): lenQueue = commandQueue.qsize() self.hostPlugin.logger.threaddebug( u'Command queue has ' + RPFrameworkUtils.to_unicode(lenQueue) + u' command(s) waiting') # the command name will identify what action should be taken... we will handle the known # commands and dispatch out to the device implementation, if necessary, to handle unknown # commands command = commandQueue.get() if command.commandName == RPFrameworkCommand.CMD_INITIALIZE_CONNECTION: # specialized command to instanciate the concurrent thread # safely ignore this... just used to spin up the thread self.hostPlugin.logger.threaddebug( u'Create connection command de-queued') # if the device supports polling for status, it may be initiated here now; however, we should implement a pause to ensure that # devices are created properly (RESTFul devices may respond too fast since no connection need be established) statusUpdateStartupDelay = float( self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_RESTFULSTATUSPOLL_STARTUPDELAY, u'3')) if statusUpdateStartupDelay > 0.0: commandQueue.put( RPFrameworkCommand.RPFrameworkCommand( RPFrameworkCommand.CMD_PAUSE_PROCESSING, commandPayload=str( statusUpdateStartupDelay))) commandQueue.put( RPFrameworkCommand.RPFrameworkCommand( RPFrameworkCommand. CMD_UPDATE_DEVICE_STATUS_FULL, parentAction=updateStatusPollerActionId)) elif command.commandName == RPFrameworkCommand.CMD_TERMINATE_PROCESSING_THREAD: # a specialized command designed to stop the processing thread indigo # the event of a shutdown continueProcessingCommands = False elif command.commandName == RPFrameworkCommand.CMD_PAUSE_PROCESSING: # the amount of time to sleep should be a float found in the # payload of the command try: pauseTime = float(command.commandPayload) self.hostPlugin.logger.threaddebug( u'Initiating sleep of ' + RPFrameworkUtils.to_unicode(pauseTime) + u' seconds from command.') time.sleep(pauseTime) except: self.hostPlugin.logger.warning( u'Invalid pause time requested') elif command.commandName == RPFrameworkCommand.CMD_UPDATE_DEVICE_STATUS_FULL: # this command instructs the plugin to update the full status of the device (all statuses # that may be read from the device should be read) if updateStatusPollerActionId != u'': self.hostPlugin.logger.debug( u'Executing full status update request...') self.hostPlugin.executeAction( None, indigoActionId=updateStatusPollerActionId, indigoDeviceId=self.indigoDevice.id, paramValues=None) updateStatusPollerNextRun = time.time( ) + updateStatusPollerInterval else: self.hostPlugin.logger.threaddebug( u'Ignoring status update request, no action specified to update device status' ) elif command.commandName == RPFrameworkCommand.CMD_NETWORKING_WOL_REQUEST: # this is a request to send a Wake-On-LAN request to a network-enabled device # the command payload should be the MAC address of the device to wake up try: RPFrameworkNetworkingWOL.sendWakeOnLAN( command.commandPayload) except: self.hostPlugin.logger.error( u'Failed to send Wake-on-LAN packet') elif command.commandName == CMD_RESTFUL_GET or command.commandName == CMD_RESTFUL_PUT or command.commandName == CMD_DOWNLOADFILE or command.commandName == CMD_DOWNLOADIMAGE: try: self.hostPlugin.logger.debug( u'Processing GET operation: ' + RPFrameworkUtils.to_unicode( command.commandPayload)) # gather all of the parameters from the command payload # the payload should have the following format: # [0] => request method (http|https|etc.) # [1] => path for the GET operation # [2] => authentication type: none|basic|digest # [3] => username # [4] => password # # CMD_DOWNLOADFILE or CMD_DOWNLOADIMAGE # [5] => download filename/path # [6] => image resize width # [7] => image resize height # # CMD_RESTFUL_PUT # [5] => data to post as the body (if any, may be blank) commandPayloadList = command.getPayloadAsList() fullGetUrl = commandPayloadList[ 0] + u'://' + deviceHTTPAddress[ 0] + u':' + RPFrameworkUtils.to_unicode( deviceHTTPAddress[1] ) + commandPayloadList[1] customHeaders = {} self.addCustomHTTPHeaders(customHeaders) authenticationParam = None authenticationType = u'none' username = u'' password = u'' if len(commandPayloadList) >= 3: authenticationType = commandPayloadList[2] if len(commandPayloadList) >= 4: username = commandPayloadList[3] if len(commandPayloadList) >= 5: password = commandPayloadList[4] if authenticationType != 'none' and username != u'': self.hostPlugin.logger.threaddebug( u'Using login credentials... Username=> ' + username + u'; Password=>' + RPFrameworkUtils.to_unicode(len( password)) + u' characters long') if authenticationType.lower() == 'digest': self.hostPlugin.logger.threaddebug( u'Enabling digest authentication') authenticationParam = HTTPDigestAuth( username, password) else: authenticationParam = (username, password) # execute the URL fetching depending upon the method requested if command.commandName == CMD_RESTFUL_GET or command.commandName == CMD_DOWNLOADFILE or command.commandName == CMD_DOWNLOADIMAGE: responseObj = requests.get( fullGetUrl, auth=authenticationParam, headers=customHeaders, verify=False) elif command.commandName == CMD_RESTFUL_PUT: dataToPost = None if len(commandPayloadList) >= 6: dataToPost = commandPayloadList[5] responseObj = requests.post( fullGetUrl, auth=authenticationParam, headers=customHeaders, verify=False, data=dataToPost) # if the network command failed then allow the error processor to handle the issue if responseObj.status_code == 200: # the response handling will depend upon the type of command... binary returns must be # handled separately from (expected) text-based ones if command.commandName == CMD_DOWNLOADFILE or command.commandName == CMD_DOWNLOADIMAGE: # this is a binary return that should be saved to the file system without modification if len(commandPayloadList) >= 6: saveLocation = commandPayloadList[5] # execute the actual save from the binary response stream try: localFile = open( RPFrameworkUtils.to_str( saveLocation), "wb") localFile.write( responseObj.content) self.hostPlugin.logger.threaddebug( u'Command Response: [' + RPFrameworkUtils.to_unicode( responseObj.status_code) + u'] -=- binary data written to ' + RPFrameworkUtils.to_unicode( saveLocation) + u'-=-') if command.commandName == CMD_DOWNLOADIMAGE: imageResizeWidth = 0 imageResizeHeight = 0 if len(command.commandPayload ) >= 7: imageResizeWidth = int( command. commandPayload[6]) if len(command.commandPayload ) >= 8: imageResizeHeight = int( command. commandPayload[7]) resizeCommandLine = u'' if imageResizeWidth > 0 and imageResizeHeight > 0: # we have a specific size as a target... resizeCommandLine = u'sips -z ' + RPFrameworkUtils.to_unicode( imageResizeHeight ) + u' ' + RPFrameworkUtils.to_unicode( imageResizeWidth ) + u' ' + saveLocation elif imageResizeWidth > 0: # we have a maximum size measurement resizeCommandLine = u'sips -Z ' + RPFrameworkUtils.to_unicode( imageResizeWidth ) + u' ' + saveLocation # if a command line has been formed, fire that off now... if resizeCommandLine == u'': self.hostPlugin.logger.debug( u'No image size specified for ' + RPFrameworkUtils. to_unicode( saveLocation) + u'; skipping resize.') else: self.hostPlugin.logger.threaddebug( u'Executing resize via command line "' + resizeCommandLine + u'"') try: subprocess.Popen( resizeCommandLine, shell=True) self.hostPlugin.logger.debug( saveLocation + u' resized via sip shell command' ) except: self.hostPlugin.logger.error( u'Error resizing image via sips' ) # we have completed the download and processing successfully... allow the # device (or its descendants) to process successful operations self.notifySuccessfulDownload( command, saveLocation) finally: if not localFile is None: localFile.close() else: self.hostPlugin.logger.error( u'Unable to complete download action - no filename specified' ) else: # handle this return as a text-based return self.hostPlugin.logger.threaddebug( u'Command Response: [' + RPFrameworkUtils.to_unicode( responseObj.status_code) + u'] ' + RPFrameworkUtils.to_unicode( responseObj.text)) self.hostPlugin.logger.threaddebug( command.commandName + u' command completed; beginning response processing' ) self.handleDeviceTextResponse( responseObj, command) self.hostPlugin.logger.threaddebug( command.commandName + u' command response processing completed' ) elif responseObj.status_code == 401: self.handleRESTfulError( command, u'401 - Unauthorized', responseObj) else: self.handleRESTfulError( command, str(responseObj.status_code), responseObj) except Exception, e: # the response value really should not be defined here as it bailed without # catching any of our response error conditions self.handleRESTfulError(command, e, None) elif command.commandName == CMD_SOAP_REQUEST or command.commandName == CMD_JSON_REQUEST: responseObj = None try: # this is to post a SOAP request to a web service... this will be similar to a restful put request # but will contain a body payload self.hostPlugin.logger.threaddebug( u'Received SOAP/JSON command request: ' + command.commandPayload) soapPayloadParser = re.compile( "^\s*([^\n]+)\n\s*([^\n]+)\n(.*)$", re.DOTALL) soapPayloadData = soapPayloadParser.match( command.commandPayload) soapPath = soapPayloadData.group(1).strip() soapAction = soapPayloadData.group(2).strip() soapBody = soapPayloadData.group(3).strip() fullGetUrl = u'http://' + deviceHTTPAddress[ 0] + u':' + RPFrameworkUtils.to_str( deviceHTTPAddress[1] ) + RPFrameworkUtils.to_str(soapPath) self.hostPlugin.logger.debug( u'Processing SOAP/JSON operation to ' + fullGetUrl) customHeaders = {} self.addCustomHTTPHeaders(customHeaders) if command.commandName == CMD_SOAP_REQUEST: customHeaders[ "Content-type"] = "text/xml; charset=\"UTF-8\"" customHeaders[ "SOAPAction"] = RPFrameworkUtils.to_str( soapAction) else: customHeaders[ "Content-type"] = "application/json" # execute the URL post to the web service self.hostPlugin.logger.threaddebug( u'Sending SOAP/JSON request:\n' + RPFrameworkUtils.to_unicode(soapBody)) self.hostPlugin.logger.threaddebug( u'Using headers: \n' + RPFrameworkUtils.to_unicode(customHeaders)) responseObj = requests.post( fullGetUrl, headers=customHeaders, verify=False, data=RPFrameworkUtils.to_str(soapBody)) if responseObj.status_code == 200: # handle this return as a text-based return self.hostPlugin.logger.threaddebug( u'Command Response: [' + RPFrameworkUtils.to_unicode( responseObj.status_code) + u'] ' + RPFrameworkUtils.to_unicode( responseObj.text)) self.hostPlugin.logger.threaddebug( command.commandName + u' command completed; beginning response processing' ) self.handleDeviceTextResponse( responseObj, command) self.hostPlugin.logger.threaddebug( command.commandName + u' command response processing completed') else: self.hostPlugin.logger.threaddebug( u'Command Response was not HTTP OK, handling RESTful error' ) self.handleRESTfulError( command, str(responseObj.status_code), responseObj) except Exception, e: self.handleRESTfulError(command, e, responseObj) else:
if continueProcessingCommands == True: # if we have just completed a command recently, half the amount of # wait time, assuming that a subsequent command could be forthcoming if lastQueuedCommandCompleted > 0: time.sleep(self.emptyQueueProcessingThreadSleepTime / 2) lastQueuedCommandCompleted = lastQueuedCommandCompleted - 1 else: time.sleep(self.emptyQueueProcessingThreadSleepTime) # check to see if we need to issue an update... if updateStatusPollerNextRun is not None and time.time( ) > updateStatusPollerNextRun: commandQueue.put( RPFrameworkCommand.RPFrameworkCommand( RPFrameworkCommand.CMD_UPDATE_DEVICE_STATUS_FULL, parentAction=updateStatusPollerActionId)) # handle any exceptions that are thrown during execution of the plugin... note that this # should terminate the thread, but it may get spun back up again except SystemExit: pass except Exception: self.hostPlugin.logger.exception( u'Exception in background processing') except: self.hostPlugin.logger.exception( u'Exception in background processing') finally: self.hostPlugin.logger.debug(u'Command thread ending processing') self.hostPlugin.closeDatabaseConnection(self.dbConn)
def generateActionCommands(self, rpPlugin, rpDevice, paramValues): # validate that the values sent in are valid for this action validationResults = self.validateActionValues(paramValues) if validationResults[0] == False: rpPlugin.logger.error( u'Invalid values sent for action ' + RPFrameworkUtils.to_unicode(self.indigoActionId) + u'; the following errors were found:') rpPlugin.logger.error( RPFrameworkUtils.to_unicode(validationResults[2])) return # determine the list of parameter values based upon the parameter definitions # and the values provided (these will be used during substitutions below) resolvedValues = dict() for rpParam in self.indigoParams: resolvedValues[rpParam.indigoId] = paramValues.get( rpParam.indigoId, rpParam.defaultValue) # generate the command for each of the ones defined for this action commandsToQueue = [] for (commandName, commandFormatString, commandExecuteCount, repeatCommandDelay, executeCondition) in self.actionCommands: # this command may have an execute condition which could prevent the command # from firing... if executeCondition != None and executeCondition != u'': # this should eval to a boolean value if eval( rpPlugin.substituteIndigoValues( executeCondition, rpDevice, resolvedValues)) == False: rpPlugin.logger.threaddebug( u'Execute condition failed, skipping execution for command: ' + commandName) continue # determine the number of times to execute this command (supports sending the same request # multiple times in a row) executeTimesStr = rpPlugin.substituteIndigoValues( commandExecuteCount, rpDevice, resolvedValues) if executeTimesStr.startswith(u'eval:'): executeTimesStr = eval(executeTimesStr.replace(u'eval:', u'')) if executeTimesStr == None or executeTimesStr == u'': executeTimesStr = u'1' executeTimes = int(executeTimesStr) # create a new command for each of the count requested... for i in range(0, executeTimes): # create the payload based upon the format string provided for the command payload = rpPlugin.substituteIndigoValues( commandFormatString, rpDevice, resolvedValues) if payload.startswith(u'eval:'): payload = eval(payload.replace(u'eval:', u'')) # determine the delay that should be added after the command (delay between repeats) delayTimeStr = rpPlugin.substituteIndigoValues( repeatCommandDelay, rpDevice, resolvedValues) delayTime = 0.0 if executeTimes > 1 and delayTimeStr != u'': delayTime = float(delayTimeStr) # create and add the command to the queue commandsToQueue.append( RPFrameworkCommand.RPFrameworkCommand( commandName, commandPayload=payload, postCommandPause=delayTime, parentAction=self)) # if the execution made it here then the list of commands has been successfully built without # error and may be queued up on the device for commandForDevice in commandsToQueue: rpDevice.queueDeviceCommand(commandForDevice)
def executeEffects(self, responseObj, rpCommand, rpDevice, rpPlugin): for effect in self.matchResultEffects: # first we need to determine if this effect should be executed (based upon a condition; by default all # effects will be executed!) if effect.updateExecCondition != None and effect.updateExecCondition != u'': # this should eval to a boolean value if eval( rpPlugin.substituteIndigoValues( effect.updateExecCondition, rpDevice, dict())) == False: rpPlugin.logger.threaddebug( u'Execute condition failed for response, skipping execution for effect: ' + effect.effectType) continue # processing for this effect is dependent upon the type try: if effect.effectType == RESPONSE_EFFECT_UPDATESTATE: # this effect should update a device state (param) with a value as formated newStateValueString = self.substituteCriteriaFormatString( effect.updateValueFormatString, responseObj, rpCommand, rpDevice, rpPlugin) if effect.evalUpdateValue == True: newStateValue = eval(newStateValueString) else: newStateValue = newStateValueString # the effect may have a UI value set... if not leave at an empty string so that # we don't attempt to update it newStateUIValue = u'' if effect.updateValueFormatExString != u"": newStateUIValueString = self.substituteCriteriaFormatString( effect.updateValueFormatExString, responseObj, rpCommand, rpDevice, rpPlugin) if effect.evalUpdateValue == True: newStateUIValue = eval(newStateUIValueString) else: newStateUIValue = newStateUIValueString # update the state... if newStateUIValue == u'': rpPlugin.logger.debug( u'Effect execution: Update state "' + effect.updateParam + u'" to "' + RPFrameworkUtils.to_unicode(newStateValue) + u'"') rpDevice.indigoDevice.updateStateOnServer( key=effect.updateParam, value=newStateValue) else: rpPlugin.logger.debug( u'Effect execution: Update state "' + effect.updateParam + '" to "' + RPFrameworkUtils.to_unicode(newStateValue) + u'" with UIValue "' + RPFrameworkUtils.to_unicode(newStateUIValue) + u'"') rpDevice.indigoDevice.updateStateOnServer( key=effect.updateParam, value=newStateValue, uiValue=newStateUIValue) elif effect.effectType == RESPONSE_EFFECT_QUEUECOMMAND: # this effect will enqueue a new command... the updateParam will define the command name # and the updateValueFormat will define the new payload queueCommandName = self.substituteCriteriaFormatString( effect.updateParam, responseObj, rpCommand, rpDevice, rpPlugin) queueCommandPayloadStr = self.substituteCriteriaFormatString( effect.updateValueFormatString, responseObj, rpCommand, rpDevice, rpPlugin) if effect.evalUpdateValue == True: queueCommandPayload = eval(queueCommandPayloadStr) else: queueCommandPayload = queueCommandPayloadStr rpPlugin.logger.debug( u'Effect execution: Queuing command {' + queueCommandName + u'}') rpDevice.queueDeviceCommand( RPFrameworkCommand.RPFrameworkCommand( queueCommandName, queueCommandPayload)) elif effect.effectType == RESPONSE_EFFECT_CALLBACK: # this should kick off a callback to a python call on the device... rpPlugin.logger.debug( u'Effect execution: Calling function ' + effect.updateParam) eval(u'rpDevice.' + effect.updateParam + u'(responseObj, rpCommand)') except: rpPlugin.logger.exception( u'Error executing effect for device id ' + RPFrameworkUtils.to_unicode(rpDevice.indigoDevice.id))
def concurrentCommandProcessingThread(self, commandQueue): try: # retrieve the keys and settings that will be used during the command processing # for this telnet device isConnectedStateKey = self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_ISCONNECTEDSTATEKEY, u'') connectionStateKey = self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_CONNECTIONSTATEKEY, u'') self.hostPlugin.logger.threaddebug( u'Read device state config... isConnected: "' + RPFrameworkUtils.to_unicode(isConnectedStateKey) + u'"; connectionState: "' + RPFrameworkUtils.to_unicode(connectionStateKey) + u'"') telnetConnectionInfo = self.getDeviceAddressInfo() # establish the telenet connection to the telnet-based which handles the primary # network remote operations self.hostPlugin.logger.debug( u'Establishing connection to ' + RPFrameworkUtils.to_unicode(telnetConnectionInfo[0])) ipConnection = self.establishDeviceConnection(telnetConnectionInfo) self.failedConnectionAttempts = 0 self.hostPlugin.logger.debug(u'Connection established') # update the states on the server to show that we have established a connectionStateKey self.indigoDevice.setErrorStateOnServer(None) if isConnectedStateKey != u'': self.indigoDevice.updateStateOnServer(key=isConnectedStateKey, value=u'true') if connectionStateKey != u'': self.indigoDevice.updateStateOnServer(key=connectionStateKey, value=u'Connected') # retrieve any configuration information that may have been setup in the # plugin configuration and/or device configuration lineEndingToken = self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_EOL, u'\r') lineEncoding = self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_SENDENCODING, u'ascii') commandResponseTimeout = float( self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_COMMANDREADTIMEOUT, u'0.5')) telnetConnectionRequiresLoginDP = self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_REQUIRES_LOGIN_DP, u'') telnetConnectionRequiresLogin = (RPFrameworkUtils.to_unicode( self.indigoDevice.pluginProps.get( telnetConnectionRequiresLoginDP, u'False')).lower() == u'true') updateStatusPollerPropertyName = self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_STATUSPOLL_INTERVALPROPERTY, u'updateInterval') updateStatusPollerInterval = int( self.indigoDevice.pluginProps.get( updateStatusPollerPropertyName, u'90')) updateStatusPollerNextRun = None updateStatusPollerActionId = self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_STATUSPOLL_ACTIONID, u'') emptyQueueReducedWaitCycles = int( self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_TELNETDEV_EMPTYQUEUE_SPEEDUPCYCLES, u'200')) # begin the infinite loop which will run as long as the queue contains commands # and we have not received an explicit shutdown request continueProcessingCommands = True lastQueuedCommandCompleted = 0 while continueProcessingCommands == True: # process pending commands now... while not commandQueue.empty(): lenQueue = commandQueue.qsize() self.hostPlugin.logger.threaddebug( u'Command queue has ' + RPFrameworkUtils.to_unicode(lenQueue) + u' command(s) waiting') # the command name will identify what action should be taken... we will handle the known # commands and dispatch out to the device implementation, if necessary, to handle unknown # commands command = commandQueue.get() if command.commandName == RPFrameworkCommand.CMD_INITIALIZE_CONNECTION: # specialized command to instanciate the thread/telnet connection # safely ignore this... just used to spin up the thread self.hostPlugin.logger.threaddebug( u'Create connection command de-queued') # if the device supports polling for status, it may be initiated here now that # the connection has been established; no additional command will come through if telnetConnectionRequiresLogin == False: commandQueue.put( RPFrameworkCommand.RPFrameworkCommand( RPFrameworkCommand. CMD_UPDATE_DEVICE_STATUS_FULL, parentAction=updateStatusPollerActionId)) elif command.commandName == RPFrameworkCommand.CMD_TERMINATE_PROCESSING_THREAD: # a specialized command designed to stop the processing thread indigo # the event of a shutdown continueProcessingCommands = False elif command.commandName == RPFrameworkCommand.CMD_PAUSE_PROCESSING: # the amount of time to sleep should be a float found in the # payload of the command try: pauseTime = float(command.commandPayload) self.hostPlugin.logger.threaddebug( u'Initiating sleep of ' + RPFrameworkUtils.to_unicode(pauseTime) + u' seconds from command.') time.sleep(pauseTime) except: self.hostPlugin.logger.error( u'Invalid pause time requested') elif command.commandName == RPFrameworkCommand.CMD_UPDATE_DEVICE_STATUS_FULL: # this command instructs the plugin to update the full status of the device (all statuses # that may be read from the device should be read) if updateStatusPollerActionId != u'': self.hostPlugin.logger.debug( u'Executing full status update request...') self.hostPlugin.executeAction( None, indigoActionId=updateStatusPollerActionId, indigoDeviceId=self.indigoDevice.id, paramValues=None) if updateStatusPollerInterval > 0: updateStatusPollerNextRun = time.time( ) + updateStatusPollerInterval else: self.hostPlugin.logger.threaddebug( u'Ignoring status update request, no action specified to update device status' ) elif command.commandName == RPFrameworkCommand.CMD_UPDATE_DEVICE_STATE: # this command is to update a device state with the payload (which may be an # eval command) newStateInfo = re.match( '^\{ds\:([a-zA-Z\d]+)\}\{(.+)\}$', command.commandPayload, re.I) if newStateInfo is None: self.hostPlugin.logger.error( u'Invalid new device state specified') else: # the new device state may include an eval statement... updateStateName = newStateInfo.group(1) updateStateValue = newStateInfo.group(2) if updateStateValue.startswith(u'eval'): updateStateValue = eval( updateStateValue.replace(u'eval:', u'')) self.hostPlugin.logger.debug( u'Updating state "' + RPFrameworkUtils.to_unicode(updateStateName) + u'" to: ' + RPFrameworkUtils.to_unicode(updateStateValue)) self.indigoDevice.updateStateOnServer( key=updateStateName, value=updateStateValue) elif command.commandName == CMD_WRITE_TO_DEVICE: # this command initiates a write of data to the device self.hostPlugin.logger.debug(u'Sending command: ' + command.commandPayload) writeCommand = command.commandPayload + lineEndingToken ipConnection.write(writeCommand.encode(lineEncoding)) self.hostPlugin.logger.threaddebug( u'Write command completed.') else: # this is an unknown command; dispatch it to another routine which is # able to handle the commands (to be overridden for individual devices) self.handleUnmanagedCommandInQueue( ipConnection, command) # determine if any response has been received from the telnet device... responseText = RPFrameworkUtils.to_unicode( self.readLine(ipConnection, lineEndingToken, commandResponseTimeout)) if responseText != u'': self.hostPlugin.logger.threaddebug("Received: " + responseText) self.handleDeviceResponse( responseText.replace(lineEndingToken, u''), command) # if the command has a pause defined for after it is completed then we # should execute that pause now if command.postCommandPause > 0.0 and continueProcessingCommands == True: self.hostPlugin.logger.threaddebug( u'Post Command Pause: ' + RPFrameworkUtils.to_unicode( command.postCommandPause)) time.sleep(command.postCommandPause) # complete the dequeuing of the command, allowing the next # command in queue to rise to the top commandQueue.task_done() lastQueuedCommandCompleted = emptyQueueReducedWaitCycles # continue with empty-queue processing unless the connection is shutting down... if continueProcessingCommands == True: # check for any pending data coming IN from the telnet connection; note this is after the # command queue has been emptied so it may be un-prompted incoming data responseText = RPFrameworkUtils.to_unicode( self.readIfAvailable(ipConnection, lineEndingToken, commandResponseTimeout)) if responseText != u'': self.hostPlugin.logger.threaddebug( u'Received w/o Command: ' + responseText) self.handleDeviceResponse( responseText.replace(lineEndingToken, u''), None) # when the queue is empty, pause a bit on each iteration if lastQueuedCommandCompleted > 0: time.sleep(self.emptyQueueProcessingThreadSleepTime / 2) lastQueuedCommandCompleted = lastQueuedCommandCompleted - 1 else: time.sleep(self.emptyQueueProcessingThreadSleepTime) # check to see if we need to issue an update... if updateStatusPollerNextRun is not None and time.time( ) > updateStatusPollerNextRun: commandQueue.put( RPFrameworkCommand.RPFrameworkCommand( RPFrameworkCommand. CMD_UPDATE_DEVICE_STATUS_FULL, parentAction=updateStatusPollerActionId)) # handle any exceptions that are thrown during execution of the plugin... note that this # should terminate the thread, but it may get spun back up again except SystemExit: # the system is shutting down communications... we can kill access now by allowing # the thread to expire pass except (socket.timeout, EOFError): # this is a standard timeout/disconnect if self.failedConnectionAttempts == 0 or self.hostPlugin.debug == True: self.hostPlugin.logger.error( u'Connection timed out for device ' + RPFrameworkUtils.to_unicode(self.indigoDevice.id)) if connectionStateKey != u'': self.indigoDevice.updateStateOnServer(key=connectionStateKey, value=u'Unavailable') connectionStateKey = u'' # prevents the finally from re-updating to disconnected # this really is an error from the user's perspective, so set that state now self.indigoDevice.setErrorStateOnServer(u'Connection Error') # check to see if we should attempt a reconnect self.scheduleReconnectionAttempt() except socket.error, e: # this is a standard socket error, such as a reset... we can attempt to recover from this with # a scheduled reconnect if self.failedConnectionAttempts == 0 or self.hostPlugin.debug == True: self.hostPlugin.logg.error( u'Connection failed for device ' + RPFrameworkUtils.to_unicode(self.indigoDevice.id) + u': ' + RPFrameworkUtils.to_unicode(e)) if connectionStateKey != u'': self.indigoDevice.updateStateOnServer(key=connectionStateKey, value=u'Unavailable') connectionStateKey = u'' # prevents the finally from re-updating to disconnected # this really is an error from the user's perspective, so set that state now self.indigoDevice.setErrorStateOnServer(u'Connection Error') # check to see if we should attempt a reconnect self.scheduleReconnectionAttempt()