22
33from __future__ import annotations
44
5+ import contextlib
56import logging
67from typing import Any
78
@@ -84,15 +85,27 @@ def __init__(
8485 post : str | None = None ,
8586 channel : int | None = None ,
8687 is_multi_channel : bool | None = False ,
88+ channel_real_index : int | None = None ,
8789 ) -> None :
8890 """Initialize the generic entity."""
8991 self ._hap = hap
9092 self ._home : AsyncHome = hap .home
9193 self ._device = device
9294 self ._post = post
9395 self ._channel = channel
96+
97+ # channel_real_index represents the actual index of the devices channel.
98+ # Accessing a functionalChannel by the channel parameter or array index is unreliable,
99+ # because the functionalChannels array is sorted as strings, not numbers.
100+ # For example, channels are ordered as: 1, 10, 11, 12, 2, 3, ...
101+ # Using channel_real_index ensures you reference the correct channel.
102+ self ._channel_real_index : int | None = channel_real_index
103+
94104 self ._is_multi_channel = is_multi_channel
95- self .functional_channel = self .get_current_channel ()
105+ self .functional_channel = None
106+ with contextlib .suppress (ValueError ):
107+ self .functional_channel = self .get_current_channel ()
108+
96109 # Marker showing that the HmIP device hase been removed.
97110 self .hmip_device_removed = False
98111
@@ -101,17 +114,20 @@ def device_info(self) -> DeviceInfo | None:
101114 """Return device specific attributes."""
102115 # Only physical devices should be HA devices.
103116 if isinstance (self ._device , Device ):
117+ device_id = str (self ._device .id )
118+ home_id = str (self ._device .homeId )
119+
104120 return DeviceInfo (
105121 identifiers = {
106122 # Serial numbers of Homematic IP device
107- (DOMAIN , self . _device . id )
123+ (DOMAIN , device_id )
108124 },
109125 manufacturer = self ._device .oem ,
110126 model = self ._device .modelType ,
111127 name = self ._device .label ,
112128 sw_version = self ._device .firmwareVersion ,
113129 # Link to the homematic ip access point.
114- via_device = (DOMAIN , self . _device . homeId ),
130+ via_device = (DOMAIN , home_id ),
115131 )
116132 return None
117133
@@ -185,25 +201,31 @@ def _async_device_removed(self, *args, **kwargs) -> None:
185201 def name (self ) -> str :
186202 """Return the name of the generic entity."""
187203
188- name = None
204+ name = ""
189205 # Try to get a label from a channel.
190- if hasattr (self ._device , "functionalChannels" ):
206+ functional_channels = getattr (self ._device , "functionalChannels" , None )
207+ if functional_channels and self .functional_channel :
191208 if self ._is_multi_channel :
192- name = self ._device .functionalChannels [self ._channel ].label
193- elif len (self ._device .functionalChannels ) > 1 :
194- name = self ._device .functionalChannels [1 ].label
209+ label = getattr (self .functional_channel , "label" , None )
210+ if label :
211+ name = str (label )
212+ elif len (functional_channels ) > 1 :
213+ label = getattr (functional_channels [1 ], "label" , None )
214+ if label :
215+ name = str (label )
195216
196217 # Use device label, if name is not defined by channel label.
197218 if not name :
198- name = self ._device .label
219+ name = self ._device .label or ""
199220 if self ._post :
200221 name = f"{ name } { self ._post } "
201222 elif self ._is_multi_channel :
202- name = f"{ name } Channel{ self ._channel } "
223+ name = f"{ name } Channel{ self .get_channel_index () } "
203224
204225 # Add a prefix to the name if the homematic ip home has a name.
205- if name and self ._home .name :
206- name = f"{ self ._home .name } { name } "
226+ home_name = getattr (self ._home , "name" , None )
227+ if name and home_name :
228+ name = f"{ home_name } { name } "
207229
208230 return name
209231
@@ -217,9 +239,7 @@ def unique_id(self) -> str:
217239 """Return a unique ID."""
218240 unique_id = f"{ self .__class__ .__name__ } _{ self ._device .id } "
219241 if self ._is_multi_channel :
220- unique_id = (
221- f"{ self .__class__ .__name__ } _Channel{ self ._channel } _{ self ._device .id } "
222- )
242+ unique_id = f"{ self .__class__ .__name__ } _Channel{ self .get_channel_index ()} _{ self ._device .id } "
223243
224244 return unique_id
225245
@@ -254,12 +274,65 @@ def extra_state_attributes(self) -> dict[str, Any]:
254274 return state_attr
255275
256276 def get_current_channel (self ) -> FunctionalChannel :
257- """Return the FunctionalChannel for device."""
258- if hasattr (self ._device , "functionalChannels" ):
259- if self ._is_multi_channel :
260- return self ._device .functionalChannels [self ._channel ]
277+ """Return the FunctionalChannel for the device.
278+
279+ Resolution priority:
280+ 1. For multi-channel entities with a real index, find channel by index match.
281+ 2. For multi-channel entities without a real index, use the provided channel position.
282+ 3. For non multi-channel entities with >1 channels, use channel at position 1
283+ (index 0 is often a meta/service channel in HmIP).
284+ Raises ValueError if no suitable channel can be resolved.
285+ """
286+ functional_channels = getattr (self ._device , "functionalChannels" , None )
287+ if not functional_channels :
288+ raise ValueError (
289+ f"Device { getattr (self ._device , 'id' , 'unknown' )} has no functionalChannels"
290+ )
291+
292+ # Multi-channel handling
293+ if self ._is_multi_channel :
294+ # Prefer real index mapping when provided to avoid ordering issues.
295+ if self ._channel_real_index is not None :
296+ for channel in functional_channels :
297+ if channel .index == self ._channel_real_index :
298+ return channel
299+ raise ValueError (
300+ f"Real channel index { self ._channel_real_index } not found for device "
301+ f"{ getattr (self ._device , 'id' , 'unknown' )} "
302+ )
303+ # Fallback: positional channel (already sorted as strings upstream).
304+ if self ._channel is not None and 0 <= self ._channel < len (
305+ functional_channels
306+ ):
307+ return functional_channels [self ._channel ]
308+ raise ValueError (
309+ f"Channel position { self ._channel } invalid for device "
310+ f"{ getattr (self ._device , 'id' , 'unknown' )} (len={ len (functional_channels )} )"
311+ )
261312
262- if len (self ._device .functionalChannels ) > 1 :
263- return self ._device .functionalChannels [1 ]
313+ # Single-channel / non multi-channel entity: choose second element if available
314+ if len (functional_channels ) > 1 :
315+ return functional_channels [1 ]
316+ return functional_channels [0 ]
264317
265- return None
318+ def get_channel_index (self ) -> int :
319+ """Return the correct channel index for this entity.
320+
321+ Prefers channel_real_index if set, otherwise returns channel.
322+ This ensures the correct channel is used even if the functionalChannels list is not numerically ordered.
323+ """
324+ if self ._channel_real_index is not None :
325+ return self ._channel_real_index
326+
327+ if self ._channel is not None :
328+ return self ._channel
329+
330+ return 1
331+
332+ def get_channel_or_raise (self ) -> FunctionalChannel :
333+ """Return the FunctionalChannel or raise an error if not found."""
334+ if not self .functional_channel :
335+ raise ValueError (
336+ f"No functional channel found for device { getattr (self ._device , 'id' , 'unknown' )} "
337+ )
338+ return self .functional_channel
0 commit comments