diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py index 87600f7c27b..9deeba9d019 100644 --- a/homeassistant/components/hyperion/const.py +++ b/homeassistant/components/hyperion/const.py @@ -1,29 +1,5 @@ """Constants for Hyperion integration.""" -from hyperion.const import ( - KEY_COMPONENTID_ALL, - KEY_COMPONENTID_BLACKBORDER, - KEY_COMPONENTID_BOBLIGHTSERVER, - KEY_COMPONENTID_FORWARDER, - KEY_COMPONENTID_GRABBER, - KEY_COMPONENTID_LEDDEVICE, - KEY_COMPONENTID_SMOOTHING, - KEY_COMPONENTID_V4L, -) - -# Maps between Hyperion API component names to Hyperion UI names. This allows Home -# Assistant to use names that match what Hyperion users may expect from the Hyperion UI. -COMPONENT_TO_NAME = { - KEY_COMPONENTID_ALL: "All", - KEY_COMPONENTID_SMOOTHING: "Smoothing", - KEY_COMPONENTID_BLACKBORDER: "Blackbar Detection", - KEY_COMPONENTID_FORWARDER: "Forwarder", - KEY_COMPONENTID_BOBLIGHTSERVER: "Boblight Server", - KEY_COMPONENTID_GRABBER: "Platform Capture", - KEY_COMPONENTID_LEDDEVICE: "LED Device", - KEY_COMPONENTID_V4L: "USB Capture", -} - CONF_AUTH_ID = "auth_id" CONF_CREATE_TOKEN = "create_token" CONF_INSTANCE = "instance" diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index 5ab74f1141b..ac2160120cc 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -147,7 +147,10 @@ class HyperionBaseLight(LightEntity): self._static_effect_list: list[str] = [KEY_EFFECT_SOLID] if self._support_external_effects: - self._static_effect_list += list(const.KEY_COMPONENTID_EXTERNAL_SOURCES) + self._static_effect_list += [ + const.KEY_COMPONENTID_TO_NAME[component] + for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES + ] self._effect_list: list[str] = self._static_effect_list[:] self._client_callbacks: Mapping[str, Callable[[dict[str, Any]], None]] = { @@ -195,7 +198,11 @@ class HyperionBaseLight(LightEntity): def icon(self) -> str: """Return state specific icon.""" if self.is_on: - if self.effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + if ( + self.effect in const.KEY_COMPONENTID_FROM_NAME + and const.KEY_COMPONENTID_FROM_NAME[self.effect] + in const.KEY_COMPONENTID_EXTERNAL_SOURCES + ): return ICON_EXTERNAL_SOURCE if self.effect != KEY_EFFECT_SOLID: return ICON_EFFECT @@ -280,8 +287,21 @@ class HyperionBaseLight(LightEntity): if ( effect and self._support_external_effects - and effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES + and ( + effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES + or effect in const.KEY_COMPONENTID_FROM_NAME + ) ): + if effect in const.KEY_COMPONENTID_FROM_NAME: + component = const.KEY_COMPONENTID_FROM_NAME[effect] + else: + _LOGGER.warning( + "Use of Hyperion effect '%s' is deprecated and will be removed " + "in a future release. Please use '%s' instead", + effect, + const.KEY_COMPONENTID_TO_NAME[effect], + ) + component = effect # Clear any color/effect. if not await self._client.async_send_clear( @@ -295,7 +315,7 @@ class HyperionBaseLight(LightEntity): **{ const.KEY_COMPONENTSTATE: { const.KEY_COMPONENT: key, - const.KEY_STATE: effect == key, + const.KEY_STATE: component == key, } } ): @@ -371,8 +391,12 @@ class HyperionBaseLight(LightEntity): if ( self._support_external_effects and componentid in const.KEY_COMPONENTID_EXTERNAL_SOURCES + and componentid in const.KEY_COMPONENTID_TO_NAME ): - self._set_internal_state(rgb_color=DEFAULT_COLOR, effect=componentid) + self._set_internal_state( + rgb_color=DEFAULT_COLOR, + effect=const.KEY_COMPONENTID_TO_NAME[componentid], + ) elif componentid == const.KEY_COMPONENTID_EFFECT: # Owner is the effect name. # See: https://docs.hyperion-project.org/en/json/ServerInfo.html#priorities @@ -594,9 +618,10 @@ class HyperionPriorityLight(HyperionBaseLight): @classmethod def _is_priority_entry_black(cls, priority: dict[str, Any] | None) -> bool: """Determine if a given priority entry is the color black.""" - if not priority: - return False - if priority.get(const.KEY_COMPONENTID) == const.KEY_COMPONENTID_COLOR: + if ( + priority + and priority.get(const.KEY_COMPONENTID) == const.KEY_COMPONENTID_COLOR + ): rgb_color = priority.get(const.KEY_VALUE, {}).get(const.KEY_RGB) if rgb_color is not None and tuple(rgb_color) == COLOR_BLACK: return True diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index 0c5e46b83e2..08b852f5302 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -5,7 +5,7 @@ "domain": "hyperion", "name": "Hyperion", "quality_scale": "platinum", - "requirements": ["hyperion-py==0.7.0"], + "requirements": ["hyperion-py==0.7.2"], "ssdp": [ { "manufacturer": "Hyperion Open Source Ambient Lighting", diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index dce92df6f35..5a7dd0c2cf5 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -14,6 +14,7 @@ from hyperion.const import ( KEY_COMPONENTID_GRABBER, KEY_COMPONENTID_LEDDEVICE, KEY_COMPONENTID_SMOOTHING, + KEY_COMPONENTID_TO_NAME, KEY_COMPONENTID_V4L, KEY_COMPONENTS, KEY_COMPONENTSTATE, @@ -39,7 +40,6 @@ from . import ( listen_for_instance_updates, ) from .const import ( - COMPONENT_TO_NAME, CONF_INSTANCE_CLIENTS, DOMAIN, HYPERION_MANUFACTURER_NAME, @@ -67,7 +67,7 @@ def _component_to_unique_id(server_id: str, component: str, instance_num: int) - server_id, instance_num, slugify( - f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {COMPONENT_TO_NAME[component]}" + f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {KEY_COMPONENTID_TO_NAME[component]}" ), ) @@ -77,7 +77,7 @@ def _component_to_switch_name(component: str, instance_name: str) -> str: return ( f"{instance_name} " f"{NAME_SUFFIX_HYPERION_COMPONENT_SWITCH} " - f"{COMPONENT_TO_NAME.get(component, component.capitalize())}" + f"{KEY_COMPONENTID_TO_NAME.get(component, component.capitalize())}" ) diff --git a/requirements_all.txt b/requirements_all.txt index 7f298e428e1..301e2665fa7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -793,7 +793,7 @@ huisbaasje-client==0.1.0 hydrawiser==0.2 # homeassistant.components.hyperion -hyperion-py==0.7.0 +hyperion-py==0.7.2 # homeassistant.components.bh1750 # homeassistant.components.bme280 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2cab2ae8964..e5f0f72081f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ huawei-lte-api==1.4.17 huisbaasje-client==0.1.0 # homeassistant.components.hyperion -hyperion-py==0.7.0 +hyperion-py==0.7.2 # homeassistant.components.iaqualink iaqualink==0.3.4 diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index a774a5ba868..bb20e644565 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -382,8 +382,9 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: assert entity_state assert entity_state.attributes["brightness"] == brightness - # On (=), 100% (=), V4L (!), [0,255,255] (=) - effect = const.KEY_COMPONENTID_EXTERNAL_SOURCES[2] # V4L + # On (=), 100% (=), "USB Capture (!), [0,255,255] (=) + component = "V4L" + effect = const.KEY_COMPONENTID_TO_NAME[component] client.async_send_clear = AsyncMock(return_value=True) client.async_send_set_component = AsyncMock(return_value=True) await hass.services.async_call( @@ -422,7 +423,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: } ), ] - client.visible_priority = {const.KEY_COMPONENTID: effect} + client.visible_priority = {const.KEY_COMPONENTID: component} call_registered_callback(client, "priorities-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state @@ -505,30 +506,126 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: assert not client.async_send_set_effect.called -async def test_light_async_turn_on_error_conditions(hass: HomeAssistantType) -> None: - """Test error conditions when turning the light on.""" +async def test_light_async_turn_on_fail_async_send_set_component( + hass: HomeAssistantType, +) -> None: + """Test set_component failure when turning the light on.""" client = create_mock_client() client.async_send_set_component = AsyncMock(return_value=False) client.is_on = Mock(return_value=False) await setup_test_config_entry(hass, hyperion_client=client) - - # On (=), 100% (=), solid (=), [255,255,255] (=) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True ) - - assert client.async_send_set_component.call_args == call( - **{ - const.KEY_COMPONENTSTATE: { - const.KEY_COMPONENT: const.KEY_COMPONENTID_ALL, - const.KEY_STATE: True, - } - } + assert client.method_calls[-1] == call.async_send_set_component( + componentstate={"component": "ALL", "state": True} ) -async def test_light_async_turn_off_error_conditions(hass: HomeAssistantType) -> None: - """Test error conditions when turning the light off.""" +async def test_light_async_turn_on_fail_async_send_set_component_source( + hass: HomeAssistantType, +) -> None: + """Test async_send_set_component failure when selecting the source.""" + client = create_mock_client() + client.async_send_clear = AsyncMock(return_value=True) + client.async_send_set_component = AsyncMock(return_value=False) + client.is_on = Mock(return_value=True) + await setup_test_config_entry(hass, hyperion_client=client) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: TEST_ENTITY_ID_1, + ATTR_EFFECT: const.KEY_COMPONENTID_TO_NAME["V4L"], + }, + blocking=True, + ) + assert client.method_calls[-1] == call.async_send_set_component( + componentstate={"component": "BOBLIGHTSERVER", "state": False} + ) + + +async def test_light_async_turn_on_fail_async_send_clear_source( + hass: HomeAssistantType, +) -> None: + """Test async_send_clear failure when turning the light on.""" + client = create_mock_client() + client.is_on = Mock(return_value=True) + client.async_send_clear = AsyncMock(return_value=False) + await setup_test_config_entry(hass, hyperion_client=client) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: TEST_ENTITY_ID_1, + ATTR_EFFECT: const.KEY_COMPONENTID_TO_NAME["V4L"], + }, + blocking=True, + ) + assert client.method_calls[-1] == call.async_send_clear(priority=180) + + +async def test_light_async_turn_on_fail_async_send_clear_effect( + hass: HomeAssistantType, +) -> None: + """Test async_send_clear failure when turning on an effect.""" + client = create_mock_client() + client.is_on = Mock(return_value=True) + client.async_send_clear = AsyncMock(return_value=False) + await setup_test_config_entry(hass, hyperion_client=client) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: "Warm Mood Blobs"}, + blocking=True, + ) + assert client.method_calls[-1] == call.async_send_clear(priority=180) + + +async def test_light_async_turn_on_fail_async_send_set_effect( + hass: HomeAssistantType, +) -> None: + """Test async_send_set_effect failure when turning on the light.""" + client = create_mock_client() + client.is_on = Mock(return_value=True) + client.async_send_clear = AsyncMock(return_value=True) + client.async_send_set_effect = AsyncMock(return_value=False) + await setup_test_config_entry(hass, hyperion_client=client) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: "Warm Mood Blobs"}, + blocking=True, + ) + assert client.method_calls[-1] == call.async_send_set_effect( + priority=180, effect={"name": "Warm Mood Blobs"}, origin="Home Assistant" + ) + + +async def test_light_async_turn_on_fail_async_send_set_color( + hass: HomeAssistantType, +) -> None: + """Test async_send_set_color failure when turning on the light.""" + client = create_mock_client() + client.is_on = Mock(return_value=True) + client.async_send_clear = AsyncMock(return_value=True) + client.async_send_set_color = AsyncMock(return_value=False) + await setup_test_config_entry(hass, hyperion_client=client) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_HS_COLOR: (240.0, 100.0)}, + blocking=True, + ) + assert client.method_calls[-1] == call.async_send_set_color( + priority=180, color=(0, 0, 255), origin="Home Assistant" + ) + + +async def test_light_async_turn_off_fail_async_send_set_component( + hass: HomeAssistantType, +) -> None: + """Test async_send_set_component failure when turning off the light.""" client = create_mock_client() client.async_send_set_component = AsyncMock(return_value=False) await setup_test_config_entry(hass, hyperion_client=client) @@ -539,17 +636,32 @@ async def test_light_async_turn_off_error_conditions(hass: HomeAssistantType) -> {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True, ) - - assert client.async_send_set_component.call_args == call( - **{ - const.KEY_COMPONENTSTATE: { - const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE, - const.KEY_STATE: False, - } - } + assert client.method_calls[-1] == call.async_send_set_component( + componentstate={"component": "LEDDEVICE", "state": False} ) +async def test_priority_light_async_turn_off_fail_async_send_clear( + hass: HomeAssistantType, +) -> None: + """Test async_send_clear failure when turning off a priority light.""" + client = create_mock_client() + client.async_send_clear = AsyncMock(return_value=False) + with patch( + "homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" + ) as enabled_by_default_mock: + enabled_by_default_mock.return_value = True + await setup_test_config_entry(hass, hyperion_client=client) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1}, + blocking=True, + ) + assert client.method_calls[-1] == call.async_send_clear(priority=180) + + async def test_light_async_turn_off(hass: HomeAssistantType) -> None: """Test turning the light off.""" client = create_mock_client() @@ -636,7 +748,10 @@ async def test_light_async_updates_from_hyperion_client( assert entity_state assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE assert entity_state.attributes["hs_color"] == (0.0, 0.0) - assert entity_state.attributes["effect"] == const.KEY_COMPONENTID_V4L + assert ( + entity_state.attributes["effect"] + == const.KEY_COMPONENTID_TO_NAME[const.KEY_COMPONENTID_V4L] + ) # Update priorities (Effect) effect = "foo" @@ -682,7 +797,10 @@ async def test_light_async_updates_from_hyperion_client( assert entity_state assert entity_state.attributes["effect_list"] == [ hyperion_light.KEY_EFFECT_SOLID - ] + const.KEY_COMPONENTID_EXTERNAL_SOURCES + [ + ] + [ + const.KEY_COMPONENTID_TO_NAME[component] + for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES + ] + [ effect[const.KEY_NAME] for effect in effects ] @@ -1171,15 +1289,17 @@ async def test_light_option_effect_hide_list(hass: HomeAssistantType) -> None: client.effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}] await setup_test_config_entry( - hass, hyperion_client=client, options={CONF_EFFECT_HIDE_LIST: ["Two", "V4L"]} + hass, + hyperion_client=client, + options={CONF_EFFECT_HIDE_LIST: ["Two", "USB Capture"]}, ) entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.attributes["effect_list"] == [ "Solid", - "BOBLIGHTSERVER", - "GRABBER", + "Boblight Server", + "Platform Capture", "One", ] @@ -1247,3 +1367,45 @@ async def test_lights_can_be_enabled(hass: HomeAssistantType) -> None: entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) assert entity_state + + +async def test_deprecated_effect_names(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def] + """Test deprecated effects function and issue a warning.""" + client = create_mock_client() + client.async_send_clear = AsyncMock(return_value=True) + client.async_send_set_component = AsyncMock(return_value=True) + + await setup_test_config_entry(hass, hyperion_client=client) + + for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: component}, + blocking=True, + ) + assert "Use of Hyperion effect '%s' is deprecated" % component in caplog.text + + # Simulate a state callback from Hyperion. + client.visible_priority = { + const.KEY_COMPONENTID: component, + } + call_registered_callback(client, "priorities-update") + + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state + assert ( + entity_state.attributes["effect"] + == const.KEY_COMPONENTID_TO_NAME[component] + ) + + +async def test_deprecated_effect_names_not_in_effect_list( + hass: HomeAssistantType, +) -> None: + """Test deprecated effects are not in shown effect list.""" + await setup_test_config_entry(hass) + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state + for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + assert component not in entity_state.attributes["effect_list"] diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index af1336bf0f8..5105d80f40d 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -5,13 +5,13 @@ from unittest.mock import AsyncMock, call, patch from hyperion.const import ( KEY_COMPONENT, KEY_COMPONENTID_ALL, + KEY_COMPONENTID_TO_NAME, KEY_COMPONENTSTATE, KEY_STATE, ) from homeassistant.components.hyperion import get_hyperion_device_id from homeassistant.components.hyperion.const import ( - COMPONENT_TO_NAME, DOMAIN, HYPERION_MANUFACTURER_NAME, HYPERION_MODEL_NAME, @@ -128,7 +128,7 @@ async def test_switch_has_correct_entities(hass: HomeAssistantType) -> None: # Setup component switch. for component in TEST_COMPONENTS: - name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])]) register_test_entity( hass, SWITCH_DOMAIN, @@ -138,7 +138,7 @@ async def test_switch_has_correct_entities(hass: HomeAssistantType) -> None: await setup_test_config_entry(hass, hyperion_client=client) for component in TEST_COMPONENTS: - name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])]) entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name entity_state = hass.states.get(entity_id) assert entity_state, f"Couldn't find entity: {entity_id}" @@ -150,13 +150,14 @@ async def test_device_info(hass: HomeAssistantType) -> None: client.components = TEST_COMPONENTS for component in TEST_COMPONENTS: - name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])]) register_test_entity( hass, SWITCH_DOMAIN, f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE}_{name}", f"{TEST_SWITCH_COMPONENT_BASE_ENTITY_ID}_{name}", ) + await setup_test_config_entry(hass, hyperion_client=client) assert hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID) is not None @@ -178,7 +179,7 @@ async def test_device_info(hass: HomeAssistantType) -> None: ] for component in TEST_COMPONENTS: - name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])]) entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name assert entity_id in entities_from_device @@ -192,7 +193,7 @@ async def test_switches_can_be_enabled(hass: HomeAssistantType) -> None: entity_registry = er.async_get(hass) for component in TEST_COMPONENTS: - name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])]) entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name entry = entity_registry.async_get(entity_id)