Merge pull request #38894 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2020-08-15 07:35:34 +02:00 committed by GitHub
commit e8b7ddc966
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 186 additions and 86 deletions

View File

@ -20,6 +20,7 @@ from .const import (
COORDINATOR_RAIN,
DOMAIN,
PLATFORMS,
UNDO_UPDATE_LISTENER,
)
_LOGGER = logging.getLogger(__name__)
@ -77,15 +78,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
async def _async_update_data_forecast_forecast():
"""Fetch data from API endpoint."""
return await hass.async_add_job(client.get_forecast, latitude, longitude)
return await hass.async_add_executor_job(
client.get_forecast, latitude, longitude
)
async def _async_update_data_rain():
"""Fetch data from API endpoint."""
return await hass.async_add_job(client.get_rain, latitude, longitude)
return await hass.async_add_executor_job(client.get_rain, latitude, longitude)
async def _async_update_data_alert():
"""Fetch data from API endpoint."""
return await hass.async_add_job(
return await hass.async_add_executor_job(
client.get_warning_current_phenomenoms, department, 0, True
)
@ -156,10 +159,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
entry.title,
)
undo_listener = entry.add_update_listener(_async_update_listener)
hass.data[DOMAIN][entry.entry_id] = {
COORDINATOR_FORECAST: coordinator_forecast,
COORDINATOR_RAIN: coordinator_rain,
COORDINATOR_ALERT: coordinator_alert,
UNDO_UPDATE_LISTENER: undo_listener,
}
for platform in PLATFORMS:
@ -192,8 +198,14 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
)
)
if unload_ok:
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
hass.data[DOMAIN].pop(entry.entry_id)
if len(hass.data[DOMAIN]) == 0:
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
return unload_ok
async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -21,13 +21,18 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Init MeteoFranceFlowHandler."""
self.places = []
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return MeteoFranceOptionsFlowHandler(config_entry)
async def _show_setup_form(self, user_input=None, errors=None):
@callback
def _show_setup_form(self, user_input=None, errors=None):
"""Show the setup form to the user."""
if user_input is None:
@ -46,7 +51,7 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors = {}
if user_input is None:
return await self._show_setup_form(user_input, errors)
return self._show_setup_form(user_input, errors)
city = user_input[CONF_CITY] # Might be a city name or a postal code
latitude = user_input.get(CONF_LATITUDE)
@ -54,13 +59,15 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if not latitude:
client = MeteoFranceClient()
places = await self.hass.async_add_executor_job(client.search_places, city)
_LOGGER.debug("places search result: %s", places)
if not places:
self.places = await self.hass.async_add_executor_job(
client.search_places, city
)
_LOGGER.debug("Places search result: %s", self.places)
if not self.places:
errors[CONF_CITY] = "empty"
return await self._show_setup_form(user_input, errors)
return self._show_setup_form(user_input, errors)
return await self.async_step_cities(places=places)
return await self.async_step_cities()
# Check if already configured
await self.async_set_unique_id(f"{latitude}, {longitude}")
@ -74,19 +81,27 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Import a config entry."""
return await self.async_step_user(user_input)
async def async_step_cities(self, user_input=None, places=None):
async def async_step_cities(self, user_input=None):
"""Step where the user choose the city from the API search results."""
if places and len(places) > 1 and self.source != SOURCE_IMPORT:
places_for_form = {}
for place in places:
places_for_form[_build_place_key(place)] = f"{place}"
if not user_input:
if len(self.places) > 1 and self.source != SOURCE_IMPORT:
places_for_form = {}
for place in self.places:
places_for_form[_build_place_key(place)] = f"{place}"
return await self._show_cities_form(places_for_form)
# for import and only 1 city in the search result
if places and not user_input:
user_input = {CONF_CITY: _build_place_key(places[0])}
return self.async_show_form(
step_id="cities",
data_schema=vol.Schema(
{
vol.Required(CONF_CITY): vol.All(
vol.Coerce(str), vol.In(places_for_form)
)
}
),
)
user_input = {CONF_CITY: _build_place_key(self.places[0])}
city_infos = user_input.get(CONF_CITY).split(";")
city_infos = user_input[CONF_CITY].split(";")
return await self.async_step_user(
{
CONF_CITY: city_infos[0],
@ -95,15 +110,6 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
}
)
async def _show_cities_form(self, cities):
"""Show the form to choose the city."""
return self.async_show_form(
step_id="cities",
data_schema=vol.Schema(
{vol.Required(CONF_CITY): vol.All(vol.Coerce(str), vol.In(cities))}
),
)
class MeteoFranceOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow."""

View File

@ -1,6 +1,9 @@
"""Meteo-France component constants."""
from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
PRESSURE_HPA,
SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS,
@ -12,6 +15,7 @@ PLATFORMS = ["sensor", "weather"]
COORDINATOR_FORECAST = "coordinator_forecast"
COORDINATOR_RAIN = "coordinator_rain"
COORDINATOR_ALERT = "coordinator_alert"
UNDO_UPDATE_LISTENER = "undo_update_listener"
ATTRIBUTION = "Data provided by Météo-France"
CONF_CITY = "city"
@ -24,7 +28,7 @@ ATTR_NEXT_RAIN_1_HOUR_FORECAST = "1_hour_forecast"
ENTITY_NAME = "name"
ENTITY_UNIT = "unit"
ENTITY_ICON = "icon"
ENTITY_CLASS = "device_class"
ENTITY_DEVICE_CLASS = "device_class"
ENTITY_ENABLE = "enable"
ENTITY_API_DATA_PATH = "data_path"
@ -32,8 +36,8 @@ SENSOR_TYPES = {
"pressure": {
ENTITY_NAME: "Pressure",
ENTITY_UNIT: PRESSURE_HPA,
ENTITY_ICON: "mdi:gauge",
ENTITY_CLASS: "pressure",
ENTITY_ICON: None,
ENTITY_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
ENTITY_ENABLE: False,
ENTITY_API_DATA_PATH: "current_forecast:sea_level",
},
@ -41,7 +45,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Rain chance",
ENTITY_UNIT: UNIT_PERCENTAGE,
ENTITY_ICON: "mdi:weather-rainy",
ENTITY_CLASS: None,
ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "probability_forecast:rain:3h",
},
@ -49,7 +53,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Snow chance",
ENTITY_UNIT: UNIT_PERCENTAGE,
ENTITY_ICON: "mdi:weather-snowy",
ENTITY_CLASS: None,
ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "probability_forecast:snow:3h",
},
@ -57,7 +61,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Freeze chance",
ENTITY_UNIT: UNIT_PERCENTAGE,
ENTITY_ICON: "mdi:snowflake",
ENTITY_CLASS: None,
ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "probability_forecast:freezing",
},
@ -65,23 +69,23 @@ SENSOR_TYPES = {
ENTITY_NAME: "Wind speed",
ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR,
ENTITY_ICON: "mdi:weather-windy",
ENTITY_CLASS: None,
ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: False,
ENTITY_API_DATA_PATH: "current_forecast:wind:speed",
},
"next_rain": {
ENTITY_NAME: "Next rain",
ENTITY_UNIT: None,
ENTITY_ICON: "mdi:weather-pouring",
ENTITY_CLASS: "timestamp",
ENTITY_ICON: None,
ENTITY_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: None,
},
"temperature": {
ENTITY_NAME: "Temperature",
ENTITY_UNIT: TEMP_CELSIUS,
ENTITY_ICON: "mdi:thermometer",
ENTITY_CLASS: "temperature",
ENTITY_ICON: None,
ENTITY_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ENTITY_ENABLE: False,
ENTITY_API_DATA_PATH: "current_forecast:T:value",
},
@ -89,7 +93,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "UV",
ENTITY_UNIT: None,
ENTITY_ICON: "mdi:sunglasses",
ENTITY_CLASS: None,
ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "today_forecast:uv",
},
@ -97,7 +101,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Weather alert",
ENTITY_UNIT: None,
ENTITY_ICON: "mdi:weather-cloudy-alert",
ENTITY_CLASS: None,
ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: None,
},
@ -105,7 +109,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Daily precipitation",
ENTITY_UNIT: "mm",
ENTITY_ICON: "mdi:cup-water",
ENTITY_CLASS: None,
ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "today_forecast:precipitation:24h",
},
@ -113,7 +117,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Cloud cover",
ENTITY_UNIT: UNIT_PERCENTAGE,
ENTITY_ICON: "mdi:weather-partly-cloudy",
ENTITY_CLASS: None,
ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "current_forecast:clouds",
},
@ -128,7 +132,7 @@ CONDITION_CLASSES = {
"Brouillard",
"Brouillard givrant",
],
"hail": ["Risque de grêle"],
"hail": ["Risque de grêle", "Risque de grèle"],
"lightning": ["Risque d'orages", "Orages"],
"lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"],
"partlycloudy": [

View File

@ -21,7 +21,7 @@ from .const import (
COORDINATOR_RAIN,
DOMAIN,
ENTITY_API_DATA_PATH,
ENTITY_CLASS,
ENTITY_DEVICE_CLASS,
ENTITY_ENABLE,
ENTITY_ICON,
ENTITY_NAME,
@ -128,7 +128,7 @@ class MeteoFranceSensor(Entity):
@property
def device_class(self):
"""Return the device class."""
return SENSOR_TYPES[self._type][ENTITY_CLASS]
return SENSOR_TYPES[self._type][ENTITY_DEVICE_CLASS]
@property
def entity_registry_enabled_default(self) -> bool:
@ -170,9 +170,15 @@ class MeteoFranceRainSensor(MeteoFranceSensor):
@property
def state(self):
"""Return the state."""
next_rain_date_locale = self.coordinator.data.next_rain_date_locale()
# search first cadran with rain
next_rain = next(
(cadran for cadran in self.coordinator.data.forecast if cadran["rain"] > 1),
None,
)
return (
dt_util.as_local(next_rain_date_locale) if next_rain_date_locale else None
dt_util.utc_from_timestamp(next_rain["dt"]).isoformat()
if next_rain
else None
)
@property
@ -180,11 +186,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor):
"""Return the state attributes."""
return {
ATTR_NEXT_RAIN_1_HOUR_FORECAST: [
{
dt_util.as_local(
self.coordinator.data.timestamp_to_locale_time(item["dt"])
).strftime("%H:%M"): item["desc"]
}
{dt_util.utc_from_timestamp(item["dt"]).isoformat(): item["desc"]}
for item in self.coordinator.data.forecast
],
ATTR_ATTRIBUTION: ATTRIBUTION,

View File

@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODE, TEMP_CELSIUS
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util
from .const import (
ATTRIBUTION,
@ -134,9 +135,9 @@ class MeteoFranceWeather(WeatherEntity):
continue
forecast_data.append(
{
ATTR_FORECAST_TIME: self.coordinator.data.timestamp_to_locale_time(
ATTR_FORECAST_TIME: dt_util.utc_from_timestamp(
forecast["dt"]
),
).isoformat(),
ATTR_FORECAST_CONDITION: format_condition(
forecast["weather"]["desc"]
),

View File

@ -40,7 +40,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
"""Fetch data from OVO Energy."""
now = datetime.utcnow()
async with async_timeout.timeout(10):
return await client.get_daily_usage(now.strftime("%Y-%m"))
try:
await client.authenticate(
entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]
)
return await client.get_daily_usage(now.strftime("%Y-%m"))
except aiohttp.ClientError as exception:
_LOGGER.warning(exception)
return None
coordinator = DataUpdateCoordinator(
hass,

View File

@ -3,6 +3,6 @@
"name": "OVO Energy",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ovo_energy",
"requirements": ["ovoenergy==1.1.6"],
"requirements": ["ovoenergy==1.1.7"],
"codeowners": ["@timmo001"]
}

View File

@ -27,18 +27,34 @@ async def async_setup_entry(
]
client: OVOEnergy = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT]
currency = coordinator.data.electricity[
len(coordinator.data.electricity) - 1
].cost.currency_unit
entities = []
if coordinator.data:
if coordinator.data.electricity:
entities.append(OVOEnergyLastElectricityReading(coordinator, client))
entities.append(
OVOEnergyLastElectricityCost(
coordinator,
client,
coordinator.data.electricity[
len(coordinator.data.electricity) - 1
].cost.currency_unit,
)
)
if coordinator.data.gas:
entities.append(OVOEnergyLastGasReading(coordinator, client))
entities.append(
OVOEnergyLastGasCost(
coordinator,
client,
coordinator.data.gas[
len(coordinator.data.gas) - 1
].cost.currency_unit,
)
)
async_add_entities(
[
OVOEnergyLastElectricityReading(coordinator, client),
OVOEnergyLastGasReading(coordinator, client),
OVOEnergyLastElectricityCost(coordinator, client, currency),
OVOEnergyLastGasCost(coordinator, client, currency),
],
True,
entities, True,
)

View File

@ -172,7 +172,7 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
else:
# transition specified by user
new_value = max(0, min(7620, kwargs[ATTR_TRANSITION]))
new_value = int(max(0, min(7620, kwargs[ATTR_TRANSITION])))
if ozw_version < (1, 6, 1205):
transition = kwargs[ATTR_TRANSITION]
if transition <= 127:

View File

@ -3,7 +3,7 @@ import asyncio
from uuid import UUID
from simplipy import API
from simplipy.errors import InvalidCredentialsError, SimplipyError
from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError
from simplipy.websocket import (
EVENT_CAMERA_MOTION_DETECTED,
EVENT_CONNECTION_LOST,
@ -555,6 +555,13 @@ class SimpliSafe:
LOGGER.error("Error while using stored refresh token: %s", err)
return
if isinstance(result, EndpointUnavailable):
# In case the user attempt an action not allowed in their current plan,
# we merely log that message at INFO level (so the user is aware,
# but not spammed with ERROR messages that they cannot change):
LOGGER.info(result)
return
if isinstance(result, SimplipyError):
LOGGER.error("SimpliSafe error while updating: %s", result)
return

View File

@ -3,6 +3,6 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==9.2.2"],
"requirements": ["simplisafe-python==9.3.0"],
"codeowners": ["@bachya"]
}

View File

@ -1,4 +1,5 @@
"""Open ports in your router for Home Assistant and provide statistics."""
import asyncio
from ipaddress import ip_address
from operator import itemgetter
@ -106,7 +107,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
# discover and construct
udn = config_entry.data.get(CONFIG_ENTRY_UDN)
st = config_entry.data.get(CONFIG_ENTRY_ST) # pylint: disable=invalid-name
device = await async_discover_and_construct(hass, udn, st)
try:
device = await async_discover_and_construct(hass, udn, st)
except asyncio.TimeoutError:
raise ConfigEntryNotReady
if not device:
_LOGGER.info("Unable to create UPnP/IGD, aborting")
raise ConfigEntryNotReady

View File

@ -3,7 +3,7 @@
"name": "Belkin WeMo",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wemo",
"requirements": ["pywemo==0.4.45"],
"requirements": ["pywemo==0.4.46"],
"ssdp": [
{
"manufacturer": "Belkin International Inc."

View File

@ -8,6 +8,7 @@ import voluptuous as vol
from zeroconf import (
DNSPointer,
DNSRecord,
Error as ZeroconfError,
InterfaceChoice,
IPVersion,
NonUniqueNameException,
@ -208,7 +209,12 @@ def setup(hass, config):
if state_change != ServiceStateChange.Added:
return
service_info = zeroconf.get_service_info(service_type, name)
try:
service_info = zeroconf.get_service_info(service_type, name)
except ZeroconfError:
_LOGGER.exception("Failed to get info for device %s", name)
return
if not service_info:
# Prevent the browser thread from collapsing as
# service_info can be None

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 114
PATCH_VERSION = "0"
PATCH_VERSION = "1"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 1)

View File

@ -23,8 +23,8 @@ if TYPE_CHECKING:
SLOW_SETUP_WARNING = 10
SLOW_SETUP_MAX_WAIT = 60
SLOW_ADD_ENTITY_MAX_WAIT = 10 # Per Entity
SLOW_ADD_MIN_TIMEOUT = 60
SLOW_ADD_ENTITY_MAX_WAIT = 15 # Per Entity
SLOW_ADD_MIN_TIMEOUT = 500
PLATFORM_NOT_READY_RETRIES = 10
DATA_ENTITY_PLATFORM = "entity_platform"

View File

@ -11,6 +11,18 @@ from homeassistant import bootstrap
from homeassistant.core import callback
from homeassistant.helpers.frame import warn_use
#
# Python 3.8 has significantly less workers by default
# than Python 3.7. In order to be consistent between
# supported versions, we need to set max_workers.
#
# In most cases the workers are not I/O bound, as they
# are sleeping/blocking waiting for data from integrations
# updating so this number should be higher than the default
# use case.
#
MAX_EXECUTOR_WORKERS = 64
@dataclasses.dataclass
class RuntimeConfig:
@ -57,7 +69,9 @@ class HassEventLoopPolicy(PolicyBase): # type: ignore
if self.debug:
loop.set_debug(True)
executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
executor = ThreadPoolExecutor(
thread_name_prefix="SyncWorker", max_workers=MAX_EXECUTOR_WORKERS
)
loop.set_default_executor(executor)
loop.set_default_executor = warn_use( # type: ignore
loop.set_default_executor, "sets default executor on the event loop"

View File

@ -1029,7 +1029,7 @@ oru==0.1.11
orvibo==1.1.1
# homeassistant.components.ovo_energy
ovoenergy==1.1.6
ovoenergy==1.1.7
# homeassistant.components.mqtt
# homeassistant.components.shiftr
@ -1837,7 +1837,7 @@ pyvolumio==0.1.1
pywebpush==1.9.2
# homeassistant.components.wemo
pywemo==0.4.45
pywemo==0.4.46
# homeassistant.components.xeoma
pyxeoma==1.4.1
@ -1963,7 +1963,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
simplisafe-python==9.2.2
simplisafe-python==9.3.0
# homeassistant.components.sisyphus
sisyphus-control==2.2.1

View File

@ -473,7 +473,7 @@ onvif-zeep-async==0.4.0
openerz-api==0.1.0
# homeassistant.components.ovo_energy
ovoenergy==1.1.6
ovoenergy==1.1.7
# homeassistant.components.mqtt
# homeassistant.components.shiftr
@ -878,7 +878,7 @@ sentry-sdk==0.13.5
simplehound==0.3
# homeassistant.components.simplisafe
simplisafe-python==9.2.2
simplisafe-python==9.3.0
# homeassistant.components.sleepiq
sleepyq==0.7

View File

@ -85,7 +85,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
assert state.state == "off"
# Test turn on without brightness
new_transition = 127
new_transition = 127.0
await hass.services.async_call(
"light",
"turn_on",

View File

@ -1,6 +1,12 @@
"""Test Zeroconf component setup process."""
import pytest
from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange
from zeroconf import (
BadTypeInNameException,
InterfaceChoice,
IPVersion,
ServiceInfo,
ServiceStateChange,
)
from homeassistant.components import zeroconf
from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6
@ -175,6 +181,20 @@ async def test_setup_with_ipv6_default(hass, mock_zeroconf):
assert mock_zeroconf.called_with()
async def test_service_with_invalid_name(hass, mock_zeroconf, caplog):
"""Test we do not crash on service with an invalid name."""
with patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
) as mock_service_browser:
mock_zeroconf.get_service_info.side_effect = BadTypeInNameException
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert len(mock_service_browser.mock_calls) == 1
assert "Failed to get info for device name" in caplog.text
async def test_homekit_match_partial_space(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.dict(