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.logDebugMessage( u"Triggering property update due to missing device property: " + RPFrameworkUtils.to_unicode(newPropertyDefn[0]), RPFrameworkPlugin.DEBUGLEVEL_LOW, ) 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.logDebugMessage( u"Triggering state reload due to missing device state: " + RPFrameworkUtils.to_unicode(newStateName), RPFrameworkPlugin.DEBUGLEVEL_LOW, ) 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 terminateCommunications(self): self.hostPlugin.logger.debug(u'Initiating shutdown of communications with ' + RPFrameworkUtils.to_unicode(self.indigoDevice.name)) if not (self.concurrentThread is None) and self.concurrentThread.isAlive() == True: self.concurrentThread.terminateThread() self.concurrentThread.join() self.concurrentThread = None self.hostPlugin.logger.debug(u'Shutdown of communications with ' + RPFrameworkUtils.to_unicode(self.indigoDevice.name) + u' complete')
def readIfAvailable(self, connection, lineEndingToken, commandResponseTimeout): if self.connectionType == CONNECTIONTYPE_TELNET: return RPFrameworkUtils.to_unicode(connection.read_eager()) elif connection.inWaiting() > 0: return RPFrameworkUtils.to_unicode(self.readLine(connection, lineEndingToken, commandResponseTimeout)) else: return u''
def __init__(self, response): r = httplib.HTTPResponse(self._FakeSocket(response)) r.begin() self.location = u'' self.usn = u'' self.st = u'' self.server = u'' self.cache = u'' if r.getheader("location") is not None: self.location = RPFrameworkUtils.to_unicode(r.getheader("location")) if r.getheader("usn") is not None: self.usn = RPFrameworkUtils.to_unicode(r.getheader("usn")) if r.getheader("st") is not None: self.st = RPFrameworkUtils.to_unicode(r.getheader("st")) if r.getheader("server") is not None: self.server = RPFrameworkUtils.to_unicode(r.getheader("server")) if r.getheader("cache-control") is not None: try: cacheControlHeader = RPFrameworkUtils.to_unicode(r.getheader("cache-control")) cacheControlHeader = cacheControlHeader.split(u'=')[1] self.cache = cacheControlHeader except: pass self.allHeaders = r.getheaders()
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: indigo.server.log(u'Invalid values sent for action ' + RPFrameworkUtils.to_unicode(self.indigoActionId) + u'; the following errors were found:') indigo.server.log(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.logDebugMessage(u'Execute condition failed, skipping execution for command: ' + commandName, RPFrameworkPlugin.DEBUGLEVEL_HIGH) 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 terminateCommunications(self): self.hostPlugin.logDebugMessage( u"Initiating shutdown of communications with " + RPFrameworkUtils.to_unicode(self.indigoDevice.name), RPFrameworkPlugin.DEBUGLEVEL_LOW, ) if not (self.concurrentThread is None) and self.concurrentThread.isAlive() == True: self.concurrentThread.terminate() self.concurrentThread.join() self.concurrentThread = None self.hostPlugin.logDebugMessage( u"Shutdown of communications with " + RPFrameworkUtils.to_unicode(self.indigoDevice.name) + u" complete", RPFrameworkPlugin.DEBUGLEVEL_LOW, )
def removeChildDevice(self, device): self.hostPlugin.logDebugMessage( u"Removing child device " + RPFrameworkUtils.to_unicode(device.indigoDevice.id) + u" from " + RPFrameworkUtils.to_unicode(self.indigoDevice.id), RPFrameworkPlugin.DEBUGLEVEL_MED, ) # the key into the dictionary will be specified by the GUI configuration variable childDeviceKey = self.getChildDeviceKeyByDevice(device) # remove the device... del self.childDevices[childDeviceKey]
def addChildDevice(self, device): self.hostPlugin.logDebugMessage( u"Adding child device " + RPFrameworkUtils.to_unicode(device.indigoDevice.id) + u" to " + RPFrameworkUtils.to_unicode(self.indigoDevice.id), RPFrameworkPlugin.DEBUGLEVEL_MED, ) # the key into the dictionary will be specified by the GUI configuration variable childDeviceKey = self.getChildDeviceKeyByDevice(device) self.hostPlugin.logDebugMessage(u"Created device key: " + childDeviceKey, RPFrameworkPlugin.DEBUGLEVEL_HIGH) # add the device to the list of those managed by this device... self.childDevices[childDeviceKey] = device
def getChildDeviceKeyByDevice(self, device): # the key into the dictionary will be specified by the GUI configuration variable # of THIS (parent) device... by default it will just be the child device's ID childDeviceKey = self.hostPlugin.substituteIndigoValues(self.hostPlugin.getGUIConfigValue(self.indigoDevice.deviceTypeId, RPFrameworkPlugin.GUI_CONFIG_CHILDDICTIONARYKEYFORMAT, u''), device, None) if childDeviceKey == u'': childDeviceKey = RPFrameworkUtils.to_unicode(device.indigoDevice.id) return childDeviceKey
def scheduleReconnectionAttempt(self): self.hostPlugin.logDebugMessage(u"Scheduling reconnection attempt...", RPFrameworkPlugin.DEBUGLEVEL_MED) 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.logDebugMessage( u"Maximum reconnection attempts reached (or not allowed) for device " + RPFrameworkUtils.to_unicode(self.indigoDevice.id), RPFrameworkPlugin.DEBUGLEVEL_LOW, ) 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.logDebugMessage( u"Reconnection attempt scheduled for " + RPFrameworkUtils.to_unicode(reconnectSeconds) + u" seconds", RPFrameworkPlugin.DEBUGLEVEL_MED, ) except e: self.hostPlugin.logErrorMessage(u"Failed to schedule reconnection attempt to device")
def handleDeviceTextResponse(self, responseObj, rpCommand): # loop through the list of response definitions defined in the (base) class # and determine if any match responseText = responseObj.text for rpResponse in self.hostPlugin.getDeviceResponseDefinitions(self.indigoDevice.deviceTypeId): if rpResponse.isResponseMatch(responseText, rpCommand, self, self.hostPlugin): self.hostPlugin.logDebugMessage(u'Found response match: ' + RPFrameworkUtils.to_unicode(rpResponse.responseId), RPFrameworkPlugin.DEBUGLEVEL_MED) rpResponse.executeEffects(responseText, rpCommand, self, self.hostPlugin)
def removeChildDevice(self, device): self.hostPlugin.logger.threaddebug(u'Removing child device ' + RPFrameworkUtils.to_unicode(device.indigoDevice.id) + u' from ' + RPFrameworkUtils.to_unicode(self.indigoDevice.id)) # the key into the dictionary will be specified by the GUI configuration variable childDeviceKey = self.getChildDeviceKeyByDevice(device) # remove the device... del self.childDevices[childDeviceKey]
def handleRESTfulError(self, rpCommand, err, response=None): if rpCommand.commandName == CMD_RESTFUL_PUT or rpCommand.commandName == CMD_RESTFUL_GET: self.hostPlugin.logger.error(u'An error occurred executing the GET/PUT request (Device: ' + RPFrameworkUtils.to_unicode(self.indigoDevice.id) + u'): ' + RPFrameworkUtils.to_unicode(err)) else: self.hostPlugin.logger.error(u'An error occurred processing the SOAP/JSON POST request: (Device: ' + RPFrameworkUtils.to_unicode(self.indigoDevice.id) + u'): ' + RPFrameworkUtils.to_unicode(err)) if not response is None: self.hostPlugin.logger.debug(RPFrameworkUtils.to_unicode(response.text))
def addChildDevice(self, device): self.hostPlugin.logger.threaddebug(u'Adding child device ' + RPFrameworkUtils.to_unicode(device.indigoDevice.id) + u' to ' + RPFrameworkUtils.to_unicode(self.indigoDevice.id)) # the key into the dictionary will be specified by the GUI configuration variable childDeviceKey = self.getChildDeviceKeyByDevice(device) self.hostPlugin.logger.threaddebug(u'Created device key: ' + childDeviceKey) # add the device to the list of those managed by this device... self.childDevices[childDeviceKey] = device
def readLine(self, connection, lineEndingToken, commandResponseTimeout): if self.connectionType == CONNECTIONTYPE_TELNET: return RPFrameworkUtils.to_unicode(connection.read_until(lineEndingToken, commandResponseTimeout)) elif self.connectionType == CONNECTIONTYPE_SERIAL: # Python 2.6 changed the readline signature to not include a line-ending token, # so we have to "manually" re-create that here #return connection.readline(None) lineRead = u'' lineEndingTokenLen = len(lineEndingToken) while True: c = connection.read(1) if c: lineRead += c if lineRead[-lineEndingTokenLen:] == lineEndingToken: break else: break return RPFrameworkUtils.to_unicode(lineRead)
def readLine(self, connection, lineEndingToken, commandResponseTimeout): if self.connectionType == CONNECTIONTYPE_TELNET: return RPFrameworkUtils.to_unicode( connection.read_until(lineEndingToken, commandResponseTimeout)) elif self.connectionType == CONNECTIONTYPE_SERIAL: # Python 2.6 changed the readline signature to not include a line-ending token, # so we have to "manually" re-create that here #return connection.readline(None) lineRead = u'' lineEndingTokenLen = len(lineEndingToken) while True: c = connection.read(1) if c: lineRead += c if lineRead[-lineEndingTokenLen:] == lineEndingToken: break else: break return RPFrameworkUtils.to_unicode(lineRead)
def getDeviceAddressInfo(self): if self.connectionType == CONNECTIONTYPE_TELNET: return (u'', 0) else: portName = RPFrameworkUtils.to_unicode(self.hostPlugin.substituteIndigoValues(self.hostPlugin.getGUIConfigValue(self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_PORTNAME, ""), self, None)) baudRate = int(self.hostPlugin.substituteIndigoValues(self.hostPlugin.getGUIConfigValue(self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_BAUDRATE, "115200"), self, None)) parity = eval("serial." + self.hostPlugin.substituteIndigoValues(self.hostPlugin.getGUIConfigValue(self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_PARITY, "PARITY_NONE"), self, None)) byteSize = eval("serial." + self.hostPlugin.substituteIndigoValues(self.hostPlugin.getGUIConfigValue(self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_BYTESIZE, "EIGHTBITS"), self, None)) stopBits = eval("serial." + self.hostPlugin.substituteIndigoValues(self.hostPlugin.getGUIConfigValue(self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_STOPBITS, "STOPBITS_ONE"), self, None)) timeout = float(self.hostPlugin.substituteIndigoValues(self.hostPlugin.getGUIConfigValue(self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_READTIMEOUT, "1.0"), self, None)) writeTimeout = float(self.hostPlugin.substituteIndigoValues(self.hostPlugin.getGUIConfigValue(self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_WRITETIMEOUT, "1.0"), self, None)) return (portName, (baudRate, parity, byteSize, stopBits, timeout, writeTimeout))
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 uPnPDiscover(service, timeout=2, retries=1): group = ("239.255.255.250", 1900) message = "\r\n".join([ "M-SEARCH * HTTP/1.1", "HOST: " + group[0] + ":" + RPFrameworkUtils.to_str(group[1]), "MAN: ""ssdp:discover""", "ST: " + service,"MX: 3","",""]) socket.setdefaulttimeout(timeout) responses = {} for _ in range(retries): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) sock.sendto(message, group) while True: try: response = SSDPResponse(sock.recv(1024)) responses[response.location] = response except socket.timeout: break return responses.values()
def substituteCriteriaFormatString(self, formatString, responseObj, rpCommand, rpDevice, rpPlugin): substitutedCriteria = formatString if substitutedCriteria is None: return u"" # substitute the response/command object values as those are # specific to commands if rpCommand is not None: substitutedCriteria = substitutedCriteria.replace(u"%cp:name%", rpCommand.commandName) substitutedCriteria = substitutedCriteria.replace( u"%cp:payload%", RPFrameworkUtils.to_unicode(rpCommand.commandPayload) ) if isinstance(responseObj, (str, unicode)): substitutedCriteria = substitutedCriteria.replace("%cp:response%", responseObj) # substitute the standard RPFramework substitutions substitutedCriteria = rpPlugin.substituteIndigoValues(substitutedCriteria, rpDevice, None) # return the result back to the caller return substitutedCriteria
def getDeviceAddressInfo(self): if self.connectionType == CONNECTIONTYPE_TELNET: return (u'', 0) else: portName = RPFrameworkUtils.to_unicode( self.hostPlugin.substituteIndigoValues( self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_PORTNAME, ""), self, None)) baudRate = int( self.hostPlugin.substituteIndigoValues( self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_BAUDRATE, "115200"), self, None)) parity = eval("serial." + self.hostPlugin.substituteIndigoValues( self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_PARITY, "PARITY_NONE"), self, None)) byteSize = eval("serial." + self.hostPlugin.substituteIndigoValues( self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_BYTESIZE, "EIGHTBITS"), self, None)) stopBits = eval("serial." + self.hostPlugin.substituteIndigoValues( self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_STOPBITS, "STOPBITS_ONE"), self, None)) timeout = float( self.hostPlugin.substituteIndigoValues( self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_READTIMEOUT, "1.0"), self, None)) writeTimeout = float( self.hostPlugin.substituteIndigoValues( self.hostPlugin.getGUIConfigValue( self.indigoDevice.deviceTypeId, GUI_CONFIG_SERIALPORT_WRITETIMEOUT, "1.0"), self, None)) return (portName, (baudRate, parity, byteSize, stopBits, timeout, writeTimeout))
else: self.handleRESTfulError(command, str(responseObj.status_code), responseObj) except Exception, e: self.handleRESTfulError(command, e, responseObj) 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(deviceHTTPAddress, 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.logDebugMessage(u'Post Command Pause: ' + RPFrameworkUtils.to_unicode(command.postCommandPause), RPFrameworkPlugin.DEBUGLEVEL_MED) 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 # when the queue is empty, pause a bit on each iteration 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:
def __repr__(self): return u'<SSDPResponse(%(location)s, %(st)s, %(usn)s, %(server)s)>' % (self.__dict__) + RPFrameworkUtils.to_unicode(self.allHeaders) + u'</SSDPResonse>'
def isValueValid(self, proposedValue): # if the value is required but empty then error here if proposedValue == None or proposedValue == u'': return not self.isRequired # now validate that the type is correct... if self.paramType == ParamTypeInteger: try: proposedIntValue = int(proposedValue) if proposedIntValue < self.minValue or proposedIntValue > self.maxValue: raise u'Param value not in range' return True except: return False elif self.paramType == ParamTypeFloat: try: proposedFltValue = float(proposedValue) if proposedFltValue < self.minValue or proposedFltValue > self.maxValue: raise u'Param value not in range' return True except: return False elif self.paramType == ParamTypeBoolean: if type(proposedValue) is bool: return True else: return proposedValue.lower() == u'true' elif self.paramType == ParamTypeOSDirectoryPath: # validate that the path exists... and that it is a directory return os.path.isdir(RPFrameworkUtils.to_str(proposedValue)) elif self.paramType == ParamTypeOSFilePath: # validate that the file exists (and that it is a file) return os.path.isfile(RPFrameworkUtils.to_str(proposedValue)) elif self.paramType == ParamTypeIPAddress: # validate the IP address using IPv4 standards for now... return self.isIPv4Valid(RPFrameworkUtils.to_str(proposedValue)) elif self.paramType == ParamTypeList: # validate that the list contains between the minimum and maximum # number of entries if len(proposedValue) < self.minValue or len(proposedValue) > self.maxValue: return False else: return True else: # default is a string value... so this will need to check against the # validation expression, if set, and string length if self.validationExpression != u'': if re.search(self.validationExpression, proposedValue, re.I) == None: return False strLength = len(proposedValue) if strLength < self.minValue or strLength > self.maxValue: return False # if string processing makes it here then all is good return True
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()
def concurrentCommandProcessingThread(self, commandQueue): try: self.hostPlugin.logDebugMessage(u'Concurrent Processing Thread started for device ' + RPFrameworkUtils.to_unicode(self.indigoDevice.id), RPFrameworkPlugin.DEBUGLEVEL_MED) # 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: indigo.server.log(u'No IP address specified for device ' + RPFrameworkUtils.to_unicode(self.indigoDevice.id) + u'; ending command processing thread.', isError=True) 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.logDebugMessage(u'Command queue has ' + RPFrameworkUtils.to_unicode(lenQueue) + u' command(s) waiting', RPFrameworkPlugin.DEBUGLEVEL_HIGH) # 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.logDebugMessage(u'Create connection command de-queued', RPFrameworkPlugin.DEBUGLEVEL_MED) # 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.logDebugMessage(u'Initiating sleep of ' + RPFrameworkUtils.to_unicode(pauseTime) + u' seconds from command.', RPFrameworkPlugin.DEBUGLEVEL_MED) time.sleep(pauseTime) except: indigo.server.log(u'Invalid pause time requested', isError=True) 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.logDebugMessage(u'Executing full status update request...', RPFrameworkPlugin.DEBUGLEVEL_MED) self.hostPlugin.executeAction(None, indigoActionId=updateStatusPollerActionId, indigoDeviceId=self.indigoDevice.id, paramValues=None) updateStatusPollerNextRun = time.time() + updateStatusPollerInterval else: self.hostPlugin.logDebugMessage(u'Ignoring status update request, no action specified to update device status', RPFrameworkPlugin.DEBUGLEVEL_HIGH) 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.logErrorMessage(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.logDebugMessage(u'Processing GET operation: ' + RPFrameworkUtils.to_unicode(command.commandPayload), RPFrameworkPlugin.DEBUGLEVEL_MED) # 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.logDebugMessage(u'Using login credentials... Username=> ' + username + u'; Password=>' + RPFrameworkUtils.to_unicode(len(password)) + u' characters long', RPFrameworkPlugin.DEBUGLEVEL_HIGH) 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.logDebugMessage(u'Command Response: [' + RPFrameworkUtils.to_unicode(responseObj.status_code) + u'] -=- binary data written to ' + RPFrameworkUtils.to_unicode(saveLocation) + u'-=-', RPFrameworkPlugin.DEBUGLEVEL_HIGH) 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.logDebugMessage(u'No image size specified for ' + RPFrameworkUtils.to_unicode(saveLocation) + u'; skipping resize.', RPFrameworkPlugin.DEBUGLEVEL_MED) else: self.hostPlugin.logDebugMessage(u'Executing resize via command line "' + resizeCommandLine + u'"', RPFrameworkPlugin.DEBUGLEVEL_HIGH) try: subprocess.Popen(resizeCommandLine, shell=True) self.hostPlugin.logDebugMessage(saveLocation + u' resized via sip shell command', RPFrameworkPlugin.DEBUGLEVEL_HIGH) except: self.hostPlugin.logErrorMessage(u'Error resizing image via sips') finally: if not localFile is None: localFile.close() else: indigo.server.log(u'Unable to complete download action - no filename specified', isError=True) else: # handle this return as a text-based return self.hostPlugin.logDebugMessage(u'Command Response: [' + RPFrameworkUtils.to_unicode(responseObj.status_code) + u'] ' + RPFrameworkUtils.to_unicode(responseObj.text), RPFrameworkPlugin.DEBUGLEVEL_HIGH) self.hostPlugin.logDebugMessage(command.commandName + u' command completed; beginning response processing', RPFrameworkPlugin.DEBUGLEVEL_HIGH) self.handleDeviceTextResponse(responseObj, command) self.hostPlugin.logDebugMessage(command.commandName + u' command response processing completed', RPFrameworkPlugin.DEBUGLEVEL_HIGH) elif responseObj.status_code == 401: self.handleRESTfulError(command, u'401 - Unauthorized', responseObj) else: self.handleRESTfulError(command, str(responseObj.status_code), responseObj) except Exception, e: self.handleRESTfulError(command, e, responseObj) 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.logDebugMessage(u'Received SOAP/JSON command request: ' + command.commandPayload, RPFrameworkPlugin.DEBUGLEVEL_HIGH) 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.logDebugMessage(u'Processing SOAP/JSON operation to ' + fullGetUrl, RPFrameworkPlugin.DEBUGLEVEL_MED) 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.logDebugMessage(u'Sending SOAP/JSON request:\n' + RPFrameworkUtils.to_str(soapBody), RPFrameworkPlugin.DEBUGLEVEL_HIGH) 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.logDebugMessage(u'Command Response: [' + RPFrameworkUtils.to_unicode(responseObj.status_code) + u'] ' + RPFrameworkUtils.to_unicode(responseObj.text), RPFrameworkPlugin.DEBUGLEVEL_HIGH) self.hostPlugin.logDebugMessage(command.commandName + u' command completed; beginning response processing', RPFrameworkPlugin.DEBUGLEVEL_HIGH) self.handleDeviceTextResponse(responseObj, command) self.hostPlugin.logDebugMessage(command.commandName + u' command response processing completed', RPFrameworkPlugin.DEBUGLEVEL_HIGH) else: self.handleRESTfulError(command, str(responseObj.status_code), responseObj) except Exception, e: self.handleRESTfulError(command, e, responseObj) else:
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) )
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: # 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(deviceHTTPAddress, 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 # when the queue is empty, pause a bit on each iteration 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:
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()