diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 5a56ae6937710..cf051103a100d 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -14,12 +14,14 @@ AsyncPluggableDimmer, AsyncWiredDimmer3, ) -from homematicip.base.enums import RGBColorState +from homematicip.base.enums import OpticalSignalBehaviour, RGBColorState from homematicip.base.functionalChannels import NotificationLightChannel +from packaging.version import Version from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_NAME, + ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, ColorMode, @@ -47,15 +49,22 @@ async def async_setup_entry( if isinstance(device, AsyncBrandSwitchMeasuring): entities.append(HomematicipLightMeasuring(hap, device)) elif isinstance(device, AsyncBrandSwitchNotificationLight): + device_version = Version(device.firmwareVersion) entities.append(HomematicipLight(hap, device)) + + entity_class = ( + HomematicipNotificationLightV2 + if device_version > Version("2.0.0") + else HomematicipNotificationLight + ) + entities.append( - HomematicipNotificationLight(hap, device, device.topLightChannelIndex) + entity_class(hap, device, device.topLightChannelIndex, "Top") ) entities.append( - HomematicipNotificationLight( - hap, device, device.bottomLightChannelIndex - ) + entity_class(hap, device, device.bottomLightChannelIndex, "Bottom") ) + elif isinstance(device, (AsyncWiredDimmer3, AsyncDinRailDimmer3)): entities.extend( HomematicipMultiDimmer(hap, device, channel=channel) @@ -158,16 +167,9 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity): _attr_supported_color_modes = {ColorMode.HS} _attr_supported_features = LightEntityFeature.TRANSITION - def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: + def __init__(self, hap: HomematicipHAP, device, channel: int, post: str) -> None: """Initialize the notification light entity.""" - if channel == 2: - super().__init__( - hap, device, post="Top", channel=channel, is_multi_channel=True - ) - else: - super().__init__( - hap, device, post="Bottom", channel=channel, is_multi_channel=True - ) + super().__init__(hap, device, post=post, channel=channel, is_multi_channel=True) self._color_switcher: dict[str, tuple[float, float]] = { RGBColorState.WHITE: (0.0, 0.0), @@ -259,6 +261,66 @@ async def async_turn_off(self, **kwargs: Any) -> None: ) +class HomematicipNotificationLightV2(HomematicipNotificationLight, LightEntity): + """Representation of HomematicIP Cloud notification light.""" + + _effect_list = [ + OpticalSignalBehaviour.BILLOW_MIDDLE, + OpticalSignalBehaviour.BLINKING_MIDDLE, + OpticalSignalBehaviour.FLASH_MIDDLE, + OpticalSignalBehaviour.OFF, + OpticalSignalBehaviour.ON, + ] + + def __init__(self, hap: HomematicipHAP, device, channel: int, post: str) -> None: + """Initialize the notification light entity.""" + super().__init__(hap, device, post=post, channel=channel) + self._attr_supported_features |= LightEntityFeature.EFFECT + + @property + def effect_list(self) -> list[str] | None: + """Return the list of supported effects.""" + return self._effect_list + + @property + def effect(self) -> str | None: + """Return the current effect.""" + return self._func_channel.opticalSignalBehaviour + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._func_channel.on + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + # Use hs_color from kwargs, + # if not applicable use current hs_color. + hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color) + simple_rgb_color = _convert_color(hs_color) + + # If no kwargs, use default value. + brightness = 255 + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + + # Minimum brightness is 10, otherwise the led is disabled + brightness = max(10, brightness) + dim_level = round(brightness / 255.0, 2) + + effect = self.effect + if ATTR_EFFECT in kwargs: + effect = kwargs[ATTR_EFFECT] + + await self._func_channel.async_set_optical_signal( + opticalSignalBehaviour=effect, rgb=simple_rgb_color, dimLevel=dim_level + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self._func_channel.async_turn_off() + + def _convert_color(color: tuple) -> RGBColorState: """Convert the given color to the reduced RGBColorState color. diff --git a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json index e67ffd7846715..442fd16d2c7f6 100644 --- a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json +++ b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json @@ -3237,6 +3237,173 @@ "type": "BRAND_SWITCH_NOTIFICATION_LIGHT", "updateState": "UP_TO_DATE" }, + "3014F711000000000000BSL2": { + "availableFirmwareVersion": "2.0.2", + "connectionType": "HMIP_RF", + "deviceArchetype": "HMIP", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "controlsMountingOrientation": null, + "daliBusState": null, + "defaultLinkedGroup": [], + "deviceCommunicationError": null, + "deviceDriveError": null, + "deviceDriveModeError": null, + "deviceId": "3014F711000000000000BSL2", + "deviceOperationMode": null, + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000007"], + "index": 0, + "label": "", + "lockJammed": null, + "lowBat": null, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -74, + "rssiPeerValue": -75, + "sensorCommunicationError": null, + "sensorError": null, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceCommunicationError": false, + "IFeatureDeviceDaliBusError": false, + "IFeatureDeviceDriveError": false, + "IFeatureDeviceDriveModeError": false, + "IFeatureDeviceIdentify": true, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceSensorCommunicationError": false, + "IFeatureDeviceSensorError": false, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": true, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDefaultLinkedGroup": false, + "IOptionalFeatureDeviceErrorLockJammed": false, + "IOptionalFeatureDeviceOperationMode": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": false, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "channelRole": null, + "deviceId": "3014F711000000000000BSL2", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "internalLinkConfiguration": { + "firstInputAction": "OFF", + "internalLinkConfigurationType": "DOUBLE_INPUT_SWITCH", + "longPressOnTimeEnabled": false, + "onTime": 111600.0, + "secondInputAction": "ON" + }, + "label": "", + "on": false, + "powerUpSwitchState": "PERMANENT_OFF", + "profileMode": "AUTOMATIC", + "supportedOptionalFeatures": { + "IFeatureAccessAuthorizationActuatorChannel": false, + "IFeatureGarageGroupActuatorChannel": false, + "IFeatureLightGroupActuatorChannel": false, + "IFeatureLightProfileActuatorChannel": false, + "IOptionalFeatureInternalLinkConfiguration": true, + "IOptionalFeaturePowerUpSwitchState": true + }, + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "channelRole": "NOTIFICATION_LIGHT_DIMMING_ACTUATOR", + "deviceId": "3014F711000000000000BSL2", + "dimLevel": 0.0, + "functionalChannelType": "NOTIFICATION_LIGHT_CHANNEL", + "groupIndex": 2, + "groups": ["00000000-0000-0000-0000-000000000021"], + "index": 2, + "label": "Led Unten", + "on": false, + "opticalSignalBehaviour": "BLINKING_MIDDLE", + "profileMode": "AUTOMATIC", + "simpleRGBColorState": "TURQUOISE", + "supportedOptionalFeatures": { + "IFeatureOpticalSignalBehaviourState": true + }, + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "channelRole": "NOTIFICATION_LIGHT_DIMMING_ACTUATOR", + "deviceId": "3014F711000000000000BSL2", + "dimLevel": 0.25, + "functionalChannelType": "NOTIFICATION_LIGHT_CHANNEL", + "groupIndex": 3, + "groups": ["00000000-0000-0000-0000-000000000021"], + "index": 3, + "label": "Led Oben", + "on": true, + "opticalSignalBehaviour": "BLINKING_MIDDLE", + "profileMode": "AUTOMATIC", + "simpleRGBColorState": "GREEN", + "supportedOptionalFeatures": { + "IFeatureOpticalSignalBehaviourState": true + }, + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711000000000000BSL2", + "label": "BSL2", + "lastStatusUpdate": 1714910246419, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manuallyUpdateForced": false, + "manufacturerCode": 1, + "measuredAttributes": {}, + "modelId": 360, + "modelType": "HmIP-BSL", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711000000000000BSL2", + "type": "BRAND_SWITCH_NOTIFICATION_LIGHT", + "updateState": "UP_TO_DATE" + }, "3014F711SLO0000000000026": { "availableFirmwareVersion": "0.0.0", "connectionType": "HMIP_RF", diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index d42b9602d3848..800811235195b 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -186,6 +186,10 @@ def update_home(self, json_state, clearConfig: bool = False): def _generate_mocks(self): """Generate mocks for groups and devices.""" self.devices = [_get_mock(device) for device in self.devices] + for device in self.devices: + device.functionalChannels = [ + _get_mock(ch) for ch in device.functionalChannels + ] self.groups = [_get_mock(group) for group in self.groups] diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 25fb31c3c6202..d5f8d0f25c46d 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -28,7 +28,7 @@ async def test_hmip_load_all_supported_devices( test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 293 + assert len(mock_hap.hmip_device_by_entity_id) == 296 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 18d490c37869a..c0717e81e0df9 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -1,12 +1,14 @@ """Tests for HomematicIP Cloud light.""" -from homematicip.base.enums import RGBColorState +from homematicip.base.enums import OpticalSignalBehaviour, RGBColorState from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, ATTR_COLOR_NAME, + ATTR_EFFECT, + ATTR_HS_COLOR, ATTR_SUPPORTED_COLOR_MODES, DOMAIN as LIGHT_DOMAIN, ColorMode, @@ -173,6 +175,101 @@ async def test_hmip_notification_light( assert not ha_state.attributes.get(ATTR_BRIGHTNESS) +async def test_hmip_notification_light_2( + hass: HomeAssistant, default_mock_hap_factory: HomeFactory +) -> None: + """Test HomematicipNotificationLight.""" + entity_id = "light.led_oben" + entity_name = "Led Oben" + device_model = "HmIP-BSL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_devices=["BSL2"]) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_EFFECT] == "BLINKING_MIDDLE" + + functional_channel = hmip_device.functionalChannels[3] + service_call_counter = len(functional_channel.mock_calls) + + # Send all color via service call. + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, ATTR_HS_COLOR: [240.0, 100.0], ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + assert functional_channel.mock_calls[-1][0] == "async_set_optical_signal" + assert functional_channel.mock_calls[-1][2] == { + "opticalSignalBehaviour": OpticalSignalBehaviour.BLINKING_MIDDLE, + "rgb": RGBColorState.BLUE, + "dimLevel": 0.5, + } + assert service_call_counter + 1 == len(functional_channel.mock_calls) + + +async def test_hmip_notification_light_2_without_brightness_and_light( + hass: HomeAssistant, default_mock_hap_factory: HomeFactory +) -> None: + """Test HomematicipNotificationLight.""" + entity_id = "light.led_oben" + entity_name = "Led Oben" + device_model = "HmIP-BSL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_devices=["BSL2"]) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + color_before = ha_state.attributes["color_name"] + + functional_channel = hmip_device.functionalChannels[3] + service_call_counter = len(functional_channel.mock_calls) + + # Send all color via service call. + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, ATTR_EFFECT: OpticalSignalBehaviour.FLASH_MIDDLE}, + blocking=True, + ) + assert functional_channel.mock_calls[-1][0] == "async_set_optical_signal" + assert functional_channel.mock_calls[-1][2] == { + "opticalSignalBehaviour": OpticalSignalBehaviour.FLASH_MIDDLE, + "rgb": color_before, + "dimLevel": 1, + } + assert service_call_counter + 1 == len(functional_channel.mock_calls) + + +async def test_hmip_notification_light_2_turn_off( + hass: HomeAssistant, default_mock_hap_factory: HomeFactory +) -> None: + """Test HomematicipNotificationLight.""" + entity_id = "light.led_oben" + entity_name = "Led Oben" + device_model = "HmIP-BSL" + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_devices=["BSL2"]) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + functional_channel = hmip_device.functionalChannels[3] + service_call_counter = len(functional_channel.mock_calls) + + # Send all color via service call. + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": entity_id}, + blocking=True, + ) + assert functional_channel.mock_calls[-1][0] == "async_turn_off" + assert service_call_counter + 1 == len(functional_channel.mock_calls) + + async def test_hmip_dimmer( hass: HomeAssistant, default_mock_hap_factory: HomeFactory ) -> None: