mirror of
https://github.com/home-assistant/core.git
synced 2025-08-06 06:05:10 +02:00
Update integration with feedback from emontnemery
This commit is contained in:
@@ -114,6 +114,7 @@ omit =
|
|||||||
homeassistant/components/bangolufsen/const.py
|
homeassistant/components/bangolufsen/const.py
|
||||||
homeassistant/components/bangolufsen/entity.py
|
homeassistant/components/bangolufsen/entity.py
|
||||||
homeassistant/components/bangolufsen/media_player.py
|
homeassistant/components/bangolufsen/media_player.py
|
||||||
|
homeassistant/components/bangolufsen/util.py
|
||||||
homeassistant/components/bangolufsen/websocket.py
|
homeassistant/components/bangolufsen/websocket.py
|
||||||
homeassistant/components/bbox/device_tracker.py
|
homeassistant/components/bbox/device_tracker.py
|
||||||
homeassistant/components/bbox/sensor.py
|
homeassistant/components/bbox/sensor.py
|
||||||
|
@@ -1,42 +1,53 @@
|
|||||||
"""The Bang & Olufsen integration."""
|
"""The Bang & Olufsen integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from multiprocessing.pool import ApplyResult
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from mozart_api.exceptions import ServiceException
|
from mozart_api.exceptions import ServiceException
|
||||||
from mozart_api.models import BatteryState
|
|
||||||
from mozart_api.mozart_client import MozartClient
|
from mozart_api.mozart_client import MozartClient
|
||||||
from urllib3.exceptions import MaxRetryError
|
from urllib3.exceptions import MaxRetryError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME, Platform
|
from homeassistant.const import CONF_HOST, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
|
|
||||||
from .const import DOMAIN, ENTITY_ENUM, WEBSOCKET_CONNECTION_DELAY
|
from .const import DOMAIN, WEBSOCKET_CONNECTION_DELAY
|
||||||
from .media_player import BangOlufsenMediaPlayer
|
|
||||||
from .websocket import BangOlufsenWebsocket
|
from .websocket import BangOlufsenWebsocket
|
||||||
|
|
||||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BangOlufsenData:
|
||||||
|
"""Dataclass for storing entities."""
|
||||||
|
|
||||||
|
websocket: BangOlufsenWebsocket
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up from a config entry."""
|
"""Set up from a config entry."""
|
||||||
hass.data.setdefault(DOMAIN, {})
|
|
||||||
|
|
||||||
# Ensure that a unique id is available
|
client = MozartClient(
|
||||||
if not entry.unique_id:
|
host=entry.data[CONF_HOST], urllib3_logging_level=logging.DEBUG
|
||||||
raise ConfigEntryError("Can't retrieve unique id from config entry. Aborting")
|
)
|
||||||
|
|
||||||
# If connection can't be made abort.
|
# Check connection and try to initialize it.
|
||||||
if not await init_entities(hass, entry):
|
try:
|
||||||
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}")
|
await client.get_battery_state(_request_timeout=3)
|
||||||
|
except (MaxRetryError, ServiceException, Exception) as error:
|
||||||
|
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error
|
||||||
|
|
||||||
|
websocket = BangOlufsenWebsocket(hass, entry)
|
||||||
|
|
||||||
|
# Add the websocket
|
||||||
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BangOlufsenData(websocket)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
async_call_later(hass, WEBSOCKET_CONNECTION_DELAY, websocket.connect_websocket)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -46,39 +57,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
if unload_ok:
|
if unload_ok:
|
||||||
hass.data[DOMAIN].pop(entry.unique_id)
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
async def init_entities(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
"""Initialise the supported entities of the device."""
|
|
||||||
client = MozartClient(
|
|
||||||
host=entry.data[CONF_HOST], urllib3_logging_level=logging.ERROR
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check connection and try to initialize it.
|
|
||||||
try:
|
|
||||||
cast(
|
|
||||||
ApplyResult[BatteryState],
|
|
||||||
client.get_battery_state(async_req=True, _request_timeout=3),
|
|
||||||
).get()
|
|
||||||
except (MaxRetryError, ServiceException):
|
|
||||||
_LOGGER.error("Unable to connect to %s", entry.data[CONF_NAME])
|
|
||||||
return False
|
|
||||||
|
|
||||||
websocket = BangOlufsenWebsocket(hass, entry)
|
|
||||||
|
|
||||||
# Create the Media Player entity.
|
|
||||||
media_player = BangOlufsenMediaPlayer(entry)
|
|
||||||
|
|
||||||
# Add the created entities
|
|
||||||
hass.data[DOMAIN][entry.unique_id] = {
|
|
||||||
ENTITY_ENUM.WEBSOCKET: websocket,
|
|
||||||
ENTITY_ENUM.MEDIA_PLAYER: media_player,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Start the WebSocket listener with a delay to allow for entity and dispatcher listener creation
|
|
||||||
async_call_later(hass, WEBSOCKET_CONNECTION_DELAY, websocket.connect_websocket)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
@@ -3,11 +3,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
from multiprocessing.pool import ApplyResult
|
from typing import Any, TypedDict
|
||||||
from typing import Any, TypedDict, cast
|
|
||||||
|
|
||||||
from mozart_api.exceptions import ApiException, NotFoundException
|
from mozart_api.exceptions import ApiException, NotFoundException
|
||||||
from mozart_api.models import BeolinkPeer, VolumeSettings
|
|
||||||
from mozart_api.mozart_client import MozartClient
|
from mozart_api.mozart_client import MozartClient
|
||||||
from urllib3.exceptions import MaxRetryError, NewConnectionError
|
from urllib3.exceptions import MaxRetryError, NewConnectionError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -28,11 +26,9 @@ from .const import (
|
|||||||
CONF_DEFAULT_VOLUME,
|
CONF_DEFAULT_VOLUME,
|
||||||
CONF_MAX_VOLUME,
|
CONF_MAX_VOLUME,
|
||||||
CONF_SERIAL_NUMBER,
|
CONF_SERIAL_NUMBER,
|
||||||
CONF_VOLUME_STEP,
|
|
||||||
DEFAULT_DEFAULT_VOLUME,
|
DEFAULT_DEFAULT_VOLUME,
|
||||||
DEFAULT_MAX_VOLUME,
|
DEFAULT_MAX_VOLUME,
|
||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
DEFAULT_VOLUME_STEP,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,40 +36,33 @@ from .const import (
|
|||||||
class UserInput(TypedDict, total=False):
|
class UserInput(TypedDict, total=False):
|
||||||
"""TypedDict for user_input."""
|
"""TypedDict for user_input."""
|
||||||
|
|
||||||
name: str
|
|
||||||
volume_step: int
|
|
||||||
default_volume: int
|
default_volume: int
|
||||||
max_volume: int
|
|
||||||
host: str
|
host: str
|
||||||
model: str
|
|
||||||
jid: str
|
jid: str
|
||||||
|
max_volume: int
|
||||||
|
model: str
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow."""
|
"""Handle a config flow."""
|
||||||
|
|
||||||
|
_beolink_jid = ""
|
||||||
|
_client: MozartClient
|
||||||
|
_host = ""
|
||||||
|
_model = ""
|
||||||
|
_name = ""
|
||||||
|
_serial_number = ""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Init the config flow."""
|
"""Init the config flow."""
|
||||||
self._host: str = ""
|
|
||||||
self._name: str = ""
|
|
||||||
self._model: str = ""
|
|
||||||
self._serial_number: str = ""
|
|
||||||
self._beolink_jid: str = ""
|
|
||||||
|
|
||||||
self._client: MozartClient | None = None
|
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
async def _compile_data(self) -> UserInput:
|
async def _compile_data(self) -> UserInput:
|
||||||
"""Compile data for entry creation."""
|
"""Compile data for entry creation."""
|
||||||
if not self._client:
|
|
||||||
self._client = MozartClient(self._host, urllib3_logging_level=logging.ERROR)
|
|
||||||
|
|
||||||
# Get current volume settings
|
# Get current volume settings
|
||||||
volume_settings = cast(
|
volume_settings = await self._client.get_volume_settings()
|
||||||
ApplyResult[VolumeSettings],
|
|
||||||
self._client.get_volume_settings(async_req=True),
|
|
||||||
).get()
|
|
||||||
|
|
||||||
# Create a dict containing all necessary information for setup
|
# Create a dict containing all necessary information for setup
|
||||||
data = UserInput()
|
data = UserInput()
|
||||||
@@ -81,7 +70,6 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
data[CONF_HOST] = self._host
|
data[CONF_HOST] = self._host
|
||||||
data[CONF_MODEL] = self._model
|
data[CONF_MODEL] = self._model
|
||||||
data[CONF_BEOLINK_JID] = self._beolink_jid
|
data[CONF_BEOLINK_JID] = self._beolink_jid
|
||||||
data[CONF_VOLUME_STEP] = DEFAULT_VOLUME_STEP
|
|
||||||
data[CONF_DEFAULT_VOLUME] = (
|
data[CONF_DEFAULT_VOLUME] = (
|
||||||
volume_settings.default.level
|
volume_settings.default.level
|
||||||
if volume_settings.default and volume_settings.default.level
|
if volume_settings.default and volume_settings.default.level
|
||||||
@@ -114,10 +102,7 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
# Try to get information from Beolink self method.
|
# Try to get information from Beolink self method.
|
||||||
try:
|
try:
|
||||||
beolink_self = cast(
|
beolink_self = await self._client.get_beolink_self(_request_timeout=3)
|
||||||
ApplyResult[BeolinkPeer],
|
|
||||||
self._client.get_beolink_self(async_req=True, _request_timeout=3),
|
|
||||||
).get()
|
|
||||||
|
|
||||||
except (
|
except (
|
||||||
ApiException,
|
ApiException,
|
||||||
@@ -173,7 +158,9 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
}
|
}
|
||||||
|
|
||||||
await self.async_set_unique_id(self._serial_number)
|
await self.async_set_unique_id(self._serial_number)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured(updates={CONF_HOST: self._host})
|
||||||
|
|
||||||
|
self._client = MozartClient(self._host, urllib3_logging_level=logging.ERROR)
|
||||||
|
|
||||||
return await self.async_step_confirm()
|
return await self.async_step_confirm()
|
||||||
|
|
||||||
|
@@ -2,37 +2,12 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum, StrEnum
|
from enum import StrEnum
|
||||||
import logging
|
from typing import Final
|
||||||
from typing import Final, cast
|
|
||||||
|
|
||||||
from mozart_api.models import (
|
from mozart_api.models import Source, SourceArray, SourceTypeEnum
|
||||||
PlaybackContentMetadata,
|
|
||||||
PlaybackProgress,
|
|
||||||
RenderingState,
|
|
||||||
Source,
|
|
||||||
SourceArray,
|
|
||||||
SourceTypeEnum,
|
|
||||||
VolumeLevel,
|
|
||||||
VolumeMute,
|
|
||||||
VolumeState,
|
|
||||||
)
|
|
||||||
from mozart_api.mozart_client import MozartClient
|
|
||||||
|
|
||||||
from homeassistant.components.media_player import MediaPlayerState, MediaType
|
from homeassistant.components.media_player import MediaPlayerState, MediaType
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_HOST
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers import device_registry as dr
|
|
||||||
from homeassistant.helpers.device_registry import DeviceEntry
|
|
||||||
|
|
||||||
|
|
||||||
class ART_SIZE_ENUM(Enum):
|
|
||||||
"""Enum used for sorting images that have size defined by a string."""
|
|
||||||
|
|
||||||
small = 1
|
|
||||||
medium = 2
|
|
||||||
large = 3
|
|
||||||
|
|
||||||
|
|
||||||
class SOURCE_ENUM(StrEnum):
|
class SOURCE_ENUM(StrEnum):
|
||||||
@@ -58,14 +33,6 @@ class SOURCE_ENUM(StrEnum):
|
|||||||
tidalConnect = "Tidal Connect" # noqa: N815
|
tidalConnect = "Tidal Connect" # noqa: N815
|
||||||
|
|
||||||
|
|
||||||
class REPEAT_ENUM(StrEnum):
|
|
||||||
"""Enum used for translating device repeat settings to Home Assistant settings."""
|
|
||||||
|
|
||||||
all = "all"
|
|
||||||
one = "track"
|
|
||||||
off = "none"
|
|
||||||
|
|
||||||
|
|
||||||
BANGOLUFSEN_STATES: dict[str, MediaPlayerState] = {
|
BANGOLUFSEN_STATES: dict[str, MediaPlayerState] = {
|
||||||
# Dict used for translating device states to Home Assistant states.
|
# Dict used for translating device states to Home Assistant states.
|
||||||
"started": MediaPlayerState.PLAYING,
|
"started": MediaPlayerState.PLAYING,
|
||||||
@@ -75,9 +42,8 @@ BANGOLUFSEN_STATES: dict[str, MediaPlayerState] = {
|
|||||||
"stopped": MediaPlayerState.PAUSED,
|
"stopped": MediaPlayerState.PAUSED,
|
||||||
"ended": MediaPlayerState.PAUSED,
|
"ended": MediaPlayerState.PAUSED,
|
||||||
"error": MediaPlayerState.IDLE,
|
"error": MediaPlayerState.IDLE,
|
||||||
# A devices initial state is "unknown" and should be treated as "idle"
|
# A device's initial state is "unknown" and should be treated as "idle"
|
||||||
"unknown": MediaPlayerState.IDLE,
|
"unknown": MediaPlayerState.IDLE,
|
||||||
# Power states
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -105,13 +71,6 @@ class MODEL_ENUM(StrEnum):
|
|||||||
BEOSOUND_THEATRE = "Beosound Theatre"
|
BEOSOUND_THEATRE = "Beosound Theatre"
|
||||||
|
|
||||||
|
|
||||||
class ENTITY_ENUM(StrEnum):
|
|
||||||
"""Enum for accessing and storing the entities in hass."""
|
|
||||||
|
|
||||||
MEDIA_PLAYER = "media_player"
|
|
||||||
WEBSOCKET = "websocket"
|
|
||||||
|
|
||||||
|
|
||||||
# Dispatcher events
|
# Dispatcher events
|
||||||
class WEBSOCKET_NOTIFICATION(StrEnum):
|
class WEBSOCKET_NOTIFICATION(StrEnum):
|
||||||
"""Enum for WebSocket notification types."""
|
"""Enum for WebSocket notification types."""
|
||||||
@@ -148,7 +107,6 @@ VOLUME_STEP_RANGE: Final[range] = range(1, 20, 1)
|
|||||||
# Configuration.
|
# Configuration.
|
||||||
CONF_DEFAULT_VOLUME: Final = "default_volume"
|
CONF_DEFAULT_VOLUME: Final = "default_volume"
|
||||||
CONF_MAX_VOLUME: Final = "max_volume"
|
CONF_MAX_VOLUME: Final = "max_volume"
|
||||||
CONF_VOLUME_STEP: Final = "volume_step"
|
|
||||||
CONF_SERIAL_NUMBER: Final = "serial_number"
|
CONF_SERIAL_NUMBER: Final = "serial_number"
|
||||||
CONF_BEOLINK_JID: Final = "jid"
|
CONF_BEOLINK_JID: Final = "jid"
|
||||||
|
|
||||||
@@ -199,57 +157,57 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
|||||||
items=[
|
items=[
|
||||||
Source(
|
Source(
|
||||||
id="uriStreamer",
|
id="uriStreamer",
|
||||||
is_enabled=True,
|
isEnabled=True,
|
||||||
is_playable=False,
|
isPlayable=False,
|
||||||
name="Audio Streamer",
|
name="Audio Streamer",
|
||||||
type=SourceTypeEnum(value="uriStreamer"),
|
type=SourceTypeEnum(value="uriStreamer"),
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="bluetooth",
|
id="bluetooth",
|
||||||
is_enabled=True,
|
isEnabled=True,
|
||||||
is_playable=False,
|
isPlayable=False,
|
||||||
name="Bluetooth",
|
name="Bluetooth",
|
||||||
type=SourceTypeEnum(value="bluetooth"),
|
type=SourceTypeEnum(value="bluetooth"),
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="spotify",
|
id="spotify",
|
||||||
is_enabled=True,
|
isEnabled=True,
|
||||||
is_playable=False,
|
isPlayable=False,
|
||||||
name="Spotify Connect",
|
name="Spotify Connect",
|
||||||
type=SourceTypeEnum(value="spotify"),
|
type=SourceTypeEnum(value="spotify"),
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="lineIn",
|
id="lineIn",
|
||||||
is_enabled=True,
|
isEnabled=True,
|
||||||
is_playable=True,
|
isPlayable=True,
|
||||||
name="Line-In",
|
name="Line-In",
|
||||||
type=SourceTypeEnum(value="lineIn"),
|
type=SourceTypeEnum(value="lineIn"),
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="spdif",
|
id="spdif",
|
||||||
is_enabled=True,
|
isEnabled=True,
|
||||||
is_playable=True,
|
isPlayable=True,
|
||||||
name="Optical",
|
name="Optical",
|
||||||
type=SourceTypeEnum(value="spdif"),
|
type=SourceTypeEnum(value="spdif"),
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="netRadio",
|
id="netRadio",
|
||||||
is_enabled=True,
|
isEnabled=True,
|
||||||
is_playable=True,
|
isPlayable=True,
|
||||||
name="B&O Radio",
|
name="B&O Radio",
|
||||||
type=SourceTypeEnum(value="netRadio"),
|
type=SourceTypeEnum(value="netRadio"),
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="deezer",
|
id="deezer",
|
||||||
is_enabled=True,
|
isEnabled=True,
|
||||||
is_playable=True,
|
isPlayable=True,
|
||||||
name="Deezer",
|
name="Deezer",
|
||||||
type=SourceTypeEnum(value="deezer"),
|
type=SourceTypeEnum(value="deezer"),
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="tidalConnect",
|
id="tidalConnect",
|
||||||
is_enabled=True,
|
isEnabled=True,
|
||||||
is_playable=True,
|
isPlayable=True,
|
||||||
name="Tidal Connect",
|
name="Tidal Connect",
|
||||||
type=SourceTypeEnum(value="tidalConnect"),
|
type=SourceTypeEnum(value="tidalConnect"),
|
||||||
),
|
),
|
||||||
@@ -265,44 +223,3 @@ CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS"
|
|||||||
|
|
||||||
# Misc.
|
# Misc.
|
||||||
WEBSOCKET_CONNECTION_DELAY: Final[float] = 3.0
|
WEBSOCKET_CONNECTION_DELAY: Final[float] = 3.0
|
||||||
|
|
||||||
|
|
||||||
def get_device(hass: HomeAssistant | None, unique_id: str) -> DeviceEntry | None:
|
|
||||||
"""Get the device."""
|
|
||||||
if not isinstance(hass, HomeAssistant):
|
|
||||||
return None
|
|
||||||
|
|
||||||
device_registry = dr.async_get(hass)
|
|
||||||
device = cast(DeviceEntry, device_registry.async_get_device({(DOMAIN, unique_id)}))
|
|
||||||
return device
|
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenVariables:
|
|
||||||
"""Shared variables for various classes."""
|
|
||||||
|
|
||||||
def __init__(self, entry: ConfigEntry) -> None:
|
|
||||||
"""Initialize the object."""
|
|
||||||
|
|
||||||
# get the input from the config entry.
|
|
||||||
self.entry: ConfigEntry = entry
|
|
||||||
|
|
||||||
# Set the configuration variables.
|
|
||||||
self._host: str = self.entry.data[CONF_HOST]
|
|
||||||
self._name: str = self.entry.title
|
|
||||||
self._unique_id: str = cast(str, self.entry.unique_id)
|
|
||||||
|
|
||||||
self._client: MozartClient = MozartClient(
|
|
||||||
host=self._host,
|
|
||||||
websocket_reconnect=True,
|
|
||||||
urllib3_logging_level=logging.ERROR,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Objects that get directly updated by notifications.
|
|
||||||
self._playback_metadata: PlaybackContentMetadata = PlaybackContentMetadata()
|
|
||||||
self._playback_progress: PlaybackProgress = PlaybackProgress(total_duration=0)
|
|
||||||
self._playback_source: Source = Source()
|
|
||||||
self._playback_state: RenderingState = RenderingState()
|
|
||||||
self._source_change: Source = Source()
|
|
||||||
self._volume: VolumeState = VolumeState(
|
|
||||||
level=VolumeLevel(level=0), muted=VolumeMute(muted=False)
|
|
||||||
)
|
|
||||||
|
@@ -2,13 +2,59 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
import logging
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from mozart_api.models import (
|
||||||
|
PlaybackContentMetadata,
|
||||||
|
PlaybackProgress,
|
||||||
|
RenderingState,
|
||||||
|
Source,
|
||||||
|
VolumeLevel,
|
||||||
|
VolumeMute,
|
||||||
|
VolumeState,
|
||||||
|
)
|
||||||
|
from mozart_api.mozart_client import MozartClient
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.device_registry import DeviceEntry, DeviceInfo
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from .const import CONNECTION_STATUS, DOMAIN, BangOlufsenVariables
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class BangOlufsenVariables:
|
||||||
|
"""Shared variables for various classes."""
|
||||||
|
|
||||||
|
def __init__(self, entry: ConfigEntry) -> None:
|
||||||
|
"""Initialize the object."""
|
||||||
|
|
||||||
|
# get the input from the config entry.
|
||||||
|
self.entry: ConfigEntry = entry
|
||||||
|
|
||||||
|
self._device: DeviceEntry | None = None
|
||||||
|
|
||||||
|
# Set the configuration variables.
|
||||||
|
self._host: str = self.entry.data[CONF_HOST]
|
||||||
|
self._name: str = self.entry.title
|
||||||
|
self._unique_id: str = cast(str, self.entry.unique_id)
|
||||||
|
|
||||||
|
self._client: MozartClient = MozartClient(
|
||||||
|
host=self._host,
|
||||||
|
websocket_reconnect=True,
|
||||||
|
urllib3_logging_level=logging.ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Objects that get directly updated by notifications.
|
||||||
|
self._playback_metadata: PlaybackContentMetadata = PlaybackContentMetadata()
|
||||||
|
self._playback_progress: PlaybackProgress = PlaybackProgress(totalDuration=0)
|
||||||
|
self._playback_source: Source = Source()
|
||||||
|
self._playback_state: RenderingState = RenderingState()
|
||||||
|
self._source_change: Source = Source()
|
||||||
|
self._volume: VolumeState = VolumeState(
|
||||||
|
level=VolumeLevel(level=0), muted=VolumeMute(muted=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenEntity(Entity, BangOlufsenVariables):
|
class BangOlufsenEntity(Entity, BangOlufsenVariables):
|
||||||
@@ -26,21 +72,6 @@ class BangOlufsenEntity(Entity, BangOlufsenVariables):
|
|||||||
self._attr_entity_category = None
|
self._attr_entity_category = None
|
||||||
self._attr_should_poll = False
|
self._attr_should_poll = False
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Turn on the dispatchers."""
|
|
||||||
self._dispatchers = [
|
|
||||||
async_dispatcher_connect(
|
|
||||||
self.hass,
|
|
||||||
f"{self._unique_id}_{CONNECTION_STATUS}",
|
|
||||||
self._update_connection_state,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
|
||||||
"""Turn off the dispatchers."""
|
|
||||||
for dispatcher in self._dispatchers:
|
|
||||||
dispatcher()
|
|
||||||
|
|
||||||
async def _update_connection_state(self, connection_state: bool) -> None:
|
async def _update_connection_state(self, connection_state: bool) -> None:
|
||||||
"""Update entity connection state."""
|
"""Update entity connection state."""
|
||||||
self._attr_available = connection_state
|
self._attr_available = connection_state
|
||||||
|
@@ -6,6 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/bangolufsen",
|
"documentation": "https://www.home-assistant.io/integrations/bangolufsen",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["mozart-api==3.2.1.150.1"],
|
"requirements": ["mozart-api==3.2.1.150.4"],
|
||||||
"zeroconf": ["_bangolufsen._tcp.local."]
|
"zeroconf": ["_bangolufsen._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
"""Media player entity for the Bang & Olufsen integration."""
|
"""Media player entity for the Bang & Olufsen integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from multiprocessing.pool import ApplyResult
|
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from mozart_api import __version__ as MOZART_API_VERSION
|
from mozart_api import __version__ as MOZART_API_VERSION
|
||||||
@@ -12,23 +11,17 @@ from mozart_api.exceptions import ApiException
|
|||||||
from mozart_api.models import (
|
from mozart_api.models import (
|
||||||
Action,
|
Action,
|
||||||
Art,
|
Art,
|
||||||
BeolinkLeader,
|
|
||||||
BeolinkListener,
|
|
||||||
OverlayPlayRequest,
|
OverlayPlayRequest,
|
||||||
PlaybackContentMetadata,
|
PlaybackContentMetadata,
|
||||||
PlaybackError,
|
PlaybackError,
|
||||||
PlaybackProgress,
|
PlaybackProgress,
|
||||||
PlayQueueItem,
|
PlayQueueItem,
|
||||||
PlayQueueItemType,
|
PlayQueueItemType,
|
||||||
PlayQueueSettings,
|
|
||||||
ProductState,
|
|
||||||
RemoteMenuItem,
|
|
||||||
RenderingState,
|
RenderingState,
|
||||||
SceneProperties,
|
SceneProperties,
|
||||||
SoftwareUpdateState,
|
SoftwareUpdateState,
|
||||||
SoftwareUpdateStatus,
|
SoftwareUpdateStatus,
|
||||||
Source,
|
Source,
|
||||||
SourceArray,
|
|
||||||
Uri,
|
Uri,
|
||||||
UserFlow,
|
UserFlow,
|
||||||
VolumeLevel,
|
VolumeLevel,
|
||||||
@@ -36,6 +29,7 @@ from mozart_api.models import (
|
|||||||
VolumeSettings,
|
VolumeSettings,
|
||||||
VolumeState,
|
VolumeState,
|
||||||
)
|
)
|
||||||
|
from mozart_api.mozart_client import get_highest_resolution_artwork
|
||||||
|
|
||||||
from homeassistant.components import media_source
|
from homeassistant.components import media_source
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
@@ -46,35 +40,32 @@ from homeassistant.components.media_player import (
|
|||||||
MediaPlayerEntityFeature,
|
MediaPlayerEntityFeature,
|
||||||
MediaPlayerState,
|
MediaPlayerState,
|
||||||
MediaType,
|
MediaType,
|
||||||
RepeatMode,
|
|
||||||
async_process_play_media_url,
|
async_process_play_media_url,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_MODEL
|
from homeassistant.const import CONF_MODEL
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.device_registry import DeviceEntry, DeviceInfo
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ART_SIZE_ENUM,
|
|
||||||
BANGOLUFSEN_MEDIA_TYPE,
|
BANGOLUFSEN_MEDIA_TYPE,
|
||||||
BANGOLUFSEN_STATES,
|
BANGOLUFSEN_STATES,
|
||||||
CONF_BEOLINK_JID,
|
CONF_BEOLINK_JID,
|
||||||
CONF_DEFAULT_VOLUME,
|
CONF_DEFAULT_VOLUME,
|
||||||
CONF_MAX_VOLUME,
|
CONF_MAX_VOLUME,
|
||||||
CONF_VOLUME_STEP,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
ENTITY_ENUM,
|
|
||||||
FALLBACK_SOURCES,
|
FALLBACK_SOURCES,
|
||||||
HIDDEN_SOURCE_IDS,
|
HIDDEN_SOURCE_IDS,
|
||||||
REPEAT_ENUM,
|
|
||||||
SOURCE_ENUM,
|
SOURCE_ENUM,
|
||||||
VALID_MEDIA_TYPES,
|
VALID_MEDIA_TYPES,
|
||||||
WEBSOCKET_NOTIFICATION,
|
WEBSOCKET_NOTIFICATION,
|
||||||
)
|
)
|
||||||
from .entity import BangOlufsenEntity
|
from .entity import BangOlufsenEntity
|
||||||
|
from .util import get_device
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -86,19 +77,15 @@ BANGOLUFSEN_FEATURES = (
|
|||||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||||
| MediaPlayerEntityFeature.NEXT_TRACK
|
| MediaPlayerEntityFeature.NEXT_TRACK
|
||||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
|
||||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||||
| MediaPlayerEntityFeature.STOP
|
| MediaPlayerEntityFeature.STOP
|
||||||
| MediaPlayerEntityFeature.CLEAR_PLAYLIST
|
| MediaPlayerEntityFeature.CLEAR_PLAYLIST
|
||||||
| MediaPlayerEntityFeature.PLAY
|
| MediaPlayerEntityFeature.PLAY
|
||||||
| MediaPlayerEntityFeature.SHUFFLE_SET
|
|
||||||
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||||
| MediaPlayerEntityFeature.REPEAT_SET
|
|
||||||
| MediaPlayerEntityFeature.TURN_OFF
|
| MediaPlayerEntityFeature.TURN_OFF
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
|
||||||
SCAN_INTERVAL = timedelta(minutes=2)
|
SCAN_INTERVAL = timedelta(minutes=2)
|
||||||
|
|
||||||
|
|
||||||
@@ -108,9 +95,10 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up a Media Player entity from config entry."""
|
"""Set up a Media Player entity from config entry."""
|
||||||
entity = hass.data[DOMAIN][config_entry.unique_id][ENTITY_ENUM.MEDIA_PLAYER]
|
|
||||||
# Add MediaPlayer entity
|
# Add MediaPlayer entity
|
||||||
async_add_entities(new_entities=[entity], update_before_add=True)
|
async_add_entities(
|
||||||
|
new_entities=[BangOlufsenMediaPlayer(config_entry)], update_before_add=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
||||||
@@ -128,9 +116,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
self._default_volume: int = self.entry.data[CONF_DEFAULT_VOLUME]
|
self._default_volume: int = self.entry.data[CONF_DEFAULT_VOLUME]
|
||||||
self._max_volume: int = self.entry.data[CONF_MAX_VOLUME]
|
self._max_volume: int = self.entry.data[CONF_MAX_VOLUME]
|
||||||
self._model: str = self.entry.data[CONF_MODEL]
|
self._model: str = self.entry.data[CONF_MODEL]
|
||||||
self._volume_step: int = self.entry.data[CONF_VOLUME_STEP]
|
|
||||||
|
|
||||||
self._attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
configuration_url=f"http://{self._host}/#/",
|
configuration_url=f"http://{self._host}/#/",
|
||||||
identifiers={(DOMAIN, self._unique_id)},
|
identifiers={(DOMAIN, self._unique_id)},
|
||||||
@@ -139,19 +125,16 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
name=cast(str, self.name),
|
name=cast(str, self.name),
|
||||||
)
|
)
|
||||||
self._attr_name = self._name
|
self._attr_name = self._name
|
||||||
self._attr_should_poll = True
|
|
||||||
self._attr_unique_id = self._unique_id
|
self._attr_unique_id = self._unique_id
|
||||||
|
self._attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||||
|
|
||||||
# Misc. variables.
|
# Misc. variables.
|
||||||
self._audio_sources: dict[str, str] = {}
|
self._audio_sources: dict[str, str] = {}
|
||||||
self._beolink_listeners: list[BeolinkListener] = []
|
|
||||||
self._last_update: datetime = datetime(1970, 1, 1, 0, 0, 0, 0)
|
|
||||||
self._media_image: Art = Art()
|
self._media_image: Art = Art()
|
||||||
self._queue_settings: PlayQueueSettings = PlayQueueSettings()
|
# self._queue_settings: PlayQueueSettings = PlayQueueSettings()
|
||||||
self._remote_leader: BeolinkLeader | None = None
|
|
||||||
self._software_status: SoftwareUpdateStatus = SoftwareUpdateStatus(
|
self._software_status: SoftwareUpdateStatus = SoftwareUpdateStatus(
|
||||||
software_version="",
|
softwareVersion="",
|
||||||
state=SoftwareUpdateState(seconds_remaining=0, value="idle"),
|
state=SoftwareUpdateState(secondsRemaining=0, value="idle"),
|
||||||
)
|
)
|
||||||
self._sources: dict[str, str] = {}
|
self._sources: dict[str, str] = {}
|
||||||
self._state: str = MediaPlayerState.IDLE
|
self._state: str = MediaPlayerState.IDLE
|
||||||
@@ -159,66 +142,72 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Turn on the dispatchers."""
|
"""Turn on the dispatchers."""
|
||||||
|
|
||||||
await self._initialize()
|
await self._initialize()
|
||||||
|
|
||||||
await super().async_added_to_hass()
|
self.async_on_remove(
|
||||||
self._dispatchers.extend(
|
async_dispatcher_connect(
|
||||||
[
|
self.hass,
|
||||||
async_dispatcher_connect(
|
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_ERROR}",
|
||||||
self.hass,
|
self._update_playback_error,
|
||||||
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_METADATA}",
|
)
|
||||||
self._update_playback_metadata,
|
|
||||||
),
|
|
||||||
async_dispatcher_connect(
|
|
||||||
self.hass,
|
|
||||||
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_ERROR}",
|
|
||||||
self._update_playback_error,
|
|
||||||
),
|
|
||||||
async_dispatcher_connect(
|
|
||||||
self.hass,
|
|
||||||
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_PROGRESS}",
|
|
||||||
self._update_playback_progress,
|
|
||||||
),
|
|
||||||
async_dispatcher_connect(
|
|
||||||
self.hass,
|
|
||||||
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_STATE}",
|
|
||||||
self._update_playback_state,
|
|
||||||
),
|
|
||||||
async_dispatcher_connect(
|
|
||||||
self.hass,
|
|
||||||
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.SOURCE_CHANGE}",
|
|
||||||
self._update_source_change,
|
|
||||||
),
|
|
||||||
async_dispatcher_connect(
|
|
||||||
self.hass,
|
|
||||||
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.VOLUME}",
|
|
||||||
self._update_volume,
|
|
||||||
),
|
|
||||||
async_dispatcher_connect(
|
|
||||||
self.hass,
|
|
||||||
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.REMOTE_MENU_CHANGED}",
|
|
||||||
self._update_sources,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
self.async_on_remove(
|
||||||
"""Update polling information."""
|
async_dispatcher_connect(
|
||||||
if self._attr_available:
|
self.hass,
|
||||||
self._queue_settings = cast(
|
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_METADATA}",
|
||||||
ApplyResult[PlayQueueSettings],
|
self._update_playback_metadata,
|
||||||
self._client.get_settings_queue(async_req=True, _request_timeout=5),
|
)
|
||||||
).get()
|
)
|
||||||
|
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass,
|
||||||
|
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_PROGRESS}",
|
||||||
|
self._update_playback_progress,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass,
|
||||||
|
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_STATE}",
|
||||||
|
self._update_playback_state,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass,
|
||||||
|
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.REMOTE_MENU_CHANGED}",
|
||||||
|
self._update_sources,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass,
|
||||||
|
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.SOURCE_CHANGE}",
|
||||||
|
self._update_source_change,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass,
|
||||||
|
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.VOLUME}",
|
||||||
|
self._update_volume,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass,
|
||||||
|
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.SOFTWARE_UPDATE_STATE}",
|
||||||
|
self._update_device,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
async def _initialize(self) -> None:
|
async def _initialize(self) -> None:
|
||||||
"""Initialize connection dependent variables."""
|
"""Initialize connection dependent variables."""
|
||||||
|
|
||||||
# Get software version.
|
# Get software version.
|
||||||
self._software_status = cast(
|
self._software_status = await self._client.get_softwareupdate_status()
|
||||||
ApplyResult[SoftwareUpdateStatus],
|
|
||||||
self._client.get_softwareupdate_status(async_req=True),
|
|
||||||
).get()
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Connected to: %s %s running SW %s",
|
"Connected to: %s %s running SW %s",
|
||||||
@@ -228,18 +217,15 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Set the default and maximum volume of the product.
|
# Set the default and maximum volume of the product.
|
||||||
self._client.set_volume_settings(
|
await self._client.set_volume_settings(
|
||||||
volume_settings=VolumeSettings(
|
volume_settings=VolumeSettings(
|
||||||
default=VolumeLevel(level=self._default_volume),
|
default=VolumeLevel(level=self._default_volume),
|
||||||
maximum=VolumeLevel(level=self._max_volume),
|
maximum=VolumeLevel(level=self._max_volume),
|
||||||
),
|
)
|
||||||
async_req=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get overall device state once. This is handled by WebSocket events the rest of the time.
|
# Get overall device state once. This is handled by WebSocket events the rest of the time.
|
||||||
product_state = cast(
|
product_state = await self._client.get_product_state()
|
||||||
ApplyResult[ProductState], self._client.get_product_state(async_req=True)
|
|
||||||
).get()
|
|
||||||
|
|
||||||
# Get volume information.
|
# Get volume information.
|
||||||
if product_state.volume:
|
if product_state.volume:
|
||||||
@@ -260,10 +246,10 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
if self._playback_state.value:
|
if self._playback_state.value:
|
||||||
self._state = self._playback_state.value
|
self._state = self._playback_state.value
|
||||||
|
|
||||||
self._last_update = utcnow()
|
self._attr_media_position_updated_at = utcnow()
|
||||||
|
|
||||||
# Get the highest resolution available of the given images.
|
# Get the highest resolution available of the given images.
|
||||||
self._update_artwork()
|
self._media_image = get_highest_resolution_artwork(self._playback_metadata)
|
||||||
|
|
||||||
# If the device has been updated with new sources, then the API will fail here.
|
# If the device has been updated with new sources, then the API will fail here.
|
||||||
await self._update_sources()
|
await self._update_sources()
|
||||||
@@ -277,10 +263,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
# Audio sources
|
# Audio sources
|
||||||
try:
|
try:
|
||||||
# Get all available sources.
|
# Get all available sources.
|
||||||
sources = cast(
|
sources = await self._client.get_available_sources(target_remote=False)
|
||||||
ApplyResult[SourceArray],
|
|
||||||
self._client.get_available_sources(target_remote=False, async_req=True),
|
|
||||||
).get()
|
|
||||||
|
|
||||||
# Use a fallback list of sources
|
# Use a fallback list of sources
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -308,10 +291,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Video sources from remote menu
|
# Video sources from remote menu
|
||||||
menu_items = cast(
|
menu_items = await self._client.get_remote_menu()
|
||||||
ApplyResult[dict[str, RemoteMenuItem]],
|
|
||||||
self._client.get_remote_menu(async_req=True),
|
|
||||||
).get()
|
|
||||||
|
|
||||||
for key in menu_items:
|
for key in menu_items:
|
||||||
menu_item = menu_items[key]
|
menu_item = menu_items[key]
|
||||||
@@ -337,40 +317,12 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
if self.hass.is_running:
|
if self.hass.is_running:
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
def _update_artwork(self) -> None:
|
|
||||||
"""Find the highest resolution image."""
|
|
||||||
# Ensure that the metadata doesn't change mid processing.
|
|
||||||
metadata = self._playback_metadata
|
|
||||||
|
|
||||||
# Check if the metadata is not null and that there is art.
|
|
||||||
if (
|
|
||||||
isinstance(metadata, PlaybackContentMetadata)
|
|
||||||
and isinstance(metadata.art, list)
|
|
||||||
and len(metadata.art) > 0
|
|
||||||
):
|
|
||||||
images = []
|
|
||||||
# Images either have a key for specifying resolution or a "size" for the image.
|
|
||||||
for image in metadata.art:
|
|
||||||
# Netradio.
|
|
||||||
if metadata.art[0].key is not None:
|
|
||||||
images.append(int(image.key.split("x")[0]))
|
|
||||||
# Everything else.
|
|
||||||
elif metadata.art[0].size is not None:
|
|
||||||
images.append(ART_SIZE_ENUM[image.size].value)
|
|
||||||
|
|
||||||
# Choose the largest image.
|
|
||||||
self._media_image = metadata.art[images.index(max(images))]
|
|
||||||
|
|
||||||
# Don't leave stale image metadata if there is no available artwork.
|
|
||||||
else:
|
|
||||||
self._media_image = Art()
|
|
||||||
|
|
||||||
async def _update_playback_metadata(self, data: PlaybackContentMetadata) -> None:
|
async def _update_playback_metadata(self, data: PlaybackContentMetadata) -> None:
|
||||||
"""Update _playback_metadata and related."""
|
"""Update _playback_metadata and related."""
|
||||||
self._playback_metadata = data
|
self._playback_metadata = data
|
||||||
|
|
||||||
# Update current artwork.
|
# Update current artwork.
|
||||||
self._update_artwork()
|
self._media_image = get_highest_resolution_artwork(self._playback_metadata)
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@@ -381,7 +333,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
async def _update_playback_progress(self, data: PlaybackProgress) -> None:
|
async def _update_playback_progress(self, data: PlaybackProgress) -> None:
|
||||||
"""Update _playback_progress and last update."""
|
"""Update _playback_progress and last update."""
|
||||||
self._playback_progress = data
|
self._playback_progress = data
|
||||||
self._last_update = utcnow()
|
self._attr_media_position_updated_at = utcnow()
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@@ -399,12 +351,35 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
"""Update _source_change and related."""
|
"""Update _source_change and related."""
|
||||||
self._source_change = data
|
self._source_change = data
|
||||||
|
|
||||||
|
# Check if source is line-in or optical and progress should be updated
|
||||||
|
if self._source_change.id in (SOURCE_ENUM.lineIn, SOURCE_ENUM.spdif):
|
||||||
|
self._playback_progress = PlaybackProgress(progress=0)
|
||||||
|
|
||||||
async def _update_volume(self, data: VolumeState) -> None:
|
async def _update_volume(self, data: VolumeState) -> None:
|
||||||
"""Update _volume."""
|
"""Update _volume."""
|
||||||
self._volume = data
|
self._volume = data
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def _update_device(self, data: SoftwareUpdateState) -> None:
|
||||||
|
"""Update HA device SW version."""
|
||||||
|
# Get software version.
|
||||||
|
software_status = await self._client.get_softwareupdate_status()
|
||||||
|
|
||||||
|
# Update the HA device if the sw version does not match
|
||||||
|
if not isinstance(self._device, DeviceEntry):
|
||||||
|
self._device = get_device(self.hass, self._unique_id)
|
||||||
|
|
||||||
|
assert isinstance(self._device, DeviceEntry)
|
||||||
|
|
||||||
|
if software_status.software_version != self._device.sw_version:
|
||||||
|
device_registry = dr.async_get(self.hass)
|
||||||
|
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device_id=self._device.id,
|
||||||
|
sw_version=software_status.software_version,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> MediaPlayerState:
|
def state(self) -> MediaPlayerState:
|
||||||
"""Return the current state of the media player."""
|
"""Return the current state of the media player."""
|
||||||
@@ -440,15 +415,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
@property
|
@property
|
||||||
def media_position(self) -> int | None:
|
def media_position(self) -> int | None:
|
||||||
"""Return the current playback progress."""
|
"""Return the current playback progress."""
|
||||||
# Don't show progress if the the device is a Beolink listener.
|
return self._playback_progress.progress
|
||||||
if self._remote_leader is None:
|
|
||||||
return self._playback_progress.progress
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_position_updated_at(self) -> datetime:
|
|
||||||
"""Return the last time that the playback position was updated."""
|
|
||||||
return self._last_update
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_image_url(self) -> str | None:
|
def media_image_url(self) -> str | None:
|
||||||
@@ -524,59 +491,19 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
|
|
||||||
return self._source_change.name
|
return self._source_change.name
|
||||||
|
|
||||||
@property
|
|
||||||
def shuffle(self) -> bool | None:
|
|
||||||
"""Return if queues should be shuffled."""
|
|
||||||
return self._queue_settings.shuffle
|
|
||||||
|
|
||||||
@property
|
|
||||||
def repeat(self) -> RepeatMode | None:
|
|
||||||
"""Return current repeat setting for queues."""
|
|
||||||
if self._queue_settings.repeat:
|
|
||||||
return cast(RepeatMode, REPEAT_ENUM(self._queue_settings.repeat).name)
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def async_turn_off(self) -> None:
|
async def async_turn_off(self) -> None:
|
||||||
"""Set the device to "networkStandby"."""
|
"""Set the device to "networkStandby"."""
|
||||||
self._client.post_standby(async_req=True)
|
await self._client.post_standby()
|
||||||
|
|
||||||
async def async_volume_up(self) -> None:
|
|
||||||
"""Volume up the on media player."""
|
|
||||||
if not self._volume.level or not self._volume.level.level:
|
|
||||||
_LOGGER.warning("Error setting volume")
|
|
||||||
return
|
|
||||||
|
|
||||||
new_volume = min(self._volume.level.level + self._volume_step, self._max_volume)
|
|
||||||
self._client.set_current_volume_level(
|
|
||||||
volume_level=VolumeLevel(level=new_volume),
|
|
||||||
async_req=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_volume_down(self) -> None:
|
|
||||||
"""Volume down the on media player."""
|
|
||||||
if not self._volume.level or not self._volume.level.level:
|
|
||||||
_LOGGER.warning("Error setting volume")
|
|
||||||
return
|
|
||||||
|
|
||||||
new_volume = max(self._volume.level.level - self._volume_step, 0)
|
|
||||||
self._client.set_current_volume_level(
|
|
||||||
volume_level=VolumeLevel(level=new_volume),
|
|
||||||
async_req=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_set_volume_level(self, volume: float) -> None:
|
async def async_set_volume_level(self, volume: float) -> None:
|
||||||
"""Set volume level, range 0..1."""
|
"""Set volume level, range 0..1."""
|
||||||
self._client.set_current_volume_level(
|
await self._client.set_current_volume_level(
|
||||||
volume_level=VolumeLevel(level=int(volume * 100)),
|
volume_level=VolumeLevel(level=int(volume * 100))
|
||||||
async_req=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_mute_volume(self, mute: bool) -> None:
|
async def async_mute_volume(self, mute: bool) -> None:
|
||||||
"""Mute or unmute media player."""
|
"""Mute or unmute media player."""
|
||||||
self._client.set_volume_mute(
|
await self._client.set_volume_mute(volume_mute=VolumeMute(muted=mute))
|
||||||
volume_mute=VolumeMute(muted=mute),
|
|
||||||
async_req=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_media_play_pause(self) -> None:
|
async def async_media_play_pause(self) -> None:
|
||||||
"""Toggle play/pause media player."""
|
"""Toggle play/pause media player."""
|
||||||
@@ -587,28 +514,26 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
|
|
||||||
async def async_media_pause(self) -> None:
|
async def async_media_pause(self) -> None:
|
||||||
"""Pause media player."""
|
"""Pause media player."""
|
||||||
self._client.post_playback_command(command="pause", async_req=True)
|
await self._client.post_playback_command(command="pause")
|
||||||
|
|
||||||
async def async_media_play(self) -> None:
|
async def async_media_play(self) -> None:
|
||||||
"""Play media player."""
|
"""Play media player."""
|
||||||
self._client.post_playback_command(command="play", async_req=True)
|
await self._client.post_playback_command(command="play")
|
||||||
|
|
||||||
async def async_media_stop(self) -> None:
|
async def async_media_stop(self) -> None:
|
||||||
"""Pause media player."""
|
"""Pause media player."""
|
||||||
self._client.post_playback_command(command="stop", async_req=True)
|
await self._client.post_playback_command(command="stop")
|
||||||
|
|
||||||
async def async_media_next_track(self) -> None:
|
async def async_media_next_track(self) -> None:
|
||||||
"""Send the next track command."""
|
"""Send the next track command."""
|
||||||
self._client.post_playback_command(command="skip", async_req=True)
|
await self._client.post_playback_command(command="skip")
|
||||||
|
|
||||||
async def async_media_seek(self, position: float) -> None:
|
async def async_media_seek(self, position: float) -> None:
|
||||||
"""Seek to position in ms."""
|
"""Seek to position in ms."""
|
||||||
if self.source == SOURCE_ENUM.deezer:
|
if self.source == SOURCE_ENUM.deezer:
|
||||||
self._client.seek_to_position(
|
await self._client.seek_to_position(position_ms=int(position * 1000))
|
||||||
position_ms=int(position * 1000), async_req=True
|
|
||||||
)
|
|
||||||
# Try to prevent the playback progress from bouncing in the UI.
|
# Try to prevent the playback progress from bouncing in the UI.
|
||||||
self._last_update = utcnow()
|
self._attr_media_position_updated_at = utcnow()
|
||||||
self._playback_progress = PlaybackProgress(progress=int(position))
|
self._playback_progress = PlaybackProgress(progress=int(position))
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
@@ -617,28 +542,11 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
|
|
||||||
async def async_media_previous_track(self) -> None:
|
async def async_media_previous_track(self) -> None:
|
||||||
"""Send the previous track command."""
|
"""Send the previous track command."""
|
||||||
self._client.post_playback_command(command="prev", async_req=True)
|
await self._client.post_playback_command(command="prev")
|
||||||
|
|
||||||
async def async_clear_playlist(self) -> None:
|
async def async_clear_playlist(self) -> None:
|
||||||
"""Clear the current playback queue."""
|
"""Clear the current playback queue."""
|
||||||
self._client.post_clear_queue(async_req=True)
|
await self._client.post_clear_queue()
|
||||||
|
|
||||||
async def async_set_shuffle(self, shuffle: bool) -> None:
|
|
||||||
"""Set playback queues to shuffle."""
|
|
||||||
self._client.set_settings_queue(
|
|
||||||
play_queue_settings=PlayQueueSettings(shuffle=shuffle),
|
|
||||||
async_req=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._queue_settings.shuffle = shuffle
|
|
||||||
|
|
||||||
async def async_set_repeat(self, repeat: RepeatMode) -> None:
|
|
||||||
"""Set playback queues to repeat."""
|
|
||||||
self._client.set_settings_queue(
|
|
||||||
play_queue_settings=PlayQueueSettings(repeat=REPEAT_ENUM[repeat]),
|
|
||||||
async_req=True,
|
|
||||||
)
|
|
||||||
self._queue_settings.repeat = REPEAT_ENUM[repeat]
|
|
||||||
|
|
||||||
async def async_select_source(self, source: str) -> None:
|
async def async_select_source(self, source: str) -> None:
|
||||||
"""Select an input source."""
|
"""Select an input source."""
|
||||||
@@ -656,10 +564,10 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
# Check for source type
|
# Check for source type
|
||||||
if source in self._audio_sources.values():
|
if source in self._audio_sources.values():
|
||||||
# Audio
|
# Audio
|
||||||
self._client.set_active_source(source_id=key, async_req=True)
|
await self._client.set_active_source(source_id=key)
|
||||||
else:
|
else:
|
||||||
# Video
|
# Video
|
||||||
self._client.post_remote_trigger(id=key, async_req=True)
|
await self._client.post_remote_trigger(id=key)
|
||||||
|
|
||||||
async def async_play_media(
|
async def async_play_media(
|
||||||
self,
|
self,
|
||||||
@@ -693,33 +601,31 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
media_id = media_id.replace(".m3u", "")
|
media_id = media_id.replace(".m3u", "")
|
||||||
|
|
||||||
if media_type in (MediaType.URL, MediaType.MUSIC):
|
if media_type in (MediaType.URL, MediaType.MUSIC):
|
||||||
self._client.post_uri_source(uri=Uri(location=media_id), async_req=True)
|
await self._client.post_uri_source(uri=Uri(location=media_id))
|
||||||
|
|
||||||
# The "provider" media_type may not be suitable for overlay all the time.
|
# The "provider" media_type may not be suitable for overlay all the time.
|
||||||
# Use it for now.
|
# Use it for now.
|
||||||
elif media_type == BANGOLUFSEN_MEDIA_TYPE.TTS:
|
elif media_type == BANGOLUFSEN_MEDIA_TYPE.TTS:
|
||||||
self._client.post_overlay_play(
|
await self._client.post_overlay_play(
|
||||||
overlay_play_request=OverlayPlayRequest(
|
overlay_play_request=OverlayPlayRequest(
|
||||||
uri=Uri(location=media_id),
|
uri=Uri(location=media_id),
|
||||||
),
|
)
|
||||||
async_req=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif media_type == BANGOLUFSEN_MEDIA_TYPE.RADIO:
|
elif media_type == BANGOLUFSEN_MEDIA_TYPE.RADIO:
|
||||||
self._client.run_provided_scene(
|
await self._client.run_provided_scene(
|
||||||
scene_properties=SceneProperties(
|
scene_properties=SceneProperties(
|
||||||
action_list=[
|
actionList=[
|
||||||
Action(
|
Action(
|
||||||
type="radio",
|
type="radio",
|
||||||
radio_station_id=media_id,
|
radioStationId=media_id,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
),
|
)
|
||||||
async_req=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif media_type == BANGOLUFSEN_MEDIA_TYPE.FAVOURITE:
|
elif media_type == BANGOLUFSEN_MEDIA_TYPE.FAVOURITE:
|
||||||
self._client.activate_preset(id=int(media_id), async_req=True)
|
await self._client.activate_preset(id=int(media_id))
|
||||||
|
|
||||||
elif media_type == BANGOLUFSEN_MEDIA_TYPE.DEEZER:
|
elif media_type == BANGOLUFSEN_MEDIA_TYPE.DEEZER:
|
||||||
try:
|
try:
|
||||||
@@ -730,8 +636,8 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
deezer_id = kwargs[ATTR_MEDIA_EXTRA]["id"]
|
deezer_id = kwargs[ATTR_MEDIA_EXTRA]["id"]
|
||||||
|
|
||||||
# Play Deezer flow.
|
# Play Deezer flow.
|
||||||
self._client.start_deezer_flow(
|
await self._client.start_deezer_flow(
|
||||||
user_flow=UserFlow(user_id=deezer_id), async_req=True
|
user_flow=UserFlow(userId=deezer_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Play a Deezer playlist or album.
|
# Play a Deezer playlist or album.
|
||||||
@@ -740,26 +646,24 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
|
|||||||
if "start_from" in kwargs[ATTR_MEDIA_EXTRA]:
|
if "start_from" in kwargs[ATTR_MEDIA_EXTRA]:
|
||||||
start_from = kwargs[ATTR_MEDIA_EXTRA]["start_from"]
|
start_from = kwargs[ATTR_MEDIA_EXTRA]["start_from"]
|
||||||
|
|
||||||
self._client.add_to_queue(
|
await self._client.add_to_queue(
|
||||||
play_queue_item=PlayQueueItem(
|
play_queue_item=PlayQueueItem(
|
||||||
provider=PlayQueueItemType(value="deezer"),
|
provider=PlayQueueItemType(value="deezer"),
|
||||||
start_now_from_position=start_from,
|
startNowFromPosition=start_from,
|
||||||
type="playlist",
|
type="playlist",
|
||||||
uri=media_id,
|
uri=media_id,
|
||||||
),
|
)
|
||||||
async_req=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Play a Deezer track.
|
# Play a Deezer track.
|
||||||
else:
|
else:
|
||||||
self._client.add_to_queue(
|
await self._client.add_to_queue(
|
||||||
play_queue_item=PlayQueueItem(
|
play_queue_item=PlayQueueItem(
|
||||||
provider=PlayQueueItemType(value="deezer"),
|
provider=PlayQueueItemType(value="deezer"),
|
||||||
start_now_from_position=0,
|
startNowFromPosition=0,
|
||||||
type="track",
|
type="track",
|
||||||
uri=media_id,
|
uri=media_id,
|
||||||
),
|
)
|
||||||
async_req=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
except ApiException as error:
|
except ApiException as error:
|
||||||
|
21
homeassistant/components/bangolufsen/util.py
Normal file
21
homeassistant/components/bangolufsen/util.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Various utilities for the Bang & Olufsen integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.device_registry import DeviceEntry
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
def get_device(hass: HomeAssistant | None, unique_id: str) -> DeviceEntry | None:
|
||||||
|
"""Get the device."""
|
||||||
|
if not isinstance(hass, HomeAssistant):
|
||||||
|
return None
|
||||||
|
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
device = cast(DeviceEntry, device_registry.async_get_device({(DOMAIN, unique_id)}))
|
||||||
|
return device
|
@@ -5,8 +5,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
from multiprocessing.pool import ApplyResult
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from mozart_api.models import (
|
from mozart_api.models import (
|
||||||
PlaybackContentMetadata,
|
PlaybackContentMetadata,
|
||||||
@@ -14,7 +12,6 @@ from mozart_api.models import (
|
|||||||
PlaybackProgress,
|
PlaybackProgress,
|
||||||
RenderingState,
|
RenderingState,
|
||||||
SoftwareUpdateState,
|
SoftwareUpdateState,
|
||||||
SoftwareUpdateStatus,
|
|
||||||
Source,
|
Source,
|
||||||
VolumeState,
|
VolumeState,
|
||||||
WebsocketNotificationTag,
|
WebsocketNotificationTag,
|
||||||
@@ -22,7 +19,6 @@ from mozart_api.models import (
|
|||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr
|
|
||||||
from homeassistant.helpers.device_registry import DeviceEntry
|
from homeassistant.helpers.device_registry import DeviceEntry
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
|
||||||
@@ -30,9 +26,9 @@ from .const import (
|
|||||||
BANGOLUFSEN_WEBSOCKET_EVENT,
|
BANGOLUFSEN_WEBSOCKET_EVENT,
|
||||||
CONNECTION_STATUS,
|
CONNECTION_STATUS,
|
||||||
WEBSOCKET_NOTIFICATION,
|
WEBSOCKET_NOTIFICATION,
|
||||||
BangOlufsenVariables,
|
|
||||||
get_device,
|
|
||||||
)
|
)
|
||||||
|
from .entity import BangOlufsenVariables
|
||||||
|
from .util import get_device
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -46,7 +42,6 @@ class BangOlufsenWebsocket(BangOlufsenVariables):
|
|||||||
BangOlufsenVariables.__init__(self, entry)
|
BangOlufsenVariables.__init__(self, entry)
|
||||||
|
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._device: DeviceEntry | None = None
|
|
||||||
|
|
||||||
# WebSocket callbacks
|
# WebSocket callbacks
|
||||||
self._client.get_on_connection(self.on_connection)
|
self._client.get_on_connection(self.on_connection)
|
||||||
@@ -165,27 +160,12 @@ class BangOlufsenWebsocket(BangOlufsenVariables):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def on_software_update_state(self, notification: SoftwareUpdateState) -> None:
|
def on_software_update_state(self, notification: SoftwareUpdateState) -> None:
|
||||||
"""Check device sw version."""
|
"""Send software_update_state dispatch."""
|
||||||
|
async_dispatcher_send(
|
||||||
# Get software version.
|
self.hass,
|
||||||
software_status = cast(
|
f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.SOFTWARE_UPDATE_STATE}",
|
||||||
ApplyResult[SoftwareUpdateStatus],
|
notification,
|
||||||
self._client.get_softwareupdate_status(async_req=True),
|
)
|
||||||
).get()
|
|
||||||
|
|
||||||
# Update the HA device if the sw version does not match
|
|
||||||
if not isinstance(self._device, DeviceEntry):
|
|
||||||
self._device = get_device(self.hass, self._unique_id)
|
|
||||||
|
|
||||||
assert isinstance(self._device, DeviceEntry)
|
|
||||||
|
|
||||||
if software_status.software_version != self._device.sw_version:
|
|
||||||
device_registry = dr.async_get(self.hass)
|
|
||||||
|
|
||||||
device_registry.async_update_device(
|
|
||||||
device_id=self._device.id,
|
|
||||||
sw_version=software_status.software_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_all_notifications_raw(self, notification: dict) -> None:
|
def on_all_notifications_raw(self, notification: dict) -> None:
|
||||||
"""Receive all notifications."""
|
"""Receive all notifications."""
|
||||||
|
@@ -1270,7 +1270,7 @@ motionblinds==0.6.18
|
|||||||
motioneye-client==0.3.14
|
motioneye-client==0.3.14
|
||||||
|
|
||||||
# homeassistant.components.bangolufsen
|
# homeassistant.components.bangolufsen
|
||||||
mozart-api==3.2.1.150.1
|
mozart-api==3.2.1.150.4
|
||||||
|
|
||||||
# homeassistant.components.mullvad
|
# homeassistant.components.mullvad
|
||||||
mullvad-api==1.0.0
|
mullvad-api==1.0.0
|
||||||
|
@@ -994,7 +994,7 @@ motionblinds==0.6.18
|
|||||||
motioneye-client==0.3.14
|
motioneye-client==0.3.14
|
||||||
|
|
||||||
# homeassistant.components.bangolufsen
|
# homeassistant.components.bangolufsen
|
||||||
mozart-api==3.2.1.150.1
|
mozart-api==3.2.1.150.4
|
||||||
|
|
||||||
# homeassistant.components.mullvad
|
# homeassistant.components.mullvad
|
||||||
mullvad-api==1.0.0
|
mullvad-api==1.0.0
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
"""Test fixtures for bangolufsen."""
|
"""Test fixtures for bangolufsen."""
|
||||||
|
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from mozart_api.models import BeolinkPeer, VolumeLevel, VolumeSettings
|
from mozart_api.models import BeolinkPeer, VolumeLevel, VolumeSettings
|
||||||
import pytest
|
import pytest
|
||||||
@@ -34,11 +34,11 @@ class MockMozartClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# API endpoints
|
# API endpoints
|
||||||
get_beolink_self = Mock()
|
get_beolink_self = AsyncMock()
|
||||||
get_beolink_self.return_value.get.return_value = get_beolink_self_result
|
get_beolink_self.return_value = get_beolink_self_result
|
||||||
|
|
||||||
get_volume_settings = Mock()
|
get_volume_settings = AsyncMock()
|
||||||
get_volume_settings.return_value.get.return_value = get_volume_settings_result
|
get_volume_settings.return_value = get_volume_settings_result
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
"""Constants used for testing the bangolufsen integration."""
|
"""Constants used for testing the bangolufsen integration."""
|
||||||
|
|
||||||
|
|
||||||
|
from ipaddress import IPv4Address
|
||||||
|
|
||||||
from homeassistant.components.bangolufsen.const import (
|
from homeassistant.components.bangolufsen.const import (
|
||||||
ATTR_FRIENDLY_NAME,
|
ATTR_FRIENDLY_NAME,
|
||||||
ATTR_ITEM_NUMBER,
|
ATTR_ITEM_NUMBER,
|
||||||
@@ -9,10 +11,9 @@ from homeassistant.components.bangolufsen.const import (
|
|||||||
CONF_BEOLINK_JID,
|
CONF_BEOLINK_JID,
|
||||||
CONF_DEFAULT_VOLUME,
|
CONF_DEFAULT_VOLUME,
|
||||||
CONF_MAX_VOLUME,
|
CONF_MAX_VOLUME,
|
||||||
CONF_VOLUME_STEP,
|
|
||||||
)
|
)
|
||||||
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
||||||
from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME
|
from homeassistant.const import CONF_HOST, CONF_MODEL
|
||||||
|
|
||||||
TEST_HOST = "192.168.0.1"
|
TEST_HOST = "192.168.0.1"
|
||||||
TEST_HOST_INVALID = "192.168.0"
|
TEST_HOST_INVALID = "192.168.0"
|
||||||
@@ -48,14 +49,12 @@ TEST_DATA_USER = {CONF_HOST: TEST_HOST, CONF_MODEL: TEST_MODEL_BALANCE}
|
|||||||
TEST_DATA_USER_INVALID = {CONF_HOST: TEST_HOST_INVALID, CONF_MODEL: TEST_MODEL_BALANCE}
|
TEST_DATA_USER_INVALID = {CONF_HOST: TEST_HOST_INVALID, CONF_MODEL: TEST_MODEL_BALANCE}
|
||||||
|
|
||||||
TEST_DATA_NO_HOST = {
|
TEST_DATA_NO_HOST = {
|
||||||
CONF_VOLUME_STEP: TEST_VOLUME_STEP,
|
|
||||||
CONF_DEFAULT_VOLUME: TEST_DEFAULT_VOLUME,
|
CONF_DEFAULT_VOLUME: TEST_DEFAULT_VOLUME,
|
||||||
CONF_MAX_VOLUME: TEST_MAX_VOLUME,
|
CONF_MAX_VOLUME: TEST_MAX_VOLUME,
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_DATA_CONFIRM = {
|
TEST_DATA_CONFIRM = {
|
||||||
CONF_HOST: TEST_HOST,
|
CONF_HOST: TEST_HOST,
|
||||||
CONF_VOLUME_STEP: TEST_VOLUME_STEP,
|
|
||||||
CONF_DEFAULT_VOLUME: TEST_DEFAULT_VOLUME,
|
CONF_DEFAULT_VOLUME: TEST_DEFAULT_VOLUME,
|
||||||
CONF_MAX_VOLUME: TEST_MAX_VOLUME,
|
CONF_MAX_VOLUME: TEST_MAX_VOLUME,
|
||||||
CONF_MODEL: TEST_MODEL_BALANCE,
|
CONF_MODEL: TEST_MODEL_BALANCE,
|
||||||
@@ -63,8 +62,8 @@ TEST_DATA_CONFIRM = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST_DATA_ZEROCONF = ZeroconfServiceInfo(
|
TEST_DATA_ZEROCONF = ZeroconfServiceInfo(
|
||||||
addresses=[TEST_HOST],
|
ip_address=IPv4Address(TEST_HOST),
|
||||||
host=TEST_HOST,
|
ip_addresses=[IPv4Address(TEST_HOST)],
|
||||||
port=80,
|
port=80,
|
||||||
hostname=TEST_HOSTNAME_ZEROCONF,
|
hostname=TEST_HOSTNAME_ZEROCONF,
|
||||||
type=TEST_TYPE_ZEROCONF,
|
type=TEST_TYPE_ZEROCONF,
|
||||||
@@ -78,26 +77,11 @@ TEST_DATA_ZEROCONF = ZeroconfServiceInfo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
TEST_DATA_ZEROCONF_NOT_MOZART = ZeroconfServiceInfo(
|
TEST_DATA_ZEROCONF_NOT_MOZART = ZeroconfServiceInfo(
|
||||||
addresses=[TEST_HOST],
|
ip_address=IPv4Address(TEST_HOST),
|
||||||
host=TEST_HOST,
|
ip_addresses=[IPv4Address(TEST_HOST)],
|
||||||
port=80,
|
port=80,
|
||||||
hostname=TEST_HOSTNAME_ZEROCONF,
|
hostname=TEST_HOSTNAME_ZEROCONF,
|
||||||
type=TEST_TYPE_ZEROCONF,
|
type=TEST_TYPE_ZEROCONF,
|
||||||
name=TEST_NAME_ZEROCONF,
|
name=TEST_NAME_ZEROCONF,
|
||||||
properties={ATTR_SERIAL_NUMBER: TEST_SERIAL_NUMBER},
|
properties={ATTR_SERIAL_NUMBER: TEST_SERIAL_NUMBER},
|
||||||
)
|
)
|
||||||
|
|
||||||
TEST_DATA_OPTIONS = {
|
|
||||||
CONF_NAME: TEST_NAME_OPTIONS,
|
|
||||||
CONF_VOLUME_STEP: TEST_VOLUME_STEP_OPTIONS,
|
|
||||||
CONF_DEFAULT_VOLUME: TEST_DEFAULT_VOLUME_OPTIONS,
|
|
||||||
CONF_MAX_VOLUME: TEST_MAX_VOLUME_OPTIONS,
|
|
||||||
}
|
|
||||||
TEST_DATA_OPTIONS_FULL = {
|
|
||||||
CONF_HOST: TEST_HOST,
|
|
||||||
CONF_VOLUME_STEP: TEST_VOLUME_STEP_OPTIONS,
|
|
||||||
CONF_DEFAULT_VOLUME: TEST_DEFAULT_VOLUME_OPTIONS,
|
|
||||||
CONF_MAX_VOLUME: TEST_MAX_VOLUME_OPTIONS,
|
|
||||||
CONF_MODEL: TEST_MODEL_BALANCE,
|
|
||||||
CONF_BEOLINK_JID: TEST_JID_1,
|
|
||||||
}
|
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
"""Test the bangolufsen config_flow."""
|
"""Test the bangolufsen config_flow."""
|
||||||
|
|
||||||
|
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from mozart_api.exceptions import ApiException, NotFoundException
|
from mozart_api.exceptions import ApiException, NotFoundException
|
||||||
import pytest
|
import pytest
|
||||||
from urllib3.exceptions import MaxRetryError, NewConnectionError
|
from urllib3.exceptions import MaxRetryError, NewConnectionError
|
||||||
@@ -14,7 +16,6 @@ from homeassistant.data_entry_flow import FlowResultType
|
|||||||
from .conftest import MockMozartClient
|
from .conftest import MockMozartClient
|
||||||
from .const import (
|
from .const import (
|
||||||
TEST_DATA_CONFIRM,
|
TEST_DATA_CONFIRM,
|
||||||
TEST_DATA_OPTIONS,
|
|
||||||
TEST_DATA_USER,
|
TEST_DATA_USER,
|
||||||
TEST_DATA_USER_INVALID,
|
TEST_DATA_USER_INVALID,
|
||||||
TEST_DATA_ZEROCONF,
|
TEST_DATA_ZEROCONF,
|
||||||
@@ -28,7 +29,7 @@ async def test_config_flow_max_retry_error(
|
|||||||
hass: HomeAssistant, mock_client: MockMozartClient
|
hass: HomeAssistant, mock_client: MockMozartClient
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we handle not_mozart_device."""
|
"""Test we handle not_mozart_device."""
|
||||||
mock_client.get_beolink_self.side_effect = MaxRetryError(pool=None, url=None)
|
mock_client.get_beolink_self.side_effect = MaxRetryError(pool=Mock(), url="")
|
||||||
|
|
||||||
result_user = await hass.config_entries.flow.async_init(
|
result_user = await hass.config_entries.flow.async_init(
|
||||||
handler=DOMAIN,
|
handler=DOMAIN,
|
||||||
@@ -64,9 +65,7 @@ async def test_config_flow_new_connection_error(
|
|||||||
hass: HomeAssistant, mock_client: MockMozartClient
|
hass: HomeAssistant, mock_client: MockMozartClient
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we handle new_connection_error."""
|
"""Test we handle new_connection_error."""
|
||||||
mock_client.get_beolink_self.side_effect = NewConnectionError(
|
mock_client.get_beolink_self.side_effect = NewConnectionError(Mock(), "")
|
||||||
pool=None, message=None
|
|
||||||
)
|
|
||||||
|
|
||||||
result_user = await hass.config_entries.flow.async_init(
|
result_user = await hass.config_entries.flow.async_init(
|
||||||
handler=DOMAIN,
|
handler=DOMAIN,
|
||||||
@@ -184,26 +183,26 @@ async def test_config_flow_zeroconf_not_mozart_device(hass: HomeAssistant) -> No
|
|||||||
assert result_user["reason"] == "not_mozart_device"
|
assert result_user["reason"] == "not_mozart_device"
|
||||||
|
|
||||||
|
|
||||||
async def test_config_flow_options(hass: HomeAssistant, mock_config_entry) -> None:
|
# async def test_config_flow_options(hass: HomeAssistant, mock_config_entry) -> None:
|
||||||
"""Test config flow options."""
|
# """Test config flow options."""
|
||||||
|
|
||||||
mock_config_entry.add_to_hass(hass)
|
# mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
# assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
|
||||||
result_user = await hass.config_entries.options.async_init(
|
# result_user = await hass.config_entries.options.async_init(
|
||||||
mock_config_entry.entry_id
|
# mock_config_entry.entry_id
|
||||||
)
|
# )
|
||||||
|
|
||||||
assert result_user["type"] == FlowResultType.FORM
|
# assert result_user["type"] == FlowResultType.FORM
|
||||||
assert result_user["step_id"] == "init"
|
# assert result_user["step_id"] == "init"
|
||||||
|
|
||||||
result_confirm = await hass.config_entries.options.async_configure(
|
# result_confirm = await hass.config_entries.options.async_configure(
|
||||||
flow_id=result_user["flow_id"],
|
# flow_id=result_user["flow_id"],
|
||||||
user_input=TEST_DATA_OPTIONS,
|
# user_input=TEST_DATA_OPTIONS,
|
||||||
)
|
# )
|
||||||
|
|
||||||
assert result_confirm["type"] == FlowResultType.CREATE_ENTRY
|
# assert result_confirm["type"] == FlowResultType.CREATE_ENTRY
|
||||||
new_data = TEST_DATA_CONFIRM
|
# new_data = TEST_DATA_CONFIRM
|
||||||
new_data.update(TEST_DATA_OPTIONS)
|
# new_data.update(TEST_DATA_OPTIONS)
|
||||||
assert result_confirm["data"] == new_data
|
# assert result_confirm["data"] == new_data
|
||||||
|
Reference in New Issue
Block a user