Add Homee select platform (#139534)

* homee select initial

* Finish select tests

* Add motor rotation

* fix snapshot after translation compilation

* string improvement

* last fixes

* fix review comments

* remove restore last known state

* readd wind monitoring state

* fix strings

* remove problematic selects

* remove motor rotation from strings

* fix review comments

* Update tests/components/homee/test_select.py

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>

* add PARALLEL_UPDATES

* parallel updates for select, not light.

---------

Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
pull/137038/merge
Markus Adrario 2025-03-06 19:56:17 +01:00 committed by GitHub
parent 99e1a7a676
commit eaad8ec49d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 282 additions and 0 deletions

View File

@ -19,6 +19,7 @@ PLATFORMS = [
Platform.COVER,
Platform.LIGHT,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.VALVE,

View File

@ -0,0 +1,63 @@
"""The Homee select platform."""
from pyHomee.const import AttributeType
from pyHomee.model import HomeeAttribute
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeeConfigEntry
from .entity import HomeeEntity
PARALLEL_UPDATES = 0
SELECT_DESCRIPTIONS: dict[AttributeType, SelectEntityDescription] = {
AttributeType.REPEATER_MODE: SelectEntityDescription(
key="repeater_mode",
options=["off", "level1", "level2"],
entity_category=EntityCategory.CONFIG,
),
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add the Homee platform for the select component."""
async_add_entities(
HomeeSelect(attribute, config_entry, SELECT_DESCRIPTIONS[attribute.type])
for node in config_entry.runtime_data.nodes
for attribute in node.attributes
if attribute.type in SELECT_DESCRIPTIONS and attribute.editable
)
class HomeeSelect(HomeeEntity, SelectEntity):
"""Representation of a Homee select entity."""
def __init__(
self,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: SelectEntityDescription,
) -> None:
"""Initialize a Homee select entity."""
super().__init__(attribute, entry)
self.entity_description = description
assert description.options is not None
self._attr_options = description.options
self._attr_translation_key = description.key
@property
def current_option(self) -> str:
"""Return the current selected option."""
return self.options[int(self._attribute.current_value)]
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await self.async_set_homee_value(self.options.index(option))

View File

@ -110,6 +110,16 @@
"name": "Wake-up interval"
}
},
"select": {
"repeater_mode": {
"name": "Repeater mode",
"state": {
"off": "[%key:common::state::off%]",
"level1": "Level 1",
"level2": "Level 2"
}
}
},
"sensor": {
"brightness": {
"name": "Illuminance"

View File

@ -0,0 +1,43 @@
{
"id": 1,
"name": "Test Select",
"profile": 33,
"image": "nodeicon_dimmablebulb",
"favorite": 0,
"order": 27,
"protocol": 3,
"routing": 0,
"state": 1,
"state_changed": 1736188706,
"added": 1610308228,
"history": 1,
"cube_type": 3,
"note": "",
"services": 7,
"phonetic_name": "",
"owner": 2,
"security": 0,
"attributes": [
{
"id": 1,
"node_id": 1,
"instance": 0,
"minimum": 0,
"maximum": 2,
"current_value": 1.0,
"target_value": 1.0,
"last_value": 1.0,
"unit": "n/a",
"step_value": 1.0,
"editable": 1,
"type": 226,
"state": 1,
"last_changed": 1680027880,
"changed_by": 1,
"changed_by_id": 0,
"based_on": 1,
"data": "",
"name": ""
}
]
}

View File

@ -0,0 +1,59 @@
# serializer version: 1
# name: test_select_snapshot[select.test_select_repeater_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'off',
'level1',
'level2',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.test_select_repeater_mode',
'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': 'Repeater mode',
'platform': 'homee',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'repeater_mode',
'unique_id': '00055511EECC-1-1',
'unit_of_measurement': None,
})
# ---
# name: test_select_snapshot[select.test_select_repeater_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Select Repeater mode',
'options': list([
'off',
'level1',
'level2',
]),
}),
'context': <ANY>,
'entity_id': 'select.test_select_repeater_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'level1',
})
# ---

View File

@ -0,0 +1,106 @@
"""Test homee selects."""
from unittest.mock import MagicMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.select import (
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_FIRST,
SERVICE_SELECT_LAST,
SERVICE_SELECT_NEXT,
SERVICE_SELECT_OPTION,
SERVICE_SELECT_PREVIOUS,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er
from . import build_mock_node, setup_integration
from tests.common import MockConfigEntry, snapshot_platform
async def setup_select(
hass: HomeAssistant, mock_homee: MagicMock, mock_config_entry: MockConfigEntry
) -> None:
"""Setups the integration for select tests."""
mock_homee.nodes = [build_mock_node("selects.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
await setup_integration(hass, mock_config_entry)
@pytest.mark.parametrize(
("service", "extra_options", "expected"),
[
(SERVICE_SELECT_FIRST, {}, 0),
(SERVICE_SELECT_LAST, {}, 2),
(SERVICE_SELECT_NEXT, {}, 2),
(SERVICE_SELECT_PREVIOUS, {}, 0),
(
SERVICE_SELECT_OPTION,
{
"option": "level2",
},
2,
),
],
)
async def test_select_services(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
service: str,
extra_options: dict[str, str],
expected: int,
) -> None:
"""Test the select services."""
await setup_select(hass, mock_homee, mock_config_entry)
OPTIONS = {ATTR_ENTITY_ID: "select.test_select_repeater_mode"}
OPTIONS.update(extra_options)
await hass.services.async_call(
SELECT_DOMAIN,
service,
OPTIONS,
blocking=True,
)
mock_homee.set_value.assert_called_once_with(1, 1, expected)
async def test_select_option_service_error(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the select_option service called with invalid option."""
await setup_select(hass, mock_homee, mock_config_entry)
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.test_select_repeater_mode",
"option": "invalid",
},
blocking=True,
)
async def test_select_snapshot(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the select entity snapshot."""
with patch("homeassistant.components.homee.PLATFORMS", [Platform.SELECT]):
await setup_select(hass, mock_homee, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)