async def get(self, request): """Get discovery information.""" opp = request.app["opp"] uuid = await opp.helpers.instance_id.async_get() system_info = await async_get_system_info(opp) data = { ATTR_UUID: uuid, ATTR_BASE_URL: None, ATTR_EXTERNAL_URL: None, ATTR_INTERNAL_URL: None, ATTR_LOCATION_NAME: opp.config.location_name, ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE], # always needs authentication ATTR_REQUIRES_API_PASSWORD: True, ATTR_VERSION: __version__, } with suppress(NoURLAvailableError): data["external_url"] = get_url(opp, allow_internal=False) with suppress(NoURLAvailableError): data["internal_url"] = get_url(opp, allow_external=False) # Set old base URL based on external or internal data["base_url"] = data["external_url"] or data["internal_url"] return self.json(data)
async def _async_register_opp_zc_service(opp: OpenPeerPower, aio_zc: HaAsyncZeroconf, uuid: str) -> None: # Get instance UUID valid_location_name = _truncate_location_name_to_valid( opp.config.location_name) params = { "location_name": valid_location_name, "uuid": uuid, "version": __version__, "external_url": "", "internal_url": "", # Old base URL, for backward compatibility "base_url": "", # Always needs authentication "requires_api_password": True, } # Get instance URL's with suppress(NoURLAvailableError): params["external_url"] = get_url(opp, allow_internal=False) with suppress(NoURLAvailableError): params["internal_url"] = get_url(opp, allow_external=False) # Set old base URL based on external or internal params["base_url"] = params["external_url"] or params["internal_url"] host_ip = util.get_local_ip() try: host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip) except OSError: host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip) _suppress_invalid_properties(params) info = ServiceInfo( ZEROCONF_TYPE, name=f"{valid_location_name}.{ZEROCONF_TYPE}", server=f"{uuid}.local.", addresses=[host_ip_pton], port=opp.http.server_port, properties=params, ) _LOGGER.info("Starting Zeroconf broadcast") try: await aio_zc.async_register_service(info) except NonUniqueNameException: _LOGGER.error( "Open Peer Power instance with identical name present in the local network" )
async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" # Handle media_source if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media( self.opp, media_id) media_type = sourced_media.mime_type media_id = sourced_media.url # If media ID is a relative URL, we serve it from HA. # Create a signed path. if media_id[0] == "/": # Sign URL with Open Peer Power Cast User config_entry_id = self.registry_entry.config_entry_id config_entry = self.opp.config_entries.async_get_entry( config_entry_id) user_id = config_entry.data["user_id"] user = await self.opp.auth.async_get_user(user_id) if user.refresh_tokens: refresh_token: RefreshToken = list( user.refresh_tokens.values())[0] media_id = async_sign_path( self.opp, refresh_token.id, quote(media_id), timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME), ) # prepend external URL hass_url = get_url(self.opp, prefer_external=True) media_id = f"{hass_url}{media_id}" await self.opp.async_add_executor_job( ft.partial(self.play_media, media_type, media_id, **kwargs))
async def post(self, request: web.Request) -> web.Response: """Generate speech and provide url.""" try: data = await request.json() except ValueError: return self.json_message("Invalid JSON specified", HTTP_BAD_REQUEST) if not data.get(ATTR_PLATFORM) and data.get(ATTR_MESSAGE): return self.json_message("Must specify platform and message", HTTP_BAD_REQUEST) p_type = data[ATTR_PLATFORM] message = data[ATTR_MESSAGE] cache = data.get(ATTR_CACHE) language = data.get(ATTR_LANGUAGE) options = data.get(ATTR_OPTIONS) try: path = await self.tts.async_get_url_path(p_type, message, cache=cache, language=language, options=options) except OpenPeerPowerError as err: _LOGGER.error("Error on init tts: %s", err) return self.json({"error": err}, HTTP_BAD_REQUEST) base = self.tts.base_url or get_url(self.tts.opp) url = base + path return self.json({"url": url, "path": path})
async def async_say_handle(service): """Service handle for say.""" entity_ids = service.data[ATTR_ENTITY_ID] message = service.data.get(ATTR_MESSAGE) cache = service.data.get(ATTR_CACHE) language = service.data.get(ATTR_LANGUAGE) options = service.data.get(ATTR_OPTIONS) try: url = await tts.async_get_url_path(p_type, message, cache=cache, language=language, options=options) except OpenPeerPowerError as err: _LOGGER.error("Error on init TTS: %s", err) return base = tts.base_url or get_url(opp) url = base + url data = { ATTR_MEDIA_CONTENT_ID: url, ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, ATTR_ENTITY_ID: entity_ids, } await opp.services.async_call( DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True, context=service.context, )
def new_media_status(self, media_status): """Handle updates of the media status.""" if (media_status and media_status.player_is_idle and media_status.idle_reason == "ERROR"): external_url = None internal_url = None tts_base_url = None url_description = "" if "tts" in self.opp.config.components: with suppress(KeyError): # base_url not configured tts_base_url = self.opp.components.tts.get_base_url( self.opp) with suppress(NoURLAvailableError): # external_url not configured external_url = get_url(self.opp, allow_internal=False) with suppress(NoURLAvailableError): # internal_url not configured internal_url = get_url(self.opp, allow_external=False) if media_status.content_id: if tts_base_url and media_status.content_id.startswith( tts_base_url): url_description = f" from tts.base_url ({tts_base_url})" if external_url and media_status.content_id.startswith( external_url): url_description = f" from external_url ({external_url})" if internal_url and media_status.content_id.startswith( internal_url): url_description = f" from internal_url ({internal_url})" _LOGGER.error( "Failed to cast media %s%s. Please make sure the URL is: " "Reachable from the cast device and either a publicly resolvable " "hostname or an IP address", media_status.content_id, url_description, ) self.media_status = media_status self.media_status_received = dt_util.utcnow() self.schedule_update_op_state()
def register_events(self, opp): """Register events on device.""" # Get the URL of this server opp_url = get_url(opp) # Override url if another is specified in the configuration if self.custom_url is not None: opp_url = self.custom_url for event in self.doorstation_events: self._register_event(opp_url, event) _LOGGER.info("Successfully registered URL for %s on %s", event, self.name)
async def async_setup_platform(opp, config): """Set up the Telegram webhooks platform.""" bot = initialize_bot(config) current_status = await opp.async_add_executor_job(bot.getWebhookInfo) base_url = config.get(CONF_URL, get_url(opp, require_ssl=True, allow_internal=False)) # Some logging of Bot current status: last_error_date = getattr(current_status, "last_error_date", None) if (last_error_date is not None) and (isinstance(last_error_date, int)): last_error_date = dt.datetime.fromtimestamp(last_error_date) _LOGGER.info( "Telegram webhook last_error_date: %s. Status: %s", last_error_date, current_status, ) else: _LOGGER.debug("telegram webhook Status: %s", current_status) handler_url = f"{base_url}{TELEGRAM_HANDLER_URL}" if not handler_url.startswith("https"): _LOGGER.error("Invalid telegram webhook %s must be https", handler_url) return False def _try_to_set_webhook(): retry_num = 0 while retry_num < 3: try: return bot.setWebhook(handler_url, timeout=5) except TimedOut: retry_num += 1 _LOGGER.warning("Timeout trying to set webhook (retry #%d)", retry_num) if current_status and current_status["url"] != handler_url: result = await opp.async_add_executor_job(_try_to_set_webhook) if result: _LOGGER.info("Set new telegram webhook %s", handler_url) else: _LOGGER.error("Set telegram webhook failed %s", handler_url) return False opp.bus.async_listen_once(EVENT_OPENPEERPOWER_STOP, lambda event: bot.setWebhook(REMOVE_HANDLER_URL)) opp.http.register_view( BotPushReceiver(opp, config[CONF_ALLOWED_CHAT_IDS], config[CONF_TRUSTED_NETWORKS])) return True
def async_desired_settings_payload(self): """Return a dict representing the desired device configuration.""" # keeping self.opp.data check for backwards compatibility # newly configured integrations store this in the config entry desired_api_host = self.options.get(CONF_API_HOST) or ( self.opp.data[DOMAIN].get(CONF_API_HOST) or get_url(self.opp) ) desired_api_endpoint = desired_api_host + ENDPOINT_ROOT return { "sensors": self.async_binary_sensor_configuration(), "actuators": self.async_actuator_configuration(), "dht_sensors": self.async_dht_sensor_configuration(), "ds18b20_sensors": self.async_ds18b20_sensor_configuration(), "auth_token": self.config.get(CONF_ACCESS_TOKEN), "endpoint": desired_api_endpoint, "blink": self.options.get(CONF_BLINK, True), "discovery": self.options.get(CONF_DISCOVERY, True), }
async def handle_show_view(call: core.ServiceCall): """Handle a Show View service call.""" hass_url = get_url(opp, require_ssl=True, prefer_external=True) controller = HomeAssistantController( # If you are developing Open Peer Power Cast, uncomment and set to your dev app id. # app_id="5FE44367", hass_url=hass_url, client_id=None, refresh_token=refresh_token.token, ) dispatcher.async_dispatcher_send( opp, SIGNAL_OPP_CAST_SHOW_VIEW, controller, call.data[ATTR_ENTITY_ID], call.data[ATTR_VIEW_PATH], call.data.get(ATTR_URL_PATH), )
async def _configure_almond_for_ha(opp: OpenPeerPower, entry: config_entries.ConfigEntry, api: WebAlmondAPI): """Configure Almond to connect to HA.""" try: if entry.data["type"] == TYPE_OAUTH2: # If we're connecting over OAuth2, we will only set up connection # with Open Peer Power if we're remotely accessible. opp_url = network.get_url(opp, allow_internal=False, prefer_cloud=True) else: opp_url = network.get_url(opp) except network.NoURLAvailableError: # If no URL is available, we're not going to configure Almond to connect to HA. return _LOGGER.debug("Configuring Almond to connect to Open Peer Power at %s", opp_url) store = storage.Store(opp, STORAGE_VERSION, STORAGE_KEY) data = await store.async_load() if data is None: data = {} user = None if "almond_user" in data: user = await opp.auth.async_get_user(data["almond_user"]) if user is None: user = await opp.auth.async_create_system_user("Almond", [GROUP_ID_ADMIN]) data["almond_user"] = user.id await store.async_save(data) refresh_token = await opp.auth.async_create_refresh_token( user, # Almond will be fine as long as we restart once every 5 years access_token_expiration=timedelta(days=365 * 5), ) # Create long lived access token access_token = opp.auth.async_create_access_token(refresh_token) # Store token in Almond try: with async_timeout.timeout(30): await api.async_create_device({ "kind": "io.openpeerpower", "oppUrl": opp_url, "accessToken": access_token, "refreshToken": "", # 5 years from now in ms. "accessTokenExpires": (time.time() + 60 * 60 * 24 * 365 * 5) * 1000, }) except (asyncio.TimeoutError, ClientError) as err: if isinstance(err, asyncio.TimeoutError): msg = "Request timeout" else: msg = err _LOGGER.warning("Unable to configure Almond: %s", msg) await opp.auth.async_remove_refresh_token(refresh_token) raise ConfigEntryNotReady from err # Clear all other refresh tokens for token in list(user.refresh_tokens.values()): if token.id != refresh_token.id: await opp.auth.async_remove_refresh_token(token)
def redirect_uri(self) -> str: """Return the redirect uri.""" url = get_url(self.opp, allow_internal=False, prefer_cloud=True) return f"{url}{AUTH_CALLBACK_PATH}"
def get_base_url(opp): """Get base URL.""" return opp.data[BASE_URL_KEY] or get_url(opp)
def async_generate_url(opp, webhook_id): """Generate the full URL for a webhook_id.""" return "{}{}".format( get_url(opp, prefer_external=True, allow_cloud=False), async_generate_path(webhook_id), )
async def test_get_current_request_url_with_known_host(opp: OpenPeerPower, current_request): """Test getting current request URL with known hosts addresses.""" opp.config.api = Mock(use_ssl=False, port=8123, local_ip="127.0.0.1") assert opp.config.internal_url is None with pytest.raises(NoURLAvailableError): get_url(opp, require_current_request=True) # Ensure we accept localhost with patch("openpeerpower.helpers.network._get_request_host", return_value="localhost"): assert get_url(opp, require_current_request=True) == "http://localhost:8123" with pytest.raises(NoURLAvailableError): get_url(opp, require_current_request=True, require_ssl=True) with pytest.raises(NoURLAvailableError): get_url(opp, require_current_request=True, require_standard_port=True) # Ensure we accept local loopback ip (e.g., 127.0.0.1) with patch("openpeerpower.helpers.network._get_request_host", return_value="127.0.0.8"): assert get_url(opp, require_current_request=True) == "http://127.0.0.8:8123" with pytest.raises(NoURLAvailableError): get_url(opp, require_current_request=True, allow_ip=False) # Ensure hostname from Supervisor is accepted transparently mock_component(opp, "oppio") opp.components.oppio.is_oppio = Mock(return_value=True) opp.components.oppio.get_host_info = Mock( return_value={"hostname": "openpeerpower"}) with patch( "openpeerpower.helpers.network._get_request_host", return_value="openpeerpower.local", ): assert (get_url( opp, require_current_request=True) == "http://openpeerpower.local:8123") with patch( "openpeerpower.helpers.network._get_request_host", return_value="openpeerpower", ): assert get_url( opp, require_current_request=True) == "http://openpeerpower:8123" with patch( "openpeerpower.helpers.network._get_request_host", return_value="unknown.local"), pytest.raises(NoURLAvailableError): get_url(opp, require_current_request=True)
async def test_get_url(opp: OpenPeerPower): """Test getting an instance URL.""" assert opp.config.external_url is None assert opp.config.internal_url is None with pytest.raises(NoURLAvailableError): get_url(opp) opp.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") assert get_url(opp) == "http://192.168.123.123:8123" assert get_url(opp, prefer_external=True) == "http://192.168.123.123:8123" with pytest.raises(NoURLAvailableError): get_url(opp, allow_internal=False) # Test only external opp.config.api = None await async_process_op_core_config( opp, {"external_url": "https://example.com"}, ) assert opp.config.external_url == "https://example.com" assert opp.config.internal_url is None assert get_url(opp) == "https://example.com" # Test preference or allowance await async_process_op_core_config( opp, { "internal_url": "http://example.local", "external_url": "https://example.com" }, ) assert opp.config.external_url == "https://example.com" assert opp.config.internal_url == "http://example.local" assert get_url(opp) == "http://example.local" assert get_url(opp, prefer_external=True) == "https://example.com" assert get_url(opp, allow_internal=False) == "https://example.com" assert (get_url(opp, prefer_external=True, allow_external=False) == "http://example.local") with pytest.raises(NoURLAvailableError): get_url(opp, allow_external=False, require_ssl=True) with pytest.raises(NoURLAvailableError): get_url(opp, allow_external=False, allow_internal=False) with pytest.raises(NoURLAvailableError): get_url(opp, require_current_request=True) with patch("openpeerpower.helpers.network._get_request_host", return_value="example.com"), patch( "openpeerpower.components.http.current_request"): assert get_url(opp, require_current_request=True) == "https://example.com" assert (get_url(opp, require_current_request=True, require_ssl=True) == "https://example.com") with pytest.raises(NoURLAvailableError): get_url(opp, require_current_request=True, allow_external=False) with patch("openpeerpower.helpers.network._get_request_host", return_value="example.local"), patch( "openpeerpower.components.http.current_request"): assert get_url(opp, require_current_request=True) == "http://example.local" with pytest.raises(NoURLAvailableError): get_url(opp, require_current_request=True, allow_internal=False) with pytest.raises(NoURLAvailableError): get_url(opp, require_current_request=True, require_ssl=True) with patch( "openpeerpower.helpers.network._get_request_host", return_value="no_match.example.com", ), pytest.raises(NoURLAvailableError): _get_internal_url(opp, require_current_request=True)
async def sync_serialize(self, agent_user_id): """Serialize entity for a SYNC response. https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ state = self.state entity_config = self.config.entity_config.get(state.entity_id, {}) name = (entity_config.get(CONF_NAME) or state.name).strip() domain = state.domain device_class = state.attributes.get(ATTR_DEVICE_CLASS) entity_entry, device_entry = await _get_entity_and_device( self.opp, state.entity_id) traits = self.traits() device_type = get_google_type(domain, device_class) device = { "id": state.entity_id, "name": { "name": name }, "attributes": {}, "traits": [trait.name for trait in traits], "willReportState": self.config.should_report_state, "type": device_type, } # use aliases aliases = entity_config.get(CONF_ALIASES) if aliases: device["name"]["nicknames"] = [name] + aliases if self.config.is_local_sdk_active and self.should_expose_local(): device["otherDeviceIds"] = [{"deviceId": self.entity_id}] device["customData"] = { "webhookId": self.config.local_sdk_webhook_id, "httpPort": self.opp.http.server_port, "httpSSL": self.opp.config.api.use_ssl, "uuid": await self.opp.helpers.instance_id.async_get(), "baseUrl": get_url(self.opp, prefer_external=True), "proxyDeviceId": agent_user_id, } for trt in traits: device["attributes"].update(trt.sync_attributes()) room = entity_config.get(CONF_ROOM_HINT) if room: device["roomHint"] = room else: area = await _get_area(self.opp, entity_entry, device_entry) if area and area.name: device["roomHint"] = area.name device_info = await _get_device_info(device_entry) if device_info: device["deviceInfo"] = device_info return device