mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 11:47:06 +00:00
Add support for disabling config entries (#46779)
This commit is contained in:
parent
2d70806035
commit
5e26bda52d
@ -1,7 +1,6 @@
|
|||||||
"""Http views to control the config manager."""
|
"""Http views to control the config manager."""
|
||||||
import aiohttp.web_exceptions
|
import aiohttp.web_exceptions
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import voluptuous_serialize
|
|
||||||
|
|
||||||
from homeassistant import config_entries, data_entry_flow
|
from homeassistant import config_entries, data_entry_flow
|
||||||
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT
|
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT
|
||||||
@ -10,7 +9,6 @@ from homeassistant.components.http import HomeAssistantView
|
|||||||
from homeassistant.const import HTTP_FORBIDDEN, HTTP_NOT_FOUND
|
from homeassistant.const import HTTP_FORBIDDEN, HTTP_NOT_FOUND
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.exceptions import Unauthorized
|
from homeassistant.exceptions import Unauthorized
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
from homeassistant.helpers.data_entry_flow import (
|
from homeassistant.helpers.data_entry_flow import (
|
||||||
FlowManagerIndexView,
|
FlowManagerIndexView,
|
||||||
FlowManagerResourceView,
|
FlowManagerResourceView,
|
||||||
@ -30,6 +28,7 @@ async def async_setup(hass):
|
|||||||
hass.http.register_view(OptionManagerFlowIndexView(hass.config_entries.options))
|
hass.http.register_view(OptionManagerFlowIndexView(hass.config_entries.options))
|
||||||
hass.http.register_view(OptionManagerFlowResourceView(hass.config_entries.options))
|
hass.http.register_view(OptionManagerFlowResourceView(hass.config_entries.options))
|
||||||
|
|
||||||
|
hass.components.websocket_api.async_register_command(config_entry_disable)
|
||||||
hass.components.websocket_api.async_register_command(config_entry_update)
|
hass.components.websocket_api.async_register_command(config_entry_update)
|
||||||
hass.components.websocket_api.async_register_command(config_entries_progress)
|
hass.components.websocket_api.async_register_command(config_entries_progress)
|
||||||
hass.components.websocket_api.async_register_command(system_options_list)
|
hass.components.websocket_api.async_register_command(system_options_list)
|
||||||
@ -39,24 +38,6 @@ async def async_setup(hass):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _prepare_json(result):
|
|
||||||
"""Convert result for JSON."""
|
|
||||||
if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
|
|
||||||
return result
|
|
||||||
|
|
||||||
data = result.copy()
|
|
||||||
|
|
||||||
schema = data["data_schema"]
|
|
||||||
if schema is None:
|
|
||||||
data["data_schema"] = []
|
|
||||||
else:
|
|
||||||
data["data_schema"] = voluptuous_serialize.convert(
|
|
||||||
schema, custom_serializer=cv.custom_serializer
|
|
||||||
)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigManagerEntryIndexView(HomeAssistantView):
|
class ConfigManagerEntryIndexView(HomeAssistantView):
|
||||||
"""View to get available config entries."""
|
"""View to get available config entries."""
|
||||||
|
|
||||||
@ -265,6 +246,21 @@ async def system_options_list(hass, connection, msg):
|
|||||||
connection.send_result(msg["id"], entry.system_options.as_dict())
|
connection.send_result(msg["id"], entry.system_options.as_dict())
|
||||||
|
|
||||||
|
|
||||||
|
def send_entry_not_found(connection, msg_id):
|
||||||
|
"""Send Config entry not found error."""
|
||||||
|
connection.send_error(
|
||||||
|
msg_id, websocket_api.const.ERR_NOT_FOUND, "Config entry not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_entry(hass, connection, entry_id, msg_id):
|
||||||
|
"""Get entry, send error message if it doesn't exist."""
|
||||||
|
entry = hass.config_entries.async_get_entry(entry_id)
|
||||||
|
if entry is None:
|
||||||
|
send_entry_not_found(connection, msg_id)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.require_admin
|
@websocket_api.require_admin
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
@ -279,13 +275,10 @@ async def system_options_update(hass, connection, msg):
|
|||||||
changes = dict(msg)
|
changes = dict(msg)
|
||||||
changes.pop("id")
|
changes.pop("id")
|
||||||
changes.pop("type")
|
changes.pop("type")
|
||||||
entry_id = changes.pop("entry_id")
|
changes.pop("entry_id")
|
||||||
entry = hass.config_entries.async_get_entry(entry_id)
|
|
||||||
|
|
||||||
|
entry = get_entry(hass, connection, msg["entry_id"], msg["id"])
|
||||||
if entry is None:
|
if entry is None:
|
||||||
connection.send_error(
|
|
||||||
msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
hass.config_entries.async_update_entry(entry, system_options=changes)
|
hass.config_entries.async_update_entry(entry, system_options=changes)
|
||||||
@ -302,20 +295,47 @@ async def config_entry_update(hass, connection, msg):
|
|||||||
changes = dict(msg)
|
changes = dict(msg)
|
||||||
changes.pop("id")
|
changes.pop("id")
|
||||||
changes.pop("type")
|
changes.pop("type")
|
||||||
entry_id = changes.pop("entry_id")
|
changes.pop("entry_id")
|
||||||
|
|
||||||
entry = hass.config_entries.async_get_entry(entry_id)
|
|
||||||
|
|
||||||
|
entry = get_entry(hass, connection, msg["entry_id"], msg["id"])
|
||||||
if entry is None:
|
if entry is None:
|
||||||
connection.send_error(
|
|
||||||
msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
hass.config_entries.async_update_entry(entry, **changes)
|
hass.config_entries.async_update_entry(entry, **changes)
|
||||||
connection.send_result(msg["id"], entry_json(entry))
|
connection.send_result(msg["id"], entry_json(entry))
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.async_response
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
"type": "config_entries/disable",
|
||||||
|
"entry_id": str,
|
||||||
|
# We only allow setting disabled_by user via API.
|
||||||
|
"disabled_by": vol.Any("user", None),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
async def config_entry_disable(hass, connection, msg):
|
||||||
|
"""Disable config entry."""
|
||||||
|
disabled_by = msg["disabled_by"]
|
||||||
|
|
||||||
|
result = False
|
||||||
|
try:
|
||||||
|
result = await hass.config_entries.async_set_disabled_by(
|
||||||
|
msg["entry_id"], disabled_by
|
||||||
|
)
|
||||||
|
except config_entries.OperationNotAllowed:
|
||||||
|
# Failed to unload the config entry
|
||||||
|
pass
|
||||||
|
except config_entries.UnknownEntry:
|
||||||
|
send_entry_not_found(connection, msg["id"])
|
||||||
|
return
|
||||||
|
|
||||||
|
result = {"require_restart": not result}
|
||||||
|
|
||||||
|
connection.send_result(msg["id"], result)
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.require_admin
|
@websocket_api.require_admin
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
@ -333,9 +353,7 @@ async def ignore_config_flow(hass, connection, msg):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if flow is None:
|
if flow is None:
|
||||||
connection.send_error(
|
send_entry_not_found(connection, msg["id"])
|
||||||
msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if "unique_id" not in flow["context"]:
|
if "unique_id" not in flow["context"]:
|
||||||
@ -357,7 +375,7 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict:
|
|||||||
"""Return JSON value of a config entry."""
|
"""Return JSON value of a config entry."""
|
||||||
handler = config_entries.HANDLERS.get(entry.domain)
|
handler = config_entries.HANDLERS.get(entry.domain)
|
||||||
supports_options = (
|
supports_options = (
|
||||||
# Guard in case handler is no longer registered (custom compnoent etc)
|
# Guard in case handler is no longer registered (custom component etc)
|
||||||
handler is not None
|
handler is not None
|
||||||
# pylint: disable=comparison-with-callable
|
# pylint: disable=comparison-with-callable
|
||||||
and handler.async_get_options_flow
|
and handler.async_get_options_flow
|
||||||
@ -372,4 +390,5 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict:
|
|||||||
"connection_class": entry.connection_class,
|
"connection_class": entry.connection_class,
|
||||||
"supports_options": supports_options,
|
"supports_options": supports_options,
|
||||||
"supports_unload": entry.supports_unload,
|
"supports_unload": entry.supports_unload,
|
||||||
|
"disabled_by": entry.disabled_by,
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import weakref
|
|||||||
import attr
|
import attr
|
||||||
|
|
||||||
from homeassistant import data_entry_flow, loader
|
from homeassistant import data_entry_flow, loader
|
||||||
|
from homeassistant.const import EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||||
from homeassistant.helpers import entity_registry
|
from homeassistant.helpers import entity_registry
|
||||||
@ -68,6 +69,8 @@ ENTRY_STATE_SETUP_RETRY = "setup_retry"
|
|||||||
ENTRY_STATE_NOT_LOADED = "not_loaded"
|
ENTRY_STATE_NOT_LOADED = "not_loaded"
|
||||||
# An error occurred when trying to unload the entry
|
# An error occurred when trying to unload the entry
|
||||||
ENTRY_STATE_FAILED_UNLOAD = "failed_unload"
|
ENTRY_STATE_FAILED_UNLOAD = "failed_unload"
|
||||||
|
# The config entry is disabled
|
||||||
|
ENTRY_STATE_DISABLED = "disabled"
|
||||||
|
|
||||||
UNRECOVERABLE_STATES = (ENTRY_STATE_MIGRATION_ERROR, ENTRY_STATE_FAILED_UNLOAD)
|
UNRECOVERABLE_STATES = (ENTRY_STATE_MIGRATION_ERROR, ENTRY_STATE_FAILED_UNLOAD)
|
||||||
|
|
||||||
@ -92,6 +95,8 @@ CONN_CLASS_LOCAL_POLL = "local_poll"
|
|||||||
CONN_CLASS_ASSUMED = "assumed"
|
CONN_CLASS_ASSUMED = "assumed"
|
||||||
CONN_CLASS_UNKNOWN = "unknown"
|
CONN_CLASS_UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
DISABLED_USER = "user"
|
||||||
|
|
||||||
RELOAD_AFTER_UPDATE_DELAY = 30
|
RELOAD_AFTER_UPDATE_DELAY = 30
|
||||||
|
|
||||||
|
|
||||||
@ -126,6 +131,7 @@ class ConfigEntry:
|
|||||||
"source",
|
"source",
|
||||||
"connection_class",
|
"connection_class",
|
||||||
"state",
|
"state",
|
||||||
|
"disabled_by",
|
||||||
"_setup_lock",
|
"_setup_lock",
|
||||||
"update_listeners",
|
"update_listeners",
|
||||||
"_async_cancel_retry_setup",
|
"_async_cancel_retry_setup",
|
||||||
@ -144,6 +150,7 @@ class ConfigEntry:
|
|||||||
unique_id: Optional[str] = None,
|
unique_id: Optional[str] = None,
|
||||||
entry_id: Optional[str] = None,
|
entry_id: Optional[str] = None,
|
||||||
state: str = ENTRY_STATE_NOT_LOADED,
|
state: str = ENTRY_STATE_NOT_LOADED,
|
||||||
|
disabled_by: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a config entry."""
|
"""Initialize a config entry."""
|
||||||
# Unique id of the config entry
|
# Unique id of the config entry
|
||||||
@ -179,6 +186,9 @@ class ConfigEntry:
|
|||||||
# Unique ID of this entry.
|
# Unique ID of this entry.
|
||||||
self.unique_id = unique_id
|
self.unique_id = unique_id
|
||||||
|
|
||||||
|
# Config entry is disabled
|
||||||
|
self.disabled_by = disabled_by
|
||||||
|
|
||||||
# Supports unload
|
# Supports unload
|
||||||
self.supports_unload = False
|
self.supports_unload = False
|
||||||
|
|
||||||
@ -198,7 +208,7 @@ class ConfigEntry:
|
|||||||
tries: int = 0,
|
tries: int = 0,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up an entry."""
|
"""Set up an entry."""
|
||||||
if self.source == SOURCE_IGNORE:
|
if self.source == SOURCE_IGNORE or self.disabled_by:
|
||||||
return
|
return
|
||||||
|
|
||||||
if integration is None:
|
if integration is None:
|
||||||
@ -441,6 +451,7 @@ class ConfigEntry:
|
|||||||
"source": self.source,
|
"source": self.source,
|
||||||
"connection_class": self.connection_class,
|
"connection_class": self.connection_class,
|
||||||
"unique_id": self.unique_id,
|
"unique_id": self.unique_id,
|
||||||
|
"disabled_by": self.disabled_by,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -711,6 +722,8 @@ class ConfigEntries:
|
|||||||
system_options=entry.get("system_options", {}),
|
system_options=entry.get("system_options", {}),
|
||||||
# New in 0.104
|
# New in 0.104
|
||||||
unique_id=entry.get("unique_id"),
|
unique_id=entry.get("unique_id"),
|
||||||
|
# New in 2021.3
|
||||||
|
disabled_by=entry.get("disabled_by"),
|
||||||
)
|
)
|
||||||
for entry in config["entries"]
|
for entry in config["entries"]
|
||||||
]
|
]
|
||||||
@ -759,13 +772,42 @@ class ConfigEntries:
|
|||||||
|
|
||||||
If an entry was not loaded, will just load.
|
If an entry was not loaded, will just load.
|
||||||
"""
|
"""
|
||||||
|
entry = self.async_get_entry(entry_id)
|
||||||
|
|
||||||
|
if entry is None:
|
||||||
|
raise UnknownEntry
|
||||||
|
|
||||||
unload_result = await self.async_unload(entry_id)
|
unload_result = await self.async_unload(entry_id)
|
||||||
|
|
||||||
if not unload_result:
|
if not unload_result or entry.disabled_by:
|
||||||
return unload_result
|
return unload_result
|
||||||
|
|
||||||
return await self.async_setup(entry_id)
|
return await self.async_setup(entry_id)
|
||||||
|
|
||||||
|
async def async_set_disabled_by(
|
||||||
|
self, entry_id: str, disabled_by: Optional[str]
|
||||||
|
) -> bool:
|
||||||
|
"""Disable an entry.
|
||||||
|
|
||||||
|
If disabled_by is changed, the config entry will be reloaded.
|
||||||
|
"""
|
||||||
|
entry = self.async_get_entry(entry_id)
|
||||||
|
|
||||||
|
if entry is None:
|
||||||
|
raise UnknownEntry
|
||||||
|
|
||||||
|
if entry.disabled_by == disabled_by:
|
||||||
|
return True
|
||||||
|
|
||||||
|
entry.disabled_by = disabled_by
|
||||||
|
self._async_schedule_save()
|
||||||
|
|
||||||
|
self.hass.bus.async_fire(
|
||||||
|
EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED, {"config_entry_id": entry_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self.async_reload(entry_id)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_update_entry(
|
def async_update_entry(
|
||||||
self,
|
self,
|
||||||
|
@ -202,6 +202,7 @@ CONF_ZONE = "zone"
|
|||||||
# #### EVENTS ####
|
# #### EVENTS ####
|
||||||
EVENT_CALL_SERVICE = "call_service"
|
EVENT_CALL_SERVICE = "call_service"
|
||||||
EVENT_COMPONENT_LOADED = "component_loaded"
|
EVENT_COMPONENT_LOADED = "component_loaded"
|
||||||
|
EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED = "config_entry_disabled_by_updated"
|
||||||
EVENT_CORE_CONFIG_UPDATE = "core_config_updated"
|
EVENT_CORE_CONFIG_UPDATE = "core_config_updated"
|
||||||
EVENT_HOMEASSISTANT_CLOSE = "homeassistant_close"
|
EVENT_HOMEASSISTANT_CLOSE = "homeassistant_close"
|
||||||
EVENT_HOMEASSISTANT_START = "homeassistant_start"
|
EVENT_HOMEASSISTANT_START = "homeassistant_start"
|
||||||
|
@ -6,7 +6,10 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union,
|
|||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
from homeassistant.const import (
|
||||||
|
EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED,
|
||||||
|
EVENT_HOMEASSISTANT_STARTED,
|
||||||
|
)
|
||||||
from homeassistant.core import Event, callback
|
from homeassistant.core import Event, callback
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
import homeassistant.util.uuid as uuid_util
|
import homeassistant.util.uuid as uuid_util
|
||||||
@ -37,6 +40,7 @@ IDX_IDENTIFIERS = "identifiers"
|
|||||||
REGISTERED_DEVICE = "registered"
|
REGISTERED_DEVICE = "registered"
|
||||||
DELETED_DEVICE = "deleted"
|
DELETED_DEVICE = "deleted"
|
||||||
|
|
||||||
|
DISABLED_CONFIG_ENTRY = "config_entry"
|
||||||
DISABLED_INTEGRATION = "integration"
|
DISABLED_INTEGRATION = "integration"
|
||||||
DISABLED_USER = "user"
|
DISABLED_USER = "user"
|
||||||
|
|
||||||
@ -65,6 +69,7 @@ class DeviceEntry:
|
|||||||
default=None,
|
default=None,
|
||||||
validator=attr.validators.in_(
|
validator=attr.validators.in_(
|
||||||
(
|
(
|
||||||
|
DISABLED_CONFIG_ENTRY,
|
||||||
DISABLED_INTEGRATION,
|
DISABLED_INTEGRATION,
|
||||||
DISABLED_USER,
|
DISABLED_USER,
|
||||||
None,
|
None,
|
||||||
@ -138,6 +143,10 @@ class DeviceRegistry:
|
|||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||||
self._clear_index()
|
self._clear_index()
|
||||||
|
self.hass.bus.async_listen(
|
||||||
|
EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED,
|
||||||
|
self.async_config_entry_disabled_by_changed,
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_get(self, device_id: str) -> Optional[DeviceEntry]:
|
def async_get(self, device_id: str) -> Optional[DeviceEntry]:
|
||||||
@ -609,6 +618,38 @@ class DeviceRegistry:
|
|||||||
if area_id == device.area_id:
|
if area_id == device.area_id:
|
||||||
self._async_update_device(dev_id, area_id=None)
|
self._async_update_device(dev_id, area_id=None)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_config_entry_disabled_by_changed(self, event: Event) -> None:
|
||||||
|
"""Handle a config entry being disabled or enabled.
|
||||||
|
|
||||||
|
Disable devices in the registry that are associated to a config entry when
|
||||||
|
the config entry is disabled.
|
||||||
|
"""
|
||||||
|
config_entry = self.hass.config_entries.async_get_entry(
|
||||||
|
event.data["config_entry_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# The config entry may be deleted already if the event handling is late
|
||||||
|
if not config_entry:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not config_entry.disabled_by:
|
||||||
|
devices = async_entries_for_config_entry(
|
||||||
|
self, event.data["config_entry_id"]
|
||||||
|
)
|
||||||
|
for device in devices:
|
||||||
|
if device.disabled_by != DISABLED_CONFIG_ENTRY:
|
||||||
|
continue
|
||||||
|
self.async_update_device(device.id, disabled_by=None)
|
||||||
|
return
|
||||||
|
|
||||||
|
devices = async_entries_for_config_entry(self, event.data["config_entry_id"])
|
||||||
|
for device in devices:
|
||||||
|
if device.disabled:
|
||||||
|
# Entity already disabled, do not overwrite
|
||||||
|
continue
|
||||||
|
self.async_update_device(device.id, disabled_by=DISABLED_CONFIG_ENTRY)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_get(hass: HomeAssistantType) -> DeviceRegistry:
|
def async_get(hass: HomeAssistantType) -> DeviceRegistry:
|
||||||
|
@ -31,6 +31,7 @@ from homeassistant.const import (
|
|||||||
ATTR_RESTORED,
|
ATTR_RESTORED,
|
||||||
ATTR_SUPPORTED_FEATURES,
|
ATTR_SUPPORTED_FEATURES,
|
||||||
ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
|
EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED,
|
||||||
EVENT_HOMEASSISTANT_START,
|
EVENT_HOMEASSISTANT_START,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
)
|
)
|
||||||
@ -157,6 +158,10 @@ class EntityRegistry:
|
|||||||
self.hass.bus.async_listen(
|
self.hass.bus.async_listen(
|
||||||
EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_modified
|
EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_modified
|
||||||
)
|
)
|
||||||
|
self.hass.bus.async_listen(
|
||||||
|
EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED,
|
||||||
|
self.async_config_entry_disabled_by_changed,
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_get_device_class_lookup(self, domain_device_classes: set) -> dict:
|
def async_get_device_class_lookup(self, domain_device_classes: set) -> dict:
|
||||||
@ -349,10 +354,49 @@ class EntityRegistry:
|
|||||||
self.async_update_entity(entity.entity_id, disabled_by=None)
|
self.async_update_entity(entity.entity_id, disabled_by=None)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if device.disabled_by == dr.DISABLED_CONFIG_ENTRY:
|
||||||
|
# Handled by async_config_entry_disabled
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fetch entities which are not already disabled
|
||||||
entities = async_entries_for_device(self, event.data["device_id"])
|
entities = async_entries_for_device(self, event.data["device_id"])
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
self.async_update_entity(entity.entity_id, disabled_by=DISABLED_DEVICE)
|
self.async_update_entity(entity.entity_id, disabled_by=DISABLED_DEVICE)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_config_entry_disabled_by_changed(self, event: Event) -> None:
|
||||||
|
"""Handle a config entry being disabled or enabled.
|
||||||
|
|
||||||
|
Disable entities in the registry that are associated to a config entry when
|
||||||
|
the config entry is disabled.
|
||||||
|
"""
|
||||||
|
config_entry = self.hass.config_entries.async_get_entry(
|
||||||
|
event.data["config_entry_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# The config entry may be deleted already if the event handling is late
|
||||||
|
if not config_entry:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not config_entry.disabled_by:
|
||||||
|
entities = async_entries_for_config_entry(
|
||||||
|
self, event.data["config_entry_id"]
|
||||||
|
)
|
||||||
|
for entity in entities:
|
||||||
|
if entity.disabled_by != DISABLED_CONFIG_ENTRY:
|
||||||
|
continue
|
||||||
|
self.async_update_entity(entity.entity_id, disabled_by=None)
|
||||||
|
return
|
||||||
|
|
||||||
|
entities = async_entries_for_config_entry(self, event.data["config_entry_id"])
|
||||||
|
for entity in entities:
|
||||||
|
if entity.disabled:
|
||||||
|
# Entity already disabled, do not overwrite
|
||||||
|
continue
|
||||||
|
self.async_update_entity(
|
||||||
|
entity.entity_id, disabled_by=DISABLED_CONFIG_ENTRY
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_update_entity(
|
def async_update_entity(
|
||||||
self,
|
self,
|
||||||
|
@ -749,6 +749,7 @@ class MockConfigEntry(config_entries.ConfigEntry):
|
|||||||
system_options={},
|
system_options={},
|
||||||
connection_class=config_entries.CONN_CLASS_UNKNOWN,
|
connection_class=config_entries.CONN_CLASS_UNKNOWN,
|
||||||
unique_id=None,
|
unique_id=None,
|
||||||
|
disabled_by=None,
|
||||||
):
|
):
|
||||||
"""Initialize a mock config entry."""
|
"""Initialize a mock config entry."""
|
||||||
kwargs = {
|
kwargs = {
|
||||||
@ -761,6 +762,7 @@ class MockConfigEntry(config_entries.ConfigEntry):
|
|||||||
"title": title,
|
"title": title,
|
||||||
"connection_class": connection_class,
|
"connection_class": connection_class,
|
||||||
"unique_id": unique_id,
|
"unique_id": unique_id,
|
||||||
|
"disabled_by": disabled_by,
|
||||||
}
|
}
|
||||||
if source is not None:
|
if source is not None:
|
||||||
kwargs["source"] = source
|
kwargs["source"] = source
|
||||||
|
@ -68,6 +68,12 @@ async def test_get_entries(hass, client):
|
|||||||
state=core_ce.ENTRY_STATE_LOADED,
|
state=core_ce.ENTRY_STATE_LOADED,
|
||||||
connection_class=core_ce.CONN_CLASS_ASSUMED,
|
connection_class=core_ce.CONN_CLASS_ASSUMED,
|
||||||
).add_to_hass(hass)
|
).add_to_hass(hass)
|
||||||
|
MockConfigEntry(
|
||||||
|
domain="comp3",
|
||||||
|
title="Test 3",
|
||||||
|
source="bla3",
|
||||||
|
disabled_by="user",
|
||||||
|
).add_to_hass(hass)
|
||||||
|
|
||||||
resp = await client.get("/api/config/config_entries/entry")
|
resp = await client.get("/api/config/config_entries/entry")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@ -83,6 +89,7 @@ async def test_get_entries(hass, client):
|
|||||||
"connection_class": "local_poll",
|
"connection_class": "local_poll",
|
||||||
"supports_options": True,
|
"supports_options": True,
|
||||||
"supports_unload": True,
|
"supports_unload": True,
|
||||||
|
"disabled_by": None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": "comp2",
|
"domain": "comp2",
|
||||||
@ -92,6 +99,17 @@ async def test_get_entries(hass, client):
|
|||||||
"connection_class": "assumed",
|
"connection_class": "assumed",
|
||||||
"supports_options": False,
|
"supports_options": False,
|
||||||
"supports_unload": False,
|
"supports_unload": False,
|
||||||
|
"disabled_by": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domain": "comp3",
|
||||||
|
"title": "Test 3",
|
||||||
|
"source": "bla3",
|
||||||
|
"state": "not_loaded",
|
||||||
|
"connection_class": "unknown",
|
||||||
|
"supports_options": False,
|
||||||
|
"supports_unload": False,
|
||||||
|
"disabled_by": "user",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -680,6 +698,25 @@ async def test_update_system_options(hass, hass_ws_client):
|
|||||||
assert entry.system_options.disable_new_entities
|
assert entry.system_options.disable_new_entities
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_system_options_nonexisting(hass, hass_ws_client):
|
||||||
|
"""Test that we can update entry."""
|
||||||
|
assert await async_setup_component(hass, "config", {})
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "config_entries/system_options/update",
|
||||||
|
"entry_id": "non_existing",
|
||||||
|
"disable_new_entities": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert not response["success"]
|
||||||
|
assert response["error"]["code"] == "not_found"
|
||||||
|
|
||||||
|
|
||||||
async def test_update_entry(hass, hass_ws_client):
|
async def test_update_entry(hass, hass_ws_client):
|
||||||
"""Test that we can update entry."""
|
"""Test that we can update entry."""
|
||||||
assert await async_setup_component(hass, "config", {})
|
assert await async_setup_component(hass, "config", {})
|
||||||
@ -722,6 +759,83 @@ async def test_update_entry_nonexisting(hass, hass_ws_client):
|
|||||||
assert response["error"]["code"] == "not_found"
|
assert response["error"]["code"] == "not_found"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_disable_entry(hass, hass_ws_client):
|
||||||
|
"""Test that we can disable entry."""
|
||||||
|
assert await async_setup_component(hass, "config", {})
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
entry = MockConfigEntry(domain="demo", state="loaded")
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
assert entry.disabled_by is None
|
||||||
|
|
||||||
|
# Disable
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "config_entries/disable",
|
||||||
|
"entry_id": entry.entry_id,
|
||||||
|
"disabled_by": "user",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert response["success"]
|
||||||
|
assert response["result"] == {"require_restart": True}
|
||||||
|
assert entry.disabled_by == "user"
|
||||||
|
assert entry.state == "failed_unload"
|
||||||
|
|
||||||
|
# Enable
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"type": "config_entries/disable",
|
||||||
|
"entry_id": entry.entry_id,
|
||||||
|
"disabled_by": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert response["success"]
|
||||||
|
assert response["result"] == {"require_restart": True}
|
||||||
|
assert entry.disabled_by is None
|
||||||
|
assert entry.state == "failed_unload"
|
||||||
|
|
||||||
|
# Enable again -> no op
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"type": "config_entries/disable",
|
||||||
|
"entry_id": entry.entry_id,
|
||||||
|
"disabled_by": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert response["success"]
|
||||||
|
assert response["result"] == {"require_restart": False}
|
||||||
|
assert entry.disabled_by is None
|
||||||
|
assert entry.state == "failed_unload"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_disable_entry_nonexisting(hass, hass_ws_client):
|
||||||
|
"""Test that we can disable entry."""
|
||||||
|
assert await async_setup_component(hass, "config", {})
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "config_entries/disable",
|
||||||
|
"entry_id": "non_existing",
|
||||||
|
"disabled_by": "user",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert not response["success"]
|
||||||
|
assert response["error"]["code"] == "not_found"
|
||||||
|
|
||||||
|
|
||||||
async def test_ignore_flow(hass, hass_ws_client):
|
async def test_ignore_flow(hass, hass_ws_client):
|
||||||
"""Test we can ignore a flow."""
|
"""Test we can ignore a flow."""
|
||||||
assert await async_setup_component(hass, "config", {})
|
assert await async_setup_component(hass, "config", {})
|
||||||
@ -763,3 +877,22 @@ async def test_ignore_flow(hass, hass_ws_client):
|
|||||||
assert entry.source == "ignore"
|
assert entry.source == "ignore"
|
||||||
assert entry.unique_id == "mock-unique-id"
|
assert entry.unique_id == "mock-unique-id"
|
||||||
assert entry.title == "Test Integration"
|
assert entry.title == "Test Integration"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ignore_flow_nonexisting(hass, hass_ws_client):
|
||||||
|
"""Test we can ignore a flow."""
|
||||||
|
assert await async_setup_component(hass, "config", {})
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "config_entries/ignore_flow",
|
||||||
|
"flow_id": "non_existing",
|
||||||
|
"title": "Test Integration",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert not response["success"]
|
||||||
|
assert response["error"]["code"] == "not_found"
|
||||||
|
@ -1209,3 +1209,41 @@ async def test_verify_suggested_area_does_not_overwrite_area_id(
|
|||||||
suggested_area="New Game Room",
|
suggested_area="New Game Room",
|
||||||
)
|
)
|
||||||
assert entry2.area_id == game_room_area.id
|
assert entry2.area_id == game_room_area.id
|
||||||
|
|
||||||
|
|
||||||
|
async def test_disable_config_entry_disables_devices(hass, registry):
|
||||||
|
"""Test that we disable entities tied to a config entry."""
|
||||||
|
config_entry = MockConfigEntry(domain="light")
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
entry1 = registry.async_get_or_create(
|
||||||
|
config_entry_id=config_entry.entry_id,
|
||||||
|
connections={("mac", "12:34:56:AB:CD:EF")},
|
||||||
|
)
|
||||||
|
entry2 = registry.async_get_or_create(
|
||||||
|
config_entry_id=config_entry.entry_id,
|
||||||
|
connections={("mac", "34:56:AB:CD:EF:12")},
|
||||||
|
disabled_by="user",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not entry1.disabled
|
||||||
|
assert entry2.disabled
|
||||||
|
|
||||||
|
await hass.config_entries.async_set_disabled_by(config_entry.entry_id, "user")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entry1 = registry.async_get(entry1.id)
|
||||||
|
assert entry1.disabled
|
||||||
|
assert entry1.disabled_by == "config_entry"
|
||||||
|
entry2 = registry.async_get(entry2.id)
|
||||||
|
assert entry2.disabled
|
||||||
|
assert entry2.disabled_by == "user"
|
||||||
|
|
||||||
|
await hass.config_entries.async_set_disabled_by(config_entry.entry_id, None)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entry1 = registry.async_get(entry1.id)
|
||||||
|
assert not entry1.disabled
|
||||||
|
entry2 = registry.async_get(entry2.id)
|
||||||
|
assert entry2.disabled
|
||||||
|
assert entry2.disabled_by == "user"
|
||||||
|
@ -757,9 +757,18 @@ async def test_disable_device_disables_entities(hass, registry):
|
|||||||
device_id=device_entry.id,
|
device_id=device_entry.id,
|
||||||
disabled_by="user",
|
disabled_by="user",
|
||||||
)
|
)
|
||||||
|
entry3 = registry.async_get_or_create(
|
||||||
|
"light",
|
||||||
|
"hue",
|
||||||
|
"EFGH",
|
||||||
|
config_entry=config_entry,
|
||||||
|
device_id=device_entry.id,
|
||||||
|
disabled_by="config_entry",
|
||||||
|
)
|
||||||
|
|
||||||
assert not entry1.disabled
|
assert not entry1.disabled
|
||||||
assert entry2.disabled
|
assert entry2.disabled
|
||||||
|
assert entry3.disabled
|
||||||
|
|
||||||
device_registry.async_update_device(device_entry.id, disabled_by="user")
|
device_registry.async_update_device(device_entry.id, disabled_by="user")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -770,6 +779,9 @@ async def test_disable_device_disables_entities(hass, registry):
|
|||||||
entry2 = registry.async_get(entry2.entity_id)
|
entry2 = registry.async_get(entry2.entity_id)
|
||||||
assert entry2.disabled
|
assert entry2.disabled
|
||||||
assert entry2.disabled_by == "user"
|
assert entry2.disabled_by == "user"
|
||||||
|
entry3 = registry.async_get(entry3.entity_id)
|
||||||
|
assert entry3.disabled
|
||||||
|
assert entry3.disabled_by == "config_entry"
|
||||||
|
|
||||||
device_registry.async_update_device(device_entry.id, disabled_by=None)
|
device_registry.async_update_device(device_entry.id, disabled_by=None)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -779,6 +791,74 @@ async def test_disable_device_disables_entities(hass, registry):
|
|||||||
entry2 = registry.async_get(entry2.entity_id)
|
entry2 = registry.async_get(entry2.entity_id)
|
||||||
assert entry2.disabled
|
assert entry2.disabled
|
||||||
assert entry2.disabled_by == "user"
|
assert entry2.disabled_by == "user"
|
||||||
|
entry3 = registry.async_get(entry3.entity_id)
|
||||||
|
assert entry3.disabled
|
||||||
|
assert entry3.disabled_by == "config_entry"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_disable_config_entry_disables_entities(hass, registry):
|
||||||
|
"""Test that we disable entities tied to a config entry."""
|
||||||
|
device_registry = mock_device_registry(hass)
|
||||||
|
config_entry = MockConfigEntry(domain="light")
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
device_entry = device_registry.async_get_or_create(
|
||||||
|
config_entry_id=config_entry.entry_id,
|
||||||
|
connections={("mac", "12:34:56:AB:CD:EF")},
|
||||||
|
)
|
||||||
|
|
||||||
|
entry1 = registry.async_get_or_create(
|
||||||
|
"light",
|
||||||
|
"hue",
|
||||||
|
"5678",
|
||||||
|
config_entry=config_entry,
|
||||||
|
device_id=device_entry.id,
|
||||||
|
)
|
||||||
|
entry2 = registry.async_get_or_create(
|
||||||
|
"light",
|
||||||
|
"hue",
|
||||||
|
"ABCD",
|
||||||
|
config_entry=config_entry,
|
||||||
|
device_id=device_entry.id,
|
||||||
|
disabled_by="user",
|
||||||
|
)
|
||||||
|
entry3 = registry.async_get_or_create(
|
||||||
|
"light",
|
||||||
|
"hue",
|
||||||
|
"EFGH",
|
||||||
|
config_entry=config_entry,
|
||||||
|
device_id=device_entry.id,
|
||||||
|
disabled_by="device",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not entry1.disabled
|
||||||
|
assert entry2.disabled
|
||||||
|
assert entry3.disabled
|
||||||
|
|
||||||
|
await hass.config_entries.async_set_disabled_by(config_entry.entry_id, "user")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entry1 = registry.async_get(entry1.entity_id)
|
||||||
|
assert entry1.disabled
|
||||||
|
assert entry1.disabled_by == "config_entry"
|
||||||
|
entry2 = registry.async_get(entry2.entity_id)
|
||||||
|
assert entry2.disabled
|
||||||
|
assert entry2.disabled_by == "user"
|
||||||
|
entry3 = registry.async_get(entry3.entity_id)
|
||||||
|
assert entry3.disabled
|
||||||
|
assert entry3.disabled_by == "device"
|
||||||
|
|
||||||
|
await hass.config_entries.async_set_disabled_by(config_entry.entry_id, None)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entry1 = registry.async_get(entry1.entity_id)
|
||||||
|
assert not entry1.disabled
|
||||||
|
entry2 = registry.async_get(entry2.entity_id)
|
||||||
|
assert entry2.disabled
|
||||||
|
assert entry2.disabled_by == "user"
|
||||||
|
# The device was re-enabled, so entity disabled by the device will be re-enabled too
|
||||||
|
entry3 = registry.async_get(entry3.entity_id)
|
||||||
|
assert not entry3.disabled_by
|
||||||
|
|
||||||
|
|
||||||
async def test_disabled_entities_excluded_from_entity_list(hass, registry):
|
async def test_disabled_entities_excluded_from_entity_list(hass, registry):
|
||||||
|
@ -1108,6 +1108,110 @@ async def test_entry_reload_error(hass, manager, state):
|
|||||||
assert entry.state == state
|
assert entry.state == state
|
||||||
|
|
||||||
|
|
||||||
|
async def test_entry_disable_succeed(hass, manager):
|
||||||
|
"""Test that we can disable an entry."""
|
||||||
|
entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
async_setup = AsyncMock(return_value=True)
|
||||||
|
async_setup_entry = AsyncMock(return_value=True)
|
||||||
|
async_unload_entry = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
mock_integration(
|
||||||
|
hass,
|
||||||
|
MockModule(
|
||||||
|
"comp",
|
||||||
|
async_setup=async_setup,
|
||||||
|
async_setup_entry=async_setup_entry,
|
||||||
|
async_unload_entry=async_unload_entry,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mock_entity_platform(hass, "config_flow.comp", None)
|
||||||
|
|
||||||
|
# Disable
|
||||||
|
assert await manager.async_set_disabled_by(
|
||||||
|
entry.entry_id, config_entries.DISABLED_USER
|
||||||
|
)
|
||||||
|
assert len(async_unload_entry.mock_calls) == 1
|
||||||
|
assert len(async_setup.mock_calls) == 0
|
||||||
|
assert len(async_setup_entry.mock_calls) == 0
|
||||||
|
assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED
|
||||||
|
|
||||||
|
# Enable
|
||||||
|
assert await manager.async_set_disabled_by(entry.entry_id, None)
|
||||||
|
assert len(async_unload_entry.mock_calls) == 1
|
||||||
|
assert len(async_setup.mock_calls) == 1
|
||||||
|
assert len(async_setup_entry.mock_calls) == 1
|
||||||
|
assert entry.state == config_entries.ENTRY_STATE_LOADED
|
||||||
|
|
||||||
|
|
||||||
|
async def test_entry_disable_without_reload_support(hass, manager):
|
||||||
|
"""Test that we can disable an entry without reload support."""
|
||||||
|
entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
async_setup = AsyncMock(return_value=True)
|
||||||
|
async_setup_entry = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
mock_integration(
|
||||||
|
hass,
|
||||||
|
MockModule(
|
||||||
|
"comp",
|
||||||
|
async_setup=async_setup,
|
||||||
|
async_setup_entry=async_setup_entry,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mock_entity_platform(hass, "config_flow.comp", None)
|
||||||
|
|
||||||
|
# Disable
|
||||||
|
assert not await manager.async_set_disabled_by(
|
||||||
|
entry.entry_id, config_entries.DISABLED_USER
|
||||||
|
)
|
||||||
|
assert len(async_setup.mock_calls) == 0
|
||||||
|
assert len(async_setup_entry.mock_calls) == 0
|
||||||
|
assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD
|
||||||
|
|
||||||
|
# Enable
|
||||||
|
with pytest.raises(config_entries.OperationNotAllowed):
|
||||||
|
await manager.async_set_disabled_by(entry.entry_id, None)
|
||||||
|
assert len(async_setup.mock_calls) == 0
|
||||||
|
assert len(async_setup_entry.mock_calls) == 0
|
||||||
|
assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD
|
||||||
|
|
||||||
|
|
||||||
|
async def test_entry_enable_without_reload_support(hass, manager):
|
||||||
|
"""Test that we can disable an entry without reload support."""
|
||||||
|
entry = MockConfigEntry(domain="comp", disabled_by=config_entries.DISABLED_USER)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
async_setup = AsyncMock(return_value=True)
|
||||||
|
async_setup_entry = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
mock_integration(
|
||||||
|
hass,
|
||||||
|
MockModule(
|
||||||
|
"comp",
|
||||||
|
async_setup=async_setup,
|
||||||
|
async_setup_entry=async_setup_entry,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mock_entity_platform(hass, "config_flow.comp", None)
|
||||||
|
|
||||||
|
# Enable
|
||||||
|
assert await manager.async_set_disabled_by(entry.entry_id, None)
|
||||||
|
assert len(async_setup.mock_calls) == 1
|
||||||
|
assert len(async_setup_entry.mock_calls) == 1
|
||||||
|
assert entry.state == config_entries.ENTRY_STATE_LOADED
|
||||||
|
|
||||||
|
# Disable
|
||||||
|
assert not await manager.async_set_disabled_by(
|
||||||
|
entry.entry_id, config_entries.DISABLED_USER
|
||||||
|
)
|
||||||
|
assert len(async_setup.mock_calls) == 1
|
||||||
|
assert len(async_setup_entry.mock_calls) == 1
|
||||||
|
assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD
|
||||||
|
|
||||||
|
|
||||||
async def test_init_custom_integration(hass):
|
async def test_init_custom_integration(hass):
|
||||||
"""Test initializing flow for custom integration."""
|
"""Test initializing flow for custom integration."""
|
||||||
integration = loader.Integration(
|
integration = loader.Integration(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user