HA integration: fix reload loop, parallel device fetch, WS guards, translations
- Fix indentation bug causing scenes device to not register - Use nonlocal tracking to prevent infinite reload loops on target/scene changes - Guard WS start/stop to avoid redundant connections - Parallel device brightness fetching via asyncio.gather - Route set_leds service to correct coordinator by source ID - Remove stale pattern cache, reuse single timeout object - Fix translations structure for light/select entities - Unregister service when last config entry unloaded Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -95,7 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
model="Scene Presets",
|
model="Scene Presets",
|
||||||
configuration_url=server_url,
|
configuration_url=server_url,
|
||||||
)
|
)
|
||||||
current_identifiers.add(scenes_identifier)
|
current_identifiers.add(scenes_identifier)
|
||||||
|
|
||||||
# Remove devices for targets that no longer exist
|
# Remove devices for targets that no longer exist
|
||||||
for device_entry in dr.async_entries_for_config_entry(
|
for device_entry in dr.async_entries_for_config_entry(
|
||||||
@@ -114,15 +114,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Track target and scene IDs to detect changes
|
# Track target and scene IDs to detect changes
|
||||||
initial_target_ids = set(
|
known_target_ids = set(
|
||||||
coordinator.data.get("targets", {}).keys() if coordinator.data else []
|
coordinator.data.get("targets", {}).keys() if coordinator.data else []
|
||||||
)
|
)
|
||||||
initial_scene_ids = set(
|
known_scene_ids = set(
|
||||||
p["id"] for p in (coordinator.data.get("scene_presets", []) if coordinator.data else [])
|
p["id"] for p in (coordinator.data.get("scene_presets", []) if coordinator.data else [])
|
||||||
)
|
)
|
||||||
|
|
||||||
def _on_coordinator_update() -> None:
|
def _on_coordinator_update() -> None:
|
||||||
"""Manage WS connections and detect target list changes."""
|
"""Manage WS connections and detect target list changes."""
|
||||||
|
nonlocal known_target_ids, known_scene_ids
|
||||||
|
|
||||||
if not coordinator.data:
|
if not coordinator.data:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -134,16 +136,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
state = target_data.get("state") or {}
|
state = target_data.get("state") or {}
|
||||||
if info.get("target_type") == TARGET_TYPE_KEY_COLORS:
|
if info.get("target_type") == TARGET_TYPE_KEY_COLORS:
|
||||||
if state.get("processing"):
|
if state.get("processing"):
|
||||||
hass.async_create_task(ws_manager.start_listening(target_id))
|
if target_id not in ws_manager._connections:
|
||||||
|
hass.async_create_task(ws_manager.start_listening(target_id))
|
||||||
else:
|
else:
|
||||||
hass.async_create_task(ws_manager.stop_listening(target_id))
|
if target_id in ws_manager._connections:
|
||||||
|
hass.async_create_task(ws_manager.stop_listening(target_id))
|
||||||
|
|
||||||
# Reload if target or scene list changed
|
# Reload if target or scene list changed
|
||||||
current_ids = set(targets.keys())
|
current_ids = set(targets.keys())
|
||||||
current_scene_ids = set(
|
current_scene_ids = set(
|
||||||
p["id"] for p in coordinator.data.get("scene_presets", [])
|
p["id"] for p in coordinator.data.get("scene_presets", [])
|
||||||
)
|
)
|
||||||
if current_ids != initial_target_ids or current_scene_ids != initial_scene_ids:
|
if current_ids != known_target_ids or current_scene_ids != known_scene_ids:
|
||||||
|
known_target_ids = current_ids
|
||||||
|
known_scene_ids = current_scene_ids
|
||||||
_LOGGER.info("Target or scene list changed, reloading integration")
|
_LOGGER.info("Target or scene list changed, reloading integration")
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
hass.config_entries.async_reload(entry.entry_id)
|
hass.config_entries.async_reload(entry.entry_id)
|
||||||
@@ -156,11 +162,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
"""Handle the set_leds service call."""
|
"""Handle the set_leds service call."""
|
||||||
source_id = call.data["source_id"]
|
source_id = call.data["source_id"]
|
||||||
segments = call.data["segments"]
|
segments = call.data["segments"]
|
||||||
|
# Route to the coordinator that owns this source
|
||||||
for entry_data in hass.data[DOMAIN].values():
|
for entry_data in hass.data[DOMAIN].values():
|
||||||
coord = entry_data.get(DATA_COORDINATOR)
|
coord = entry_data.get(DATA_COORDINATOR)
|
||||||
if coord:
|
if not coord or not coord.data:
|
||||||
|
continue
|
||||||
|
source_ids = {
|
||||||
|
s["id"] for s in coord.data.get("css_sources", [])
|
||||||
|
}
|
||||||
|
if source_id in source_ids:
|
||||||
await coord.push_segments(source_id, segments)
|
await coord.push_segments(source_id, segments)
|
||||||
break
|
return
|
||||||
|
_LOGGER.error("No server found with source_id %s", source_id)
|
||||||
|
|
||||||
if not hass.services.has_service(DOMAIN, "set_leds"):
|
if not hass.services.has_service(DOMAIN, "set_leds"):
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
@@ -188,5 +201,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
if unload_ok:
|
if unload_ok:
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
# Unregister service if no entries remain
|
||||||
|
if not hass.data[DOMAIN]:
|
||||||
|
hass.services.async_remove(DOMAIN, "set_leds")
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|||||||
@@ -65,10 +65,9 @@ class SceneActivateButton(CoordinatorEntity, ButtonEntity):
|
|||||||
"""Return if entity is available."""
|
"""Return if entity is available."""
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return False
|
return False
|
||||||
return any(
|
return self._preset_id in {
|
||||||
p["id"] == self._preset_id
|
p["id"] for p in self.coordinator.data.get("scene_presets", [])
|
||||||
for p in self.coordinator.data.get("scene_presets", [])
|
}
|
||||||
)
|
|
||||||
|
|
||||||
async def async_press(self) -> None:
|
async def async_press(self) -> None:
|
||||||
"""Activate the scene preset."""
|
"""Activate the scene preset."""
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.server_version = "unknown"
|
self.server_version = "unknown"
|
||||||
self._auth_headers = {"Authorization": f"Bearer {api_key}"}
|
self._auth_headers = {"Authorization": f"Bearer {api_key}"}
|
||||||
self._pattern_cache: dict[str, list[dict]] = {}
|
self._timeout = aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
@@ -85,7 +85,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
kc_settings = target.get("key_colors_settings") or {}
|
kc_settings = target.get("key_colors_settings") or {}
|
||||||
template_id = kc_settings.get("pattern_template_id", "")
|
template_id = kc_settings.get("pattern_template_id", "")
|
||||||
if template_id:
|
if template_id:
|
||||||
result["rectangles"] = await self._get_rectangles(
|
result["rectangles"] = await self._fetch_rectangles(
|
||||||
template_id
|
template_id
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -136,7 +136,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
try:
|
try:
|
||||||
async with self.session.get(
|
async with self.session.get(
|
||||||
f"{self.server_url}/health",
|
f"{self.server_url}/health",
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
@@ -150,7 +150,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
async with self.session.get(
|
async with self.session.get(
|
||||||
f"{self.server_url}/api/v1/output-targets",
|
f"{self.server_url}/api/v1/output-targets",
|
||||||
headers=self._auth_headers,
|
headers=self._auth_headers,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
@@ -161,7 +161,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
async with self.session.get(
|
async with self.session.get(
|
||||||
f"{self.server_url}/api/v1/output-targets/{target_id}/state",
|
f"{self.server_url}/api/v1/output-targets/{target_id}/state",
|
||||||
headers=self._auth_headers,
|
headers=self._auth_headers,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return await resp.json()
|
return await resp.json()
|
||||||
@@ -171,27 +171,22 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
async with self.session.get(
|
async with self.session.get(
|
||||||
f"{self.server_url}/api/v1/output-targets/{target_id}/metrics",
|
f"{self.server_url}/api/v1/output-targets/{target_id}/metrics",
|
||||||
headers=self._auth_headers,
|
headers=self._auth_headers,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return await resp.json()
|
return await resp.json()
|
||||||
|
|
||||||
async def _get_rectangles(self, template_id: str) -> list[dict]:
|
async def _fetch_rectangles(self, template_id: str) -> list[dict]:
|
||||||
"""Get rectangles for a pattern template, using cache."""
|
"""Fetch rectangles for a pattern template (no cache — always fresh)."""
|
||||||
if template_id in self._pattern_cache:
|
|
||||||
return self._pattern_cache[template_id]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with self.session.get(
|
async with self.session.get(
|
||||||
f"{self.server_url}/api/v1/pattern-templates/{template_id}",
|
f"{self.server_url}/api/v1/pattern-templates/{template_id}",
|
||||||
headers=self._auth_headers,
|
headers=self._auth_headers,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
rectangles = data.get("rectangles", [])
|
return data.get("rectangles", [])
|
||||||
self._pattern_cache[template_id] = rectangles
|
|
||||||
return rectangles
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Failed to fetch pattern template %s: %s", template_id, err
|
"Failed to fetch pattern template %s: %s", template_id, err
|
||||||
@@ -204,7 +199,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
async with self.session.get(
|
async with self.session.get(
|
||||||
f"{self.server_url}/api/v1/devices",
|
f"{self.server_url}/api/v1/devices",
|
||||||
headers=self._auth_headers,
|
headers=self._auth_headers,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
@@ -213,18 +208,16 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
_LOGGER.warning("Failed to fetch devices: %s", err)
|
_LOGGER.warning("Failed to fetch devices: %s", err)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
devices_data: dict[str, dict[str, Any]] = {}
|
# Fetch brightness for all capable devices in parallel
|
||||||
|
async def fetch_device_entry(device: dict) -> tuple[str, dict[str, Any]]:
|
||||||
for device in devices:
|
|
||||||
device_id = device["id"]
|
device_id = device["id"]
|
||||||
entry: dict[str, Any] = {"info": device, "brightness": None}
|
entry: dict[str, Any] = {"info": device, "brightness": None}
|
||||||
|
|
||||||
if "brightness_control" in (device.get("capabilities") or []):
|
if "brightness_control" in (device.get("capabilities") or []):
|
||||||
try:
|
try:
|
||||||
async with self.session.get(
|
async with self.session.get(
|
||||||
f"{self.server_url}/api/v1/devices/{device_id}/brightness",
|
f"{self.server_url}/api/v1/devices/{device_id}/brightness",
|
||||||
headers=self._auth_headers,
|
headers=self._auth_headers,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
if resp.status == 200:
|
if resp.status == 200:
|
||||||
bri_data = await resp.json()
|
bri_data = await resp.json()
|
||||||
@@ -234,7 +227,19 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
"Failed to fetch brightness for device %s: %s",
|
"Failed to fetch brightness for device %s: %s",
|
||||||
device_id, err,
|
device_id, err,
|
||||||
)
|
)
|
||||||
|
return device_id, entry
|
||||||
|
|
||||||
|
results = await asyncio.gather(
|
||||||
|
*(fetch_device_entry(d) for d in devices),
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
devices_data: dict[str, dict[str, Any]] = {}
|
||||||
|
for r in results:
|
||||||
|
if isinstance(r, Exception):
|
||||||
|
_LOGGER.warning("Device fetch failed: %s", r)
|
||||||
|
continue
|
||||||
|
device_id, entry = r
|
||||||
devices_data[device_id] = entry
|
devices_data[device_id] = entry
|
||||||
|
|
||||||
return devices_data
|
return devices_data
|
||||||
@@ -245,7 +250,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
f"{self.server_url}/api/v1/devices/{device_id}/brightness",
|
f"{self.server_url}/api/v1/devices/{device_id}/brightness",
|
||||||
headers={**self._auth_headers, "Content-Type": "application/json"},
|
headers={**self._auth_headers, "Content-Type": "application/json"},
|
||||||
json={"brightness": brightness},
|
json={"brightness": brightness},
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
body = await resp.text()
|
body = await resp.text()
|
||||||
@@ -262,7 +267,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
f"{self.server_url}/api/v1/devices/{device_id}/color",
|
f"{self.server_url}/api/v1/devices/{device_id}/color",
|
||||||
headers={**self._auth_headers, "Content-Type": "application/json"},
|
headers={**self._auth_headers, "Content-Type": "application/json"},
|
||||||
json={"color": color},
|
json={"color": color},
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
body = await resp.text()
|
body = await resp.text()
|
||||||
@@ -280,7 +285,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
f"{self.server_url}/api/v1/output-targets/{target_id}",
|
f"{self.server_url}/api/v1/output-targets/{target_id}",
|
||||||
headers={**self._auth_headers, "Content-Type": "application/json"},
|
headers={**self._auth_headers, "Content-Type": "application/json"},
|
||||||
json={"key_colors_settings": {"brightness": brightness_float}},
|
json={"key_colors_settings": {"brightness": brightness_float}},
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
body = await resp.text()
|
body = await resp.text()
|
||||||
@@ -297,7 +302,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
async with self.session.get(
|
async with self.session.get(
|
||||||
f"{self.server_url}/api/v1/color-strip-sources",
|
f"{self.server_url}/api/v1/color-strip-sources",
|
||||||
headers=self._auth_headers,
|
headers=self._auth_headers,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
@@ -312,7 +317,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
async with self.session.get(
|
async with self.session.get(
|
||||||
f"{self.server_url}/api/v1/value-sources",
|
f"{self.server_url}/api/v1/value-sources",
|
||||||
headers=self._auth_headers,
|
headers=self._auth_headers,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
@@ -327,7 +332,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
async with self.session.get(
|
async with self.session.get(
|
||||||
f"{self.server_url}/api/v1/scene-presets",
|
f"{self.server_url}/api/v1/scene-presets",
|
||||||
headers=self._auth_headers,
|
headers=self._auth_headers,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
@@ -342,7 +347,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
f"{self.server_url}/api/v1/color-strip-sources/{source_id}/colors",
|
f"{self.server_url}/api/v1/color-strip-sources/{source_id}/colors",
|
||||||
headers={**self._auth_headers, "Content-Type": "application/json"},
|
headers={**self._auth_headers, "Content-Type": "application/json"},
|
||||||
json={"colors": colors},
|
json={"colors": colors},
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
if resp.status not in (200, 204):
|
if resp.status not in (200, 204):
|
||||||
body = await resp.text()
|
body = await resp.text()
|
||||||
@@ -358,7 +363,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
f"{self.server_url}/api/v1/color-strip-sources/{source_id}/colors",
|
f"{self.server_url}/api/v1/color-strip-sources/{source_id}/colors",
|
||||||
headers={**self._auth_headers, "Content-Type": "application/json"},
|
headers={**self._auth_headers, "Content-Type": "application/json"},
|
||||||
json={"segments": segments},
|
json={"segments": segments},
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
if resp.status not in (200, 204):
|
if resp.status not in (200, 204):
|
||||||
body = await resp.text()
|
body = await resp.text()
|
||||||
@@ -373,7 +378,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
async with self.session.post(
|
async with self.session.post(
|
||||||
f"{self.server_url}/api/v1/scene-presets/{preset_id}/activate",
|
f"{self.server_url}/api/v1/scene-presets/{preset_id}/activate",
|
||||||
headers=self._auth_headers,
|
headers=self._auth_headers,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
body = await resp.text()
|
body = await resp.text()
|
||||||
@@ -390,7 +395,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
f"{self.server_url}/api/v1/color-strip-sources/{source_id}",
|
f"{self.server_url}/api/v1/color-strip-sources/{source_id}",
|
||||||
headers={**self._auth_headers, "Content-Type": "application/json"},
|
headers={**self._auth_headers, "Content-Type": "application/json"},
|
||||||
json=kwargs,
|
json=kwargs,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
body = await resp.text()
|
body = await resp.text()
|
||||||
@@ -398,14 +403,15 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
"Failed to update source %s: %s %s",
|
"Failed to update source %s: %s %s",
|
||||||
source_id, resp.status, body,
|
source_id, resp.status, body,
|
||||||
)
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
async def update_target(self, target_id: str, **kwargs: Any) -> None:
|
async def update_target(self, target_id: str, **kwargs: Any) -> None:
|
||||||
"""Update a output target's fields."""
|
"""Update an output target's fields."""
|
||||||
async with self.session.put(
|
async with self.session.put(
|
||||||
f"{self.server_url}/api/v1/output-targets/{target_id}",
|
f"{self.server_url}/api/v1/output-targets/{target_id}",
|
||||||
headers={**self._auth_headers, "Content-Type": "application/json"},
|
headers={**self._auth_headers, "Content-Type": "application/json"},
|
||||||
json=kwargs,
|
json=kwargs,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
body = await resp.text()
|
body = await resp.text()
|
||||||
@@ -421,7 +427,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
async with self.session.post(
|
async with self.session.post(
|
||||||
f"{self.server_url}/api/v1/output-targets/{target_id}/start",
|
f"{self.server_url}/api/v1/output-targets/{target_id}/start",
|
||||||
headers=self._auth_headers,
|
headers=self._auth_headers,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
if resp.status == 409:
|
if resp.status == 409:
|
||||||
_LOGGER.debug("Target %s already processing", target_id)
|
_LOGGER.debug("Target %s already processing", target_id)
|
||||||
@@ -439,7 +445,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
async with self.session.post(
|
async with self.session.post(
|
||||||
f"{self.server_url}/api/v1/output-targets/{target_id}/stop",
|
f"{self.server_url}/api/v1/output-targets/{target_id}/stop",
|
||||||
headers=self._auth_headers,
|
headers=self._auth_headers,
|
||||||
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
timeout=self._timeout,
|
||||||
) as resp:
|
) as resp:
|
||||||
if resp.status == 409:
|
if resp.status == 409:
|
||||||
_LOGGER.debug("Target %s already stopped", target_id)
|
_LOGGER.debug("Target %s already stopped", target_id)
|
||||||
|
|||||||
@@ -63,9 +63,13 @@ class ApiInputLight(CoordinatorEntity, LightEntity):
|
|||||||
self._entry_id = entry_id
|
self._entry_id = entry_id
|
||||||
self._attr_unique_id = f"{self._source_id}_light"
|
self._attr_unique_id = f"{self._source_id}_light"
|
||||||
|
|
||||||
# Local state — not derived from coordinator data
|
# Restore state from fallback_color
|
||||||
self._is_on: bool = False
|
fallback = self._get_fallback_color()
|
||||||
self._rgb_color: tuple[int, int, int] = (255, 255, 255)
|
is_off = fallback == [0, 0, 0]
|
||||||
|
self._is_on: bool = not is_off
|
||||||
|
self._rgb_color: tuple[int, int, int] = (
|
||||||
|
(255, 255, 255) if is_off else tuple(fallback) # type: ignore[arg-type]
|
||||||
|
)
|
||||||
self._brightness: int = 255
|
self._brightness: int = 255
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class CSSSourceSelect(CoordinatorEntity, SelectEntity):
|
|||||||
return self._target_id in self.coordinator.data.get("targets", {})
|
return self._target_id in self.coordinator.data.get("targets", {})
|
||||||
|
|
||||||
async def async_select_option(self, option: str) -> None:
|
async def async_select_option(self, option: str) -> None:
|
||||||
source_id = self._name_to_id(option)
|
source_id = self._name_to_id_map().get(option)
|
||||||
if source_id is None:
|
if source_id is None:
|
||||||
_LOGGER.error("CSS source not found: %s", option)
|
_LOGGER.error("CSS source not found: %s", option)
|
||||||
return
|
return
|
||||||
@@ -104,12 +104,9 @@ class CSSSourceSelect(CoordinatorEntity, SelectEntity):
|
|||||||
self._target_id, color_strip_source_id=source_id
|
self._target_id, color_strip_source_id=source_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def _name_to_id(self, name: str) -> str | None:
|
def _name_to_id_map(self) -> dict[str, str]:
|
||||||
sources = (self.coordinator.data or {}).get("css_sources") or []
|
sources = (self.coordinator.data or {}).get("css_sources") or []
|
||||||
for s in sources:
|
return {s["name"]: s["id"] for s in sources}
|
||||||
if s["name"] == name:
|
|
||||||
return s["id"]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class BrightnessSourceSelect(CoordinatorEntity, SelectEntity):
|
class BrightnessSourceSelect(CoordinatorEntity, SelectEntity):
|
||||||
@@ -167,17 +164,14 @@ class BrightnessSourceSelect(CoordinatorEntity, SelectEntity):
|
|||||||
if option == NONE_OPTION:
|
if option == NONE_OPTION:
|
||||||
source_id = ""
|
source_id = ""
|
||||||
else:
|
else:
|
||||||
source_id = self._name_to_id(option)
|
name_map = {
|
||||||
|
s["name"]: s["id"]
|
||||||
|
for s in (self.coordinator.data or {}).get("value_sources") or []
|
||||||
|
}
|
||||||
|
source_id = name_map.get(option)
|
||||||
if source_id is None:
|
if source_id is None:
|
||||||
_LOGGER.error("Value source not found: %s", option)
|
_LOGGER.error("Value source not found: %s", option)
|
||||||
return
|
return
|
||||||
await self.coordinator.update_target(
|
await self.coordinator.update_target(
|
||||||
self._target_id, brightness_value_source_id=source_id
|
self._target_id, brightness_value_source_id=source_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def _name_to_id(self, name: str) -> str | None:
|
|
||||||
sources = (self.coordinator.data or {}).get("value_sources") or []
|
|
||||||
for s in sources:
|
|
||||||
if s["name"] == name:
|
|
||||||
return s["id"]
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -31,6 +31,11 @@
|
|||||||
"name": "{scene_name}"
|
"name": "{scene_name}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"light": {
|
||||||
|
"api_input_light": {
|
||||||
|
"name": "Light"
|
||||||
|
}
|
||||||
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
"processing": {
|
"processing": {
|
||||||
"name": "Processing"
|
"name": "Processing"
|
||||||
@@ -58,9 +63,12 @@
|
|||||||
"name": "Brightness"
|
"name": "Brightness"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"light": {
|
"select": {
|
||||||
"light": {
|
"color_strip_source": {
|
||||||
"name": "Light"
|
"name": "Color Strip Source"
|
||||||
|
},
|
||||||
|
"brightness_source": {
|
||||||
|
"name": "Brightness Source"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,11 @@
|
|||||||
"name": "{scene_name}"
|
"name": "{scene_name}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"light": {
|
||||||
|
"api_input_light": {
|
||||||
|
"name": "Подсветка"
|
||||||
|
}
|
||||||
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
"processing": {
|
"processing": {
|
||||||
"name": "Обработка"
|
"name": "Обработка"
|
||||||
@@ -58,9 +63,12 @@
|
|||||||
"name": "Яркость"
|
"name": "Яркость"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"light": {
|
"select": {
|
||||||
"light": {
|
"color_strip_source": {
|
||||||
"name": "Подсветка"
|
"name": "Источник цветовой полосы"
|
||||||
|
},
|
||||||
|
"brightness_source": {
|
||||||
|
"name": "Источник яркости"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user