Add support HmIP-BSL after firmware update to 2.0 to homematicip_cloud (#117657)

* Rebase

* Fix number of loaded entities

* Reduce redundant code

* Remove unneccessary import in test_light
This commit is contained in:
hahn-th 2024-10-15 15:26:33 +02:00 committed by GitHub
parent d2db25c7dd
commit cf9e5ae5a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 346 additions and 16 deletions

View file

@ -14,12 +14,14 @@ from homematicip.aio.device import (
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 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity):
)
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.

View file

@ -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",

View file

@ -186,6 +186,10 @@ class HomeTemplate(Home):
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]

View file

@ -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(

View file

@ -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: