feat: nachgezogen, was ha-ninlight laengst hat
Michaels Befund: die Integration ist nicht dasselbe wie die von ninlight. Verglichen und die Luecken geschlossen - alle Felder gibt es in der Firmware schon, sie waren nur nicht bedienbar: * number: fade_in, fade_out, transition_default in Millisekunden, mit den Grenzen der Firmware (10 s, 10 s, 60 s). * select: Einschaltverhalten (last, on, off). * switch: bei Aenderung einschalten. * light: Uebergangszeit beim Ein- und Ausschalten. HA rechnet in Sekunden, das Geraet in Millisekunden; laengeres als 60 s wird gekappt, sonst wiese das Geraet die ganze Anfrage mit 400 ab. Ohne Angabe wird kein Feld geschickt, sonst ueberschriebe jeder Schaltbefehl die Vorgabe. * sensor: letzter Update-Fehler und Startzeit (Zeitstempel statt einer Laufzeit, die jede Abfrage weiterlaeuft), beide ab Werk aus. * diagnostics.py: Diagnosedaten zum Herunterladen, ohne Token, Adressen, MAC und SSID. * config_flow: Neu-Konfigurieren von Adresse und Token, mit Gegenprobe der MAC - ein Tippfehler in der Adresse soll nicht still den falschen Eintrag umbiegen. * Hinweis in den Reparaturen, wenn ein Geraet noch auf dem oeffentlichen Werks-Token steht. Nicht uebernommen: die Auswahl sacn_layout. ninstrip ist immer rgb, ein Strip hat keinen Masterkanal - eine Auswahl mit einer Option waere nur Zierde. 241 Tests, 97 %. Die Kappung der Uebergangszeit per Mutation gegengeprueft.
This commit is contained in:
@@ -3,14 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .api import NinstripClient
|
||||
from .const import DEFAULT_PORT, DOMAIN
|
||||
from .const import DEFAULT_PORT, DEFAULT_TOKEN, DOMAIN
|
||||
from .coordinator import NinstripConfigEntry, NinstripCoordinator
|
||||
from .shared import async_get_shared, async_start_catalog, async_stop_catalog
|
||||
|
||||
@@ -46,6 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: NinstripConfigEntry) ->
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
_async_werks_token_hinweis(hass, entry)
|
||||
await async_start_catalog(hass, async_get_shared(hass))
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
@@ -66,6 +67,30 @@ async def async_unload_entry(hass: HomeAssistant, entry: NinstripConfigEntry) ->
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: NinstripConfigEntry) -> None:
|
||||
ir.async_delete_issue(hass, DOMAIN, f"wifi_guard_{entry.entry_id}")
|
||||
ir.async_delete_issue(hass, DOMAIN, f"default_token_{entry.entry_id}")
|
||||
|
||||
|
||||
@callback
|
||||
def _async_werks_token_hinweis(hass: HomeAssistant, entry: NinstripConfigEntry) -> None:
|
||||
"""Weist darauf hin, dass ein Geraet noch auf dem oeffentlichen Token steht.
|
||||
|
||||
Das Werks-Token steht im Repo. Wer es kennt und im selben Netz ist, kann
|
||||
das Geraet umkonfigurieren, neu starten, flashen und zuruecksetzen. Wie in
|
||||
ha-ninlight.
|
||||
"""
|
||||
issue_id = f"default_token_{entry.entry_id}"
|
||||
if entry.data[CONF_TOKEN] == DEFAULT_TOKEN:
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="default_token",
|
||||
translation_placeholders={"name": entry.title},
|
||||
)
|
||||
else:
|
||||
ir.async_delete_issue(hass, DOMAIN, issue_id)
|
||||
|
||||
|
||||
# Kein add_update_listener zum Neuladen: der Einstellungsdialog ist ein
|
||||
|
||||
@@ -125,6 +125,37 @@ class NinstripConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Adresse und Token eines bestehenden Geraets aendern.
|
||||
|
||||
Gegengeprueft wird die MAC: wer aus Versehen die Adresse eines anderen
|
||||
Geraets eintraegt, soll nicht stillschweigend den falschen Eintrag
|
||||
umbiegen.
|
||||
"""
|
||||
eintrag = self._get_reconfigure_entry()
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST].strip()
|
||||
info, errors = await self._async_probe(host, user_input[CONF_TOKEN])
|
||||
if info is not None:
|
||||
await self.async_set_unique_id(format_mac(info["mac"]))
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_device")
|
||||
return self.async_update_reload_and_abort(
|
||||
eintrag, data_updates={CONF_HOST: host, CONF_TOKEN: user_input[CONF_TOKEN]}
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST, default=eintrag.data[CONF_HOST]): str,
|
||||
vol.Required(CONF_TOKEN, default=eintrag.data[CONF_TOKEN]): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> ConfigFlowResult:
|
||||
self._host = entry_data[CONF_HOST]
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
@@ -27,6 +27,12 @@ MAX_SCAN_INTERVAL: Final = 300
|
||||
# sofort alle Entitaeten ausgrauen.
|
||||
MAX_POLL_FAILURES: Final = 3
|
||||
|
||||
# Laengster Uebergang, den die Firmware annimmt (nl_api_device.c:
|
||||
# "transition ausserhalb 0..60000 ms").
|
||||
MAX_TRANSITION_MS: Final = 60_000
|
||||
# Die drei Millisekunden-Felder in device/state, mit den Grenzen der Firmware.
|
||||
MAX_FADE_MS: Final = 10_000
|
||||
|
||||
# Die Firmware erlaubt hoechstens 16 Segmente (nl_segments.h) und 512 Pixel.
|
||||
MAX_SEGMENTS: Final = 16
|
||||
MAX_PIXELS: Final = 512
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Diagnosedaten zum Herunterladen, fuer Fehlermeldungen."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_HOST, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import NinstripConfigEntry
|
||||
from .shared import DATA_SHARED
|
||||
|
||||
# Token, Adressen und Kennungen fliegen raus: so eine Datei landet schnell in
|
||||
# einem Fehlerbericht.
|
||||
TO_REDACT = {CONF_TOKEN, CONF_HOST, "ssid", "mac", "ip"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: NinstripConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
coordinator = entry.runtime_data
|
||||
device_type = coordinator.data.info.get("device_type")
|
||||
shared = hass.data.get(DATA_SHARED)
|
||||
abbilder = (
|
||||
[]
|
||||
if shared is None
|
||||
else [
|
||||
{"file": image.path.name, "device_type": image.device_type, "version": image.version}
|
||||
for image in shared.catalog.images
|
||||
if image.device_type == device_type
|
||||
]
|
||||
)
|
||||
return {
|
||||
**async_redact_data(
|
||||
{
|
||||
"entry": {"data": dict(entry.data), "options": dict(entry.options)},
|
||||
"state": coordinator.data.state,
|
||||
"info": coordinator.data.info,
|
||||
"segments": coordinator.data.segments,
|
||||
"settings": coordinator.data.settings,
|
||||
},
|
||||
TO_REDACT,
|
||||
),
|
||||
"firmware_images": abbilder,
|
||||
}
|
||||
@@ -15,12 +15,15 @@ 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 NinstripConfigEntry, NinstripCoordinator
|
||||
from .entity import NinstripEntity
|
||||
|
||||
@@ -31,6 +34,15 @@ MIN_KELVIN = 2700
|
||||
MAX_KELVIN = 6500
|
||||
|
||||
|
||||
def uebergang_ms(sekunden: float) -> int:
|
||||
"""Home Assistant rechnet in Sekunden, das Geraet in Millisekunden.
|
||||
|
||||
Gekappt auf das, was die Firmware annimmt - ein laengerer Wert wuerde
|
||||
sonst die ganze Anfrage mit 400 abweisen, statt nur langsamer zu blenden.
|
||||
"""
|
||||
return max(0, min(MAX_TRANSITION_MS, round(sekunden * 1000)))
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: NinstripConfigEntry,
|
||||
@@ -44,6 +56,7 @@ class NinstripLight(NinstripEntity, LightEntity):
|
||||
|
||||
_attr_name = None
|
||||
_attr_supported_color_modes = {ColorMode.RGB, ColorMode.COLOR_TEMP}
|
||||
_attr_supported_features = LightEntityFeature.TRANSITION
|
||||
_attr_min_color_temp_kelvin = MIN_KELVIN
|
||||
_attr_max_color_temp_kelvin = MAX_KELVIN
|
||||
|
||||
@@ -131,7 +144,12 @@ class NinstripLight(NinstripEntity, LightEntity):
|
||||
fields: dict[str, Any] = {"on": True}
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
fields["brightness"] = round(int(kwargs[ATTR_BRIGHTNESS]) * 100 / 255)
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fields["transition"] = uebergang_ms(kwargs[ATTR_TRANSITION])
|
||||
await self.coordinator.async_set_state(**fields)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
await self.coordinator.async_set_state(on=False)
|
||||
fields: dict[str, Any] = {"on": False}
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fields["transition"] = uebergang_ms(kwargs[ATTR_TRANSITION])
|
||||
await self.coordinator.async_set_state(**fields)
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
"""sACN-Zahlenfelder eines ninstrip-Geraets: Universe und Startslot."""
|
||||
"""Zahlenfelder eines ninstrip-Geraets: Uebergangszeiten und sACN."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberMode
|
||||
from homeassistant.const import EntityCategory
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberDeviceClass,
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.const import EntityCategory, 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 NinstripSacnEntity
|
||||
from .entity import NinstripEntity, NinstripSacnEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -17,13 +25,62 @@ PARALLEL_UPDATES = 0
|
||||
_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
|
||||
async_add_entities([NinstripSacnUniverse(coordinator), NinstripSacnStartSlot(coordinator)])
|
||||
entitaeten: list[NumberEntity] = [NinstripZeit(coordinator, d) for d in ZEITEN]
|
||||
entitaeten.append(NinstripSacnUniverse(coordinator))
|
||||
entitaeten.append(NinstripSacnStartSlot(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):
|
||||
|
||||
@@ -17,6 +17,9 @@ NONE_OPTION = "—"
|
||||
# Was das Geraet 2,5 s nach dem letzten sACN-Paket tut. Werte wie ha-ninlight.
|
||||
ON_TIMEOUT_OPTIONS = ["release", "hold"]
|
||||
|
||||
# Was nach einer Stromunterbrechung passiert (nl_settings.c: last, on, off).
|
||||
POWER_ON_OPTIONS = ["last", "on", "off"]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -25,10 +28,33 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
[NinstripPresetSelect(coordinator), NinstripSacnOnTimeoutSelect(coordinator)]
|
||||
[
|
||||
NinstripPresetSelect(coordinator),
|
||||
NinstripPowerOnSelect(coordinator),
|
||||
NinstripSacnOnTimeoutSelect(coordinator),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class NinstripPowerOnSelect(NinstripEntity, SelectEntity):
|
||||
"""Was der Strip tut, wenn der Strom wiederkommt."""
|
||||
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_translation_key = "power_on_behaviour"
|
||||
_attr_options = POWER_ON_OPTIONS
|
||||
|
||||
def __init__(self, coordinator: NinstripCoordinator) -> None:
|
||||
super().__init__(coordinator, "power_on_behaviour")
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
wert = self.coordinator.data.state.get("power_on_behaviour")
|
||||
return wert if wert in POWER_ON_OPTIONS else None
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
await self.coordinator.async_set_state(power_on_behaviour=option)
|
||||
|
||||
|
||||
class NinstripPresetSelect(NinstripEntity, SelectEntity):
|
||||
"""Waehlt eine der belegten Szenen aus."""
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -18,12 +19,18 @@ from homeassistant.const import (
|
||||
UnitOfInformation,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .coordinator import NinstripConfigEntry, NinstripCoordinator, NinstripData
|
||||
from .entity import NinstripEntity
|
||||
|
||||
# Die Laufzeit springt bei jeder Abfrage um ein paar Sekunden; daraus eine
|
||||
# Startzeit zu rechnen ergaebe sonst dauernd einen neuen Zeitstempel. Wie
|
||||
# ha-ninlight.
|
||||
BOOT_TIME_TOLERANCE = timedelta(seconds=30)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class NinstripSensorDescription(SensorEntityDescription):
|
||||
@@ -91,6 +98,15 @@ SENSORS: tuple[NinstripSensorDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value=lambda d: _strip(d, "power_scale"),
|
||||
),
|
||||
# Was bei der letzten Aktualisierung schiefging - leer, solange nichts
|
||||
# schiefging. Aus der Ferne der einzige Hinweis darauf.
|
||||
NinstripSensorDescription(
|
||||
key="last_ota_error",
|
||||
translation_key="last_ota_error",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value=lambda d: d.info.get("last_ota_error"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -100,7 +116,9 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(NinstripSensor(coordinator, d) for d in SENSORS)
|
||||
entitaeten: list[SensorEntity] = [NinstripSensor(coordinator, d) for d in SENSORS]
|
||||
entitaeten.append(NinstripBootTimeSensor(coordinator))
|
||||
async_add_entities(entitaeten)
|
||||
|
||||
|
||||
class NinstripSensor(NinstripEntity, SensorEntity):
|
||||
@@ -115,3 +133,37 @@ class NinstripSensor(NinstripEntity, SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> Any:
|
||||
return self.entity_description.value(self.coordinator.data)
|
||||
|
||||
|
||||
class NinstripBootTimeSensor(NinstripEntity, SensorEntity):
|
||||
"""Wann der Strip zuletzt gestartet ist - ein Zeitstempel statt einer
|
||||
Laufzeit, die jede Abfrage weiterlaeuft."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.TIMESTAMP
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_entity_registry_enabled_default = False
|
||||
_attr_translation_key = "boot_time"
|
||||
|
||||
def __init__(self, coordinator: NinstripCoordinator) -> None:
|
||||
super().__init__(coordinator, "boot_time")
|
||||
self._boot: datetime | None = None
|
||||
self._boot_aktualisieren()
|
||||
|
||||
def _boot_aktualisieren(self) -> None:
|
||||
uptime = self.coordinator.data.info.get("uptime")
|
||||
if not isinstance(uptime, (int, float)):
|
||||
return
|
||||
boot = dt_util.utcnow() - timedelta(seconds=uptime)
|
||||
# Nur uebernehmen, wenn er wirklich abweicht - sonst wandert der
|
||||
# Zeitstempel bei jeder Abfrage um die Messungenauigkeit.
|
||||
if self._boot is None or abs(boot - self._boot) > BOOT_TIME_TOLERANCE:
|
||||
self._boot = boot
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
self._boot_aktualisieren()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def native_value(self) -> datetime | None:
|
||||
return self._boot
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
"data": {
|
||||
"token": "API-Token"
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Gerät ändern",
|
||||
"description": "Adresse und API-Token des Geräts {host}. Die MAC wird gegengeprüft — ein anderes Gerät wird abgelehnt.",
|
||||
"data": {
|
||||
"host": "Adresse",
|
||||
"token": "API-Token"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -32,7 +40,9 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Dieses Gerät ist bereits eingerichtet",
|
||||
"reauth_successful": "Token erneuert"
|
||||
"reauth_successful": "Token erneuert",
|
||||
"wrong_device": "Dort antwortet ein anderes Gerät",
|
||||
"reconfigure_successful": "Gerät geändert"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -64,6 +74,14 @@
|
||||
"release": "Zurück zum vorherigen Bild",
|
||||
"hold": "Letztes Bild halten"
|
||||
}
|
||||
},
|
||||
"power_on_behaviour": {
|
||||
"name": "Einschaltverhalten",
|
||||
"state": {
|
||||
"last": "Wie zuvor",
|
||||
"on": "An",
|
||||
"off": "Aus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
@@ -86,11 +104,20 @@
|
||||
},
|
||||
"power_scale": {
|
||||
"name": "Leistungsbegrenzung"
|
||||
},
|
||||
"last_ota_error": {
|
||||
"name": "Letzter Update-Fehler"
|
||||
},
|
||||
"boot_time": {
|
||||
"name": "Gestartet"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"sacn_enabled": {
|
||||
"name": "sACN-Empfang"
|
||||
},
|
||||
"wake_on_change": {
|
||||
"name": "Bei Änderung einschalten"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
@@ -99,6 +126,15 @@
|
||||
},
|
||||
"sacn_start_slot": {
|
||||
"name": "sACN-Startslot"
|
||||
},
|
||||
"fade_in": {
|
||||
"name": "Einblendzeit"
|
||||
},
|
||||
"fade_out": {
|
||||
"name": "Ausblendzeit"
|
||||
},
|
||||
"transition_default": {
|
||||
"name": "Übergangszeit (Vorgabe)"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
@@ -157,6 +193,10 @@
|
||||
"wifi_guard": {
|
||||
"title": "Firmware für {name} wird nicht angeboten",
|
||||
"description": "Im Firmware-Ordner liegt {version} für {device_type}, aber ohne WLAN-Zugangsdaten für {ssid}. Das Gerät kennt sein WLAN nur aus der Firmware und verlöre nach dieser Aktualisierung seine Netzverbindung. Deshalb wird sie nicht angeboten. Wer das Gerät stattdessen über das Onboarding ins WLAN gebracht hat, bekommt das Update ohne diesen Vorbehalt."
|
||||
},
|
||||
"default_token": {
|
||||
"title": "{name} nutzt das öffentliche Werks-Token",
|
||||
"description": "Mit dem öffentlichen Werks-Token kann jeder im selben Netz dieses Gerät umkonfigurieren, neu starten, neu flashen oder auf Werk zurücksetzen — dabei verliert es seine WLAN-Daten und fällt in den Onboarding-AP zurück. Schalten und Dimmen steht ohnehin jedem im Netz offen, solange `auth_all` aus ist. Setze am Gerät ein eigenes Token und trage es über „Neu konfigurieren\" ein."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""sACN-Schalter eines ninstrip-Geraets."""
|
||||
"""Schalter eines ninstrip-Geraets: Aufwecken bei Aenderung und sACN."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import NinstripConfigEntry, NinstripCoordinator
|
||||
from .entity import NinstripSacnEntity
|
||||
from .entity import NinstripEntity, NinstripSacnEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -20,7 +20,30 @@ async def async_setup_entry(
|
||||
entry: NinstripConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
async_add_entities([NinstripSacnSwitch(entry.runtime_data)])
|
||||
async_add_entities(
|
||||
[NinstripWakeSwitch(entry.runtime_data), NinstripSacnSwitch(entry.runtime_data)]
|
||||
)
|
||||
|
||||
|
||||
class NinstripWakeSwitch(NinstripEntity, SwitchEntity):
|
||||
"""Schaltet den Strip ein, sobald Farbe, Farbtemperatur oder Helligkeit
|
||||
geaendert werden - statt die Aenderung nur zu merken."""
|
||||
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_translation_key = "wake_on_change"
|
||||
|
||||
def __init__(self, coordinator: NinstripCoordinator) -> None:
|
||||
super().__init__(coordinator, "wake_on_change")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
return bool(self.coordinator.data.state.get("wake_on_change"))
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
await self.coordinator.async_set_state(wake_on_change=True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
await self.coordinator.async_set_state(wake_on_change=False)
|
||||
|
||||
|
||||
class NinstripSacnSwitch(NinstripSacnEntity, SwitchEntity):
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
"data": {
|
||||
"token": "API-Token"
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Gerät ändern",
|
||||
"description": "Adresse und API-Token des Geräts {host}. Die MAC wird gegengeprüft — ein anderes Gerät wird abgelehnt.",
|
||||
"data": {
|
||||
"host": "Adresse",
|
||||
"token": "API-Token"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -32,7 +40,9 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Dieses Gerät ist bereits eingerichtet",
|
||||
"reauth_successful": "Token erneuert"
|
||||
"reauth_successful": "Token erneuert",
|
||||
"wrong_device": "Dort antwortet ein anderes Gerät",
|
||||
"reconfigure_successful": "Gerät geändert"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -64,6 +74,14 @@
|
||||
"release": "Zurück zum vorherigen Bild",
|
||||
"hold": "Letztes Bild halten"
|
||||
}
|
||||
},
|
||||
"power_on_behaviour": {
|
||||
"name": "Einschaltverhalten",
|
||||
"state": {
|
||||
"last": "Wie zuvor",
|
||||
"on": "An",
|
||||
"off": "Aus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
@@ -86,11 +104,20 @@
|
||||
},
|
||||
"power_scale": {
|
||||
"name": "Leistungsbegrenzung"
|
||||
},
|
||||
"last_ota_error": {
|
||||
"name": "Letzter Update-Fehler"
|
||||
},
|
||||
"boot_time": {
|
||||
"name": "Gestartet"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"sacn_enabled": {
|
||||
"name": "sACN-Empfang"
|
||||
},
|
||||
"wake_on_change": {
|
||||
"name": "Bei Änderung einschalten"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
@@ -99,6 +126,15 @@
|
||||
},
|
||||
"sacn_start_slot": {
|
||||
"name": "sACN-Startslot"
|
||||
},
|
||||
"fade_in": {
|
||||
"name": "Einblendzeit"
|
||||
},
|
||||
"fade_out": {
|
||||
"name": "Ausblendzeit"
|
||||
},
|
||||
"transition_default": {
|
||||
"name": "Übergangszeit (Vorgabe)"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
@@ -157,6 +193,10 @@
|
||||
"wifi_guard": {
|
||||
"title": "Firmware für {name} wird nicht angeboten",
|
||||
"description": "Im Firmware-Ordner liegt {version} für {device_type}, aber ohne WLAN-Zugangsdaten für {ssid}. Das Gerät kennt sein WLAN nur aus der Firmware und verlöre nach dieser Aktualisierung seine Netzverbindung. Deshalb wird sie nicht angeboten. Wer das Gerät stattdessen über das Onboarding ins WLAN gebracht hat, bekommt das Update ohne diesen Vorbehalt."
|
||||
},
|
||||
"default_token": {
|
||||
"title": "{name} nutzt das öffentliche Werks-Token",
|
||||
"description": "Mit dem öffentlichen Werks-Token kann jeder im selben Netz dieses Gerät umkonfigurieren, neu starten, neu flashen oder auf Werk zurücksetzen — dabei verliert es seine WLAN-Daten und fällt in den Onboarding-AP zurück. Schalten und Dimmen steht ohnehin jedem im Netz offen, solange `auth_all` aus ist. Setze am Gerät ein eigenes Token und trage es über „Neu konfigurieren\" ein."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
"data": {
|
||||
"token": "API token"
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Change device",
|
||||
"description": "Address and API token of device {host}. The MAC is checked — a different device is rejected.",
|
||||
"data": {
|
||||
"host": "Address",
|
||||
"token": "API token"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -32,7 +40,9 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This device is already configured",
|
||||
"reauth_successful": "Token renewed"
|
||||
"reauth_successful": "Token renewed",
|
||||
"wrong_device": "A different device answers there",
|
||||
"reconfigure_successful": "Device changed"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -64,6 +74,14 @@
|
||||
"release": "Return to previous image",
|
||||
"hold": "Hold last image"
|
||||
}
|
||||
},
|
||||
"power_on_behaviour": {
|
||||
"name": "Power-on behaviour",
|
||||
"state": {
|
||||
"last": "As before",
|
||||
"on": "On",
|
||||
"off": "Off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
@@ -86,11 +104,20 @@
|
||||
},
|
||||
"power_scale": {
|
||||
"name": "Power limiting"
|
||||
},
|
||||
"last_ota_error": {
|
||||
"name": "Last update error"
|
||||
},
|
||||
"boot_time": {
|
||||
"name": "Started"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"sacn_enabled": {
|
||||
"name": "sACN receiver"
|
||||
},
|
||||
"wake_on_change": {
|
||||
"name": "Turn on when changed"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
@@ -99,6 +126,15 @@
|
||||
},
|
||||
"sacn_start_slot": {
|
||||
"name": "sACN start slot"
|
||||
},
|
||||
"fade_in": {
|
||||
"name": "Fade-in time"
|
||||
},
|
||||
"fade_out": {
|
||||
"name": "Fade-out time"
|
||||
},
|
||||
"transition_default": {
|
||||
"name": "Default transition time"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
@@ -157,6 +193,10 @@
|
||||
"wifi_guard": {
|
||||
"title": "Firmware for {name} not offered",
|
||||
"description": "The firmware folder contains {version} for {device_type}, but without Wi-Fi credentials for {ssid}. The device only knows its Wi-Fi from its firmware and would lose its network connection after this update, so the update is not offered. A device that was put on the network through onboarding instead gets the update without this reservation."
|
||||
},
|
||||
"default_token": {
|
||||
"title": "{name} uses the public default token",
|
||||
"description": "With the public default token, anyone on your network can reconfigure, restart, reflash or factory-reset this device, losing its Wi-Fi settings and falling back to its onboarding access point. Switching and dimming are open to anyone on the network anyway while `auth_all` is off. Set your own token on the device and enter it via \"Reconfigure\"."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user