diff --git a/README.md b/README.md index 318d869..8f64a33 100644 --- a/README.md +++ b/README.md @@ -161,44 +161,6 @@ The favourite buttons correspond to the physical favourite buttons on the device ### Automation examples -#### Using the overlay as doorbell - -```yaml -description: Play doorbell sound overlay on doorbell press. -mode: single -trigger: - - platform: device - device_id: 1234567890abcdef1234567890abcdef - domain: example - type: doorbell -condition: [] -action: - - service: bang_olufsen.overlay_audio - data: - uri: media-source://media_source/local/doorbell.mp3 - absolute_volume: 60 - target: - entity_id: media_player.beosound_balance_12345678 -``` - -#### Using the overlay TTS as a bedtime reminder - -```yaml -description: "Daily bedtime reminder using overlay TTS." -mode: single -trigger: - - platform: time - at: "22:00:00" -condition: [] -action: - - service: bang_olufsen.overlay_audio - data: - absolute_volume: 70 - tts: It is 22:00. Time to go to bed! - target: - entity_id: media_player.beosound_balance_12345678 -``` - #### Using the Beoremote One to control lights ```yaml @@ -238,7 +200,7 @@ action: ### play_media services -The Bang & Olufsen integration supports different playback types in the `media_player.play_media` service: playback from URL, activating a favourite, playback from a local file, playing a radio station, activating a Deezer flow and Deezer playlists, albums and tracks. +The Bang & Olufsen integration supports different playback types in the `media_player.play_media` service: playback from URL, activating a favourite, playback from a local file, playing a radio station, activating a Deezer flow and Deezer/Tidal playlists, albums and tracks. Additionally `announce` can be set to `True` to play TTS or files as an overlay. #### play_media examples @@ -373,6 +335,50 @@ data: media_content_type: radio ``` +Playing a doorbell file with an absolute volume + +```yaml +service: media_player.play_media +target: + entity_id: media_player.beosound_balance_12345678 +data: + media_content_type: music + media_content_id: media-source://media_source/local/doorbell.mp3 + announce: true + extra: + overlay_absolute_volume: 60 +``` + +Playing an overlay TTS with an offset volume + +TTS messages can be quiet, so an offset is useful in this scenario. + +```yaml +service: media_player.play_media +target: + entity_id: media_player.beosound_balance_12345678 +data: + media_content_type: overlay_tts + media_content_id: This is a test + announce: true + extra: + overlay_offset_volume: 10 +``` + +Playing a Bang & Olufsen Cloud TTS message with a local language + +```yaml +service: media_player.play_media +target: + entity_id: media_player.beosound_balance_12345678 +data: + media_content_type: overlay_tts + media_content_id: Dette er en test + announce: true + extra: + overlay_tts_language: da-dk +``` + _NOTE_: To easily obtain the media_content_id for a Deezer/Tidal track or B&O Radio station, you can enable the 'Media id' sensor on Mozart device in Home Assistant (disabled by default). Once enabled, start playing the content you wish to activate in a service call - the Media id sensor will then provide the value to be used in the media_content_id field. diff --git a/custom_components/bang_olufsen/__init__.py b/custom_components/bang_olufsen/__init__.py index ef8da97..a61da9e 100644 --- a/custom_components/bang_olufsen/__init__.py +++ b/custom_components/bang_olufsen/__init__.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_MODEL, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -24,7 +24,6 @@ class BangOlufsenData: coordinator: DataUpdateCoordinator client: MozartClient - entities_initialized: int = 0 platforms_initialized: int = 0 @@ -40,29 +39,18 @@ class BangOlufsenData: ] -async def _start_websocket_listener( - hass: HomeAssistant, entry: ConfigEntry, data: BangOlufsenData -) -> None: - """Start WebSocket listener when all entities have been initialized.""" - entity_registry = er.async_get(hass) +async def _start_websocket_listener(data: BangOlufsenData) -> None: + """Start WebSocket listener when all platforms have been initialized.""" while True: - # Get all entries for entities and filter all disabled entities - entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - expected_entities = len( - [entry for entry in entries if not entry.disabled or not entry.disabled_by] - ) - - # Check if all entities and platforms have been initialized and start WebSocket listener - if ( - expected_entities == data.entities_initialized - and len(PLATFORMS) == data.platforms_initialized - ): - await data.client.connect_notifications(remote_control=True, reconnect=True) - return + # Check if all platforms have been initialized and start WebSocket listener + if len(PLATFORMS) == data.platforms_initialized: + break await asyncio.sleep(0) + await data.client.connect_notifications(remote_control=True, reconnect=True) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" @@ -102,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Start WebSocket connection when all entities have been initialized entry.async_create_background_task( hass, - _start_websocket_listener(hass, entry, hass.data[DOMAIN][entry.entry_id]), + _start_websocket_listener(hass.data[DOMAIN][entry.entry_id]), f"{DOMAIN}-{entry.unique_id}-websocket_starter", ) diff --git a/custom_components/bang_olufsen/binary_sensor.py b/custom_components/bang_olufsen/binary_sensor.py index 1f05043..bd9bd22 100644 --- a/custom_components/bang_olufsen/binary_sensor.py +++ b/custom_components/bang_olufsen/binary_sensor.py @@ -93,8 +93,6 @@ async def async_added_to_hass(self) -> None: ) ) - self.set_entity_initialized() - async def _update_battery_charging(self, data: BatteryState) -> None: """Update battery charging.""" self._attr_is_on = data.is_charging @@ -130,8 +128,6 @@ async def async_added_to_hass(self) -> None: ) ) - self.set_entity_initialized() - async def _update_proximity(self, data: WebsocketNotificationTag) -> None: """Update proximity.""" if data.value: diff --git a/custom_components/bang_olufsen/button.py b/custom_components/bang_olufsen/button.py index 4a5e2ed..ec5bf0a 100644 --- a/custom_components/bang_olufsen/button.py +++ b/custom_components/bang_olufsen/button.py @@ -91,8 +91,6 @@ async def async_added_to_hass(self) -> None: self._attr_extra_state_attributes = self._generate_favourite_attributes() - self.set_entity_initialized() - async def async_press(self) -> None: """Handle the action.""" await self._client.activate_preset(id=self._favourite_id) diff --git a/custom_components/bang_olufsen/entity.py b/custom_components/bang_olufsen/entity.py index b384dde..fbe2dcf 100644 --- a/custom_components/bang_olufsen/entity.py +++ b/custom_components/bang_olufsen/entity.py @@ -91,7 +91,3 @@ def _async_update_connection_state(self, connection_state: bool) -> None: self._attr_available = connection_state self.async_write_ha_state() - - def set_entity_initialized(self) -> None: - """Increment number of initialized entities.""" - self.hass.data[DOMAIN][self.entry.entry_id].entities_initialized += 1 diff --git a/custom_components/bang_olufsen/icons.json b/custom_components/bang_olufsen/icons.json index d965cd5..50d7d18 100644 --- a/custom_components/bang_olufsen/icons.json +++ b/custom_components/bang_olufsen/icons.json @@ -1,12 +1,12 @@ { - "services": { - "beolink_join": "mdi:location-enter", - "beolink_expand": "mdi:location-enter", - "beolink_unexpand": "mdi:location-exit", - "beolink_leave": "mdi:close-circle-outline", - "beolink_allstandby": "mdi:close-circle-multiple-outline", - "beolink_set_volume": "mdi:volume-equal", - "beolink_set_relative_volume": "mdi:volume-plus", - "beolink_leader_command": "mdi:location-enter" - } + "services": { + "beolink_join": "mdi:location-enter", + "beolink_expand": "mdi:location-enter", + "beolink_unexpand": "mdi:location-exit", + "beolink_leave": "mdi:close-circle-outline", + "beolink_allstandby": "mdi:close-circle-multiple-outline", + "beolink_set_volume": "mdi:volume-equal", + "beolink_set_relative_volume": "mdi:volume-plus", + "beolink_leader_command": "mdi:location-enter" + } } diff --git a/custom_components/bang_olufsen/manifest.json b/custom_components/bang_olufsen/manifest.json index 23ca585..fcbeba7 100644 --- a/custom_components/bang_olufsen/manifest.json +++ b/custom_components/bang_olufsen/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://github.com/bang-olufsen/bang_olufsen-hacs", "iot_class": "local_push", "issue_tracker": "https://github.com/bang-olufsen/bang_olufsen-hacs/issues", - "requirements": ["mozart-api==3.4.1.8.5"], - "version": "2.3.0", + "requirements": ["mozart-api==3.4.1.8.6"], + "version": "2.3.1", "zeroconf": ["_bangolufsen._tcp.local."] } diff --git a/custom_components/bang_olufsen/media_player.py b/custom_components/bang_olufsen/media_player.py index 0f4eb59..5a7dcb9 100644 --- a/custom_components/bang_olufsen/media_player.py +++ b/custom_components/bang_olufsen/media_player.py @@ -59,6 +59,7 @@ SupportsResponse, callback, ) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -391,8 +392,6 @@ async def async_added_to_hass(self) -> None: ) ) - self.set_entity_initialized() - async def _initialize(self) -> None: """Initialize connection dependent variables.""" @@ -1104,9 +1103,11 @@ async def async_play_media( media_id = async_process_play_media_url(self.hass, sourced_media.url) - # Remove playlist extension as it is unsupported. + # Exit if the source uses unsupported file. if media_id.endswith(".m3u"): - media_id = media_id.replace(".m3u", "") + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="m3u_invalid_format" + ) if announce: extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) diff --git a/custom_components/bang_olufsen/number.py b/custom_components/bang_olufsen/number.py index 0734a9e..3acb611 100644 --- a/custom_components/bang_olufsen/number.py +++ b/custom_components/bang_olufsen/number.py @@ -86,8 +86,6 @@ async def async_added_to_hass(self) -> None: ) ) - self.set_entity_initialized() - async def _update_sound_settings(self, data: SoundSettings) -> None: """Update sound settings.""" if data.adjustments and data.adjustments.treble: @@ -133,8 +131,6 @@ async def async_added_to_hass(self) -> None: ) ) - self.set_entity_initialized() - async def _update_sound_settings(self, data: SoundSettings) -> None: """Update sound settings.""" if data.adjustments and data.adjustments.bass: diff --git a/custom_components/bang_olufsen/select.py b/custom_components/bang_olufsen/select.py index 8b318a5..9250ec6 100644 --- a/custom_components/bang_olufsen/select.py +++ b/custom_components/bang_olufsen/select.py @@ -101,8 +101,6 @@ async def async_added_to_hass(self) -> None: await self._update_listening_positions() - self.set_entity_initialized() - async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self._client.post_scene_trigger(id=self._listening_positions[option]) diff --git a/custom_components/bang_olufsen/sensor.py b/custom_components/bang_olufsen/sensor.py index 28b8969..f86e84f 100644 --- a/custom_components/bang_olufsen/sensor.py +++ b/custom_components/bang_olufsen/sensor.py @@ -94,8 +94,6 @@ async def async_added_to_hass(self) -> None: ) ) - self.set_entity_initialized() - async def _update_battery(self, data: BatteryState) -> None: """Update sensor value.""" self._attr_native_value = data.battery_level @@ -134,8 +132,6 @@ async def async_added_to_hass(self) -> None: ) ) - self.set_entity_initialized() - async def _update_battery(self, data: BatteryState) -> None: """Update sensor value.""" @@ -184,8 +180,6 @@ async def async_added_to_hass(self) -> None: ) ) - self.set_entity_initialized() - async def _update_battery(self, data: BatteryState) -> None: """Update sensor value.""" self._attr_available = True @@ -235,8 +229,6 @@ async def async_added_to_hass(self) -> None: ) ) - self.set_entity_initialized() - async def _update_playback_metadata(self, data: PlaybackContentMetadata) -> None: """Update Sensor value.""" self._attr_native_value = data.source_internal_id @@ -275,8 +267,6 @@ async def async_added_to_hass(self) -> None: ) ) - self.set_entity_initialized() - async def _update_playback_metadata(self, data: PlaybackContentMetadata) -> None: """Update Sensor value.""" if data.encoding: diff --git a/custom_components/bang_olufsen/strings.json b/custom_components/bang_olufsen/strings.json index b5b59a7..69dddaa 100644 --- a/custom_components/bang_olufsen/strings.json +++ b/custom_components/bang_olufsen/strings.json @@ -247,5 +247,10 @@ "friendly_name": { "name": "Device friendly name" }, "home_control_uri": { "name": "Home Control URI" } } + }, + "exceptions": { + "m3u_invalid_format": { + "message": "Media sources with the .m3u extension are not supported." + } } } diff --git a/custom_components/bang_olufsen/switch.py b/custom_components/bang_olufsen/switch.py index 5cb6a14..a08cc1d 100644 --- a/custom_components/bang_olufsen/switch.py +++ b/custom_components/bang_olufsen/switch.py @@ -88,8 +88,6 @@ async def async_added_to_hass(self) -> None: ) ) - self.set_entity_initialized() - async def _update_sound_settings(self, data: SoundSettings) -> None: """Update sound settings.""" if data.adjustments and data.adjustments.loudness is not None: diff --git a/custom_components/bang_olufsen/text.py b/custom_components/bang_olufsen/text.py index e3b6ccc..cf932a3 100644 --- a/custom_components/bang_olufsen/text.py +++ b/custom_components/bang_olufsen/text.py @@ -85,8 +85,6 @@ async def async_added_to_hass(self) -> None: beolink_self = await self._client.get_beolink_self() self._attr_native_value = beolink_self.friendly_name - self.set_entity_initialized() - async def async_set_value(self, value: str) -> None: """Set the friendly name.""" self._attr_native_value = value @@ -129,8 +127,6 @@ async def async_added_to_hass(self) -> None: home_control = await self._client.get_remote_home_control_uri() self._attr_native_value = home_control.uri - self.set_entity_initialized() - async def async_set_value(self, value: str) -> None: """Set the Home Control URI name.""" self._attr_native_value = value diff --git a/custom_components/bang_olufsen/translations/en.json b/custom_components/bang_olufsen/translations/en.json index caa297b..f8c09d7 100644 --- a/custom_components/bang_olufsen/translations/en.json +++ b/custom_components/bang_olufsen/translations/en.json @@ -190,6 +190,11 @@ } } }, + "exceptions": { + "m3u_invalid_format": { + "message": "Media sources with the .m3u extension are not supported." + } + }, "services": { "beolink_allstandby": { "description": "Set all Connected Beolink devices to standby.",