Merge pull request #62182 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2021-12-17 13:43:37 +01:00 committed by GitHub
commit 7fc36c4fe0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 270 additions and 72 deletions

View File

@ -3,7 +3,7 @@
"name": "Brunt Blind Engine",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/brunt",
"requirements": ["brunt==1.0.1"],
"requirements": ["brunt==1.0.2"],
"codeowners": ["@eavanvalkenburg"],
"iot_class": "cloud_polling"
}

View File

@ -2,7 +2,7 @@
"domain": "ebusd",
"name": "ebusd",
"documentation": "https://www.home-assistant.io/integrations/ebusd",
"requirements": ["ebusdpy==0.0.16"],
"requirements": ["ebusdpy==0.0.17"],
"codeowners": [],
"iot_class": "local_polling"
}

View File

@ -94,6 +94,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
firmware_date=None,
model_info=None,
model_description=None,
remote_access_enabled=None,
remote_access_host=None,
remote_access_port=None,
)
return await self._async_handle_discovery()
@ -261,6 +264,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
firmware_date=None,
model_info=None,
model_description=bulb.model_data.description,
remote_access_enabled=None,
remote_access_host=None,
remote_access_port=None,
)

View File

@ -3,7 +3,7 @@
"name": "Flux LED/MagicHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.26.7"],
"requirements": ["flux_led==0.26.15"],
"quality_scale": "platinum",
"codeowners": ["@icemanch"],
"iot_class": "local_push",

View File

@ -120,10 +120,11 @@ class Light(HomeAccessory):
if self._event_timer:
self._event_timer()
self._event_timer = async_call_later(
self.hass, CHANGE_COALESCE_TIME_WINDOW, self._send_events
self.hass, CHANGE_COALESCE_TIME_WINDOW, self._async_send_events
)
def _send_events(self, *_):
@callback
def _async_send_events(self, *_):
"""Process all changes at once."""
_LOGGER.debug("Coalesced _set_chars: %s", self._pending_events)
char_values = self._pending_events

View File

@ -3,7 +3,7 @@
"name": "Philips Hue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue",
"requirements": ["aiohue==3.0.5"],
"requirements": ["aiohue==3.0.6"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

@ -146,8 +146,10 @@ async def hue_activate_scene_v2(
continue
# found match!
if transition:
transition = transition * 100 # in steps of 100ms
await api.scenes.recall(scene.id, dynamic=dynamic, duration=transition)
transition = transition * 1000 # transition is in ms
await bridge.async_request_call(
api.scenes.recall, scene.id, dynamic=dynamic, duration=transition
)
return True
LOGGER.debug(
"Unable to find scene %s for group %s on bridge %s",

View File

@ -29,6 +29,7 @@ from homeassistant.const import (
CONF_PORT,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
@ -44,6 +45,7 @@ from .const import (
CONF_KNX_INDIVIDUAL_ADDRESS,
CONF_KNX_ROUTING,
CONF_KNX_TUNNELING,
DATA_HASS_CONFIG,
DATA_KNX_CONFIG,
DOMAIN,
KNX_ADDRESS,
@ -195,6 +197,7 @@ SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Start the KNX integration."""
hass.data[DATA_HASS_CONFIG] = config
conf: ConfigType | None = config.get(DOMAIN)
if conf is None:
@ -251,15 +254,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
hass.config_entries.async_setup_platforms(
entry, [platform for platform in SUPPORTED_PLATFORMS if platform in config]
entry,
[
platform
for platform in SUPPORTED_PLATFORMS
if platform in config and platform is not Platform.NOTIFY
],
)
# set up notify platform, no entry support for notify component yet,
# have to use discovery to load platform.
if NotifySchema.PLATFORM in conf:
# set up notify platform, no entry support for notify component yet
if NotifySchema.PLATFORM in config:
hass.async_create_task(
discovery.async_load_platform(
hass, "notify", DOMAIN, conf[NotifySchema.PLATFORM], config
hass, "notify", DOMAIN, {}, hass.data[DATA_HASS_CONFIG]
)
)
@ -312,6 +319,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
platform
for platform in SUPPORTED_PLATFORMS
if platform in hass.data[DATA_KNX_CONFIG]
and platform is not Platform.NOTIFY
],
)
if unload_ok:

View File

@ -42,7 +42,10 @@ CONF_STATE_ADDRESS: Final = "state_address"
CONF_SYNC_STATE: Final = "sync_state"
CONF_KNX_INITIAL_CONNECTION_TYPES: Final = [CONF_KNX_TUNNELING, CONF_KNX_ROUTING]
# yaml config merged with config entry data
DATA_KNX_CONFIG: Final = "knx_config"
# original hass yaml config
DATA_HASS_CONFIG: Final = "knx_hass_config"
ATTR_COUNTER: Final = "counter"
ATTR_SOURCE: Final = "source"

View File

@ -11,7 +11,8 @@ from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN, KNX_ADDRESS
from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS
from .schema import NotifySchema
async def async_get_service(
@ -20,10 +21,10 @@ async def async_get_service(
discovery_info: DiscoveryInfoType | None = None,
) -> KNXNotificationService | None:
"""Get the KNX notification service."""
if not discovery_info:
if discovery_info is None:
return None
platform_config: dict = discovery_info
if platform_config := hass.data[DATA_KNX_CONFIG].get(NotifySchema.PLATFORM):
xknx: XKNX = hass.data[DOMAIN].xknx
notification_devices = []
@ -36,9 +37,13 @@ async def async_get_service(
)
)
return (
KNXNotificationService(notification_devices) if notification_devices else None
KNXNotificationService(notification_devices)
if notification_devices
else None
)
return None
class KNXNotificationService(BaseNotificationService):
"""Implement demo notification service."""

View File

@ -112,6 +112,7 @@ def log_entry(hass, name, message, domain=None, entity_id=None, context=None):
hass.add_job(async_log_entry, hass, name, message, domain, entity_id, context)
@callback
@bind_hass
def async_log_entry(hass, name, message, domain=None, entity_id=None, context=None):
"""Add an entry to the logbook."""

View File

@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["ffmpeg", "http", "media_source"],
"documentation": "https://www.home-assistant.io/integrations/nest",
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.8"],
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.9"],
"codeowners": ["@allenporter"],
"quality_scale": "platinum",
"dhcp": [

View File

@ -214,7 +214,7 @@ class NextBusDepartureSensor(SensorEntity):
# Generate list of upcoming times
self._attributes["upcoming"] = ", ".join(
sorted(p["minutes"] for p in predictions)
sorted((p["minutes"] for p in predictions), key=int)
)
latest_prediction = maybe_first(predictions)

View File

@ -1,6 +1,8 @@
"""The 1-Wire component."""
import logging
from pyownet import protocol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
@ -18,7 +20,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
onewirehub = OneWireHub(hass)
try:
await onewirehub.initialize(entry)
except CannotConnect as exc:
except (
CannotConnect, # Failed to connect to the server
protocol.OwnetError, # Connected to server, but failed to list the devices
) as exc:
raise ConfigEntryNotReady() from exc
hass.data[DOMAIN][entry.entry_id] = onewirehub

View File

@ -68,13 +68,12 @@ from .utils import (
BLOCK_PLATFORMS: Final = [
"binary_sensor",
"button",
"climate",
"cover",
"light",
"sensor",
"switch",
]
BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"]
BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "climate", "sensor"]
RPC_PLATFORMS: Final = ["binary_sensor", "button", "light", "sensor", "switch"]
_LOGGER: Final = logging.getLogger(__name__)

View File

@ -3,12 +3,13 @@ from __future__ import annotations
import asyncio
import logging
from types import MappingProxyType
from typing import Any, Final, cast
from aioshelly.block_device import Block
import async_timeout
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity
from homeassistant.components.climate.const import (
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
@ -20,11 +21,12 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.components.shelly import BlockDeviceWrapper
from homeassistant.components.shelly.entity import ShellyBlockEntity
from homeassistant.components.shelly.utils import get_device_entry_gen
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import device_registry, entity, entity_registry
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
@ -49,10 +51,30 @@ async def async_setup_entry(
if get_device_entry_gen(config_entry) == 2:
return
wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
][BLOCK]
if wrapper.device.initialized:
await async_setup_climate_entities(async_add_entities, wrapper)
else:
await async_restore_climate_entities(
hass, config_entry, async_add_entities, wrapper
)
async def async_setup_climate_entities(
async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper,
) -> None:
"""Set up online climate devices."""
_LOGGER.info("Setup online climate device %s", wrapper.name)
device_block: Block | None = None
sensor_block: Block | None = None
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK]
assert wrapper.device.blocks
for block in wrapper.device.blocks:
if block.type == "device":
device_block = block
@ -60,10 +82,37 @@ async def async_setup_entry(
sensor_block = block
if sensor_block and device_block:
async_add_entities([ShellyClimate(wrapper, sensor_block, device_block)])
async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)])
class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
async def async_restore_climate_entities(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper,
) -> None:
"""Restore sleeping climate devices."""
_LOGGER.info("Setup sleeping climate device %s", wrapper.name)
ent_reg = await entity_registry.async_get_registry(hass)
entries = entity_registry.async_entries_for_config_entry(
ent_reg, config_entry.entry_id
)
for entry in entries:
if entry.domain != CLIMATE_DOMAIN:
continue
_LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain)
async_add_entities([BlockSleepingClimate(wrapper, None, None, entry)])
class BlockSleepingClimate(
RestoreEntity,
ClimateEntity,
entity.Entity,
):
"""Representation of a Shelly climate device."""
_attr_hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_HEAT]
@ -74,45 +123,77 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
_attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"]
_attr_temperature_unit = TEMP_CELSIUS
# pylint: disable=super-init-not-called
def __init__(
self, wrapper: BlockDeviceWrapper, sensor_block: Block, device_block: Block
self,
wrapper: BlockDeviceWrapper,
sensor_block: Block | None,
device_block: Block | None,
entry: entity_registry.RegistryEntry | None = None,
) -> None:
"""Initialize climate."""
super().__init__(wrapper, sensor_block)
self.device_block = device_block
assert self.block.channel
self.wrapper = wrapper
self.block: Block | None = sensor_block
self.control_result: dict[str, Any] | None = None
self.device_block: Block | None = device_block
self.last_state: State | None = None
self.last_state_attributes: MappingProxyType[str, Any]
self._preset_modes: list[str] = []
self._attr_name = self.wrapper.name
self._attr_unique_id = self.wrapper.mac
self._attr_preset_modes: list[str] = [
if self.block is not None and self.device_block is not None:
self._unique_id = f"{self.wrapper.mac}-{self.block.description}"
assert self.block.channel
self._preset_modes = [
PRESET_NONE,
*wrapper.device.settings["thermostats"][int(self.block.channel)][
"schedule_profile_names"
],
]
elif entry is not None:
self._unique_id = entry.unique_id
@property
def unique_id(self) -> str:
"""Set unique id of entity."""
return self._unique_id
@property
def name(self) -> str:
"""Name of entity."""
return self.wrapper.name
@property
def should_poll(self) -> bool:
"""If device should be polled."""
return False
@property
def target_temperature(self) -> float | None:
"""Set target temperature."""
if self.block is not None:
return cast(float, self.block.targetTemp)
return self.last_state_attributes.get("temperature")
@property
def current_temperature(self) -> float | None:
"""Return current temperature."""
if self.block is not None:
return cast(float, self.block.temp)
return self.last_state_attributes.get("current_temperature")
@property
def available(self) -> bool:
"""Device availability."""
if self.device_block is not None:
return not cast(bool, self.device_block.valveError)
return self.wrapper.last_update_success
@property
def hvac_mode(self) -> str:
"""HVAC current mode."""
if self.device_block is None:
return self.last_state.state if self.last_state else HVAC_MODE_OFF
if self.device_block.mode is None or self._check_is_off():
return HVAC_MODE_OFF
@ -121,20 +202,45 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
@property
def preset_mode(self) -> str | None:
"""Preset current mode."""
if self.device_block is None:
return self.last_state_attributes.get("preset_mode")
if self.device_block.mode is None:
return None
return self._attr_preset_modes[cast(int, self.device_block.mode)]
return PRESET_NONE
return self._preset_modes[cast(int, self.device_block.mode)]
@property
def hvac_action(self) -> str | None:
"""HVAC current action."""
if self.device_block.status is None or self._check_is_off():
if (
self.device_block is None
or self.device_block.status is None
or self._check_is_off()
):
return CURRENT_HVAC_OFF
return (
CURRENT_HVAC_IDLE if self.device_block.status == "0" else CURRENT_HVAC_HEAT
)
@property
def preset_modes(self) -> list[str]:
"""Preset available modes."""
return self._preset_modes
@property
def device_info(self) -> DeviceInfo:
"""Device info."""
return {
"connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)}
}
@property
def channel(self) -> str | None:
"""Device channel."""
if self.block is not None:
return self.block.channel
return self.last_state_attributes.get("channel")
def _check_is_off(self) -> bool:
"""Return if valve is off or on."""
return bool(
@ -148,7 +254,7 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
return await self.wrapper.device.http_request(
"get", f"thermostat/{self.block.channel}", kwargs
"get", f"thermostat/{self.channel}", kwargs
)
except (asyncio.TimeoutError, OSError) as err:
_LOGGER.error(
@ -186,3 +292,41 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
await self.set_state_full_path(
schedule=1, schedule_profile=f"{preset_index}"
)
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
_LOGGER.info("Restoring entity %s", self.name)
last_state = await self.async_get_last_state()
if last_state is not None:
self.last_state = last_state
self.last_state_attributes = self.last_state.attributes
self._preset_modes = cast(
list, self.last_state.attributes.get("preset_modes")
)
self.async_on_remove(self.wrapper.async_add_listener(self._update_callback))
async def async_update(self) -> None:
"""Update entity with latest info."""
await self.wrapper.async_request_refresh()
@callback
def _update_callback(self) -> None:
"""Handle device update."""
if not self.wrapper.device.initialized:
self.async_write_ha_state()
return
assert self.wrapper.device.blocks
for block in self.wrapper.device.blocks:
if block.type == "device":
self.device_block = block
if hasattr(block, "targetTemp"):
self.block = block
_LOGGER.debug("Entity %s attached to block", self.name)
self.async_write_ha_state()
return

View File

@ -641,9 +641,13 @@ class LightTemplate(TemplateEntity, LightEntity):
@callback
def _update_color(self, render):
"""Update the hs_color from the template."""
if render is None:
self._color = None
return
h_str = s_str = None
if isinstance(render, str):
if render in (None, "None", ""):
if render in ("None", ""):
self._color = None
return
h_str, s_str = map(

View File

@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from . import ValloxDataUpdateCoordinator
from .const import (
@ -107,7 +108,7 @@ class ValloxFilterRemainingSensor(ValloxSensor):
days_remaining_delta = timedelta(days=days_remaining)
now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0)
return now + days_remaining_delta
return (now + days_remaining_delta).astimezone(dt_util.UTC)
class ValloxCellStateSensor(ValloxSensor):

View File

@ -234,6 +234,7 @@ class GroupProbe:
unsub()
self._unsubs.remove(unsub)
@callback
def _reprobe_group(self, group_id: int) -> None:
"""Reprobe a group for entities after its members change."""
zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY]

View File

@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)

View File

@ -1052,6 +1052,7 @@ class Script:
if self._change_listener_job:
self._hass.async_run_hass_job(self._change_listener_job)
@callback
def _chain_change_listener(self, sub_script: Script) -> None:
if sub_script.is_running:
self.last_action = sub_script.last_action

View File

@ -186,7 +186,7 @@ aiohomekit==0.6.4
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==3.0.5
aiohue==3.0.6
# homeassistant.components.imap
aioimaplib==0.9.0
@ -440,7 +440,7 @@ brother==1.1.0
brottsplatskartan==0.0.1
# homeassistant.components.brunt
brunt==1.0.1
brunt==1.0.2
# homeassistant.components.bsblan
bsblan==0.4.0
@ -573,7 +573,7 @@ dweepy==0.3.0
dynalite_devices==0.1.46
# homeassistant.components.ebusd
ebusdpy==0.0.16
ebusdpy==0.0.17
# homeassistant.components.ecoal_boiler
ecoaliface==0.4.0
@ -658,7 +658,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.26.7
flux_led==0.26.15
# homeassistant.components.homekit
fnvhash==0.1.0
@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0
google-cloud-texttospeech==0.4.0
# homeassistant.components.nest
google-nest-sdm==0.4.8
google-nest-sdm==0.4.9
# homeassistant.components.google_travel_time
googlemaps==2.5.1

View File

@ -131,7 +131,7 @@ aiohomekit==0.6.4
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==3.0.5
aiohue==3.0.6
# homeassistant.components.apache_kafka
aiokafka==0.6.0
@ -281,7 +281,7 @@ broadlink==0.18.0
brother==1.1.0
# homeassistant.components.brunt
brunt==1.0.1
brunt==1.0.2
# homeassistant.components.bsblan
bsblan==0.4.0
@ -399,7 +399,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.26.7
flux_led==0.26.15
# homeassistant.components.homekit
fnvhash==0.1.0
@ -461,7 +461,7 @@ google-api-python-client==1.6.4
google-cloud-pubsub==2.1.0
# homeassistant.components.nest
google-nest-sdm==0.4.8
google-nest-sdm==0.4.9
# homeassistant.components.google_travel_time
googlemaps==2.5.1

View File

@ -40,6 +40,7 @@ BASIC_RESULTS = {
{"minutes": "1", "epochTime": "1553807371000"},
{"minutes": "2", "epochTime": "1553807372000"},
{"minutes": "3", "epochTime": "1553807373000"},
{"minutes": "10", "epochTime": "1553807380000"},
],
},
}
@ -128,7 +129,7 @@ async def test_verify_valid_state(
assert state.attributes["route"] == VALID_ROUTE_TITLE
assert state.attributes["stop"] == VALID_STOP_TITLE
assert state.attributes["direction"] == "Outbound"
assert state.attributes["upcoming"] == "1, 2, 3"
assert state.attributes["upcoming"] == "1, 2, 3, 10"
async def test_message_dict(

View File

@ -1,6 +1,8 @@
"""Tests for 1-Wire config flow."""
import logging
from unittest.mock import MagicMock
from pyownet import protocol
import pytest
from homeassistant.components.onewire.const import DOMAIN
@ -19,6 +21,20 @@ async def test_owserver_connect_failure(hass: HomeAssistant, config_entry: Confi
assert not hass.data.get(DOMAIN)
async def test_owserver_listing_failure(
hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock
):
"""Test listing failure raises ConfigEntryNotReady."""
owproxy.return_value.dir.side_effect = protocol.OwnetError()
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert config_entry.state is ConfigEntryState.SETUP_RETRY
assert not hass.data.get(DOMAIN)
@pytest.mark.usefixtures("owproxy")
async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Test being able to unload an entry."""