feat(api-input): expose timeout/interpolation, fix 422 on light turn-on

- coordinator.update_source now auto-injects source_type from cached data so
  the server's discriminated-union body validates (was rejecting
  fallback_color updates with 422).
- Add Fallback Timeout (number) and Interpolation (select) entities per
  api_input CSS source.
- Light no longer zeros fallback_color on turn_off; state restored across
  HA restarts via RestoreEntity so the chosen default color persists.
- Bump manifest to 0.2.2 and update en/ru translations.
This commit is contained in:
2026-04-26 02:26:06 +03:00
parent 5c8b3c259a
commit 0a16dc9058
8 changed files with 226 additions and 14 deletions
+66
View File
@@ -29,6 +29,11 @@ async def async_setup_entry(
coordinator: LedGrabCoordinator = data[DATA_COORDINATOR]
entities: list[SelectEntity] = []
if coordinator.data:
for source in coordinator.data.get("css_sources") or []:
if source.get("source_type") == "api_input":
entities.append(ApiInputInterpolationSelect(coordinator, source, entry.entry_id))
if coordinator.data and "targets" in coordinator.data:
for target_id, target_data in coordinator.data["targets"].items():
info = target_data["info"]
@@ -176,3 +181,64 @@ class BrightnessSourceSelect(CoordinatorEntity, SelectEntity):
self._target_id,
brightness={"value": 1.0, "source_id": source_id} if source_id else 1.0,
)
# --- api_input CSS source select entities ---
INTERPOLATION_OPTIONS = ["none", "linear", "nearest"]
class ApiInputInterpolationSelect(CoordinatorEntity, SelectEntity):
"""LED count interpolation mode for an api_input CSS source.
Controls how the source resamples pushed colors when the target strip
has a different LED count than the input. Server accepts
``none`` | ``linear`` | ``nearest``.
"""
_attr_has_entity_name = True
_attr_icon = "mdi:vector-polyline"
_attr_options = INTERPOLATION_OPTIONS
def __init__(
self,
coordinator: LedGrabCoordinator,
source: dict[str, Any],
entry_id: str,
) -> None:
super().__init__(coordinator)
self._source_id: str = source["id"]
self._entry_id = entry_id
self._attr_unique_id = f"{self._source_id}_interpolation"
self._attr_translation_key = "api_input_interpolation"
@property
def device_info(self) -> dict[str, Any]:
return {"identifiers": {(DOMAIN, self._source_id)}}
@property
def current_option(self) -> str | None:
source = self._get_source()
if not source:
return None
value = source.get("interpolation")
return value if value in INTERPOLATION_OPTIONS else None
@property
def available(self) -> bool:
return self._get_source() is not None
async def async_select_option(self, option: str) -> None:
if option not in INTERPOLATION_OPTIONS:
_LOGGER.error("Invalid interpolation option: %s", option)
return
await self.coordinator.update_source(self._source_id, interpolation=option)
await self.coordinator.async_request_refresh()
def _get_source(self) -> dict[str, Any] | None:
if not self.coordinator.data:
return None
for source in self.coordinator.data.get("css_sources") or []:
if source.get("id") == self._source_id:
return source
return None