-
-
Notifications
You must be signed in to change notification settings - Fork 37.8k
Add OpenRGB profile select entity #154732
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
c93b673
39d40ca
67ccbdb
a321553
efa6703
f3f122b
24d3e60
80be89d
73ed447
f94b056
3003b91
1fc66c7
24bc83d
e54ecbe
f8b8950
82ed5eb
b5b4738
a54db9e
bd00897
9b1d62c
efda077
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| """Select platform for OpenRGB integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from homeassistant.components.select import SelectEntity | ||
| from homeassistant.const import CONF_NAME | ||
| from homeassistant.core import HomeAssistant, callback | ||
| from homeassistant.exceptions import HomeAssistantError | ||
| from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo | ||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||
| from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
|
||
| from .const import CONNECTION_ERRORS, DOMAIN, UID_SEPARATOR | ||
| from .coordinator import OpenRGBConfigEntry, OpenRGBCoordinator | ||
|
|
||
| PARALLEL_UPDATES = 0 | ||
|
|
||
|
|
||
| async def async_setup_entry( | ||
| hass: HomeAssistant, | ||
| config_entry: OpenRGBConfigEntry, | ||
| async_add_entities: AddConfigEntryEntitiesCallback, | ||
| ) -> None: | ||
| """Set up the OpenRGB select platform.""" | ||
| coordinator = config_entry.runtime_data | ||
| async_add_entities([OpenRGBProfileSelect(coordinator, config_entry)]) | ||
|
|
||
|
|
||
| class OpenRGBProfileSelect(CoordinatorEntity[OpenRGBCoordinator], SelectEntity): | ||
| """Representation of an OpenRGB profile select entity.""" | ||
|
|
||
| _attr_translation_key = "profile" | ||
| _attr_has_entity_name = True | ||
|
|
||
| _state_hash: int | None = None | ||
| _pending_profile: str | None = None | ||
|
|
||
| def __init__( | ||
| self, coordinator: OpenRGBCoordinator, entry: OpenRGBConfigEntry | ||
| ) -> None: | ||
| """Initialize the select entity.""" | ||
| super().__init__(coordinator) | ||
| self._attr_unique_id = UID_SEPARATOR.join([entry.entry_id, "profile"]) | ||
| self._attr_device_info = DeviceInfo( | ||
| identifiers={(DOMAIN, entry.entry_id)}, | ||
| name=entry.data[CONF_NAME], | ||
| model="OpenRGB SDK Server", | ||
| manufacturer="OpenRGB", | ||
| sw_version=coordinator.get_client_protocol_version(), | ||
| entry_type=DeviceEntryType.SERVICE, | ||
| ) | ||
| self._update_attrs() | ||
|
|
||
| def _compute_state_hash(self) -> int: | ||
| """Compute a hash of device states (modes and all LED colors).""" | ||
| state_data = tuple( | ||
| ( | ||
| device.active_mode, | ||
| tuple((color.red, color.green, color.blue) for color in device.colors), | ||
| ) | ||
| for device in self.coordinator.client.devices | ||
| ) | ||
| return hash(state_data) | ||
|
|
||
| @callback | ||
| def _update_attrs(self) -> None: | ||
| """Update the attributes based on the current profile list.""" | ||
| profiles = self.coordinator.client.profiles | ||
| self._attr_options = [profile.name for profile in profiles] | ||
|
|
||
| # If a profile was just applied, set it as current | ||
| if self._pending_profile is not None: | ||
| self._attr_current_option = self._pending_profile | ||
| self._pending_profile = None | ||
| self._state_hash = self._compute_state_hash() | ||
| # Only check for state changes if we have a current option to potentially clear | ||
| elif self._attr_current_option is not None: | ||
| current_hash = self._compute_state_hash() | ||
| # If state changed, we can no longer assume current profile | ||
| if current_hash != self._state_hash: | ||
| self._attr_current_option = None | ||
| self._state_hash = None | ||
|
Comment on lines
+63
to
+70
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need the hash? Can't we check based on name?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which name? OpenRGB SDK does not expose data of the current profile. I crafted this method so that I can at least determine whether the applied profile is still active. Please read this discussion for a better context.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I improved the hash function a little bit by reusing @jath03's idea. Also, this will be simplified in future. The author of OpenRGB accepted the suggestion of tracking this within OpenRGB itself. |
||
|
|
||
| @callback | ||
| def _handle_coordinator_update(self) -> None: | ||
| """Handle updated data from the coordinator.""" | ||
| self._update_attrs() | ||
| super()._handle_coordinator_update() | ||
|
|
||
| @property | ||
| def available(self) -> bool: | ||
| """Return if the select is available.""" | ||
| return super().available and bool(self._attr_options) | ||
|
|
||
| async def async_select_option(self, option: str) -> None: | ||
| """Load the selected profile.""" | ||
| async with self.coordinator.client_lock: | ||
| try: | ||
| await self.hass.async_add_executor_job( | ||
| self.coordinator.client.load_profile, option | ||
| ) | ||
| except CONNECTION_ERRORS as err: | ||
| raise HomeAssistantError( | ||
| translation_domain=DOMAIN, | ||
| translation_key="communication_error", | ||
| translation_placeholders={ | ||
| "server_address": self.coordinator.server_address, | ||
| "error": str(err), | ||
| }, | ||
| ) from err | ||
| except ValueError as err: | ||
| raise HomeAssistantError( | ||
| translation_domain=DOMAIN, | ||
| translation_key="openrgb_error", | ||
| translation_placeholders={ | ||
| "error": str(err), | ||
| }, | ||
| ) from err | ||
|
|
||
| self._pending_profile = option | ||
| await self.coordinator.async_refresh() | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # serializer version: 1 | ||
| # name: test_entities[select.test_computer_profile-entry] | ||
| EntityRegistryEntrySnapshot({ | ||
| 'aliases': set({ | ||
| }), | ||
| 'area_id': None, | ||
| 'capabilities': dict({ | ||
| 'options': list([ | ||
| ]), | ||
| }), | ||
| 'config_entry_id': <ANY>, | ||
| 'config_subentry_id': <ANY>, | ||
| 'device_class': None, | ||
| 'device_id': <ANY>, | ||
| 'disabled_by': None, | ||
| 'domain': 'select', | ||
| 'entity_category': None, | ||
| 'entity_id': 'select.test_computer_profile', | ||
| 'has_entity_name': True, | ||
| 'hidden_by': None, | ||
| 'icon': None, | ||
| 'id': <ANY>, | ||
| 'labels': set({ | ||
| }), | ||
| 'name': None, | ||
| 'options': dict({ | ||
| }), | ||
| 'original_device_class': None, | ||
| 'original_icon': None, | ||
| 'original_name': 'Profile', | ||
| 'platform': 'openrgb', | ||
| 'previous_unique_id': None, | ||
| 'suggested_object_id': None, | ||
| 'supported_features': 0, | ||
| 'translation_key': 'profile', | ||
| 'unique_id': '01J0EXAMPLE0CONFIGENTRY00||profile', | ||
| 'unit_of_measurement': None, | ||
| }) | ||
| # --- | ||
| # name: test_entities[select.test_computer_profile-state] | ||
| StateSnapshot({ | ||
| 'attributes': ReadOnlyDict({ | ||
| 'friendly_name': 'Test Computer Profile', | ||
| 'options': list([ | ||
| ]), | ||
| }), | ||
| 'context': <ANY>, | ||
| 'entity_id': 'select.test_computer_profile', | ||
| 'last_changed': <ANY>, | ||
| 'last_reported': <ANY>, | ||
| 'last_updated': <ANY>, | ||
| 'state': 'unavailable', | ||
| }) | ||
| # --- |
Uh oh!
There was an error while loading. Please reload this page.