diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3ec09f68a52c09..dbf52fcdd777af 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ on: type: boolean env: - CACHE_VERSION: 9 + CACHE_VERSION: 1 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1 HA_SHORT_VERSION: "2025.11" diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index 23b0062fc82e86..8a7bd098d75801 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -48,6 +48,8 @@ from .const import DOMAIN, LOGGER +DEVICES_URL = "https://developer.lametric.com/user/devices" + class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): """Handle a LaMetric config flow.""" @@ -164,6 +166,9 @@ async def async_step_manual_entry( return self.async_show_form( step_id="manual_entry", data_schema=vol.Schema(schema), + description_placeholders={ + "devices_url": DEVICES_URL, + }, errors=errors, ) diff --git a/homeassistant/components/lametric/strings.json b/homeassistant/components/lametric/strings.json index f3fa1e81112165..acf6923983012f 100644 --- a/homeassistant/components/lametric/strings.json +++ b/homeassistant/components/lametric/strings.json @@ -24,7 +24,7 @@ }, "data_description": { "host": "The IP address or hostname of your LaMetric TIME on your network.", - "api_key": "You can find this API key in the [devices page in your LaMetric developer account](https://developer.lametric.com/user/devices)." + "api_key": "You can find this API key in the [devices page in your LaMetric developer account]({devices_url})." } }, "cloud_select_device": { diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 31a0601a8e7531..d0000ef275137e 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -13,5 +13,5 @@ "iot_class": "cloud_push", "loggers": ["pylitterbot"], "quality_scale": "bronze", - "requirements": ["pylitterbot==2024.2.4"] + "requirements": ["pylitterbot==2024.2.6"] } diff --git a/homeassistant/components/mcp/config_flow.py b/homeassistant/components/mcp/config_flow.py index 064fff32b24d17..a44b22baadc2e4 100644 --- a/homeassistant/components/mcp/config_flow.py +++ b/homeassistant/components/mcp/config_flow.py @@ -47,6 +47,8 @@ "MCP-Protocol-Version": "2025-03-26", } +EXAMPLE_URL = "http://example/sse" + @dataclass class OAuthConfig: @@ -182,7 +184,10 @@ async def async_step_user( return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + errors=errors, + description_placeholders={"example_url": EXAMPLE_URL}, ) async def async_step_auth_discovery( diff --git a/homeassistant/components/mcp/strings.json b/homeassistant/components/mcp/strings.json index 5614609ecd4f4b..bb27370f137d04 100644 --- a/homeassistant/components/mcp/strings.json +++ b/homeassistant/components/mcp/strings.json @@ -6,7 +6,7 @@ "url": "[%key:common::config_flow::data::url%]" }, "data_description": { - "url": "The remote MCP server URL for the SSE endpoint, for example http://example/sse" + "url": "The remote MCP server URL for the SSE endpoint, for example {example_url}" } }, "credentials_choice": { @@ -35,7 +35,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]", "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", - "invalid_url": "Must be a valid MCP server URL e.g. https://example.com/sse" + "invalid_url": "Must be a valid MCP server URL" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/xbox/entity.py b/homeassistant/components/xbox/entity.py index 410ef7306ed544..89f9076906cd56 100644 --- a/homeassistant/components/xbox/entity.py +++ b/homeassistant/components/xbox/entity.py @@ -2,12 +2,23 @@ from __future__ import annotations +from xbox.webapi.api.provider.smartglass.models import ConsoleType, SmartglassConsole + from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import Person, XboxUpdateCoordinator +from .coordinator import ConsoleData, Person, XboxUpdateCoordinator + +MAP_MODEL = { + ConsoleType.XboxOne: "Xbox One", + ConsoleType.XboxOneS: "Xbox One S", + ConsoleType.XboxOneSDigital: "Xbox One S All-Digital", + ConsoleType.XboxOneX: "Xbox One X", + ConsoleType.XboxSeriesS: "Xbox Series S", + ConsoleType.XboxSeriesX: "Xbox Series X", +} class XboxBaseEntity(CoordinatorEntity[XboxUpdateCoordinator]): @@ -21,7 +32,7 @@ def __init__( xuid: str, entity_description: EntityDescription, ) -> None: - """Initialize Xbox binary sensor.""" + """Initialize Xbox entity.""" super().__init__(coordinator) self.xuid = xuid self.entity_description = entity_description @@ -40,3 +51,35 @@ def __init__( def data(self) -> Person: """Return coordinator data for this console.""" return self.coordinator.data.presence[self.xuid] + + +class XboxConsoleBaseEntity(CoordinatorEntity[XboxUpdateCoordinator]): + """Console base entity for the Xbox integration.""" + + _attr_has_entity_name = True + + def __init__( + self, + console: SmartglassConsole, + coordinator: XboxUpdateCoordinator, + ) -> None: + """Initialize the Xbox Console entity.""" + + super().__init__(coordinator) + self.client = coordinator.client + self._console = console + + self._attr_name = None + self._attr_unique_id = console.id + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, console.id)}, + manufacturer="Microsoft", + model=MAP_MODEL.get(self._console.console_type, "Unknown"), + name=console.name, + ) + + @property + def data(self) -> ConsoleData: + """Return coordinator data for this console.""" + return self.coordinator.data.consoles[self._console.id] diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index 505e5f51162ca8..912047c4c65db9 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -2,31 +2,28 @@ from __future__ import annotations -import re from typing import Any from xbox.webapi.api.provider.catalog.models import Image from xbox.webapi.api.provider.smartglass.models import ( PlaybackState, PowerState, - SmartglassConsole, VolumeDirection, ) from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, MediaType, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .browse_media import build_item_response -from .const import DOMAIN -from .coordinator import ConsoleData, XboxConfigEntry, XboxUpdateCoordinator +from .coordinator import XboxConfigEntry +from .entity import XboxConsoleBaseEntity SUPPORT_XBOX = ( MediaPlayerEntityFeature.TURN_ON @@ -69,33 +66,10 @@ async def async_setup_entry( ) -class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntity): +class XboxMediaPlayer(XboxConsoleBaseEntity, MediaPlayerEntity): """Representation of an Xbox Media Player.""" - def __init__( - self, - console: SmartglassConsole, - coordinator: XboxUpdateCoordinator, - ) -> None: - """Initialize the Xbox Media Player.""" - super().__init__(coordinator) - self.client = coordinator.client - self._console = console - - @property - def name(self): - """Return the device name.""" - return self._console.name - - @property - def unique_id(self): - """Console device ID.""" - return self._console.id - - @property - def data(self) -> ConsoleData: - """Return coordinator data for this console.""" - return self.coordinator.data.consoles[self._console.id] + _attr_media_image_remotely_accessible = True @property def state(self) -> MediaPlayerState | None: @@ -117,7 +91,7 @@ def supported_features(self) -> MediaPlayerEntityFeature: return SUPPORT_XBOX @property - def media_content_type(self): + def media_content_type(self) -> MediaType: """Media content type.""" app_details = self.data.app_details if app_details and app_details.product_family == "Games": @@ -125,7 +99,7 @@ def media_content_type(self): return MediaType.APP @property - def media_title(self): + def media_title(self) -> str | None: """Title of current playing media.""" if not (app_details := self.data.app_details): return None @@ -135,13 +109,11 @@ def media_title(self): ) @property - def media_image_url(self): + def media_image_url(self) -> str | None: """Image url of current playing media.""" - if not (app_details := self.data.app_details): - return None - image = _find_media_image(app_details.localized_properties[0].images) - - if not image: + if not (app_details := self.data.app_details) or not ( + image := _find_media_image(app_details.localized_properties[0].images) + ): return None url = image.uri @@ -149,11 +121,6 @@ def media_image_url(self): url = f"http:{url}" return url - @property - def media_image_remotely_accessible(self) -> bool: - """If the image url is remotely accessible.""" - return True - async def async_turn_on(self) -> None: """Turn the media player on.""" await self.client.smartglass.wake_up(self._console.id) @@ -193,15 +160,20 @@ async def async_media_next_track(self) -> None: """Send next track command.""" await self.client.smartglass.next(self._console.id) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" + return await build_item_response( self.client, self._console.id, self.data.status.is_tv_configured, - media_content_type, - media_content_id, - ) + media_content_type or "", + media_content_id or "", + ) # type: ignore[return-value] async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any @@ -214,22 +186,6 @@ async def async_play_media( else: await self.client.smartglass.launch_app(self._console.id, media_id) - @property - def device_info(self) -> DeviceInfo: - """Return a device description for device registry.""" - # Turns "XboxOneX" into "Xbox One X" for display - matches = re.finditer( - ".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)", - self._console.console_type, - ) - - return DeviceInfo( - identifiers={(DOMAIN, self._console.id)}, - manufacturer="Microsoft", - model=" ".join([m.group(0) for m in matches]), - name=self._console.name, - ) - def _find_media_image(images: list[Image]) -> Image | None: purpose_order = ["FeaturePromotionalSquareArt", "Tile", "Logo", "BoxArt"] diff --git a/homeassistant/components/xbox/remote.py b/homeassistant/components/xbox/remote.py index 7f8545e0a20b72..e20a5d230f5763 100644 --- a/homeassistant/components/xbox/remote.py +++ b/homeassistant/components/xbox/remote.py @@ -4,14 +4,9 @@ import asyncio from collections.abc import Iterable -import re from typing import Any -from xbox.webapi.api.provider.smartglass.models import ( - InputKeyType, - PowerState, - SmartglassConsole, -) +from xbox.webapi.api.provider.smartglass.models import InputKeyType, PowerState from homeassistant.components.remote import ( ATTR_DELAY_SECS, @@ -20,12 +15,10 @@ RemoteEntity, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN -from .coordinator import ConsoleData, XboxConfigEntry, XboxUpdateCoordinator +from .coordinator import XboxConfigEntry +from .entity import XboxConsoleBaseEntity async def async_setup_entry( @@ -41,36 +34,11 @@ async def async_setup_entry( ) -class XboxRemote(CoordinatorEntity[XboxUpdateCoordinator], RemoteEntity): +class XboxRemote(XboxConsoleBaseEntity, RemoteEntity): """Representation of an Xbox remote.""" - def __init__( - self, - console: SmartglassConsole, - coordinator: XboxUpdateCoordinator, - ) -> None: - """Initialize the Xbox Media Player.""" - super().__init__(coordinator) - self.client = coordinator.client - self._console = console - - @property - def name(self): - """Return the device name.""" - return f"{self._console.name} Remote" - - @property - def unique_id(self): - """Console device ID.""" - return self._console.id - @property - def data(self) -> ConsoleData: - """Return coordinator data for this console.""" - return self.coordinator.data.consoles[self._console.id] - - @property - def is_on(self): + def is_on(self) -> bool: """Return True if device is on.""" return self.data.status.power_state == PowerState.On @@ -97,19 +65,3 @@ async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> Non self._console.id, single_command ) await asyncio.sleep(delay) - - @property - def device_info(self) -> DeviceInfo: - """Return a device description for device registry.""" - # Turns "XboxOneX" into "Xbox One X" for display - matches = re.finditer( - ".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)", - self._console.console_type, - ) - - return DeviceInfo( - identifiers={(DOMAIN, self._console.id)}, - manufacturer="Microsoft", - model=" ".join([m.group(0) for m in matches]), - name=self._console.name, - ) diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 7bf139e9c3be7a..af7bbc1c66985f 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -11,6 +11,7 @@ from homeassistant.components import media_source from homeassistant.components.media_player import ( + BrowseError, BrowseMedia, MediaClass, MediaPlayerEntity, @@ -372,7 +373,7 @@ def get_content_type(item): ] if add_media_source: - with contextlib.suppress(media_source.BrowseError): + with contextlib.suppress(BrowseError): item = await media_source.async_browse_media( self.hass, None, diff --git a/requirements_all.txt b/requirements_all.txt index 7a7d350802d04d..e61c3f69c9563e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2162,7 +2162,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.3 # homeassistant.components.litterrobot -pylitterbot==2024.2.4 +pylitterbot==2024.2.6 # homeassistant.components.lutron_caseta pylutron-caseta==0.25.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8dc556c848f338..86951dfd82814c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1810,7 +1810,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.3 # homeassistant.components.litterrobot -pylitterbot==2024.2.4 +pylitterbot==2024.2.6 # homeassistant.components.lutron_caseta pylutron-caseta==0.25.0 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index fd3caf5128e8c9..f413b15872cc27 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -314,8 +314,6 @@ "lyric": {"homeassistant": {"aiolyric"}}, # https://github.com/microBeesTech/pythonSDK/ "microbees": {"homeassistant": {"microbeespy"}}, - # https://github.com/tiagocoutinho/async_modbus - "nibe_heatpump": {"nibe": {"async-modbus"}}, # https://github.com/ejpenney/pyobihai "obihai": {"homeassistant": {"pyobihai"}}, # https://github.com/iamkubi/pydactyl diff --git a/tests/components/xbox/snapshots/test_media_player.ambr b/tests/components/xbox/snapshots/test_media_player.ambr index 2bcf5bc54694c4..e68d1df01c1898 100644 --- a/tests/components/xbox/snapshots/test_media_player.ambr +++ b/tests/components/xbox/snapshots/test_media_player.ambr @@ -14,7 +14,7 @@ 'domain': 'media_player', 'entity_category': None, 'entity_id': 'media_player.xone', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -25,7 +25,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'XONE', + 'original_name': None, 'platform': 'xbox', 'previous_unique_id': None, 'suggested_object_id': None, @@ -68,7 +68,7 @@ 'domain': 'media_player', 'entity_category': None, 'entity_id': 'media_player.xonex', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -79,7 +79,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'XONEX', + 'original_name': None, 'platform': 'xbox', 'previous_unique_id': None, 'suggested_object_id': None, diff --git a/tests/components/xbox/snapshots/test_remote.ambr b/tests/components/xbox/snapshots/test_remote.ambr index da215bbf0016a5..3017649ad5aef8 100644 --- a/tests/components/xbox/snapshots/test_remote.ambr +++ b/tests/components/xbox/snapshots/test_remote.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_remotes[remote.xone_remote-entry] +# name: test_remotes[remote.xone-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,8 +12,8 @@ 'disabled_by': None, 'domain': 'remote', 'entity_category': None, - 'entity_id': 'remote.xone_remote', - 'has_entity_name': False, + 'entity_id': 'remote.xone', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -24,7 +24,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'XONE Remote', + 'original_name': None, 'platform': 'xbox', 'previous_unique_id': None, 'suggested_object_id': None, @@ -34,21 +34,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_remotes[remote.xone_remote-state] +# name: test_remotes[remote.xone-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'XONE Remote', + 'friendly_name': 'XONE', 'supported_features': , }), 'context': , - 'entity_id': 'remote.xone_remote', + 'entity_id': 'remote.xone', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_remotes[remote.xonex_remote-entry] +# name: test_remotes[remote.xonex-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -61,8 +61,8 @@ 'disabled_by': None, 'domain': 'remote', 'entity_category': None, - 'entity_id': 'remote.xonex_remote', - 'has_entity_name': False, + 'entity_id': 'remote.xonex', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -73,7 +73,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'XONEX Remote', + 'original_name': None, 'platform': 'xbox', 'previous_unique_id': None, 'suggested_object_id': None, @@ -83,14 +83,14 @@ 'unit_of_measurement': None, }) # --- -# name: test_remotes[remote.xonex_remote-state] +# name: test_remotes[remote.xonex-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'XONEX Remote', + 'friendly_name': 'XONEX', 'supported_features': , }), 'context': , - 'entity_id': 'remote.xonex_remote', + 'entity_id': 'remote.xonex', 'last_changed': , 'last_reported': , 'last_updated': ,