Skip to content

Commit

Permalink
Add Light entity support
Browse files Browse the repository at this point in the history
Rework wheel action handling
Add switch wheel action
Add default button settings
Add Halo configuration action
Add Halo notification action
Tweaks
  • Loading branch information
mj23000 committed Jan 3, 2025
1 parent 5008201 commit 8d0609e
Show file tree
Hide file tree
Showing 11 changed files with 607 additions and 136 deletions.
9 changes: 7 additions & 2 deletions custom_components/bang_olufsen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@
Platform.TEXT,
]

HALO_PLATFORMS = [Platform.EVENT, Platform.SENSOR, Platform.BINARY_SENSOR]
HALO_PLATFORMS = [
Platform.EVENT,
Platform.SENSOR,
Platform.BINARY_SENSOR,
]


@dataclass
Expand Down Expand Up @@ -146,7 +150,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
name=config_entry.title,
model=config_entry.data[CONF_MODEL],
serial_number=config_entry.unique_id,
manufacturer="Bang & Olufsen",
manufacturer=MANUFACTURER,
)

if is_halo(config_entry):
Expand Down Expand Up @@ -201,6 +205,7 @@ async def _setup_mozart(hass: HomeAssistant, config_entry: MozartConfigEntry) ->

async def _setup_halo(hass: HomeAssistant, config_entry: HaloConfigEntry) -> bool:
"""Set up a Halo."""

client = Halo(host=config_entry.data[CONF_HOST])

# Check API and WebSocket connection
Expand Down
131 changes: 118 additions & 13 deletions custom_components/bang_olufsen/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
ATTR_ITEM_NUMBER,
ATTR_MOZART_SERIAL_NUMBER,
ATTR_TYPE_NUMBER,
CONF_DEFAULT_BUTTON,
CONF_ENTITY_MAP,
CONF_HALO,
CONF_PAGE_NAME,
Expand Down Expand Up @@ -73,18 +74,10 @@

def halo_uuid() -> str:
"""Get a properly formatted Halo UUID."""
# UUIDs from uuid1() are not unique when generated in Home Assistant (???)
# Use this function to generate and format UUIDs instead.
temp_uuid = random_uuid_hex()
return (
temp_uuid[:8]
+ "-"
+ temp_uuid[8:12]
+ "-"
+ temp_uuid[12:16]
+ "-"
+ temp_uuid[16:20]
+ "-"
+ temp_uuid[20:32]
)
return f"{temp_uuid[:8]}-{temp_uuid[8:12]}-{temp_uuid[12:16]}-{temp_uuid[16:20]}-{temp_uuid[20:32]}-"


class BangOlufsenEntryData(TypedDict, total=False):
Expand Down Expand Up @@ -309,6 +302,7 @@ def __init__(self) -> None:
self._entity_ids: list[str] = []
self._entity_map: dict[str, str] = {}
self._page: Page
self._current_default: str = str(None)

async def async_step_init(
self, user_input: dict[str, Any] | None = None
Expand All @@ -321,16 +315,23 @@ async def async_step_init(
description_placeholders={"model": self.config_entry.data[CONF_MODEL]},
)

# Load stored configuration and entity map
# Load stored configuration, entity map and current default button
if self.config_entry.options:
self._configuration = BaseConfiguration.from_dict(
self.config_entry.options[CONF_HALO]
)
self._entity_map = self.config_entry.options[CONF_ENTITY_MAP]

# Check for a current default button.
# There should only be a single default in the whole configuration
for page in self._configuration.configuration.pages:
for button in page.buttons:
if button.default:
self._current_default = f"{page.title}-{button.title} ({button.id})"

return self.async_show_menu(
step_id="init",
menu_options=["add_page", "delete_pages"],
menu_options=["add_page", "delete_pages", "modify_default"],
)

async def async_step_add_page(
Expand Down Expand Up @@ -476,3 +477,107 @@ async def async_step_delete_pages(
}
),
)

async def async_step_modify_default(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Enter default options."""

return self.async_show_menu(
step_id="modify_default",
menu_options=["select_default", "remove_default"],
description_placeholders={"current_default": self._current_default},
)

async def async_step_select_default(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Select a default button."""
if user_input is not None:
# Update configuration with new default
new_default = user_input[CONF_DEFAULT_BUTTON]

# Find the pages and buttons in the configuration
for page_idx, page in enumerate(self._configuration.configuration.pages):
for button_idx, button in enumerate(page.buttons):
# Add new default to configuration
if button.id in new_default:
self._configuration.configuration.pages[page_idx].buttons[
button_idx
].default = True

# Remove current default from configuration
if button.id in self._current_default:
self._configuration.configuration.pages[page_idx].buttons[
button_idx
].default = False

return self.async_create_entry(
title="Updated configuration",
data=BangOlufsenEntryData(
host=self.config_entry.data[CONF_HOST],
model=self.config_entry.data[CONF_MODEL],
name=self.config_entry.title,
halo=self._configuration.to_dict(),
entity_map=self._entity_map,
),
)

# Get all buttons and check for a current default button
buttons = []
for page in self._configuration.configuration.pages:
buttons.extend(
[
f"{page.title}-{button.title} ({button.id})"
for button in page.buttons
if button.default is False
]
)

# Abort if no buttons are available
if len(buttons) == 0:
return self.async_abort(reason="no_pages")

return self.async_show_form(
step_id="select_default",
data_schema=vol.Schema(
{
vol.Required(CONF_DEFAULT_BUTTON): SelectSelector(
SelectSelectorConfig(
options=buttons,
multiple=False,
sort=True,
)
),
}
),
description_placeholders={"current_default": self._current_default},
)

async def async_step_remove_default(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Remove the default attribute from a button."""

# Abort if no buttons are available
if self._current_default == str(None):
return self.async_abort(reason="no_default")

# Remove current default from configuration
for page_idx, page in enumerate(self._configuration.configuration.pages):
for button_idx, button in enumerate(page.buttons):
if button.id in self._current_default:
self._configuration.configuration.pages[page_idx].buttons[
button_idx
].default = False

return self.async_create_entry(
title="Updated configuration",
data=BangOlufsenEntryData(
host=self.config_entry.data[CONF_HOST],
model=self.config_entry.data[CONF_MODEL],
name=self.config_entry.title,
halo=self._configuration.to_dict(),
entity_map=self._entity_map,
),
)
6 changes: 5 additions & 1 deletion custom_components/bang_olufsen/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,17 @@ class WebsocketNotification(StrEnum):
CONF_ENTITY_MAP: Final = "entity_map"
CONF_TITLE: Final = "title"
CONF_SUBTITLE: Final = "subtitle"
CONF_DEFAULT_BUTTON: Final = "default_button"
HALO_TITLE_LENGTH: Final = 15
HALO_PAGE_LENGTH: Final = 35
HALO_TEXT_LENGTH: Final = 6

# The names of compatible button icons for the Beoremote Halo
HALO_BUTTON_ICONS: list[str] = [icon.name for icon in Icons]

# Timeout for sending wheel events in seconds
HALO_WHEEL_TIMEOUT: Final = 0.125

# Mozart models
MOZART_MODELS: Final[list[BangOlufsenModel]] = [
model
Expand Down Expand Up @@ -427,7 +431,7 @@ class WebsocketNotification(StrEnum):
BEOLINK_RELATIVE_VOLUME: Final[str] = "BEOLINK_RELATIVE_VOLUME"


# Valid commands and their expected parameter type for beolink_command service
# Valid commands and their expected parameter type for beolink_command action
FLOAT_PARAMETERS: Final[tuple[str, str, str, type[float]]] = (
"set_volume_level",
"media_seek",
Expand Down
62 changes: 57 additions & 5 deletions custom_components/bang_olufsen/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@

from __future__ import annotations

from typing import cast

from mozart_api.models import PairedRemote
import voluptuous as vol

from homeassistant.components.event import EventDeviceClass, EventEntity
from homeassistant.components.homeassistant import ServiceResponse
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODEL
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant, SupportsResponse, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)

from . import HaloConfigEntry, MozartConfigEntry, set_platform_initialized
from .const import (
Expand All @@ -19,6 +27,7 @@
BEO_REMOTE_KEYS,
BEO_REMOTE_SUBMENU_CONTROL,
BEO_REMOTE_SUBMENU_LIGHT,
CONF_HALO,
CONNECTION_STATUS,
DEVICE_BUTTON_EVENTS,
DEVICE_BUTTONS,
Expand All @@ -31,7 +40,7 @@
WebsocketNotification,
)
from .entity import HaloEntity, MozartEntity
from .halo import SystemEvent
from .halo import BaseUpdate, Notification, SystemEvent
from .util import get_remotes, is_halo


Expand All @@ -44,6 +53,32 @@ async def async_setup_entry(
entities: list[BangOlufsenEvent] = []

if is_halo(config_entry):
# Register halo services

platform = async_get_current_platform()

platform.async_register_entity_service(
name="halo_configuration",
schema=None,
func="async_halo_configuration",
supports_response=SupportsResponse.ONLY,
)

platform.async_register_entity_service(
name="halo_notification",
schema={
vol.Required("title"): vol.All(
vol.Length(min=1, max=62),
cv.string,
),
vol.Required("subtitle"): vol.All(
vol.Length(min=1, max=256),
cv.string,
),
},
func="async_halo_notification",
)

entities.extend(await _get_halo_entities(config_entry))
else:
entities.extend(await _get_mozart_entities(config_entry))
Expand Down Expand Up @@ -257,10 +292,9 @@ async def _get_halo_entities(
class BangOlufsenEventHaloSystem(BangOlufsenHaloEvent):
"""Event class for Halo system events."""

# _attr_device_class = EventDeviceClass.MOTION
_attr_entity_registry_enabled_default = True
_attr_event_types = HALO_SYSTEM_EVENTS
_attr_translation_key = "halo_system"
_attr_icon = "mdi:power"

def __init__(self, config_entry: HaloConfigEntry) -> None:
"""Init the proximity event."""
Expand Down Expand Up @@ -290,3 +324,21 @@ def _update_system(self, event: SystemEvent) -> None:
"""Handle system event."""
self._trigger_event(event.state)
self.async_write_ha_state()

# Setup custom actions
def async_halo_configuration(self) -> ServiceResponse:
"""Get raw configuration for the Halo."""

return cast(ServiceResponse, self.entry.options[CONF_HALO])

async def async_halo_notification(self, title: str, subtitle: str) -> None:
"""Send a notification to the Halo."""

await self._client.send(
BaseUpdate(
update=Notification(
title=title,
subtitle=subtitle,
)
)
)
Loading

0 comments on commit 8d0609e

Please sign in to comment.