Example #1
0
File: utils.py Project: wlky/fauxmo
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
Example #2
0
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
Example #3
0
    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)
Example #4
0
    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)
Example #5
0
    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()
Example #6
0
    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()
Example #7
0
    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()
Example #8
0
 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()
Example #9
0
 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)
Example #10
0
    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)
Example #11
0
    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)
Example #12
0
    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)
Example #13
0
    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)
Example #14
0
    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()
Example #15
0
    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)
Example #16
0
    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)
Example #17
0
    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()
Example #18
0
    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()
Example #19
0
    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()
Example #20
0
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
Example #21
0
    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)
Example #22
0
    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)
Example #23
0
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()
Example #24
0
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()
Example #25
0
 def connection_made(self, transport):
     peername = transport.get_extra_info('peername')
     logger.debug("Connection made with: {}".format(peername))
     self.transport = transport
Example #26
0
    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()
Example #27
0
    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()