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.
146 lines
4.9 KiB
Python
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)
|