diff --git a/custom_components/smartthings_customize/__init__.py b/custom_components/smartthings_customize/__init__.py index b86ee70..d84bebc 100644 --- a/custom_components/smartthings_customize/__init__.py +++ b/custom_components/smartthings_customize/__init__.py @@ -179,6 +179,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Validate and setup the app. #app = await api.app(entry.data[CONF_APP_ID]) smart_app = setup_smartapp(hass, app) + elif not smart_app.public_key and app and app.webhook_public_key: + # The SmartApp was registered during the OAuth setup flow without a + # public key (no PAT available at that time). Now that we have a + # valid access token and the full app info, apply the public key so + # that incoming webhook requests are properly signature-verified. + smart_app.public_key = app.webhook_public_key # Validate and retrieve the installed app. installed_app = await validate_installed_app( diff --git a/custom_components/smartthings_customize/config_flow.py b/custom_components/smartthings_customize/config_flow.py index 3120287..746d375 100644 --- a/custom_components/smartthings_customize/config_flow.py +++ b/custom_components/smartthings_customize/config_flow.py @@ -1,12 +1,10 @@ """Config flow to configure SmartThings.""" from collections.abc import Mapping -from http import HTTPStatus import logging from typing import Any -from aiohttp import ClientResponseError -from .pysmartthings import APIResponseError, AppOAuth, SmartThings +from .pysmartthings import SmartThings from .pysmartthings.installedapp import format_install_url import voluptuous as vol @@ -18,8 +16,6 @@ from homeassistant.core import callback from .const import ( - APP_OAUTH_CLIENT_NAME, - APP_OAUTH_SCOPES, CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, @@ -31,31 +27,27 @@ CONF_ENABLE_SYNTAX_PROPERTY, ) from .smartapp import ( - create_app, - find_app, format_unique_id, get_webhook_url, - setup_smartapp, + setup_smartapp_for_oauth, setup_smartapp_endpoint, - update_app, validate_webhook_requirements, ) _LOGGER = logging.getLogger(__name__) + class SmartThingsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle configuration of SmartThings integrations.""" VERSION = 2 - api: SmartThings app_id: str location_id: str def __init__(self) -> None: """Create a new instance of the flow handler.""" - self.access_token: str | None = None self.oauth_client_secret = None self.oauth_client_id = None self.installed_app_id = None @@ -97,117 +89,72 @@ async def async_step_user( ) # Show the next screen - return await self.async_step_pat() + return await self.async_step_oauth_credentials() - async def async_step_pat( + async def async_step_oauth_credentials( self, user_input: dict[str, str] | None = None ) -> ConfigFlowResult: - """Get the Personal Access Token and validate it.""" + """Collect the OAuth credentials created in the SmartThings Developer Portal.""" errors: dict[str, str] = {} - if user_input is None or CONF_ACCESS_TOKEN not in user_input: - return self._show_step_pat(errors) - - self.access_token = user_input[CONF_ACCESS_TOKEN] - - # Ensure token is a UUID - if not VAL_UID_MATCHER.match(self.access_token): - errors[CONF_ACCESS_TOKEN] = "token_invalid_format" - return self._show_step_pat(errors) - - # Setup end-point - self.api = SmartThings(async_get_clientsession(self.hass), self.access_token) - try: - app = await find_app(self.hass, self.api) - if app: - await app.refresh() # load all attributes - await update_app(self.hass, app) - # Find an existing entry to copy the oauth client - existing = next( - ( - entry - for entry in self._async_current_entries() - if entry.data[CONF_APP_ID] == app.app_id - ), - None, - ) - if existing: - self.oauth_client_id = existing.data[CONF_CLIENT_ID] - self.oauth_client_secret = existing.data[CONF_CLIENT_SECRET] - else: - # Get oauth client id/secret by regenerating it - app_oauth = AppOAuth(app.app_id) - app_oauth.client_name = APP_OAUTH_CLIENT_NAME - app_oauth.scope.extend(APP_OAUTH_SCOPES) - client = await self.api.generate_app_oauth(app_oauth) - self.oauth_client_secret = client.client_secret - self.oauth_client_id = client.client_id - else: - app, client = await create_app(self.hass, self.api) - self.oauth_client_secret = client.client_secret - self.oauth_client_id = client.client_id - setup_smartapp(self.hass, app) - self.app_id = app.app_id - - except APIResponseError as ex: - if ex.is_target_error(): - errors["base"] = "webhook_error" - else: - errors["base"] = "app_setup_error" - _LOGGER.exception( - "API error setting up the SmartApp: %s", ex.raw_error_response - ) - return self._show_step_pat(errors) - except ClientResponseError as ex: - if ex.status == HTTPStatus.UNAUTHORIZED: - errors[CONF_ACCESS_TOKEN] = "token_unauthorized" - _LOGGER.debug( - "Unauthorized error received setting up SmartApp", exc_info=True - ) - elif ex.status == HTTPStatus.FORBIDDEN: - errors[CONF_ACCESS_TOKEN] = "token_forbidden" - _LOGGER.debug( - "Forbidden error received setting up SmartApp", exc_info=True - ) - else: - errors["base"] = "app_setup_error" - _LOGGER.exception("Unexpected error setting up the SmartApp") - return self._show_step_pat(errors) - except Exception: - errors["base"] = "app_setup_error" - _LOGGER.exception("Unexpected error setting up the SmartApp") - return self._show_step_pat(errors) - - return await self.async_step_select_location() - - async def async_step_select_location( - self, user_input: dict[str, str] | None = None - ) -> ConfigFlowResult: - """Ask user to select the location to setup.""" - if user_input is None or CONF_LOCATION_ID not in user_input: - # Get available locations - existing_locations = [ - entry.data[CONF_LOCATION_ID] for entry in self._async_current_entries() - ] - locations = await self.api.locations() - locations_options = { - location.location_id: location.name - for location in locations - if location.location_id not in existing_locations - } - if not locations_options: - return self.async_abort(reason="no_available_locations") + if user_input is None: + return self._show_step_oauth_credentials(errors) + + app_id = user_input.get(CONF_APP_ID, "").strip() + client_id = user_input.get(CONF_CLIENT_ID, "").strip() + client_secret = user_input.get(CONF_CLIENT_SECRET, "").strip() + + # Validate app_id is UUID format + if not VAL_UID_MATCHER.match(app_id): + errors[CONF_APP_ID] = "app_id_invalid_format" + return self._show_step_oauth_credentials(errors) + + self.app_id = app_id + self.oauth_client_id = client_id + self.oauth_client_secret = client_secret + + # If an existing entry already uses this app_id, reuse its OAuth credentials + # so the user does not have to re-enter them for a second location. + existing = next( + ( + entry + for entry in self._async_current_entries() + if entry.data.get(CONF_APP_ID) == app_id + ), + None, + ) + if existing: + self.oauth_client_id = existing.data[CONF_CLIENT_ID] + self.oauth_client_secret = existing.data[CONF_CLIENT_SECRET] - return self.async_show_form( - step_id="select_location", - data_schema=vol.Schema( - {vol.Required(CONF_LOCATION_ID): vol.In(locations_options)} - ), - ) + # Store app_id in flow context so the webhook callback can locate this flow. + self.context["app_id"] = app_id + + # Register the SmartApp with the webhook manager so incoming lifecycle + # events (INSTALL, PING, etc.) can be dispatched. The public key is not + # yet available because we have no access token; signature verification + # will be enabled once async_setup_entry fetches the full app info. + setup_smartapp_for_oauth(self.hass, app_id) - self.location_id = user_input[CONF_LOCATION_ID] - await self.async_set_unique_id(format_unique_id(self.app_id, self.location_id)) return await self.async_step_authorize() + def _show_step_oauth_credentials(self, errors): + webhook_url = get_webhook_url(self.hass) + return self.async_show_form( + step_id="oauth_credentials", + data_schema=vol.Schema( + { + vol.Required(CONF_APP_ID): str, + vol.Required(CONF_CLIENT_ID): str, + vol.Required(CONF_CLIENT_SECRET): str, + } + ), + errors=errors, + description_placeholders={ + "developer_url": "https://smartthings.developer.samsung.com/", + "webhook_url": webhook_url, + }, + ) + async def async_step_authorize( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -215,41 +162,31 @@ async def async_step_authorize( user_input = {} if user_input is None else user_input self.installed_app_id = user_input.get(CONF_INSTALLED_APP_ID) self.refresh_token = user_input.get(CONF_REFRESH_TOKEN) + + # The OAuth callback includes the location chosen by the user in SmartThings. + if CONF_LOCATION_ID in user_input: + self.location_id = user_input[CONF_LOCATION_ID] + if self.installed_app_id is None: - # Launch the external setup URL - url = format_install_url(self.app_id, self.location_id) + # Launch the external setup URL. + # location_id may not be known yet for OAuth flows; SmartThings will + # let the user pick a location on its own authorization page. + location_id = getattr(self, "location_id", "") or "" + url = format_install_url(self.app_id, location_id) return self.async_external_step(step_id="authorize", url=url) + # Set the unique_id now that we have both app_id and location_id. + if not self.context.get("unique_id"): + await self.async_set_unique_id( + format_unique_id(self.app_id, self.location_id) + ) + self._abort_if_unique_id_configured() + next_step_id = "install" if self.source == SOURCE_REAUTH: next_step_id = "update" return self.async_external_step_done(next_step_id=next_step_id) - def _show_step_pat(self, errors): - if self.access_token is None: - # Get the token from an existing entry to make it easier to setup multiple locations. - self.access_token = next( - ( - entry.data.get(CONF_ACCESS_TOKEN) - for entry in self._async_current_entries() - ), - None, - ) - - return self.async_show_form( - step_id="pat", - data_schema=vol.Schema( - {vol.Required(CONF_ACCESS_TOKEN, default=self.access_token): str} - ), - errors=errors, - description_placeholders={ - "token_url": "https://account.smartthings.com/tokens", - "component_url": ( - "https://www.home-assistant.io/integrations/smartthings/" - ), - }, - ) - async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: @@ -262,8 +199,11 @@ async def async_step_reauth_confirm( """Handle re-authentication of an existing config entry.""" if user_input is None: return self.async_show_form(step_id="reauth_confirm") - self.app_id = self._get_reauth_entry().data[CONF_APP_ID] - self.location_id = self._get_reauth_entry().data[CONF_LOCATION_ID] + entry = self._get_reauth_entry() + self.app_id = entry.data[CONF_APP_ID] + self.location_id = entry.data[CONF_LOCATION_ID] + self.oauth_client_id = entry.data[CONF_CLIENT_ID] + self.oauth_client_secret = entry.data[CONF_CLIENT_SECRET] self._set_confirm_only() return await self.async_step_authorize() @@ -287,9 +227,19 @@ async def async_step_update_confirm( async def async_step_install(self, data=None): """Create a config entry at completion of a flow and authorization of the app.""" - data = { - CONF_ACCESS_TOKEN: self.access_token, - CONF_REFRESH_TOKEN: self.refresh_token, + session = async_get_clientsession(self.hass) + + # Exchange the refresh token for a fresh access token. + api = SmartThings(session, "") + token = await api.generate_tokens( + self.oauth_client_id, + self.oauth_client_secret, + self.refresh_token, + ) + + config_data = { + CONF_ACCESS_TOKEN: token.access_token, + CONF_REFRESH_TOKEN: token.refresh_token, CONF_CLIENT_ID: self.oauth_client_id, CONF_CLIENT_SECRET: self.oauth_client_secret, CONF_LOCATION_ID: self.location_id, @@ -297,10 +247,11 @@ async def async_step_install(self, data=None): CONF_INSTALLED_APP_ID: self.installed_app_id, } - location = await self.api.location(data[CONF_LOCATION_ID]) - self.location = location + # Fetch the location name to use as the config entry title. + api = SmartThings(session, token.access_token) + location = await api.location(config_data[CONF_LOCATION_ID]) - return self.async_create_entry(title=location.name, data=data) + return self.async_create_entry(title=location.name, data=config_data) @staticmethod @callback diff --git a/custom_components/smartthings_customize/pysmartapp/request.py b/custom_components/smartthings_customize/pysmartapp/request.py index a7666e4..1841972 100644 --- a/custom_components/smartthings_customize/pysmartapp/request.py +++ b/custom_components/smartthings_customize/pysmartapp/request.py @@ -60,7 +60,7 @@ def _init_installed_app(self, installed_app): async def process(self, app, headers: list = None, validate_signature: bool = True) -> Response: """Process the request with the SmartApp.""" - if validate_signature and self._supports_validation: + if validate_signature and self._supports_validation and app.public_key: try: verifier = HeaderVerifier( headers=headers, secret=app.public_key, method='POST', diff --git a/custom_components/smartthings_customize/pysmartapp/smartapp.py b/custom_components/smartthings_customize/pysmartapp/smartapp.py index afa897f..220d64f 100644 --- a/custom_components/smartthings_customize/pysmartapp/smartapp.py +++ b/custom_components/smartthings_customize/pysmartapp/smartapp.py @@ -148,6 +148,11 @@ def public_key(self): """Get the public key of the SmartApp used to verify events.""" return self._public_key + @public_key.setter + def public_key(self, value): + """Set the public key of the SmartApp used to verify events.""" + self._public_key = value + class SmartAppManager(SmartAppBase): """Service to support multiple SmartApps at the same end-point.""" diff --git a/custom_components/smartthings_customize/pysmartthings/installedapp.py b/custom_components/smartthings_customize/pysmartthings/installedapp.py index 931ad72..a17aaec 100644 --- a/custom_components/smartthings_customize/pysmartthings/installedapp.py +++ b/custom_components/smartthings_customize/pysmartthings/installedapp.py @@ -2,15 +2,23 @@ from enum import Enum from typing import Sequence +from urllib.parse import quote from .api import Api from .entity import Entity from .subscription import SubscriptionEntity -def format_install_url(app_id: str, location_id: str) -> str: +def format_install_url(app_id: str, location_id: str = "") -> str: """Return a web-based URL to auth and install a SmartApp.""" - return f"https://account.smartthings.com/login?redirect=https%3A%2F%2Fstrongman-regional.api.smartthings.com%2F%3FappId%3D{app_id}%26locationId%3D{location_id}%26appType%3DENDPOINTAPP%26language%3Den%26clientOS%3Dweb" + params = f"appId={app_id}" + if location_id: + params += f"&locationId={location_id}" + params += "&appType=ENDPOINTAPP&language=en&clientOS=web" + redirect = quote( + "https://strongman-regional.api.smartthings.com/?" + params, safe="" + ) + return f"https://account.smartthings.com/login?redirect={redirect}" class InstalledAppType(Enum): diff --git a/custom_components/smartthings_customize/smartapp.py b/custom_components/smartthings_customize/smartapp.py index 3d64d08..ec4e6fd 100644 --- a/custom_components/smartthings_customize/smartapp.py +++ b/custom_components/smartthings_customize/smartapp.py @@ -224,7 +224,10 @@ def setup_smartapp(hass, app): """ manager = hass.data[DOMAIN][DATA_MANAGER] if smartapp := manager.smartapps.get(app.app_id): - # already setup + # Update the public key if it was previously registered without one + # (e.g., during OAuth-only setup before the app info was fetched). + if not smartapp.public_key and app.webhook_public_key: + smartapp.public_key = app.webhook_public_key return smartapp smartapp = manager.register(app.app_id, app.webhook_public_key) smartapp.name = app.display_name @@ -233,6 +236,24 @@ def setup_smartapp(hass, app): return smartapp +def setup_smartapp_for_oauth(hass: HomeAssistant, app_id: str): + """Register a SmartApp for an OAuth-only setup flow. + + This registers the SmartApp with the manager using only the app_id so that + the installation webhook callback can be received and processed. Signature + verification is intentionally skipped at this stage (public_key is None) + and the real public key is applied later in async_setup_entry once an OAuth + access token is available to fetch the full app details. + """ + manager = hass.data[DOMAIN][DATA_MANAGER] + if smartapp := manager.smartapps.get(app_id): + return smartapp + smartapp = manager.register(app_id, None) + smartapp.name = APP_OAUTH_CLIENT_NAME + smartapp.permissions.extend(APP_OAUTH_SCOPES) + return smartapp + + async def setup_smartapp_endpoint(hass: HomeAssistant, fresh_install: bool): """Configure the SmartApp webhook in hass. @@ -456,6 +477,8 @@ async def _find_and_continue_flow( refresh_token: str, ): """Continue a config flow if one is in progress for the specific installed app.""" + # First, look for a flow that already has its unique_id set (PAT or reauth flows + # where the location was pre-selected before authorization). unique_id = format_unique_id(app_id, location_id) flow = next( ( @@ -467,6 +490,29 @@ async def _find_and_continue_flow( ) if flow is not None: await _continue_flow(hass, app_id, installed_app_id, refresh_token, flow) + return + + # Fall back to finding an OAuth flow that is waiting in the authorize step + # with a matching app_id but no location pre-selected (unique_id not yet set). + flow = next( + ( + flow + for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN) + if flow["step_id"] == "authorize" + and flow["context"].get("app_id") == app_id + ), + None, + ) + if flow is not None: + await _continue_flow( + hass, app_id, installed_app_id, refresh_token, flow, location_id + ) + _LOGGER.debug( + "Continued OAuth config flow '%s' for SmartApp '%s' under parent app '%s'", + flow["flow_id"], + installed_app_id, + app_id, + ) async def _continue_flow( @@ -475,13 +521,17 @@ async def _continue_flow( installed_app_id: str, refresh_token: str, flow: ConfigFlowResult, + location_id: str | None = None, ) -> None: + data = { + CONF_INSTALLED_APP_ID: installed_app_id, + CONF_REFRESH_TOKEN: refresh_token, + } + if location_id is not None: + data[CONF_LOCATION_ID] = location_id await hass.config_entries.flow.async_configure( flow["flow_id"], - { - CONF_INSTALLED_APP_ID: installed_app_id, - CONF_REFRESH_TOKEN: refresh_token, - }, + data, ) _LOGGER.debug( "Continued config flow '%s' for SmartApp '%s' under parent app '%s'", diff --git a/custom_components/smartthings_customize/strings.json b/custom_components/smartthings_customize/strings.json index 92b3b00..6e20375 100644 --- a/custom_components/smartthings_customize/strings.json +++ b/custom_components/smartthings_customize/strings.json @@ -5,28 +5,27 @@ "title": "Confirm Callback URL", "description": "SmartThings will be configured to send push updates to Home Assistant at:\n> {webhook_url}\n\nIf this is not correct, please update your configuration, restart Home Assistant, and try again." }, - "pat": { - "title": "Enter Personal Access Token", - "description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}). This will be used to create the Home Assistant integration within your SmartThings account.", + "oauth_credentials": { + "title": "Enter SmartThings OAuth Credentials", + "description": "Create a **WebHook SmartApp** in the [SmartThings Developer Portal]({developer_url}) with the following webhook URL:\n> {webhook_url}\n\nThen enter the **App ID**, **OAuth Client ID**, and **OAuth Client Secret** from the portal below.", "data": { - "access_token": "[%key:common::config_flow::data::access_token%]" + "app_id": "App ID", + "client_id": "OAuth Client ID", + "client_secret": "OAuth Client Secret" } }, - "select_location": { - "title": "Select Location", - "description": "Please select the SmartThings Location you wish to add to Home Assistant. We will then open a new window and ask you to login and authorize installation of the Home Assistant integration into the selected location.", - "data": { "location_id": "[%key:common::config_flow::data::location%]" } - }, - "authorize": { "title": "Authorize Home Assistant" } + "authorize": { "title": "Authorize Home Assistant" }, + "reauth_confirm": { + "title": "Re-authorize Home Assistant", + "description": "SmartThings needs to be re-authorized. Click **Submit** to open the SmartThings authorization page." + } }, "abort": { "invalid_webhook_url": "Home Assistant is not configured correctly to receive updates from SmartThings. The webhook URL is invalid:\n> {webhook_url}\n\nPlease update your configuration per the [instructions]({component_url}), restart Home Assistant, and try again.", "no_available_locations": "There are no available SmartThings Locations to set up in Home Assistant." }, "error": { - "token_invalid_format": "The token must be in the UID/GUID format", - "token_unauthorized": "The token is invalid or no longer authorized.", - "token_forbidden": "The token does not have the required OAuth scopes.", + "app_id_invalid_format": "The App ID must be in the UUID/GUID format.", "app_setup_error": "Unable to set up the SmartApp. Please try again.", "webhook_error": "SmartThings could not validate the webhook URL. Please ensure the webhook URL is reachable from the internet and try again." } diff --git a/custom_components/smartthings_customize/translations/en.json b/custom_components/smartthings_customize/translations/en.json index ded899a..e1582c4 100644 --- a/custom_components/smartthings_customize/translations/en.json +++ b/custom_components/smartthings_customize/translations/en.json @@ -5,34 +5,43 @@ "no_available_locations": "There are no available SmartThings Locations to setup in Home Assistant." }, "error": { + "app_id_invalid_format": "The App ID must be in the UUID/GUID format.", "app_setup_error": "Unable to setup the SmartApp. Please try again.", - "token_forbidden": "The token does not have the required OAuth scopes.", - "token_invalid_format": "The token must be in the UID/GUID format", - "token_unauthorized": "The token is invalid or no longer authorized.", "webhook_error": "SmartThings could not validate the webhook URL. Please ensure the webhook URL is reachable from the internet and try again." }, "step": { "authorize": { "title": "Authorize Home Assistant" }, - "pat": { + "oauth_credentials": { "data": { - "access_token": "Access Token" + "app_id": "App ID", + "client_id": "OAuth Client ID", + "client_secret": "OAuth Client Secret" }, - "description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}). This will be used to create the Home Assistant integration within your SmartThings account.", - "title": "Enter Personal Access Token" + "description": "Create a **WebHook SmartApp** in the [SmartThings Developer Portal]({developer_url}) with the following webhook URL:\n> {webhook_url}\n\nThen enter the **App ID**, **OAuth Client ID**, and **OAuth Client Secret** from the portal below.", + "title": "Enter SmartThings OAuth Credentials" }, - "select_location": { - "data": { - "location_id": "Location" - }, - "description": "Please select the SmartThings Location you wish to add to Home Assistant. We will then open a new window and ask you to login and authorize installation of the Home Assistant integration into the selected location.", - "title": "Select Location" + "reauth_confirm": { + "title": "Re-authorize Home Assistant", + "description": "SmartThings needs to be re-authorized. Click Submit to open the SmartThings authorization page." }, "user": { "description": "SmartThings will be configured to send push updates to Home Assistant at:\n> {webhook_url}\n\nIf this is not correct, please update your configuration, restart Home Assistant, and try again.", "title": "Confirm Callback URL" } } + }, + "options": { + "step": { + "init": { + "title": "SmartThings Customize Configuration", + "data": { + "enable_default_entities": "enable default entities", + "enable_syntax_property": "enable syntax property", + "resetting_entities": "resetting entities" + } + } + } } -} \ No newline at end of file +} diff --git a/custom_components/smartthings_customize/translations/ko.json b/custom_components/smartthings_customize/translations/ko.json index c981c7e..0c89ec1 100644 --- a/custom_components/smartthings_customize/translations/ko.json +++ b/custom_components/smartthings_customize/translations/ko.json @@ -5,29 +5,26 @@ "no_available_locations": "Home Assistant\uc5d0\uc11c \uc124\uc815\ud560 \uc218 \uc788\ub294 SmartThings \uc704\uce58\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." }, "error": { + "app_id_invalid_format": "\uc571 ID\ub294 UUID/GUID \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4.", "app_setup_error": "SmartApp \uc744 \uc124\uc815\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "token_forbidden": "\ud1a0\ud070\uc5d0 \ud544\uc694\ud55c OAuth \ubc94\uc704\ubaa9\ub85d\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.", - "token_invalid_format": "\ud1a0\ud070\uc740 UID/GUID \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4", - "token_unauthorized": "\ud1a0\ud070\uc774 \uc720\ud6a8\ud558\uc9c0 \uc54a\uac70\ub098 \uc2b9\uc778\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", "webhook_error": "SmartThings \uac00 \uc6f9 \ud6c5 URL \uc744 \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc778\ud130\ub137\uc5d0\uc11c \uc6f9 \ud6c5 URL \uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\ub294\uc9c0 \ud655\uc778\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." }, "step": { "authorize": { "title": "Home Assistant \uc2b9\uc778\ud558\uae30" }, - "pat": { + "oauth_credentials": { "data": { - "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070" + "app_id": "\uc571 ID", + "client_id": "OAuth \ud074\ub77c\uc774\uc5b8\ud2b8 ID", + "client_secret": "OAuth \ud074\ub77c\uc774\uc5b8\ud2b8 \uc2dc\ud06c\ub9bf" }, - "description": "[\uc548\ub0b4]({component_url})\uc5d0 \ub530\ub77c \uc0dd\uc131\ub41c SmartThings [\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070]({token_url})\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. SmartThings \uacc4\uc815\uc5d0\uc11c Home Assistant \uc5f0\ub3d9\uc744 \ub9cc\ub4dc\ub294\ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4.", - "title": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" + "description": "[SmartThings \uac1c\ubc1c\uc790 \ud3ec\ud138]({developer_url})\uc5d0\uc11c \uc544\ub798\uc758 \uc6f9\ud6c5 URL\ub85c **WebHook SmartApp**\uc744 \uc0dd\uc131\ud558\uc138\uc694:\n> {webhook_url}\n\n\uc0dd\uc131 \ud6c4 \ud3ec\ud138\uc5d0\uc11c **\uc571 ID**, **OAuth \ud074\ub77c\uc774\uc5b8\ud2b8 ID**, **OAuth \ud074\ub77c\uc774\uc5b8\ud2b8 \uc2dc\ud06c\ub9bf**\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "SmartThings OAuth \uc790\uaca9 \uc99d\uba85 \uc785\ub825" }, - "select_location": { - "data": { - "location_id": "\uc704\uce58" - }, - "description": "Home Assistant\uc5d0 \ucd94\uac00\ud558\ub824\ub294 SmartThings \uc704\uce58\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc0c8\ub86d\uac8c \uc5f4\ub9b0 \ub85c\uadf8\uc778 \ucc3d\uc5d0\uc11c \ub85c\uadf8\uc778\uc744 \ud558\uba74 \uc120\ud0dd\ud55c \uc704\uce58\uc5d0 Home Assistant \uc5f0\ub3d9\uc744 \uc2b9\uc778\ud558\ub77c\ub294 \uba54\uc2dc\uc9c0\uac00 \ud45c\uc2dc\ub429\ub2c8\ub2e4.", - "title": "\uc704\uce58 \uc120\ud0dd\ud558\uae30" + "reauth_confirm": { + "title": "Home Assistant \uc7ac\uc778\uc99d", + "description": "SmartThings \uc7ac\uc778\uc99d\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc81c\ucd9c \ubc84\ud2bc\uc744 \ub20c\ub7ec SmartThings \uc778\uc99d \ud398\uc774\uc9c0\ub97c \uc5ec\uc138\uc694." }, "user": { "description": "SmartThings\ub294 \uc544\ub798\uc758 \uc6f9 \ud6c5 \uc8fc\uc18c\ub85c Home Assistant\uc5d0 \ud478\uc2dc \uc5c5\ub370\uc774\ud2b8\ub97c \ubcf4\ub0b4\ub3c4\ub85d \uad6c\uc131\ub429\ub2c8\ub2e4. \n > {webhook_url} \n\n\uc774 \uad6c\uc131\uc774 \uc62c\ubc14\ub974\uc9c0 \uc54a\ub2e4\uba74 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud558\uace0 Home Assistant\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", @@ -47,4 +44,4 @@ } } } -} \ No newline at end of file +}