def get_local_ip(ip_address: str = None) -> str: """Attempt to get the local network-connected IP address. Args: ip_address: Either desired ip address or string or "auto" Returns: Current IP address as string """ if ip_address is None or ip_address.lower() == "auto": logger.debug("Attempting to get IP address automatically") hostname = socket.gethostname() try: ip_address = socket.gethostbyname(hostname) except socket.gaierror: ip_address = "unknown" # Workaround for Linux returning localhost # See: SO question #166506 by @UnkwnTech if ip_address in ["127.0.1.1", "127.0.0.1", "localhost", "unknown"]: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.connect(("8.8.8.8", 80)) ip_address = sock.getsockname()[0] logger.debug(f"Using IP address: {ip_address}") return ip_address
def get_local_ip(ip_address: str = None) -> str: """Attempt to get the local network-connected IP address. Args: ip_address: Either desired ip address or string or "auto" Returns: Current IP address as string """ if ip_address is None or ip_address.lower() == "auto": logger.debug("Attempting to get IP address automatically") hostname = socket.gethostname() ip_address = socket.gethostbyname(hostname) # Workaround for Linux returning localhost # See: SO question #166506 by @UnkwnTech if ip_address in ['127.0.1.1', '127.0.0.1', 'localhost']: tempsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) tempsock.connect(('8.8.8.8', 0)) ip_address = tempsock.getsockname()[0] tempsock.close() logger.debug(f"Using IP address: {ip_address}") return ip_address
def respond_to_search(self, addr: Tuple[str, int]) -> None: """Build and send an appropriate response to an SSDP search request. Args: addr: Address sending search request """ date_str = formatdate(timeval=None, localtime=False, usegmt=True) for device in self.devices: name = device.get('name') ip_address = device.get('ip_address') port = device.get('port') location = f'http://{ip_address}:{port}/setup.xml' serial = make_serial(name) response = '\n'.join([ 'HTTP/1.1 200 OK', 'CACHE-CONTROL: max-age=86400', f'DATE: {date_str}', 'EXT:', f'LOCATION: {location}', 'OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01', f'01-NLS: {uuid.uuid4()}', 'SERVER: Fauxmo, UPnP/1.0, Unspecified', 'ST: urn:Belkin:device:**', f'USN: uuid:Socket-1_0-{serial}::urn:Belkin:device:**', ]) + '\n\n' logger.debug(f"Sending response to {addr}:\n{response}") self.transport.sendto(response.encode(), addr) random.shuffle(self.devices)
def datagram_received(self, data: Union[bytes, Text], addr: Tuple[str, int]) -> None: """Check incoming UDP data for requests for Wemo devices. Args: data: Incoming data content addr: Address sending data """ if isinstance(data, bytes): data = data.decode('utf8') logger.debug(f"Received data below from {addr}:") logger.debug(data) discover_patterns = [ 'ST: urn:Belkin:device:**', 'ST: upnp:rootdevice', 'ST: ssdp:all', ] discover_pattern = next((pattern for pattern in discover_patterns if pattern in data), None) if 'man: "ssdp:discover"' in data.lower() and \ discover_pattern is not None: mx = 0. mx_line = next((line for line in str(data).splitlines() if line.startswith("MX: ")), None) if mx_line: mx_str = mx_line.split()[-1] if mx_str.replace('.', '', 1).isnumeric(): mx = float(mx_str) self.respond_to_search(addr, discover_pattern, mx)
def handle_setup(self) -> None: """Create a response to the Echo's setup request.""" date_str = formatdate(timeval=None, localtime=False, usegmt=True) # Made as a separate string because it requires `len(setup_xml)` setup_xml = ( '<?xml version="1.0"?>' '<root>' '<device>' '<deviceType>urn:Fauxmo:device:controllee:1</deviceType>' f'<friendlyName>{self.name}</friendlyName>' '<manufacturer>Belkin International Inc.</manufacturer>' '<modelName>Emulated Socket</modelName>' '<modelNumber>3.1415</modelNumber>' f'<UDN>uuid:Socket-1_0-{self.serial}</UDN>' '</device>' '</root>' ) setup_response = '\n'.join([ 'HTTP/1.1 200 OK', f'CONTENT-LENGTH: {len(setup_xml)}', 'CONTENT-TYPE: text/xml', f'DATE: {date_str}', 'LAST-MODIFIED: Sat, 01 Jan 2000 00:01:15 GMT', 'SERVER: Unspecified, UPnP/1.0, Unspecified', 'X-User-Agent: Fauxmo', 'CONNECTION: close\n', f"{setup_xml}" ]) logger.debug(f"Fauxmo response to setup request:\n{setup_response}") self.transport.write(setup_response.encode()) self.transport.close()
def handle_metainfo(self) -> None: """Respond to request for metadata.""" metainfo_xml = ('<scpd xmlns="urn:Belkin:service-1-0">' "<specVersion>" "<major>1</major>" "<minor>0</minor>" "</specVersion>" "<actionList>" "<action>" "<name>GetMetaInfo</name>" "<argumentList>" "<retval />" "<name>GetMetaInfo</name>" "<relatedStateVariable>MetaInfo</relatedStateVariable>" "<direction>in</direction>" "</argumentList>" "</action>" "</actionList>" "<serviceStateTable>" '<stateVariable sendEvents="yes">' "<name>MetaInfo</name>" "<dataType>string</dataType>" "<defaultValue>0</defaultValue>" "</stateVariable>" "</serviceStateTable>" "</scpd>") + 2 * Fauxmo.NEWLINE meta_response = self.add_http_headers(metainfo_xml) logger.debug(f"Fauxmo response to setup request:\n{meta_response}") self.transport.write(meta_response.encode()) self.transport.close()
def handle_action(self, msg): """Execute `on` or `off` method of `action_handler` Args: msg (str): Body of the Echo's HTTP request to trigger an action """ success = False if '<BinaryState>0</BinaryState>' in msg: # `off()` method called success = self.action_handler.off() elif '<BinaryState>1</BinaryState>' in msg: # `on()` method called success = self.action_handler.on() else: logger.debug("Unrecognized request:\n{}".format(msg)) if success: date_str = formatdate(timeval=None, localtime=False, usegmt=True) response = '\r\n'.join([ 'HTTP/1.1 200 OK', 'CONTENT-LENGTH: 0', 'CONTENT-TYPE: text/xml charset="utf-8"', 'DATE: {}'.format(date_str), 'EXT:', 'SERVER: Unspecified, UPnP/1.0, Unspecified', 'X-User-Agent: Fauxmo', 'CONNECTION: close' ]) + 2 * '\r\n' logger.debug(response) self.transport.write(response.encode()) self.transport.close()
def handle_metainfo(self) -> None: """Respond to request for metadata.""" metainfo = ('<scpd xmlns="urn:Belkin:service-1-0">' '<specVersion>' '<major>1</major>' '<minor>0</minor>' '</specVersion>' '<actionList>' '<action>' '<name>GetMetaInfo</name>' '<argumentList>' '<retval />' '<name>GetMetaInfo</name>' '<relatedStateVariable>MetaInfo</relatedStateVariable>' '<direction>in</direction>' '</argumentList>' '</action>' '</actionList>' '<serviceStateTable>' '<stateVariable sendEvents="yes">' '<name>MetaInfo</name>' '<dataType>string</dataType>' '<defaultValue>0</defaultValue>' '</stateVariable>' '</serviceStateTable>' '</scpd>\r\n\r\n') logger.debug(f"Fauxmo response to setup request:\n{metainfo}") self.transport.write(metainfo.encode()) self.transport.close()
async def _send_async_response(self, response: bytes, addr: Tuple[str, int], mx: float = 0.) -> None: logger.debug(f"Sending response to {addr} with mx {mx}:\n{response}") asyncio.sleep(random.random() * max(0, min(5, mx))) self.transport.sendto(response, addr)
def respond_to_search(self, addr): """Build and send an appropriate response to an SSDP search request.""" date_str = formatdate(timeval=None, localtime=False, usegmt=True) for device in self.devices: name = device.get('name') ip_address = device.get('ip_address') port = device.get('port') location = 'http://{}:{}/setup.xml'.format(ip_address, port) serial = make_serial(name) response = '\r\n'.join([ 'HTTP/1.1 200 OK', 'CACHE-CONTROL: max-age=86400', 'DATE: {}'.format(date_str), 'EXT:', 'LOCATION: {}'.format(location), 'OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01', '01-NLS: {}'.format(uuid.uuid4()), 'SERVER: Unspecified, UPnP/1.0, Unspecified', 'ST: urn:Belkin:device:**', 'USN: uuid:Socket-1_0-{}::urn:Belkin:device:**'.format(serial) ]) + 2 * '\r\n' logger.debug("Sending response to {}:\n{}".format(addr, response)) self.transport.sendto(response.encode(), addr)
def connection_made(self, transport: asyncio.BaseTransport) -> None: """Accept an incoming TCP connection. Args: transport: Passed in asyncio.Transport """ peername = transport.get_extra_info('peername') logger.debug(f"Connection made with: {peername}") self.transport = cast(asyncio.Transport, transport)
def datagram_received(self, data, addr): """Check incoming UDP data for requests for Wemo devices""" logger.debug("Received data below from {}:".format(addr)) logger.debug(data) if all(b in data for b in [b'"ssdp:discover"', b'urn:Belkin:device:**']): self.respond_to_search(addr)
def data_received(self, data): """Decode data and determine if it is a setup or action request""" msg = data.decode() logger.debug("Received message:\n{}".format(msg)) if msg.startswith('GET /setup.xml HTTP/1.1'): logger.debug("setup.xml requested by Echo") self.handle_setup() elif msg.startswith('POST /upnp/control/basicevent1 HTTP/1.1'): self.handle_action(msg)
def handle_setup(self) -> None: """Create a response to the Echo's setup request.""" date_str = formatdate(timeval=None, localtime=False, usegmt=True) # Made as a separate string because it requires `len(setup_xml)` setup_xml = ( '<?xml version="1.0"?>' '<root>' '<specVersion><major>1</major><minor>0</minor></specVersion>' '<device>' '<deviceType>urn:Belkin:device:controllee:1</deviceType>' f'<friendlyName>{self.name}</friendlyName>' '<manufacturer>Belkin International Inc.</manufacturer>' '<modelName>Emulated Socket</modelName>' '<modelNumber>3.1415</modelNumber>' f'<UDN>uuid:Socket-1_0-{self.serial}</UDN>' '<serviceList>' '<service>' '<serviceType>urn:Belkin:service:basicevent:1</serviceType>' '<serviceId>urn:Belkin:serviceId:basicevent1</serviceId>' '<controlURL>/upnp/control/basicevent1</controlURL>' '<eventSubURL>/upnp/event/basicevent1</eventSubURL>' '<SCPDURL>/eventservice.xml</SCPDURL>' '</service>' '<service>' '<serviceType>urn:Belkin:service:metainfo:1</serviceType>' '<serviceId>urn:Belkin:serviceId:metainfo1</serviceId>' '<controlURL>/upnp/control/metainfo1</controlURL>' '<eventSubURL>/upnp/event/metainfo1</eventSubURL>' '<SCPDURL>/metainfoservice.xml</SCPDURL>' '</service>' '</serviceList>' '</device>' '</root>' ) setup_response = (Fauxmo.NEWLINE).join([ 'HTTP/1.1 200 OK', f'CONTENT-LENGTH: {len(setup_xml)}', 'CONTENT-TYPE: text/xml', f'DATE: {date_str}', 'LAST-MODIFIED: Sat, 01 Jan 2000 00:01:15 GMT', 'SERVER: Unspecified, UPnP/1.0, Unspecified', 'X-User-Agent: Fauxmo', f'CONNECTION: close{Fauxmo.NEWLINE}', f"{setup_xml}" ]) logger.debug(f"Fauxmo response to setup request:\n{setup_response}") self.transport.write(setup_response.encode()) self.transport.close()
def datagram_received(self, data: AnyStr, # type: ignore addr: Tuple[str, int]) -> None: """Check incoming UDP data for requests for Wemo devices. Args: data: Incoming data content addr: Address sending data """ logger.debug(f"Received data below from {addr}:") logger.debug(str(data)) if all(b in data for b in [b'"ssdp:discover"', b'urn:Belkin:device:**']): self.respond_to_search(addr)
def data_received(self, data: bytes) -> None: """Decode incoming data. Args: data: Incoming message, either setup request or action request """ msg = data.decode() logger.debug(f"Received message:\n{msg}") if msg.startswith('GET /setup.xml HTTP/1.1'): logger.info("setup.xml requested by Echo") self.handle_setup() elif msg.startswith('POST /upnp/control/basicevent1 HTTP/1.1'): self.handle_action(msg)
def handle_event(self) -> None: """Respond to request for eventservice.xml.""" eventservice_xml = ( '<scpd xmlns="urn:Belkin:service-1-0">' "<actionList>" "<action>" "<name>SetBinaryState</name>" "<argumentList>" "<argument>" "<retval/>" "<name>BinaryState</name>" "<relatedStateVariable>BinaryState</relatedStateVariable>" "<direction>in</direction>" "</argument>" "</argumentList>" "</action>" "<action>" "<name>GetBinaryState</name>" "<argumentList>" "<argument>" "<retval/>" "<name>BinaryState</name>" "<relatedStateVariable>BinaryState</relatedStateVariable>" "<direction>out</direction>" "</argument>" "</argumentList>" "</action>" "</actionList>" "<serviceStateTable>" '<stateVariable sendEvents="yes">' "<name>BinaryState</name>" "<dataType>Boolean</dataType>" "<defaultValue>0</defaultValue>" "</stateVariable>" '<stateVariable sendEvents="yes">' "<name>level</name>" "<dataType>string</dataType>" "<defaultValue>0</defaultValue>" "</stateVariable>" "</serviceStateTable>" "</scpd>" ) + 2 * Fauxmo.NEWLINE event_response = self.add_http_headers(eventservice_xml) logger.debug(f"Fauxmo response to setup request:\n{event_response}") self.transport.write(event_response.encode()) self.transport.close()
def handle_event(self) -> None: """Respond to request for eventservice.xml.""" eventservice_xml = ( '<scpd xmlns="urn:Belkin:service-1-0">' '<actionList>' '<action>' '<name>SetBinaryState</name>' '<argumentList>' '<argument>' '<retval/>' '<name>BinaryState</name>' '<relatedStateVariable>BinaryState</relatedStateVariable>' '<direction>in</direction>' '</argument>' '</argumentList>' '</action>' '<action>' '<name>GetBinaryState</name>' '<argumentList>' '<argument>' '<retval/>' '<name>BinaryState</name>' '<relatedStateVariable>BinaryState</relatedStateVariable>' '<direction>out</direction>' '</argument>' '</argumentList>' '</action>' '</actionList>' '<serviceStateTable>' '<stateVariable sendEvents="yes">' '<name>BinaryState</name>' '<dataType>Boolean</dataType>' '<defaultValue>0</defaultValue>' '</stateVariable>' '<stateVariable sendEvents="yes">' '<name>level</name>' '<dataType>string</dataType>' '<defaultValue>0</defaultValue>' '</stateVariable>' '</serviceStateTable>' '</scpd>\r\n\r\n') logger.debug(f"Fauxmo response to setup request:\n{eventservice_xml}") self.transport.write(eventservice_xml.encode()) self.transport.close()
def handle_setup(self) -> None: """Create a response to the Echo's setup request.""" setup_xml = ( '<?xml version="1.0"?>' "<root>" "<specVersion><major>1</major><minor>0</minor></specVersion>" "<device>" "<deviceType>urn:Belkin:device:controllee:1</deviceType>" f"<friendlyName>{self.name}</friendlyName>" "<manufacturer>Belkin International Inc.</manufacturer>" "<modelName>Emulated Socket</modelName>" "<modelNumber>3.1415</modelNumber>" f"<UDN>uuid:Socket-1_0-{self.serial}</UDN>" "<serviceList>" "<service>" "<serviceType>urn:Belkin:service:basicevent:1</serviceType>" "<serviceId>urn:Belkin:serviceId:basicevent1</serviceId>" "<controlURL>/upnp/control/basicevent1</controlURL>" "<eventSubURL>/upnp/event/basicevent1</eventSubURL>" "<SCPDURL>/eventservice.xml</SCPDURL>" "</service>" "<service>" "<serviceType>urn:Belkin:service:metainfo:1</serviceType>" "<serviceId>urn:Belkin:serviceId:metainfo1</serviceId>" "<controlURL>/upnp/control/metainfo1</controlURL>" "<eventSubURL>/upnp/event/metainfo1</eventSubURL>" "<SCPDURL>/metainfoservice.xml</SCPDURL>" "</service>" "<service>" "<serviceType>urn:Belkin:service:remoteaccess:1</serviceType>" "<serviceId>urn:Belkin:serviceId:remoteaccess1</serviceId>" "<controlURL>/upnp/control/remoteaccess1</controlURL>" "<eventSubURL>/upnp/event/remoteaccess1</eventSubURL>" "<SCPDURL>/remoteaccess.xml</SCPDURL>" "</service>" "</serviceList>" "</device>" "</root>") setup_response = self.add_http_headers(setup_xml) logger.debug(f"Fauxmo response to setup request:\n{setup_response}") self.transport.write(setup_response.encode()) self.transport.close()
def get_local_ip(ip_address): """Attempt to get the local network-connected IP address""" if ip_address is None or ip_address.lower() == "auto": logger.debug("Attempting to get IP address automatically") hostname = socket.gethostname() ip_address = socket.gethostbyname(hostname) # Workaround for Linux returning localhost # See: SO question #166506 by @UnkwnTech if ip_address in ['127.0.1.1', '127.0.0.1', 'localhost']: tempsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) tempsock.connect(('8.8.8.8', 0)) ip_address = tempsock.getsockname()[0] tempsock.close() logger.debug("Using IP address: {}".format(ip_address)) return ip_address
def data_received(self, data: bytes) -> None: """Decode incoming data. Args: data: Incoming message, either setup request or action request """ msg = data.decode() logger.debug(f"Received message:\n{msg}") if msg.startswith("GET /setup.xml HTTP/1.1"): logger.info("setup.xml requested by Echo") self.handle_setup() elif "/eventservice.xml" in msg: logger.info("eventservice.xml request by Echo") self.handle_event() elif "/metainfoservice.xml" in msg: logger.info("metainfoservice.xml request by Echo") self.handle_metainfo() elif msg.startswith("POST /upnp/control/basicevent1 HTTP/1.1"): logger.info("request BasicEvent1") self.handle_action(msg)
def datagram_received(self, data_: AnyStr, addr: Tuple[str, int]) -> None: """Check incoming UDP data for requests for Wemo devices. #TODO data_ is a workaround for AnyStr issue with casting (see https://github.com/python/typeshed/issues/439). If https://github.com/python/typeshed/pull/1819 is merged, fix this. Args: data_: Incoming data content addr: Address sending data """ if isinstance(data_, bytes): data = data_.decode('utf8') else: data = data_ logger.debug(f"Received data below from {addr}:") logger.debug(data) discover_patterns = [ 'ST: urn:Belkin:device:**', 'ST: upnp:rootdevice', 'ST: ssdp:all', ] discover_pattern = next( (pattern for pattern in discover_patterns if pattern in data), None) if 'MAN: "ssdp:discover"' in data and discover_pattern: mx = 0. mx_line = next((line for line in str(data).splitlines() if line.startswith("MX: ")), None) if mx_line: mx_str = mx_line.split()[-1] if mx_str.replace('.', '', 1).isnumeric(): mx = float(mx_str) self.respond_to_search(addr, discover_pattern, mx)
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()
def connection_made(self, transport): peername = transport.get_extra_info('peername') logger.debug("Connection made with: {}".format(peername)) self.transport = transport
def handle_action(self, msg: str) -> None: """Execute `on`, `off`, or `get_state` method of plugin. Args: msg: Body of the Echo's HTTP request to trigger an action """ logger.debug(f"Handling action for plugin type {self.plugin}") success = False soap_format = ( '<s:Envelope ' 'xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" ' 's:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' '<s:Body>' '<u:{action}BinaryStateResponse ' 'xmlns:u="urn:Belkin:service:basicevent:1">' '<BinaryState>{state_int}</BinaryState>' '</u:{action}BinaryStateResponse>' '</s:Body>' '</s:Envelope>').format command_format = ( 'SOAPACTION: "urn:Belkin:service:basicevent:1#{}BinaryState"' ).format soap_message: str = None action: str = None state_int: int = None if command_format("Get") in msg: logger.info(f"Attempting to get state for {self.plugin.name}") action = "Get" try: state = self.plugin.get_state() except AttributeError: logger.warning(f"Plugin {self.plugin.__module__} has not " "implemented a `get_state` method.") else: logger.info(f"{self.plugin.name} state: {state}") success = True state_int = int(state.lower() == "on") elif command_format("Set") in msg: action = "Set" if '<BinaryState>0</BinaryState>' in msg: logger.info(f"Attempting to turn off {self.plugin.name}") state_int = 0 success = self.plugin.off() elif '<BinaryState>1</BinaryState>' in msg: logger.info(f"Attempting to turn on {self.plugin.name}") state_int = 1 success = self.plugin.on() else: logger.warning(f"Unrecognized request:\n{msg}") if success: date_str = formatdate(timeval=None, localtime=False, usegmt=True) soap_message = soap_format(action=action, state_int=state_int) response = '\n'.join([ 'HTTP/1.1 200 OK', f'CONTENT-LENGTH: {len(soap_message)}', 'CONTENT-TYPE: text/xml charset="utf-8"', f'DATE: {date_str}', 'EXT:', 'SERVER: Unspecified, UPnP/1.0, Unspecified', 'X-User-Agent: Fauxmo', 'CONNECTION: close\n', f'{soap_message}', ]) logger.debug(response) self.transport.write(response.encode()) else: errmsg = (f"Unable to complete command for {self.plugin.name}:\n" f"{msg}") logger.warning(errmsg) self.transport.close()
def handle_action(self, msg: str) -> None: """Execute `on`, `off`, or `get_state` method of plugin. Args: msg: Body of the Echo's HTTP request to trigger an action """ logger.debug(f"Handling action for plugin type {self.plugin}") soap_format = ( "<s:Envelope " 'xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" ' 's:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' "<s:Body>" "<u:{action}{action_type}Response " 'xmlns:u="urn:Belkin:service:basicevent:1">' "<{action_type}>{return_val}</{action_type}>" "</u:{action}{action_type}Response>" "</s:Body>" "</s:Envelope>").format command_format = ( 'SOAPACTION: "urn:Belkin:service:basicevent:1#{}"').format soap_message: str = None action: str = None action_type: str = None return_val: str = None success: bool = False if command_format("GetBinaryState").casefold() in msg.casefold(): logger.info(f"Attempting to get state for {self.plugin.name}") action = "Get" action_type = "BinaryState" state = self.plugin.get_state().casefold() logger.info(f"{self.plugin.name} state: {state}") if state in ["off", "on"]: success = True return_val = str(int(state.lower() == "on")) elif command_format("SetBinaryState").casefold() in msg.casefold(): action = "Set" action_type = "BinaryState" if "<BinaryState>0</BinaryState>" in msg: logger.info(f"Attempting to turn off {self.plugin.name}") return_val = "0" success = self.plugin.off() elif "<BinaryState>1</BinaryState>" in msg: logger.info(f"Attempting to turn on {self.plugin.name}") return_val = "1" success = self.plugin.on() else: logger.warning(f"Unrecognized request:\n{msg}") elif command_format("GetFriendlyName").casefold() in msg.casefold(): action = "Get" action_type = "FriendlyName" return_val = self.plugin.name success = True logger.info(f"{self.plugin.name} returning friendly name") if success: soap_message = soap_format(action=action, action_type=action_type, return_val=return_val) response = self.add_http_headers(soap_message) logger.debug(response) self.transport.write(response.encode()) else: errmsg = ( f"Unable to complete command for {self.plugin.name}:\n{msg}") logger.warning(errmsg) self.transport.close()