diff --git a/homeassistant/components/bang_olufsen/__init__.py b/homeassistant/components/bang_olufsen/__init__.py index 70f9cc7b572..04566f1a4c3 100644 --- a/homeassistant/components/bang_olufsen/__init__.py +++ b/homeassistant/components/bang_olufsen/__init__.py @@ -34,7 +34,12 @@ class BeoData: type BeoConfigEntry = ConfigEntry[BeoData] -PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER, Platform.SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.EVENT, + Platform.MEDIA_PLAYER, + Platform.SENSOR, +] async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool: diff --git a/homeassistant/components/bang_olufsen/binary_sensor.py b/homeassistant/components/bang_olufsen/binary_sensor.py new file mode 100644 index 00000000000..f90f648d96f --- /dev/null +++ b/homeassistant/components/bang_olufsen/binary_sensor.py @@ -0,0 +1,63 @@ +"""Binary Sensor entities for the Bang & Olufsen integration.""" + +from __future__ import annotations + +from mozart_api.models import BatteryState + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.core import HomeAssistant +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 supports_battery + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: BeoConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Binary Sensor entities from config entry.""" + if await supports_battery(config_entry.runtime_data.client): + async_add_entities(new_entities=[BeoBinarySensorBatteryCharging(config_entry)]) + + +class BeoBinarySensorBatteryCharging(BinarySensorEntity, BeoEntity): + """Battery charging Binary Sensor.""" + + _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING + _attr_is_on = False + + def __init__(self, config_entry: BeoConfigEntry) -> None: + """Init the battery charging Binary Sensor.""" + super().__init__(config_entry, config_entry.runtime_data.client) + + self._attr_unique_id = f"{self._unique_id}_charging" + + 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_charging, + ) + ) + + async def _update_battery_charging(self, data: BatteryState) -> None: + """Update battery charging.""" + self._attr_is_on = bool(data.is_charging) + self.async_write_ha_state() diff --git a/homeassistant/components/bang_olufsen/diagnostics.py b/homeassistant/components/bang_olufsen/diagnostics.py index d5be9cf2e92..b484fdaab05 100644 --- a/homeassistant/components/bang_olufsen/diagnostics.py +++ b/homeassistant/components/bang_olufsen/diagnostics.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN 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 @@ -97,4 +98,15 @@ async def async_get_config_entry_diagnostics( state_dict.pop("context") data["battery_level"] = state_dict + # Get Mozart battery charging entity + if entity_id := entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{config_entry.unique_id}_charging" + ): + 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["charging"] = state_dict + return data diff --git a/tests/components/bang_olufsen/const.py b/tests/components/bang_olufsen/const.py index 129c9bebee0..d0dc6ec5641 100644 --- a/tests/components/bang_olufsen/const.py +++ b/tests/components/bang_olufsen/const.py @@ -72,7 +72,10 @@ 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" +TEST_BATTERY_SENSOR_ENTITY_ID = f"sensor.beosound_a5_{TEST_SERIAL_NUMBER_4}_battery" +TEST_BATTERY_CHARGING_BINARY_SENSOR_ENTITY_ID = ( + f"binary_sensor.beosound_a5_{TEST_SERIAL_NUMBER_4}_charging" +) # Beoremote One TEST_REMOTE_SERIAL = "55555555" diff --git a/tests/components/bang_olufsen/snapshots/test_diagnostics.ambr b/tests/components/bang_olufsen/snapshots/test_diagnostics.ambr index b5b4a97c020..0d45adf710b 100644 --- a/tests/components/bang_olufsen/snapshots/test_diagnostics.ambr +++ b/tests/components/bang_olufsen/snapshots/test_diagnostics.ambr @@ -131,6 +131,14 @@ 'entity_id': 'sensor.beosound_a5_44444444_battery', 'state': '5', }), + 'charging': dict({ + 'attributes': dict({ + 'device_class': 'battery_charging', + 'friendly_name': 'Living room Balance Charging', + }), + 'entity_id': 'binary_sensor.beosound_a5_44444444_charging', + 'state': 'off', + }), 'config_entry': dict({ 'data': dict({ 'host': '192.168.0.4', diff --git a/tests/components/bang_olufsen/snapshots/test_event.ambr b/tests/components/bang_olufsen/snapshots/test_event.ambr index ab83a00f42e..65c3681e41c 100644 --- a/tests/components/bang_olufsen/snapshots/test_event.ambr +++ b/tests/components/bang_olufsen/snapshots/test_event.ambr @@ -1,6 +1,7 @@ # serializer version: 1 # name: test_button_event_creation_a5 list([ + 'binary_sensor.beosound_a5_44444444_charging', 'event.beosound_a5_44444444_bluetooth', 'event.beosound_a5_44444444_next', 'event.beosound_a5_44444444_play_pause', diff --git a/tests/components/bang_olufsen/test_binary_sensor.py b/tests/components/bang_olufsen/test_binary_sensor.py new file mode 100644 index 00000000000..72a5b776bd8 --- /dev/null +++ b/tests/components/bang_olufsen/test_binary_sensor.py @@ -0,0 +1,44 @@ +"""Test the bang_olufsen binary sensor entities.""" + +from unittest.mock import AsyncMock + +from mozart_api.models import BatteryState + +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import EntityRegistry + +from .conftest import mock_websocket_connection +from .const import TEST_BATTERY, TEST_BATTERY_CHARGING_BINARY_SENSOR_ENTITY_ID + +from tests.common import MockConfigEntry + + +async def test_battery_charging( + hass: HomeAssistant, + entity_registry: EntityRegistry, + mock_mozart_client: AsyncMock, + mock_config_entry_a5: MockConfigEntry, +) -> None: + """Test the battery charging time 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) + await mock_websocket_connection(hass, mock_mozart_client) + await hass.async_block_till_done() + + # Initial state is False + assert (states := hass.states.get(TEST_BATTERY_CHARGING_BINARY_SENSOR_ENTITY_ID)) + assert states.state == STATE_OFF + + # Check binary sensor reacts as expected to WebSocket events + battery_callback = mock_mozart_client.get_battery_notifications.call_args[0][0] + + battery_callback(BatteryState(is_charging=True)) + await hass.async_block_till_done() + + assert (states := hass.states.get(TEST_BATTERY_CHARGING_BINARY_SENSOR_ENTITY_ID)) + assert states.state == STATE_ON diff --git a/tests/components/bang_olufsen/test_diagnostics.py b/tests/components/bang_olufsen/test_diagnostics.py index 2d4bac00122..f8984cbb564 100644 --- a/tests/components/bang_olufsen/test_diagnostics.py +++ b/tests/components/bang_olufsen/test_diagnostics.py @@ -56,7 +56,6 @@ async def test_async_get_config_entry_diagnostics( 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, diff --git a/tests/components/bang_olufsen/test_sensor.py b/tests/components/bang_olufsen/test_sensor.py index d379f874efd..f1219ecb1b1 100644 --- a/tests/components/bang_olufsen/test_sensor.py +++ b/tests/components/bang_olufsen/test_sensor.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from .conftest import mock_websocket_connection from .const import ( TEST_BATTERY, - TEST_BATTERY_A5_SENSOR_ENTITY_ID, + TEST_BATTERY_SENSOR_ENTITY_ID, TEST_REMOTE_BATTERY_LEVEL_SENSOR_ENTITY_ID, TEST_REMOTE_SERIAL, ) @@ -34,13 +34,13 @@ async def test_battery_level( 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 := hass.states.get(TEST_BATTERY_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 := hass.states.get(TEST_BATTERY_SENSOR_ENTITY_ID)) assert states.state == str(TEST_BATTERY.battery_level) diff --git a/tests/components/bang_olufsen/util.py b/tests/components/bang_olufsen/util.py index 03c7b0092c7..caf1fe77352 100644 --- a/tests/components/bang_olufsen/util.py +++ b/tests/components/bang_olufsen/util.py @@ -11,7 +11,8 @@ from homeassistant.components.bang_olufsen.const import ( ) from .const import ( - TEST_BATTERY_A5_SENSOR_ENTITY_ID, + TEST_BATTERY_CHARGING_BINARY_SENSOR_ENTITY_ID, + TEST_BATTERY_SENSOR_ENTITY_ID, TEST_MEDIA_PLAYER_ENTITY_ID, TEST_MEDIA_PLAYER_ENTITY_ID_2, TEST_MEDIA_PLAYER_ENTITY_ID_3, @@ -52,7 +53,8 @@ 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, + TEST_BATTERY_SENSOR_ENTITY_ID, + TEST_BATTERY_CHARGING_BINARY_SENSOR_ENTITY_ID, *_get_button_entity_ids("beosound_a5_44444444"), ] buttons.remove("event.beosound_a5_44444444_microphone")