Files
ha-ninstrip/custom_components/ninstrip/number.py
T
Michael Sauer 616515ef77 feat: Strom, Leistung und Strombudget
Zeigt strip.current_ma und strip.power_w aus system/info als Sensoren
und macht max_milliamps aus system/settings ueber ein Zahlenfeld
einstellbar. Schreiben liest wie bei sACN zurueck statt den
geschriebenen Wert anzunehmen.
2026-09-23 14:35:50 +02:00

170 lines
6.1 KiB
Python

"""Zahlenfelder eines ninstrip-Geraets: Uebergangszeiten und sACN."""
from __future__ import annotations
from dataclasses import dataclass
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import EntityCategory, UnitOfElectricCurrent, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import MAX_FADE_MS, MAX_TRANSITION_MS
from .coordinator import NinstripConfigEntry, NinstripCoordinator
from .entity import NinstripEntity, NinstripSacnEntity
PARALLEL_UPDATES = 0
# Kanaele je Pixel, falls das Geraet sie nicht meldet. Die Firmware stellt fest
# drei ein (R, G, B) - so senden LedFx, xLights und Resolume.
_KANAELE_VORGABE = 3
@dataclass(frozen=True, kw_only=True)
class NinstripZeitDescription(NumberEntityDescription):
"""Ein Millisekundenfeld aus device/state."""
entity_category: EntityCategory = EntityCategory.CONFIG
device_class: NumberDeviceClass = NumberDeviceClass.DURATION
native_unit_of_measurement: str = UnitOfTime.MILLISECONDS
native_min_value: float = 0
native_step: float = 50
mode: NumberMode = NumberMode.BOX
# Grenzen wie in der Firmware (nl_api_device.c). Feldnamen wie in ninlight.
ZEITEN = (
NinstripZeitDescription(key="fade_in", translation_key="fade_in", native_max_value=MAX_FADE_MS),
NinstripZeitDescription(
key="fade_out", translation_key="fade_out", native_max_value=MAX_FADE_MS
),
NinstripZeitDescription(
key="transition_default",
translation_key="transition_default",
native_max_value=MAX_TRANSITION_MS,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: NinstripConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
coordinator = entry.runtime_data
entitaeten: list[NumberEntity] = [NinstripZeit(coordinator, d) for d in ZEITEN]
entitaeten.append(NinstripSacnUniverse(coordinator))
entitaeten.append(NinstripSacnStartSlot(coordinator))
entitaeten.append(NinstripStrombudget(coordinator))
async_add_entities(entitaeten)
class NinstripZeit(NinstripEntity, NumberEntity):
"""Ein Millisekundenfeld: Einblenden, Ausblenden, Vorgabe fuer Uebergaenge."""
entity_description: NinstripZeitDescription
def __init__(
self, coordinator: NinstripCoordinator, description: NinstripZeitDescription
) -> None:
super().__init__(coordinator, description.key)
self.entity_description = description
@property
def native_value(self) -> float | None:
wert = self.coordinator.data.state.get(self.entity_description.key)
return float(wert) if isinstance(wert, (int, float)) else None
async def async_set_native_value(self, value: float) -> None:
await self.coordinator.async_set_state(**{self.entity_description.key: int(value)})
class NinstripSacnUniverse(NinstripSacnEntity, NumberEntity):
"""Das erste Universe. Braucht der Strip mehr, folgen die naechsten."""
_attr_entity_category = EntityCategory.CONFIG
_attr_translation_key = "sacn_universe"
_attr_native_min_value = 1
_attr_native_max_value = 63_999
_attr_native_step = 1
_attr_mode = NumberMode.BOX
def __init__(self, coordinator: NinstripCoordinator) -> None:
super().__init__(coordinator, "sacn_universe")
@property
def native_value(self) -> float | None:
value = self.sacn.get("universe")
return float(value) if isinstance(value, (int, float)) else None
async def async_set_native_value(self, value: float) -> None:
await self.coordinator.async_set_sacn(universe=int(value))
class NinstripSacnStartSlot(NinstripSacnEntity, NumberEntity):
"""Der erste Slot - er gilt in JEDEM belegten Universe, nicht nur im ersten.
Anders als bei ninlight, wo eine Lampe ein einzelner Slotsatz ist: ein
Strip verteilt sich ueber mehrere Universes, und in jedem beginnt er an
derselben Stelle.
"""
_attr_entity_category = EntityCategory.CONFIG
_attr_translation_key = "sacn_start_slot"
_attr_native_min_value = 1
_attr_native_step = 1
_attr_mode = NumberMode.BOX
def __init__(self, coordinator: NinstripCoordinator) -> None:
super().__init__(coordinator, "sacn_start_slot")
@property
def native_max_value(self) -> float:
"""Der hoechste Startslot, hinter dem noch ein ganzes Pixel Platz hat.
Bei drei Kanaelen sind das 510. Ein festes 512 wuerde Werte anbieten,
bei denen kein einziges Pixel mehr ins Universe passt.
"""
kanaele = self.sacn.get("channels_per_pixel")
if not isinstance(kanaele, int) or not 1 <= kanaele <= 512:
kanaele = _KANAELE_VORGABE
return 513 - kanaele
@property
def native_value(self) -> float | None:
value = self.sacn.get("start_slot")
return float(value) if isinstance(value, (int, float)) else None
async def async_set_native_value(self, value: float) -> None:
await self.coordinator.async_set_sacn(start_slot=int(value))
class NinstripStrombudget(NinstripEntity, NumberEntity):
"""Was das Netzteil hergibt. Die Firmware regelt daran."""
_attr_entity_category = EntityCategory.CONFIG
_attr_translation_key = "max_milliamps"
_attr_native_min_value = 100
# Die Firmware nimmt fuer das Budget hoechstens 65535 mA an (uint16).
_attr_native_max_value = 65535
_attr_native_step = 100
_attr_mode = NumberMode.BOX
_attr_native_unit_of_measurement = UnitOfElectricCurrent.MILLIAMPERE
def __init__(self, coordinator: NinstripCoordinator) -> None:
super().__init__(coordinator, "max_milliamps")
@property
def native_value(self) -> float | None:
strip = self.coordinator.data.settings.get("strip")
wert = strip.get("max_milliamps") if isinstance(strip, dict) else None
return float(wert) if isinstance(wert, (int, float)) else None
async def async_set_native_value(self, value: float) -> None:
await self.coordinator.async_set_strip(max_milliamps=int(value))