2021-12-29 04:36:18 +00:00
|
|
|
"""Support for Ubiquiti's UniFi Protect NVR."""
|
2024-03-08 15:35:23 +00:00
|
|
|
|
2021-12-29 04:36:18 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import logging
|
2024-06-13 16:44:29 +00:00
|
|
|
from typing import Any
|
2021-12-29 04:36:18 +00:00
|
|
|
|
2024-06-09 23:25:39 +00:00
|
|
|
from uiprotect.data import (
|
2022-08-26 11:46:11 +00:00
|
|
|
Camera,
|
|
|
|
ProtectAdoptableDeviceModel,
|
|
|
|
ProtectModelWithId,
|
2022-08-28 17:31:07 +00:00
|
|
|
StateType,
|
2022-08-26 11:46:11 +00:00
|
|
|
)
|
2024-06-09 23:25:39 +00:00
|
|
|
from uiprotect.exceptions import StreamError
|
2021-12-29 04:36:18 +00:00
|
|
|
|
2022-03-14 20:16:22 +00:00
|
|
|
from homeassistant.components import media_source
|
2021-12-29 04:36:18 +00:00
|
|
|
from homeassistant.components.media_player import (
|
2022-03-14 20:16:22 +00:00
|
|
|
BrowseMedia,
|
2022-01-09 00:06:00 +00:00
|
|
|
MediaPlayerDeviceClass,
|
2021-12-29 04:36:18 +00:00
|
|
|
MediaPlayerEntity,
|
|
|
|
MediaPlayerEntityDescription,
|
2022-04-07 07:35:15 +00:00
|
|
|
MediaPlayerEntityFeature,
|
2022-09-08 21:22:16 +00:00
|
|
|
MediaPlayerState,
|
|
|
|
MediaType,
|
2022-03-14 20:16:22 +00:00
|
|
|
async_process_play_media_url,
|
|
|
|
)
|
2021-12-29 04:36:18 +00:00
|
|
|
from homeassistant.core import HomeAssistant, callback
|
2021-12-29 18:39:45 +00:00
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
2021-12-29 04:36:18 +00:00
|
|
|
|
2024-06-16 14:00:14 +00:00
|
|
|
from .data import UFPConfigEntry
|
2021-12-29 04:36:18 +00:00
|
|
|
from .entity import ProtectDeviceEntity
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2024-06-16 14:00:14 +00:00
|
|
|
_SPEAKER_DESCRIPTION = MediaPlayerEntityDescription(
|
|
|
|
key="speaker", name="Speaker", device_class=MediaPlayerDeviceClass.SPEAKER
|
|
|
|
)
|
|
|
|
|
2021-12-29 04:36:18 +00:00
|
|
|
|
|
|
|
async def async_setup_entry(
|
|
|
|
hass: HomeAssistant,
|
2024-06-12 15:49:18 +00:00
|
|
|
entry: UFPConfigEntry,
|
2021-12-29 18:39:45 +00:00
|
|
|
async_add_entities: AddEntitiesCallback,
|
2021-12-29 04:36:18 +00:00
|
|
|
) -> None:
|
|
|
|
"""Discover cameras with speakers on a UniFi Protect NVR."""
|
2024-06-12 15:49:18 +00:00
|
|
|
data = entry.runtime_data
|
2021-12-29 04:36:18 +00:00
|
|
|
|
2024-02-21 05:47:31 +00:00
|
|
|
@callback
|
|
|
|
def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
|
2022-08-28 17:31:07 +00:00
|
|
|
if isinstance(device, Camera) and (
|
|
|
|
device.has_speaker or device.has_removable_speaker
|
|
|
|
):
|
2022-06-27 21:03:25 +00:00
|
|
|
async_add_entities([ProtectMediaPlayer(data, device)])
|
|
|
|
|
2024-06-13 16:44:29 +00:00
|
|
|
data.async_subscribe_adopt(_add_new_device)
|
|
|
|
async_add_entities(
|
2024-06-16 14:00:14 +00:00
|
|
|
ProtectMediaPlayer(data, device, _SPEAKER_DESCRIPTION)
|
2024-06-13 16:44:29 +00:00
|
|
|
for device in data.get_cameras()
|
|
|
|
if device.has_speaker or device.has_removable_speaker
|
2022-06-29 03:00:26 +00:00
|
|
|
)
|
2022-06-27 21:03:25 +00:00
|
|
|
|
2021-12-29 04:36:18 +00:00
|
|
|
|
|
|
|
class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
|
|
|
|
"""A Ubiquiti UniFi Protect Speaker."""
|
|
|
|
|
2022-01-10 04:37:24 +00:00
|
|
|
device: Camera
|
|
|
|
entity_description: MediaPlayerEntityDescription
|
2022-04-07 07:35:15 +00:00
|
|
|
_attr_supported_features = (
|
|
|
|
MediaPlayerEntityFeature.PLAY_MEDIA
|
|
|
|
| MediaPlayerEntityFeature.VOLUME_SET
|
|
|
|
| MediaPlayerEntityFeature.VOLUME_STEP
|
|
|
|
| MediaPlayerEntityFeature.STOP
|
|
|
|
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
|
|
|
)
|
2024-06-16 14:00:14 +00:00
|
|
|
_attr_media_content_type = MediaType.MUSIC
|
2024-06-14 19:29:18 +00:00
|
|
|
_state_attrs = ("_attr_available", "_attr_state", "_attr_volume_level")
|
2022-01-10 04:37:24 +00:00
|
|
|
|
2021-12-29 04:36:18 +00:00
|
|
|
@callback
|
2022-06-21 03:52:41 +00:00
|
|
|
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
|
|
|
|
super()._async_update_device_from_protect(device)
|
2023-07-16 16:24:27 +00:00
|
|
|
updated_device = self.device
|
|
|
|
self._attr_volume_level = float(updated_device.speaker_settings.volume / 100)
|
2021-12-29 04:36:18 +00:00
|
|
|
|
|
|
|
if (
|
2023-07-16 16:24:27 +00:00
|
|
|
updated_device.talkback_stream is not None
|
|
|
|
and updated_device.talkback_stream.is_running
|
2021-12-29 04:36:18 +00:00
|
|
|
):
|
2022-09-08 21:22:16 +00:00
|
|
|
self._attr_state = MediaPlayerState.PLAYING
|
2021-12-29 04:36:18 +00:00
|
|
|
else:
|
2022-09-08 21:22:16 +00:00
|
|
|
self._attr_state = MediaPlayerState.IDLE
|
2021-12-29 04:36:18 +00:00
|
|
|
|
2022-08-28 17:31:07 +00:00
|
|
|
is_connected = self.data.last_update_success and (
|
2024-01-05 12:27:10 +00:00
|
|
|
updated_device.state is StateType.CONNECTED
|
2023-07-16 16:24:27 +00:00
|
|
|
or (not updated_device.is_adopted_by_us and updated_device.can_adopt)
|
2022-08-28 17:31:07 +00:00
|
|
|
)
|
2023-07-16 16:24:27 +00:00
|
|
|
self._attr_available = is_connected and updated_device.feature_flags.has_speaker
|
2022-08-28 17:31:07 +00:00
|
|
|
|
2021-12-29 04:36:18 +00:00
|
|
|
async def async_set_volume_level(self, volume: float) -> None:
|
|
|
|
"""Set volume level, range 0..1."""
|
|
|
|
|
|
|
|
volume_int = int(volume * 100)
|
|
|
|
await self.device.set_speaker_volume(volume_int)
|
|
|
|
|
|
|
|
async def async_media_stop(self) -> None:
|
|
|
|
"""Send stop command."""
|
|
|
|
|
|
|
|
if (
|
|
|
|
self.device.talkback_stream is not None
|
|
|
|
and self.device.talkback_stream.is_running
|
|
|
|
):
|
2022-06-22 20:57:21 +00:00
|
|
|
_LOGGER.debug("Stopping playback for %s Speaker", self.device.display_name)
|
2021-12-29 04:36:18 +00:00
|
|
|
await self.device.stop_audio()
|
2022-06-21 03:52:41 +00:00
|
|
|
self._async_updated_event(self.device)
|
2021-12-29 04:36:18 +00:00
|
|
|
|
|
|
|
async def async_play_media(
|
2022-09-08 21:22:16 +00:00
|
|
|
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
2021-12-29 04:36:18 +00:00
|
|
|
) -> None:
|
|
|
|
"""Play a piece of media."""
|
2022-03-14 20:16:22 +00:00
|
|
|
if media_source.is_media_source_id(media_id):
|
2022-09-08 21:22:16 +00:00
|
|
|
media_type = MediaType.MUSIC
|
2022-05-27 16:05:06 +00:00
|
|
|
play_item = await media_source.async_resolve_media(
|
|
|
|
self.hass, media_id, self.entity_id
|
|
|
|
)
|
2022-03-14 20:16:22 +00:00
|
|
|
media_id = async_process_play_media_url(self.hass, play_item.url)
|
2021-12-29 04:36:18 +00:00
|
|
|
|
2022-09-08 21:22:16 +00:00
|
|
|
if media_type != MediaType.MUSIC:
|
2022-03-14 20:16:22 +00:00
|
|
|
raise HomeAssistantError("Only music media type is supported")
|
2021-12-29 04:36:18 +00:00
|
|
|
|
2022-06-22 20:57:21 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"Playing Media %s for %s Speaker", media_id, self.device.display_name
|
|
|
|
)
|
2021-12-29 04:36:18 +00:00
|
|
|
await self.async_media_stop()
|
|
|
|
try:
|
|
|
|
await self.device.play_audio(media_id, blocking=False)
|
|
|
|
except StreamError as err:
|
2022-03-14 20:16:22 +00:00
|
|
|
raise HomeAssistantError(err) from err
|
2023-01-18 13:10:13 +00:00
|
|
|
|
|
|
|
# update state after starting player
|
|
|
|
self._async_updated_event(self.device)
|
|
|
|
# wait until player finishes to update state again
|
|
|
|
await self.device.wait_until_audio_completes()
|
2021-12-29 04:36:18 +00:00
|
|
|
|
2022-06-21 03:52:41 +00:00
|
|
|
self._async_updated_event(self.device)
|
2022-03-14 20:16:22 +00:00
|
|
|
|
|
|
|
async def async_browse_media(
|
2022-09-08 21:22:16 +00:00
|
|
|
self,
|
|
|
|
media_content_type: MediaType | str | None = None,
|
|
|
|
media_content_id: str | None = None,
|
2022-03-14 20:16:22 +00:00
|
|
|
) -> BrowseMedia:
|
|
|
|
"""Implement the websocket media browsing helper."""
|
|
|
|
return await media_source.async_browse_media(
|
|
|
|
self.hass,
|
|
|
|
media_content_id,
|
|
|
|
content_filter=lambda item: item.media_content_type.startswith("audio/"),
|
|
|
|
)
|