feat: Coordinator, Geraet und Licht-Entitaet
This commit is contained in:
@@ -1 +1,65 @@
|
||||
"""The ninlight integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .api import NinlightClient
|
||||
from .const import DEFAULT_TOKEN, DOMAIN
|
||||
from .coordinator import NinlightConfigEntry, NinlightCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.LIGHT]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: NinlightConfigEntry) -> bool:
|
||||
client = NinlightClient(
|
||||
async_get_clientsession(hass), entry.data[CONF_HOST], entry.data[CONF_TOKEN]
|
||||
)
|
||||
coordinator = NinlightCoordinator(hass, entry, client)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
_async_update_default_token_issue(hass, entry)
|
||||
|
||||
@callback
|
||||
def _sync_firmware_version() -> None:
|
||||
info = coordinator.data.info
|
||||
registry = dr.async_get(hass)
|
||||
device = registry.async_get_device_by_identifier(
|
||||
(DOMAIN, format_mac(info["mac"])), entry.entry_id
|
||||
)
|
||||
if device is not None and device.sw_version != info["fw_version"]:
|
||||
registry.async_update_device(device.id, sw_version=info["fw_version"])
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_sync_firmware_version))
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: NinlightConfigEntry) -> bool:
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: NinlightConfigEntry) -> None:
|
||||
ir.async_delete_issue(hass, DOMAIN, f"default_token_{entry.entry_id}")
|
||||
|
||||
|
||||
@callback
|
||||
def _async_update_default_token_issue(hass: HomeAssistant, entry: NinlightConfigEntry) -> None:
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Minimal config flow stub; Task 5 replaces this with the real flow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class NinlightConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Placeholder so manifest.json's config_flow: true can be imported."""
|
||||
|
||||
VERSION = 1
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Data coordinator for one ninlight lamp."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .api import (
|
||||
NinlightAuthError,
|
||||
NinlightBusyError,
|
||||
NinlightClient,
|
||||
NinlightError,
|
||||
NinlightRequestError,
|
||||
)
|
||||
from .const import CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type NinlightConfigEntry = ConfigEntry[NinlightCoordinator]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class NinlightData:
|
||||
"""Last known lamp state and system info."""
|
||||
|
||||
state: dict[str, Any]
|
||||
info: dict[str, Any]
|
||||
|
||||
|
||||
class NinlightCoordinator(DataUpdateCoordinator[NinlightData]):
|
||||
"""Polls one lamp; every request to the lamp goes through `lock`."""
|
||||
|
||||
config_entry: NinlightConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, entry: NinlightConfigEntry, client: NinlightClient
|
||||
) -> None:
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"{DOMAIN} {client.host}",
|
||||
update_interval=timedelta(
|
||||
seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
),
|
||||
)
|
||||
self.client = client
|
||||
# The W600 web server has a listen backlog of 4: never talk to one lamp concurrently.
|
||||
self.lock = asyncio.Lock()
|
||||
# While a firmware update runs, the update entity watches the lamp itself.
|
||||
self.ota_in_progress = False
|
||||
|
||||
async def _async_update_data(self) -> NinlightData:
|
||||
if self.ota_in_progress and self.data is not None:
|
||||
return self.data
|
||||
try:
|
||||
async with self.lock:
|
||||
state = await self.client.get_state()
|
||||
info = await self.client.get_info()
|
||||
except NinlightAuthError as err:
|
||||
raise ConfigEntryAuthFailed(f"{self.client.host}: token rejected") from err
|
||||
except NinlightError as err:
|
||||
raise UpdateFailed(f"{self.client.host}: {err}") from err
|
||||
return NinlightData(state, info)
|
||||
|
||||
async def async_call[T](self, action: Callable[[NinlightClient], Awaitable[T]]) -> T:
|
||||
"""Run one request under the lock and translate lamp errors for the UI."""
|
||||
try:
|
||||
async with self.lock:
|
||||
return await action(self.client)
|
||||
except NinlightAuthError as err:
|
||||
self.config_entry.async_start_reauth(self.hass)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="auth_failed"
|
||||
) from err
|
||||
except NinlightBusyError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="update_busy"
|
||||
) from err
|
||||
except NinlightRequestError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="request_failed",
|
||||
translation_placeholders={"message": err.message},
|
||||
) from err
|
||||
except NinlightError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="cannot_connect"
|
||||
) from err
|
||||
|
||||
async def async_set_state(self, **fields: Any) -> None:
|
||||
"""Write and adopt the lamp's complete answer directly - no debounced refresh."""
|
||||
state = await self.async_call(lambda client: client.set_state(**fields))
|
||||
self.async_set_updated_data(NinlightData(state, self.data.info))
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Base entity and device info for ninlight lamps."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import NinlightCoordinator
|
||||
|
||||
|
||||
def device_name(info: dict[str, Any]) -> str:
|
||||
"""The lamp's own name, else '<product> <last four MAC digits>'."""
|
||||
name = str(info.get("name") or "").strip()
|
||||
if name:
|
||||
return name
|
||||
mac4 = format_mac(info["mac"]).replace(":", "")[-4:]
|
||||
product = info.get("product_name") or info["device_type"]
|
||||
return f"{product} {mac4}"
|
||||
|
||||
|
||||
class NinlightEntity(CoordinatorEntity[NinlightCoordinator]):
|
||||
"""Common base: unique id from the MAC, one device per lamp."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: NinlightCoordinator, key: str) -> None:
|
||||
super().__init__(coordinator)
|
||||
info = coordinator.data.info
|
||||
mac = format_mac(info["mac"])
|
||||
self._attr_unique_id = f"{mac}_{key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, mac)},
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, mac)},
|
||||
manufacturer="ninlight",
|
||||
model=info.get("product_name") or None,
|
||||
model_id=info["device_type"],
|
||||
name=device_name(info),
|
||||
sw_version=info["fw_version"],
|
||||
)
|
||||
@@ -0,0 +1,126 @@
|
||||
"""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"]))
|
||||
|
||||
@property
|
||||
def supported_color_modes(self) -> set[ColorMode]:
|
||||
if self._state["capabilities"].get("color"):
|
||||
return {ColorMode.RGB, ColorMode.COLOR_TEMP}
|
||||
return {ColorMode.COLOR_TEMP}
|
||||
|
||||
@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)
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up ninlight lamp",
|
||||
"description": "Enter the address of the lamp. The token is prefilled with the public default token – anyone on your network who knows it can control and reflash the lamp.",
|
||||
"data": { "host": "Host", "token": "Token" },
|
||||
"data_description": {
|
||||
"host": "IP address or ninlight-xxxx.local name of the lamp.",
|
||||
"token": "Authorization token of the lamp."
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"title": "Set up {name}",
|
||||
"description": "Found the ninlight lamp {name} at {host}. The token is prefilled with the public default token – anyone on your network who knows it can control and reflash the lamp.",
|
||||
"data": { "token": "Token" }
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "Re-authenticate {name}",
|
||||
"description": "The lamp rejected the stored token. Enter its current token.",
|
||||
"data": { "token": "Token" }
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Reconfigure lamp",
|
||||
"data": { "host": "Host", "token": "Token" }
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Cannot reach the lamp.",
|
||||
"invalid_auth": "The lamp rejected the token.",
|
||||
"not_ninlight": "The device did not answer like a ninlight lamp.",
|
||||
"unknown": "Unexpected error."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This lamp is already set up.",
|
||||
"not_ninlight": "The device did not announce itself like a ninlight lamp.",
|
||||
"wrong_device": "This address belongs to a different lamp.",
|
||||
"reauth_successful": "Re-authentication was successful.",
|
||||
"reconfigure_successful": "Reconfiguration was successful."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "ninlight options",
|
||||
"data": {
|
||||
"scan_interval": "Polling interval (seconds)",
|
||||
"lamp_base_url": "Address lamps use to reach Home Assistant"
|
||||
},
|
||||
"data_description": {
|
||||
"lamp_base_url": "Only needed for firmware updates if lamps cannot reach Home Assistant's internal URL over plain http, e.g. http://192.168.1.50:8123"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_base_url": "Must be a plain http:// address with host and optional port, without path.",
|
||||
"base_url_too_long": "Address too long – the lamp accepts at most 255 characters for the whole firmware URL."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"number": {
|
||||
"fade_in": { "name": "Fade-in" },
|
||||
"fade_out": { "name": "Fade-out" },
|
||||
"transition_default": { "name": "Default transition" }
|
||||
},
|
||||
"select": {
|
||||
"power_on_behaviour": {
|
||||
"name": "Power-on behaviour",
|
||||
"state": { "last": "Last state", "on": "On", "off": "Off" }
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"wake_on_change": { "name": "Turn on when changed" }
|
||||
},
|
||||
"sensor": {
|
||||
"ip_address": { "name": "IP address" },
|
||||
"free_heap": { "name": "Free memory" },
|
||||
"last_ota_error": { "name": "Last update error" },
|
||||
"boot_time": { "name": "Last start" }
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": { "message": "Cannot reach the lamp." },
|
||||
"auth_failed": { "message": "The lamp rejected the token." },
|
||||
"request_failed": { "message": "The lamp rejected the request: {message}" },
|
||||
"update_busy": { "message": "An update is already running on the lamp." },
|
||||
"no_update": { "message": "No firmware update is available." },
|
||||
"no_http_url": { "message": "Lamps can only reach Home Assistant over plain http. Set the address in the integration options." },
|
||||
"lamp_url_too_long": { "message": "The firmware URL is longer than the 255 characters the lamp accepts." },
|
||||
"image_invalid": { "message": "The firmware file changed or is no longer valid." },
|
||||
"wifi_guard": { "message": "This firmware does not contain the Wi-Fi credentials for {ssid}; the lamp would lose its network connection." },
|
||||
"update_failed": { "message": "The update failed: {reason}" },
|
||||
"update_timeout": { "message": "The lamp did not report the new firmware within 12 minutes." }
|
||||
},
|
||||
"issues": {
|
||||
"default_token": {
|
||||
"title": "{name} uses the public default token",
|
||||
"description": "Anyone on your network can control and reflash this lamp with the default token. Set your own token on the lamp and enter it via reconfigure."
|
||||
},
|
||||
"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 lamp only knows its Wi-Fi from its firmware and would lose its network connection after this update, so the update is not offered."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "ninlight-Lampe einrichten",
|
||||
"description": "Adresse der Lampe eingeben. Das Token ist mit dem öffentlichen Vorgabe-Token vorbelegt – wer es kennt, kann die Lampe in deinem Netz steuern und neu flashen.",
|
||||
"data": { "host": "Host", "token": "Token" },
|
||||
"data_description": {
|
||||
"host": "IP-Adresse oder ninlight-xxxx.local-Name der Lampe.",
|
||||
"token": "Autorisierungs-Token der Lampe."
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"title": "{name} einrichten",
|
||||
"description": "ninlight-Lampe {name} unter {host} gefunden. Das Token ist mit dem öffentlichen Vorgabe-Token vorbelegt – wer es kennt, kann die Lampe in deinem Netz steuern und neu flashen.",
|
||||
"data": { "token": "Token" }
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "{name} neu anmelden",
|
||||
"description": "Die Lampe hat das gespeicherte Token abgelehnt. Gib ihr aktuelles Token ein.",
|
||||
"data": { "token": "Token" }
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Lampe neu konfigurieren",
|
||||
"data": { "host": "Host", "token": "Token" }
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Die Lampe ist nicht erreichbar.",
|
||||
"invalid_auth": "Die Lampe hat das Token abgelehnt.",
|
||||
"not_ninlight": "Das Gerät antwortet nicht wie eine ninlight-Lampe.",
|
||||
"unknown": "Unerwarteter Fehler."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Diese Lampe ist bereits eingerichtet.",
|
||||
"not_ninlight": "Das Gerät meldet sich nicht wie eine ninlight-Lampe.",
|
||||
"wrong_device": "Unter dieser Adresse antwortet eine andere Lampe.",
|
||||
"reauth_successful": "Neuanmeldung erfolgreich.",
|
||||
"reconfigure_successful": "Neukonfiguration erfolgreich."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "ninlight-Optionen",
|
||||
"data": {
|
||||
"scan_interval": "Abfrageintervall (Sekunden)",
|
||||
"lamp_base_url": "Adresse, unter der Lampen Home Assistant erreichen"
|
||||
},
|
||||
"data_description": {
|
||||
"lamp_base_url": "Nur für Firmware-Updates nötig, wenn die Lampen die interne URL von Home Assistant nicht über reines http erreichen, z. B. http://192.168.1.50:8123"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_base_url": "Muss eine reine http://-Adresse mit Host und optionalem Port sein, ohne Pfad.",
|
||||
"base_url_too_long": "Adresse zu lang – die Lampe akzeptiert höchstens 255 Zeichen für die gesamte Firmware-URL."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"number": {
|
||||
"fade_in": { "name": "Einblenden" },
|
||||
"fade_out": { "name": "Ausblenden" },
|
||||
"transition_default": { "name": "Standardübergang" }
|
||||
},
|
||||
"select": {
|
||||
"power_on_behaviour": {
|
||||
"name": "Einschaltverhalten",
|
||||
"state": { "last": "Letzter Zustand", "on": "Ein", "off": "Aus" }
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"wake_on_change": { "name": "Bei Änderung einschalten" }
|
||||
},
|
||||
"sensor": {
|
||||
"ip_address": { "name": "IP-Adresse" },
|
||||
"free_heap": { "name": "Freier Speicher" },
|
||||
"last_ota_error": { "name": "Letzter Update-Fehler" },
|
||||
"boot_time": { "name": "Letzter Start" }
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": { "message": "Die Lampe ist nicht erreichbar." },
|
||||
"auth_failed": { "message": "Die Lampe hat das Token abgelehnt." },
|
||||
"request_failed": { "message": "Die Lampe hat die Anfrage abgelehnt: {message}" },
|
||||
"update_busy": { "message": "Auf der Lampe läuft bereits ein Update." },
|
||||
"no_update": { "message": "Kein Firmware-Update verfügbar." },
|
||||
"no_http_url": { "message": "Lampen erreichen Home Assistant nur über reines http. Setze die Adresse in den Optionen der Integration." },
|
||||
"lamp_url_too_long": { "message": "Die Firmware-URL ist länger als die 255 Zeichen, die die Lampe akzeptiert." },
|
||||
"image_invalid": { "message": "Die Firmware-Datei hat sich geändert oder ist nicht mehr gültig." },
|
||||
"wifi_guard": { "message": "Diese Firmware enthält keine WLAN-Zugangsdaten für {ssid}; die Lampe wäre danach nicht mehr im Netz." },
|
||||
"update_failed": { "message": "Das Update ist fehlgeschlagen: {reason}" },
|
||||
"update_timeout": { "message": "Die Lampe hat die neue Firmware nicht innerhalb von 12 Minuten gemeldet." }
|
||||
},
|
||||
"issues": {
|
||||
"default_token": {
|
||||
"title": "{name} nutzt das öffentliche Vorgabe-Token",
|
||||
"description": "Mit dem Vorgabe-Token kann jeder in deinem Netz diese Lampe steuern und neu flashen. Setze ein eigenes Token auf der Lampe und trage es über „Neu konfigurieren“ ein."
|
||||
},
|
||||
"wifi_guard": {
|
||||
"title": "Firmware für {name} nicht angeboten",
|
||||
"description": "Im Firmware-Ordner liegt {version} für {device_type}, aber ohne WLAN-Zugangsdaten für {ssid}. Die Lampe kennt ihr WLAN nur aus ihrer Firmware und wäre nach diesem Update nicht mehr im Netz – das Update wird deshalb nicht angeboten."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up ninlight lamp",
|
||||
"description": "Enter the address of the lamp. The token is prefilled with the public default token – anyone on your network who knows it can control and reflash the lamp.",
|
||||
"data": { "host": "Host", "token": "Token" },
|
||||
"data_description": {
|
||||
"host": "IP address or ninlight-xxxx.local name of the lamp.",
|
||||
"token": "Authorization token of the lamp."
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"title": "Set up {name}",
|
||||
"description": "Found the ninlight lamp {name} at {host}. The token is prefilled with the public default token – anyone on your network who knows it can control and reflash the lamp.",
|
||||
"data": { "token": "Token" }
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "Re-authenticate {name}",
|
||||
"description": "The lamp rejected the stored token. Enter its current token.",
|
||||
"data": { "token": "Token" }
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Reconfigure lamp",
|
||||
"data": { "host": "Host", "token": "Token" }
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Cannot reach the lamp.",
|
||||
"invalid_auth": "The lamp rejected the token.",
|
||||
"not_ninlight": "The device did not answer like a ninlight lamp.",
|
||||
"unknown": "Unexpected error."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This lamp is already set up.",
|
||||
"not_ninlight": "The device did not announce itself like a ninlight lamp.",
|
||||
"wrong_device": "This address belongs to a different lamp.",
|
||||
"reauth_successful": "Re-authentication was successful.",
|
||||
"reconfigure_successful": "Reconfiguration was successful."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "ninlight options",
|
||||
"data": {
|
||||
"scan_interval": "Polling interval (seconds)",
|
||||
"lamp_base_url": "Address lamps use to reach Home Assistant"
|
||||
},
|
||||
"data_description": {
|
||||
"lamp_base_url": "Only needed for firmware updates if lamps cannot reach Home Assistant's internal URL over plain http, e.g. http://192.168.1.50:8123"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_base_url": "Must be a plain http:// address with host and optional port, without path.",
|
||||
"base_url_too_long": "Address too long – the lamp accepts at most 255 characters for the whole firmware URL."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"number": {
|
||||
"fade_in": { "name": "Fade-in" },
|
||||
"fade_out": { "name": "Fade-out" },
|
||||
"transition_default": { "name": "Default transition" }
|
||||
},
|
||||
"select": {
|
||||
"power_on_behaviour": {
|
||||
"name": "Power-on behaviour",
|
||||
"state": { "last": "Last state", "on": "On", "off": "Off" }
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"wake_on_change": { "name": "Turn on when changed" }
|
||||
},
|
||||
"sensor": {
|
||||
"ip_address": { "name": "IP address" },
|
||||
"free_heap": { "name": "Free memory" },
|
||||
"last_ota_error": { "name": "Last update error" },
|
||||
"boot_time": { "name": "Last start" }
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": { "message": "Cannot reach the lamp." },
|
||||
"auth_failed": { "message": "The lamp rejected the token." },
|
||||
"request_failed": { "message": "The lamp rejected the request: {message}" },
|
||||
"update_busy": { "message": "An update is already running on the lamp." },
|
||||
"no_update": { "message": "No firmware update is available." },
|
||||
"no_http_url": { "message": "Lamps can only reach Home Assistant over plain http. Set the address in the integration options." },
|
||||
"lamp_url_too_long": { "message": "The firmware URL is longer than the 255 characters the lamp accepts." },
|
||||
"image_invalid": { "message": "The firmware file changed or is no longer valid." },
|
||||
"wifi_guard": { "message": "This firmware does not contain the Wi-Fi credentials for {ssid}; the lamp would lose its network connection." },
|
||||
"update_failed": { "message": "The update failed: {reason}" },
|
||||
"update_timeout": { "message": "The lamp did not report the new firmware within 12 minutes." }
|
||||
},
|
||||
"issues": {
|
||||
"default_token": {
|
||||
"title": "{name} uses the public default token",
|
||||
"description": "Anyone on your network can control and reflash this lamp with the default token. Set your own token on the lamp and enter it via reconfigure."
|
||||
},
|
||||
"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 lamp only knows its Wi-Fi from its firmware and would lose its network connection after this update, so the update is not offered."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user