mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
2024.5.3 (#117203)
This commit is contained in:
commit
9b6500582a
@ -60,8 +60,8 @@
|
||||
"description": "Type of push notification to send to list members."
|
||||
},
|
||||
"item": {
|
||||
"name": "Item (Required if message type `Breaking news` selected)",
|
||||
"description": "Item name to include in a breaking news message e.g. `Breaking news - Please get cilantro!`"
|
||||
"name": "Article (Required if message type `Urgent Message` selected)",
|
||||
"description": "Article name to include in an urgent message e.g. `Urgent Message - Please buy Cilantro urgently`"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -69,10 +69,10 @@
|
||||
"selector": {
|
||||
"notification_type_selector": {
|
||||
"options": {
|
||||
"going_shopping": "I'm going shopping! - Last chance for adjustments",
|
||||
"changed_list": "List changed - Check it out",
|
||||
"shopping_done": "Shopping done - you can relax",
|
||||
"urgent_message": "Breaking news - Please get `item`!"
|
||||
"going_shopping": "I'm going shopping! - Last chance to make changes",
|
||||
"changed_list": "List updated - Take a look at the articles",
|
||||
"shopping_done": "Shopping done - The fridge is well stocked",
|
||||
"urgent_message": "Urgent Message - Please buy `Article name` urgently"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.9", "deebot-client==7.1.0"]
|
||||
"requirements": ["py-sucks==0.9.9", "deebot-client==7.2.0"]
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"requirements": ["pyenphase==1.20.1"],
|
||||
"requirements": ["pyenphase==1.20.3"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/goodwe",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["goodwe"],
|
||||
"requirements": ["goodwe==0.3.4"]
|
||||
"requirements": ["goodwe==0.3.5"]
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ NUMBERS = (
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_step=1,
|
||||
native_min_value=0,
|
||||
native_max_value=100,
|
||||
native_max_value=200,
|
||||
getter=lambda inv: inv.get_grid_export_limit(),
|
||||
setter=lambda inv, val: inv.set_grid_export_limit(val),
|
||||
filter=lambda inv: _get_setting_unit(inv, "grid_export_limit") == "%",
|
||||
|
@ -95,7 +95,10 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
_LOGGER.error(err)
|
||||
raise AbortFlow(
|
||||
"addon_set_config_failed",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": addon_manager.addon_name,
|
||||
},
|
||||
) from err
|
||||
|
||||
async def _async_get_addon_info(self, addon_manager: AddonManager) -> AddonInfo:
|
||||
|
@ -212,13 +212,15 @@ class HomeKitWindowCover(HomeKitEntity, CoverEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self) -> int:
|
||||
def current_cover_tilt_position(self) -> int | None:
|
||||
"""Return current position of cover tilt."""
|
||||
tilt_position = self.service.value(CharacteristicsTypes.VERTICAL_TILT_CURRENT)
|
||||
if not tilt_position:
|
||||
tilt_position = self.service.value(
|
||||
CharacteristicsTypes.HORIZONTAL_TILT_CURRENT
|
||||
)
|
||||
if tilt_position is None:
|
||||
return None
|
||||
# Recalculate to convert from arcdegree scale to percentage scale.
|
||||
if self.is_vertical_tilt:
|
||||
scale = 0.9
|
||||
|
@ -317,7 +317,7 @@ class EnsureJobAfterCooldown:
|
||||
self._loop = asyncio.get_running_loop()
|
||||
self._timeout = timeout
|
||||
self._callback = callback_job
|
||||
self._task: asyncio.Future | None = None
|
||||
self._task: asyncio.Task | None = None
|
||||
self._timer: asyncio.TimerHandle | None = None
|
||||
|
||||
def set_timeout(self, timeout: float) -> None:
|
||||
@ -332,28 +332,23 @@ class EnsureJobAfterCooldown:
|
||||
_LOGGER.error("%s", ha_error)
|
||||
|
||||
@callback
|
||||
def _async_task_done(self, task: asyncio.Future) -> None:
|
||||
def _async_task_done(self, task: asyncio.Task) -> None:
|
||||
"""Handle task done."""
|
||||
self._task = None
|
||||
|
||||
@callback
|
||||
def _async_execute(self) -> None:
|
||||
def async_execute(self) -> asyncio.Task:
|
||||
"""Execute the job."""
|
||||
if self._task:
|
||||
# Task already running,
|
||||
# so we schedule another run
|
||||
self.async_schedule()
|
||||
return
|
||||
return self._task
|
||||
|
||||
self._async_cancel_timer()
|
||||
self._task = create_eager_task(self._async_job())
|
||||
self._task.add_done_callback(self._async_task_done)
|
||||
|
||||
async def async_fire(self) -> None:
|
||||
"""Execute the job immediately."""
|
||||
if self._task:
|
||||
await self._task
|
||||
self._async_execute()
|
||||
return self._task
|
||||
|
||||
@callback
|
||||
def _async_cancel_timer(self) -> None:
|
||||
@ -368,7 +363,7 @@ class EnsureJobAfterCooldown:
|
||||
# We want to reschedule the timer in the future
|
||||
# every time this is called.
|
||||
self._async_cancel_timer()
|
||||
self._timer = self._loop.call_later(self._timeout, self._async_execute)
|
||||
self._timer = self._loop.call_later(self._timeout, self.async_execute)
|
||||
|
||||
async def async_cleanup(self) -> None:
|
||||
"""Cleanup any pending task."""
|
||||
@ -497,6 +492,9 @@ class MQTT:
|
||||
mqttc.on_subscribe = self._async_mqtt_on_callback
|
||||
mqttc.on_unsubscribe = self._async_mqtt_on_callback
|
||||
|
||||
# suppress exceptions at callback
|
||||
mqttc.suppress_exceptions = True
|
||||
|
||||
if will := self.conf.get(CONF_WILL_MESSAGE, DEFAULT_WILL):
|
||||
will_message = PublishMessage(**will)
|
||||
mqttc.will_set(
|
||||
@ -883,7 +881,7 @@ class MQTT:
|
||||
await self._discovery_cooldown() # Wait for MQTT discovery to cool down
|
||||
# Update subscribe cooldown period to a shorter time
|
||||
# and make sure we flush the debouncer
|
||||
await self._subscribe_debouncer.async_fire()
|
||||
await self._subscribe_debouncer.async_execute()
|
||||
self._subscribe_debouncer.set_timeout(SUBSCRIBE_COOLDOWN)
|
||||
await self.async_publish(
|
||||
topic=birth_message.topic,
|
||||
@ -993,10 +991,21 @@ class MQTT:
|
||||
def _async_mqtt_on_message(
|
||||
self, _mqttc: mqtt.Client, _userdata: None, msg: mqtt.MQTTMessage
|
||||
) -> None:
|
||||
topic = msg.topic
|
||||
# msg.topic is a property that decodes the topic to a string
|
||||
# every time it is accessed. Save the result to avoid
|
||||
# decoding the same topic multiple times.
|
||||
try:
|
||||
# msg.topic is a property that decodes the topic to a string
|
||||
# every time it is accessed. Save the result to avoid
|
||||
# decoding the same topic multiple times.
|
||||
topic = msg.topic
|
||||
except UnicodeDecodeError:
|
||||
bare_topic: bytes = getattr(msg, "_topic")
|
||||
_LOGGER.warning(
|
||||
"Skipping received%s message on invalid topic %s (qos=%s): %s",
|
||||
" retained" if msg.retain else "",
|
||||
bare_topic,
|
||||
msg.qos,
|
||||
msg.payload[0:8192],
|
||||
)
|
||||
return
|
||||
_LOGGER.debug(
|
||||
"Received%s message on %s (qos=%s): %s",
|
||||
" retained" if msg.retain else "",
|
||||
|
@ -1015,8 +1015,7 @@ class MqttDiscoveryUpdate(Entity):
|
||||
self.hass.async_create_task(
|
||||
_async_process_discovery_update_and_remove(
|
||||
payload, self._discovery_data
|
||||
),
|
||||
eager_start=False,
|
||||
)
|
||||
)
|
||||
elif self._discovery_update:
|
||||
if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]:
|
||||
@ -1025,8 +1024,7 @@ class MqttDiscoveryUpdate(Entity):
|
||||
self.hass.async_create_task(
|
||||
_async_process_discovery_update(
|
||||
payload, self._discovery_update, self._discovery_data
|
||||
),
|
||||
eager_start=False,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Non-empty, unchanged payload: Ignore to avoid changing states
|
||||
@ -1059,6 +1057,15 @@ class MqttDiscoveryUpdate(Entity):
|
||||
# rediscovered after a restart
|
||||
await async_remove_discovery_payload(self.hass, self._discovery_data)
|
||||
|
||||
@final
|
||||
async def add_to_platform_finish(self) -> None:
|
||||
"""Finish adding entity to platform."""
|
||||
await super().add_to_platform_finish()
|
||||
# Only send the discovery done after the entity is fully added
|
||||
# and the state is written to the state machine.
|
||||
if self._discovery_data is not None:
|
||||
send_discovery_done(self.hass, self._discovery_data)
|
||||
|
||||
@callback
|
||||
def add_to_platform_abort(self) -> None:
|
||||
"""Abort adding an entity to a platform."""
|
||||
@ -1218,8 +1225,6 @@ class MqttEntity(
|
||||
self._prepare_subscribe_topics()
|
||||
await self._subscribe_topics()
|
||||
await self.mqtt_async_added_to_hass()
|
||||
if self._discovery_data is not None:
|
||||
send_discovery_done(self.hass, self._discovery_data)
|
||||
|
||||
async def mqtt_async_added_to_hass(self) -> None:
|
||||
"""Call before the discovery message is acknowledged.
|
||||
|
@ -2,8 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
import datetime
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from pynws import SimpleNWS, call_with_retry
|
||||
@ -58,36 +60,49 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
nws_data = SimpleNWS(latitude, longitude, api_key, client_session)
|
||||
await nws_data.set_station(station)
|
||||
|
||||
async def update_observation() -> None:
|
||||
"""Retrieve recent observations."""
|
||||
await call_with_retry(
|
||||
nws_data.update_observation,
|
||||
RETRY_INTERVAL,
|
||||
RETRY_STOP,
|
||||
start_time=utcnow() - UPDATE_TIME_PERIOD,
|
||||
)
|
||||
def async_setup_update_observation(
|
||||
retry_interval: datetime.timedelta | float,
|
||||
retry_stop: datetime.timedelta | float,
|
||||
) -> Callable[[], Awaitable[None]]:
|
||||
async def update_observation() -> None:
|
||||
"""Retrieve recent observations."""
|
||||
await call_with_retry(
|
||||
nws_data.update_observation,
|
||||
retry_interval,
|
||||
retry_stop,
|
||||
start_time=utcnow() - UPDATE_TIME_PERIOD,
|
||||
)
|
||||
|
||||
async def update_forecast() -> None:
|
||||
"""Retrieve twice-daily forecsat."""
|
||||
await call_with_retry(
|
||||
return update_observation
|
||||
|
||||
def async_setup_update_forecast(
|
||||
retry_interval: datetime.timedelta | float,
|
||||
retry_stop: datetime.timedelta | float,
|
||||
) -> Callable[[], Awaitable[None]]:
|
||||
return partial(
|
||||
call_with_retry,
|
||||
nws_data.update_forecast,
|
||||
RETRY_INTERVAL,
|
||||
RETRY_STOP,
|
||||
retry_interval,
|
||||
retry_stop,
|
||||
)
|
||||
|
||||
async def update_forecast_hourly() -> None:
|
||||
"""Retrieve hourly forecast."""
|
||||
await call_with_retry(
|
||||
def async_setup_update_forecast_hourly(
|
||||
retry_interval: datetime.timedelta | float,
|
||||
retry_stop: datetime.timedelta | float,
|
||||
) -> Callable[[], Awaitable[None]]:
|
||||
return partial(
|
||||
call_with_retry,
|
||||
nws_data.update_forecast_hourly,
|
||||
RETRY_INTERVAL,
|
||||
RETRY_STOP,
|
||||
retry_interval,
|
||||
retry_stop,
|
||||
)
|
||||
|
||||
# Don't use retries in setup
|
||||
coordinator_observation = TimestampDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"NWS observation station {station}",
|
||||
update_method=update_observation,
|
||||
update_method=async_setup_update_observation(0, 0),
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
request_refresh_debouncer=debounce.Debouncer(
|
||||
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
|
||||
@ -98,7 +113,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"NWS forecast station {station}",
|
||||
update_method=update_forecast,
|
||||
update_method=async_setup_update_forecast(0, 0),
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
request_refresh_debouncer=debounce.Debouncer(
|
||||
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
|
||||
@ -109,7 +124,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"NWS forecast hourly station {station}",
|
||||
update_method=update_forecast_hourly,
|
||||
update_method=async_setup_update_forecast_hourly(0, 0),
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
request_refresh_debouncer=debounce.Debouncer(
|
||||
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
|
||||
@ -128,6 +143,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await coordinator_forecast.async_refresh()
|
||||
await coordinator_forecast_hourly.async_refresh()
|
||||
|
||||
# Use retries
|
||||
coordinator_observation.update_method = async_setup_update_observation(
|
||||
RETRY_INTERVAL, RETRY_STOP
|
||||
)
|
||||
coordinator_forecast.update_method = async_setup_update_forecast(
|
||||
RETRY_INTERVAL, RETRY_STOP
|
||||
)
|
||||
coordinator_forecast_hourly.update_method = async_setup_update_forecast_hourly(
|
||||
RETRY_INTERVAL, RETRY_STOP
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
@ -11,7 +11,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["rokuecp"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["rokuecp==0.19.2"],
|
||||
"requirements": ["rokuecp==0.19.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "roku:ecp",
|
||||
|
@ -39,7 +39,7 @@ from homeassistant.components.plex.services import process_plex_payload
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TIME
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform, service
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@ -432,7 +432,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
fav = [fav for fav in self.speaker.favorites if fav.title == name]
|
||||
|
||||
if len(fav) != 1:
|
||||
return
|
||||
raise ServiceValidationError(
|
||||
translation_domain=SONOS_DOMAIN,
|
||||
translation_key="invalid_favorite",
|
||||
translation_placeholders={
|
||||
"name": name,
|
||||
},
|
||||
)
|
||||
|
||||
src = fav.pop()
|
||||
self._play_favorite(src)
|
||||
@ -445,7 +451,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
MUSIC_SRC_RADIO,
|
||||
MUSIC_SRC_LINE_IN,
|
||||
]:
|
||||
soco.play_uri(uri, title=favorite.title)
|
||||
soco.play_uri(uri, title=favorite.title, timeout=LONG_SERVICE_TIMEOUT)
|
||||
else:
|
||||
soco.clear_queue()
|
||||
soco.add_to_queue(favorite.reference, timeout=LONG_SERVICE_TIMEOUT)
|
||||
|
@ -173,5 +173,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_favorite": {
|
||||
"message": "Could not find a Sonos favorite: {name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/v2c",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pytrydan==0.4.0"]
|
||||
"requirements": ["pytrydan==0.6.0"]
|
||||
}
|
||||
|
@ -83,7 +83,7 @@
|
||||
"button_fan": "Button Fan \"{subtype}\"",
|
||||
"button_swing": "Button Swing \"{subtype}\"",
|
||||
"button_decrease_speed": "Button Decrease Speed \"{subtype}\"",
|
||||
"button_increase_speed": "Button Inrease Speed \"{subtype}\"",
|
||||
"button_increase_speed": "Button Increase Speed \"{subtype}\"",
|
||||
"button_stop": "Button Stop \"{subtype}\"",
|
||||
"button_light": "Button Light \"{subtype}\"",
|
||||
"button_wind_speed": "Button Wind Speed \"{subtype}\"",
|
||||
|
@ -6,5 +6,5 @@
|
||||
"dependencies": ["auth", "application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/yolink",
|
||||
"iot_class": "cloud_push",
|
||||
"requirements": ["yolink-api==0.4.3"]
|
||||
"requirements": ["yolink-api==0.4.4"]
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 5
|
||||
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, 12, 0)
|
||||
|
@ -182,7 +182,10 @@ class EntityComponent(Generic[_EntityT]):
|
||||
key = config_entry.entry_id
|
||||
|
||||
if key in self._platforms:
|
||||
raise ValueError("Config entry has already been setup!")
|
||||
raise ValueError(
|
||||
f"Config entry {config_entry.title} ({key}) for "
|
||||
f"{platform_type}.{self.domain} has already been setup!"
|
||||
)
|
||||
|
||||
self._platforms[key] = self._async_init_entity_platform(
|
||||
platform_type,
|
||||
|
@ -36,7 +36,7 @@ home-assistant-frontend==20240501.1
|
||||
home-assistant-intents==2024.4.24
|
||||
httpx==0.27.0
|
||||
ifaddr==0.2.0
|
||||
Jinja2==3.1.3
|
||||
Jinja2==3.1.4
|
||||
lru-dict==1.3.0
|
||||
mutagen==1.47.0
|
||||
orjson==3.9.15
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.5.2"
|
||||
version = "2024.5.3"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
@ -46,7 +46,7 @@ dependencies = [
|
||||
"httpx==0.27.0",
|
||||
"home-assistant-bluetooth==1.12.0",
|
||||
"ifaddr==0.2.0",
|
||||
"Jinja2==3.1.3",
|
||||
"Jinja2==3.1.4",
|
||||
"lru-dict==1.3.0",
|
||||
"PyJWT==2.8.0",
|
||||
# PyJWT has loose dependency. We want the latest one.
|
||||
|
@ -22,7 +22,7 @@ hass-nabucasa==0.78.0
|
||||
httpx==0.27.0
|
||||
home-assistant-bluetooth==1.12.0
|
||||
ifaddr==0.2.0
|
||||
Jinja2==3.1.3
|
||||
Jinja2==3.1.4
|
||||
lru-dict==1.3.0
|
||||
PyJWT==2.8.0
|
||||
cryptography==42.0.5
|
||||
|
@ -697,7 +697,7 @@ debugpy==1.8.1
|
||||
# decora==0.6
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==7.1.0
|
||||
deebot-client==7.2.0
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
@ -952,7 +952,7 @@ glances-api==0.6.0
|
||||
goalzero==0.2.2
|
||||
|
||||
# homeassistant.components.goodwe
|
||||
goodwe==0.3.4
|
||||
goodwe==0.3.5
|
||||
|
||||
# homeassistant.components.google_mail
|
||||
# homeassistant.components.google_tasks
|
||||
@ -1800,7 +1800,7 @@ pyefergy==22.1.1
|
||||
pyegps==0.2.5
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
pyenphase==1.20.1
|
||||
pyenphase==1.20.3
|
||||
|
||||
# homeassistant.components.envisalink
|
||||
pyenvisalink==4.6
|
||||
@ -2337,7 +2337,7 @@ pytradfri[async]==9.0.1
|
||||
pytrafikverket==0.3.10
|
||||
|
||||
# homeassistant.components.v2c
|
||||
pytrydan==0.4.0
|
||||
pytrydan==0.6.0
|
||||
|
||||
# homeassistant.components.usb
|
||||
pyudev==0.24.1
|
||||
@ -2460,7 +2460,7 @@ rjpl==0.3.6
|
||||
rocketchat-API==0.6.1
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.19.2
|
||||
rokuecp==0.19.3
|
||||
|
||||
# homeassistant.components.romy
|
||||
romy==0.0.10
|
||||
@ -2914,7 +2914,7 @@ yeelight==0.7.14
|
||||
yeelightsunflower==0.0.10
|
||||
|
||||
# homeassistant.components.yolink
|
||||
yolink-api==0.4.3
|
||||
yolink-api==0.4.4
|
||||
|
||||
# homeassistant.components.youless
|
||||
youless-api==1.0.1
|
||||
|
@ -575,7 +575,7 @@ dbus-fast==2.21.1
|
||||
debugpy==1.8.1
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==7.1.0
|
||||
deebot-client==7.2.0
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
@ -781,7 +781,7 @@ glances-api==0.6.0
|
||||
goalzero==0.2.2
|
||||
|
||||
# homeassistant.components.goodwe
|
||||
goodwe==0.3.4
|
||||
goodwe==0.3.5
|
||||
|
||||
# homeassistant.components.google_mail
|
||||
# homeassistant.components.google_tasks
|
||||
@ -1405,7 +1405,7 @@ pyefergy==22.1.1
|
||||
pyegps==0.2.5
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
pyenphase==1.20.1
|
||||
pyenphase==1.20.3
|
||||
|
||||
# homeassistant.components.everlights
|
||||
pyeverlights==0.1.0
|
||||
@ -1816,7 +1816,7 @@ pytradfri[async]==9.0.1
|
||||
pytrafikverket==0.3.10
|
||||
|
||||
# homeassistant.components.v2c
|
||||
pytrydan==0.4.0
|
||||
pytrydan==0.6.0
|
||||
|
||||
# homeassistant.components.usb
|
||||
pyudev==0.24.1
|
||||
@ -1906,7 +1906,7 @@ rflink==0.0.66
|
||||
ring-doorbell[listen]==0.8.11
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.19.2
|
||||
rokuecp==0.19.3
|
||||
|
||||
# homeassistant.components.romy
|
||||
romy==0.0.10
|
||||
@ -2264,7 +2264,7 @@ yalexs==3.0.1
|
||||
yeelight==0.7.14
|
||||
|
||||
# homeassistant.components.yolink
|
||||
yolink-api==0.4.3
|
||||
yolink-api==0.4.4
|
||||
|
||||
# homeassistant.components.youless
|
||||
youless-api==1.0.1
|
||||
|
@ -3,6 +3,7 @@
|
||||
from aiohomekit.model.characteristics import CharacteristicsTypes
|
||||
from aiohomekit.model.services import ServicesTypes
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
@ -94,6 +95,24 @@ def create_window_covering_service_with_v_tilt_2(accessory):
|
||||
tilt_target.maxValue = 0
|
||||
|
||||
|
||||
def create_window_covering_service_with_none_tilt(accessory):
|
||||
"""Define a window-covering characteristics as per page 219 of HAP spec.
|
||||
|
||||
This accessory uses None for the tilt value unexpectedly.
|
||||
"""
|
||||
service = create_window_covering_service(accessory)
|
||||
|
||||
tilt_current = service.add_char(CharacteristicsTypes.VERTICAL_TILT_CURRENT)
|
||||
tilt_current.value = None
|
||||
tilt_current.minValue = -90
|
||||
tilt_current.maxValue = 0
|
||||
|
||||
tilt_target = service.add_char(CharacteristicsTypes.VERTICAL_TILT_TARGET)
|
||||
tilt_target.value = None
|
||||
tilt_target.minValue = -90
|
||||
tilt_target.maxValue = 0
|
||||
|
||||
|
||||
async def test_change_window_cover_state(hass: HomeAssistant) -> None:
|
||||
"""Test that we can turn a HomeKit alarm on and off again."""
|
||||
helper = await setup_test_component(hass, create_window_covering_service)
|
||||
@ -212,6 +231,21 @@ async def test_read_window_cover_tilt_vertical_2(hass: HomeAssistant) -> None:
|
||||
assert state.attributes["current_tilt_position"] == 83
|
||||
|
||||
|
||||
async def test_read_window_cover_tilt_missing_tilt(hass: HomeAssistant) -> None:
|
||||
"""Test that missing tilt is handled."""
|
||||
helper = await setup_test_component(
|
||||
hass, create_window_covering_service_with_none_tilt
|
||||
)
|
||||
|
||||
await helper.async_update(
|
||||
ServicesTypes.WINDOW_COVERING,
|
||||
{CharacteristicsTypes.OBSTRUCTION_DETECTED: True},
|
||||
)
|
||||
state = await helper.poll_and_get_state()
|
||||
assert "current_tilt_position" not in state.attributes
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_write_window_cover_tilt_horizontal(hass: HomeAssistant) -> None:
|
||||
"""Test that horizontal tilt is written correctly."""
|
||||
helper = await setup_test_component(
|
||||
|
@ -6,8 +6,9 @@ from datetime import datetime, timedelta
|
||||
import json
|
||||
import socket
|
||||
import ssl
|
||||
import time
|
||||
from typing import Any, TypedDict
|
||||
from unittest.mock import ANY, MagicMock, call, mock_open, patch
|
||||
from unittest.mock import ANY, MagicMock, Mock, call, mock_open, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import paho.mqtt.client as paho_mqtt
|
||||
@ -938,6 +939,42 @@ async def test_receiving_non_utf8_message_gets_logged(
|
||||
)
|
||||
|
||||
|
||||
async def test_receiving_message_with_non_utf8_topic_gets_logged(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
record_calls: MessageCallbackType,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test receiving a non utf8 encoded topic."""
|
||||
await mqtt_mock_entry()
|
||||
await mqtt.async_subscribe(hass, "test-topic", record_calls)
|
||||
|
||||
# Local import to avoid processing MQTT modules when running a testcase
|
||||
# which does not use MQTT.
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from paho.mqtt.client import MQTTMessage
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.mqtt.models import MqttData
|
||||
|
||||
msg = MQTTMessage(topic=b"tasmota/discovery/18FE34E0B760\xcc\x02")
|
||||
msg.payload = b"Payload"
|
||||
msg.qos = 2
|
||||
msg.retain = True
|
||||
msg.timestamp = time.monotonic()
|
||||
|
||||
mqtt_data: MqttData = hass.data["mqtt"]
|
||||
assert mqtt_data.client
|
||||
mqtt_data.client._async_mqtt_on_message(Mock(), None, msg)
|
||||
|
||||
assert (
|
||||
"Skipping received retained message on invalid "
|
||||
"topic b'tasmota/discovery/18FE34E0B760\\xcc\\x02' "
|
||||
"(qos=2): b'Payload'" in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_all_subscriptions_run_when_decode_fails(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
@ -2589,19 +2626,19 @@ async def test_subscription_done_when_birth_message_is_sent(
|
||||
mqtt_client_mock.on_connect(None, None, 0, 0)
|
||||
await hass.async_block_till_done()
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await mqtt.async_subscribe(hass, "topic/test", record_calls)
|
||||
# We wait until we receive a birth message
|
||||
await asyncio.wait_for(birth.wait(), 1)
|
||||
# Assert we already have subscribed at the client
|
||||
# for new config payloads at the time we the birth message is received
|
||||
assert ("homeassistant/+/+/config", 0) in help_all_subscribe_calls(
|
||||
mqtt_client_mock
|
||||
)
|
||||
assert ("homeassistant/+/+/+/config", 0) in help_all_subscribe_calls(
|
||||
mqtt_client_mock
|
||||
)
|
||||
mqtt_client_mock.publish.assert_called_with(
|
||||
"homeassistant/status", "online", 0, False
|
||||
)
|
||||
|
||||
# Assert we already have subscribed at the client
|
||||
# for new config payloads at the time we the birth message is received
|
||||
subscribe_calls = help_all_subscribe_calls(mqtt_client_mock)
|
||||
assert ("homeassistant/+/+/config", 0) in subscribe_calls
|
||||
assert ("homeassistant/+/+/+/config", 0) in subscribe_calls
|
||||
mqtt_client_mock.publish.assert_called_with(
|
||||
"homeassistant/status", "online", 0, False
|
||||
)
|
||||
assert ("topic/test", 0) in subscribe_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -335,6 +335,9 @@ async def test_default_entity_and_device_name(
|
||||
|
||||
# Assert that no issues ware registered
|
||||
assert len(events) == 0
|
||||
await hass.async_block_till_done()
|
||||
# Assert that no issues ware registered
|
||||
assert len(events) == 0
|
||||
|
||||
|
||||
async def test_name_attribute_is_set_or_not(
|
||||
|
@ -9,6 +9,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
import pytest
|
||||
from soco import SoCo
|
||||
from soco.alarms import Alarms
|
||||
from soco.data_structures import DidlFavorite, SearchResult
|
||||
from soco.events_base import Event as SonosEvent
|
||||
|
||||
from homeassistant.components import ssdp, zeroconf
|
||||
@ -17,7 +18,7 @@ from homeassistant.components.sonos import DOMAIN
|
||||
from homeassistant.const import CONF_HOSTS
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
from tests.common import MockConfigEntry, load_fixture, load_json_value_fixture
|
||||
|
||||
|
||||
class SonosMockEventListener:
|
||||
@ -304,6 +305,14 @@ def config_fixture():
|
||||
return {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: ["192.168.42.2"]}}}
|
||||
|
||||
|
||||
@pytest.fixture(name="sonos_favorites")
|
||||
def sonos_favorites_fixture() -> SearchResult:
|
||||
"""Create sonos favorites fixture."""
|
||||
favorites = load_json_value_fixture("sonos_favorites.json", "sonos")
|
||||
favorite_list = [DidlFavorite.from_dict(fav) for fav in favorites]
|
||||
return SearchResult(favorite_list, "favorites", 3, 3, 1)
|
||||
|
||||
|
||||
class MockMusicServiceItem:
|
||||
"""Mocks a Soco MusicServiceItem."""
|
||||
|
||||
@ -408,10 +417,10 @@ def mock_get_music_library_information(
|
||||
|
||||
|
||||
@pytest.fixture(name="music_library")
|
||||
def music_library_fixture():
|
||||
def music_library_fixture(sonos_favorites: SearchResult) -> Mock:
|
||||
"""Create music_library fixture."""
|
||||
music_library = MagicMock()
|
||||
music_library.get_sonos_favorites.return_value.update_id = 1
|
||||
music_library.get_sonos_favorites.return_value = sonos_favorites
|
||||
music_library.browse_by_idstring = mock_browse_by_idstring
|
||||
music_library.get_music_library_information = mock_get_music_library_information
|
||||
return music_library
|
||||
|
38
tests/components/sonos/fixtures/sonos_favorites.json
Normal file
38
tests/components/sonos/fixtures/sonos_favorites.json
Normal file
@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"title": "66 - Watercolors",
|
||||
"parent_id": "FV:2",
|
||||
"item_id": "FV:2/4",
|
||||
"resource_meta_data": "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns:r=\"urn:schemas-rinconnetworks-com:metadata-1-0/\" xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\"><item id=\"10090120Api%3atune%3aliveAudio%3ajazzcafe%3ae4b5402c-9999-9999-9999-4bc8e2cdccce\" parentID=\"10086064live%3f93b0b9cb-9999-9999-9999-bcf75971fcfe\" restricted=\"false\"><dc:title>66 - Watercolors</dc:title><upnp:class>object.item.audioItem.audioBroadcast</upnp:class><desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">SA_RINCON9479_X_#Svc9479-99999999-Token</desc></item></DIDL-Lite>",
|
||||
"resources": [
|
||||
{
|
||||
"uri": "x-sonosapi-hls:Api%3atune%3aliveAudio%3ajazzcafe%3aetc",
|
||||
"protocol_info": "a:b:c:d"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "James Taylor Radio",
|
||||
"parent_id": "FV:2",
|
||||
"item_id": "FV:2/13",
|
||||
"resource_meta_data": "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns:r=\"urn:schemas-rinconnetworks-com:metadata-1-0/\" xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\"><item id=\"100c2068ST%3a1683194971234567890\" parentID=\"10fe2064myStations\" restricted=\"true\"><dc:title>James Taylor Radio</dc:title><upnp:class>object.item.audioItem.audioBroadcast.#station</upnp:class><desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">SA_RINCON60423_X_#Svc60423-99999999-Token</desc></item></DIDL-Lite>",
|
||||
"resources": [
|
||||
{
|
||||
"uri": "x-sonosapi-radio:ST%3aetc",
|
||||
"protocol_info": "a:b:c:d"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "1984",
|
||||
"parent_id": "FV:2",
|
||||
"item_id": "FV:2/8",
|
||||
"resource_meta_data": "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns:r=\"urn:schemas-rinconnetworks-com:metadata-1-0/\" xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\"><item id=\"A:ALBUMARTIST/Aerosmith/1984\" parentID=\"A:ALBUMARTIST/Aerosmith\" restricted=\"true\"><dc:title>1984</dc:title><upnp:class>object.container.album.musicAlbum</upnp:class><desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">RINCON_AssociatedZPUDN</desc></item></DIDL-Lite>",
|
||||
"resources": [
|
||||
{
|
||||
"uri": "x-rincon-playlist:RINCON_test#A:ALBUMARTIST/Aerosmith/1984",
|
||||
"protocol_info": "a:b:c:d"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -1,6 +1,7 @@
|
||||
"""Tests for the Sonos Media Player platform."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
@ -9,10 +10,15 @@ from homeassistant.components.media_player import (
|
||||
SERVICE_PLAY_MEDIA,
|
||||
MediaPlayerEnqueue,
|
||||
)
|
||||
from homeassistant.components.media_player.const import ATTR_MEDIA_ENQUEUE
|
||||
from homeassistant.components.media_player.const import (
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
)
|
||||
from homeassistant.components.sonos.const import SOURCE_LINEIN, SOURCE_TV
|
||||
from homeassistant.components.sonos.media_player import LONG_SERVICE_TIMEOUT
|
||||
from homeassistant.const import STATE_IDLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
CONNECTION_UPNP,
|
||||
@ -272,3 +278,154 @@ async def test_play_media_music_library_playlist_dne(
|
||||
assert soco_mock.play_uri.call_count == 0
|
||||
assert media_content_id in caplog.text
|
||||
assert "playlist" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("source", "result"),
|
||||
[
|
||||
(
|
||||
SOURCE_LINEIN,
|
||||
{
|
||||
"switch_to_line_in": 1,
|
||||
},
|
||||
),
|
||||
(
|
||||
SOURCE_TV,
|
||||
{
|
||||
"switch_to_tv": 1,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_select_source_line_in_tv(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
async_autosetup_sonos,
|
||||
source: str,
|
||||
result: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test the select_source method with a variety of inputs."""
|
||||
soco_mock = soco_factory.mock_list.get("192.168.42.2")
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
{
|
||||
"entity_id": "media_player.zone_a",
|
||||
"source": source,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert soco_mock.switch_to_line_in.call_count == result.get("switch_to_line_in", 0)
|
||||
assert soco_mock.switch_to_tv.call_count == result.get("switch_to_tv", 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("source", "result"),
|
||||
[
|
||||
(
|
||||
"James Taylor Radio",
|
||||
{
|
||||
"play_uri": 1,
|
||||
"play_uri_uri": "x-sonosapi-radio:ST%3aetc",
|
||||
"play_uri_title": "James Taylor Radio",
|
||||
},
|
||||
),
|
||||
(
|
||||
"66 - Watercolors",
|
||||
{
|
||||
"play_uri": 1,
|
||||
"play_uri_uri": "x-sonosapi-hls:Api%3atune%3aliveAudio%3ajazzcafe%3aetc",
|
||||
"play_uri_title": "66 - Watercolors",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_select_source_play_uri(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
async_autosetup_sonos,
|
||||
source: str,
|
||||
result: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test the select_source method with a variety of inputs."""
|
||||
soco_mock = soco_factory.mock_list.get("192.168.42.2")
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
{
|
||||
"entity_id": "media_player.zone_a",
|
||||
"source": source,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert soco_mock.play_uri.call_count == result.get("play_uri")
|
||||
soco_mock.play_uri.assert_called_with(
|
||||
result.get("play_uri_uri"),
|
||||
title=result.get("play_uri_title"),
|
||||
timeout=LONG_SERVICE_TIMEOUT,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("source", "result"),
|
||||
[
|
||||
(
|
||||
"1984",
|
||||
{
|
||||
"add_to_queue": 1,
|
||||
"add_to_queue_item_id": "A:ALBUMARTIST/Aerosmith/1984",
|
||||
"clear_queue": 1,
|
||||
"play_from_queue": 1,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_select_source_play_queue(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
async_autosetup_sonos,
|
||||
source: str,
|
||||
result: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test the select_source method with a variety of inputs."""
|
||||
soco_mock = soco_factory.mock_list.get("192.168.42.2")
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
{
|
||||
"entity_id": "media_player.zone_a",
|
||||
"source": source,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert soco_mock.clear_queue.call_count == result.get("clear_queue")
|
||||
assert soco_mock.add_to_queue.call_count == result.get("add_to_queue")
|
||||
assert soco_mock.add_to_queue.call_args_list[0].args[0].item_id == result.get(
|
||||
"add_to_queue_item_id"
|
||||
)
|
||||
assert (
|
||||
soco_mock.add_to_queue.call_args_list[0].kwargs["timeout"]
|
||||
== LONG_SERVICE_TIMEOUT
|
||||
)
|
||||
assert soco_mock.play_from_queue.call_count == result.get("play_from_queue")
|
||||
soco_mock.play_from_queue.assert_called_with(0)
|
||||
|
||||
|
||||
async def test_select_source_error(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
async_autosetup_sonos,
|
||||
) -> None:
|
||||
"""Test the select_source method with a variety of inputs."""
|
||||
with pytest.raises(ServiceValidationError) as sve:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
{
|
||||
"entity_id": "media_player.zone_a",
|
||||
"source": "invalid_source",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert "invalid_source" in str(sve.value)
|
||||
assert "Could not find a Sonos favorite" in str(sve.value)
|
||||
|
@ -3,6 +3,7 @@
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import re
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
@ -365,7 +366,13 @@ async def test_setup_entry_fails_duplicate(hass: HomeAssistant) -> None:
|
||||
|
||||
assert await component.async_setup_entry(entry)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=re.escape(
|
||||
f"Config entry Mock Title ({entry.entry_id}) for "
|
||||
"entry_domain.test_domain has already been setup!"
|
||||
),
|
||||
):
|
||||
await component.async_setup_entry(entry)
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user