fix(ha-integration): ausgeschaltete Lampe -> "unavailable" statt Fehlerzustand

War eine Lampe stromlos, wurde der Verbindungsfehler als UpdateFailed
gemeldet; HA schob den Config-Entry in SETUP_RETRY und die Integration
fiel in den Fehlerzustand. Jetzt wird Connection-Refused/Timeout als
NinlightUnavailable gefuehrt: der Coordinator liefert den letzten
bekannten State mit available=False, der Entry bleibt geladen und die
Entity zeigt "unavailable". Protokollfehler (400, kaputtes JSON) und
Auth (401 -> Reauth) verhalten sich unveraendert.

Light-Faehigkeiten (RGB/CT, Kelvin-Bereich) sind deshalb dynamische
Properties — ein Entry, der mit stromloser Lampe angelegt wurde,
erkennt sie, sobald die Lampe antwortet.
This commit is contained in:
Robin von Bülow
2026-10-02 08:37:50 +02:00
parent 19a0d6539a
commit 031830454a
3 changed files with 63 additions and 10 deletions
@@ -54,17 +54,33 @@ class NinlightLight(NinlightEntity, LightEntity):
def __init__(self, coordinator: NinlightCoordinator, entry: NinlightConfigEntry) -> None:
super().__init__(coordinator, entry)
state = coordinator.data
self._attr_unique_id = f"{(entry.unique_id or '').lower()}-light"
# Both modes are advertised for RGB bulbs: the firmware keeps the last color and
# the last color temperature and picks between them with `mode`, which is exactly
# how HA models a color_temp + rgb light.
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.RGB} if state.has_rgb else {
ColorMode.COLOR_TEMP
}
self._attr_supported_features = LightEntityFeature.TRANSITION
self._attr_min_color_temp_kelvin = state.ct_min
self._attr_max_color_temp_kelvin = state.ct_max
# Capabilities come from the polled state, not from a one-time __init__ snapshot: an
# entry set up while the bulb was powered off starts from defaults and must pick up
# the real RGB/CT range as soon as the bulb answers again.
@property
def available(self) -> bool:
# An unreachable (powered-off) bulb is an expected state: the entity goes
# unavailable instead of the config entry erroring.
return self.coordinator.last_update_success and self.coordinator.data.available
@property
def supported_color_modes(self) -> set[ColorMode]:
state = self.coordinator.data
# Both modes for RGB bulbs: the firmware keeps the last color and the last color
# temperature and picks between them with `mode`, exactly how HA models a
# color_temp + rgb light.
return {ColorMode.COLOR_TEMP, ColorMode.RGB} if state.has_rgb else {ColorMode.COLOR_TEMP}
@property
def min_color_temp_kelvin(self) -> int:
return self.coordinator.data.ct_min
@property
def max_color_temp_kelvin(self) -> int:
return self.coordinator.data.ct_max
@property
def is_on(self) -> bool: