Ejemplo n.º 1
0
    def get(self, request, username):
        """Process a request to get the list of available lights."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("Only local IPs allowed",
                                     HTTP_UNAUTHORIZED)

        return self.json(create_list_of_entities(self.config, request))
Ejemplo n.º 2
0
def async_get_external_url(opp: OpenPeerPower) -> Optional[str]:
    """Get external url of this instance.

    Note: currently it takes 30 seconds after Open Peer Power starts for
    cloud.async_remote_ui_url to work.
    """
    if "cloud" in opp.config.components:
        try:
            return cast(str, opp.components.cloud.async_remote_ui_url())
        except opp.components.cloud.CloudNotAvailable:
            pass

    if opp.config.api is None:
        return None

    base_url = yarl.URL(opp.config.api.base_url)

    try:
        if is_local(ip_address(base_url.host)):
            return None
    except ValueError:
        # ip_address raises ValueError if host is not an IP address
        pass

    return str(base_url)
Ejemplo n.º 3
0
    def get(self, request, username, entity_id):
        """Process a request to get the state of an individual light."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("Only local IPs allowed",
                                     HTTP_UNAUTHORIZED)

        opp = request.app["opp"]
        opp_entity_id = self.config.number_to_entity_id(entity_id)

        if opp_entity_id is None:
            _LOGGER.error(
                "Unknown entity number: %s not found in emulated_hue_ids.json",
                entity_id,
            )
            return self.json_message("Entity not found", HTTP_NOT_FOUND)

        entity = opp.states.get(opp_entity_id)

        if entity is None:
            _LOGGER.error("Entity not found: %s", opp_entity_id)
            return self.json_message("Entity not found", HTTP_NOT_FOUND)

        if not self.config.is_entity_exposed(entity):
            _LOGGER.error("Entity not exposed: %s", entity_id)
            return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED)

        json_response = entity_to_json(self.config, entity)

        return self.json(json_response)
Ejemplo n.º 4
0
    def get(self, request, username):
        """Process a request to make the Brilliant Lightpad work."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("Only local IPs allowed",
                                     HTTP_UNAUTHORIZED)

        return self.json({})
Ejemplo n.º 5
0
    def get(self, request, username=""):
        """Process a request to get the configuration."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("only local IPs allowed",
                                     HTTP_UNAUTHORIZED)

        json_response = create_config_model(self.config, request)

        return self.json(json_response)
Ejemplo n.º 6
0
    def put(self, request, username):
        """Process a request to make the Logitech Pop working."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("Only local IPs allowed",
                                     HTTP_UNAUTHORIZED)

        return self.json([{
            "error": {
                "address": "/groups/0/action/scene",
                "type": 7,
                "description": "invalid value, dummy for parameter, scene",
            }
        }])
Ejemplo n.º 7
0
    def get(self, request, username):
        """Process a request to get the list of available lights."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("only local IPs allowed",
                                     HTTP_UNAUTHORIZED)
        if username != HUE_API_USERNAME:
            return self.json(UNAUTHORIZED_USER)

        json_response = {
            "lights": create_list_of_entities(self.config, request),
            "config": create_config_model(self.config, request),
        }

        return self.json(json_response)
Ejemplo n.º 8
0
    async def post(self, request):
        """Handle a POST request."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("Only local IPs allowed",
                                     HTTP_UNAUTHORIZED)

        try:
            data = await request.json()
        except ValueError:
            return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)

        if "devicetype" not in data:
            return self.json_message("devicetype not specified",
                                     HTTP_BAD_REQUEST)

        return self.json([{"success": {"username": HUE_API_USERNAME}}])
Ejemplo n.º 9
0
def test_is_local():
    """Test local addresses."""
    assert network_util.is_local(ip_address("192.168.0.1"))
    assert network_util.is_local(ip_address("127.0.0.1"))
    assert not network_util.is_local(ip_address("208.5.4.2"))
Ejemplo n.º 10
0
def _parse_client_id(client_id):
    """Test if client id is a valid URL according to IndieAuth section 3.2.

    https://indieauth.spec.indieweb.org/#client-identifier
    """
    parts = _parse_url(client_id)

    # Client identifier URLs
    # MUST have either an https or http scheme
    if parts.scheme not in ("http", "https"):
        raise ValueError()

    # MUST contain a path component
    # Handled by url canonicalization.

    # MUST NOT contain single-dot or double-dot path segments
    if any(segment in (".", "..") for segment in parts.path.split("/")):
        raise ValueError(
            "Client ID cannot contain single-dot or double-dot path segments")

    # MUST NOT contain a fragment component
    if parts.fragment != "":
        raise ValueError("Client ID cannot contain a fragment")

    # MUST NOT contain a username or password component
    if parts.username is not None:
        raise ValueError("Client ID cannot contain username")

    if parts.password is not None:
        raise ValueError("Client ID cannot contain password")

    # MAY contain a port
    try:
        # parts raises ValueError when port cannot be parsed as int
        parts.port
    except ValueError as ex:
        raise ValueError("Client ID contains invalid port") from ex

    # Additionally, hostnames
    # MUST be domain names or a loopback interface and
    # MUST NOT be IPv4 or IPv6 addresses except for IPv4 127.0.0.1
    # or IPv6 [::1]

    # We are not goint to follow the spec here. We are going to allow
    # any internal network IP to be used inside a client id.

    address = None

    try:
        netloc = parts.netloc

        # Strip the [, ] from ipv6 addresses before parsing
        if netloc[0] == "[" and netloc[-1] == "]":
            netloc = netloc[1:-1]

        address = ip_address(netloc)
    except ValueError:
        # Not an ip address
        pass

    if address is None or is_local(address):
        return parts

    raise ValueError("Hostname should be a domain name or local IP address")
Ejemplo n.º 11
0
    async def put(self, request, username, entity_number):  # noqa: C901
        """Process a request to set the state of an individual light."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("Only local IPs allowed",
                                     HTTP_UNAUTHORIZED)

        config = self.config
        opp = request.app["opp"]
        entity_id = config.number_to_entity_id(entity_number)

        if entity_id is None:
            _LOGGER.error("Unknown entity number: %s", entity_number)
            return self.json_message("Entity not found", HTTP_NOT_FOUND)

        entity = opp.states.get(entity_id)

        if entity is None:
            _LOGGER.error("Entity not found: %s", entity_id)
            return self.json_message("Entity not found", HTTP_NOT_FOUND)

        if not config.is_entity_exposed(entity):
            _LOGGER.error("Entity not exposed: %s", entity_id)
            return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED)

        try:
            request_json = await request.json()
        except ValueError:
            _LOGGER.error("Received invalid json")
            return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)

        # Get the entity's supported features
        entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
        if entity.domain == light.DOMAIN:
            color_modes = entity.attributes.get(
                light.ATTR_SUPPORTED_COLOR_MODES, [])

        # Parse the request
        parsed = {
            STATE_ON: False,
            STATE_BRIGHTNESS: None,
            STATE_HUE: None,
            STATE_SATURATION: None,
            STATE_COLOR_TEMP: None,
            STATE_XY: None,
            STATE_TRANSITON: None,
        }

        if HUE_API_STATE_ON in request_json:
            if not isinstance(request_json[HUE_API_STATE_ON], bool):
                _LOGGER.error("Unable to parse data: %s", request_json)
                return self.json_message("Bad request", HTTP_BAD_REQUEST)
            parsed[STATE_ON] = request_json[HUE_API_STATE_ON]
        else:
            parsed[STATE_ON] = entity.state != STATE_OFF

        for (key, attr) in (
            (HUE_API_STATE_BRI, STATE_BRIGHTNESS),
            (HUE_API_STATE_HUE, STATE_HUE),
            (HUE_API_STATE_SAT, STATE_SATURATION),
            (HUE_API_STATE_CT, STATE_COLOR_TEMP),
            (HUE_API_STATE_TRANSITION, STATE_TRANSITON),
        ):
            if key in request_json:
                try:
                    parsed[attr] = int(request_json[key])
                except ValueError:
                    _LOGGER.error("Unable to parse data (2): %s", request_json)
                    return self.json_message("Bad request", HTTP_BAD_REQUEST)
        if HUE_API_STATE_XY in request_json:
            try:
                parsed[STATE_XY] = (
                    float(request_json[HUE_API_STATE_XY][0]),
                    float(request_json[HUE_API_STATE_XY][1]),
                )
            except ValueError:
                _LOGGER.error("Unable to parse data (2): %s", request_json)
                return self.json_message("Bad request", HTTP_BAD_REQUEST)

        if HUE_API_STATE_BRI in request_json:
            if entity.domain == light.DOMAIN:
                if light.brightness_supported(color_modes):
                    parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0
                else:
                    parsed[STATE_BRIGHTNESS] = None

            elif entity.domain == scene.DOMAIN:
                parsed[STATE_BRIGHTNESS] = None
                parsed[STATE_ON] = True

            elif entity.domain in [
                    script.DOMAIN,
                    media_player.DOMAIN,
                    fan.DOMAIN,
                    cover.DOMAIN,
                    climate.DOMAIN,
                    humidifier.DOMAIN,
            ]:
                # Convert 0-254 to 0-100
                level = (parsed[STATE_BRIGHTNESS] /
                         HUE_API_STATE_BRI_MAX) * 100
                parsed[STATE_BRIGHTNESS] = round(level)
                parsed[STATE_ON] = True

        # Choose general OPP domain
        domain = core.DOMAIN

        # Entity needs separate call to turn on
        turn_on_needed = False

        # Convert the resulting "on" status into the service we need to call
        service = SERVICE_TURN_ON if parsed[STATE_ON] else SERVICE_TURN_OFF

        # Construct what we need to send to the service
        data = {ATTR_ENTITY_ID: entity_id}

        # If the requested entity is a light, set the brightness, hue,
        # saturation and color temp
        if entity.domain == light.DOMAIN:
            if parsed[STATE_ON]:
                if (light.brightness_supported(color_modes)
                        and parsed[STATE_BRIGHTNESS] is not None):
                    data[ATTR_BRIGHTNESS] = hue_brightness_to_opp(
                        parsed[STATE_BRIGHTNESS])

                if light.color_supported(color_modes):
                    if any((parsed[STATE_HUE], parsed[STATE_SATURATION])):
                        if parsed[STATE_HUE] is not None:
                            hue = parsed[STATE_HUE]
                        else:
                            hue = 0

                        if parsed[STATE_SATURATION] is not None:
                            sat = parsed[STATE_SATURATION]
                        else:
                            sat = 0

                        # Convert hs values to opp hs values
                        hue = int((hue / HUE_API_STATE_HUE_MAX) * 360)
                        sat = int((sat / HUE_API_STATE_SAT_MAX) * 100)

                        data[ATTR_HS_COLOR] = (hue, sat)

                    if parsed[STATE_XY] is not None:
                        data[ATTR_XY_COLOR] = parsed[STATE_XY]

                if (light.color_temp_supported(color_modes)
                        and parsed[STATE_COLOR_TEMP] is not None):
                    data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP]

                if (entity_features & SUPPORT_TRANSITION
                        and parsed[STATE_TRANSITON] is not None):
                    data[ATTR_TRANSITION] = parsed[STATE_TRANSITON] / 10

        # If the requested entity is a script, add some variables
        elif entity.domain == script.DOMAIN:
            data["variables"] = {
                "requested_state": STATE_ON if parsed[STATE_ON] else STATE_OFF
            }

            if parsed[STATE_BRIGHTNESS] is not None:
                data["variables"]["requested_level"] = parsed[STATE_BRIGHTNESS]

        # If the requested entity is a climate, set the temperature
        elif entity.domain == climate.DOMAIN:
            # We don't support turning climate devices on or off,
            # only setting the temperature
            service = None

            if (entity_features & SUPPORT_TARGET_TEMPERATURE
                    and parsed[STATE_BRIGHTNESS] is not None):
                domain = entity.domain
                service = SERVICE_SET_TEMPERATURE
                data[ATTR_TEMPERATURE] = parsed[STATE_BRIGHTNESS]

        # If the requested entity is a humidifier, set the humidity
        elif entity.domain == humidifier.DOMAIN:
            if parsed[STATE_BRIGHTNESS] is not None:
                turn_on_needed = True
                domain = entity.domain
                service = SERVICE_SET_HUMIDITY
                data[ATTR_HUMIDITY] = parsed[STATE_BRIGHTNESS]

        # If the requested entity is a media player, convert to volume
        elif entity.domain == media_player.DOMAIN:
            if (entity_features & SUPPORT_VOLUME_SET
                    and parsed[STATE_BRIGHTNESS] is not None):
                turn_on_needed = True
                domain = entity.domain
                service = SERVICE_VOLUME_SET
                # Convert 0-100 to 0.0-1.0
                data[
                    ATTR_MEDIA_VOLUME_LEVEL] = parsed[STATE_BRIGHTNESS] / 100.0

        # If the requested entity is a cover, convert to open_cover/close_cover
        elif entity.domain == cover.DOMAIN:
            domain = entity.domain
            if service == SERVICE_TURN_ON:
                service = SERVICE_OPEN_COVER
            else:
                service = SERVICE_CLOSE_COVER

            if (entity_features & SUPPORT_SET_POSITION
                    and parsed[STATE_BRIGHTNESS] is not None):
                domain = entity.domain
                service = SERVICE_SET_COVER_POSITION
                data[ATTR_POSITION] = parsed[STATE_BRIGHTNESS]

        # If the requested entity is a fan, convert to speed
        elif (entity.domain == fan.DOMAIN
              and entity_features & SUPPORT_SET_SPEED
              and parsed[STATE_BRIGHTNESS] is not None):
            domain = entity.domain
            # Convert 0-100 to a fan speed
            brightness = parsed[STATE_BRIGHTNESS]
            if brightness == 0:
                data[ATTR_SPEED] = SPEED_OFF
            elif 0 < brightness <= 33.3:
                data[ATTR_SPEED] = SPEED_LOW
            elif 33.3 < brightness <= 66.6:
                data[ATTR_SPEED] = SPEED_MEDIUM
            elif 66.6 < brightness <= 100:
                data[ATTR_SPEED] = SPEED_HIGH

        # Map the off command to on
        if entity.domain in config.off_maps_to_on_domains:
            service = SERVICE_TURN_ON

        # Separate call to turn on needed
        if turn_on_needed:
            opp.async_create_task(
                opp.services.async_call(
                    core.DOMAIN,
                    SERVICE_TURN_ON,
                    {ATTR_ENTITY_ID: entity_id},
                    blocking=True,
                ))

        if service is not None:
            state_will_change = parsed[STATE_ON] != (entity.state != STATE_OFF)

            opp.async_create_task(
                opp.services.async_call(domain, service, data, blocking=True))

            if state_will_change:
                # Wait for the state to change.
                await wait_for_state_change_or_timeout(opp, entity_id,
                                                       STATE_CACHED_TIMEOUT)

        # Create success responses for all received keys
        json_response = [
            create_hue_success_response(entity_number, HUE_API_STATE_ON,
                                        parsed[STATE_ON])
        ]

        for (key, val) in (
            (STATE_BRIGHTNESS, HUE_API_STATE_BRI),
            (STATE_HUE, HUE_API_STATE_HUE),
            (STATE_SATURATION, HUE_API_STATE_SAT),
            (STATE_COLOR_TEMP, HUE_API_STATE_CT),
            (STATE_XY, HUE_API_STATE_XY),
            (STATE_TRANSITON, HUE_API_STATE_TRANSITION),
        ):
            if parsed[key] is not None:
                json_response.append(
                    create_hue_success_response(entity_number, val,
                                                parsed[key]))

        if entity.domain in config.off_maps_to_on_domains:
            # Caching is required because things like scripts and scenes won't
            # report as "off" to Alexa if an "off" command is received, because
            # they'll map to "on". Thus, instead of reporting its actual
            # status, we report what Alexa will want to see, which is the same
            # as the actual requested command.
            config.cached_states[entity_id] = [parsed, None]
        else:
            config.cached_states[entity_id] = [parsed, time.time()]

        return self.json(json_response)