2019-07-08 15:14:19 +00:00
|
|
|
"""Arcam media player."""
|
|
|
|
import logging
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes
|
2019-07-08 15:14:19 +00:00
|
|
|
from arcam.fmj.state import State
|
|
|
|
|
|
|
|
from homeassistant import config_entries
|
2020-09-06 13:52:59 +00:00
|
|
|
from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity
|
2019-07-08 15:14:19 +00:00
|
|
|
from homeassistant.components.media_player.const import (
|
2020-09-08 14:42:01 +00:00
|
|
|
MEDIA_CLASS_DIRECTORY,
|
|
|
|
MEDIA_CLASS_MUSIC,
|
2019-07-08 15:14:19 +00:00
|
|
|
MEDIA_TYPE_MUSIC,
|
2020-09-05 22:10:18 +00:00
|
|
|
SUPPORT_BROWSE_MEDIA,
|
|
|
|
SUPPORT_PLAY_MEDIA,
|
2019-07-08 15:14:19 +00:00
|
|
|
SUPPORT_SELECT_SOUND_MODE,
|
|
|
|
SUPPORT_SELECT_SOURCE,
|
|
|
|
SUPPORT_TURN_OFF,
|
2019-12-09 12:57:24 +00:00
|
|
|
SUPPORT_TURN_ON,
|
2019-07-08 15:14:19 +00:00
|
|
|
SUPPORT_VOLUME_MUTE,
|
|
|
|
SUPPORT_VOLUME_SET,
|
|
|
|
SUPPORT_VOLUME_STEP,
|
|
|
|
)
|
2020-09-05 22:10:18 +00:00
|
|
|
from homeassistant.components.media_player.errors import BrowseError
|
2020-06-06 20:43:28 +00:00
|
|
|
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
|
2021-04-17 10:48:03 +00:00
|
|
|
from homeassistant.core import HomeAssistant, callback
|
2019-07-08 15:14:19 +00:00
|
|
|
|
2020-06-06 20:43:28 +00:00
|
|
|
from .config_flow import get_entry_client
|
2019-07-08 15:14:19 +00:00
|
|
|
from .const import (
|
2019-12-09 12:57:24 +00:00
|
|
|
DOMAIN,
|
2020-05-14 21:24:19 +00:00
|
|
|
EVENT_TURN_ON,
|
2019-07-08 15:14:19 +00:00
|
|
|
SIGNAL_CLIENT_DATA,
|
|
|
|
SIGNAL_CLIENT_STARTED,
|
|
|
|
SIGNAL_CLIENT_STOPPED,
|
|
|
|
)
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_entry(
|
2021-04-17 10:48:03 +00:00
|
|
|
hass: HomeAssistant,
|
2019-07-31 19:25:30 +00:00
|
|
|
config_entry: config_entries.ConfigEntry,
|
|
|
|
async_add_entities,
|
2019-07-08 15:14:19 +00:00
|
|
|
):
|
|
|
|
"""Set up the configuration entry."""
|
2020-06-06 20:43:28 +00:00
|
|
|
|
|
|
|
client = get_entry_client(hass, config_entry)
|
2019-07-08 15:14:19 +00:00
|
|
|
|
|
|
|
async_add_entities(
|
|
|
|
[
|
|
|
|
ArcamFmj(
|
2020-06-06 20:43:28 +00:00
|
|
|
config_entry.title,
|
2019-07-08 15:14:19 +00:00
|
|
|
State(client, zone),
|
2020-05-14 21:24:19 +00:00
|
|
|
config_entry.unique_id or config_entry.entry_id,
|
2019-07-08 15:14:19 +00:00
|
|
|
)
|
2021-07-19 13:57:06 +00:00
|
|
|
for zone in (1, 2)
|
2020-04-27 19:57:57 +00:00
|
|
|
],
|
|
|
|
True,
|
2019-07-08 15:14:19 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2020-04-25 16:00:57 +00:00
|
|
|
class ArcamFmj(MediaPlayerEntity):
|
2019-07-08 15:14:19 +00:00
|
|
|
"""Representation of a media device."""
|
|
|
|
|
2021-07-15 08:19:18 +00:00
|
|
|
_attr_should_poll = False
|
|
|
|
|
2020-05-14 21:24:19 +00:00
|
|
|
def __init__(
|
2020-08-27 11:56:20 +00:00
|
|
|
self,
|
|
|
|
device_name,
|
|
|
|
state: State,
|
|
|
|
uuid: str,
|
2020-05-14 21:24:19 +00:00
|
|
|
):
|
2019-07-08 15:14:19 +00:00
|
|
|
"""Initialize device."""
|
|
|
|
self._state = state
|
2020-06-06 20:43:28 +00:00
|
|
|
self._device_name = device_name
|
2021-07-15 08:19:18 +00:00
|
|
|
self._attr_name = f"{device_name} - Zone: {state.zn}"
|
2020-05-14 21:24:19 +00:00
|
|
|
self._uuid = uuid
|
2021-07-15 08:19:18 +00:00
|
|
|
self._attr_supported_features = (
|
2019-07-08 15:14:19 +00:00
|
|
|
SUPPORT_SELECT_SOURCE
|
2020-09-05 22:10:18 +00:00
|
|
|
| SUPPORT_PLAY_MEDIA
|
|
|
|
| SUPPORT_BROWSE_MEDIA
|
2019-07-08 15:14:19 +00:00
|
|
|
| SUPPORT_VOLUME_SET
|
|
|
|
| SUPPORT_VOLUME_MUTE
|
|
|
|
| SUPPORT_VOLUME_STEP
|
|
|
|
| SUPPORT_TURN_OFF
|
2020-05-14 21:24:19 +00:00
|
|
|
| SUPPORT_TURN_ON
|
2019-07-08 15:14:19 +00:00
|
|
|
)
|
|
|
|
if state.zn == 1:
|
2021-07-15 08:19:18 +00:00
|
|
|
self._attr_supported_features |= SUPPORT_SELECT_SOUND_MODE
|
|
|
|
self._attr_unique_id = f"{uuid}-{state.zn}"
|
|
|
|
self._attr_entity_registry_enabled_default = state.zn == 1
|
2019-07-08 15:14:19 +00:00
|
|
|
|
|
|
|
def _get_2ch(self):
|
|
|
|
"""Return if source is 2 channel or not."""
|
|
|
|
audio_format, _ = self._state.get_incoming_audio_format()
|
|
|
|
return bool(
|
|
|
|
audio_format
|
2020-04-27 19:57:57 +00:00
|
|
|
in (
|
|
|
|
IncomingAudioFormat.PCM,
|
|
|
|
IncomingAudioFormat.ANALOGUE_DIRECT,
|
|
|
|
IncomingAudioFormat.UNDETECTED,
|
|
|
|
None,
|
|
|
|
)
|
2019-07-08 15:14:19 +00:00
|
|
|
)
|
|
|
|
|
2020-06-06 20:43:28 +00:00
|
|
|
@property
|
2021-07-15 08:19:18 +00:00
|
|
|
def state(self):
|
|
|
|
"""Return the state of the device."""
|
|
|
|
if self._state.get_power():
|
|
|
|
return STATE_ON
|
|
|
|
return STATE_OFF
|
2020-05-14 21:24:19 +00:00
|
|
|
|
2019-07-08 15:14:19 +00:00
|
|
|
@property
|
|
|
|
def device_info(self):
|
|
|
|
"""Return a device description for device registry."""
|
|
|
|
return {
|
2020-06-06 20:43:28 +00:00
|
|
|
"name": self._device_name,
|
|
|
|
"identifiers": {
|
|
|
|
(DOMAIN, self._uuid),
|
|
|
|
(DOMAIN, self._state.client.host, self._state.client.port),
|
|
|
|
},
|
|
|
|
"model": "Arcam FMJ AVR",
|
2019-07-08 15:14:19 +00:00
|
|
|
"manufacturer": "Arcam",
|
|
|
|
}
|
|
|
|
|
|
|
|
async def async_added_to_hass(self):
|
2019-08-02 21:20:07 +00:00
|
|
|
"""Once registered, add listener for events."""
|
2019-07-08 15:14:19 +00:00
|
|
|
await self._state.start()
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _data(host):
|
|
|
|
if host == self._state.client.host:
|
2020-04-01 21:19:51 +00:00
|
|
|
self.async_write_ha_state()
|
2019-07-08 15:14:19 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def _started(host):
|
|
|
|
if host == self._state.client.host:
|
|
|
|
self.async_schedule_update_ha_state(force_refresh=True)
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _stopped(host):
|
|
|
|
if host == self._state.client.host:
|
|
|
|
self.async_schedule_update_ha_state(force_refresh=True)
|
|
|
|
|
2020-04-02 16:25:33 +00:00
|
|
|
self.async_on_remove(
|
|
|
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
|
|
|
SIGNAL_CLIENT_DATA, _data
|
|
|
|
)
|
|
|
|
)
|
2019-07-08 15:14:19 +00:00
|
|
|
|
2020-04-02 16:25:33 +00:00
|
|
|
self.async_on_remove(
|
|
|
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
|
|
|
SIGNAL_CLIENT_STARTED, _started
|
|
|
|
)
|
2019-07-08 15:14:19 +00:00
|
|
|
)
|
|
|
|
|
2020-04-02 16:25:33 +00:00
|
|
|
self.async_on_remove(
|
|
|
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
|
|
|
SIGNAL_CLIENT_STOPPED, _stopped
|
|
|
|
)
|
2019-07-08 15:14:19 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
async def async_update(self):
|
|
|
|
"""Force update of state."""
|
|
|
|
_LOGGER.debug("Update state %s", self.name)
|
|
|
|
await self._state.update()
|
|
|
|
|
|
|
|
async def async_mute_volume(self, mute):
|
|
|
|
"""Send mute command."""
|
|
|
|
await self._state.set_mute(mute)
|
2020-04-01 21:19:51 +00:00
|
|
|
self.async_write_ha_state()
|
2019-07-08 15:14:19 +00:00
|
|
|
|
|
|
|
async def async_select_source(self, source):
|
|
|
|
"""Select a specific source."""
|
|
|
|
try:
|
|
|
|
value = SourceCodes[source]
|
|
|
|
except KeyError:
|
|
|
|
_LOGGER.error("Unsupported source %s", source)
|
|
|
|
return
|
|
|
|
|
|
|
|
await self._state.set_source(value)
|
2020-04-01 21:19:51 +00:00
|
|
|
self.async_write_ha_state()
|
2019-07-08 15:14:19 +00:00
|
|
|
|
|
|
|
async def async_select_sound_mode(self, sound_mode):
|
|
|
|
"""Select a specific source."""
|
|
|
|
try:
|
|
|
|
if self._get_2ch():
|
2019-07-31 19:25:30 +00:00
|
|
|
await self._state.set_decode_mode_2ch(DecodeMode2CH[sound_mode])
|
2019-07-08 15:14:19 +00:00
|
|
|
else:
|
2019-07-31 19:25:30 +00:00
|
|
|
await self._state.set_decode_mode_mch(DecodeModeMCH[sound_mode])
|
2019-07-08 15:14:19 +00:00
|
|
|
except KeyError:
|
|
|
|
_LOGGER.error("Unsupported sound_mode %s", sound_mode)
|
|
|
|
return
|
|
|
|
|
2020-04-01 21:19:51 +00:00
|
|
|
self.async_write_ha_state()
|
2019-07-08 15:14:19 +00:00
|
|
|
|
|
|
|
async def async_set_volume_level(self, volume):
|
|
|
|
"""Set volume level, range 0..1."""
|
|
|
|
await self._state.set_volume(round(volume * 99.0))
|
2020-04-01 21:19:51 +00:00
|
|
|
self.async_write_ha_state()
|
2019-07-08 15:14:19 +00:00
|
|
|
|
|
|
|
async def async_volume_up(self):
|
|
|
|
"""Turn volume up for media player."""
|
|
|
|
await self._state.inc_volume()
|
2020-04-01 21:19:51 +00:00
|
|
|
self.async_write_ha_state()
|
2019-07-08 15:14:19 +00:00
|
|
|
|
|
|
|
async def async_volume_down(self):
|
|
|
|
"""Turn volume up for media player."""
|
|
|
|
await self._state.dec_volume()
|
2020-04-01 21:19:51 +00:00
|
|
|
self.async_write_ha_state()
|
2019-07-08 15:14:19 +00:00
|
|
|
|
|
|
|
async def async_turn_on(self):
|
|
|
|
"""Turn the media player on."""
|
|
|
|
if self._state.get_power() is not None:
|
|
|
|
_LOGGER.debug("Turning on device using connection")
|
|
|
|
await self._state.set_power(True)
|
|
|
|
else:
|
2020-05-14 21:24:19 +00:00
|
|
|
_LOGGER.debug("Firing event to turn on device")
|
|
|
|
self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id})
|
2019-07-08 15:14:19 +00:00
|
|
|
|
|
|
|
async def async_turn_off(self):
|
|
|
|
"""Turn the media player off."""
|
|
|
|
await self._state.set_power(False)
|
|
|
|
|
2020-09-05 22:10:18 +00:00
|
|
|
async def async_browse_media(self, media_content_type=None, media_content_id=None):
|
|
|
|
"""Implement the websocket media browsing helper."""
|
|
|
|
if media_content_id not in (None, "root"):
|
|
|
|
raise BrowseError(
|
|
|
|
f"Media not found: {media_content_type} / {media_content_id}"
|
|
|
|
)
|
|
|
|
|
|
|
|
presets = self._state.get_preset_details()
|
|
|
|
|
|
|
|
radio = [
|
2020-09-06 13:52:59 +00:00
|
|
|
BrowseMedia(
|
|
|
|
title=preset.name,
|
2020-09-08 14:42:01 +00:00
|
|
|
media_class=MEDIA_CLASS_MUSIC,
|
2020-09-06 13:52:59 +00:00
|
|
|
media_content_id=f"preset:{preset.index}",
|
|
|
|
media_content_type=MEDIA_TYPE_MUSIC,
|
|
|
|
can_play=True,
|
|
|
|
can_expand=False,
|
|
|
|
)
|
2020-09-05 22:10:18 +00:00
|
|
|
for preset in presets.values()
|
|
|
|
]
|
|
|
|
|
2020-09-06 13:52:59 +00:00
|
|
|
root = BrowseMedia(
|
|
|
|
title="Root",
|
2020-09-08 14:42:01 +00:00
|
|
|
media_class=MEDIA_CLASS_DIRECTORY,
|
2020-09-06 13:52:59 +00:00
|
|
|
media_content_id="root",
|
|
|
|
media_content_type="library",
|
|
|
|
can_play=False,
|
|
|
|
can_expand=True,
|
|
|
|
children=radio,
|
|
|
|
)
|
2020-09-05 22:10:18 +00:00
|
|
|
|
|
|
|
return root
|
|
|
|
|
|
|
|
async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None:
|
|
|
|
"""Play media."""
|
|
|
|
|
|
|
|
if media_id.startswith("preset:"):
|
|
|
|
preset = int(media_id[7:])
|
|
|
|
await self._state.set_tuner_preset(preset)
|
|
|
|
else:
|
|
|
|
_LOGGER.error("Media %s is not supported", media_id)
|
|
|
|
return
|
|
|
|
|
2019-07-08 15:14:19 +00:00
|
|
|
@property
|
|
|
|
def source(self):
|
|
|
|
"""Return the current input source."""
|
|
|
|
value = self._state.get_source()
|
|
|
|
if value is None:
|
|
|
|
return None
|
|
|
|
return value.name
|
|
|
|
|
|
|
|
@property
|
|
|
|
def source_list(self):
|
|
|
|
"""List of available input sources."""
|
|
|
|
return [x.name for x in self._state.get_source_list()]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def sound_mode(self):
|
|
|
|
"""Name of the current sound mode."""
|
|
|
|
if self._state.zn != 1:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if self._get_2ch():
|
|
|
|
value = self._state.get_decode_mode_2ch()
|
|
|
|
else:
|
|
|
|
value = self._state.get_decode_mode_mch()
|
|
|
|
if value:
|
|
|
|
return value.name
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def sound_mode_list(self):
|
|
|
|
"""List of available sound modes."""
|
|
|
|
if self._state.zn != 1:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if self._get_2ch():
|
|
|
|
return [x.name for x in DecodeMode2CH]
|
|
|
|
return [x.name for x in DecodeModeMCH]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_volume_muted(self):
|
|
|
|
"""Boolean if volume is currently muted."""
|
|
|
|
value = self._state.get_mute()
|
|
|
|
if value is None:
|
|
|
|
return None
|
|
|
|
return value
|
|
|
|
|
|
|
|
@property
|
|
|
|
def volume_level(self):
|
|
|
|
"""Volume level of device."""
|
|
|
|
value = self._state.get_volume()
|
|
|
|
if value is None:
|
|
|
|
return None
|
|
|
|
return value / 99.0
|
|
|
|
|
|
|
|
@property
|
|
|
|
def media_content_type(self):
|
|
|
|
"""Content type of current playing media."""
|
|
|
|
source = self._state.get_source()
|
|
|
|
if source == SourceCodes.DAB:
|
|
|
|
value = MEDIA_TYPE_MUSIC
|
|
|
|
elif source == SourceCodes.FM:
|
|
|
|
value = MEDIA_TYPE_MUSIC
|
|
|
|
else:
|
|
|
|
value = None
|
|
|
|
return value
|
2020-09-05 22:10:18 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def media_content_id(self):
|
|
|
|
"""Content type of current playing media."""
|
|
|
|
source = self._state.get_source()
|
|
|
|
if source in (SourceCodes.DAB, SourceCodes.FM):
|
|
|
|
preset = self._state.get_tuner_preset()
|
|
|
|
if preset:
|
|
|
|
value = f"preset:{preset}"
|
|
|
|
else:
|
|
|
|
value = None
|
|
|
|
else:
|
|
|
|
value = None
|
|
|
|
|
|
|
|
return value
|
2019-07-08 15:14:19 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def media_channel(self):
|
|
|
|
"""Channel currently playing."""
|
|
|
|
source = self._state.get_source()
|
|
|
|
if source == SourceCodes.DAB:
|
|
|
|
value = self._state.get_dab_station()
|
|
|
|
elif source == SourceCodes.FM:
|
|
|
|
value = self._state.get_rds_information()
|
|
|
|
else:
|
|
|
|
value = None
|
|
|
|
return value
|
|
|
|
|
|
|
|
@property
|
|
|
|
def media_artist(self):
|
|
|
|
"""Artist of current playing media, music track only."""
|
|
|
|
source = self._state.get_source()
|
|
|
|
if source == SourceCodes.DAB:
|
|
|
|
value = self._state.get_dls_pdt()
|
|
|
|
else:
|
|
|
|
value = None
|
|
|
|
return value
|
|
|
|
|
|
|
|
@property
|
|
|
|
def media_title(self):
|
|
|
|
"""Title of current playing media."""
|
|
|
|
source = self._state.get_source()
|
|
|
|
if source is None:
|
|
|
|
return None
|
|
|
|
|
2021-10-17 17:56:00 +00:00
|
|
|
if channel := self.media_channel:
|
2019-09-03 14:11:36 +00:00
|
|
|
value = f"{source.name} - {channel}"
|
2019-07-08 15:14:19 +00:00
|
|
|
else:
|
|
|
|
value = source.name
|
|
|
|
return value
|