1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 09:38:58 +01:00

Add battery charging binary sensor to Bang & Olufsen (#160527)

Co-authored-by: Josef Zweck <josef@zweck.dev>
This commit is contained in:
Markus Jacobsen
2026-01-09 09:59:20 +01:00
committed by GitHub
parent 11dde08d79
commit ea48dc3c58
10 changed files with 145 additions and 8 deletions
@@ -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:
@@ -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()
@@ -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
+4 -1
View File
@@ -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"
@@ -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',
@@ -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',
@@ -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
@@ -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,
+3 -3
View File
@@ -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)
+4 -2
View File
@@ -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")