mirror of
https://github.com/home-assistant/core.git
synced 2025-07-31 17:18:23 +00:00
Compare commits
8 Commits
60293648dc
...
c3037bae39
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c3037bae39 | ||
![]() |
9b1ab34352 | ||
![]() |
221a8597da | ||
![]() |
45022752a0 | ||
![]() |
aa342eb476 | ||
![]() |
32b26b8270 | ||
![]() |
e07c29caad | ||
![]() |
b487c12ab1 |
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from aioesphomeapi import APIClient
|
from aioesphomeapi import APIClient
|
||||||
|
|
||||||
from homeassistant.components import ffmpeg, zeroconf
|
from homeassistant.components import zeroconf
|
||||||
from homeassistant.components.bluetooth import async_remove_scanner
|
from homeassistant.components.bluetooth import async_remove_scanner
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
@ -17,13 +17,10 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
from homeassistant.helpers.issue_registry import async_delete_issue
|
from homeassistant.helpers.issue_registry import async_delete_issue
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import CONF_BLUETOOTH_MAC_ADDRESS, CONF_NOISE_PSK, DATA_FFMPEG_PROXY, DOMAIN
|
from . import dashboard, ffmpeg_proxy
|
||||||
from .dashboard import async_setup as async_setup_dashboard
|
from .const import CONF_BLUETOOTH_MAC_ADDRESS, CONF_NOISE_PSK, DOMAIN
|
||||||
from .domain_data import DomainData
|
from .domain_data import DomainData
|
||||||
|
|
||||||
# Import config flow so that it's added to the registry
|
|
||||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||||
from .ffmpeg_proxy import FFmpegProxyData, FFmpegProxyView
|
|
||||||
from .manager import DEVICE_CONFLICT_ISSUE_FORMAT, ESPHomeManager, cleanup_instance
|
from .manager import DEVICE_CONFLICT_ISSUE_FORMAT, ESPHomeManager, cleanup_instance
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
@ -33,12 +30,8 @@ CLIENT_INFO = f"Home Assistant {ha_version}"
|
|||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the esphome component."""
|
"""Set up the esphome component."""
|
||||||
proxy_data = hass.data[DATA_FFMPEG_PROXY] = FFmpegProxyData()
|
ffmpeg_proxy.async_setup(hass)
|
||||||
|
await dashboard.async_setup(hass)
|
||||||
await async_setup_dashboard(hass)
|
|
||||||
hass.http.register_view(
|
|
||||||
FFmpegProxyView(ffmpeg.get_ffmpeg_manager(hass), proxy_data)
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,6 +47,7 @@ from .const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from .dashboard import async_get_or_create_dashboard_manager, async_set_dashboard_info
|
from .dashboard import async_get_or_create_dashboard_manager, async_set_dashboard_info
|
||||||
|
from .entry_data import ESPHomeConfigEntry
|
||||||
from .manager import async_replace_device
|
from .manager import async_replace_device
|
||||||
|
|
||||||
ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
|
ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
|
||||||
@ -608,7 +609,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
config_entry: ConfigEntry,
|
config_entry: ESPHomeConfigEntry,
|
||||||
) -> OptionsFlowHandler:
|
) -> OptionsFlowHandler:
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return OptionsFlowHandler()
|
return OptionsFlowHandler()
|
||||||
|
@ -22,5 +22,3 @@ PROJECT_URLS = {
|
|||||||
# ESPHome always uses .0 for the changelog URL
|
# ESPHome always uses .0 for the changelog URL
|
||||||
STABLE_BLE_URL_VERSION = f"{STABLE_BLE_VERSION.major}.{STABLE_BLE_VERSION.minor}.0"
|
STABLE_BLE_URL_VERSION = f"{STABLE_BLE_VERSION.major}.{STABLE_BLE_VERSION.minor}.0"
|
||||||
DEFAULT_URL = f"https://esphome.io/changelog/{STABLE_BLE_URL_VERSION}.html"
|
DEFAULT_URL = f"https://esphome.io/changelog/{STABLE_BLE_URL_VERSION}.html"
|
||||||
|
|
||||||
DATA_FFMPEG_PROXY = f"{DOMAIN}.ffmpeg_proxy"
|
|
||||||
|
@ -28,6 +28,8 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
# Import config flow so that it's added to the registry
|
# Import config flow so that it's added to the registry
|
||||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||||
from .enum_mapper import EsphomeEnumMapper
|
from .enum_mapper import EsphomeEnumMapper
|
||||||
@ -167,7 +169,12 @@ def convert_api_error_ha_error[**_P, _R, _EntityT: EsphomeEntity[Any, Any]](
|
|||||||
return await func(self, *args, **kwargs)
|
return await func(self, *args, **kwargs)
|
||||||
except APIConnectionError as error:
|
except APIConnectionError as error:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Error communicating with device: {error}"
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="error_communicating_with_device",
|
||||||
|
translation_placeholders={
|
||||||
|
"device_name": self._device_info.name,
|
||||||
|
"error": str(error),
|
||||||
|
},
|
||||||
) from error
|
) from error
|
||||||
|
|
||||||
return handler
|
return handler
|
||||||
@ -194,6 +201,7 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
|||||||
_static_info: _InfoT
|
_static_info: _InfoT
|
||||||
_state: _StateT
|
_state: _StateT
|
||||||
_has_state: bool
|
_has_state: bool
|
||||||
|
device_entry: dr.DeviceEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -11,17 +11,20 @@ from typing import Final
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.abc import AbstractStreamWriter, BaseRequest
|
from aiohttp.abc import AbstractStreamWriter, BaseRequest
|
||||||
|
|
||||||
|
from homeassistant.components import ffmpeg
|
||||||
from homeassistant.components.ffmpeg import FFmpegManager
|
from homeassistant.components.ffmpeg import FFmpegManager
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
from .const import DATA_FFMPEG_PROXY
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
_MAX_CONVERSIONS_PER_DEVICE: Final[int] = 2
|
_MAX_CONVERSIONS_PER_DEVICE: Final[int] = 2
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
def async_create_proxy_url(
|
def async_create_proxy_url(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
@ -32,7 +35,7 @@ def async_create_proxy_url(
|
|||||||
width: int | None = None,
|
width: int | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create a use proxy URL that automatically converts the media."""
|
"""Create a use proxy URL that automatically converts the media."""
|
||||||
data: FFmpegProxyData = hass.data[DATA_FFMPEG_PROXY]
|
data = hass.data[DATA_FFMPEG_PROXY]
|
||||||
return data.async_create_proxy_url(
|
return data.async_create_proxy_url(
|
||||||
device_id, media_url, media_format, rate, channels, width
|
device_id, media_url, media_format, rate, channels, width
|
||||||
)
|
)
|
||||||
@ -313,3 +316,16 @@ class FFmpegProxyView(HomeAssistantView):
|
|||||||
assert writer is not None
|
assert writer is not None
|
||||||
await resp.transcode(request, writer)
|
await resp.transcode(request, writer)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
DATA_FFMPEG_PROXY: HassKey[FFmpegProxyData] = HassKey(f"{DOMAIN}.ffmpeg_proxy")
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_setup(hass: HomeAssistant) -> None:
|
||||||
|
"""Set up the ffmpeg proxy."""
|
||||||
|
proxy_data = FFmpegProxyData()
|
||||||
|
hass.data[DATA_FFMPEG_PROXY] = proxy_data
|
||||||
|
hass.http.register_view(
|
||||||
|
FFmpegProxyView(ffmpeg.get_ffmpeg_manager(hass), proxy_data)
|
||||||
|
)
|
||||||
|
20
homeassistant/components/esphome/icons.json
Normal file
20
homeassistant/components/esphome/icons.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"binary_sensor": {
|
||||||
|
"assist_in_progress": {
|
||||||
|
"default": "mdi:timer-sand"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"select": {
|
||||||
|
"pipeline": {
|
||||||
|
"default": "mdi:filter-outline"
|
||||||
|
},
|
||||||
|
"vad_sensitivity": {
|
||||||
|
"default": "mdi:volume-high"
|
||||||
|
},
|
||||||
|
"wake_word": {
|
||||||
|
"default": "mdi:microphone"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -148,10 +148,6 @@ class EsphomeMediaPlayer(
|
|||||||
announcement: bool,
|
announcement: bool,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Get URL for ffmpeg proxy."""
|
"""Get URL for ffmpeg proxy."""
|
||||||
if self.device_entry is None:
|
|
||||||
# Device id is required
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Choose the first default or announcement supported format
|
# Choose the first default or announcement supported format
|
||||||
format_to_use: MediaPlayerSupportedFormat | None = None
|
format_to_use: MediaPlayerSupportedFormat | None = None
|
||||||
for supported_format in supported_formats:
|
for supported_format in supported_formats:
|
||||||
|
@ -20,13 +20,13 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.util.enum import try_parse_enum
|
from homeassistant.util.enum import try_parse_enum
|
||||||
|
|
||||||
from .entity import EsphomeEntity, platform_async_setup_entry
|
from .entity import EsphomeEntity, platform_async_setup_entry
|
||||||
|
from .entry_data import ESPHomeConfigEntry
|
||||||
from .enum_mapper import EsphomeEnumMapper
|
from .enum_mapper import EsphomeEnumMapper
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@ -34,7 +34,7 @@ PARALLEL_UPDATES = 0
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: ESPHomeConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up esphome sensors based on a config entry."""
|
"""Set up esphome sensors based on a config entry."""
|
||||||
|
@ -184,6 +184,15 @@
|
|||||||
"exceptions": {
|
"exceptions": {
|
||||||
"action_call_failed": {
|
"action_call_failed": {
|
||||||
"message": "Failed to execute the action call {call_name} on {device_name}: {error}"
|
"message": "Failed to execute the action call {call_name} on {device_name}: {error}"
|
||||||
|
},
|
||||||
|
"error_communicating_with_device": {
|
||||||
|
"message": "Error communicating with the device {device_name}: {error}"
|
||||||
|
},
|
||||||
|
"error_compiling": {
|
||||||
|
"message": "Error compiling {configuration}; Try again in ESPHome dashboard for more information."
|
||||||
|
},
|
||||||
|
"error_uploading": {
|
||||||
|
"message": "Error during OTA of {configuration}; Try again in ESPHome dashboard for more information."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ from homeassistant.components.update import (
|
|||||||
UpdateEntity,
|
UpdateEntity,
|
||||||
UpdateEntityFeature,
|
UpdateEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
@ -27,6 +26,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util.enum import try_parse_enum
|
from homeassistant.util.enum import try_parse_enum
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
from .coordinator import ESPHomeDashboardCoordinator
|
from .coordinator import ESPHomeDashboardCoordinator
|
||||||
from .dashboard import async_get_dashboard
|
from .dashboard import async_get_dashboard
|
||||||
from .domain_data import DomainData
|
from .domain_data import DomainData
|
||||||
@ -36,7 +36,7 @@ from .entity import (
|
|||||||
esphome_state_property,
|
esphome_state_property,
|
||||||
platform_async_setup_entry,
|
platform_async_setup_entry,
|
||||||
)
|
)
|
||||||
from .entry_data import RuntimeEntryData
|
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ NO_FEATURES = UpdateEntityFeature(0)
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: ESPHomeConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up ESPHome update based on a config entry."""
|
"""Set up ESPHome update based on a config entry."""
|
||||||
@ -202,16 +202,23 @@ class ESPHomeDashboardUpdateEntity(
|
|||||||
api = coordinator.api
|
api = coordinator.api
|
||||||
device = coordinator.data.get(self._device_info.name)
|
device = coordinator.data.get(self._device_info.name)
|
||||||
assert device is not None
|
assert device is not None
|
||||||
|
configuration = device["configuration"]
|
||||||
try:
|
try:
|
||||||
if not await api.compile(device["configuration"]):
|
if not await api.compile(configuration):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Error compiling {device['configuration']}; "
|
translation_domain=DOMAIN,
|
||||||
"Try again in ESPHome dashboard for more information."
|
translation_key="error_compiling",
|
||||||
|
translation_placeholders={
|
||||||
|
"configuration": configuration,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if not await api.upload(device["configuration"], "OTA"):
|
if not await api.upload(configuration, "OTA"):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Error updating {device['configuration']} via OTA; "
|
translation_domain=DOMAIN,
|
||||||
"Try again in ESPHome dashboard for more information."
|
translation_key="error_uploading",
|
||||||
|
translation_placeholders={
|
||||||
|
"configuration": configuration,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
@ -68,6 +68,9 @@
|
|||||||
"repeat_set": {
|
"repeat_set": {
|
||||||
"service": "mdi:repeat"
|
"service": "mdi:repeat"
|
||||||
},
|
},
|
||||||
|
"search_media": {
|
||||||
|
"service": "mdi:text-search"
|
||||||
|
},
|
||||||
"select_sound_mode": {
|
"select_sound_mode": {
|
||||||
"service": "mdi:surround-sound"
|
"service": "mdi:surround-sound"
|
||||||
},
|
},
|
||||||
|
@ -181,6 +181,35 @@ browse_media:
|
|||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
|
|
||||||
|
search_media:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: media_player
|
||||||
|
supported_features:
|
||||||
|
- media_player.MediaPlayerEntityFeature.SEARCH_MEDIA
|
||||||
|
fields:
|
||||||
|
search_query:
|
||||||
|
required: true
|
||||||
|
example: "Beatles"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
media_content_type:
|
||||||
|
required: false
|
||||||
|
example: "music"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
media_content_id:
|
||||||
|
required: false
|
||||||
|
example: "A:ALBUMARTIST/Beatles"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
media_filter_classes:
|
||||||
|
required: false
|
||||||
|
example: ["album", "artist"]
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
multiple: true
|
||||||
|
|
||||||
select_source:
|
select_source:
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
|
@ -274,6 +274,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"search_media": {
|
||||||
|
"name": "Search media",
|
||||||
|
"description": "Searches the available media.",
|
||||||
|
"fields": {
|
||||||
|
"media_content_id": {
|
||||||
|
"name": "[%key:component::media_player::services::browse_media::fields::media_content_id::name%]",
|
||||||
|
"description": "[%key:component::media_player::services::browse_media::fields::media_content_id::description%]"
|
||||||
|
},
|
||||||
|
"media_content_type": {
|
||||||
|
"name": "[%key:component::media_player::services::browse_media::fields::media_content_type::name%]",
|
||||||
|
"description": "[%key:component::media_player::services::browse_media::fields::media_content_type::description%]"
|
||||||
|
},
|
||||||
|
"search_query": {
|
||||||
|
"name": "Search query",
|
||||||
|
"description": "The term to search for."
|
||||||
|
},
|
||||||
|
"media_filter_classes": {
|
||||||
|
"name": "Media filter classes",
|
||||||
|
"description": "List of media classes to filter the search results by."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"select_source": {
|
"select_source": {
|
||||||
"name": "Select source",
|
"name": "Select source",
|
||||||
"description": "Sends the media player the command to change input source.",
|
"description": "Sends the media player the command to change input source.",
|
||||||
|
@ -33,7 +33,7 @@ from .const import (
|
|||||||
URI_SCHEME,
|
URI_SCHEME,
|
||||||
URI_SCHEME_REGEX,
|
URI_SCHEME_REGEX,
|
||||||
)
|
)
|
||||||
from .error import MediaSourceError, Unresolvable
|
from .error import MediaSourceError, UnknownMediaSource, Unresolvable
|
||||||
from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia
|
from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -113,7 +113,11 @@ def _get_media_item(
|
|||||||
return MediaSourceItem(hass, domain, "", target_media_player)
|
return MediaSourceItem(hass, domain, "", target_media_player)
|
||||||
|
|
||||||
if item.domain is not None and item.domain not in hass.data[DOMAIN]:
|
if item.domain is not None and item.domain not in hass.data[DOMAIN]:
|
||||||
raise ValueError("Unknown media source")
|
raise UnknownMediaSource(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="unknown_media_source",
|
||||||
|
translation_placeholders={"domain": item.domain},
|
||||||
|
)
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
@ -132,7 +136,14 @@ async def async_browse_media(
|
|||||||
try:
|
try:
|
||||||
item = await _get_media_item(hass, media_content_id, None).async_browse()
|
item = await _get_media_item(hass, media_content_id, None).async_browse()
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
raise BrowseError(str(err)) from err
|
raise BrowseError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="browse_media_failed",
|
||||||
|
translation_placeholders={
|
||||||
|
"media_content_id": str(media_content_id),
|
||||||
|
"error": str(err),
|
||||||
|
},
|
||||||
|
) from err
|
||||||
|
|
||||||
if content_filter is None or item.children is None:
|
if content_filter is None or item.children is None:
|
||||||
return item
|
return item
|
||||||
@ -165,7 +176,14 @@ async def async_resolve_media(
|
|||||||
try:
|
try:
|
||||||
item = _get_media_item(hass, media_content_id, target_media_player)
|
item = _get_media_item(hass, media_content_id, target_media_player)
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
raise Unresolvable(str(err)) from err
|
raise Unresolvable(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="resolve_media_failed",
|
||||||
|
translation_placeholders={
|
||||||
|
"media_content_id": str(media_content_id),
|
||||||
|
"error": str(err),
|
||||||
|
},
|
||||||
|
) from err
|
||||||
|
|
||||||
return await item.async_resolve()
|
return await item.async_resolve()
|
||||||
|
|
||||||
|
@ -9,3 +9,7 @@ class MediaSourceError(HomeAssistantError):
|
|||||||
|
|
||||||
class Unresolvable(MediaSourceError):
|
class Unresolvable(MediaSourceError):
|
||||||
"""When media ID is not resolvable."""
|
"""When media ID is not resolvable."""
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownMediaSource(MediaSourceError, ValueError):
|
||||||
|
"""When media source is unknown."""
|
||||||
|
13
homeassistant/components/media_source/strings.json
Normal file
13
homeassistant/components/media_source/strings.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"exceptions": {
|
||||||
|
"browse_media_failed": {
|
||||||
|
"message": "Failed to browse media with content id {media_content_id}: {error}"
|
||||||
|
},
|
||||||
|
"resolve_media_failed": {
|
||||||
|
"message": "Failed to resolve media with content id {media_content_id}: {error}"
|
||||||
|
},
|
||||||
|
"unknown_media_source": {
|
||||||
|
"message": "Unknown media source: {domain}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -81,6 +81,7 @@ async def test_restore_dashboard_storage_end_to_end(
|
|||||||
assert mock_dashboard_api.mock_calls[0][1][0] == "http://new-host:6052"
|
assert mock_dashboard_api.mock_calls[0][1][0] == "http://new-host:6052"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("hassio_stubs")
|
||||||
async def test_restore_dashboard_storage_skipped_if_addon_uninstalled(
|
async def test_restore_dashboard_storage_skipped_if_addon_uninstalled(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_storage: dict[str, Any],
|
hass_storage: dict[str, Any],
|
||||||
@ -105,9 +106,7 @@ async def test_restore_dashboard_storage_skipped_if_addon_uninstalled(
|
|||||||
return_value={},
|
return_value={},
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
await async_setup_component(hass, "hassio", {})
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
await hass.async_block_till_done()
|
|
||||||
await async_setup_component(hass, DOMAIN, {})
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert "test-slug is no longer installed" in caplog.text
|
assert "test-slug is no longer installed" in caplog.text
|
||||||
assert not mock_dashboard_api.called
|
assert not mock_dashboard_api.called
|
||||||
|
@ -3,17 +3,16 @@
|
|||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from unittest.mock import AsyncMock, Mock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from aiohasupervisor.models import AddonsStats, AddonState
|
from aiohasupervisor.models import AddonsStats, AddonState
|
||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.auth.models import RefreshToken
|
from homeassistant.auth.models import RefreshToken
|
||||||
from homeassistant.components.hassio.handler import HassIO, HassioAPIError
|
from homeassistant.components.hassio.handler import HassIO
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.setup import async_setup_component
|
|
||||||
|
|
||||||
from . import SUPERVISOR_TOKEN
|
from . import SUPERVISOR_TOKEN
|
||||||
|
|
||||||
@ -31,55 +30,6 @@ def disable_security_filter() -> Generator[None]:
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def hassio_env(supervisor_is_connected: AsyncMock) -> Generator[None]:
|
|
||||||
"""Fixture to inject hassio env."""
|
|
||||||
with (
|
|
||||||
patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}),
|
|
||||||
patch.dict(os.environ, {"SUPERVISOR_TOKEN": SUPERVISOR_TOKEN}),
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.hassio.HassIO.get_info",
|
|
||||||
Mock(side_effect=HassioAPIError()),
|
|
||||||
),
|
|
||||||
):
|
|
||||||
yield
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
async def hassio_stubs(
|
|
||||||
hassio_env: None,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
hass_client: ClientSessionGenerator,
|
|
||||||
aioclient_mock: AiohttpClientMocker,
|
|
||||||
supervisor_client: AsyncMock,
|
|
||||||
) -> RefreshToken:
|
|
||||||
"""Create mock hassio http client."""
|
|
||||||
with (
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.hassio.HassIO.update_hass_api",
|
|
||||||
return_value={"result": "ok"},
|
|
||||||
) as hass_api,
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.hassio.HassIO.update_hass_timezone",
|
|
||||||
return_value={"result": "ok"},
|
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.hassio.HassIO.get_info",
|
|
||||||
side_effect=HassioAPIError(),
|
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.hassio.HassIO.get_ingress_panels",
|
|
||||||
return_value={"panels": []},
|
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.hassio.issues.SupervisorIssues.setup",
|
|
||||||
),
|
|
||||||
):
|
|
||||||
await async_setup_component(hass, "hassio", {})
|
|
||||||
|
|
||||||
return hass_api.call_args[0][1]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def hassio_client(
|
async def hassio_client(
|
||||||
hassio_stubs: RefreshToken, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
hassio_stubs: RefreshToken, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
|
@ -57,8 +57,10 @@ async def test_async_browse_media(hass: HomeAssistant) -> None:
|
|||||||
# Test invalid base
|
# Test invalid base
|
||||||
with pytest.raises(BrowseError) as excinfo:
|
with pytest.raises(BrowseError) as excinfo:
|
||||||
await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/")
|
await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/")
|
||||||
assert str(excinfo.value) == "Invalid media source URI"
|
assert str(excinfo.value) == (
|
||||||
|
"Failed to browse media with content id media-source://netatmo/: "
|
||||||
|
"Invalid media source URI"
|
||||||
|
)
|
||||||
# Test successful listing
|
# Test successful listing
|
||||||
media = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/events")
|
media = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/events")
|
||||||
|
|
||||||
|
@ -119,8 +119,10 @@ from .typing import (
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# Local import to avoid processing recorder and SQLite modules when running a
|
# Local import to avoid processing recorder and SQLite modules when running a
|
||||||
# testcase which does not use the recorder.
|
# testcase which does not use the recorder.
|
||||||
|
from homeassistant.auth.models import RefreshToken
|
||||||
from homeassistant.components import recorder
|
from homeassistant.components import recorder
|
||||||
|
|
||||||
|
|
||||||
pytest.register_assert_rewrite("tests.common")
|
pytest.register_assert_rewrite("tests.common")
|
||||||
|
|
||||||
from .common import ( # noqa: E402, isort:skip
|
from .common import ( # noqa: E402, isort:skip
|
||||||
@ -1894,6 +1896,67 @@ def mock_bleak_scanner_start() -> Generator[MagicMock]:
|
|||||||
yield mock_bleak_scanner_start
|
yield mock_bleak_scanner_start
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def hassio_env(supervisor_is_connected: AsyncMock) -> Generator[None]:
|
||||||
|
"""Fixture to inject hassio env."""
|
||||||
|
from homeassistant.components.hassio import ( # pylint: disable=import-outside-toplevel
|
||||||
|
HassioAPIError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .components.hassio import ( # pylint: disable=import-outside-toplevel
|
||||||
|
SUPERVISOR_TOKEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}),
|
||||||
|
patch.dict(os.environ, {"SUPERVISOR_TOKEN": SUPERVISOR_TOKEN}),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.get_info",
|
||||||
|
Mock(side_effect=HassioAPIError()),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def hassio_stubs(
|
||||||
|
hassio_env: None,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: ClientSessionGenerator,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
supervisor_client: AsyncMock,
|
||||||
|
) -> RefreshToken:
|
||||||
|
"""Create mock hassio http client."""
|
||||||
|
from homeassistant.components.hassio import ( # pylint: disable=import-outside-toplevel
|
||||||
|
HassioAPIError,
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.update_hass_api",
|
||||||
|
return_value={"result": "ok"},
|
||||||
|
) as hass_api,
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.update_hass_timezone",
|
||||||
|
return_value={"result": "ok"},
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.get_info",
|
||||||
|
side_effect=HassioAPIError(),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.get_ingress_panels",
|
||||||
|
return_value={"panels": []},
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.hassio.issues.SupervisorIssues.setup",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
await async_setup_component(hass, "hassio", {})
|
||||||
|
|
||||||
|
return hass_api.call_args[0][1]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def integration_frame_path() -> str:
|
def integration_frame_path() -> str:
|
||||||
"""Return the path to the integration frame.
|
"""Return the path to the integration frame.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user