Skip to content

Commit ab45460

Browse files
mj23000joostlekMartinHjelmare
authored
Add Beoremote One support to Bang & Olufsen (home-assistant#155082)
Co-authored-by: Joost Lekkerkerker <[email protected]> Co-authored-by: Martin Hjelmare <[email protected]>
1 parent c8fd6db commit ab45460

File tree

16 files changed

+2433
-67
lines changed

16 files changed

+2433
-67
lines changed

homeassistant/components/bang_olufsen/config_flow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@
2121
ATTR_ITEM_NUMBER,
2222
ATTR_SERIAL_NUMBER,
2323
ATTR_TYPE_NUMBER,
24-
COMPATIBLE_MODELS,
2524
CONF_SERIAL_NUMBER,
2625
DEFAULT_MODEL,
2726
DOMAIN,
27+
SELECTABLE_MODELS,
2828
)
2929
from .util import get_serial_number_from_jid
3030

@@ -70,7 +70,7 @@ async def async_step_user(
7070
{
7171
vol.Required(CONF_HOST): str,
7272
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): SelectSelector(
73-
SelectSelectorConfig(options=COMPATIBLE_MODELS)
73+
SelectSelectorConfig(options=SELECTABLE_MODELS)
7474
),
7575
}
7676
)

homeassistant/components/bang_olufsen/const.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class BangOlufsenMediaType(StrEnum):
6262
class BangOlufsenModel(StrEnum):
6363
"""Enum for compatible model names."""
6464

65+
# Mozart devices
6566
BEOCONNECT_CORE = "Beoconnect Core"
6667
BEOLAB_8 = "BeoLab 8"
6768
BEOLAB_28 = "BeoLab 28"
@@ -73,6 +74,8 @@ class BangOlufsenModel(StrEnum):
7374
BEOSOUND_LEVEL = "Beosound Level"
7475
BEOSOUND_PREMIERE = "Beosound Premiere"
7576
BEOSOUND_THEATRE = "Beosound Theatre"
77+
# Remote devices
78+
BEOREMOTE_ONE = "Beoremote One"
7679

7780

7881
# Physical "buttons" on devices
@@ -96,6 +99,7 @@ class WebsocketNotification(StrEnum):
9699
"""Enum for WebSocket notification types."""
97100

98101
ACTIVE_LISTENING_MODE = "active_listening_mode"
102+
BEO_REMOTE_BUTTON = "beo_remote_button"
99103
BUTTON = "button"
100104
PLAYBACK_ERROR = "playback_error"
101105
PLAYBACK_METADATA = "playback_metadata"
@@ -113,6 +117,7 @@ class WebsocketNotification(StrEnum):
113117
BEOLINK_AVAILABLE_LISTENERS = "beolinkAvailableListeners"
114118
CONFIGURATION = "configuration"
115119
NOTIFICATION = "notification"
120+
REMOTE_CONTROL_DEVICES = "remoteControlDevices"
116121
REMOTE_MENU_CHANGED = "remoteMenuChanged"
117122

118123
ALL = "all"
@@ -128,7 +133,11 @@ class WebsocketNotification(StrEnum):
128133
CONF_BEOLINK_JID: Final = "jid"
129134

130135
# Models to choose from in manual configuration.
131-
COMPATIBLE_MODELS: list[str] = [x.value for x in BangOlufsenModel]
136+
SELECTABLE_MODELS: list[str] = [
137+
model.value for model in BangOlufsenModel if model != BangOlufsenModel.BEOREMOTE_ONE
138+
]
139+
140+
MANUFACTURER: Final[str] = "Bang & Olufsen"
132141

133142
# Attribute names for zeroconf discovery.
134143
ATTR_TYPE_NUMBER: Final[str] = "tn"
@@ -227,6 +236,10 @@ class WebsocketNotification(StrEnum):
227236

228237
# Dict used to translate native Bang & Olufsen event names to string.json compatible ones
229238
EVENT_TRANSLATION_MAP: dict[str, str] = {
239+
# Beoremote One
240+
"KeyPress": "key_press",
241+
"KeyRelease": "key_release",
242+
# Physical "buttons"
230243
"shortPress (Release)": "short_press_release",
231244
"longPress (Timeout)": "long_press_timeout",
232245
"longPress (Release)": "long_press_release",
@@ -247,6 +260,70 @@ class WebsocketNotification(StrEnum):
247260
"very_long_press_release",
248261
]
249262

263+
BEO_REMOTE_SUBMENU_CONTROL: Final[str] = "Control"
264+
BEO_REMOTE_SUBMENU_LIGHT: Final[str] = "Light"
265+
266+
# Common for both submenus
267+
BEO_REMOTE_KEYS: Final[tuple[str, ...]] = (
268+
"Blue",
269+
"Digit0",
270+
"Digit1",
271+
"Digit2",
272+
"Digit3",
273+
"Digit4",
274+
"Digit5",
275+
"Digit6",
276+
"Digit7",
277+
"Digit8",
278+
"Digit9",
279+
"Down",
280+
"Green",
281+
"Left",
282+
"Play",
283+
"Red",
284+
"Rewind",
285+
"Right",
286+
"Select",
287+
"Stop",
288+
"Up",
289+
"Wind",
290+
"Yellow",
291+
"Func1",
292+
"Func2",
293+
"Func3",
294+
"Func4",
295+
"Func5",
296+
"Func6",
297+
"Func7",
298+
"Func8",
299+
"Func9",
300+
"Func10",
301+
"Func11",
302+
"Func12",
303+
"Func13",
304+
"Func14",
305+
"Func15",
306+
"Func16",
307+
"Func17",
308+
)
309+
310+
# "keys" that are unique to the Control submenu
311+
BEO_REMOTE_CONTROL_KEYS: Final[tuple[str, ...]] = (
312+
"Func18",
313+
"Func19",
314+
"Func20",
315+
"Func21",
316+
"Func22",
317+
"Func23",
318+
"Func24",
319+
"Func25",
320+
"Func26",
321+
"Func27",
322+
)
323+
324+
BEO_REMOTE_KEY_EVENTS: Final[list[str]] = ["key_press", "key_release"]
325+
326+
250327
# Beolink Converter NL/ML sources need to be transformed to upper case
251328
BEOLINK_JOIN_SOURCES_TO_UPPER = (
252329
"aux_a",

homeassistant/components/bang_olufsen/event.py

Lines changed: 139 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,34 @@
22

33
from __future__ import annotations
44

5+
from typing import TYPE_CHECKING
6+
7+
from mozart_api.models import PairedRemote
8+
59
from homeassistant.components.event import EventDeviceClass, EventEntity
610
from homeassistant.const import CONF_MODEL
711
from homeassistant.core import HomeAssistant, callback
12+
from homeassistant.helpers import device_registry as dr
13+
from homeassistant.helpers.device_registry import DeviceInfo
814
from homeassistant.helpers.dispatcher import async_dispatcher_connect
915
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
1016

1117
from . import BangOlufsenConfigEntry
12-
from .const import CONNECTION_STATUS, DEVICE_BUTTON_EVENTS, WebsocketNotification
18+
from .const import (
19+
BEO_REMOTE_CONTROL_KEYS,
20+
BEO_REMOTE_KEY_EVENTS,
21+
BEO_REMOTE_KEYS,
22+
BEO_REMOTE_SUBMENU_CONTROL,
23+
BEO_REMOTE_SUBMENU_LIGHT,
24+
CONNECTION_STATUS,
25+
DEVICE_BUTTON_EVENTS,
26+
DOMAIN,
27+
MANUFACTURER,
28+
BangOlufsenModel,
29+
WebsocketNotification,
30+
)
1331
from .entity import BangOlufsenEntity
14-
from .util import get_device_buttons
32+
from .util import get_device_buttons, get_remotes
1533

1634
PARALLEL_UPDATES = 0
1735

@@ -21,24 +39,87 @@ async def async_setup_entry(
2139
config_entry: BangOlufsenConfigEntry,
2240
async_add_entities: AddConfigEntryEntitiesCallback,
2341
) -> None:
24-
"""Set up Sensor entities from config entry."""
42+
"""Set up Event entities from config entry."""
43+
entities: list[BangOlufsenEvent] = []
2544

2645
async_add_entities(
2746
BangOlufsenButtonEvent(config_entry, button_type)
2847
for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
2948
)
3049

50+
# Check for connected Beoremote One
51+
remotes = await get_remotes(config_entry.runtime_data.client)
52+
53+
for remote in remotes:
54+
# Add Light keys
55+
entities.extend(
56+
[
57+
BangOlufsenRemoteKeyEvent(
58+
config_entry,
59+
remote,
60+
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
61+
)
62+
for key_type in BEO_REMOTE_KEYS
63+
]
64+
)
65+
66+
# Add Control keys
67+
entities.extend(
68+
[
69+
BangOlufsenRemoteKeyEvent(
70+
config_entry,
71+
remote,
72+
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
73+
)
74+
for key_type in (*BEO_REMOTE_KEYS, *BEO_REMOTE_CONTROL_KEYS)
75+
]
76+
)
3177

32-
class BangOlufsenButtonEvent(BangOlufsenEntity, EventEntity):
33-
"""Event class for Button events."""
78+
# If the remote is no longer available, then delete the device.
79+
# The remote may appear as being available to the device after it has been unpaired on the remote
80+
# As it has to be removed from the device on the app.
81+
82+
device_registry = dr.async_get(hass)
83+
devices = device_registry.devices.get_devices_for_config_entry_id(
84+
config_entry.entry_id
85+
)
86+
for device in devices:
87+
if (
88+
device.model == BangOlufsenModel.BEOREMOTE_ONE
89+
and device.serial_number not in {remote.serial_number for remote in remotes}
90+
):
91+
device_registry.async_update_device(
92+
device.id, remove_config_entry_id=config_entry.entry_id
93+
)
94+
95+
async_add_entities(new_entities=entities)
96+
97+
98+
class BangOlufsenEvent(BangOlufsenEntity, EventEntity):
99+
"""Base Event class."""
34100

35101
_attr_device_class = EventDeviceClass.BUTTON
36102
_attr_entity_registry_enabled_default = False
103+
104+
def __init__(self, config_entry: BangOlufsenConfigEntry) -> None:
105+
"""Initialize Event."""
106+
super().__init__(config_entry, config_entry.runtime_data.client)
107+
108+
@callback
109+
def _async_handle_event(self, event: str) -> None:
110+
"""Handle event."""
111+
self._trigger_event(event)
112+
self.async_write_ha_state()
113+
114+
115+
class BangOlufsenButtonEvent(BangOlufsenEvent):
116+
"""Event class for Button events."""
117+
37118
_attr_event_types = DEVICE_BUTTON_EVENTS
38119

39120
def __init__(self, config_entry: BangOlufsenConfigEntry, button_type: str) -> None:
40121
"""Initialize Button."""
41-
super().__init__(config_entry, config_entry.runtime_data.client)
122+
super().__init__(config_entry)
42123

43124
self._attr_unique_id = f"{self._unique_id}_{button_type}"
44125

@@ -52,20 +133,65 @@ async def async_added_to_hass(self) -> None:
52133
self.async_on_remove(
53134
async_dispatcher_connect(
54135
self.hass,
55-
f"{self._unique_id}_{CONNECTION_STATUS}",
136+
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
56137
self._async_update_connection_state,
57138
)
58139
)
59140
self.async_on_remove(
60141
async_dispatcher_connect(
61142
self.hass,
62-
f"{self._unique_id}_{WebsocketNotification.BUTTON}_{self._button_type}",
143+
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BUTTON}_{self._button_type}",
63144
self._async_handle_event,
64145
)
65146
)
66147

67-
@callback
68-
def _async_handle_event(self, event: str) -> None:
69-
"""Handle event."""
70-
self._trigger_event(event)
71-
self.async_write_ha_state()
148+
149+
class BangOlufsenRemoteKeyEvent(BangOlufsenEvent):
150+
"""Event class for Beoremote One key events."""
151+
152+
_attr_event_types = BEO_REMOTE_KEY_EVENTS
153+
154+
def __init__(
155+
self,
156+
config_entry: BangOlufsenConfigEntry,
157+
remote: PairedRemote,
158+
key_type: str,
159+
) -> None:
160+
"""Initialize Beoremote One key."""
161+
super().__init__(config_entry)
162+
163+
if TYPE_CHECKING:
164+
assert remote.serial_number
165+
166+
self._attr_unique_id = f"{remote.serial_number}_{self._unique_id}_{key_type}"
167+
self._attr_device_info = DeviceInfo(
168+
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")},
169+
name=f"{BangOlufsenModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}",
170+
model=BangOlufsenModel.BEOREMOTE_ONE,
171+
serial_number=remote.serial_number,
172+
sw_version=remote.app_version,
173+
manufacturer=MANUFACTURER,
174+
via_device=(DOMAIN, self._unique_id),
175+
)
176+
177+
# Make the native key name Home Assistant compatible
178+
self._attr_translation_key = key_type.lower().replace("/", "_")
179+
180+
self._key_type = key_type
181+
182+
async def async_added_to_hass(self) -> None:
183+
"""Listen to WebSocket Beoremote One key events."""
184+
self.async_on_remove(
185+
async_dispatcher_connect(
186+
self.hass,
187+
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
188+
self._async_update_connection_state,
189+
)
190+
)
191+
self.async_on_remove(
192+
async_dispatcher_connect(
193+
self.hass,
194+
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BEO_REMOTE_BUTTON}_{self._key_type}",
195+
self._async_handle_event,
196+
)
197+
)

0 commit comments

Comments
 (0)