Skip to content
This repository was archived by the owner on Mar 31, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ systembridgeshared==4.0.7
typer==0.12.5
uvicorn[standard]==0.30.6
zeroconf==0.133.0
monitorcontrol==3.1.0
3 changes: 3 additions & 0 deletions systembridgebackend/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@
TYPE_SETTINGS_RESULT = "SETTINGS_RESULT"
TYPE_UNREGISTER_DATA_LISTENER = "UNREGISTER_DATA_LISTENER"
TYPE_UPDATE_SETTINGS = "UPDATE_SETTINGS"
TYPE_DISPLAY_UPDATE_SETTING = "DISPLAY_UPDATE_SETTING"
TYPE_DISPLAY_SETTING_UPDATED = "DISPLAY_SETTING_UPDATED"

# Event Subtypes
SUBTYPE_BAD_DIRECTORY = "BAD_DIRECTORY"
Expand All @@ -108,4 +110,5 @@
SUBTYPE_MISSING_TITLE = "MISSING_TITLE"
SUBTYPE_MISSING_TOKEN = "MISSING_TOKEN"
SUBTYPE_MISSING_VALUE = "MISSING_VALUE"
SUBTYPE_OPERATION_FAILED = "OPERATION_FAILED"
SUBTYPE_UNKNOWN_EVENT = "UNKNOWN_EVENT"
56 changes: 46 additions & 10 deletions systembridgebackend/handlers/data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Data."""

from collections.abc import Awaitable, Callable
from queue import Queue
from typing import Any

from systembridgemodels.modules import ModulesData
Expand All @@ -11,45 +12,80 @@


class DataUpdate(Base):
"""Data Update."""
"""
The update threads scheduler, managing data update thread and media update thread.
Also holds a reference to the collected data.
"""

def __init__(
self,
updated_callback: Callable[[str], Awaitable[None]],
updated_callback: Callable[[str], Awaitable[None]]
) -> None:
"""Initialise."""
"""
Initialize a new instance of DataUpdate.

:param updated_callback: The callback to be invoked when data is updated.
"""
super().__init__()
self._updated_callback = updated_callback
self.update_data_thread: DataUpdateThread | None = None
self.update_media_thread: MediaUpdateThread | None = None
self.update_data_queue: Queue[dict] = Queue()
self.update_media_queue: Queue[dict] = Queue()

self.data = ModulesData()
"""Data collected by the system bridge"""

async def _data_updated_callback(
self,
name: str,
data: Any,
) -> None:
"""Update the data with the given name and value, and invoke the updated callback."""
"""
Update collected data of given module, then invoke :field:`_updated_callback`.

:param name: module name triggering the update. should be any field names of :class:`ModulesData`.
:param data: The dataclass object to be assigned to given module.
"""
setattr(self.data, name, data)
await self._updated_callback(name)

def request_update_data(self) -> None:
"""Request update data."""
def request_update_data(self, **kwargs) -> None:
"""
Trigger data update by enqueueing `kwargs` to :field:`update_data_queue`,
these args will be passed to `DataUpdateThread.update`,
and then to `ModulesUpdate.update_data`.

will start :field:`update_data_thread` if necessary.

:param kwargs: The parameters to be passed into `ModulesUpdate.update_data`.
"""

if self.update_data_thread is not None and self.update_data_thread.is_alive():
self._logger.info("Update data thread already running")
self._logger.warning("Force update data with params: %s", kwargs)
self.update_data_queue.put_nowait(kwargs)
return

self._logger.info("Starting update data thread..")
self.update_data_thread = DataUpdateThread(self._data_updated_callback)
self.update_data_thread = DataUpdateThread(self._data_updated_callback, self.update_data_queue)
self.update_data_thread.start()
if kwargs:
self.update_data_queue.put_nowait(kwargs)

def request_update_media_data(self) -> None:
"""Request update media data."""
"""
Trigger media update by enqueueing `kwargs` to :field:`update_media_queue`,
these args will be passed to `MediaUpdateThread.update`,
and then to `Media.update_media_info`.

will start :field:`update_media_thread` if necessary.

:param kwargs: The parameters to be passed into `Media.update_media_info`.
"""
if self.update_media_thread is not None and self.update_media_thread.is_alive():
self._logger.info("Update media thread already running")
return

self._logger.info("Starting update media thread..")
self.update_media_thread = MediaUpdateThread(self._data_updated_callback)
self.update_media_thread = MediaUpdateThread(self._data_updated_callback, self.update_media_queue)
self.update_media_thread.start()
99 changes: 99 additions & 0 deletions systembridgebackend/handlers/display.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""display control DDC/CI handlers."""
from monitorcontrol.monitorcontrol import get_monitors

from systembridgemodels.modules.displays import InputSource, PowerMode

from ..modules.displays import vcpcode_volume


def set_brightness(
monitor_id: int,
brightness: int,
) -> None:
"""
Set the brightness of a monitor.

Args:
monitor_id (int): The ID of the monitor to set the brightness for.
brightness (int): The new brightness level, on a scale from 0 to 100.

Raises:
ValueError: If the brightness value is not in the range [0, 100].
"""
monitors = get_monitors()
with monitors[monitor_id] as monitor:
monitor.set_luminance(brightness)


def set_contrast(
monitor_id: int,
contrast: int,
) -> None:
"""
Set the contrast of a monitor.

Args:
monitor_id (int): The ID of the monitor to set the contrast for.
contrast (int): The new contrast level, on a scale from 0 to 100.

Raises:
ValueError: If the contrast value is not in the range [0, 100].
"""
monitors = get_monitors()
with monitors[monitor_id] as monitor:
monitor.set_contrast(contrast)


def set_volume(
monitor_id: int,
volume: int,
) -> None:
"""
Set the volume of a monitor.

Args:
monitor_id (int): The ID of the monitor to set the volume for.
volume (int): The new volume level, on a scale from 0 to 100.

Raises:
ValueError: If the volume value is not in the range [0, 100].
"""
monitors = get_monitors()
with monitors[monitor_id] as monitor:
monitor._set_vcp_feature(vcpcode_volume, volume) # pylint: disable=protected-access


def set_power_state(
monitor_id: int,
power_state: PowerMode | int | str,
) -> None:
"""
Set the power state of a monitor.

Args:
monitor_id (int): The ID of the monitor to set the power state for.
power_state (int | str | PowerMode): The new power state, can be an integer,
a string representing the power mode, or a `PowerMode` enum value.
"""
monitors = get_monitors()
with monitors[monitor_id] as monitor:
monitor.set_power_mode(power_state)


def set_input_source(
monitor_id: int,
input_source: InputSource | int | str,
) -> None:
"""
Set the input source of a monitor.

Args:
monitor_id: The ID of the monitor to set the input source for.
input_source: The new input source, which can be an integer, string, or `InputSource` enum value.

Raises:
ValueError: If the input source is not recognized by the monitor.
"""
monitors = get_monitors()
with monitors[monitor_id] as monitor:
monitor.set_input_source(input_source)
12 changes: 9 additions & 3 deletions systembridgebackend/handlers/threads/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,18 @@ def run(self) -> None:
"""Run."""
raise NotImplementedError

def join(
def interrupt(
self,
timeout: float | None = None,
) -> None:
"""Join."""
self._logger.info("Stopping thread")
"""
Interrupt thread running by setting the `self.stopping` flag.
Child classes should check `self.stopping` in its `run()` implementation
to support this feature.

Should be called instead of `BaseThread.join()`.
"""
self._logger.info("Interrupting %s", self.__class__.__name__)
self.stopping = True
loop = asyncio.get_event_loop()
asyncio.tasks.all_tasks(loop).clear()
Expand Down
8 changes: 5 additions & 3 deletions systembridgebackend/handlers/threads/data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Data update thread handler."""

from collections.abc import Awaitable, Callable
from queue import Queue
from typing import Any, Final, override

from ...modules import ModulesUpdate
Expand All @@ -15,15 +16,16 @@ class DataUpdateThread(UpdateThread):
def __init__(
self,
updated_callback: Callable[[str, Any], Awaitable[None]],
update_queue: Queue[str],
) -> None:
"""Initialise."""
super().__init__(UPDATE_INTERVAL)
super().__init__(UPDATE_INTERVAL, update_queue)
self._update_cls = ModulesUpdate(updated_callback)

@override
async def update(self) -> None:
async def update(self, modules=None) -> None:
"""Update."""
if self.stopping:
return

await self._update_cls.update_data()
await self._update_cls.update_data(modules=modules)
4 changes: 3 additions & 1 deletion systembridgebackend/handlers/threads/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from collections.abc import Awaitable, Callable
import datetime
import platform
from queue import Queue
from typing import Final, override

from systembridgemodels.modules.media import Media as MediaInfo
Expand All @@ -18,9 +19,10 @@ class MediaUpdateThread(UpdateThread):
def __init__(
self,
updated_callback: Callable[[str, MediaInfo], Awaitable[None]],
update_queue: Queue[str],
) -> None:
"""Initialise."""
super().__init__(UPDATE_INTERVAL)
super().__init__(UPDATE_INTERVAL, update_queue)
self._updated_callback = updated_callback

if platform.system() != "Windows":
Expand Down
Loading