Add LED light support for WiredPushButton (HmIPW-WRC2/WRC6) (#161841)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Christian Lackas
2026-02-19 00:36:29 +01:00
committed by GitHub
parent ca4d537529
commit fafa193549
5 changed files with 382 additions and 21 deletions

View File

@@ -22,6 +22,7 @@ from homematicip.device import (
PluggableDimmer,
SwitchMeasuring,
WiredDimmer3,
WiredPushButton,
)
from packaging.version import Version
@@ -93,6 +94,20 @@ async def async_setup_entry(
(Dimmer, PluggableDimmer, BrandDimmer, FullFlushDimmer),
):
entities.append(HomematicipDimmer(hap, device))
elif isinstance(device, WiredPushButton):
optical_channels = sorted(
(
ch
for ch in device.functionalChannels
if ch.functionalChannelType
== FunctionalChannelType.OPTICAL_SIGNAL_CHANNEL
),
key=lambda ch: ch.index,
)
for led_number, ch in enumerate(optical_channels, start=1):
entities.append(
HomematicipOpticalSignalLight(hap, device, ch.index, led_number)
)
async_add_entities(entities)
@@ -421,3 +436,129 @@ def _convert_color(color: tuple) -> RGBColorState:
if 270 < hue <= 330:
return RGBColorState.PURPLE
return RGBColorState.RED
class HomematicipOpticalSignalLight(HomematicipGenericEntity, LightEntity):
"""Representation of HomematicIP WiredPushButton LED light."""
_attr_color_mode = ColorMode.HS
_attr_supported_color_modes = {ColorMode.HS}
_attr_supported_features = LightEntityFeature.EFFECT
_attr_translation_key = "optical_signal_light"
_effect_to_behaviour: dict[str, OpticalSignalBehaviour] = {
"on": OpticalSignalBehaviour.ON,
"blinking": OpticalSignalBehaviour.BLINKING_MIDDLE,
"flash": OpticalSignalBehaviour.FLASH_MIDDLE,
"billow": OpticalSignalBehaviour.BILLOW_MIDDLE,
}
_behaviour_to_effect: dict[OpticalSignalBehaviour, str] = {
v: k for k, v in _effect_to_behaviour.items()
}
_attr_effect_list = list(_effect_to_behaviour)
_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: WiredPushButton,
channel_index: int,
led_number: int,
) -> None:
"""Initialize the optical signal light entity."""
super().__init__(
hap,
device,
post=f"LED {led_number}",
channel=channel_index,
is_multi_channel=True,
channel_real_index=channel_index,
)
@property
def is_on(self) -> bool:
"""Return true if light is on."""
channel = self.get_channel_or_raise()
return channel.on is True
@property
def brightness(self) -> int:
"""Return the brightness of this light between 0..255."""
channel = self.get_channel_or_raise()
return int((channel.dimLevel or 0.0) * 255)
@property
def hs_color(self) -> tuple[float, float]:
"""Return the hue and saturation color value [float, float]."""
channel = self.get_channel_or_raise()
simple_rgb_color = channel.simpleRGBColorState
return self._color_switcher.get(simple_rgb_color, (0.0, 0.0))
@property
def effect(self) -> str | None:
"""Return the current effect."""
channel = self.get_channel_or_raise()
return self._behaviour_to_effect.get(channel.opticalSignalBehaviour)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the optical signal light."""
state_attr = super().extra_state_attributes
channel = self.get_channel_or_raise()
if self.is_on:
state_attr[ATTR_COLOR_NAME] = channel.simpleRGBColorState
return state_attr
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]
elif effect is None:
effect = "on"
behaviour = self._effect_to_behaviour.get(effect, OpticalSignalBehaviour.ON)
await self._device.set_optical_signal_async(
channelIndex=self._channel,
opticalSignalBehaviour=behaviour,
rgb=simple_rgb_color,
dimLevel=dim_level,
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
channel = self.get_channel_or_raise()
simple_rgb_color = channel.simpleRGBColorState
await self._device.set_optical_signal_async(
channelIndex=self._channel,
opticalSignalBehaviour=OpticalSignalBehaviour.OFF,
rgb=simple_rgb_color,
dimLevel=0.0,
)

View File

@@ -28,6 +28,20 @@
}
},
"entity": {
"light": {
"optical_signal_light": {
"state_attributes": {
"effect": {
"state": {
"billow": "Billow",
"blinking": "Blinking",
"flash": "Flash",
"on": "[%key:common::state::on%]"
}
}
}
}
},
"sensor": {
"smoke_detector_alarm_counter": {
"name": "Alarm counter"

View File

@@ -3779,7 +3779,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F711000000000AAAAA25",
"label": "Bewegungsmelder für 55er Rahmen innen",
"label": "Bewegungsmelder f\u00fcr 55er Rahmen \u2013 innen",
"lastStatusUpdate": 1546776387401,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -3841,7 +3841,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000038",
"label": "Weather Sensor plus",
"label": "Weather Sensor \u2013 plus",
"lastStatusUpdate": 1546789939739,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -3958,7 +3958,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000BBBBB1",
"label": "Fußbodenheizungsaktor",
"label": "Fu\u00dfbodenheizungsaktor",
"lastStatusUpdate": 1545746610807,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -4110,7 +4110,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F71100000000000BBB17",
"label": "Außen Küche",
"label": "Au\u00dfen K\u00fcche",
"lastStatusUpdate": 1546776559553,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -4220,7 +4220,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000000",
"label": "Balkontüre",
"label": "Balkont\u00fcre",
"lastStatusUpdate": 1524516526498,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -4439,7 +4439,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000003",
"label": "Küche",
"label": "K\u00fcche",
"lastStatusUpdate": 1524514836466,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -4606,7 +4606,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000006",
"label": "Wohnungstüre",
"label": "Wohnungst\u00fcre",
"lastStatusUpdate": 1524516489316,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -4946,7 +4946,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000010",
"label": "Büro",
"label": "B\u00fcro",
"lastStatusUpdate": 1524513613922,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -5101,7 +5101,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000012",
"label": "Heizkörperthermostat",
"label": "Heizk\u00f6rperthermostat",
"lastStatusUpdate": 1524514105832,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -5154,7 +5154,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000013",
"label": "Heizkörperthermostat2",
"label": "Heizk\u00f6rperthermostat2",
"lastStatusUpdate": 1524514007132,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -5207,7 +5207,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F71100000000ETRV0013",
"label": "Heizkörperthermostat4",
"label": "Heizk\u00f6rperthermostat4",
"lastStatusUpdate": 1524514007132,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -5260,7 +5260,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000014",
"label": "Küche-Heizung",
"label": "K\u00fcche-Heizung",
"lastStatusUpdate": 1524513898337,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -5366,7 +5366,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000016",
"label": "Heizkörperthermostat3",
"label": "Heizk\u00f6rperthermostat3",
"lastStatusUpdate": 1524514626157,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -5902,7 +5902,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000029",
"label": "Kontakt-Schnittstelle Unterputz 1-fach",
"label": "Kontakt-Schnittstelle Unterputz \u2013 1-fach",
"lastStatusUpdate": 1547923306429,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -6016,7 +6016,7 @@
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F711AAAA000000000002",
"label": "Temperatur- und Luftfeuchtigkeitssensor - außen",
"label": "Temperatur- und Luftfeuchtigkeitssensor - au\u00dfen",
"lastStatusUpdate": 1524513950325,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
@@ -7100,7 +7100,7 @@
"groupIndex": 3,
"groups": ["00000000-0000-0000-0000-000000000044"],
"index": 3,
"label": "Tür",
"label": "T\u00fcr",
"multiModeInputMode": "KEY_BEHAVIOR",
"supportedOptionalFeatures": {
"IOptionalFeatureWindowState": true
@@ -9247,6 +9247,77 @@
"serializedGlobalTradeItemNumber": "3014F7110000000000000SB8",
"type": "STATUS_BOARD_8",
"updateState": "UP_TO_DATE"
},
"3014F711000000000000WRC6": {
"availableFirmwareVersion": "1.0.0",
"connectionType": "HMIP_WIRED",
"deviceArchetype": "HMIP",
"firmwareVersion": "1.0.0",
"firmwareVersionInteger": 65536,
"functionalChannels": {
"0": {
"configPending": false,
"deviceId": "3014F711000000000000WRC6",
"dutyCycle": false,
"functionalChannelType": "DEVICE_BASE",
"groupIndex": 0,
"groups": [],
"index": 0,
"label": "",
"lowBat": null,
"routerModuleEnabled": false,
"routerModuleSupported": false,
"rssiDeviceValue": -50,
"rssiPeerValue": -52,
"unreach": false,
"supportedOptionalFeatures": {}
},
"7": {
"deviceId": "3014F711000000000000WRC6",
"dimLevel": 0.5,
"functionalChannelType": "OPTICAL_SIGNAL_CHANNEL",
"groupIndex": 7,
"groups": [],
"index": 7,
"label": "LED 1",
"on": true,
"opticalSignalBehaviour": "ON",
"powerUpSwitchState": "PERMANENT_OFF",
"profileMode": "AUTOMATIC",
"simpleRGBColorState": "GREEN",
"supportedOptionalFeatures": {},
"userDesiredProfileMode": "AUTOMATIC"
},
"8": {
"deviceId": "3014F711000000000000WRC6",
"dimLevel": 0.0,
"functionalChannelType": "OPTICAL_SIGNAL_CHANNEL",
"groupIndex": 8,
"groups": [],
"index": 8,
"label": "LED 2",
"on": false,
"opticalSignalBehaviour": "OFF",
"powerUpSwitchState": "PERMANENT_OFF",
"profileMode": "AUTOMATIC",
"simpleRGBColorState": "RED",
"supportedOptionalFeatures": {},
"userDesiredProfileMode": "AUTOMATIC"
}
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F711000000000000WRC6",
"label": "Wired Taster 6-fach",
"lastStatusUpdate": 1595225686220,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
"modelId": 400,
"modelType": "HmIPW-WRC6",
"oem": "eQ-3",
"permanentlyReachable": true,
"serializedGlobalTradeItemNumber": "3014F711000000000000WRC6",
"type": "WIRED_PUSH_BUTTON_6",
"updateState": "UP_TO_DATE"
}
},
"groups": {
@@ -9525,7 +9596,7 @@
"humidityLimitEnabled": true,
"humidityLimitValue": 60,
"id": "00000000-0000-0000-0000-000000000010",
"label": "Büro",
"label": "B\u00fcro",
"lastSetPointReachedTimestamp": 1557767559939,
"lastSetPointUpdatedTimestamp": 1557767559939,
"lastStatusUpdate": 1524516454116,
@@ -9642,7 +9713,7 @@
"dutyCycle": false,
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "00000000-0000-0000-0000-000000000009",
"label": "Büro",
"label": "B\u00fcro",
"lastStatusUpdate": 1524515854304,
"lowBat": false,
"metaGroupId": "00000000-0000-0000-0000-000000000008",
@@ -10008,7 +10079,7 @@
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "00000000-0000-0000-0000-000000000008",
"incorrectPositioned": null,
"label": "Büro",
"label": "B\u00fcro",
"lastStatusUpdate": 1524516454116,
"lowBat": false,
"metaGroupId": null,
@@ -11065,7 +11136,7 @@
"inboxGroup": "00000000-0000-0000-0000-000000000044",
"lastReadyForUpdateTimestamp": 1522319489138,
"location": {
"city": "1010 Wien, Österreich",
"city": "1010 Wien, \u00d6sterreich",
"latitude": "48.208088",
"longitude": "16.358608"
},

View File

@@ -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) == 346
assert len(mock_hap.hmip_device_by_entity_id) == 348
async def test_hmip_remove_device(

View File

@@ -676,3 +676,138 @@ async def test_hmip_light_hs(
"saturation_level": hmip_device.functionalChannels[1].saturationLevel,
"dim_level": 0.16,
}
async def test_hmip_wired_push_button_led(
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
) -> None:
"""Test HomematicipOpticalSignalLight."""
entity_id = "light.led_1"
entity_name = "LED 1"
device_model = "HmIPW-WRC6"
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=["Wired Taster 6-fach"]
)
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_COLOR_MODE] == ColorMode.HS
assert ha_state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.HS]
assert ha_state.attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.EFFECT
assert ha_state.attributes[ATTR_BRIGHTNESS] == 127
assert ha_state.attributes[ATTR_COLOR_NAME] == "GREEN"
service_call_counter = len(hmip_device.mock_calls)
# Test turning 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: 128},
blocking=True,
)
assert hmip_device.mock_calls[-1][0] == "set_optical_signal_async"
assert hmip_device.mock_calls[-1][2] == {
"channelIndex": 7,
"opticalSignalBehaviour": OpticalSignalBehaviour.ON,
"rgb": "BLUE",
"dimLevel": 0.5,
}
assert len(hmip_device.mock_calls) == service_call_counter + 1
# Test turning on with effect
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": entity_id, ATTR_EFFECT: "blinking"},
blocking=True,
)
assert hmip_device.mock_calls[-1][0] == "set_optical_signal_async"
assert (
hmip_device.mock_calls[-1][2]["opticalSignalBehaviour"]
== OpticalSignalBehaviour.BLINKING_MIDDLE
)
assert len(hmip_device.mock_calls) == service_call_counter + 2
async def test_hmip_wired_push_button_led_turn_off(
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
) -> None:
"""Test HomematicipOpticalSignalLight turn off."""
entity_id = "light.led_1"
entity_name = "LED 1"
device_model = "HmIPW-WRC6"
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=["Wired Taster 6-fach"]
)
ha_state, hmip_device = get_and_check_entity_basics(
hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_ON
service_call_counter = len(hmip_device.mock_calls)
# Test turning off
await hass.services.async_call(
"light",
"turn_off",
{"entity_id": entity_id},
blocking=True,
)
assert hmip_device.mock_calls[-1][0] == "set_optical_signal_async"
assert hmip_device.mock_calls[-1][2] == {
"channelIndex": 7,
"opticalSignalBehaviour": OpticalSignalBehaviour.OFF,
"rgb": "GREEN",
"dimLevel": 0.0,
}
assert len(hmip_device.mock_calls) == service_call_counter + 1
# Verify state after turning off
await async_manipulate_test_data(
hass, hmip_device, "on", False, channel_real_index=7
)
await async_manipulate_test_data(
hass, hmip_device, "dimLevel", 0.0, channel_real_index=7
)
ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_OFF
async def test_hmip_wired_push_button_led_2(
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
) -> None:
"""Test HomematicipOpticalSignalLight second LED."""
entity_id = "light.led_2"
entity_name = "LED 2"
device_model = "HmIPW-WRC6"
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=["Wired Taster 6-fach"]
)
ha_state, hmip_device = get_and_check_entity_basics(
hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == STATE_OFF
assert ha_state.attributes[ATTR_COLOR_MODE] is None
assert ha_state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.HS]
assert ha_state.attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.EFFECT
service_call_counter = len(hmip_device.mock_calls)
# Test turning on second LED
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": entity_id},
blocking=True,
)
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