mirror of
https://github.com/home-assistant/core.git
synced 2026-02-15 07:36:16 +00:00
Add battery support to Bang & Olufsen (#159994)
This commit is contained in:
@@ -34,7 +34,7 @@ class BeoData:
|
||||
|
||||
type BeoConfigEntry = ConfigEntry[BeoData]
|
||||
|
||||
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
|
||||
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
|
||||
|
||||
@@ -115,6 +115,7 @@ class WebsocketNotification(StrEnum):
|
||||
"""Enum for WebSocket notification types."""
|
||||
|
||||
ACTIVE_LISTENING_MODE = "active_listening_mode"
|
||||
BATTERY = "battery"
|
||||
BEO_REMOTE_BUTTON = "beo_remote_button"
|
||||
BUTTON = "button"
|
||||
PLAYBACK_ERROR = "playback_error"
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN
|
||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import CONF_MODEL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -55,6 +56,19 @@ async def async_get_config_entry_diagnostics(
|
||||
|
||||
# Get remotes
|
||||
for remote in await get_remotes(config_entry.runtime_data.client):
|
||||
# Get Battery Sensor states
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{remote.serial_number}_{config_entry.unique_id}_remote_battery_level",
|
||||
):
|
||||
if state := hass.states.get(entity_id):
|
||||
state_dict = dict(state.as_dict())
|
||||
|
||||
# Remove context as it is not relevant
|
||||
state_dict.pop("context")
|
||||
data[f"remote_{remote.serial_number}_battery_level"] = state_dict
|
||||
|
||||
# Get key Event entity states (if enabled)
|
||||
for key_type in get_remote_keys():
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
@@ -72,4 +86,15 @@ async def async_get_config_entry_diagnostics(
|
||||
# Add remote Mozart model
|
||||
data[f"remote_{remote.serial_number}"] = dict(remote)
|
||||
|
||||
# Get Mozart battery entity
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, f"{config_entry.unique_id}_battery_level"
|
||||
):
|
||||
if state := hass.states.get(entity_id):
|
||||
state_dict = dict(state.as_dict())
|
||||
|
||||
# Remove context as it is not relevant
|
||||
state_dict.pop("context")
|
||||
data["battery_level"] = state_dict
|
||||
|
||||
return data
|
||||
|
||||
139
homeassistant/components/bang_olufsen/sensor.py
Normal file
139
homeassistant/components/bang_olufsen/sensor.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Sensor entities for the Bang & Olufsen integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from datetime import timedelta
|
||||
|
||||
from aiohttp import ClientConnectorError
|
||||
from mozart_api.exceptions import ApiException
|
||||
from mozart_api.models import BatteryState, PairedRemote
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BeoConfigEntry
|
||||
from .const import CONNECTION_STATUS, DOMAIN, WebsocketNotification
|
||||
from .entity import BeoEntity
|
||||
from .util import get_remotes, supports_battery
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=15)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: BeoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Sensor entities from config entry."""
|
||||
entities: list[BeoSensor] = []
|
||||
|
||||
# Check for Mozart device with battery
|
||||
if await supports_battery(config_entry.runtime_data.client):
|
||||
entities.append(BeoSensorBatteryLevel(config_entry))
|
||||
|
||||
# Add any Beoremote One remotes
|
||||
entities.extend(
|
||||
[
|
||||
BeoSensorRemoteBatteryLevel(config_entry, remote)
|
||||
for remote in (await get_remotes(config_entry.runtime_data.client))
|
||||
]
|
||||
)
|
||||
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
||||
class BeoSensor(SensorEntity, BeoEntity):
|
||||
"""Base Bang & Olufsen Sensor."""
|
||||
|
||||
def __init__(self, config_entry: BeoConfigEntry) -> None:
|
||||
"""Initialize Sensor."""
|
||||
super().__init__(config_entry, config_entry.runtime_data.client)
|
||||
|
||||
|
||||
class BeoSensorBatteryLevel(BeoSensor):
|
||||
"""Battery level Sensor for Mozart devices."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.BATTERY
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, config_entry: BeoConfigEntry) -> None:
|
||||
"""Init the battery level Sensor."""
|
||||
super().__init__(config_entry)
|
||||
|
||||
self._attr_unique_id = f"{self._unique_id}_battery_level"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Turn on the dispatchers."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
|
||||
self._async_update_connection_state,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
|
||||
self._update_battery,
|
||||
)
|
||||
)
|
||||
|
||||
async def _update_battery(self, data: BatteryState) -> None:
|
||||
"""Update sensor value."""
|
||||
self._attr_native_value = data.battery_level
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class BeoSensorRemoteBatteryLevel(BeoSensor):
|
||||
"""Battery level Sensor for the Beoremote One."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.BATTERY
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_should_poll = True
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, config_entry: BeoConfigEntry, remote: PairedRemote) -> None:
|
||||
"""Init the battery level Sensor."""
|
||||
super().__init__(config_entry)
|
||||
# Serial number is not None, as the remote object is provided by get_remotes
|
||||
assert remote.serial_number
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{remote.serial_number}_{self._unique_id}_remote_battery_level"
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")}
|
||||
)
|
||||
self._attr_native_value = remote.battery_level
|
||||
self._remote = remote
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Turn on the dispatchers."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
|
||||
self._async_update_connection_state,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Poll battery status."""
|
||||
with contextlib.suppress(ApiException, ClientConnectorError, TimeoutError):
|
||||
for remote in await get_remotes(self._client):
|
||||
if remote.serial_number == self._remote.serial_number:
|
||||
self._attr_native_value = remote.battery_level
|
||||
break
|
||||
@@ -84,3 +84,10 @@ def get_remote_keys() -> list[str]:
|
||||
for key_type in (*BEO_REMOTE_KEYS, *BEO_REMOTE_CONTROL_KEYS)
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
async def supports_battery(client: MozartClient) -> bool:
|
||||
"""Get if a Mozart device has a battery."""
|
||||
battery_state = await client.get_battery_state()
|
||||
|
||||
return battery_state.state != "BatteryNotPresent"
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from mozart_api.models import (
|
||||
BatteryState,
|
||||
BeoRemoteButton,
|
||||
ButtonEvent,
|
||||
ListeningModeProps,
|
||||
@@ -60,6 +61,7 @@ class BeoWebsocket(BeoBase):
|
||||
self._client.get_active_listening_mode_notifications(
|
||||
self.on_active_listening_mode
|
||||
)
|
||||
self._client.get_battery_notifications(self.on_battery_notification)
|
||||
self._client.get_beo_remote_button_notifications(
|
||||
self.on_beo_remote_button_notification
|
||||
)
|
||||
@@ -115,6 +117,14 @@ class BeoWebsocket(BeoBase):
|
||||
notification,
|
||||
)
|
||||
|
||||
def on_battery_notification(self, notification: BatteryState) -> None:
|
||||
"""Send battery dispatch."""
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
|
||||
notification,
|
||||
)
|
||||
|
||||
def on_beo_remote_button_notification(self, notification: BeoRemoteButton) -> None:
|
||||
"""Send beo_remote_button dispatch."""
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from mozart_api.models import (
|
||||
Action,
|
||||
BatteryState,
|
||||
BeolinkPeer,
|
||||
BeolinkSelf,
|
||||
ContentItem,
|
||||
@@ -34,6 +35,7 @@ from homeassistant.components.bang_olufsen.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import (
|
||||
TEST_BATTERY,
|
||||
TEST_DATA_CREATE_ENTRY,
|
||||
TEST_DATA_CREATE_ENTRY_2,
|
||||
TEST_DATA_CREATE_ENTRY_3,
|
||||
@@ -125,6 +127,7 @@ async def mock_websocket_connection(
|
||||
playback_metadata_callback = (
|
||||
mock_mozart_client.get_playback_metadata_notifications.call_args[0][0]
|
||||
)
|
||||
battery_callback = mock_mozart_client.get_battery_notifications.call_args[0][0]
|
||||
|
||||
# Trigger callbacks. Try to use existing data
|
||||
volume_callback(mock_mozart_client.get_product_state.return_value.volume)
|
||||
@@ -137,6 +140,10 @@ async def mock_websocket_connection(
|
||||
playback_metadata_callback(
|
||||
mock_mozart_client.get_product_state.return_value.playback.metadata
|
||||
)
|
||||
|
||||
# This should not affect non-battery devices.
|
||||
battery_callback(TEST_BATTERY)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@@ -403,6 +410,14 @@ def mock_mozart_client() -> Generator[AsyncMock]:
|
||||
)
|
||||
]
|
||||
)
|
||||
client.get_battery_state = AsyncMock()
|
||||
client.get_battery_state.return_value = BatteryState(
|
||||
battery_level=0,
|
||||
is_charging=False,
|
||||
remaining_charging_time_minutes=0,
|
||||
remaining_playing_time_minutes=0,
|
||||
state="BatteryNotPresent",
|
||||
)
|
||||
client.post_standby = AsyncMock()
|
||||
client.set_current_volume_level = AsyncMock()
|
||||
client.set_volume_mute = AsyncMock()
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import Mock
|
||||
from mozart_api.exceptions import ApiException
|
||||
from mozart_api.models import (
|
||||
Action,
|
||||
BatteryState,
|
||||
ListeningModeRef,
|
||||
OverlayPlayRequest,
|
||||
OverlayPlayRequestTextToSpeechTextToSpeech,
|
||||
@@ -71,14 +72,18 @@ TEST_NAME_4 = f"{TEST_MODEL_A5}-{TEST_SERIAL_NUMBER_4}"
|
||||
TEST_JID_4 = f"{TEST_TYPE_NUMBER}.{TEST_ITEM_NUMBER}.{TEST_SERIAL_NUMBER_4}@products.bang-olufsen.com"
|
||||
TEST_MEDIA_PLAYER_ENTITY_ID_4 = f"media_player.beosound_a5_{TEST_SERIAL_NUMBER_4}"
|
||||
TEST_HOST_4 = "192.168.0.4"
|
||||
TEST_BATTERY_A5_SENSOR_ENTITY_ID = f"sensor.beosound_a5_{TEST_SERIAL_NUMBER_4}_battery"
|
||||
|
||||
# Beoremote One
|
||||
TEST_REMOTE_SERIAL = "55555555"
|
||||
TEST_REMOTE_SERIAL_PAIRED = f"{TEST_REMOTE_SERIAL}_{TEST_SERIAL_NUMBER}"
|
||||
TEST_REMOTE_SW_VERSION = "1.0.0"
|
||||
|
||||
TEST_BUTTON_EVENT_ENTITY_ID = "event.beosound_balance_11111111_play_pause"
|
||||
TEST_REMOTE_KEY_EVENT_ENTITY_ID = "event.beoremote_one_55555555_11111111_control_play"
|
||||
TEST_REMOTE_BATTERY_LEVEL_SENSOR_ENTITY_ID = (
|
||||
"sensor.beoremote_one_55555555_11111111_battery"
|
||||
)
|
||||
TEST_BUTTON_EVENT_ENTITY_ID = "event.beosound_balance_11111111_play_pause"
|
||||
|
||||
TEST_HOSTNAME_ZEROCONF = TEST_NAME.replace(" ", "-") + ".local."
|
||||
TEST_TYPE_ZEROCONF = "_bangolufsen._tcp.local."
|
||||
@@ -255,3 +260,10 @@ TEST_SOUND_MODES = [
|
||||
TEST_ACTIVE_SOUND_MODE_NAME_2,
|
||||
f"{TEST_SOUND_MODE_NAME} 2 (345)",
|
||||
]
|
||||
TEST_BATTERY = BatteryState(
|
||||
battery_level=5,
|
||||
is_charging=False,
|
||||
remaining_charging_time_minutes=0,
|
||||
remaining_playing_time_minutes=0,
|
||||
state="BatteryVeryLow",
|
||||
)
|
||||
|
||||
@@ -106,6 +106,117 @@
|
||||
'entity_id': 'event.beoremote_one_55555555_11111111_control_play',
|
||||
'state': 'unknown',
|
||||
}),
|
||||
'remote_55555555_battery_level': dict({
|
||||
'attributes': dict({
|
||||
'device_class': 'battery',
|
||||
'friendly_name': 'Beoremote One-55555555-11111111 Battery',
|
||||
'state_class': 'measurement',
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'entity_id': 'sensor.beoremote_one_55555555_11111111_battery',
|
||||
'state': '50',
|
||||
}),
|
||||
'websocket_connected': False,
|
||||
})
|
||||
# ---
|
||||
# name: test_async_get_config_entry_diagnostics_with_battery
|
||||
dict({
|
||||
'battery_level': dict({
|
||||
'attributes': dict({
|
||||
'device_class': 'battery',
|
||||
'friendly_name': 'Living room Balance Battery',
|
||||
'state_class': 'measurement',
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'entity_id': 'sensor.beosound_a5_44444444_battery',
|
||||
'state': '5',
|
||||
}),
|
||||
'config_entry': dict({
|
||||
'data': dict({
|
||||
'host': '192.168.0.4',
|
||||
'jid': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
'model': 'Beosound A5',
|
||||
'name': 'Beosound A5-44444444',
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'discovery_keys': dict({
|
||||
}),
|
||||
'domain': 'bang_olufsen',
|
||||
'minor_version': 1,
|
||||
'options': dict({
|
||||
}),
|
||||
'pref_disable_new_entities': False,
|
||||
'pref_disable_polling': False,
|
||||
'source': 'user',
|
||||
'subentries': list([
|
||||
]),
|
||||
'title': 'Beosound A5-44444444',
|
||||
'unique_id': '44444444',
|
||||
'version': 1,
|
||||
}),
|
||||
'media_player': dict({
|
||||
'attributes': dict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
'Living room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
}),
|
||||
'device_class': 'speaker',
|
||||
'entity_picture_local': None,
|
||||
'friendly_name': 'Living room Balance',
|
||||
'group_members': list([
|
||||
'media_player.beosound_a5_44444444',
|
||||
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'media_player.beosound_a5_44444444',
|
||||
]),
|
||||
'media_content_type': 'music',
|
||||
'repeat': 'off',
|
||||
'shuffle': False,
|
||||
'sound_mode': 'Test Listening Mode (123)',
|
||||
'sound_mode_list': list([
|
||||
'Test Listening Mode (123)',
|
||||
'Test Listening Mode (234)',
|
||||
'Test Listening Mode 2 (345)',
|
||||
]),
|
||||
'source_list': list([
|
||||
'Tidal',
|
||||
'Line-In',
|
||||
'HDMI A',
|
||||
]),
|
||||
'supported_features': 2095933,
|
||||
}),
|
||||
'entity_id': 'media_player.beosound_a5_44444444',
|
||||
'state': 'playing',
|
||||
}),
|
||||
'remote_55555555': dict({
|
||||
'address': '',
|
||||
'app_version': '1.0.0',
|
||||
'battery_level': 50,
|
||||
'connected': True,
|
||||
'db_version': None,
|
||||
'last_seen': None,
|
||||
'name': 'BEORC',
|
||||
'serial_number': '55555555',
|
||||
'updated': None,
|
||||
}),
|
||||
'remote_55555555_battery_level': dict({
|
||||
'attributes': dict({
|
||||
'device_class': 'battery',
|
||||
'friendly_name': 'Beoremote One-55555555-44444444 Battery',
|
||||
'state_class': 'measurement',
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'entity_id': 'sensor.beoremote_one_55555555_44444444_battery',
|
||||
'state': '50',
|
||||
}),
|
||||
'websocket_connected': False,
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
'event.beoremote_one_55555555_44444444_control_function_25',
|
||||
'event.beoremote_one_55555555_44444444_control_function_26',
|
||||
'event.beoremote_one_55555555_44444444_control_function_27',
|
||||
'sensor.beosound_a5_44444444_battery',
|
||||
'sensor.beoremote_one_55555555_44444444_battery',
|
||||
'media_player.beosound_a5_44444444',
|
||||
])
|
||||
# ---
|
||||
@@ -205,6 +207,7 @@
|
||||
'event.beoremote_one_55555555_11111111_control_function_25',
|
||||
'event.beoremote_one_55555555_11111111_control_function_26',
|
||||
'event.beoremote_one_55555555_11111111_control_function_27',
|
||||
'sensor.beoremote_one_55555555_11111111_battery',
|
||||
'media_player.beosound_balance_11111111',
|
||||
])
|
||||
# ---
|
||||
@@ -308,6 +311,7 @@
|
||||
'event.beoremote_one_55555555_33333333_control_function_25',
|
||||
'event.beoremote_one_55555555_33333333_control_function_26',
|
||||
'event.beoremote_one_55555555_33333333_control_function_27',
|
||||
'sensor.beoremote_one_55555555_33333333_battery',
|
||||
'media_player.beosound_premiere_33333333',
|
||||
])
|
||||
# ---
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
'event.beoremote_one_55555555_11111111_control_function_25',
|
||||
'event.beoremote_one_55555555_11111111_control_function_26',
|
||||
'event.beoremote_one_55555555_11111111_control_function_27',
|
||||
'sensor.beoremote_one_55555555_11111111_battery',
|
||||
'media_player.beosound_balance_11111111',
|
||||
])
|
||||
# ---
|
||||
@@ -206,6 +207,7 @@
|
||||
'event.beoremote_one_55555555_11111111_control_function_25',
|
||||
'event.beoremote_one_55555555_11111111_control_function_26',
|
||||
'event.beoremote_one_55555555_11111111_control_function_27',
|
||||
'sensor.beoremote_one_55555555_11111111_battery',
|
||||
'media_player.beosound_balance_11111111',
|
||||
'event.beoremote_one_66666666_11111111_light_blue',
|
||||
'event.beoremote_one_66666666_11111111_light_digit_0',
|
||||
@@ -297,6 +299,7 @@
|
||||
'event.beoremote_one_66666666_11111111_control_function_25',
|
||||
'event.beoremote_one_66666666_11111111_control_function_26',
|
||||
'event.beoremote_one_66666666_11111111_control_function_27',
|
||||
'sensor.beoremote_one_66666666_11111111_battery',
|
||||
])
|
||||
# ---
|
||||
# name: test_on_remote_control_unpaired
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test bang_olufsen config entry diagnostics."""
|
||||
|
||||
from mozart_api.models import BatteryState
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from syrupy.filters import props
|
||||
|
||||
@@ -51,3 +52,39 @@ async def test_async_get_config_entry_diagnostics(
|
||||
"modified_at",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def test_async_get_config_entry_diagnostics_with_battery(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: EntityRegistry,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_config_entry_a5: MockConfigEntry,
|
||||
mock_mozart_client: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test config entry diagnostics for devices with a battery."""
|
||||
mock_mozart_client.get_battery_state.return_value = BatteryState(
|
||||
battery_level=1, state="BatteryVeryLow"
|
||||
)
|
||||
|
||||
# Load entry
|
||||
mock_config_entry_a5.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry_a5.entry_id)
|
||||
await mock_websocket_connection(hass, mock_mozart_client)
|
||||
|
||||
result = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, mock_config_entry_a5
|
||||
)
|
||||
|
||||
assert result == snapshot(
|
||||
exclude=props(
|
||||
"created_at",
|
||||
"entry_id",
|
||||
"id",
|
||||
"last_changed",
|
||||
"last_reported",
|
||||
"last_updated",
|
||||
"media_position_updated_at",
|
||||
"modified_at",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
|
||||
from .conftest import mock_websocket_connection
|
||||
from .const import (
|
||||
TEST_BATTERY,
|
||||
TEST_BUTTON_EVENT_ENTITY_ID,
|
||||
TEST_REMOTE_KEY_EVENT_ENTITY_ID,
|
||||
TEST_SERIAL_NUMBER_3,
|
||||
@@ -130,6 +131,7 @@ async def test_button_event_creation_a5(
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test Microphone button event entity is not created when using a Beosound A5."""
|
||||
mock_mozart_client.get_battery_state.return_value = TEST_BATTERY
|
||||
|
||||
await _check_button_event_creation(
|
||||
hass,
|
||||
|
||||
80
tests/components/bang_olufsen/test_sensor.py
Normal file
80
tests/components/bang_olufsen/test_sensor.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Test the bang_olufsen sensor entities."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from mozart_api.models import PairedRemote, PairedRemoteResponse
|
||||
|
||||
from homeassistant.components.bang_olufsen.sensor import SCAN_INTERVAL
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import mock_websocket_connection
|
||||
from .const import (
|
||||
TEST_BATTERY,
|
||||
TEST_BATTERY_A5_SENSOR_ENTITY_ID,
|
||||
TEST_REMOTE_BATTERY_LEVEL_SENSOR_ENTITY_ID,
|
||||
TEST_REMOTE_SERIAL,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_battery_level(
|
||||
hass: HomeAssistant,
|
||||
mock_mozart_client: AsyncMock,
|
||||
mock_config_entry_a5: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the battery level entity."""
|
||||
# Ensure battery entities are created
|
||||
mock_mozart_client.get_battery_state.return_value = TEST_BATTERY
|
||||
|
||||
# Load entry
|
||||
mock_config_entry_a5.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry_a5.entry_id)
|
||||
# Deliberately avoid triggering a battery notification
|
||||
|
||||
assert (states := hass.states.get(TEST_BATTERY_A5_SENSOR_ENTITY_ID))
|
||||
assert states.state is STATE_UNKNOWN
|
||||
|
||||
# Check sensor reacts as expected to WebSocket events
|
||||
await mock_websocket_connection(hass, mock_mozart_client)
|
||||
|
||||
assert (states := hass.states.get(TEST_BATTERY_A5_SENSOR_ENTITY_ID))
|
||||
assert states.state == str(TEST_BATTERY.battery_level)
|
||||
|
||||
|
||||
async def test_remote_battery_level(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
integration: None,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_mozart_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the remote battery level entity."""
|
||||
|
||||
# Check the default value is set
|
||||
assert (states := hass.states.get(TEST_REMOTE_BATTERY_LEVEL_SENSOR_ENTITY_ID))
|
||||
assert states.state == "50"
|
||||
|
||||
# Change battery level
|
||||
mock_mozart_client.get_bluetooth_remotes.return_value = PairedRemoteResponse(
|
||||
items=[
|
||||
PairedRemote(
|
||||
address="",
|
||||
app_version="1.0.0",
|
||||
battery_level=45,
|
||||
connected=True,
|
||||
serial_number=TEST_REMOTE_SERIAL,
|
||||
name="BEORC",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# Trigger poll update
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (states := hass.states.get(TEST_REMOTE_BATTERY_LEVEL_SENSOR_ENTITY_ID))
|
||||
assert states.state == "45"
|
||||
@@ -130,7 +130,7 @@ async def test_on_remote_control_already_added(
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
# Check device and API call count
|
||||
assert mock_mozart_client.get_bluetooth_remotes.call_count == 1
|
||||
assert mock_mozart_client.get_bluetooth_remotes.call_count == 3
|
||||
assert device_registry.async_get_device({(DOMAIN, TEST_REMOTE_SERIAL_PAIRED)})
|
||||
|
||||
# Check number of entities (remote and button events and media_player)
|
||||
@@ -149,7 +149,7 @@ async def test_on_remote_control_already_added(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check device and API call count (triggered once by the WebSocket notification)
|
||||
assert mock_mozart_client.get_bluetooth_remotes.call_count == 2
|
||||
assert mock_mozart_client.get_bluetooth_remotes.call_count == 4
|
||||
assert device_registry.async_get_device({(DOMAIN, TEST_REMOTE_SERIAL_PAIRED)})
|
||||
|
||||
# Check number of entities (remote and button events and media_player)
|
||||
@@ -176,7 +176,7 @@ async def test_on_remote_control_paired(
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
# Check device and API call count
|
||||
assert mock_mozart_client.get_bluetooth_remotes.call_count == 1
|
||||
assert mock_mozart_client.get_bluetooth_remotes.call_count == 3
|
||||
assert device_registry.async_get_device({(DOMAIN, TEST_REMOTE_SERIAL_PAIRED)})
|
||||
|
||||
# Check number of entities (button and remote events and media_player)
|
||||
@@ -217,7 +217,7 @@ async def test_on_remote_control_paired(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check device and API call count
|
||||
assert mock_mozart_client.get_bluetooth_remotes.call_count == 3
|
||||
assert mock_mozart_client.get_bluetooth_remotes.call_count == 8
|
||||
assert device_registry.async_get_device({(DOMAIN, TEST_REMOTE_SERIAL_PAIRED)})
|
||||
assert device_registry.async_get_device(
|
||||
{(DOMAIN, f"66666666_{TEST_SERIAL_NUMBER}")}
|
||||
@@ -257,7 +257,7 @@ async def test_on_remote_control_unpaired(
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
# Check device and API call count
|
||||
assert mock_mozart_client.get_bluetooth_remotes.call_count == 1
|
||||
assert mock_mozart_client.get_bluetooth_remotes.call_count == 3
|
||||
assert device_registry.async_get_device({(DOMAIN, TEST_REMOTE_SERIAL_PAIRED)})
|
||||
|
||||
# Check number of entities (button and remote events and media_player)
|
||||
@@ -280,7 +280,7 @@ async def test_on_remote_control_unpaired(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check device and API call count
|
||||
assert mock_mozart_client.get_bluetooth_remotes.call_count == 3
|
||||
assert mock_mozart_client.get_bluetooth_remotes.call_count == 6
|
||||
assert (
|
||||
device_registry.async_get_device({(DOMAIN, TEST_REMOTE_SERIAL_PAIRED)}) is None
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.components.bang_olufsen.const import (
|
||||
)
|
||||
|
||||
from .const import (
|
||||
TEST_BATTERY_A5_SENSOR_ENTITY_ID,
|
||||
TEST_MEDIA_PLAYER_ENTITY_ID,
|
||||
TEST_MEDIA_PLAYER_ENTITY_ID_2,
|
||||
TEST_MEDIA_PLAYER_ENTITY_ID_3,
|
||||
@@ -51,6 +52,7 @@ def get_a5_entity_ids() -> list[str]:
|
||||
"""Return a list of entity_ids that a Beosound A5 provides."""
|
||||
buttons = [
|
||||
TEST_MEDIA_PLAYER_ENTITY_ID_4,
|
||||
TEST_BATTERY_A5_SENSOR_ENTITY_ID,
|
||||
*_get_button_entity_ids("beosound_a5_44444444"),
|
||||
]
|
||||
buttons.remove("event.beosound_a5_44444444_microphone")
|
||||
@@ -66,7 +68,9 @@ def get_remote_entity_ids(
|
||||
remote_serial: str = TEST_REMOTE_SERIAL, device_serial: str = TEST_SERIAL_NUMBER
|
||||
) -> list[str]:
|
||||
"""Return a list of entity_ids that the Beoremote One provides."""
|
||||
entity_ids: list[str] = []
|
||||
entity_ids: list[str] = [
|
||||
f"sensor.beoremote_one_{remote_serial}_{device_serial}_battery"
|
||||
]
|
||||
|
||||
# Add remote light key Event entity ids
|
||||
entity_ids.extend(
|
||||
|
||||
Reference in New Issue
Block a user