def waitForRemoteConnectionReady(self): """ Wait for Linknx's XML server to accept incoming connections. This method should be called if unsure about when the Linknx has been started. It may take some time to get ready. This method attempts to connect to Linknx during 10 seconds and raises an Exception if Linknx still is unreachable after this delay. """ # Ask for config. logger.reportInfo("Start connecting to linknx on {0}.".format(self.address)) attemptId = 0 maxAttemptCount = 10 while attemptId < maxAttemptCount: attemptId += 1 try: conf = self.config # Linknx is ready if we reach this point. logger.reportInfo("Linknx is up and ready, let's start.") return except ConnectionRefusedError: logger.reportInfo("Linknx is not yet ready... (attempt {0}/{1})".format(attemptId, maxAttemptCount)) except Exception as e: logger.reportException() time.sleep(1) raise Exception("Linknx is not reachable.")
def setUp(self, linknxConfFile='linknx_test_conf.xml', usesCommunicator=True, hwConfigFile=os.path.join(os.path.dirname(__file__), 'homewatcher_test_conf.xml')): usesLinknx = linknxConfFile != None communicatorAddress = ('localhost', 1031) if usesCommunicator else None userScript = os.path.join(os.path.dirname(configuration.__file__), 'linknxuserfile.py') userScriptArgs = {'hwconfig':hwConfigFile} try: if usesCommunicator: linknxPatchedFile = tempfile.mkstemp(suffix='.xml', text=True)[1] hwConfigurator = configurator.Configurator(hwConfigFile, linknxConfFile, linknxPatchedFile) hwConfigurator.generateConfig() hwConfigurator.writeConfig() else: linknxPatchedFile = None base.WithLinknxTestCase.setUp(self, linknxConfFile=linknxPatchedFile, communicatorAddr=communicatorAddress, patchLinknxConfig=False, userScript=userScript, userScriptArgs=userScriptArgs) finally: if linknxPatchedFile is not None: os.remove(linknxPatchedFile) self.homewatcherScriptsDirectory = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..')) self.homewatcherModulesDirectory = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) try: # Redirect the emailing capability of the daemon. if self.alarmDaemon: logger.reportInfo('Redirecting email capability of the alarm daemon to the mock for testing.') self.alarmDaemon.sendEmail = self.sendEmailMock else: logger.reportInfo('No alarm daemon. Email redirection is not set.') self.emailInfo = None except: logger.reportException('Error in setUp.') self.tearDown() self.fail('Test setup failed.') raise
def waitForData(self, endChar=chr(4)): # Wait for a connection. try: conn, address = self._socket.accept() except socket.timeout: return (None, None) # Read incoming data. data = b'' while True: try: chunk = conn.recv(4) endCharIx = chunk.find(endChar) isLastChunk = endCharIx != -1 if isLastChunk: chunk = chunk[:endCharIx] data += chunk if isLastChunk: break except: logger.reportException( 'Exception when waiting for incoming data.') try: if not conn is None: conn.close() except: logger.reportException( 'Could not close connection. Connection is discarded and process continues.' ) pass return None, None return (data, conn)
def isEnabled(self, value): with self._lock: logger.reportDebug('{1}.isEnabled={0}, activationTimer is {2}'.format(value, self, self._activationTimer)) if not value: self.stopActivationTimer() if self._enabledObject.value == value: return # Make sure this sensor is still required by the current mode. # Safer in case of data race between the thread that runs # updateModeFromLinknx and the one that runs the activation timer. if self.isRequiredByCurrentMode() or not value: self._enabledObject.value = value if value: try: if self.persistenceObject != None: self.persistenceObject.value = False self.onEnabled() except Exception as e: self._enabledObject.value = False logger.reportException() else: # Sensor may currently be in alert. self.alert.removeSensorFromAlert(self) self.onDisabled() logger.reportInfo('Sensor {0} is now {1}'.format(self.name, 'enabled' if value else 'disabled'))
def waitForRemoteConnectionReady(self): """ Wait for Linknx's XML server to accept incoming connections. This method should be called if unsure about when the Linknx has been started. It may take some time to get ready. This method attempts to connect to Linknx during 10 seconds and raises an Exception if Linknx still is unreachable after this delay. """ # Ask for config. logger.reportInfo('Start connecting to linknx on {0}.'.format( self.address)) attemptId = 0 maxAttemptCount = 10 while attemptId < maxAttemptCount: attemptId += 1 try: conf = self.config # Linknx is ready if we reach this point. logger.reportInfo('Linknx is up and ready, let\'s start.') return except ConnectionRefusedError: logger.reportInfo( 'Linknx is not yet ready... (attempt {0}/{1})'.format( attemptId, maxAttemptCount)) except Exception as e: logger.reportException() time.sleep(1) raise Exception('Linknx is not reachable.')
def setUp(self, linknxConfFile='linknx_test_conf.xml', communicatorAddr=('localhost', 1031), patchLinknxConfig=True, userScript='linknxuserfile.py', userScriptArgs=None): TestCaseBase.setUp(self) self.linknxProcess = None self.linknx = None self.communicator = None self.linknxOutputFDs = None self.linknxXMLConfig = linknxConfFile self.communicatorAddress = communicatorAddr try: # Patch config. if self.linknxXMLConfig != None: testDir = 'test_files' if not os.path.exists(testDir): os.mkdir(testDir) if self.communicatorAddress != None and patchLinknxConfig: linknxPatchedConfigFile = os.path.join(testDir, 'autogenlinknx.conf.xml') if os.path.exists(linknxPatchedConfigFile): os.remove(linknxPatchedConfigFile) self.configurator = configurator.Configurator(self.linknxXMLConfig, linknxPatchedConfigFile, self.communicatorAddress) self.configurator.cleanConfig() self.configurator.generateConfig() self.configurator.writeConfig() else: linknxPatchedConfigFile = self.linknxXMLConfig # Start linknx. linknxErrFilename = 'test_files/{0}.linknx.err'.format(self.name) linknxOutFilename = 'test_files/{0}.linknx.out'.format(self.name) efdw = open(linknxErrFilename, 'w') efdr = open(linknxErrFilename, 'r') ofdw = open(linknxOutFilename, 'w') self.linknxOutputFDs = (efdr, efdw, ofdw) self.linknxProcess = subprocess.Popen( ['linknx', '--config={0}'.format(linknxPatchedConfigFile)], stdout=self.linknxOutputFDs[2], stderr=self.linknxOutputFDs[1]) logger.reportInfo('linknx started with pid {0}'.format(self.linknxProcess.pid)) self.linknx = linknx.Linknx('localhost', 1030) self.currentAssertions.append(self.checkLinknx) # Start pyknx. if self.communicatorAddress != None: self.communicator = communicator.Communicator(self.linknx, userScript, self.communicatorAddress, userScriptArgs) self.communicator.startListening() else: self.communicator = None logger.reportInfo('Set up finished.') self.waitDuring(0.5, 'Pause to make sure everything gets ready.') except: logger.reportException('Error in setUp.') self.tearDown() self.fail('Test setup failed.') raise
def setUp(self, linknxConfFile='linknx_test_conf.xml', usesCommunicator=True, hwConfigFile=os.path.join(os.path.dirname(__file__), 'homewatcher_test_conf.xml')): usesLinknx = linknxConfFile != None communicatorAddress = ('localhost', 1031) if usesCommunicator else None userScript = os.path.join(os.path.dirname(configuration.__file__), 'linknxuserfile.py') userScriptArgs = {'hwconfig': hwConfigFile} try: if usesCommunicator: linknxPatchedFile = tempfile.mkstemp(suffix='.xml', text=True)[1] hwConfigurator = configurator.Configurator( hwConfigFile, linknxConfFile, linknxPatchedFile) hwConfigurator.generateConfig() hwConfigurator.writeConfig() else: linknxPatchedFile = None base.WithLinknxTestCase.setUp(self, linknxConfFile=linknxPatchedFile, communicatorAddr=communicatorAddress, patchLinknxConfig=False, userScript=userScript, userScriptArgs=userScriptArgs) finally: if linknxPatchedFile is not None: os.remove(linknxPatchedFile) self.homewatcherScriptsDirectory = os.path.normpath( os.path.join(os.path.dirname(__file__), '..', '..')) self.homewatcherModulesDirectory = os.path.normpath( os.path.join(os.path.dirname(__file__), '..')) try: # Redirect the emailing capability of the daemon. if self.alarmDaemon: logger.reportInfo( 'Redirecting email capability of the alarm daemon to the mock for testing.' ) self.alarmDaemon.sendEmail = self.sendEmailMock mock = TestCaseBase.ExecuteActionMock(self.alarmDaemon.linknx, self) self.alarmDaemon.linknx.executeAction = mock.executeAction else: logger.reportInfo( 'No alarm daemon. Email redirection is not set.') self.emailInfo = None self.shellCmdInfo = None except: logger.reportException('Error in setUp.') self.tearDown() self.fail('Test setup failed.') raise
def _executeUserCallback(self, callbackName, context, isOptional=False): try: if hasattr(self._userModule, callbackName): logger.reportDebug('Calling user callback {0} with context {1}'.format(callbackName, context)) callback = getattr(self._userModule, callbackName) res = callback(context) logger.reportDebug('Callback {0} returned {1}'.format(callbackName, res)) return res else: message='No function {0} defined in {1}'.format(callbackName, self._userFile) if isOptional: logger.reportInfo(message + ', skipping') else: logger.reportWarning(message) except Exception as e: logger.reportException('User code execution failed.')
def startListening(self): """ Start the communicator. It is then waiting for incoming information from Linknx. If provided, the initializeUserScript function of the user file is called. Its context contains the arguments that were optionally given to the __init__ method. """ if self.isListening: return # Make sure linknx is ready. self.linknx.waitForRemoteConnectionReady() # Start listening early to avoid communication errors from linknx. Those # errors are never harmful but the user may be surprized and worried # about them! self._listenerThread = Communicator.Listener(self._address, self) self._listenerThread.start() timeout = time.time() + 4 while not self._listenerThread.isReady and time.time() < timeout: time.sleep(0.3) if not self._listenerThread.isReady: raise Exception('Could not initialize listening socket.') # Initialize user-provided script. The purpose of this callback is to # let the user initialize its script by reading state from linknx (and # possibly anywhere else). Thus, linknx should not raise events yet, # since user script would likely be partially initialized. The # isUserScriptInitialized flag is used for that purpose. if self._loadUserFile(): logger.reportInfo('Initializing user script...') try: self._executeUserCallback('initializeUserScript', CallbackContext(self, args=self._userScriptArgs), True) except Exception as e: logger.reportException('User script initialization failed, communicator will stop immediately.') self.stopListening() return logger.reportInfo('User script initialized.') self.isUserScriptInitialized = True
def run(self): logger.reportInfo('Listening on ' + str(self._address)) self._isStopRequested = False try: self._socket.bind(self._address) # Thread loop. while not self._isStopRequested: self.isReady = True data, conn = self._socket.waitForString(endChar='$') # Throw data away if script has not been initialized yet. # See startListening for details. if data is None or not self._communicator.isUserScriptInitialized: time.sleep(0.1) continue logger.reportDebug('Data received: {0}'.format(data)) # Handle request. tokens = data.split('|') callbackName = tokens[0] # Parse arguments. First is object id. args={} for token in tokens[1:]: argName, sep, argValue = token.partition('=') if argValue: argValue = argValue.strip() args[argName.strip()] = argValue context = CallbackContext(self, args) res = self._communicator._executeUserCallback(callbackName, context) if res: conn.sendall(res + '$') conn.close() except Exception as e: logger.reportException() finally: logger.reportDebug('Closing socket...') self._socket.close() logger.reportInfo('Socket closed. Listening terminated.') self._socket = None
def loadPlugins(): global _plugins pluginDirectory = os.path.dirname(__file__) pluginModules = glob.glob(os.path.join(pluginDirectory, '*.py')) # Scan all modules in the 'plugins' subdirectory and instanciate all classes # that inherit Plugin. for moduleFile in pluginModules: if os.path.basename(moduleFile) == '__init__.py': continue module = importlib.import_module('homewatcher.plugins.{0}'.format( os.path.splitext(os.path.basename(moduleFile))[0])) for symbolName in dir(module): symbol = vars(module)[symbolName] if isinstance(symbol, type) and issubclass( symbol, homewatcher.plugin.Plugin): logger.reportInfo('Loading {0}'.format(symbol)) plugin = symbol() try: plugin.load() logger.reportInfo('{0} loaded.'.format(symbol)) except Exception as e: logger.reportException( 'Failed to load plugin {0}'.format(plugin))
raise Exception('Malformed value for ' + option +'. Expecting a tuple (hostname:port)') return (addrStr[0:ix], int(addrStr[ix + 1:])) def makeArgumentParser(description): parser = argparse.ArgumentParser(description=description) parser.add_argument('-c', '--comm-addr', dest='communicatorAddress', help='Address of the communicator. This argument must specify the hostname or the ip address followed by a colon and the port to listen on. Default is "localhost:1029"', default='localhost:1029') parser.add_argument('-l', '--linknx-addr', dest='linknxAddress', help='Address of the linknx server to bind to. This argument must specify the hostname or the ip address followed by a colon and the port. Default is "localhost:1028"', default='localhost:1028') parser.add_argument('userFile', help='use FILE as the user python script that implements callbacks functions declared in the linknx configuration (see the pyknxcallback attributes in XML).', metavar='FILE') parser.add_argument('--log-file', dest='logFile', help='write communicator\'s output to FILE rather than to standard output.', metavar='FILE', default=None) parser.add_argument('-d', '--daemonize', help='ask daemon to detach and run as a background daemon.', action='store_true', default=False) parser.add_argument('--pid-file', dest='pidFile', help='writes the PID of the daemon process to PIDFILE.', metavar='PIDFILE') parser.add_argument('-v', '--verbose', dest='verbosityLevel', help='set verbosity level. Default is "error".', metavar='LEVEL', choices=[l.lower() for l in logger.getLevelsToString()], default='error') return parser if __name__ == '__main__': parser = makeArgumentParser(__doc__) args = parser.parse_args() # Configure logger. logger.initLogger(None, args.verbosityLevel.upper()) args.communicatorAddress = parseAddress(args.communicatorAddress, 'communicator address') try: Communicator.run(args.linknxAddress, args.userFile, args.communicatorAddress, logFile=args.logFile, verbosityLevel=args.verbosityLevel, daemonizes=args.daemonize, pidFile=args.pidFile) except SystemExit: # This is a normal exit. pass except: logger.reportException()
import argparse import sys import logging import os from pyknx import logger __doc__ = __doc__.format(scriptname=os.path.basename(__file__)) if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('linknxConfig', help='use LKNCONF as the source linknx configuration.', metavar='LKNCONF') parser.add_argument('-i', '--input-file', dest='homewatcherConfig', help='read homewatcher configuration from HWCONF rather than from standard input.', metavar='HWCONF') parser.add_argument('-o', '--output-file', dest='outputFile', help='write the modified linknx configuration to FILE rather than to standard output.', metavar='FILE') parser.add_argument('-v', '--verbose', dest='verbosityLevel', help='set verbosity level.', metavar='LEVEL', choices=[l.lower() for l in logger.getLevelsToString()], default='error') args = parser.parse_args() # Configure logger. logger.initLogger(None, args.verbosityLevel.upper()) # Start configurator. configurator = Configurator(args.homewatcherConfig, args.linknxConfig, args.outputFile) # Generate config. try: configurator.cleanConfig() configurator.generateConfig() configurator.writeConfig() except: logger.reportException() sys.exit(2)
def handleRequest(requestType, doc): logger.initLogger(None, logging.INFO, usesDetailedLogging=False) parser = argparse.ArgumentParser(description=doc) parser.add_argument( '-s', '--server', dest='host', help= 'Hostname of the machine running the linknx daemon. Default is localhost.', default='localhost') parser.add_argument('-p', '--port', dest='port', help='Port linknx listens on. Default is 1028.', default=1028) if requestType == 'read': parser.add_argument( 'objectIds', help='ID represents the identifier of the object to read.', metavar='ID', nargs='+') parser.add_argument( '-R', '--regex', action='store_true', help= 'ID in the "object" argument is interpreted as a regex and used to find objects to read. The pattern must comply with the \'re\' python module.' ) parser.add_argument( '--value-only', action='store_true', help= 'Output the value of the queried object but do not prefix it with the object\'s id.' ) parser.add_argument( '--expected-value', help= 'Expected value of the object. This script will exit with a non-zero return code if the value is not the expected one. This is useful when using this script in a "if" test of a shell script.' ) elif requestType == 'write': parser.add_argument( 'object', help='ID represents the identifier of the object to write to.', metavar='ID') parser.add_argument( 'value', help='Assigns VALUE to the object identified by ID.', metavar='VALUE') elif requestType == 'execute': parser.add_argument( '--action', help= 'use the ACTION string as the XML representation of the action to execute rather than reading it from standard input.', metavar='ACTION') else: raise Exception('Unsupported request type "{0}".'.format(requestType)) parser.add_argument( '-v', '--verbose', dest='verbosityLevel', help='Set verbosity level. Default is "error".', metavar='LEVEL', choices=[l.lower() for l in logger.getLevelsToString()], default='error') args = parser.parse_args() # Configure logger. logger.initLogger(None, args.verbosityLevel.upper()) # Start linknx. linknx = Linknx(args.host, int(args.port)) try: if requestType == 'read': objects = linknx.getObjects( objectIds=args.objectIds ) if not args.regex else linknx.getObjects(patterns=args.objectIds) # No object. if not objects: logger.reportWarning('No such object.') sys.exit(10) # Count tabs to align columns. report = objects.getValues() longestId = max([len(obj) for obj in report.keys()]) succeeds = True for o in sorted(report): v = report[o] spaceCount = longestId - len(o) spaces = '' while spaceCount > 0: spaces += ' ' spaceCount -= 1 if args.value_only: print('{0}'.format(v)) else: print('{0} {2} {1}'.format(o, v, spaces)) if args.expected_value != None: obj = linknx.getObject(o) convertedExpectedValue = obj.convertValueToString( args.expected_value) convertedObjectValue = obj.convertValueToString(v) succeeds = succeeds and convertedExpectedValue == convertedObjectValue if not succeeds: exit(100) elif requestType == 'write': linknx.getObject(args.object).value = args.value elif requestType == 'execute': if args.action == None: action = ''.join(sys.stdin.readlines()) else: action = args.action linknx.executeAction(action) else: raise Exception('Unsupported request type.') except Exception as e: logger.reportException() sys.exit(3)
def setUp(self, linknxConfFile='linknx_test_conf.xml', communicatorAddr=('localhost', 1031), patchLinknxConfig=True, userScript='linknxuserfile.py', userScriptArgs=None): TestCaseBase.setUp(self) self.linknxProcess = None self.linknx = None self.communicator = None self.linknxOutputFDs = None self.linknxXMLConfig = linknxConfFile self.communicatorAddress = communicatorAddr try: # Patch config. if self.linknxXMLConfig != None: testDir = 'test_files' if not os.path.exists(testDir): os.mkdir(testDir) if self.communicatorAddress != None and patchLinknxConfig: linknxPatchedConfigFile = os.path.join( testDir, 'autogenlinknx.conf.xml') if os.path.exists(linknxPatchedConfigFile): os.remove(linknxPatchedConfigFile) self.configurator = configurator.Configurator( self.linknxXMLConfig, linknxPatchedConfigFile, self.communicatorAddress) self.configurator.cleanConfig() self.configurator.generateConfig() self.configurator.writeConfig() else: linknxPatchedConfigFile = self.linknxXMLConfig # Start linknx. linknxErrFilename = 'test_files/{0}.linknx.err'.format( self.name) linknxOutFilename = 'test_files/{0}.linknx.out'.format( self.name) efdw = open(linknxErrFilename, 'w') efdr = open(linknxErrFilename, 'r') ofdw = open(linknxOutFilename, 'w') self.linknxOutputFDs = (efdr, efdw, ofdw) self.linknxProcess = subprocess.Popen( ['linknx', '--config={0}'.format(linknxPatchedConfigFile)], stdout=self.linknxOutputFDs[2], stderr=self.linknxOutputFDs[1]) logger.reportInfo('linknx started with pid {0}'.format( self.linknxProcess.pid)) self.linknx = linknx.Linknx('localhost', 1030) self.currentAssertions.append(self.checkLinknx) # Start pyknx. if self.communicatorAddress != None: self.communicator = communicator.Communicator( self.linknx, userScript, self.communicatorAddress, userScriptArgs) self.communicator.startListening() else: self.communicator = None logger.reportInfo('Set up finished.') self.waitDuring(0.5, 'Pause to make sure everything gets ready.') except: logger.reportException('Error in setUp.') self.tearDown() self.fail('Test setup failed.') raise
def handleRequest(requestType, doc): logger.initLogger(None, logging.INFO, usesDetailedLogging=False) parser = argparse.ArgumentParser(description=doc) parser.add_argument('-s', '--server', dest='host', help='Hostname of the machine running the linknx daemon. Default is localhost.', default='localhost') parser.add_argument('-p', '--port', dest='port', help='Port linknx listens on. Default is 1028.', default=1028) if requestType == 'read': parser.add_argument('objectIds', help='ID represents the identifier of the object to read.', metavar='ID', nargs='+') parser.add_argument('-R', '--regex', action='store_true', help='ID in the "object" argument is interpreted as a regex and used to find objects to read. The pattern must comply with the \'re\' python module.') parser.add_argument('--value-only', action='store_true', help='Output the value of the queried object but do not prefix it with the object\'s id.') parser.add_argument('--expected-value', help='Expected value of the object. This script will exit with a non-zero return code if the value is not the expected one. This is useful when using this script in a "if" test of a shell script.') elif requestType == 'write': parser.add_argument('object', help='ID represents the identifier of the object to write to.', metavar='ID') parser.add_argument('value', help='Assigns VALUE to the object identified by ID.', metavar='VALUE') elif requestType == 'execute': parser.add_argument('--action', help='use the ACTION string as the XML representation of the action to execute rather than reading it from standard input.', metavar='ACTION') else: raise Exception('Unsupported request type "{0}".'.format(requestType)) parser.add_argument('-v', '--verbose', dest='verbosityLevel', help='Set verbosity level. Default is "error".', metavar='LEVEL', choices=[l.lower() for l in logger.getLevelsToString()], default='error') args = parser.parse_args() # Configure logger. logger.initLogger(None, args.verbosityLevel.upper()) # Start linknx. linknx = Linknx(args.host, int(args.port)) try: if requestType == 'read': objects = linknx.getObjects(objectIds=args.objectIds) if not args.regex else linknx.getObjects(patterns=args.objectIds) # No object. if not objects: logger.reportWarning('No such object.') sys.exit(10) # Count tabs to align columns. report = objects.getValues() longestId = max([len(obj) for obj in report.keys()]) succeeds = True for o in sorted(report): v = report[o] spaceCount = longestId - len(o) spaces='' while spaceCount > 0: spaces+=' ' spaceCount -= 1 if args.value_only: print('{0}'.format(v)) else: print('{0} {2} {1}'.format(o, v, spaces)) if args.expected_value != None: obj = linknx.getObject(o) convertedExpectedValue = obj.convertValueToString(args.expected_value) convertedObjectValue = obj.convertValueToString(v) succeeds = succeeds and convertedExpectedValue == convertedObjectValue if not succeeds: exit(100) elif requestType == 'write': linknx.getObject(args.object).value = args.value elif requestType == 'execute': if args.action == None: action = ''.join(sys.stdin.readlines()) else: action = args.action linknx.executeAction(action) else: raise Exception('Unsupported request type.') except Exception as e: logger.reportException() sys.exit(3)