mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +00:00
Delete subscription on shutdown of SmartThings (#140135)
* Cache subscription url in SmartThings * Cache subscription url in SmartThings * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Bump pysmartthings to 2.7.1 * 2.7.2 --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
b5c7bdd98f
commit
f2f653efcf
@ -16,12 +16,18 @@ from pysmartthings import (
|
||||
Scene,
|
||||
SmartThings,
|
||||
SmartThingsAuthenticationFailedError,
|
||||
SmartThingsSinkError,
|
||||
Status,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_TOKEN,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@ -33,6 +39,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
from .const import (
|
||||
CONF_INSTALLED_APP_ID,
|
||||
CONF_LOCATION_ID,
|
||||
CONF_SUBSCRIPTION_ID,
|
||||
DOMAIN,
|
||||
EVENT_BUTTON,
|
||||
MAIN,
|
||||
@ -99,6 +106,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
|
||||
|
||||
client.refresh_token_function = _refresh_token
|
||||
|
||||
def _handle_max_connections() -> None:
|
||||
_LOGGER.debug("We hit the limit of max connections")
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
client.max_connections_reached_callback = _handle_max_connections
|
||||
|
||||
def _handle_new_subscription_identifier(identifier: str | None) -> None:
|
||||
"""Handle a new subscription identifier."""
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data={
|
||||
**entry.data,
|
||||
CONF_SUBSCRIPTION_ID: identifier,
|
||||
},
|
||||
)
|
||||
if identifier is not None:
|
||||
_LOGGER.debug("Updating subscription ID to %s", identifier)
|
||||
else:
|
||||
_LOGGER.debug("Removing subscription ID")
|
||||
|
||||
client.new_subscription_id_callback = _handle_new_subscription_identifier
|
||||
|
||||
if (old_identifier := entry.data.get(CONF_SUBSCRIPTION_ID)) is not None:
|
||||
_LOGGER.debug("Trying to delete old subscription %s", old_identifier)
|
||||
await client.delete_subscription(old_identifier)
|
||||
|
||||
_LOGGER.debug("Trying to create a new subscription")
|
||||
try:
|
||||
subscription = await client.create_subscription(
|
||||
entry.data[CONF_LOCATION_ID],
|
||||
entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID],
|
||||
)
|
||||
except SmartThingsSinkError as err:
|
||||
_LOGGER.debug("Couldn't create a new subscription: %s", err)
|
||||
raise ConfigEntryNotReady from err
|
||||
subscription_id = subscription.subscription_id
|
||||
_handle_new_subscription_identifier(subscription_id)
|
||||
|
||||
entry.async_create_background_task(
|
||||
hass,
|
||||
client.subscribe(
|
||||
entry.data[CONF_LOCATION_ID],
|
||||
entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID],
|
||||
subscription,
|
||||
),
|
||||
"smartthings_socket",
|
||||
)
|
||||
|
||||
device_status: dict[str, FullDevice] = {}
|
||||
try:
|
||||
devices = await client.get_devices()
|
||||
@ -145,12 +200,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
|
||||
client.add_unspecified_device_event_listener(handle_button_press)
|
||||
)
|
||||
|
||||
entry.async_create_background_task(
|
||||
hass,
|
||||
client.subscribe(
|
||||
entry.data[CONF_LOCATION_ID], entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID]
|
||||
),
|
||||
"smartthings_webhook",
|
||||
async def _handle_shutdown(_: Event) -> None:
|
||||
"""Handle shutdown."""
|
||||
await client.delete_subscription(subscription_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _handle_shutdown)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
@ -176,6 +231,9 @@ async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: SmartThingsConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
client = entry.runtime_data.client
|
||||
if (subscription_id := entry.data.get(CONF_SUBSCRIPTION_ID)) is not None:
|
||||
await client.delete_subscription(subscription_id)
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
|
@ -33,4 +33,5 @@ CONF_REFRESH_TOKEN = "refresh_token"
|
||||
MAIN = "main"
|
||||
OLD_DATA = "old_data"
|
||||
|
||||
CONF_SUBSCRIPTION_ID = "subscription_id"
|
||||
EVENT_BUTTON = "smartthings.button"
|
||||
|
@ -29,5 +29,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/smartthings",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"requirements": ["pysmartthings==2.7.0"]
|
||||
"requirements": ["pysmartthings==2.7.2"]
|
||||
}
|
||||
|
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@ -2310,7 +2310,7 @@ pysma==0.7.5
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==2.7.0
|
||||
pysmartthings==2.7.2
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.2
|
||||
|
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@ -1882,7 +1882,7 @@ pysma==0.7.5
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==2.7.0
|
||||
pysmartthings==2.7.2
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.2
|
||||
|
@ -9,6 +9,7 @@ from pysmartthings.models import (
|
||||
DeviceStatus,
|
||||
LocationResponse,
|
||||
SceneResponse,
|
||||
Subscription,
|
||||
)
|
||||
import pytest
|
||||
|
||||
@ -78,6 +79,9 @@ def mock_smartthings() -> Generator[AsyncMock]:
|
||||
client.get_locations.return_value = LocationResponse.from_json(
|
||||
load_fixture("locations.json", DOMAIN)
|
||||
).items
|
||||
client.create_subscription.return_value = Subscription.from_json(
|
||||
load_fixture("subscription.json", DOMAIN)
|
||||
)
|
||||
yield client
|
||||
|
||||
|
||||
|
16
tests/components/smartthings/fixtures/subscription.json
Normal file
16
tests/components/smartthings/fixtures/subscription.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"subscriptionId": "f5768ce8-c9e5-4507-9020-912c0c60e0ab",
|
||||
"registrationUrl": "https://spigot-regional.api.smartthings.com/filters/f5768ce8-c9e5-4507-9020-912c0c60e0ab/activate?filterRegion=eu-west-1",
|
||||
"name": "My Home Assistant sub",
|
||||
"version": 20250122,
|
||||
"subscriptionFilters": [
|
||||
{
|
||||
"type": "LOCATIONIDS",
|
||||
"value": ["88a3a314-f0c8-40b4-bb44-44ba06c9c42e"],
|
||||
"eventType": ["DEVICE_EVENT"],
|
||||
"attribute": null,
|
||||
"capability": null,
|
||||
"component": null
|
||||
}
|
||||
]
|
||||
}
|
@ -10,6 +10,7 @@ from homeassistant.components.smartthings.const import (
|
||||
CONF_INSTALLED_APP_ID,
|
||||
CONF_LOCATION_ID,
|
||||
CONF_REFRESH_TOKEN,
|
||||
CONF_SUBSCRIPTION_ID,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
|
||||
@ -508,6 +509,7 @@ async def test_migration(
|
||||
"installed_app_id": "123123123-2be1-4e40-b257-e4ef59083324",
|
||||
},
|
||||
CONF_LOCATION_ID: "397678e5-9995-4a39-9d9f-ae6ba310236c",
|
||||
CONF_SUBSCRIPTION_ID: "f5768ce8-c9e5-4507-9020-912c0c60e0ab",
|
||||
}
|
||||
assert mock_old_config_entry.unique_id == "397678e5-9995-4a39-9d9f-ae6ba310236c"
|
||||
assert mock_old_config_entry.version == 3
|
||||
|
@ -2,18 +2,21 @@
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from pysmartthings import Attribute, Capability
|
||||
from pysmartthings import Attribute, Capability, SmartThingsSinkError
|
||||
from pysmartthings.models import Subscription
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.smartthings import EVENT_BUTTON
|
||||
from homeassistant.components.smartthings.const import DOMAIN
|
||||
from homeassistant.components.smartthings.const import CONF_SUBSCRIPTION_ID, DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import setup_integration, trigger_update
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
async def test_devices(
|
||||
@ -63,6 +66,178 @@ async def test_button_event(
|
||||
assert events[0] == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||
async def test_create_subscription(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test creating a subscription."""
|
||||
assert CONF_SUBSCRIPTION_ID not in mock_config_entry.data
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
devices.create_subscription.assert_called_once()
|
||||
|
||||
assert (
|
||||
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
|
||||
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||
)
|
||||
|
||||
devices.subscribe.assert_called_once_with(
|
||||
"397678e5-9995-4a39-9d9f-ae6ba310236c",
|
||||
"5aaaa925-2be1-4e40-b257-e4ef59083324",
|
||||
Subscription.from_json(load_fixture("subscription.json", DOMAIN)),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||
async def test_create_subscription_sink_error(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test handling an error when creating a subscription."""
|
||||
assert CONF_SUBSCRIPTION_ID not in mock_config_entry.data
|
||||
|
||||
devices.create_subscription.side_effect = SmartThingsSinkError("Sink error")
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
devices.subscribe.assert_not_called()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
assert CONF_SUBSCRIPTION_ID not in mock_config_entry.data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||
async def test_update_subscription_identifier(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test updating the subscription identifier."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert (
|
||||
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
|
||||
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||
)
|
||||
|
||||
devices.new_subscription_id_callback("abc")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.data[CONF_SUBSCRIPTION_ID] == "abc"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||
async def test_stale_subscription_id(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test updating the subscription identifier."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
mock_config_entry,
|
||||
data={**mock_config_entry.data, CONF_SUBSCRIPTION_ID: "test"},
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
|
||||
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||
)
|
||||
devices.delete_subscription.assert_called_once_with("test")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||
async def test_remove_subscription_identifier(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test removing the subscription identifier."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert (
|
||||
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
|
||||
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||
)
|
||||
|
||||
devices.new_subscription_id_callback(None)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.data[CONF_SUBSCRIPTION_ID] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||
async def test_max_connections_handling(
|
||||
hass: HomeAssistant, devices: AsyncMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test handling reaching max connections."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert (
|
||||
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
|
||||
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||
)
|
||||
|
||||
devices.create_subscription.side_effect = SmartThingsSinkError("Sink error")
|
||||
|
||||
devices.max_connections_reached_callback()
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||
async def test_unloading(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test unloading the integration."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
devices.delete_subscription.assert_called_once_with(
|
||||
"f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||
)
|
||||
# Deleting the subscription automatically deletes the subscription ID
|
||||
devices.new_subscription_id_callback(None)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert mock_config_entry.data[CONF_SUBSCRIPTION_ID] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||
async def test_shutdown(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test shutting down Home Assistant."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||
devices.delete_subscription.assert_called_once_with(
|
||||
"f5768ce8-c9e5-4507-9020-912c0c60e0ab"
|
||||
)
|
||||
# Deleting the subscription automatically deletes the subscription ID
|
||||
devices.new_subscription_id_callback(None)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert mock_config_entry.data[CONF_SUBSCRIPTION_ID] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||
async def test_removing_stale_devices(
|
||||
hass: HomeAssistant,
|
||||
|
Loading…
x
Reference in New Issue
Block a user