mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Add support for subscribing to config entry changes (#77803)
This commit is contained in:
parent
804e4ab989
commit
016a59ac94
@ -20,6 +20,7 @@ from homeassistant.helpers.data_entry_flow import (
|
|||||||
FlowManagerIndexView,
|
FlowManagerIndexView,
|
||||||
FlowManagerResourceView,
|
FlowManagerResourceView,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.loader import (
|
from homeassistant.loader import (
|
||||||
Integration,
|
Integration,
|
||||||
IntegrationNotFound,
|
IntegrationNotFound,
|
||||||
@ -43,6 +44,7 @@ async def async_setup(hass):
|
|||||||
websocket_api.async_register_command(hass, config_entries_get)
|
websocket_api.async_register_command(hass, config_entries_get)
|
||||||
websocket_api.async_register_command(hass, config_entry_disable)
|
websocket_api.async_register_command(hass, config_entry_disable)
|
||||||
websocket_api.async_register_command(hass, config_entry_update)
|
websocket_api.async_register_command(hass, config_entry_update)
|
||||||
|
websocket_api.async_register_command(hass, config_entries_subscribe)
|
||||||
websocket_api.async_register_command(hass, config_entries_progress)
|
websocket_api.async_register_command(hass, config_entries_progress)
|
||||||
websocket_api.async_register_command(hass, ignore_config_flow)
|
websocket_api.async_register_command(hass, ignore_config_flow)
|
||||||
|
|
||||||
@ -408,6 +410,54 @@ async def config_entries_get(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "config_entries/subscribe",
|
||||||
|
vol.Optional("type_filter"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def config_entries_subscribe(
|
||||||
|
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Subscribe to config entry updates."""
|
||||||
|
type_filter = msg.get("type_filter")
|
||||||
|
|
||||||
|
async def async_forward_config_entry_changes(
|
||||||
|
change: config_entries.ConfigEntryChange, entry: config_entries.ConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Forward config entry state events to websocket."""
|
||||||
|
if type_filter:
|
||||||
|
integration = await async_get_integration(hass, entry.domain)
|
||||||
|
if integration.integration_type != type_filter:
|
||||||
|
return
|
||||||
|
|
||||||
|
connection.send_message(
|
||||||
|
websocket_api.event_message(
|
||||||
|
msg["id"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": change,
|
||||||
|
"entry": entry_json(entry),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
current_entries = await async_matching_config_entries(hass, type_filter, None)
|
||||||
|
connection.subscriptions[msg["id"]] = async_dispatcher_connect(
|
||||||
|
hass,
|
||||||
|
config_entries.SIGNAL_CONFIG_ENTRY_CHANGED,
|
||||||
|
async_forward_config_entry_changes,
|
||||||
|
)
|
||||||
|
connection.send_result(msg["id"])
|
||||||
|
connection.send_message(
|
||||||
|
websocket_api.event_message(
|
||||||
|
msg["id"], [{"type": None, "entry": entry} for entry in current_entries]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_matching_config_entries(
|
async def async_matching_config_entries(
|
||||||
hass: HomeAssistant, type_filter: str | None, domain: str | None
|
hass: HomeAssistant, type_filter: str | None, domain: str | None
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
|
@ -19,6 +19,7 @@ from .const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, Platfo
|
|||||||
from .core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback
|
from .core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback
|
||||||
from .exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError
|
from .exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError
|
||||||
from .helpers import device_registry, entity_registry, storage
|
from .helpers import device_registry, entity_registry, storage
|
||||||
|
from .helpers.dispatcher import async_dispatcher_send
|
||||||
from .helpers.event import async_call_later
|
from .helpers.event import async_call_later
|
||||||
from .helpers.frame import report
|
from .helpers.frame import report
|
||||||
from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType
|
from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType
|
||||||
@ -136,6 +137,16 @@ RECONFIGURE_NOTIFICATION_ID = "config_entry_reconfigure"
|
|||||||
|
|
||||||
EVENT_FLOW_DISCOVERED = "config_entry_discovered"
|
EVENT_FLOW_DISCOVERED = "config_entry_discovered"
|
||||||
|
|
||||||
|
SIGNAL_CONFIG_ENTRY_CHANGED = "config_entry_changed"
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigEntryChange(StrEnum):
|
||||||
|
"""What was changed in a config entry."""
|
||||||
|
|
||||||
|
ADDED = "added"
|
||||||
|
REMOVED = "removed"
|
||||||
|
UPDATED = "updated"
|
||||||
|
|
||||||
|
|
||||||
class ConfigEntryDisabler(StrEnum):
|
class ConfigEntryDisabler(StrEnum):
|
||||||
"""What disabled a config entry."""
|
"""What disabled a config entry."""
|
||||||
@ -310,7 +321,7 @@ class ConfigEntry:
|
|||||||
|
|
||||||
# Only store setup result as state if it was not forwarded.
|
# Only store setup result as state if it was not forwarded.
|
||||||
if self.domain == integration.domain:
|
if self.domain == integration.domain:
|
||||||
self.state = ConfigEntryState.SETUP_IN_PROGRESS
|
self.async_set_state(hass, ConfigEntryState.SETUP_IN_PROGRESS, None)
|
||||||
|
|
||||||
self.supports_unload = await support_entry_unload(hass, self.domain)
|
self.supports_unload = await support_entry_unload(hass, self.domain)
|
||||||
self.supports_remove_device = await support_remove_from_device(
|
self.supports_remove_device = await support_remove_from_device(
|
||||||
@ -327,8 +338,7 @@ class ConfigEntry:
|
|||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
if self.domain == integration.domain:
|
if self.domain == integration.domain:
|
||||||
self.state = ConfigEntryState.SETUP_ERROR
|
self.async_set_state(hass, ConfigEntryState.SETUP_ERROR, "Import error")
|
||||||
self.reason = "Import error"
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.domain == integration.domain:
|
if self.domain == integration.domain:
|
||||||
@ -341,14 +351,12 @@ class ConfigEntry:
|
|||||||
self.domain,
|
self.domain,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
self.state = ConfigEntryState.SETUP_ERROR
|
self.async_set_state(hass, ConfigEntryState.SETUP_ERROR, "Import error")
|
||||||
self.reason = "Import error"
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Perform migration
|
# Perform migration
|
||||||
if not await self.async_migrate(hass):
|
if not await self.async_migrate(hass):
|
||||||
self.state = ConfigEntryState.MIGRATION_ERROR
|
self.async_set_state(hass, ConfigEntryState.MIGRATION_ERROR, None)
|
||||||
self.reason = None
|
|
||||||
return
|
return
|
||||||
|
|
||||||
error_reason = None
|
error_reason = None
|
||||||
@ -378,8 +386,7 @@ class ConfigEntry:
|
|||||||
self.async_start_reauth(hass)
|
self.async_start_reauth(hass)
|
||||||
result = False
|
result = False
|
||||||
except ConfigEntryNotReady as ex:
|
except ConfigEntryNotReady as ex:
|
||||||
self.state = ConfigEntryState.SETUP_RETRY
|
self.async_set_state(hass, ConfigEntryState.SETUP_RETRY, str(ex) or None)
|
||||||
self.reason = str(ex) or None
|
|
||||||
wait_time = 2 ** min(tries, 4) * 5
|
wait_time = 2 ** min(tries, 4) * 5
|
||||||
tries += 1
|
tries += 1
|
||||||
message = str(ex)
|
message = str(ex)
|
||||||
@ -427,11 +434,9 @@ class ConfigEntry:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
self.state = ConfigEntryState.LOADED
|
self.async_set_state(hass, ConfigEntryState.LOADED, None)
|
||||||
self.reason = None
|
|
||||||
else:
|
else:
|
||||||
self.state = ConfigEntryState.SETUP_ERROR
|
self.async_set_state(hass, ConfigEntryState.SETUP_ERROR, error_reason)
|
||||||
self.reason = error_reason
|
|
||||||
|
|
||||||
async def async_shutdown(self) -> None:
|
async def async_shutdown(self) -> None:
|
||||||
"""Call when Home Assistant is stopping."""
|
"""Call when Home Assistant is stopping."""
|
||||||
@ -452,8 +457,7 @@ class ConfigEntry:
|
|||||||
Returns if unload is possible and was successful.
|
Returns if unload is possible and was successful.
|
||||||
"""
|
"""
|
||||||
if self.source == SOURCE_IGNORE:
|
if self.source == SOURCE_IGNORE:
|
||||||
self.state = ConfigEntryState.NOT_LOADED
|
self.async_set_state(hass, ConfigEntryState.NOT_LOADED, None)
|
||||||
self.reason = None
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if self.state == ConfigEntryState.NOT_LOADED:
|
if self.state == ConfigEntryState.NOT_LOADED:
|
||||||
@ -467,8 +471,7 @@ class ConfigEntry:
|
|||||||
# that was uninstalled, or an integration
|
# that was uninstalled, or an integration
|
||||||
# that has been renamed without removing the config
|
# that has been renamed without removing the config
|
||||||
# entry.
|
# entry.
|
||||||
self.state = ConfigEntryState.NOT_LOADED
|
self.async_set_state(hass, ConfigEntryState.NOT_LOADED, None)
|
||||||
self.reason = None
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
component = integration.get_component()
|
component = integration.get_component()
|
||||||
@ -479,17 +482,16 @@ class ConfigEntry:
|
|||||||
|
|
||||||
if self.state is not ConfigEntryState.LOADED:
|
if self.state is not ConfigEntryState.LOADED:
|
||||||
self.async_cancel_retry_setup()
|
self.async_cancel_retry_setup()
|
||||||
|
self.async_set_state(hass, ConfigEntryState.NOT_LOADED, None)
|
||||||
self.state = ConfigEntryState.NOT_LOADED
|
|
||||||
self.reason = None
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
supports_unload = hasattr(component, "async_unload_entry")
|
supports_unload = hasattr(component, "async_unload_entry")
|
||||||
|
|
||||||
if not supports_unload:
|
if not supports_unload:
|
||||||
if integration.domain == self.domain:
|
if integration.domain == self.domain:
|
||||||
self.state = ConfigEntryState.FAILED_UNLOAD
|
self.async_set_state(
|
||||||
self.reason = "Unload not supported"
|
hass, ConfigEntryState.FAILED_UNLOAD, "Unload not supported"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -499,20 +501,20 @@ class ConfigEntry:
|
|||||||
|
|
||||||
# Only adjust state if we unloaded the component
|
# Only adjust state if we unloaded the component
|
||||||
if result and integration.domain == self.domain:
|
if result and integration.domain == self.domain:
|
||||||
self.state = ConfigEntryState.NOT_LOADED
|
self.async_set_state(hass, ConfigEntryState.NOT_LOADED, None)
|
||||||
self.reason = None
|
|
||||||
|
|
||||||
await self._async_process_on_unload()
|
await self._async_process_on_unload()
|
||||||
|
|
||||||
# https://github.com/python/mypy/issues/11839
|
# https://github.com/python/mypy/issues/11839
|
||||||
return result # type: ignore[no-any-return]
|
return result # type: ignore[no-any-return]
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception as ex: # pylint: disable=broad-except
|
||||||
_LOGGER.exception(
|
_LOGGER.exception(
|
||||||
"Error unloading entry %s for %s", self.title, integration.domain
|
"Error unloading entry %s for %s", self.title, integration.domain
|
||||||
)
|
)
|
||||||
if integration.domain == self.domain:
|
if integration.domain == self.domain:
|
||||||
self.state = ConfigEntryState.FAILED_UNLOAD
|
self.async_set_state(
|
||||||
self.reason = "Unknown error"
|
hass, ConfigEntryState.FAILED_UNLOAD, str(ex) or "Unknown error"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def async_remove(self, hass: HomeAssistant) -> None:
|
async def async_remove(self, hass: HomeAssistant) -> None:
|
||||||
@ -541,6 +543,17 @@ class ConfigEntry:
|
|||||||
integration.domain,
|
integration.domain,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_set_state(
|
||||||
|
self, hass: HomeAssistant, state: ConfigEntryState, reason: str | None
|
||||||
|
) -> None:
|
||||||
|
"""Set the state of the config entry."""
|
||||||
|
self.state = state
|
||||||
|
self.reason = reason
|
||||||
|
async_dispatcher_send(
|
||||||
|
hass, SIGNAL_CONFIG_ENTRY_CHANGED, ConfigEntryChange.UPDATED, self
|
||||||
|
)
|
||||||
|
|
||||||
async def async_migrate(self, hass: HomeAssistant) -> bool:
|
async def async_migrate(self, hass: HomeAssistant) -> bool:
|
||||||
"""Migrate an entry.
|
"""Migrate an entry.
|
||||||
|
|
||||||
@ -895,6 +908,7 @@ class ConfigEntries:
|
|||||||
)
|
)
|
||||||
self._entries[entry.entry_id] = entry
|
self._entries[entry.entry_id] = entry
|
||||||
self._domain_index.setdefault(entry.domain, []).append(entry.entry_id)
|
self._domain_index.setdefault(entry.domain, []).append(entry.entry_id)
|
||||||
|
self._async_dispatch(ConfigEntryChange.ADDED, entry)
|
||||||
await self.async_setup(entry.entry_id)
|
await self.async_setup(entry.entry_id)
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
|
|
||||||
@ -950,6 +964,7 @@ class ConfigEntries:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._async_dispatch(ConfigEntryChange.REMOVED, entry)
|
||||||
return {"require_restart": not unload_success}
|
return {"require_restart": not unload_success}
|
||||||
|
|
||||||
async def _async_shutdown(self, event: Event) -> None:
|
async def _async_shutdown(self, event: Event) -> None:
|
||||||
@ -1161,9 +1176,18 @@ class ConfigEntries:
|
|||||||
self.hass.async_create_task(listener(self.hass, entry))
|
self.hass.async_create_task(listener(self.hass, entry))
|
||||||
|
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
|
self._async_dispatch(ConfigEntryChange.UPDATED, entry)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_dispatch(
|
||||||
|
self, change_type: ConfigEntryChange, entry: ConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Dispatch a config entry change."""
|
||||||
|
async_dispatcher_send(
|
||||||
|
self.hass, SIGNAL_CONFIG_ENTRY_CHANGED, change_type, entry
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_setup_platforms(
|
def async_setup_platforms(
|
||||||
self, entry: ConfigEntry, platforms: Iterable[Platform | str]
|
self, entry: ConfigEntry, platforms: Iterable[Platform | str]
|
||||||
|
@ -1303,3 +1303,317 @@ async def test_get_entries_ws(hass, hass_ws_client, clear_handlers):
|
|||||||
|
|
||||||
assert response["id"] == 8
|
assert response["id"] == 8
|
||||||
assert response["success"] is False
|
assert response["success"] is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_subscribe_entries_ws(hass, hass_ws_client, clear_handlers):
|
||||||
|
"""Test subscribe entries with the websocket api."""
|
||||||
|
assert await async_setup_component(hass, "config", {})
|
||||||
|
mock_integration(hass, MockModule("comp1"))
|
||||||
|
mock_integration(
|
||||||
|
hass, MockModule("comp2", partial_manifest={"integration_type": "helper"})
|
||||||
|
)
|
||||||
|
mock_integration(hass, MockModule("comp3"))
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain="comp1",
|
||||||
|
title="Test 1",
|
||||||
|
source="bla",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
MockConfigEntry(
|
||||||
|
domain="comp2",
|
||||||
|
title="Test 2",
|
||||||
|
source="bla2",
|
||||||
|
state=core_ce.ConfigEntryState.SETUP_ERROR,
|
||||||
|
reason="Unsupported API",
|
||||||
|
).add_to_hass(hass)
|
||||||
|
MockConfigEntry(
|
||||||
|
domain="comp3",
|
||||||
|
title="Test 3",
|
||||||
|
source="bla3",
|
||||||
|
disabled_by=core_ce.ConfigEntryDisabler.USER,
|
||||||
|
).add_to_hass(hass)
|
||||||
|
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "config_entries/subscribe",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["id"] == 5
|
||||||
|
assert response["result"] is None
|
||||||
|
assert response["success"] is True
|
||||||
|
assert response["type"] == "result"
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["id"] == 5
|
||||||
|
assert response["event"] == [
|
||||||
|
{
|
||||||
|
"type": None,
|
||||||
|
"entry": {
|
||||||
|
"disabled_by": None,
|
||||||
|
"domain": "comp1",
|
||||||
|
"entry_id": ANY,
|
||||||
|
"pref_disable_new_entities": False,
|
||||||
|
"pref_disable_polling": False,
|
||||||
|
"reason": None,
|
||||||
|
"source": "bla",
|
||||||
|
"state": "not_loaded",
|
||||||
|
"supports_options": False,
|
||||||
|
"supports_remove_device": False,
|
||||||
|
"supports_unload": False,
|
||||||
|
"title": "Test 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": None,
|
||||||
|
"entry": {
|
||||||
|
"disabled_by": None,
|
||||||
|
"domain": "comp2",
|
||||||
|
"entry_id": ANY,
|
||||||
|
"pref_disable_new_entities": False,
|
||||||
|
"pref_disable_polling": False,
|
||||||
|
"reason": "Unsupported API",
|
||||||
|
"source": "bla2",
|
||||||
|
"state": "setup_error",
|
||||||
|
"supports_options": False,
|
||||||
|
"supports_remove_device": False,
|
||||||
|
"supports_unload": False,
|
||||||
|
"title": "Test 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": None,
|
||||||
|
"entry": {
|
||||||
|
"disabled_by": "user",
|
||||||
|
"domain": "comp3",
|
||||||
|
"entry_id": ANY,
|
||||||
|
"pref_disable_new_entities": False,
|
||||||
|
"pref_disable_polling": False,
|
||||||
|
"reason": None,
|
||||||
|
"source": "bla3",
|
||||||
|
"state": "not_loaded",
|
||||||
|
"supports_options": False,
|
||||||
|
"supports_remove_device": False,
|
||||||
|
"supports_unload": False,
|
||||||
|
"title": "Test 3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
assert hass.config_entries.async_update_entry(entry, title="changed")
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["id"] == 5
|
||||||
|
assert response["event"] == [
|
||||||
|
{
|
||||||
|
"entry": {
|
||||||
|
"disabled_by": None,
|
||||||
|
"domain": "comp1",
|
||||||
|
"entry_id": ANY,
|
||||||
|
"pref_disable_new_entities": False,
|
||||||
|
"pref_disable_polling": False,
|
||||||
|
"reason": None,
|
||||||
|
"source": "bla",
|
||||||
|
"state": "not_loaded",
|
||||||
|
"supports_options": False,
|
||||||
|
"supports_remove_device": False,
|
||||||
|
"supports_unload": False,
|
||||||
|
"title": "changed",
|
||||||
|
},
|
||||||
|
"type": "updated",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
await hass.config_entries.async_remove(entry.entry_id)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["id"] == 5
|
||||||
|
assert response["event"] == [
|
||||||
|
{
|
||||||
|
"entry": {
|
||||||
|
"disabled_by": None,
|
||||||
|
"domain": "comp1",
|
||||||
|
"entry_id": ANY,
|
||||||
|
"pref_disable_new_entities": False,
|
||||||
|
"pref_disable_polling": False,
|
||||||
|
"reason": None,
|
||||||
|
"source": "bla",
|
||||||
|
"state": "not_loaded",
|
||||||
|
"supports_options": False,
|
||||||
|
"supports_remove_device": False,
|
||||||
|
"supports_unload": False,
|
||||||
|
"title": "changed",
|
||||||
|
},
|
||||||
|
"type": "removed",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
await hass.config_entries.async_add(entry)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["id"] == 5
|
||||||
|
assert response["event"] == [
|
||||||
|
{
|
||||||
|
"entry": {
|
||||||
|
"disabled_by": None,
|
||||||
|
"domain": "comp1",
|
||||||
|
"entry_id": ANY,
|
||||||
|
"pref_disable_new_entities": False,
|
||||||
|
"pref_disable_polling": False,
|
||||||
|
"reason": None,
|
||||||
|
"source": "bla",
|
||||||
|
"state": "not_loaded",
|
||||||
|
"supports_options": False,
|
||||||
|
"supports_remove_device": False,
|
||||||
|
"supports_unload": False,
|
||||||
|
"title": "changed",
|
||||||
|
},
|
||||||
|
"type": "added",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_subscribe_entries_ws_filtered(hass, hass_ws_client, clear_handlers):
|
||||||
|
"""Test subscribe entries with the websocket api with a type filter."""
|
||||||
|
assert await async_setup_component(hass, "config", {})
|
||||||
|
mock_integration(hass, MockModule("comp1"))
|
||||||
|
mock_integration(
|
||||||
|
hass, MockModule("comp2", partial_manifest={"integration_type": "helper"})
|
||||||
|
)
|
||||||
|
mock_integration(hass, MockModule("comp3"))
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain="comp1",
|
||||||
|
title="Test 1",
|
||||||
|
source="bla",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
entry2 = MockConfigEntry(
|
||||||
|
domain="comp2",
|
||||||
|
title="Test 2",
|
||||||
|
source="bla2",
|
||||||
|
state=core_ce.ConfigEntryState.SETUP_ERROR,
|
||||||
|
reason="Unsupported API",
|
||||||
|
)
|
||||||
|
entry2.add_to_hass(hass)
|
||||||
|
MockConfigEntry(
|
||||||
|
domain="comp3",
|
||||||
|
title="Test 3",
|
||||||
|
source="bla3",
|
||||||
|
disabled_by=core_ce.ConfigEntryDisabler.USER,
|
||||||
|
).add_to_hass(hass)
|
||||||
|
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "config_entries/subscribe",
|
||||||
|
"type_filter": "integration",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["id"] == 5
|
||||||
|
assert response["result"] is None
|
||||||
|
assert response["success"] is True
|
||||||
|
assert response["type"] == "result"
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["id"] == 5
|
||||||
|
assert response["event"] == [
|
||||||
|
{
|
||||||
|
"type": None,
|
||||||
|
"entry": {
|
||||||
|
"disabled_by": None,
|
||||||
|
"domain": "comp1",
|
||||||
|
"entry_id": ANY,
|
||||||
|
"pref_disable_new_entities": False,
|
||||||
|
"pref_disable_polling": False,
|
||||||
|
"reason": None,
|
||||||
|
"source": "bla",
|
||||||
|
"state": "not_loaded",
|
||||||
|
"supports_options": False,
|
||||||
|
"supports_remove_device": False,
|
||||||
|
"supports_unload": False,
|
||||||
|
"title": "Test 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": None,
|
||||||
|
"entry": {
|
||||||
|
"disabled_by": "user",
|
||||||
|
"domain": "comp3",
|
||||||
|
"entry_id": ANY,
|
||||||
|
"pref_disable_new_entities": False,
|
||||||
|
"pref_disable_polling": False,
|
||||||
|
"reason": None,
|
||||||
|
"source": "bla3",
|
||||||
|
"state": "not_loaded",
|
||||||
|
"supports_options": False,
|
||||||
|
"supports_remove_device": False,
|
||||||
|
"supports_unload": False,
|
||||||
|
"title": "Test 3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
assert hass.config_entries.async_update_entry(entry, title="changed")
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["id"] == 5
|
||||||
|
assert response["event"] == [
|
||||||
|
{
|
||||||
|
"entry": {
|
||||||
|
"disabled_by": None,
|
||||||
|
"domain": "comp1",
|
||||||
|
"entry_id": ANY,
|
||||||
|
"pref_disable_new_entities": False,
|
||||||
|
"pref_disable_polling": False,
|
||||||
|
"reason": None,
|
||||||
|
"source": "bla",
|
||||||
|
"state": "not_loaded",
|
||||||
|
"supports_options": False,
|
||||||
|
"supports_remove_device": False,
|
||||||
|
"supports_unload": False,
|
||||||
|
"title": "changed",
|
||||||
|
},
|
||||||
|
"type": "updated",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
await hass.config_entries.async_remove(entry.entry_id)
|
||||||
|
await hass.config_entries.async_remove(entry2.entry_id)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["id"] == 5
|
||||||
|
assert response["event"] == [
|
||||||
|
{
|
||||||
|
"entry": {
|
||||||
|
"disabled_by": None,
|
||||||
|
"domain": "comp1",
|
||||||
|
"entry_id": ANY,
|
||||||
|
"pref_disable_new_entities": False,
|
||||||
|
"pref_disable_polling": False,
|
||||||
|
"reason": None,
|
||||||
|
"source": "bla",
|
||||||
|
"state": "not_loaded",
|
||||||
|
"supports_options": False,
|
||||||
|
"supports_remove_device": False,
|
||||||
|
"supports_unload": False,
|
||||||
|
"title": "changed",
|
||||||
|
},
|
||||||
|
"type": "removed",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
await hass.config_entries.async_add(entry)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["id"] == 5
|
||||||
|
assert response["event"] == [
|
||||||
|
{
|
||||||
|
"entry": {
|
||||||
|
"disabled_by": None,
|
||||||
|
"domain": "comp1",
|
||||||
|
"entry_id": ANY,
|
||||||
|
"pref_disable_new_entities": False,
|
||||||
|
"pref_disable_polling": False,
|
||||||
|
"reason": None,
|
||||||
|
"source": "bla",
|
||||||
|
"state": "not_loaded",
|
||||||
|
"supports_options": False,
|
||||||
|
"supports_remove_device": False,
|
||||||
|
"supports_unload": False,
|
||||||
|
"title": "changed",
|
||||||
|
},
|
||||||
|
"type": "added",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user