Files
Michael Sauer b482f84131 fix(light): kein Farbtemperaturmodus ohne echten Bereich
Das Montana-Black-Shield (neues Ziel ledshield_rgb in der Firmware) steuert
analoge RGB-Streifen und hat keinen eigenen Weisskanal. Sein Board meldet
deshalb ct_min == ct_max (nl_board.c: 4000/4000) - die Konvention der Firmware
fuer "weiss entsteht als RGB-Mischung".

supported_color_modes bot COLOR_TEMP bisher IMMER an. Beim Shield bekaeme Home
Assistant damit einen Farbtemperaturregler, dessen Grenzen zusammenfallen.
Jetzt entscheidet der gemeldete Bereich: nur wenn oben > unten, gibt es
COLOR_TEMP. Dieselbe Regel benutzt ninlight_core schon (device_state.dart,
hasColorTemp), dort faellt der Regler beim Shield von selbst weg.

Die Menge darf nie leer werden - HA verlangt mindestens einen Modus -, deshalb
faellt sie im Grenzfall auf BRIGHTNESS zurueck: ein Geraet ohne Farbe und ohne
Bereich kann immer noch dimmen.
2026-09-26 18:13:03 +02:00

146 lines
4.9 KiB
Python

"""Light platform for ninlight lamps."""
from __future__ import annotations
from typing import Any
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR,
ATTR_TRANSITION,
ColorMode,
LightEntity,
LightEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import MAX_TRANSITION_MS
from .coordinator import NinlightConfigEntry, NinlightCoordinator
from .entity import NinlightEntity
PARALLEL_UPDATES = 0 # the coordinator lock serializes per lamp
_FALLBACK_KELVIN = (2700, 6500)
def ha_to_lamp_brightness(value: int) -> int:
"""HA 1-255 -> lamp 1-100. Never 0: 0 would switch the lamp off."""
return max(1, min(100, round(value * 100 / 255)))
def lamp_to_ha_brightness(value: int) -> int:
"""Lamp 1-100 -> HA 1-255; round-trips back to the same lamp value."""
return max(1, min(255, round(value * 255 / 100)))
def transition_ms(seconds: float) -> int:
return max(0, min(MAX_TRANSITION_MS, round(seconds * 1000)))
async def async_setup_entry(
hass: HomeAssistant,
entry: NinlightConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
async_add_entities([NinlightLight(entry.runtime_data)])
class NinlightLight(NinlightEntity, LightEntity):
"""The lamp itself."""
_attr_name = None
_attr_supported_features = LightEntityFeature.TRANSITION
def __init__(self, coordinator: NinlightCoordinator) -> None:
super().__init__(coordinator, "light")
@property
def _state(self) -> dict[str, Any]:
return self.coordinator.data.state
@property
def is_on(self) -> bool:
return bool(self._state["on"])
@property
def brightness(self) -> int:
return lamp_to_ha_brightness(int(self._state["brightness"]))
def _has_ct_range(self) -> bool:
"""Ob das Geraet einen echten Farbtemperaturbereich hat.
Das Montana-Black-Shield steuert analoge RGB-Streifen ohne eigenen
Weisskanal und meldet deshalb ct_min == ct_max (nl_board.c,
"ledshield_rgb": 4000/4000) - die Konvention der Firmware fuer "weiss
entsteht als RGB-Mischung". Ein Bereich ohne Spielraum ist keiner;
wuerde er hier als COLOR_TEMP durchgehen, bekaeme Home Assistant einen
Regler mit min == max.
"""
unten, oben = self._kelvin_range()
return oben > unten
@property
def supported_color_modes(self) -> set[ColorMode]:
farbe = bool(self._state["capabilities"].get("color"))
modi: set[ColorMode] = set()
if farbe:
modi.add(ColorMode.RGB)
if self._has_ct_range():
modi.add(ColorMode.COLOR_TEMP)
# Leer darf die Menge nie sein - HA verlangt mindestens einen Modus.
# Ein Geraet ohne Farbe und ohne Bereich kann immer noch dimmen.
return modi or {ColorMode.BRIGHTNESS}
@property
def color_mode(self) -> ColorMode:
if self._state.get("mode") == "color" and ColorMode.RGB in self.supported_color_modes:
return ColorMode.RGB
return ColorMode.COLOR_TEMP
@property
def rgb_color(self) -> tuple[int, int, int] | None:
color = self._state.get("color")
if not isinstance(color, dict):
return None
return (int(color["r"]), int(color["g"]), int(color["b"]))
@property
def color_temp_kelvin(self) -> int | None:
value = self._state.get("color_temp")
return int(value) if value is not None else None
def _kelvin_range(self) -> tuple[int, int]:
value = self._state["capabilities"].get("color_temp")
if isinstance(value, list) and len(value) == 2:
return (int(value[0]), int(value[1]))
return _FALLBACK_KELVIN
@property
def min_color_temp_kelvin(self) -> int:
return self._kelvin_range()[0]
@property
def max_color_temp_kelvin(self) -> int:
return self._kelvin_range()[1]
async def async_turn_on(self, **kwargs: Any) -> None:
fields: dict[str, Any] = {"on": True}
if ATTR_BRIGHTNESS in kwargs:
fields["brightness"] = ha_to_lamp_brightness(kwargs[ATTR_BRIGHTNESS])
if ATTR_RGB_COLOR in kwargs:
red, green, blue = kwargs[ATTR_RGB_COLOR]
fields["color"] = {"r": red, "g": green, "b": blue}
if ATTR_COLOR_TEMP_KELVIN in kwargs:
fields["color_temp"] = int(kwargs[ATTR_COLOR_TEMP_KELVIN])
if ATTR_TRANSITION in kwargs:
fields["transition"] = transition_ms(kwargs[ATTR_TRANSITION])
await self.coordinator.async_set_state(**fields)
async def async_turn_off(self, **kwargs: Any) -> None:
fields: dict[str, Any] = {"on": False}
if ATTR_TRANSITION in kwargs:
fields["transition"] = transition_ms(kwargs[ATTR_TRANSITION])
await self.coordinator.async_set_state(**fields)