Skip to content

Commit

Permalink
Add support for multiple remotes paired to one device
Browse files Browse the repository at this point in the history
Remove useless icon definition
Clean up Beoremote One handling
  • Loading branch information
mj23000 committed Dec 19, 2024
1 parent 838c93b commit e2b8bb5
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 110 deletions.
76 changes: 44 additions & 32 deletions custom_components/bang_olufsen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
import homeassistant.helpers.device_registry as dr
from homeassistant.util.ssl import get_default_context

from .const import BEO_REMOTE_MODEL, DOMAIN
from .util import get_remote
from .const import DOMAIN, MANUFACTURER, BangOlufsenModel
from .util import get_remotes
from .websocket import BangOlufsenWebsocket

PLATFORMS = [
Expand Down Expand Up @@ -66,6 +66,46 @@ async def _start_websocket_listener(data: BangOlufsenData) -> None:
await data.client.connect_notifications(remote_control=True, reconnect=True)


async def _handle_remote_devices(
hass: HomeAssistant, config_entry: ConfigEntry, client: MozartClient
) -> None:
"""Add or remove paired Beoremote One devices."""
# Check for connected Beoremote One
if remotes := await get_remotes(client):
for remote in remotes:
if TYPE_CHECKING:
assert remote.serial_number
assert config_entry.unique_id

# Create Beoremote One device
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, remote.serial_number)},
name=f"{BangOlufsenModel.BEOREMOTE_ONE}-{remote.serial_number}",
model=BangOlufsenModel.BEOREMOTE_ONE,
serial_number=remote.serial_number,
sw_version=remote.app_version,
manufacturer=MANUFACTURER,
via_device=(DOMAIN, config_entry.unique_id),
)

# If the remote is no longer available, then delete the device.
# The remote may appear as being available to the device after has been unpaired on the remote
# As it has to be removed from the device on the app.

device_registry = dr.async_get(hass)
devices = device_registry.devices.get_devices_for_config_entry_id(
config_entry.entry_id
)
for device in devices:
if (
device.model == BangOlufsenModel.BEOREMOTE_ONE
and device.serial_number not in [remote.serial_number for remote in remotes]
):
device_registry.async_remove_device(device.id)


async def async_setup_entry(
hass: HomeAssistant, config_entry: BangOlufsenConfigEntry
) -> bool:
Expand Down Expand Up @@ -110,36 +150,8 @@ async def async_setup_entry(
# Add the coordinator and API client
config_entry.runtime_data = BangOlufsenData(websocket, client)

# Check for connected Beoremote One
if remote := await get_remote(client):
if TYPE_CHECKING:
assert remote.serial_number

# Create Beoremote One device
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, remote.serial_number)},
name=f"{BEO_REMOTE_MODEL}-{remote.serial_number}",
model=BEO_REMOTE_MODEL,
serial_number=remote.serial_number,
sw_version=remote.app_version,
manufacturer="Bang & Olufsen",
via_device=(DOMAIN, config_entry.unique_id),
)
else:
# If the remote is no longer available, then delete the device.
# The remote may appear as being available to the device after has been unpaired on the remote
# As it has to be removed from the device on the app.

device_registry = dr.async_get(hass)
devices = device_registry.devices.get_devices_for_config_entry_id(
config_entry.entry_id
)
for device in devices:
assert device.model is not None
if device.model == BEO_REMOTE_MODEL:
device_registry.async_remove_device(device.id)
# Handle paired Beoremote One devices
await _handle_remote_devices(hass, config_entry, client)

await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)

Expand Down
4 changes: 2 additions & 2 deletions custom_components/bang_olufsen/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
ATTR_ITEM_NUMBER,
ATTR_SERIAL_NUMBER,
ATTR_TYPE_NUMBER,
COMPATIBLE_MODELS,
CONF_SERIAL_NUMBER,
DEFAULT_MODEL,
DOMAIN,
SELECTABLE_MODELS,
)
from .util import get_serial_number_from_jid

Expand Down Expand Up @@ -70,7 +70,7 @@ async def async_step_user(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): SelectSelector(
SelectSelectorConfig(options=COMPATIBLE_MODELS)
SelectSelectorConfig(options=SELECTABLE_MODELS)
),
}
)
Expand Down
9 changes: 6 additions & 3 deletions custom_components/bang_olufsen/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class BangOlufsenModel(StrEnum):
BEOSOUND_EMERGE = "Beosound Emerge"
BEOSOUND_LEVEL = "Beosound Level"
BEOSOUND_THEATRE = "Beosound Theatre"
BEOREMOTE_ONE = "Beoremote One"


# Dispatcher events
Expand Down Expand Up @@ -126,12 +127,16 @@ class WebsocketNotification(StrEnum):
# Default values for configuration.
DEFAULT_MODEL: Final[str] = BangOlufsenModel.BEOSOUND_BALANCE

MANUFACTURER: Final[str] = "Bang & Olufsen"

# Configuration.
CONF_BEOLINK_JID: Final = "jid"
CONF_SERIAL_NUMBER: Final = "serial_number"

# Models to choose from in manual configuration.
COMPATIBLE_MODELS: list[str] = [x.value for x in BangOlufsenModel]
SELECTABLE_MODELS: list[str] = [
model for model in BangOlufsenModel if model != BangOlufsenModel.BEOREMOTE_ONE
]

# Attribute names for zeroconf discovery.
ATTR_TYPE_NUMBER: Final[str] = "tn"
Expand Down Expand Up @@ -308,8 +313,6 @@ class WebsocketNotification(StrEnum):
]


BEO_REMOTE_MODEL: Final[str] = "Beoremote One"

BEO_REMOTE_SUBMENU_CONTROL: Final[str] = "Control"
BEO_REMOTE_SUBMENU_LIGHT: Final[str] = "Light"

Expand Down
49 changes: 25 additions & 24 deletions custom_components/bang_olufsen/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
WebsocketNotification,
)
from .entity import BangOlufsenEntity
from .util import get_remote
from .util import get_remotes


async def async_setup_entry(
Expand All @@ -55,30 +55,31 @@ async def async_setup_entry(
entities.append(BangOlufsenEventProximity(config_entry))

# Check for connected Beoremote One
if remote := await get_remote(config_entry.runtime_data.client):
# Add Light keys
entities.extend(
[
BangOlufsenRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
)
for key_type in BEO_REMOTE_KEYS
]
)
if remotes := await get_remotes(config_entry.runtime_data.client):
for remote in remotes:
# Add Light keys
entities.extend(
[
BangOlufsenRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
)
for key_type in BEO_REMOTE_KEYS
]
)

# Add Control keys
entities.extend(
[
BangOlufsenRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
)
for key_type in (*BEO_REMOTE_KEYS, *BEO_REMOTE_CONTROL_KEYS)
]
)
# Add Control keys
entities.extend(
[
BangOlufsenRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
)
for key_type in (*BEO_REMOTE_KEYS, *BEO_REMOTE_CONTROL_KEYS)
]
)

async_add_entities(new_entities=entities)

Expand Down
30 changes: 0 additions & 30 deletions custom_components/bang_olufsen/icons.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
{
"entity": {
"event": {
"bluetooth": {
"default": "mdi:gesture-tap-button"
},
"control_blue": {
"default": "mdi:remote"
},
Expand Down Expand Up @@ -273,33 +270,6 @@
},
"light_yellow": {
"default": "mdi:remote"
},
"microphone": {
"default": "mdi:gesture-tap-button"
},
"next": {
"default": "mdi:gesture-tap-button"
},
"playpause": {
"default": "mdi:gesture-tap-button"
},
"preset1": {
"default": "mdi:gesture-tap-button"
},
"preset2": {
"default": "mdi:gesture-tap-button"
},
"preset3": {
"default": "mdi:gesture-tap-button"
},
"preset4": {
"default": "mdi:gesture-tap-button"
},
"previous": {
"default": "mdi:gesture-tap-button"
},
"volume": {
"default": "mdi:gesture-tap-button"
}
},
"select": {
Expand Down
2 changes: 1 addition & 1 deletion custom_components/bang_olufsen/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"iot_class": "local_push",
"issue_tracker": "https://github.com/bang-olufsen/bang_olufsen-hacs/issues",
"requirements": ["mozart-api==4.1.1.116.4"],
"version": "3.1.4",
"version": "3.2.0",
"zeroconf": ["_bangolufsen._tcp.local."]
}
4 changes: 2 additions & 2 deletions custom_components/bang_olufsen/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
)
from homeassistant.util.dt import utcnow

from . import BangOlufsenConfigEntry, set_platform_initialized
from . import MANUFACTURER, BangOlufsenConfigEntry, set_platform_initialized
from .const import (
ACCEPTED_COMMANDS,
ACCEPTED_COMMANDS_LISTS,
Expand Down Expand Up @@ -245,7 +245,7 @@ def __init__(self, config_entry: BangOlufsenConfigEntry) -> None:
self._attr_device_info = DeviceInfo(
configuration_url=f"http://{self._host}/#/",
identifiers={(DOMAIN, self._unique_id)},
manufacturer="Bang & Olufsen",
manufacturer=MANUFACTURER,
model=self._model,
serial_number=self._unique_id,
)
Expand Down
11 changes: 8 additions & 3 deletions custom_components/bang_olufsen/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from . import BangOlufsenConfigEntry, set_platform_initialized
from .const import CONNECTION_STATUS, DOMAIN, WebsocketNotification
from .entity import BangOlufsenEntity
from .util import get_remote
from .util import get_remotes

SCAN_INTERVAL = timedelta(minutes=15)

Expand Down Expand Up @@ -55,8 +55,13 @@ async def async_setup_entry(
)

# Check for connected Beoremote One
if remote := await get_remote(config_entry.runtime_data.client):
entities.append(BangOlufsenSensorRemoteBatteryLevel(config_entry, remote))
if remotes := await get_remotes(config_entry.runtime_data.client):
entities.extend(
[
BangOlufsenSensorRemoteBatteryLevel(config_entry, remote)
for remote in remotes
]
)

async_add_entities(new_entities=entities)

Expand Down
21 changes: 8 additions & 13 deletions custom_components/bang_olufsen/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,15 @@ def get_serial_number_from_jid(jid: str) -> str:
return jid.split(".")[2].split("@")[0]


async def get_remote(client: MozartClient) -> PairedRemote | None:
async def get_remotes(client: MozartClient) -> list[PairedRemote]:
"""Get remote status easier."""
remote: PairedRemote | None = None

# Get if a remote control is connected and the remote
bluetooth_remote_list = await client.get_bluetooth_remotes()

if bool(len(cast(list[PairedRemote], bluetooth_remote_list.items))):
# Support only the first remote for now.
temp_remote = cast(list[PairedRemote], bluetooth_remote_list.items)[0]

# Remotes that been unpaired on the remote may still be available on the device,
# But should not be treated as available.
if temp_remote.serial_number is not None:
remote = temp_remote

return remote
# Remotes that been unpaired on the remote may still be available on the device,
# But should not be treated as available.
return [
remote
for remote in cast(list[PairedRemote], bluetooth_remote_list.items)
if remote.serial_number is not None
]

0 comments on commit e2b8bb5

Please sign in to comment.