681 lines
29 KiB
Python
681 lines
29 KiB
Python
"""Async OpenRGB SDK v5 adapter with reconnect and serialized writes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import contextlib
|
|
import logging
|
|
import struct
|
|
import time
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from ...config import Settings
|
|
from ...errors import ConnectorError
|
|
from ..base import (
|
|
Connector,
|
|
ConnectorDevice,
|
|
ConnectorHealth,
|
|
DeviceState,
|
|
HealthStatus,
|
|
InventoryCallback,
|
|
RGBColor,
|
|
)
|
|
from .protocol import (
|
|
HEADER,
|
|
PROTOCOL_VERSION,
|
|
Controller,
|
|
Mode,
|
|
ModeFlag,
|
|
PacketHeader,
|
|
PacketId,
|
|
ProtocolError,
|
|
ZoneFlag,
|
|
pack_header,
|
|
pack_mode,
|
|
pack_update_leds,
|
|
pack_update_single_led,
|
|
pack_update_zone,
|
|
parse_controller,
|
|
parse_header,
|
|
parse_profile_list,
|
|
)
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class OpenRGBAdapter(Connector):
|
|
id = "openrgb-local"
|
|
kind = "openrgb"
|
|
|
|
def __init__(self, settings: Settings) -> None:
|
|
self.settings = settings
|
|
self._reader: asyncio.StreamReader | None = None
|
|
self._writer: asyncio.StreamWriter | None = None
|
|
self._reader_task: asyncio.Task[None] | None = None
|
|
self._monitor_task: asyncio.Task[None] | None = None
|
|
self._connect_lock = asyncio.Lock()
|
|
self._request_lock = asyncio.Lock()
|
|
self._write_lock = asyncio.Lock()
|
|
self._response_queues: dict[int, asyncio.Queue[tuple[PacketHeader, bytes] | Exception]] = {}
|
|
self._controllers: dict[str, Controller] = {}
|
|
self._inventory_callback: InventoryCallback | None = None
|
|
self._stopping = False
|
|
self._connected_at: datetime | None = None
|
|
self._last_success_at: datetime | None = None
|
|
self._last_error: str | None = None
|
|
self._protocol_version: int | None = None
|
|
self._reconnect_attempt = 0
|
|
self._last_nonzero: dict[str, list[RGBColor]] = {}
|
|
|
|
@property
|
|
def connected(self) -> bool:
|
|
return self._writer is not None and not self._writer.is_closing() and self._protocol_version == 5
|
|
|
|
async def start(self) -> None:
|
|
if self._monitor_task and not self._monitor_task.done():
|
|
return
|
|
self._stopping = False
|
|
self._monitor_task = asyncio.create_task(self._monitor(), name="openrgb-monitor")
|
|
|
|
async def stop(self) -> None:
|
|
self._stopping = True
|
|
if self._monitor_task:
|
|
self._monitor_task.cancel()
|
|
with contextlib.suppress(asyncio.CancelledError):
|
|
await self._monitor_task
|
|
await self._disconnect()
|
|
|
|
def set_inventory_callback(self, callback: InventoryCallback | None) -> None:
|
|
self._inventory_callback = callback
|
|
|
|
async def _monitor(self) -> None:
|
|
while not self._stopping:
|
|
try:
|
|
await self._ensure_connected()
|
|
if not self._controllers:
|
|
await self.inventory()
|
|
await self._notify_inventory_callback()
|
|
await asyncio.sleep(10)
|
|
except asyncio.CancelledError:
|
|
raise
|
|
except Exception as exc:
|
|
self._last_error = str(exc)
|
|
LOGGER.warning(
|
|
"OpenRGB connection unavailable: %s",
|
|
exc,
|
|
extra={"connector": self.id, "error_category": "connection"},
|
|
)
|
|
await self._disconnect()
|
|
delay = min(30.0, 0.5 * (2 ** min(self._reconnect_attempt, 6)))
|
|
self._reconnect_attempt += 1
|
|
await asyncio.sleep(delay)
|
|
|
|
async def _ensure_connected(self) -> None:
|
|
if self.connected:
|
|
return
|
|
async with self._connect_lock:
|
|
if self.connected:
|
|
return
|
|
await self._disconnect()
|
|
try:
|
|
connect = asyncio.open_connection(self.settings.openrgb_host, self.settings.openrgb_port)
|
|
self._reader, self._writer = await asyncio.wait_for(
|
|
connect, timeout=self.settings.openrgb_connect_timeout
|
|
)
|
|
self._reader_task = asyncio.create_task(self._reader_loop(), name="openrgb-reader")
|
|
header, payload = await self._request_connected(
|
|
PacketId.REQUEST_PROTOCOL_VERSION,
|
|
struct.pack("<I", PROTOCOL_VERSION),
|
|
timeout=self.settings.openrgb_connect_timeout,
|
|
)
|
|
del header
|
|
if len(payload) != 4:
|
|
raise ProtocolError("OpenRGB-protocolantwoord heeft geen vier bytes.")
|
|
server_version = struct.unpack("<I", payload)[0]
|
|
if server_version != PROTOCOL_VERSION:
|
|
raise ConnectorError(
|
|
"openrgb_version_mismatch",
|
|
f"OpenRGB SDK-protocol {server_version} is niet compatibel met vereist protocol 5.",
|
|
status_code=503,
|
|
retryable=False,
|
|
details={"server_version": server_version, "required_version": PROTOCOL_VERSION},
|
|
)
|
|
self._protocol_version = server_version
|
|
await self._send_connected(
|
|
0, PacketId.SET_CLIENT_NAME, b"LumaOps\0", expect_response=False
|
|
)
|
|
self._connected_at = datetime.now(UTC)
|
|
self._last_success_at = self._connected_at
|
|
self._last_error = None
|
|
self._reconnect_attempt = 0
|
|
LOGGER.info(
|
|
"Connected to OpenRGB SDK protocol 5",
|
|
extra={"connector": self.id},
|
|
)
|
|
except Exception:
|
|
await self._disconnect()
|
|
raise
|
|
|
|
async def _disconnect(self) -> None:
|
|
writer, self._writer = self._writer, None
|
|
reader_task, self._reader_task = self._reader_task, None
|
|
self._reader = None
|
|
self._protocol_version = None
|
|
self._controllers.clear()
|
|
if reader_task and reader_task is not asyncio.current_task():
|
|
reader_task.cancel()
|
|
with contextlib.suppress(asyncio.CancelledError):
|
|
await reader_task
|
|
if writer:
|
|
writer.close()
|
|
with contextlib.suppress(Exception):
|
|
await writer.wait_closed()
|
|
error = ConnectorError(
|
|
"openrgb_disconnected",
|
|
"De verbinding met OpenRGB is verbroken.",
|
|
retryable=True,
|
|
)
|
|
for queue in self._response_queues.values():
|
|
with contextlib.suppress(asyncio.QueueFull):
|
|
queue.put_nowait(error)
|
|
|
|
async def _reader_loop(self) -> None:
|
|
assert self._reader is not None
|
|
try:
|
|
while not self._stopping:
|
|
raw_header = await self._reader.readexactly(HEADER.size)
|
|
header = parse_header(raw_header, self.settings.openrgb_max_packet_size)
|
|
payload = await self._reader.readexactly(header.payload_size)
|
|
if header.packet_id == PacketId.DEVICE_LIST_UPDATED:
|
|
if payload:
|
|
raise ProtocolError("Device-list-updated bevat onverwachte data.")
|
|
self._controllers.clear()
|
|
if self._inventory_callback:
|
|
asyncio.create_task(
|
|
self._notify_inventory_callback(), name="openrgb-inventory-change"
|
|
)
|
|
continue
|
|
queue = self._response_queues.setdefault(header.packet_id, asyncio.Queue(maxsize=1))
|
|
if queue.full():
|
|
raise ProtocolError(
|
|
"Onverwacht dubbel OpenRGB-antwoord.", packet_id=header.packet_id
|
|
)
|
|
queue.put_nowait((header, payload))
|
|
except asyncio.CancelledError:
|
|
raise
|
|
except (asyncio.IncompleteReadError, ConnectionError, OSError, ProtocolError) as exc:
|
|
self._last_error = str(exc)
|
|
error = ConnectorError(
|
|
"openrgb_connection_lost",
|
|
"De verbinding met OpenRGB is onverwacht verbroken.",
|
|
retryable=True,
|
|
details={"reason": str(exc)},
|
|
)
|
|
for queue in self._response_queues.values():
|
|
with contextlib.suppress(asyncio.QueueFull):
|
|
queue.put_nowait(error)
|
|
if self._writer:
|
|
self._writer.close()
|
|
|
|
async def _notify_inventory_callback(self) -> None:
|
|
if self._inventory_callback:
|
|
await self._inventory_callback(self.id)
|
|
|
|
async def _send_connected(
|
|
self,
|
|
device_index: int,
|
|
packet_id: int | PacketId,
|
|
payload: bytes = b"",
|
|
*,
|
|
expect_response: bool = False,
|
|
) -> None:
|
|
del expect_response
|
|
if self._writer is None or self._writer.is_closing():
|
|
raise ConnectorError(
|
|
"openrgb_disconnected", "OpenRGB is niet verbonden.", retryable=True
|
|
)
|
|
if len(payload) > self.settings.openrgb_max_packet_size:
|
|
raise ProtocolError("Uitgaand OpenRGB-pakket is te groot.", size=len(payload))
|
|
self._writer.write(pack_header(device_index, packet_id, len(payload)) + payload)
|
|
await self._writer.drain()
|
|
|
|
async def _request_connected(
|
|
self,
|
|
packet_id: int | PacketId,
|
|
payload: bytes = b"",
|
|
*,
|
|
device_index: int = 0,
|
|
timeout: float | None = None,
|
|
) -> tuple[PacketHeader, bytes]:
|
|
packet_value = int(packet_id)
|
|
async with self._request_lock:
|
|
queue = self._response_queues.setdefault(packet_value, asyncio.Queue(maxsize=1))
|
|
while not queue.empty():
|
|
queue.get_nowait()
|
|
await self._send_connected(device_index, packet_id, payload, expect_response=True)
|
|
try:
|
|
result = await asyncio.wait_for(
|
|
queue.get(), timeout=timeout or self.settings.openrgb_command_timeout
|
|
)
|
|
except TimeoutError as exc:
|
|
raise ConnectorError(
|
|
"openrgb_timeout",
|
|
"OpenRGB antwoordde niet binnen de ingestelde tijd.",
|
|
retryable=True,
|
|
details={"packet_id": packet_value},
|
|
) from exc
|
|
if isinstance(result, Exception):
|
|
raise result
|
|
return result
|
|
|
|
async def _request(
|
|
self,
|
|
packet_id: int | PacketId,
|
|
payload: bytes = b"",
|
|
*,
|
|
device_index: int = 0,
|
|
) -> tuple[PacketHeader, bytes]:
|
|
await self._ensure_connected()
|
|
return await self._request_connected(
|
|
packet_id, payload, device_index=device_index
|
|
)
|
|
|
|
async def _write(self, device_index: int, packet_id: int | PacketId, payload: bytes = b"") -> None:
|
|
await self._ensure_connected()
|
|
async with self._write_lock:
|
|
await asyncio.wait_for(
|
|
self._send_connected(device_index, packet_id, payload),
|
|
timeout=self.settings.openrgb_command_timeout,
|
|
)
|
|
self._last_success_at = datetime.now(UTC)
|
|
|
|
async def test_connection(self) -> ConnectorHealth:
|
|
started = time.perf_counter()
|
|
try:
|
|
await self._ensure_connected()
|
|
await self._request(PacketId.REQUEST_CONTROLLER_COUNT)
|
|
return ConnectorHealth(
|
|
status=HealthStatus.HEALTHY,
|
|
message="OpenRGB SDK protocol 5 is bereikbaar.",
|
|
connected=True,
|
|
latency_ms=(time.perf_counter() - started) * 1000,
|
|
last_success_at=datetime.now(UTC),
|
|
details={"protocol_version": self._protocol_version},
|
|
)
|
|
except Exception as exc:
|
|
return ConnectorHealth(
|
|
status=HealthStatus.DEGRADED,
|
|
message=str(exc),
|
|
connected=False,
|
|
latency_ms=(time.perf_counter() - started) * 1000,
|
|
last_success_at=self._last_success_at,
|
|
details={"host": self.settings.openrgb_host, "port": self.settings.openrgb_port},
|
|
)
|
|
|
|
async def health(self) -> ConnectorHealth:
|
|
return ConnectorHealth(
|
|
status=HealthStatus.HEALTHY if self.connected else HealthStatus.DEGRADED,
|
|
message="OpenRGB is verbonden." if self.connected else (self._last_error or "OpenRGB is niet verbonden."),
|
|
connected=self.connected,
|
|
last_success_at=self._last_success_at,
|
|
details={
|
|
"host": self.settings.openrgb_host,
|
|
"port": self.settings.openrgb_port,
|
|
"protocol_version": self._protocol_version,
|
|
"controllers": len(self._controllers),
|
|
"connected_at": self._connected_at.isoformat() if self._connected_at else None,
|
|
},
|
|
)
|
|
|
|
async def discover(self) -> list[ConnectorDevice]:
|
|
return await self.inventory()
|
|
|
|
async def inventory(self) -> list[ConnectorDevice]:
|
|
_header, payload = await self._request(PacketId.REQUEST_CONTROLLER_COUNT)
|
|
if len(payload) != 4:
|
|
raise ProtocolError("OpenRGB-controllertelling heeft geen vier bytes.")
|
|
count = struct.unpack("<I", payload)[0]
|
|
if count > 4096:
|
|
raise ProtocolError("OpenRGB rapporteert onredelijk veel controllers.", count=count)
|
|
controllers: dict[str, Controller] = {}
|
|
for index in range(count):
|
|
_header, controller_payload = await self._request(
|
|
PacketId.REQUEST_CONTROLLER_DATA,
|
|
struct.pack("<I", PROTOCOL_VERSION),
|
|
device_index=index,
|
|
)
|
|
controller = parse_controller(controller_payload, index)
|
|
controllers[controller.fingerprint] = controller
|
|
self._controllers = controllers
|
|
self._last_success_at = datetime.now(UTC)
|
|
return [self._normalize(controller) for controller in controllers.values()]
|
|
|
|
def _normalize(self, controller: Controller) -> ConnectorDevice:
|
|
active_mode = next(
|
|
(mode for mode in controller.modes if mode.index == controller.active_mode), None
|
|
)
|
|
active_flags = ModeFlag(active_mode.flags) if active_mode else ModeFlag(0)
|
|
colors = (
|
|
active_mode.colors
|
|
if active_mode and ModeFlag.HAS_MODE_SPECIFIC_COLOR in active_flags
|
|
else controller.colors
|
|
)
|
|
direct_mode = not active_mode or active_mode.name.casefold() in {"direct", "custom"}
|
|
if active_mode and active_mode.name.casefold() == "off":
|
|
power = False
|
|
elif direct_mode:
|
|
power = any(color.red or color.green or color.blue for color in controller.colors)
|
|
else:
|
|
power = True
|
|
brightness = None
|
|
if active_mode and ModeFlag.HAS_BRIGHTNESS in ModeFlag(active_mode.flags):
|
|
span = active_mode.brightness_max - active_mode.brightness_min
|
|
brightness = (
|
|
round((active_mode.brightness - active_mode.brightness_min) * 100 / span)
|
|
if span
|
|
else 100
|
|
)
|
|
return ConnectorDevice(
|
|
external_id=controller.fingerprint,
|
|
fingerprint=controller.fingerprint,
|
|
name=controller.name,
|
|
vendor=controller.vendor or None,
|
|
model=controller.description or controller.name,
|
|
serial=controller.serial or None,
|
|
location=controller.location or None,
|
|
firmware_version=controller.firmware_version or None,
|
|
controller_index=controller.index,
|
|
source="openrgb",
|
|
device_type=controller.device_type_name,
|
|
capabilities=controller.capabilities,
|
|
state=DeviceState(
|
|
power=power,
|
|
brightness=brightness,
|
|
colors=colors,
|
|
mode=active_mode.name if active_mode else None,
|
|
mode_index=active_mode.index if active_mode else None,
|
|
speed=active_mode.speed if active_mode and active_mode.flags & ModeFlag.HAS_SPEED else None,
|
|
direction=(
|
|
active_mode.direction
|
|
if active_mode
|
|
and active_mode.flags
|
|
& (ModeFlag.HAS_DIRECTION_LR | ModeFlag.HAS_DIRECTION_UD | ModeFlag.HAS_DIRECTION_HV)
|
|
else None
|
|
),
|
|
),
|
|
zones=[
|
|
{
|
|
"index": zone.index,
|
|
"name": zone.name,
|
|
"type": zone.zone_type,
|
|
"led_count": zone.led_count,
|
|
"leds_min": zone.leds_min,
|
|
"leds_max": zone.leds_max,
|
|
"start_index": zone.start_index,
|
|
"resizable_effects_only": bool(zone.flags & ZoneFlag.RESIZE_EFFECTS_ONLY),
|
|
"segments": [
|
|
{
|
|
"name": segment.name,
|
|
"type": segment.zone_type,
|
|
"start_index": segment.start_index,
|
|
"led_count": segment.led_count,
|
|
}
|
|
for segment in zone.segments
|
|
],
|
|
}
|
|
for zone in controller.zones
|
|
],
|
|
modes=[
|
|
{
|
|
"index": mode.index,
|
|
"name": mode.name,
|
|
"flags": mode.flags,
|
|
"speed_min": mode.speed_min if mode.flags & ModeFlag.HAS_SPEED else None,
|
|
"speed_max": mode.speed_max if mode.flags & ModeFlag.HAS_SPEED else None,
|
|
"brightness": bool(mode.flags & ModeFlag.HAS_BRIGHTNESS),
|
|
"colors_min": mode.colors_min,
|
|
"colors_max": mode.colors_max,
|
|
}
|
|
for mode in controller.modes
|
|
],
|
|
led_count=len(controller.leds),
|
|
metadata={"controller_flags": controller.flags, "led_alt_names": controller.led_alt_names},
|
|
)
|
|
|
|
def _controller(self, external_id: str) -> Controller:
|
|
try:
|
|
return self._controllers[external_id]
|
|
except KeyError as exc:
|
|
raise ConnectorError(
|
|
"openrgb_identity_changed",
|
|
"De OpenRGB-controlleridentiteit is gewijzigd; voer eerst een rescan uit.",
|
|
status_code=409,
|
|
retryable=True,
|
|
) from exc
|
|
|
|
async def get_state(self, external_id: str) -> DeviceState:
|
|
controller = self._controller(external_id)
|
|
_header, payload = await self._request(
|
|
PacketId.REQUEST_CONTROLLER_DATA,
|
|
struct.pack("<I", PROTOCOL_VERSION),
|
|
device_index=controller.index,
|
|
)
|
|
refreshed = parse_controller(payload, controller.index)
|
|
if refreshed.fingerprint != external_id:
|
|
self._controllers.clear()
|
|
raise ConnectorError(
|
|
"openrgb_identity_mismatch",
|
|
"OpenRGB rapporteert een ander apparaat op de verwachte controllerindex.",
|
|
status_code=409,
|
|
retryable=True,
|
|
)
|
|
self._controllers[external_id] = refreshed
|
|
return self._normalize(refreshed).state
|
|
|
|
async def set_state(self, external_id: str, state: DeviceState) -> DeviceState:
|
|
controller = self._controller(external_id)
|
|
# Re-read identity immediately before a hardware write.
|
|
await self.get_state(external_id)
|
|
controller = self._controller(external_id)
|
|
colors = list(state.colors or [])
|
|
selected_mode: Mode | None = None
|
|
mode_specific_colors = False
|
|
|
|
if (
|
|
state.mode is not None
|
|
or state.mode_index is not None
|
|
or state.brightness is not None
|
|
or state.speed is not None
|
|
or state.direction is not None
|
|
):
|
|
selected_mode = self._select_mode(controller, state)
|
|
mode = selected_mode
|
|
mode_flags = ModeFlag(mode.flags)
|
|
mode_specific_colors = bool(
|
|
colors
|
|
and ModeFlag.HAS_MODE_SPECIFIC_COLOR in mode_flags
|
|
and state.zone_index is None
|
|
and state.led_index is None
|
|
)
|
|
brightness_percent = state.brightness
|
|
if (
|
|
brightness_percent is not None
|
|
and ModeFlag.HAS_BRIGHTNESS not in ModeFlag(mode.flags)
|
|
and colors
|
|
):
|
|
LOGGER.info(
|
|
"OpenRGB brightness omitted for color write: controller=%s mode=%s",
|
|
controller.index,
|
|
mode.name,
|
|
)
|
|
brightness_percent = None
|
|
payload = pack_mode(
|
|
mode,
|
|
mode.index,
|
|
brightness_percent=brightness_percent,
|
|
speed=state.speed,
|
|
direction=state.direction,
|
|
colors=colors if mode_specific_colors else None,
|
|
)
|
|
await self._write(controller.index, PacketId.UPDATE_MODE, payload)
|
|
controller.active_mode = mode.index
|
|
if brightness_percent is not None:
|
|
span = mode.brightness_max - mode.brightness_min
|
|
mode.brightness = round(mode.brightness_min + span * brightness_percent / 100)
|
|
if state.speed is not None:
|
|
mode.speed = state.speed
|
|
if state.direction is not None:
|
|
mode.direction = state.direction
|
|
if mode_specific_colors:
|
|
mode.colors = list(colors)
|
|
|
|
has_leds = bool(controller.leds)
|
|
if state.power is False:
|
|
if has_leds and controller.colors and any(
|
|
c.red or c.green or c.blue for c in controller.colors
|
|
):
|
|
self._last_nonzero[external_id] = list(controller.colors)
|
|
colors = [RGBColor(red=0, green=0, blue=0)] if has_leds else []
|
|
mode_specific_colors = False
|
|
elif state.power is True and not colors and selected_mode is None and has_leds:
|
|
colors = self._last_nonzero.get(
|
|
external_id, [RGBColor(red=255, green=255, blue=255)]
|
|
)
|
|
|
|
if colors and not mode_specific_colors:
|
|
active_mode = next(
|
|
(mode for mode in controller.modes if mode.index == controller.active_mode),
|
|
None,
|
|
)
|
|
if (
|
|
selected_mode is not None
|
|
or active_mode is None
|
|
or active_mode.name.casefold() not in {"custom", "direct"}
|
|
):
|
|
await self._write(controller.index, PacketId.SET_CUSTOM_MODE)
|
|
custom_mode = next(
|
|
(
|
|
mode
|
|
for mode in controller.modes
|
|
if mode.name.casefold() in {"custom", "direct"}
|
|
),
|
|
None,
|
|
)
|
|
if custom_mode is not None:
|
|
controller.active_mode = custom_mode.index
|
|
if state.led_index is not None:
|
|
if len(colors) != 1:
|
|
raise ProtocolError("Een ledopdracht vereist exact één kleur.")
|
|
payload = pack_update_single_led(state.led_index, colors[0], len(controller.leds))
|
|
await self._write(controller.index, PacketId.UPDATE_SINGLE_LED, payload)
|
|
controller.colors[state.led_index] = colors[0]
|
|
elif state.zone_index is not None:
|
|
if state.zone_index >= len(controller.zones):
|
|
raise ProtocolError("OpenRGB-zoneindex valt buiten bereik.")
|
|
zone = controller.zones[state.zone_index]
|
|
expanded = expand_colors(colors, zone.led_count)
|
|
payload = pack_update_zone(zone.index, expanded, zone.led_count)
|
|
await self._write(controller.index, PacketId.UPDATE_ZONE_LEDS, payload)
|
|
controller.colors[zone.start_index : zone.start_index + zone.led_count] = expanded
|
|
else:
|
|
expanded = expand_colors(colors, len(controller.leds))
|
|
payload = pack_update_leds(expanded, len(controller.leds))
|
|
await self._write(controller.index, PacketId.UPDATE_LEDS, payload)
|
|
controller.colors = expanded
|
|
if any(color.red or color.green or color.blue for color in controller.colors):
|
|
self._last_nonzero[external_id] = list(controller.colors)
|
|
# OpenRGB may map SET_CUSTOM_MODE to a controller-specific mode (for example,
|
|
# Corsair DRAM reports Direct even when it also advertises Custom). Read the
|
|
# controller back so persisted desired state reflects hardware truth and does
|
|
# not cause an endless reconciliation loop after discovery or restart.
|
|
return await self.get_state(external_id)
|
|
|
|
def _select_mode(self, controller: Controller, state: DeviceState) -> Mode:
|
|
if state.mode_index is not None:
|
|
mode = next((item for item in controller.modes if item.index == state.mode_index), None)
|
|
elif state.mode is not None:
|
|
mode = next(
|
|
(item for item in controller.modes if item.name.casefold() == state.mode.casefold()),
|
|
None,
|
|
)
|
|
else:
|
|
mode = next(
|
|
(item for item in controller.modes if item.index == controller.active_mode), None
|
|
)
|
|
if mode is None:
|
|
raise ProtocolError("De gevraagde OpenRGB-modus bestaat niet.")
|
|
return mode
|
|
|
|
async def rescan(self) -> None:
|
|
await self._write(0, PacketId.REQUEST_RESCAN_DEVICES)
|
|
self._controllers.clear()
|
|
|
|
async def list_profiles(self) -> list[str]:
|
|
_header, payload = await self._request(PacketId.REQUEST_PROFILE_LIST)
|
|
return parse_profile_list(payload)
|
|
|
|
async def save_profile(self, name: str) -> None:
|
|
await self._profile_write(PacketId.REQUEST_SAVE_PROFILE, name)
|
|
|
|
async def load_profile(self, name: str) -> None:
|
|
await self._profile_write(PacketId.REQUEST_LOAD_PROFILE, name)
|
|
devices = await self.inventory()
|
|
for device in devices:
|
|
controller = self._controller(device.external_id)
|
|
mode = self._select_mode(controller, DeviceState())
|
|
await self._write(controller.index, PacketId.UPDATE_MODE, pack_mode(mode, mode.index))
|
|
|
|
async def delete_profile(self, name: str) -> None:
|
|
await self._profile_write(PacketId.REQUEST_DELETE_PROFILE, name)
|
|
|
|
async def _profile_write(self, packet_id: PacketId, name: str) -> None:
|
|
if not name or "\0" in name or len(name.encode("utf-8")) > 255:
|
|
raise ProtocolError("Ongeldige OpenRGB-profielnaam.")
|
|
await self._write(0, packet_id, name.encode("utf-8") + b"\0")
|
|
|
|
async def resize_zone(self, external_id: str, zone_index: int, new_size: int) -> None:
|
|
controller = self._controller(external_id)
|
|
if not 0 <= zone_index < len(controller.zones):
|
|
raise ProtocolError("OpenRGB-zoneindex valt buiten bereik.")
|
|
zone = controller.zones[zone_index]
|
|
if not min(zone.leds_min, zone.leds_max) <= new_size <= max(zone.leds_min, zone.leds_max):
|
|
raise ProtocolError("Nieuwe OpenRGB-zonegrootte valt buiten bereik.")
|
|
await self._write(controller.index, PacketId.RESIZE_ZONE, struct.pack("<ii", zone_index, new_size))
|
|
LOGGER.info(
|
|
"OpenRGB-zone resized: controller=%s zone=%s leds=%s",
|
|
controller.index,
|
|
zone_index,
|
|
new_size,
|
|
)
|
|
|
|
async def clear_segments(self, external_id: str, zone_index: int) -> None:
|
|
controller = self._controller(external_id)
|
|
if not 0 <= zone_index < len(controller.zones):
|
|
raise ProtocolError("OpenRGB-zoneindex valt buiten bereik.")
|
|
await self._write(controller.index, PacketId.CLEAR_SEGMENTS, struct.pack("<i", zone_index))
|
|
|
|
def configuration_schema(self) -> dict[str, Any]:
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"host": {"type": "string", "default": "127.0.0.1"},
|
|
"port": {"type": "integer", "minimum": 1, "maximum": 65535, "default": 6742},
|
|
},
|
|
"required": ["host", "port"],
|
|
"secret_fields": [],
|
|
"warning": "OpenRGB SDK heeft geen authenticatie of TLS; gebruik uitsluitend loopback of een beveiligde tunnel.",
|
|
}
|
|
|
|
|
|
def expand_colors(colors: list[RGBColor], expected: int) -> list[RGBColor]:
|
|
if expected <= 0:
|
|
raise ProtocolError("OpenRGB-controller heeft geen adresseerbare leds.")
|
|
if colors and all(color == colors[0] for color in colors):
|
|
return [colors[0]] * expected
|
|
if len(colors) != expected:
|
|
raise ProtocolError(
|
|
"Geef één kleur of exact één kleur per led op.", colors=len(colors), expected=expected
|
|
)
|
|
return colors
|