Replace HAOS light entity with select entities, add zero-brightness optimization
- Remove light.py platform (static color control via HA light entity) - Add select.py with CSS Source and Brightness Source dropdowns for LED targets - Coordinator now fetches color-strip-sources and value-sources lists - Add generic update_target() method for partial target updates - Clean up stale device registry entries on integration reload - Skip frame sends when effective brightness is ~0 (suppresses unnecessary UDP/HTTP traffic while LEDs are dark) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,7 @@ PLATFORMS: list[Platform] = [
|
|||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
Platform.NUMBER,
|
Platform.NUMBER,
|
||||||
Platform.LIGHT,
|
Platform.SELECT,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -57,8 +57,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
event_listener = EventStreamListener(hass, server_url, api_key, coordinator)
|
event_listener = EventStreamListener(hass, server_url, api_key, coordinator)
|
||||||
await event_listener.start()
|
await event_listener.start()
|
||||||
|
|
||||||
# Create device entries for each target
|
# Create device entries for each target and remove stale ones
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
|
current_identifiers: set[tuple[str, str]] = set()
|
||||||
if coordinator.data and "targets" in coordinator.data:
|
if coordinator.data and "targets" in coordinator.data:
|
||||||
for target_id, target_data in coordinator.data["targets"].items():
|
for target_id, target_data in coordinator.data["targets"].items():
|
||||||
info = target_data["info"]
|
info = target_data["info"]
|
||||||
@@ -76,6 +77,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
model=model,
|
model=model,
|
||||||
configuration_url=server_url,
|
configuration_url=server_url,
|
||||||
)
|
)
|
||||||
|
current_identifiers.add((DOMAIN, target_id))
|
||||||
|
|
||||||
|
# Remove devices for targets that no longer exist
|
||||||
|
for device_entry in dr.async_entries_for_config_entry(
|
||||||
|
device_registry, entry.entry_id
|
||||||
|
):
|
||||||
|
if not device_entry.identifiers & current_identifiers:
|
||||||
|
_LOGGER.info("Removing stale device: %s", device_entry.name)
|
||||||
|
device_registry.async_remove_device(device_entry.id)
|
||||||
|
|
||||||
# Store data
|
# Store data
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|||||||
@@ -107,12 +107,18 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
target_id, data = r
|
target_id, data = r
|
||||||
targets_data[target_id] = data
|
targets_data[target_id] = data
|
||||||
|
|
||||||
# Fetch devices with capabilities and brightness
|
# Fetch devices, CSS sources, and value sources in parallel
|
||||||
devices_data = await self._fetch_devices()
|
devices_data, css_sources, value_sources = await asyncio.gather(
|
||||||
|
self._fetch_devices(),
|
||||||
|
self._fetch_css_sources(),
|
||||||
|
self._fetch_value_sources(),
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"targets": targets_data,
|
"targets": targets_data,
|
||||||
"devices": devices_data,
|
"devices": devices_data,
|
||||||
|
"css_sources": css_sources,
|
||||||
|
"value_sources": value_sources,
|
||||||
"server_version": self.server_version,
|
"server_version": self.server_version,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +287,53 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
await self.async_request_refresh()
|
await self.async_request_refresh()
|
||||||
|
|
||||||
|
async def _fetch_css_sources(self) -> list[dict[str, Any]]:
|
||||||
|
"""Fetch all color strip sources."""
|
||||||
|
try:
|
||||||
|
async with self.session.get(
|
||||||
|
f"{self.server_url}/api/v1/color-strip-sources",
|
||||||
|
headers=self._auth_headers,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
||||||
|
) as resp:
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = await resp.json()
|
||||||
|
return data.get("sources", [])
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.warning("Failed to fetch CSS sources: %s", err)
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def _fetch_value_sources(self) -> list[dict[str, Any]]:
|
||||||
|
"""Fetch all value sources."""
|
||||||
|
try:
|
||||||
|
async with self.session.get(
|
||||||
|
f"{self.server_url}/api/v1/value-sources",
|
||||||
|
headers=self._auth_headers,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
||||||
|
) as resp:
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = await resp.json()
|
||||||
|
return data.get("sources", [])
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.warning("Failed to fetch value sources: %s", err)
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def update_target(self, target_id: str, **kwargs: Any) -> None:
|
||||||
|
"""Update a picture target's fields."""
|
||||||
|
async with self.session.put(
|
||||||
|
f"{self.server_url}/api/v1/picture-targets/{target_id}",
|
||||||
|
headers={**self._auth_headers, "Content-Type": "application/json"},
|
||||||
|
json=kwargs,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
body = await resp.text()
|
||||||
|
_LOGGER.error(
|
||||||
|
"Failed to update target %s: %s %s",
|
||||||
|
target_id, resp.status, body,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
await self.async_request_refresh()
|
||||||
|
|
||||||
async def start_processing(self, target_id: str) -> None:
|
async def start_processing(self, target_id: str) -> None:
|
||||||
"""Start processing for a target."""
|
"""Start processing for a target."""
|
||||||
async with self.session.post(
|
async with self.session.post(
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
"""Light platform for LED Screen Controller (static color + brightness)."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
|
||||||
ATTR_BRIGHTNESS,
|
|
||||||
ATTR_RGB_COLOR,
|
|
||||||
ColorMode,
|
|
||||||
LightEntity,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import DOMAIN, DATA_COORDINATOR, TARGET_TYPE_KEY_COLORS
|
|
||||||
from .coordinator import WLEDScreenControllerCoordinator
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: ConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up LED Screen Controller light entities."""
|
|
||||||
data = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
coordinator: WLEDScreenControllerCoordinator = data[DATA_COORDINATOR]
|
|
||||||
|
|
||||||
entities = []
|
|
||||||
if coordinator.data and "targets" in coordinator.data:
|
|
||||||
devices = coordinator.data.get("devices") or {}
|
|
||||||
|
|
||||||
for target_id, target_data in coordinator.data["targets"].items():
|
|
||||||
info = target_data["info"]
|
|
||||||
|
|
||||||
# Only LED targets (skip KC targets)
|
|
||||||
if info.get("target_type") == TARGET_TYPE_KEY_COLORS:
|
|
||||||
continue
|
|
||||||
|
|
||||||
device_id = info.get("device_id", "")
|
|
||||||
if not device_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
device_data = devices.get(device_id)
|
|
||||||
if not device_data:
|
|
||||||
continue
|
|
||||||
|
|
||||||
capabilities = device_data.get("info", {}).get("capabilities") or []
|
|
||||||
|
|
||||||
# Light entity requires BOTH brightness_control AND static_color
|
|
||||||
if "brightness_control" in capabilities and "static_color" in capabilities:
|
|
||||||
entities.append(
|
|
||||||
WLEDScreenControllerLight(
|
|
||||||
coordinator, target_id, device_id, entry.entry_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
|
||||||
|
|
||||||
|
|
||||||
class WLEDScreenControllerLight(CoordinatorEntity, LightEntity):
|
|
||||||
"""Light entity for an LED device with brightness and static color."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_color_mode = ColorMode.RGB
|
|
||||||
_attr_supported_color_modes = {ColorMode.RGB}
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: WLEDScreenControllerCoordinator,
|
|
||||||
target_id: str,
|
|
||||||
device_id: str,
|
|
||||||
entry_id: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the light entity."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._target_id = target_id
|
|
||||||
self._device_id = device_id
|
|
||||||
self._entry_id = entry_id
|
|
||||||
self._attr_unique_id = f"{target_id}_light"
|
|
||||||
self._attr_translation_key = "light"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> dict[str, Any]:
|
|
||||||
"""Return device information."""
|
|
||||||
return {"identifiers": {(DOMAIN, self._target_id)}}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self) -> bool | None:
|
|
||||||
"""Return True if static_color is set (not null)."""
|
|
||||||
device_data = self._get_device_data()
|
|
||||||
if not device_data:
|
|
||||||
return None
|
|
||||||
static_color = device_data.get("info", {}).get("static_color")
|
|
||||||
return static_color is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def brightness(self) -> int | None:
|
|
||||||
"""Return the brightness (0-255)."""
|
|
||||||
device_data = self._get_device_data()
|
|
||||||
if not device_data:
|
|
||||||
return None
|
|
||||||
return device_data.get("brightness")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rgb_color(self) -> tuple[int, int, int] | None:
|
|
||||||
"""Return the RGB color tuple."""
|
|
||||||
device_data = self._get_device_data()
|
|
||||||
if not device_data:
|
|
||||||
return None
|
|
||||||
static_color = device_data.get("info", {}).get("static_color")
|
|
||||||
if static_color is not None and len(static_color) == 3:
|
|
||||||
return tuple(static_color)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return False
|
|
||||||
targets = self.coordinator.data.get("targets", {})
|
|
||||||
devices = self.coordinator.data.get("devices", {})
|
|
||||||
return self._target_id in targets and self._device_id in devices
|
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
|
||||||
"""Turn the light on (set static color and/or brightness)."""
|
|
||||||
if ATTR_BRIGHTNESS in kwargs:
|
|
||||||
await self.coordinator.set_brightness(
|
|
||||||
self._device_id, int(kwargs[ATTR_BRIGHTNESS])
|
|
||||||
)
|
|
||||||
|
|
||||||
if ATTR_RGB_COLOR in kwargs:
|
|
||||||
r, g, b = kwargs[ATTR_RGB_COLOR]
|
|
||||||
await self.coordinator.set_color(self._device_id, [r, g, b])
|
|
||||||
elif not self.is_on:
|
|
||||||
# Turning on without specifying color: default to white
|
|
||||||
await self.coordinator.set_color(self._device_id, [255, 255, 255])
|
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
||||||
"""Turn the light off (clear static color)."""
|
|
||||||
await self.coordinator.set_color(self._device_id, None)
|
|
||||||
|
|
||||||
def _get_device_data(self) -> dict[str, Any] | None:
|
|
||||||
"""Get device data from coordinator."""
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return None
|
|
||||||
return self.coordinator.data.get("devices", {}).get(self._device_id)
|
|
||||||
183
custom_components/wled_screen_controller/select.py
Normal file
183
custom_components/wled_screen_controller/select.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
"""Select platform for LED Screen Controller (CSS source & brightness source)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.select import SelectEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN, DATA_COORDINATOR, TARGET_TYPE_KEY_COLORS
|
||||||
|
from .coordinator import WLEDScreenControllerCoordinator
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
NONE_OPTION = "— None —"
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up LED Screen Controller select entities."""
|
||||||
|
data = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
coordinator: WLEDScreenControllerCoordinator = data[DATA_COORDINATOR]
|
||||||
|
|
||||||
|
entities: list[SelectEntity] = []
|
||||||
|
if coordinator.data and "targets" in coordinator.data:
|
||||||
|
for target_id, target_data in coordinator.data["targets"].items():
|
||||||
|
info = target_data["info"]
|
||||||
|
|
||||||
|
# Only LED targets
|
||||||
|
if info.get("target_type") == TARGET_TYPE_KEY_COLORS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
entities.append(
|
||||||
|
CSSSourceSelect(coordinator, target_id, entry.entry_id)
|
||||||
|
)
|
||||||
|
entities.append(
|
||||||
|
BrightnessSourceSelect(coordinator, target_id, entry.entry_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class CSSSourceSelect(CoordinatorEntity, SelectEntity):
|
||||||
|
"""Select entity for choosing a color strip source for an LED target."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_icon = "mdi:palette"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: WLEDScreenControllerCoordinator,
|
||||||
|
target_id: str,
|
||||||
|
entry_id: str,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._target_id = target_id
|
||||||
|
self._entry_id = entry_id
|
||||||
|
self._attr_unique_id = f"{target_id}_css_source"
|
||||||
|
self._attr_translation_key = "color_strip_source"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> dict[str, Any]:
|
||||||
|
return {"identifiers": {(DOMAIN, self._target_id)}}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> list[str]:
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return []
|
||||||
|
sources = self.coordinator.data.get("css_sources") or []
|
||||||
|
return [s["name"] for s in sources]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str | None:
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return None
|
||||||
|
target_data = self.coordinator.data.get("targets", {}).get(self._target_id)
|
||||||
|
if not target_data:
|
||||||
|
return None
|
||||||
|
current_id = target_data["info"].get("color_strip_source_id", "")
|
||||||
|
sources = self.coordinator.data.get("css_sources") or []
|
||||||
|
for s in sources:
|
||||||
|
if s["id"] == current_id:
|
||||||
|
return s["name"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return False
|
||||||
|
return self._target_id in self.coordinator.data.get("targets", {})
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
source_id = self._name_to_id(option)
|
||||||
|
if source_id is None:
|
||||||
|
_LOGGER.error("CSS source not found: %s", option)
|
||||||
|
return
|
||||||
|
await self.coordinator.update_target(
|
||||||
|
self._target_id, color_strip_source_id=source_id
|
||||||
|
)
|
||||||
|
|
||||||
|
def _name_to_id(self, name: str) -> str | None:
|
||||||
|
sources = (self.coordinator.data or {}).get("css_sources") or []
|
||||||
|
for s in sources:
|
||||||
|
if s["name"] == name:
|
||||||
|
return s["id"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class BrightnessSourceSelect(CoordinatorEntity, SelectEntity):
|
||||||
|
"""Select entity for choosing a brightness value source for an LED target."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_icon = "mdi:brightness-auto"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: WLEDScreenControllerCoordinator,
|
||||||
|
target_id: str,
|
||||||
|
entry_id: str,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._target_id = target_id
|
||||||
|
self._entry_id = entry_id
|
||||||
|
self._attr_unique_id = f"{target_id}_brightness_source"
|
||||||
|
self._attr_translation_key = "brightness_source"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> dict[str, Any]:
|
||||||
|
return {"identifiers": {(DOMAIN, self._target_id)}}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> list[str]:
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return [NONE_OPTION]
|
||||||
|
sources = self.coordinator.data.get("value_sources") or []
|
||||||
|
return [NONE_OPTION] + [s["name"] for s in sources]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str | None:
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return None
|
||||||
|
target_data = self.coordinator.data.get("targets", {}).get(self._target_id)
|
||||||
|
if not target_data:
|
||||||
|
return None
|
||||||
|
current_id = target_data["info"].get("brightness_value_source_id", "")
|
||||||
|
if not current_id:
|
||||||
|
return NONE_OPTION
|
||||||
|
sources = self.coordinator.data.get("value_sources") or []
|
||||||
|
for s in sources:
|
||||||
|
if s["id"] == current_id:
|
||||||
|
return s["name"]
|
||||||
|
return NONE_OPTION
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return False
|
||||||
|
return self._target_id in self.coordinator.data.get("targets", {})
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
if option == NONE_OPTION:
|
||||||
|
source_id = ""
|
||||||
|
else:
|
||||||
|
source_id = self._name_to_id(option)
|
||||||
|
if source_id is None:
|
||||||
|
_LOGGER.error("Value source not found: %s", option)
|
||||||
|
return
|
||||||
|
await self.coordinator.update_target(
|
||||||
|
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
|
||||||
@@ -53,9 +53,12 @@
|
|||||||
"name": "Brightness"
|
"name": "Brightness"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"light": {
|
"select": {
|
||||||
"light": {
|
"color_strip_source": {
|
||||||
"name": "Light"
|
"name": "Color Strip Source"
|
||||||
|
},
|
||||||
|
"brightness_source": {
|
||||||
|
"name": "Brightness Source"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -577,6 +577,16 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
|
|
||||||
cur_brightness = _effective_brightness(device_info)
|
cur_brightness = _effective_brightness(device_info)
|
||||||
|
|
||||||
|
# Zero-brightness suppression: if output is black and
|
||||||
|
# the last sent frame was also black, skip sending.
|
||||||
|
if cur_brightness <= 1 and _prev_brightness <= 1 and has_any_frame:
|
||||||
|
self._metrics.frames_skipped += 1
|
||||||
|
while send_timestamps and send_timestamps[0] < loop_start - 1.0:
|
||||||
|
send_timestamps.popleft()
|
||||||
|
self._metrics.fps_current = len(send_timestamps)
|
||||||
|
await asyncio.sleep(SKIP_REPOLL)
|
||||||
|
continue
|
||||||
|
|
||||||
if frame is prev_frame_ref and cur_brightness == _prev_brightness:
|
if frame is prev_frame_ref and cur_brightness == _prev_brightness:
|
||||||
# Same frame + same brightness — keepalive or skip
|
# Same frame + same brightness — keepalive or skip
|
||||||
if self._needs_keepalive and has_any_frame and (loop_start - last_send_time) >= keepalive_interval:
|
if self._needs_keepalive and has_any_frame and (loop_start - last_send_time) >= keepalive_interval:
|
||||||
|
|||||||
Reference in New Issue
Block a user