def cli() -> None: """Parse command line options, provide entry point for console scripts.""" arguments = sys.argv[1:] parser = argparse.ArgumentParser( description="Emulate Belkin Wemo devices " "for use with Amaazon Echo" ) parser.add_argument( "-v", "--verbose", help="increase verbosity (may " "repeat up to -vvv)", action="count", default=0, ) parser.add_argument("-c", "--config", help="specify alternate config file") parser.add_argument( "-V", "--version", action="version", version=__version__ ) args = parser.parse_args(arguments) # args.verbose defaults to 0 # 40 - 10 * 0 = 40 == logging.ERROR verbosity = max(40 - 10 * args.verbose, 10) logger.setLevel(verbosity) main(config_path_str=args.config, verbosity=verbosity)
def main(config_path_str: str = None, verbosity: int = 20) -> None: """Run the main fauxmo process. Spawns a UDP server to handle the Echo's UPnP / SSDP device discovery process as well as multiple TCP servers to respond to the Echo's device setup requests and handle its process for turning devices on and off. Args: config_path_str: Path to config file. If not given will search for `config.json` in cwd, `~/.fauxmo/`, and `/etc/fauxmo/`. verbosity: Logging verbosity, defaults to 20 """ logger.setLevel(verbosity) logger.info(f"Fauxmo version {__version__}") logger.debug(sys.version) if config_path_str: config_path = pathlib.Path(config_path_str) else: for config_dir in ('.', "~/.fauxmo", "/etc/fauxmo"): config_path = pathlib.Path(config_dir) / 'config.json' if config_path.is_file(): logger.info(f"Using config: {config_path}") break try: config = json.loads(config_path.read_text()) except FileNotFoundError: logger.error("Could not find config file in default search path. Try " "specifying your file with `-c`.\n") raise # Every config should include a FAUXMO section fauxmo_config = config.get("FAUXMO") fauxmo_ip = get_local_ip(fauxmo_config.get("ip_address")) ssdp_server = SSDPServer() servers = [] loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) if verbosity < 20: loop.set_debug(True) logging.getLogger('asyncio').setLevel(logging.DEBUG) try: plugins = config['PLUGINS'] except KeyError: # Give a meaningful message without a nasty traceback if it looks like # user is running a pre-v0.4.0 config. errmsg = ("`PLUGINS` key not found in your config.\n" "You may be trying to use an outdated config.\n" "If so, please review <https://github.com/n8henrie/fauxmo> " "and update your config for Fauxmo >= v0.4.0.") print(errmsg) sys.exit(1) for plugin in plugins: modname = f"{__package__}.plugins.{plugin.lower()}" try: module = importlib.import_module(modname) # Will fail until https://github.com/python/typeshed/pull/1083 merged # and included in the next mypy release except ModuleNotFoundError: # type: ignore path_str = config['PLUGINS'][plugin]['path'] module = module_from_file(modname, path_str) PluginClass = getattr(module, plugin) # noqa if not issubclass(PluginClass, FauxmoPlugin): raise TypeError(f"Plugins must inherit from {repr(FauxmoPlugin)}") # Pass along variables defined at the plugin level that don't change # per device plugin_vars = { k: v for k, v in config['PLUGINS'][plugin].items() if k not in {"DEVICES", "path"} } logger.debug(f"plugin_vars: {repr(plugin_vars)}") for device in config['PLUGINS'][plugin]['DEVICES']: logger.debug(f"device config: {repr(device)}") # Ensure port is `int`, set it if not given (`None`) or 0 device["port"] = int(device.get('port', 0)) or find_unused_port() try: plugin = PluginClass(**plugin_vars, **device) except TypeError: logger.error(f"Error in plugin {repr(PluginClass)}") raise fauxmo = partial(Fauxmo, name=plugin.name, plugin=plugin) coro = loop.create_server(fauxmo, host=fauxmo_ip, port=plugin.port) server = loop.run_until_complete(coro) servers.append(server) ssdp_server.add_device(plugin.name, fauxmo_ip, plugin.port) logger.debug(f"Started fauxmo device: {repr(fauxmo.keywords)}") logger.info("Starting UDP server") # mypy will fail until https://github.com/python/typeshed/pull/1084 merged, # pulled into mypy, and new mypy released listen = loop.create_datagram_endpoint( lambda: ssdp_server, # type: ignore sock=make_udp_sock()) transport, _ = loop.run_until_complete(listen) # type: ignore for signame in ('SIGINT', 'SIGTERM'): try: loop.add_signal_handler(getattr(signal, signame), loop.stop) # Workaround for Windows (https://github.com/n8henrie/fauxmo/issues/21) except NotImplementedError: if sys.platform == 'win32': pass else: raise loop.run_forever() # Will not reach this part unless SIGINT or SIGTERM triggers `loop.stop()` logger.debug("Shutdown starting...") transport.close() for idx, server in enumerate(servers): logger.debug(f"Shutting down server {idx}...") server.close() loop.run_until_complete(server.wait_closed()) loop.close()
def main(config_path=None, verbosity=20): """Runs the main fauxmo process Spawns a UDP server to handle the Echo's UPnP / SSDP device discovery process as well as multiple TCP servers to respond to the Echo's device setup requests and handle its process for turning devices on and off. Kwargs: config_path (str): Path to config file. If not given will search for `config.json` in cwd, `~/.fauxmo/`, and `/etc/fauxmo/`. verbosity (int): Logging verbosity, defaults to 20 """ logger.setLevel(verbosity) logger.debug(sys.version) if not config_path: config_dirs = ['.', os.path.expanduser("~/.fauxmo"), "/etc/fauxmo"] for config_dir in config_dirs: config_path = os.path.join(config_dir, 'config.json') if os.path.isfile(config_path): logger.info("Using config: {}".format(config_path)) break try: with open(config_path) as config_file: config = json.load(config_file) except FileNotFoundError: logger.error("Could not find config file in default search path. " "Try specifying your file with `-c` flag.\n") raise # Every config should include a FAUXMO section fauxmo_config = config.get("FAUXMO") fauxmo_ip = get_local_ip(fauxmo_config.get("ip_address")) ssdp_server = SSDPServer() servers = [] loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.set_debug(True) # Initialize Fauxmo devices for device in config.get('DEVICES'): name = device.get('description') port = int(device.get("port")) action_handler = RESTAPIHandler(**device.get("handler")) fauxmo = partial(Fauxmo, name=name, action_handler=action_handler) coro = loop.create_server(fauxmo, host=fauxmo_ip, port=port) server = loop.run_until_complete(coro) servers.append(server) ssdp_server.add_device(name, fauxmo_ip, port) logger.debug(fauxmo.keywords) # Initialize Home Assistant devices if config exists and enable is True if config.get("HOMEASSISTANT", {}).get("enable") is True: hass_config = config.get("HOMEASSISTANT") hass_host = hass_config.get("host") hass_password = hass_config.get("password") hass_port = hass_config.get("port") for device in hass_config.get('DEVICES'): name = device.get('description') device_port = device.get("port") entity = device.get("entity_id") action_handler = HassAPIHandler(host=hass_host, password=hass_password, entity=entity, port=hass_port) fauxmo = partial(Fauxmo, name=name, action_handler=action_handler) coro = loop.create_server(fauxmo, host=fauxmo_ip, port=device_port) server = loop.run_until_complete(coro) servers.append(server) ssdp_server.add_device(name, fauxmo_ip, device_port) logger.debug(fauxmo.keywords) logger.info("Starting UDP server") listen = loop.create_datagram_endpoint(lambda: ssdp_server, local_addr=('0.0.0.0', 1900), family=socket.AF_INET) transport, protocol = loop.run_until_complete(listen) for signame in ('SIGINT', 'SIGTERM'): loop.add_signal_handler(getattr(signal, signame), loop.stop) loop.run_forever() # Will not reach this part unless SIGINT or SIGTERM triggers `loop.stop()` logger.debug("Shutdown starting...") transport.close() for idx, server in enumerate(servers): logger.debug("Shutting down server {}...".format(idx)) server.close() loop.run_until_complete(server.wait_closed()) loop.close()