From 879178e8a2f8178465d5f9bba5fd685fcf423dce Mon Sep 17 00:00:00 2001 From: Christian Lackas Date: Mon, 16 Mar 2026 20:43:36 +0100 Subject: [PATCH] Add light support for HmIP-MP3P (Combination Signalling Device) (#162825) --- .../components/homematicip_cloud/light.py | 75 ++++++++++++++++++- .../homematicip_cloud/test_device.py | 2 +- .../homematicip_cloud/test_light.py | 56 ++++++++++++++ 3 files changed, 131 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index c7fd40adabc..6affad00b3f 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -11,10 +11,14 @@ from homematicip.base.enums import ( OpticalSignalBehaviour, RGBColorState, ) -from homematicip.base.functionalChannels import NotificationLightChannel +from homematicip.base.functionalChannels import ( + NotificationLightChannel, + NotificationMp3SoundChannel, +) from homematicip.device import ( BrandDimmer, BrandSwitchNotificationLight, + CombinationSignallingDevice, Device, Dimmer, DinRailDimmer3, @@ -108,6 +112,8 @@ async def async_setup_entry( entities.append( HomematicipOpticalSignalLight(hap, device, ch.index, led_number) ) + elif isinstance(device, CombinationSignallingDevice): + entities.append(HomematicipCombinationSignallingLight(hap, device)) async_add_entities(entities) @@ -586,3 +592,70 @@ class HomematicipOpticalSignalLight(HomematicipGenericEntity, LightEntity): rgb=simple_rgb_color, dimLevel=0.0, ) + + +class HomematicipCombinationSignallingLight(HomematicipGenericEntity, LightEntity): + """Representation of the HomematicIP combination signalling device light (HmIP-MP3P).""" + + _attr_color_mode = ColorMode.HS + _attr_supported_color_modes = {ColorMode.HS} + + _color_switcher: dict[str, tuple[float, float]] = { + RGBColorState.WHITE: (0.0, 0.0), + RGBColorState.RED: (0.0, 100.0), + RGBColorState.YELLOW: (60.0, 100.0), + RGBColorState.GREEN: (120.0, 100.0), + RGBColorState.TURQUOISE: (180.0, 100.0), + RGBColorState.BLUE: (240.0, 100.0), + RGBColorState.PURPLE: (300.0, 100.0), + } + + def __init__( + self, hap: HomematicipHAP, device: CombinationSignallingDevice + ) -> None: + """Initialize the combination signalling light entity.""" + super().__init__(hap, device, channel=1, is_multi_channel=False) + + @property + def _func_channel(self) -> NotificationMp3SoundChannel: + return self._device.functionalChannels[self._channel] + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._func_channel.on + + @property + def brightness(self) -> int: + """Return the brightness of this light between 0..255.""" + return int((self._func_channel.dimLevel or 0.0) * 255) + + @property + def hs_color(self) -> tuple[float, float]: + """Return the hue and saturation color value [float, float].""" + simple_rgb_color = self._func_channel.simpleRGBColorState + return self._color_switcher.get(simple_rgb_color, (0.0, 0.0)) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color) + simple_rgb_color = _convert_color(hs_color) + + brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness) + + # Default to full brightness when no kwargs given + if not kwargs: + brightness = 255 + + # Minimum brightness is 10, otherwise the LED is disabled + brightness = max(10, brightness) + dim_level = brightness / 255.0 + + await self._func_channel.set_rgb_dim_level_async( + rgb_color_state=simple_rgb_color.name, + dim_level=dim_level, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self._func_channel.turn_off_async() diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 7f07b288d43..e151446b17b 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices( test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 350 + assert len(mock_hap.hmip_device_by_entity_id) == 351 async def test_hmip_remove_device( diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index 2e856798454..a92eef4c5c4 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -854,3 +854,59 @@ async def test_hmip_wired_push_button_led_2( assert hmip_device.mock_calls[-1][0] == "set_optical_signal_async" assert hmip_device.mock_calls[-1][2]["channelIndex"] == 8 assert len(hmip_device.mock_calls) == service_call_counter + 1 + + +async def test_hmip_combination_signalling_light( + hass: HomeAssistant, default_mock_hap_factory: HomeFactory +) -> None: + """Test HomematicipCombinationSignallingLight (HmIP-MP3P).""" + entity_id = "light.kombisignalmelder" + entity_name = "Kombisignalmelder" + device_model = "HmIP-MP3P" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Kombisignalmelder"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + # Fixture has dimLevel=0.5, simpleRGBColorState=RED, on=true + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_COLOR_MODE] == ColorMode.HS + assert ha_state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.HS] + assert ha_state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert ha_state.attributes[ATTR_BRIGHTNESS] == 127 # 0.5 * 255 + assert ha_state.attributes[ATTR_HS_COLOR] == (0.0, 100.0) # RED + + functional_channel = hmip_device.functionalChannels[1] + service_call_counter = len(functional_channel.mock_calls) + + # Test turn_on with color and brightness + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, ATTR_HS_COLOR: [240.0, 100.0], ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + assert functional_channel.mock_calls[-1][0] == "set_rgb_dim_level_async" + assert functional_channel.mock_calls[-1][2] == { + "rgb_color_state": "BLUE", + "dim_level": 1.0, + } + assert len(functional_channel.mock_calls) == service_call_counter + 1 + + # Test turn_off + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": entity_id}, + blocking=True, + ) + assert functional_channel.mock_calls[-1][0] == "turn_off_async" + assert len(functional_channel.mock_calls) == service_call_counter + 2 + + # Test state update when turned off + await async_manipulate_test_data(hass, hmip_device, "on", False, channel=1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF