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))
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)
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)
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({})
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)
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", } }])
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)
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}}])
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"))
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")
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)