From c0bc06bdc54bd171b510de593b099be72859c77f Mon Sep 17 00:00:00 2001 From: mj23000 Date: Thu, 4 Apr 2024 15:37:57 +0200 Subject: [PATCH] Add all_discovered key to beolink_expand service Improve Beolink services input validation Move queue_settings from coordinator into media_player polling Update config_flow to 2024.4 Fix tidal being a selectable source --- custom_components/bang_olufsen/button.py | 4 +- custom_components/bang_olufsen/config_flow.py | 10 +- custom_components/bang_olufsen/const.py | 1 + custom_components/bang_olufsen/coordinator.py | 36 +- custom_components/bang_olufsen/manifest.json | 2 +- .../bang_olufsen/media_player.py | 109 ++-- custom_components/bang_olufsen/services.yaml | 7 +- custom_components/bang_olufsen/strings.json | 548 +++++++++--------- .../bang_olufsen/translations/en.json | 4 + hacs.json | 2 +- 10 files changed, 350 insertions(+), 373 deletions(-) diff --git a/custom_components/bang_olufsen/button.py b/custom_components/bang_olufsen/button.py index 418261a..3236d3b 100644 --- a/custom_components/bang_olufsen/button.py +++ b/custom_components/bang_olufsen/button.py @@ -29,7 +29,7 @@ async def async_setup_entry( entities: list[BangOlufsenEntity] = [] # Get available favourites from coordinator. - favourites = data.coordinator.data.favourites + favourites = data.coordinator.data entities.extend( [ @@ -100,7 +100,7 @@ async def async_press(self) -> None: @callback def _update_favourite(self) -> None: """Update favourite attribute.""" - self._favourite = self.coordinator.data.favourites[str(self._favourite_id)] + self._favourite = self.coordinator.data[str(self._favourite_id)] self._attr_extra_state_attributes = self._generate_favourite_attributes() diff --git a/custom_components/bang_olufsen/config_flow.py b/custom_components/bang_olufsen/config_flow.py index 76e07cc..3fd0330 100644 --- a/custom_components/bang_olufsen/config_flow.py +++ b/custom_components/bang_olufsen/config_flow.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.zeroconf import ZeroconfServiceInfo -from homeassistant.config_entries import ConfigFlow, FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_MODEL from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig @@ -61,7 +61,7 @@ def __init__(self) -> None: async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" data_schema = vol.Schema( { @@ -116,7 +116,7 @@ async def async_step_user( async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle discovery using Zeroconf.""" # Check if the discovered device is a Mozart device @@ -144,7 +144,7 @@ async def async_step_zeroconf( return await self.async_step_zeroconf_confirm() - async def _create_entry(self) -> FlowResult: + async def _create_entry(self) -> ConfigFlowResult: """Create the config entry for a discovered or manually configured Bang & Olufsen device.""" # Ensure that created entities have a unique and easily identifiable id and not a "friendly name" self._name = f"{self._model}-{self._serial_number}" @@ -161,7 +161,7 @@ async def _create_entry(self) -> FlowResult: async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm the configuration of the device.""" if user_input is not None: return await self._create_entry() diff --git a/custom_components/bang_olufsen/const.py b/custom_components/bang_olufsen/const.py index 963f355..9fd4a42 100644 --- a/custom_components/bang_olufsen/const.py +++ b/custom_components/bang_olufsen/const.py @@ -196,6 +196,7 @@ class BangOlufsenModelSupport(Enum): "beolink", "classicsAdapter", "usbIn", + "tidal", ) # Fallback sources to use in case of API failure. diff --git a/custom_components/bang_olufsen/coordinator.py b/custom_components/bang_olufsen/coordinator.py index 4968188..7b63852 100644 --- a/custom_components/bang_olufsen/coordinator.py +++ b/custom_components/bang_olufsen/coordinator.py @@ -2,9 +2,9 @@ from __future__ import annotations -from dataclasses import dataclass from datetime import timedelta import logging +from typing import Any from aiohttp.client_exceptions import ClientConnectorError from mozart_api.exceptions import ApiException @@ -16,8 +16,6 @@ PlaybackContentMetadata, PlaybackError, PlaybackProgress, - PlayQueueSettings, - Preset, RenderingState, SoftwareUpdateState, SoundSettings, @@ -48,14 +46,6 @@ _LOGGER = logging.getLogger(__name__) -@dataclass -class CoordinatorData: - """Dataclass for coordinator data.""" - - favourites: dict[str, Preset] - queue_settings: PlayQueueSettings - - class BangOlufsenCoordinator(DataUpdateCoordinator, BangOlufsenBase): """The entity coordinator and WebSocket listener(s).""" @@ -73,11 +63,6 @@ def __init__( ) BangOlufsenBase.__init__(self, entry, client) - self._coordinator_data: CoordinatorData = CoordinatorData( - favourites={}, - queue_settings=PlayQueueSettings(), - ) - self._device = self._get_device() # WebSocket callbacks @@ -119,22 +104,13 @@ def __init__( # Used for firing events and debugging self._client.get_all_notifications_raw(self.on_all_notifications_raw) - async def _update_variables(self) -> None: - """Update the coordinator data.""" - favourites = await self._client.get_presets(_request_timeout=5) - queue_settings = await self._client.get_settings_queue(_request_timeout=5) - - self._coordinator_data = CoordinatorData( - favourites=favourites, - queue_settings=queue_settings, - ) - - async def _async_update_data(self) -> CoordinatorData: + async def _async_update_data(self) -> dict[str, Any]: """Get all information needed by the polling entities.""" # Try to update coordinator_data. try: - await self._update_variables() - return self._coordinator_data + favourites = await self._client.get_presets(_request_timeout=5) + + return favourites except (TimeoutError, ClientConnectorError, ApiException) as error: raise UpdateFailed from error @@ -350,5 +326,5 @@ def on_all_notifications_raw(self, notification: dict) -> None: notification["device_id"] = self._device.id notification["serial_number"] = int(self._unique_id) - _LOGGER.warning("%s", notification) + _LOGGER.debug("%s", notification) self.hass.bus.async_fire(BANG_OLUFSEN_WEBSOCKET_EVENT, notification) diff --git a/custom_components/bang_olufsen/manifest.json b/custom_components/bang_olufsen/manifest.json index df31d08..94ab60b 100644 --- a/custom_components/bang_olufsen/manifest.json +++ b/custom_components/bang_olufsen/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/bang-olufsen/bang_olufsen-hacs/issues", "requirements": ["mozart-api==3.4.1.8.4"], - "version": "2.1.3", + "version": "2.2.0", "zeroconf": ["_bangolufsen._tcp.local."] } diff --git a/custom_components/bang_olufsen/media_player.py b/custom_components/bang_olufsen/media_player.py index 633089b..934298a 100644 --- a/custom_components/bang_olufsen/media_player.py +++ b/custom_components/bang_olufsen/media_player.py @@ -7,7 +7,7 @@ from typing import Any, cast from mozart_api import __version__ as MOZART_API_VERSION -from mozart_api.exceptions import ApiException +from mozart_api.exceptions import ApiException, NotFoundException from mozart_api.models import ( Action, Art, @@ -34,7 +34,7 @@ VolumeMute, VolumeState, ) -from mozart_api.mozart_client import check_valid_jid, get_highest_resolution_artwork +from mozart_api.mozart_client import get_highest_resolution_artwork import voluptuous as vol from homeassistant.components import media_source @@ -51,12 +51,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODEL, Platform -from homeassistant.core import ( - HomeAssistant, - ServiceResponse, - SupportsResponse, - callback, -) +from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -72,7 +67,6 @@ async_get_current_platform, ) from homeassistant.helpers.entity_registry import RegistryEntry -from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow from . import BangOlufsenData @@ -133,7 +127,7 @@ async def async_setup_entry( set_platform_initialized(data) - async_add_entities(new_entities=entities) + async_add_entities(new_entities=entities, update_before_add=True) # Register services. platform = async_get_current_platform() @@ -141,10 +135,9 @@ async def async_setup_entry( platform.async_register_entity_service( name="beolink_join", schema={ - vol.Optional("beolink_jid"): vol.All( - vol.Coerce(type=cv.string), - vol.Length(min=47, max=47), - ), + vol.Optional("beolink_jid"): vol.Match( + r"(^\d{4})[.](\d{7})[.](\d{8})(@products\.bang-olufsen\.com)$" + ) }, func="async_beolink_join", supports_response=SupportsResponse.OPTIONAL, @@ -153,15 +146,19 @@ async def async_setup_entry( platform.async_register_entity_service( name="beolink_expand", schema={ - vol.Required("beolink_jids"): vol.All( + vol.Exclusive("all_discovered", "devices", ""): cv.boolean, + vol.Exclusive( + "beolink_jids", + "devices", + "Define either specific Beolink JIDs or all discovered", + ): vol.All( cv.ensure_list, [ - vol.All( - vol.Coerce(type=cv.string), - vol.Length(min=47, max=47), + vol.Match( + r"(^\d{4})[.](\d{7})[.](\d{8})(@products\.bang-olufsen\.com)$" ) ], - ) + ), }, func="async_beolink_expand", supports_response=SupportsResponse.OPTIONAL, @@ -173,12 +170,11 @@ async def async_setup_entry( vol.Required("beolink_jids"): vol.All( cv.ensure_list, [ - vol.All( - vol.Coerce(type=cv.string), - vol.Length(min=47, max=47), + vol.Match( + r"(^\d{4})[.](\d{7})[.](\d{8})(@products\.bang-olufsen\.com)$" ) ], - ) + ), }, func="async_beolink_unexpand", supports_response=SupportsResponse.OPTIONAL, @@ -240,7 +236,7 @@ async def async_setup_entry( ) -class BangOlufsenMediaPlayer(MediaPlayerEntity, CoordinatorEntity, BangOlufsenEntity): +class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): """Representation of a media player.""" _attr_device_class = MediaPlayerDeviceClass.SPEAKER @@ -251,8 +247,8 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, CoordinatorEntity, BangOlufsenEn def __init__(self, entry: ConfigEntry, data: BangOlufsenData) -> None: """Initialize the media player.""" MediaPlayerEntity.__init__(self) - CoordinatorEntity.__init__(self, data.coordinator) BangOlufsenEntity.__init__(self, entry, data.client) + self._attr_should_poll = True self._beolink_jid: str = self.entry.data[CONF_BEOLINK_JID] self._model: str = self.entry.data[CONF_MODEL] @@ -401,10 +397,6 @@ async def async_added_to_hass(self) -> None: ) ) - self.async_on_remove( - self.coordinator.async_add_listener(self._update_queue_settings) - ) - self.set_entity_initialized() async def _initialize(self) -> None: @@ -447,9 +439,6 @@ async def _initialize(self) -> None: self._media_image = get_highest_resolution_artwork(self._playback_metadata) - # Get queue_settings from coordinator - self._queue_settings = self.coordinator.data.queue_settings - # If the device has been updated with new sources, then the API will fail here. await self._update_sources() @@ -458,12 +447,9 @@ async def _initialize(self) -> None: await self._update_name_and_beolink() - @callback - def _update_queue_settings(self) -> None: + async def async_update(self) -> None: """Update queue settings.""" - self._queue_settings = self.coordinator.data.queue_settings - - self.async_write_ha_state() + self._queue_settings = await self._client.get_settings_queue() async def _update_name_and_beolink(self) -> None: """Update the device friendly name.""" @@ -1051,7 +1037,7 @@ async def async_join_players(self, group_members: list[str]) -> None: jids.append(jid) - await self.async_beolink_expand(jids) + await self.async_beolink_expand(beolink_jids=jids) async def async_unjoin_player(self) -> None: """Unjoin Beolink session. End session if leader.""" @@ -1155,7 +1141,8 @@ async def async_play_media( ) except ApiException as error: - _LOGGER.error(json.loads(error.body)["message"]) + if error.body: + _LOGGER.error(json.loads(error.body)["message"]) async def async_browse_media( self, @@ -1177,46 +1164,46 @@ async def async_beolink_join( if beolink_jid is None: response = await self._client.join_latest_beolink_experience() else: - if not check_valid_jid(beolink_jid): - return {"invalid_jid": beolink_jid} response = await self._client.join_beolink_peer(jid=beolink_jid) return response.dict() - async def async_beolink_expand(self, beolink_jids: list[str]) -> ServiceResponse: + async def async_beolink_expand( + self, beolink_jids: list[str] | None = None, all_discovered: bool = False + ) -> ServiceResponse: """Expand a Beolink multi-room experience with a device or devices.""" - response: dict[str, Any] = {"invalid_jid": []} + response: dict[str, Any] = {"not_on_network": []} # Ensure that the current source is expandable if not self._beolink_sources[cast(str, self._source_change.id)]: return {"invalid_source": self.source} - for beolink_jid in beolink_jids: - if not check_valid_jid(beolink_jid): - response["invalid_jid"].append(beolink_jid) - continue - await self._client.post_beolink_expand(jid=beolink_jid) + # Expand to all discovered devices + if all_discovered: + peers = await self._client.get_beolink_peers() + + for peer in peers: + await self._client.post_beolink_expand(jid=peer.jid) - if len(response["invalid_jid"]) > 0: - return response + # Try to expand to all defined devices + elif beolink_jids: + for beolink_jid in beolink_jids: + try: + await self._client.post_beolink_expand(jid=beolink_jid) + except NotFoundException: + response["not_on_network"].append(beolink_jid) + + if len(response["not_on_network"]) > 0: + return response return None - async def async_beolink_unexpand(self, beolink_jids: list[str]) -> ServiceResponse: + async def async_beolink_unexpand(self, beolink_jids: list[str]) -> None: """Unexpand a Beolink multi-room experience with a device or devices.""" - response: dict[str, Any] = {"invalid_jid": []} - + # Unexpand all defined devices for beolink_jid in beolink_jids: - if not check_valid_jid(beolink_jid): - response["invalid_jid"].append(beolink_jid) - continue await self._client.post_beolink_unexpand(jid=beolink_jid) - if len(response["invalid_jid"]) > 0: - return response - - return None - async def async_beolink_leave(self) -> None: """Leave the current Beolink experience.""" await self._client.post_beolink_leave() diff --git a/custom_components/bang_olufsen/services.yaml b/custom_components/bang_olufsen/services.yaml index 5ed6a67..4f5316b 100644 --- a/custom_components/bang_olufsen/services.yaml +++ b/custom_components/bang_olufsen/services.yaml @@ -20,8 +20,13 @@ beolink_expand: device: integration: bang_olufsen fields: + all_discovered: + required: false + example: false + selector: + boolean: beolink_jids: - required: true + required: false example: >- [ 1111.2222222.33333333@products.bang-olufsen.com, diff --git a/custom_components/bang_olufsen/strings.json b/custom_components/bang_olufsen/strings.json index 3eca52f..d6c564a 100644 --- a/custom_components/bang_olufsen/strings.json +++ b/custom_components/bang_olufsen/strings.json @@ -1,274 +1,278 @@ { - "config": { - "error": { - "api_exception": "[%key:common::config_flow::error::cannot_connect%]", - "client_connector_error": "[%key:common::config_flow::error::cannot_connect%]", - "timeout_error": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_ip": "Invalid IPv4 address" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::single_instance_allowed%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" - }, - "flow_title": "{name}", - "step": { - "user": { - "data": { - "host": "[%key:common::config_flow::data::ip%]", - "model": "[%key:common::generic::model%]" - }, - "description": "Manually configure your Bang & Olufsen device." - }, - "zeroconf_confirm": { - "title": "Setup Bang & Olufsen device", - "description": "Confirm the configuration of the {model}-{serial_number} @ {host}." - } - } - }, - "device_automation": { - "trigger_type": { - "Bluetooth_shortPress": "Short press of Bluetooth", - "Bluetooth_longPress": "Long press of Bluetooth", - "Control/Blue_KeyPress": "Press of Beoremote One Control: Blue", - "Control/Blue_KeyRelease": "Release of Beoremote One Control: Blue", - "Control/Digit0_KeyPress": "Press of Beoremote One Control: 0", - "Control/Digit0_KeyRelease": "Release of Beoremote One Control: 0", - "Control/Digit1_KeyPress": "Press of Beoremote One Control: 1", - "Control/Digit1_KeyRelease": "Release of Beoremote One Control: 1", - "Control/Digit2_KeyPress": "Press of Beoremote One Control: 2", - "Control/Digit2_KeyRelease": "Release of Beoremote One Control: 2", - "Control/Digit3_KeyPress": "Press of Beoremote One Control: 3", - "Control/Digit3_KeyRelease": "Release of Beoremote One Control: 3", - "Control/Digit4_KeyPress": "Press of Beoremote One Control: 4", - "Control/Digit4_KeyRelease": "Release of Beoremote One Control: 4", - "Control/Digit5_KeyPress": "Press of Beoremote One Control: 5", - "Control/Digit5_KeyRelease": "Release of Beoremote One Control: 5", - "Control/Digit6_KeyPress": "Press of Beoremote One Control: 6", - "Control/Digit6_KeyRelease": "Release of Beoremote One Control: 6", - "Control/Digit7_KeyPress": "Press of Beoremote One Control: 7", - "Control/Digit7_KeyRelease": "Release of Beoremote One Control: 7", - "Control/Digit8_KeyPress": "Press of Beoremote One Control: 8", - "Control/Digit8_KeyRelease": "Release of Beoremote One Control: 8", - "Control/Digit9_KeyPress": "Press of Beoremote One Control: 9", - "Control/Digit9_KeyRelease": "Release of Beoremote One Control: 9", - "Control/Down_KeyPress": "Press of Beoremote One Control: Down", - "Control/Down_KeyRelease": "Release of Beoremote One Control: Down", - "Control/Green_KeyPress": "Press of Beoremote One Control: Green", - "Control/Green_KeyRelease": "Release of Beoremote One Control: Green", - "Control/Left_KeyPress": "Press of Beoremote One Control: Left", - "Control/Left_KeyRelease": "Release of Beoremote One Control: Left", - "Control/Play_KeyPress": "Press of Beoremote One Control: Play", - "Control/Play_KeyRelease": "Release of Beoremote One Control: Play", - "Control/Red_KeyPress": "Press of Beoremote One Control: Red", - "Control/Red_KeyRelease": "Release of Beoremote One Control: Red", - "Control/Rewind_KeyPress": "Press of Beoremote One Control Rewind", - "Control/Rewind_KeyRelease": "Release of Beoremote One Control Rewind", - "Control/Right_KeyPress": "Press of Beoremote One Control: Right", - "Control/Right_KeyRelease": "Release of Beoremote One Control: Right", - "Control/Select_KeyPress": "Press of Beoremote One Control: Select", - "Control/Select_KeyRelease": "Release of Beoremote One Control: Select", - "Control/Stop_KeyPress": "Press of Beoremote One Control: Stop", - "Control/Stop_KeyRelease": "Release of Beoremote One Control: Stop", - "Control/Up_KeyPress": "Press of Beoremote One Control: Up", - "Control/Up_KeyRelease": "Release of Beoremote One Control: Up", - "Control/Wind_KeyPress": "Press of Beoremote One Control Wind", - "Control/Wind_KeyRelease": "Release of Beoremote One Control Wind", - "Control/Yellow_KeyPress": "Press of Beoremote One Control: Yellow", - "Control/Yellow_KeyRelease": "Release of Beoremote One Control: Yellow", - "Light/Blue_KeyPress": "Press of Beoremote One Light: Blue", - "Light/Blue_KeyRelease": "Release of Beoremote One Light: Blue", - "Light/Digit0_KeyPress": "Press of Beoremote One Light: 0", - "Light/Digit0_KeyRelease": "Release of Beoremote One Light: 0", - "Light/Digit1_KeyPress": "Press of Beoremote One Light: 1", - "Light/Digit1_KeyRelease": "Release of Beoremote One Light: 1", - "Light/Digit2_KeyPress": "Press of Beoremote One Light: 2", - "Light/Digit2_KeyRelease": "Release of Beoremote One Light: 2", - "Light/Digit3_KeyPress": "Press of Beoremote One Light: 3", - "Light/Digit3_KeyRelease": "Release of Beoremote One Light: 3", - "Light/Digit4_KeyPress": "Press of Beoremote One Light: 4", - "Light/Digit4_KeyRelease": "Release of Beoremote One Light: 4", - "Light/Digit5_KeyPress": "Press of Beoremote One Light: 5", - "Light/Digit5_KeyRelease": "Release of Beoremote One Light: 5", - "Light/Digit6_KeyPress": "Press of Beoremote One Light: 6", - "Light/Digit6_KeyRelease": "Release of Beoremote One Light: 6", - "Light/Digit7_KeyPress": "Press of Beoremote One Light: 7", - "Light/Digit7_KeyRelease": "Release of Beoremote One Light: 7", - "Light/Digit8_KeyPress": "Press of Beoremote One Light: 8", - "Light/Digit8_KeyRelease": "Release of Beoremote One Light: 8", - "Light/Digit9_KeyPress": "Press of Beoremote One Light: 9", - "Light/Digit9_KeyRelease": "Release of Beoremote One Light: 9", - "Light/Down_KeyPress": "Press of Beoremote One Light: Down", - "Light/Down_KeyRelease": "Release of Beoremote One Light: Down", - "Light/Green_KeyPress": "Press of Beoremote One Light: Green", - "Light/Green_KeyRelease": "Release of Beoremote One Light: Green", - "Light/Left_KeyPress": "Press of Beoremote One Light: Left", - "Light/Left_KeyRelease": "Release of Beoremote One Light: Left", - "Light/Play_KeyPress": "Press of Beoremote One Light: Play", - "Light/Play_KeyRelease": "Release of Beoremote One Light: Play", - "Light/Red_KeyPress": "Press of Beoremote One Light: Red", - "Light/Red_KeyRelease": "Release of Beoremote One Light: Red", - "Light/Rewind_KeyPress": "Press of Beoremote One Light Rewind", - "Light/Rewind_KeyRelease": "Release of Beoremote One Light Rewind", - "Light/Right_KeyPress": "Press of Beoremote One Light: Right", - "Light/Right_KeyRelease": "Release of Beoremote One Light: Right", - "Light/Select_KeyPress": " Press of Beoremote One Light: Select", - "Light/Select_KeyRelease": " Release of Beoremote One Light: Select", - "Light/Stop_KeyPress": "Press of Beoremote One Light: Stop", - "Light/Stop_KeyRelease": "Release of Beoremote One Light: Stop", - "Light/Up_KeyPress": "Press of Beoremote One Light: Up", - "Light/Up_KeyRelease": "Release of Beoremote One Light: Up", - "Light/Wind_KeyPress": "Press of Beoremote One Light Wind", - "Light/Wind_KeyRelease": "Release of Beoremote One Light Wind", - "Light/Yellow_KeyPress": "Press of Beoremote One Light: Yellow", - "Light/Yellow_KeyRelease": "Release of Beoremote One Light: Yellow", - "Microphone_shortPress": "Short press of Microphone", - "Next_shortPress": "Short press of Next", - "PlayPause_shortPress": "Short press of Play/Pause", - "PlayPause_longPress": "Long press of Play/Pause", - "Preset1_shortPress": "Short press of Favourite 1", - "Preset2_shortPress": "Short press of Favourite 2", - "Preset3_shortPress": "Short press of Favourite 3", - "Preset4_shortPress": "Short press of Favourite 4", - "Previous_shortPress": "Short press of Previous" - } - }, - "services": { - "beolink_join": { - "name": "Beolink join", - "description": "Join a Beolink experience.", - "fields": { - "beolink_jid": { - "name": "Beolink JID", - "description": "Manually specify Beolink JID to join." - } - } - }, - "beolink_expand": { - "name": "Beolink expand", - "description": "Expand current Beolink experience.", - "fields": { - "beolink_jids": { - "name": "Beolink JIDs", - "description": "Specify which Beolink JIDs will join current Beolink experience." - } - } - }, - "beolink_unexpand": { - "name": "Beolink unexpand", - "description": "Unexpand from current Beolink experience.", - "fields": { - "beolink_jids": { - "name": "Beolink JIDs", - "description": "Specify which Beolink JIDs will leave from current Beolink experience." - } - } - }, - "beolink_leave": { - "name": "Beolink leave", - "description": "Leave a Beolink experience." - }, - "beolink_allstandby": { - "name": "Beolink allstandby", - "description": "Set all Connected Beolink devices to standby." - }, - "beolink_set_volume": { - "name": "Beolink set volume", - "description": "Set a volume level for all connected Beolink devices.", - "fields": { - "volume_level": { - "name": "Volume level", - "description": "Specify the volume level." - } - } - }, - "beolink_set_relative_volume": { - "name": "Beolink set relative volume", - "description": "Set a volume level relative to the current level of all connected Beolink devices.", - "fields": { - "volume_level": { - "name": "Volume level", - "description": "Specify the volume level." - } - } - }, - "beolink_leader_command": { - "name": "Beolink leader command", - "description": "Send a media_player command to Beolink leader.", - "fields": { - "command": { - "name": "Command", - "description": "Specify the media_player command." - }, - "parameter": { - "name": "Parameter", - "description": "Specify the media_player command's parameter." - } - } - }, - "overlay_audio": { - "name": "Overlay audio", - "description": "Overlay audio over any currently playing audio. TTS is generated by Bang & Olufsen and is limited to 100 unique messages a day. Generated TTS messages are cached for 24 hours.", - "fields": { - "uri": { - "name": "URI", - "description": "Specify the audio to play." - }, - "absolute_volume": { - "name": "Absolute volume", - "description": "Specify an absolute volume for the overlay." - }, - "volume_offset": { - "name": "Volume offset", - "description": "Specify a volume offset to be added to the current volume level." - }, - "tts": { - "name": "Text to speech", - "description": "Specify a string to be converted to a TTS message." - }, - "tts_language": { - "name": "Text to speech language", - "description": "Specify TTS language." - } - } - } - }, - "entity": { - "binary_sensor": { - "battery_charging": { - "name": "Battery charging" - }, - "proximity": { - "name": "Proximity" - } - }, - "button": { - "favourite": { - "name": "Favourite {id}" - } - }, - "number": { - "treble": { "name": "Treble" }, - "bass": { "name": "Bass" } - }, - "select": { - "sound_mode": { "name": "Sound mode" }, - "listening_position": { "name": "Listening position" } - }, - "sensor": { - "battery_level": { "name": "Battery level" }, - "battery_charging_time": { "name": "Battery charging time" }, - "battery_playing_time": { "name": "Battery playing time" }, - "media_id": { "name": "Media id" }, - "input_signal": { "name": "Input signal" } - }, - "switch": { - "loudness": { - "name": "Loudness" - } - }, - "text": { - "friendly_name": { "name": "Device friendly name" }, - "home_control_uri": { "name": "Home Control URI" } - } - } + "config": { + "error": { + "api_exception": "[%key:common::config_flow::error::cannot_connect%]", + "client_connector_error": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_error": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_ip": "Invalid IPv4 address" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::single_instance_allowed%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::ip%]", + "model": "[%key:common::generic::model%]" + }, + "description": "Manually configure your Bang & Olufsen device." + }, + "zeroconf_confirm": { + "title": "Setup Bang & Olufsen device", + "description": "Confirm the configuration of the {model}-{serial_number} @ {host}." + } + } + }, + "device_automation": { + "trigger_type": { + "Bluetooth_shortPress": "Short press of Bluetooth", + "Bluetooth_longPress": "Long press of Bluetooth", + "Control/Blue_KeyPress": "Press of Beoremote One Control: Blue", + "Control/Blue_KeyRelease": "Release of Beoremote One Control: Blue", + "Control/Digit0_KeyPress": "Press of Beoremote One Control: 0", + "Control/Digit0_KeyRelease": "Release of Beoremote One Control: 0", + "Control/Digit1_KeyPress": "Press of Beoremote One Control: 1", + "Control/Digit1_KeyRelease": "Release of Beoremote One Control: 1", + "Control/Digit2_KeyPress": "Press of Beoremote One Control: 2", + "Control/Digit2_KeyRelease": "Release of Beoremote One Control: 2", + "Control/Digit3_KeyPress": "Press of Beoremote One Control: 3", + "Control/Digit3_KeyRelease": "Release of Beoremote One Control: 3", + "Control/Digit4_KeyPress": "Press of Beoremote One Control: 4", + "Control/Digit4_KeyRelease": "Release of Beoremote One Control: 4", + "Control/Digit5_KeyPress": "Press of Beoremote One Control: 5", + "Control/Digit5_KeyRelease": "Release of Beoremote One Control: 5", + "Control/Digit6_KeyPress": "Press of Beoremote One Control: 6", + "Control/Digit6_KeyRelease": "Release of Beoremote One Control: 6", + "Control/Digit7_KeyPress": "Press of Beoremote One Control: 7", + "Control/Digit7_KeyRelease": "Release of Beoremote One Control: 7", + "Control/Digit8_KeyPress": "Press of Beoremote One Control: 8", + "Control/Digit8_KeyRelease": "Release of Beoremote One Control: 8", + "Control/Digit9_KeyPress": "Press of Beoremote One Control: 9", + "Control/Digit9_KeyRelease": "Release of Beoremote One Control: 9", + "Control/Down_KeyPress": "Press of Beoremote One Control: Down", + "Control/Down_KeyRelease": "Release of Beoremote One Control: Down", + "Control/Green_KeyPress": "Press of Beoremote One Control: Green", + "Control/Green_KeyRelease": "Release of Beoremote One Control: Green", + "Control/Left_KeyPress": "Press of Beoremote One Control: Left", + "Control/Left_KeyRelease": "Release of Beoremote One Control: Left", + "Control/Play_KeyPress": "Press of Beoremote One Control: Play", + "Control/Play_KeyRelease": "Release of Beoremote One Control: Play", + "Control/Red_KeyPress": "Press of Beoremote One Control: Red", + "Control/Red_KeyRelease": "Release of Beoremote One Control: Red", + "Control/Rewind_KeyPress": "Press of Beoremote One Control Rewind", + "Control/Rewind_KeyRelease": "Release of Beoremote One Control Rewind", + "Control/Right_KeyPress": "Press of Beoremote One Control: Right", + "Control/Right_KeyRelease": "Release of Beoremote One Control: Right", + "Control/Select_KeyPress": "Press of Beoremote One Control: Select", + "Control/Select_KeyRelease": "Release of Beoremote One Control: Select", + "Control/Stop_KeyPress": "Press of Beoremote One Control: Stop", + "Control/Stop_KeyRelease": "Release of Beoremote One Control: Stop", + "Control/Up_KeyPress": "Press of Beoremote One Control: Up", + "Control/Up_KeyRelease": "Release of Beoremote One Control: Up", + "Control/Wind_KeyPress": "Press of Beoremote One Control Wind", + "Control/Wind_KeyRelease": "Release of Beoremote One Control Wind", + "Control/Yellow_KeyPress": "Press of Beoremote One Control: Yellow", + "Control/Yellow_KeyRelease": "Release of Beoremote One Control: Yellow", + "Light/Blue_KeyPress": "Press of Beoremote One Light: Blue", + "Light/Blue_KeyRelease": "Release of Beoremote One Light: Blue", + "Light/Digit0_KeyPress": "Press of Beoremote One Light: 0", + "Light/Digit0_KeyRelease": "Release of Beoremote One Light: 0", + "Light/Digit1_KeyPress": "Press of Beoremote One Light: 1", + "Light/Digit1_KeyRelease": "Release of Beoremote One Light: 1", + "Light/Digit2_KeyPress": "Press of Beoremote One Light: 2", + "Light/Digit2_KeyRelease": "Release of Beoremote One Light: 2", + "Light/Digit3_KeyPress": "Press of Beoremote One Light: 3", + "Light/Digit3_KeyRelease": "Release of Beoremote One Light: 3", + "Light/Digit4_KeyPress": "Press of Beoremote One Light: 4", + "Light/Digit4_KeyRelease": "Release of Beoremote One Light: 4", + "Light/Digit5_KeyPress": "Press of Beoremote One Light: 5", + "Light/Digit5_KeyRelease": "Release of Beoremote One Light: 5", + "Light/Digit6_KeyPress": "Press of Beoremote One Light: 6", + "Light/Digit6_KeyRelease": "Release of Beoremote One Light: 6", + "Light/Digit7_KeyPress": "Press of Beoremote One Light: 7", + "Light/Digit7_KeyRelease": "Release of Beoremote One Light: 7", + "Light/Digit8_KeyPress": "Press of Beoremote One Light: 8", + "Light/Digit8_KeyRelease": "Release of Beoremote One Light: 8", + "Light/Digit9_KeyPress": "Press of Beoremote One Light: 9", + "Light/Digit9_KeyRelease": "Release of Beoremote One Light: 9", + "Light/Down_KeyPress": "Press of Beoremote One Light: Down", + "Light/Down_KeyRelease": "Release of Beoremote One Light: Down", + "Light/Green_KeyPress": "Press of Beoremote One Light: Green", + "Light/Green_KeyRelease": "Release of Beoremote One Light: Green", + "Light/Left_KeyPress": "Press of Beoremote One Light: Left", + "Light/Left_KeyRelease": "Release of Beoremote One Light: Left", + "Light/Play_KeyPress": "Press of Beoremote One Light: Play", + "Light/Play_KeyRelease": "Release of Beoremote One Light: Play", + "Light/Red_KeyPress": "Press of Beoremote One Light: Red", + "Light/Red_KeyRelease": "Release of Beoremote One Light: Red", + "Light/Rewind_KeyPress": "Press of Beoremote One Light Rewind", + "Light/Rewind_KeyRelease": "Release of Beoremote One Light Rewind", + "Light/Right_KeyPress": "Press of Beoremote One Light: Right", + "Light/Right_KeyRelease": "Release of Beoremote One Light: Right", + "Light/Select_KeyPress": " Press of Beoremote One Light: Select", + "Light/Select_KeyRelease": " Release of Beoremote One Light: Select", + "Light/Stop_KeyPress": "Press of Beoremote One Light: Stop", + "Light/Stop_KeyRelease": "Release of Beoremote One Light: Stop", + "Light/Up_KeyPress": "Press of Beoremote One Light: Up", + "Light/Up_KeyRelease": "Release of Beoremote One Light: Up", + "Light/Wind_KeyPress": "Press of Beoremote One Light Wind", + "Light/Wind_KeyRelease": "Release of Beoremote One Light Wind", + "Light/Yellow_KeyPress": "Press of Beoremote One Light: Yellow", + "Light/Yellow_KeyRelease": "Release of Beoremote One Light: Yellow", + "Microphone_shortPress": "Short press of Microphone", + "Next_shortPress": "Short press of Next", + "PlayPause_shortPress": "Short press of Play/Pause", + "PlayPause_longPress": "Long press of Play/Pause", + "Preset1_shortPress": "Short press of Favourite 1", + "Preset2_shortPress": "Short press of Favourite 2", + "Preset3_shortPress": "Short press of Favourite 3", + "Preset4_shortPress": "Short press of Favourite 4", + "Previous_shortPress": "Short press of Previous" + } + }, + "services": { + "beolink_join": { + "name": "Beolink join", + "description": "Join a Beolink experience.", + "fields": { + "beolink_jid": { + "name": "Beolink JID", + "description": "Manually specify Beolink JID to join." + } + } + }, + "beolink_expand": { + "name": "Beolink expand", + "description": "Expand current Beolink experience.", + "fields": { + "all_discovered": { + "name": "All discovered", + "description": "Expand Beolink experience to all discovered devices." + }, + "beolink_jids": { + "name": "Beolink JIDs", + "description": "Specify which Beolink JIDs will join current Beolink experience." + } + } + }, + "beolink_unexpand": { + "name": "Beolink unexpand", + "description": "Unexpand from current Beolink experience.", + "fields": { + "beolink_jids": { + "name": "Beolink JIDs", + "description": "Specify which Beolink JIDs will leave from current Beolink experience." + } + } + }, + "beolink_leave": { + "name": "Beolink leave", + "description": "Leave a Beolink experience." + }, + "beolink_allstandby": { + "name": "Beolink allstandby", + "description": "Set all Connected Beolink devices to standby." + }, + "beolink_set_volume": { + "name": "Beolink set volume", + "description": "Set a volume level for all connected Beolink devices.", + "fields": { + "volume_level": { + "name": "Volume level", + "description": "Specify the volume level." + } + } + }, + "beolink_set_relative_volume": { + "name": "Beolink set relative volume", + "description": "Set a volume level relative to the current level of all connected Beolink devices.", + "fields": { + "volume_level": { + "name": "Volume level", + "description": "Specify the volume level." + } + } + }, + "beolink_leader_command": { + "name": "Beolink leader command", + "description": "Send a media_player command to Beolink leader.", + "fields": { + "command": { + "name": "Command", + "description": "Specify the media_player command." + }, + "parameter": { + "name": "Parameter", + "description": "Specify the media_player command's parameter." + } + } + }, + "overlay_audio": { + "name": "Overlay audio", + "description": "Overlay audio over any currently playing audio. TTS is generated by Bang & Olufsen and is limited to 100 unique messages a day. Generated TTS messages are cached for 24 hours.", + "fields": { + "uri": { + "name": "URI", + "description": "Specify the audio to play." + }, + "absolute_volume": { + "name": "Absolute volume", + "description": "Specify an absolute volume for the overlay." + }, + "volume_offset": { + "name": "Volume offset", + "description": "Specify a volume offset to be added to the current volume level." + }, + "tts": { + "name": "Text to speech", + "description": "Specify a string to be converted to a TTS message." + }, + "tts_language": { + "name": "Text to speech language", + "description": "Specify TTS language." + } + } + } + }, + "entity": { + "binary_sensor": { + "battery_charging": { + "name": "Battery charging" + }, + "proximity": { + "name": "Proximity" + } + }, + "button": { + "favourite": { + "name": "Favourite {id}" + } + }, + "number": { + "treble": { "name": "Treble" }, + "bass": { "name": "Bass" } + }, + "select": { + "sound_mode": { "name": "Sound mode" }, + "listening_position": { "name": "Listening position" } + }, + "sensor": { + "battery_level": { "name": "Battery level" }, + "battery_charging_time": { "name": "Battery charging time" }, + "battery_playing_time": { "name": "Battery playing time" }, + "media_id": { "name": "Media id" }, + "input_signal": { "name": "Input signal" } + }, + "switch": { + "loudness": { + "name": "Loudness" + } + }, + "text": { + "friendly_name": { "name": "Device friendly name" }, + "home_control_uri": { "name": "Home Control URI" } + } + } } diff --git a/custom_components/bang_olufsen/translations/en.json b/custom_components/bang_olufsen/translations/en.json index c57047a..baa6fe1 100644 --- a/custom_components/bang_olufsen/translations/en.json +++ b/custom_components/bang_olufsen/translations/en.json @@ -201,6 +201,10 @@ "beolink_expand": { "description": "Expand current Beolink experience.", "fields": { + "all_discovered": { + "description": "Expand Beolink experience to all discovered devices.", + "name": "All discovered" + }, "beolink_jids": { "description": "Specify which Beolink JIDs will join current Beolink experience.", "name": "Beolink JIDs" diff --git a/hacs.json b/hacs.json index 4679424..1a7a3a9 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { "name": "Bang & Olufsen", - "homeassistant": "2024.2.0", + "homeassistant": "2024.4.0", "render_readme": true }