1 """Support for SmartThings Cloud."""
3 from __future__
import annotations
6 from collections.abc
import Iterable
7 from http
import HTTPStatus
11 from aiohttp.client_exceptions
import ClientConnectionError, ClientResponseError
12 from pysmartapp.event
import EVENT_TYPE_DEVICE
13 from pysmartthings
import Attribute, Capability, SmartThings
27 from .config_flow
import SmartThingsFlowHandler
30 CONF_INSTALLED_APP_ID,
38 SIGNAL_SMARTTHINGS_UPDATE,
39 TOKEN_REFRESH_INTERVAL,
41 from .smartapp
import (
44 setup_smartapp_endpoint,
45 smartapp_sync_subscriptions,
46 unload_smartapp_endpoint,
47 validate_installed_app,
48 validate_webhook_requirements,
51 _LOGGER = logging.getLogger(__name__)
53 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
56 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
57 """Initialize the SmartThings platform."""
63 """Handle migration of a previous version config entry.
65 A config entry created under a previous version must go through the
66 integration setup again so we can properly retrieve the needed data
67 elements. Force this by removing the entry and triggering a new flow.
70 hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))
72 if not hass.config_entries.flow.async_progress_by_handler(DOMAIN):
73 hass.async_create_task(
74 hass.config_entries.flow.async_init(
75 DOMAIN, context={
"source": SOURCE_IMPORT}
84 """Initialize config entry which represents an installed SmartApp."""
86 if entry.unique_id
is None:
87 hass.config_entries.async_update_entry(
90 entry.data[CONF_APP_ID], entry.data[CONF_LOCATION_ID]
96 "The 'base_url' of the 'http' integration must be configured and start with"
113 manager = hass.data[DOMAIN][DATA_MANAGER]
114 smart_app = manager.smartapps.get(entry.data[CONF_APP_ID])
117 app = await api.app(entry.data[CONF_APP_ID])
122 api, entry.data[CONF_INSTALLED_APP_ID]
129 token = await api.generate_tokens(
130 entry.data[CONF_CLIENT_ID],
131 entry.data[CONF_CLIENT_SECRET],
132 entry.data[CONF_REFRESH_TOKEN],
134 hass.config_entries.async_update_entry(
135 entry, data={**entry.data, CONF_REFRESH_TOKEN: token.refresh_token}
139 devices = await api.devices(location_ids=[installed_app.location_id])
141 async
def retrieve_device_status(device):
143 await device.status.refresh()
144 except ClientResponseError:
147 "Unable to update status for device: %s (%s), the device will"
154 devices.remove(device)
156 await asyncio.gather(*(retrieve_device_status(d)
for d
in devices.copy()))
162 installed_app.location_id,
163 installed_app.installed_app_id,
172 broker = await hass.async_add_import_executor_job(
173 DeviceBroker, hass, entry, token, smart_app, devices, scenes
176 hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker
178 except ClientResponseError
as ex:
179 if ex.status
in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
182 "Unable to setup configuration entry '%s' - please reconfigure the"
189 _LOGGER.debug(ex, exc_info=
True)
190 raise ConfigEntryNotReady
from ex
191 except (ClientConnectionError, RuntimeWarning)
as ex:
192 _LOGGER.debug(ex, exc_info=
True)
193 raise ConfigEntryNotReady
from ex
196 hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))
198 if not hass.config_entries.flow.async_progress_by_handler(DOMAIN):
199 hass.async_create_task(
200 hass.config_entries.flow.async_init(
201 DOMAIN, context={
"source": SOURCE_IMPORT}
206 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
211 """Get the scenes within an integration."""
213 return await api.scenes(location_id=entry.data[CONF_LOCATION_ID])
214 except ClientResponseError
as ex:
215 if ex.status == HTTPStatus.FORBIDDEN:
218 "Unable to load scenes for configuration entry '%s' because the"
219 " access token does not have the required access"
229 """Unload a config entry."""
230 broker = hass.data[DOMAIN][DATA_BROKERS].pop(entry.entry_id,
None)
234 return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
238 """Perform clean-up when entry is being removed."""
242 installed_app_id = entry.data[CONF_INSTALLED_APP_ID]
244 await api.delete_installed_app(installed_app_id)
245 except ClientResponseError
as ex:
246 if ex.status == HTTPStatus.FORBIDDEN:
248 "Installed app %s has already been removed",
254 _LOGGER.debug(
"Removed installed app %s", installed_app_id)
258 all_entries = hass.config_entries.async_entries(DOMAIN)
259 app_id = entry.data[CONF_APP_ID]
260 app_count = sum(1
for entry
in all_entries
if entry.data[CONF_APP_ID] == app_id)
264 "App %s was not removed because it is in use by other configuration"
272 await api.delete_app(app_id)
273 except ClientResponseError
as ex:
274 if ex.status == HTTPStatus.FORBIDDEN:
275 _LOGGER.debug(
"App %s has already been removed", app_id, exc_info=
True)
278 _LOGGER.debug(
"Removed app %s", app_id)
280 if len(all_entries) == 1:
285 """Manages an individual SmartThings config entry."""
296 """Create a new instance of the DeviceBroker."""
305 self.
devicesdevices = {device.device_id: device
for device
in devices}
306 self.
scenesscenes = {scene.scene_id: scene
for scene
in scenes}
309 """Assign platforms to capabilities."""
311 for device
in devices:
312 capabilities = device.capabilities.copy()
314 for platform
in PLATFORMS:
315 platform_module = importlib.import_module(
316 f
".{platform}", self.__module__
318 if not hasattr(platform_module,
"get_capabilities"):
320 assigned = platform_module.get_capabilities(capabilities)
324 for capability
in assigned:
325 if capability
not in capabilities:
327 capabilities.remove(capability)
328 slots[capability] = platform
329 assignments[device.device_id] = slots
333 """Connect handlers/listeners for device/lifecycle events."""
337 async
def regenerate_refresh_token(now):
338 """Generate a new refresh token and update the config entry."""
339 await self.
_token_token.refresh(
340 self.
_entry_entry.data[CONF_CLIENT_ID],
341 self.
_entry_entry.data[CONF_CLIENT_SECRET],
343 self.
_hass_hass.config_entries.async_update_entry(
347 CONF_REFRESH_TOKEN: self.
_token_token.refresh_token,
351 "Regenerated refresh token for installed app: %s",
356 self.
_hass_hass, regenerate_refresh_token, TOKEN_REFRESH_INTERVAL
363 """Disconnects handlers/listeners for device/lifecycle events."""
370 """Get the capabilities assigned to the platform."""
372 return [key
for key, value
in slots.items()
if value == platform]
375 """Return True if the platform has any assigned capabilities."""
377 return any(value
for value
in slots.values()
if value == platform)
380 """Broker for incoming events."""
386 updated_devices = set()
387 for evt
in req.events:
388 if evt.event_type != EVENT_TYPE_DEVICE:
390 if not (device := self.
devicesdevices.
get(evt.device_id)):
392 device.status.apply_attribute_update(
402 evt.capability == Capability.button
403 and evt.attribute == Attribute.button
406 "component_id": evt.component_id,
407 "device_id": evt.device_id,
408 "location_id": evt.location_id,
410 "name": device.label,
413 self.
_hass_hass.bus.async_fire(EVENT_BUTTON, data)
414 _LOGGER.debug(
"Fired button event: %s", data)
417 "location_id": evt.location_id,
418 "device_id": evt.device_id,
419 "component_id": evt.component_id,
420 "capability": evt.capability,
421 "attribute": evt.attribute,
425 _LOGGER.debug(
"Push update received: %s", data)
427 updated_devices.add(device.device_id)
def get_assigned(self, str device_id, str platform)
def _event_handler(self, req, resp, app)
def _assign_capabilities(self, Iterable devices)
None __init__(self, HomeAssistant hass, ConfigEntry entry, token, smart_app, Iterable devices, Iterable scenes)
def any_assigned(self, str device_id, str platform)
web.Response get(self, web.Request request, str config_key)
def format_unique_id(creds, mac_address)
def setup_smartapp_endpoint(HomeAssistant hass, bool fresh_install)
bool validate_webhook_requirements(HomeAssistant hass)
def smartapp_sync_subscriptions(HomeAssistant hass, str auth_token, str location_id, str installed_app_id, devices)
def setup_smartapp(hass, app)
def validate_installed_app(api, str installed_app_id)
def unload_smartapp_endpoint(HomeAssistant hass)
bool async_setup(HomeAssistant hass, ConfigType config)
bool async_migrate_entry(HomeAssistant hass, ConfigEntry entry)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
None async_remove_entry(HomeAssistant hass, ConfigEntry entry)
def async_get_entry_scenes(ConfigEntry entry, api)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
Integration async_get_loaded_integration(HomeAssistant hass, str domain)
Generator[None] async_pause_setup(core.HomeAssistant hass, SetupPhases phase)