1 """Support for interface with a Bose SoundTouch."""
3 from __future__
import annotations
5 from functools
import partial
9 from libsoundtouch.device
import SoundTouchDevice
10 from libsoundtouch.utils
import Source
15 MediaPlayerDeviceClass,
17 MediaPlayerEntityFeature,
20 async_process_play_media_url,
26 CONNECTION_NETWORK_MAC,
32 from .const
import DOMAIN
34 _LOGGER = logging.getLogger(__name__)
37 "PLAY_STATE": MediaPlayerState.PLAYING,
38 "BUFFERING_STATE": MediaPlayerState.PLAYING,
39 "PAUSE_STATE": MediaPlayerState.PAUSED,
40 "STOP_STATE": MediaPlayerState.OFF,
43 ATTR_SOUNDTOUCH_GROUP =
"soundtouch_group"
44 ATTR_SOUNDTOUCH_ZONE =
"soundtouch_zone"
50 async_add_entities: AddEntitiesCallback,
52 """Set up the Bose SoundTouch media player based on a config entry."""
53 device = hass.data[DOMAIN][entry.entry_id].device
58 hass.data[DOMAIN][entry.entry_id].media_player = media_player
62 """Representation of a SoundTouch Bose device."""
64 _attr_supported_features = (
65 MediaPlayerEntityFeature.PAUSE
66 | MediaPlayerEntityFeature.VOLUME_STEP
67 | MediaPlayerEntityFeature.VOLUME_MUTE
68 | MediaPlayerEntityFeature.PREVIOUS_TRACK
69 | MediaPlayerEntityFeature.NEXT_TRACK
70 | MediaPlayerEntityFeature.TURN_OFF
71 | MediaPlayerEntityFeature.VOLUME_SET
72 | MediaPlayerEntityFeature.TURN_ON
73 | MediaPlayerEntityFeature.PLAY
74 | MediaPlayerEntityFeature.PLAY_MEDIA
75 | MediaPlayerEntityFeature.SELECT_SOURCE
76 | MediaPlayerEntityFeature.BROWSE_MEDIA
78 _attr_device_class = MediaPlayerDeviceClass.SPEAKER
79 _attr_has_entity_name =
True
83 Source.BLUETOOTH.value,
86 def __init__(self, device: SoundTouchDevice) ->
None:
87 """Create SoundTouch media player entity."""
93 identifiers={(DOMAIN, device.config.device_id)},
95 (CONNECTION_NETWORK_MAC,
format_mac(device.config.mac_address))
97 manufacturer=
"Bose Corporation",
98 model=device.config.type,
99 name=device.config.name,
108 """Return SoundTouch device."""
112 """Retrieve the latest data."""
119 """Volume level of the media player (0..1)."""
120 return self.
_volume_volume.actual / 100
123 def state(self) -> MediaPlayerState | None:
124 """Return the state of the device."""
125 if self.
_status_status
is None or self.
_status_status.source ==
"STANDBY":
126 return MediaPlayerState.OFF
128 if self.
_status_status.source ==
"INVALID_SOURCE":
131 return MAP_STATUS.get(self.
_status_status.play_status)
135 """Name of the current input source."""
136 return self.
_status_status.source
140 """Boolean if volume is currently muted."""
141 return self.
_volume_volume.muted
144 """Turn off media player."""
145 self.
_device_device.power_off()
148 """Turn on media player."""
152 """Volume up the media player."""
156 """Volume down media player."""
160 """Set volume level, range 0..1."""
161 self.
_device_device.set_volume(
int(volume * 100))
164 """Send mute command."""
168 """Simulate play pause media player."""
169 self.
_device_device.play_pause()
172 """Send play command."""
176 """Send media pause command to media player."""
180 """Send next track command."""
181 self.
_device_device.next_track()
184 """Send the previous track command."""
185 self.
_device_device.previous_track()
189 """Image url of current playing media."""
190 return self.
_status_status.image
194 """Title of current playing media."""
195 if self.
_status_status.station_name
is not None:
196 return self.
_status_status.station_name
197 if self.
_status_status.artist
is not None:
198 return f
"{self._status.artist} - {self._status.track}"
204 """Duration of current playing media in seconds."""
205 return self.
_status_status.duration
209 """Artist of current playing media."""
210 return self.
_status_status.artist
214 """Artist of current playing media."""
215 return self.
_status_status.track
219 """Album name of current playing media."""
220 return self.
_status_status.album
223 """Populate zone info which requires entity_id."""
226 def async_update_on_start(event):
227 """Schedule an update when all platform entities have been added."""
230 self.
hasshass.bus.async_listen_once(
231 EVENT_HOMEASSISTANT_START, async_update_on_start
235 self, media_type: MediaType | str, media_id: str, **kwargs: Any
237 """Play a piece of media."""
238 if media_source.is_media_source_id(media_id):
239 play_item = await media_source.async_resolve_media(
244 await self.
hasshass.async_add_executor_job(
249 self, media_type: MediaType | str, media_id: str, **kwargs: Any
251 """Play a piece of media."""
252 _LOGGER.debug(
"Starting media with media_id: %s", media_id)
253 if str(media_id).lower().startswith(
"http://"):
255 _LOGGER.debug(
"Playing URL %s",
str(media_id))
259 presets = self.
_device_device.presets()
262 [preset
for preset
in presets
if preset.preset_id ==
str(media_id)]
266 if preset
is not None:
267 _LOGGER.debug(
"Playing preset: %s", preset.name)
268 self.
_device_device.select_preset(preset)
270 _LOGGER.warning(
"Unable to find preset with id %s", media_id)
273 """Select input source."""
274 if source == Source.AUX.value:
275 _LOGGER.debug(
"Selecting source AUX")
276 self.
_device_device.select_source_aux()
277 elif source == Source.BLUETOOTH.value:
278 _LOGGER.debug(
"Selecting source Bluetooth")
279 self.
_device_device.select_source_bluetooth()
281 _LOGGER.warning(
"Source %s is not supported", source)
284 """Create a zone (multi-room) and play on selected devices.
286 :param slaves: slaves on which to play
290 _LOGGER.warning(
"Unable to create zone without slaves")
292 _LOGGER.debug(
"Creating zone with master %s", self.
_device_device.config.name)
296 """Remove slave(s) from and existing zone (multi-room).
298 Zone must already exist and slaves array cannot be empty.
299 Note: If removing last slave, the zone will be deleted and you'll have
300 to create a new one. You will not be able to add a new slave anymore
302 :param slaves: slaves to remove from the zone
306 _LOGGER.warning(
"Unable to find slaves to remove")
309 "Removing slaves from zone with master %s", self.
_device_device.config.name
316 if slave.entity_id != self.
entity_identity_id:
320 """Add slave(s) to and existing zone (multi-room).
322 Zone must already exist and slaves array cannot be empty.
324 :param slaves:slaves to add
328 _LOGGER.warning(
"Unable to find slaves to add")
331 "Adding slaves to zone with master %s", self.
_device_device.config.name
337 """Return entity specific state attributes."""
340 if self.
_zone_zone
and "master" in self.
_zone_zone:
341 attributes[ATTR_SOUNDTOUCH_ZONE] = self.
_zone_zone
344 group_members = [self.
_zone_zone[
"master"]] + self.
_zone_zone[
"slaves"]
345 attributes[ATTR_SOUNDTOUCH_GROUP] = group_members
351 media_content_type: MediaType | str |
None =
None,
352 media_content_id: str |
None =
None,
354 """Implement the websocket media browsing helper."""
355 return await media_source.async_browse_media(self.
hasshass, media_content_id)
358 """Return the current zone info."""
359 zone_status = self.
_device_device.zone_status()
371 if zone_status.master_id == self.
_device_device.config.device_id:
378 if master_instance
is not None:
379 master_zone_status = master_instance.device.zone_status()
381 master_instance.entity_id, master_zone_status.slaves
390 """Search and return a SoundTouchDevice instance by it's IP address."""
391 for data
in self.
hasshass.data[DOMAIN].values():
392 if data.device.config.device_ip == ip_address:
393 return data.media_player
397 """Search and return a SoundTouchDevice instance by it's ID (aka MAC address)."""
398 for data
in self.
hasshass.data[DOMAIN].values():
399 if data.device.config.device_id == instance_id:
400 return data.media_player
404 """Build the exposed zone attributes."""
407 for slave
in zone_slaves:
409 if slave_instance
and slave_instance.entity_id != master:
410 slaves.append(slave_instance.entity_id)
414 "is_master": master == self.
entity_identity_id,
None async_schedule_update_ha_state(self, bool force_refresh=False)