Skip to content

Commit 5cf5d07

Browse files
committed
2 parents ad035f2 + a2ed941 commit 5cf5d07

File tree

13 files changed

+205
-88
lines changed

13 files changed

+205
-88
lines changed

README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ The SDK allows you to create custom event listeners and events by extending the
292292
293293
To create a custom event listener:
294294
295-
1. Create new event model that inherits from `EventBase`.
295+
1. Create new event model that inherits from `EventBase["eventName"]`.
296296
2. Create a new class that inherits from `EventListener`.
297297
a. Implement the required `listen` and `stop` methods. The `listen` method should yield results as a json string that matches the new event model.
298298
b. List the new event classes in the `event_models` class variable of the new `EventListener` class.
@@ -307,11 +307,15 @@ from streamdeck.event_listener import EventListener
307307
from streamdeck.models.events import EventBase
308308
309309
310-
class MyCustomEvent(EventBase):
311-
event: Literal["somethingHappened"]
312-
... # Define additional data attributes here
310+
class MyCustomEvent(EventBase["somethingHappened"]):
311+
# The 'event' field's type annotation is internally set as Literal["somethingHappened"]
312+
# Define additional data attributes here
313+
result: str
314+
313315

314316
class MyCustomEventListener(EventListener):
317+
event_models = [MyCustomEvent]
318+
315319
def listen(self) -> Generator[str | bytes, None, None]:
316320
...
317321
# Listen/poll for something here in a loop, and yield the result.
@@ -320,7 +324,7 @@ class MyCustomEventListener(EventListener):
320324
# while self._running is True:
321325
# result = module.check_status()
322326
# if result is not None:
323-
# yield json.dumps({"event": "somethingHappend", "result": result})
327+
# yield json.dumps({"event": "somethingHappened", "result": result})
324328
# time.sleep(1)
325329

326330
def stop(self) -> None:
@@ -344,7 +348,7 @@ To use your custom event listener, add it to your `pyproject.toml` file:
344348
]
345349
```
346350
347-
The `event_listeners` list should contain strings in module format for each module you want to use.
351+
The `event_listener_modules` list should contain strings in module format for each module you want to use.
348352
349353
350354
## Creating and Packaging Plugins

streamdeck/actions.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,12 @@
1111
from collections.abc import Callable, Generator
1212
from typing import Protocol
1313

14-
from typing_extensions import ParamSpec, TypeAlias, TypeVar # noqa: UP035
14+
from typing_extensions import ParamSpec, TypeVar
1515

1616
from streamdeck.models.events import EventBase
17+
from streamdeck.types import ActionUUIDStr, EventNameStr
1718

1819

19-
EventNameStr: TypeAlias = str # noqa: UP040
20-
"""Type alias for the event name string.
21-
22-
We don't define literal string values here, as the list of available event names can be added to dynamically.
23-
"""
24-
2520

2621
EventModel_contra = TypeVar("EventModel_contra", bound=EventBase, default=EventBase, contravariant=True)
2722
InjectableParams = ParamSpec("InjectableParams", default=...)
@@ -39,11 +34,7 @@ class ActionBase(ABC):
3934
"""Base class for all actions."""
4035

4136
def __init__(self) -> None:
42-
"""Initialize an Action instance.
43-
44-
Args:
45-
uuid (str): The unique identifier for the action.
46-
"""
37+
"""Initialize an Action instance."""
4738
self._events: dict[EventNameStr, set[EventHandlerFunc]] = defaultdict(set)
4839

4940
def on(self, event_name: EventNameStr, /) -> Callable[[EventHandlerFunc[EventModel_contra, InjectableParams]], EventHandlerFunc[EventModel_contra, InjectableParams]]:
@@ -59,8 +50,8 @@ def on(self, event_name: EventNameStr, /) -> Callable[[EventHandlerFunc[EventMod
5950
KeyError: If the provided event name is not available.
6051
"""
6152
def _wrapper(func: EventHandlerFunc[EventModel_contra, InjectableParams]) -> EventHandlerFunc[EventModel_contra, InjectableParams]:
62-
# Cast to EventHandler with default type arguments so that the storage type is consistent.
63-
self._events[event_name].add(cast("EventHandler", func))
53+
# Cast to EventHandlerFunc with default type arguments so that the storage type is consistent.
54+
self._events[event_name].add(cast("EventHandlerFunc", func))
6455
return func
6556

6657
return _wrapper
@@ -98,7 +89,7 @@ class GlobalAction(ActionBase):
9889
class Action(ActionBase):
9990
"""Represents an action that can be performed for a specific action, with event handlers for specific event types."""
10091

101-
def __init__(self, uuid: str) -> None:
92+
def __init__(self, uuid: ActionUUIDStr) -> None:
10293
"""Initialize an Action instance.
10394
10495
Args:
@@ -128,7 +119,7 @@ def register(self, action: ActionBase) -> None:
128119
"""
129120
self._plugin_actions.append(action)
130121

131-
def get_action_handlers(self, event_name: EventNameStr, event_action_uuid: str | None = None) -> Generator[EventHandlerFunc, None, None]:
122+
def get_action_handlers(self, event_name: EventNameStr, event_action_uuid: ActionUUIDStr | None = None) -> Generator[EventHandlerFunc, None, None]:
132123
"""Get all event handlers for a specific event from all registered actions.
133124
134125
Args:

streamdeck/command_sender.py

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
if TYPE_CHECKING:
88
from typing import Any, Literal
99

10+
from streamdeck.types import (
11+
ActionInstanceUUIDStr,
12+
ActionUUIDStr,
13+
DeviceUUIDStr,
14+
EventNameStr,
15+
PluginDefinedData,
16+
)
1017
from streamdeck.websocket import WebSocketClient
1118

1219

@@ -16,30 +23,31 @@
1623

1724
class StreamDeckCommandSender:
1825
"""Class for sending command event messages to the Stream Deck software through a WebSocket client."""
26+
1927
def __init__(self, client: WebSocketClient, plugin_registration_uuid: str):
2028
self._client = client
2129
self._plugin_registration_uuid = plugin_registration_uuid
2230

23-
def _send_event(self, event: str, **kwargs: Any) -> None:
31+
def _send_event(self, event: EventNameStr, **kwargs: Any) -> None:
2432
self._client.send_event({
2533
"event": event,
2634
**kwargs,
2735
})
2836

29-
def set_settings(self, context: str, payload: dict[str, Any]) -> None:
37+
def set_settings(self, context: ActionInstanceUUIDStr, payload: PluginDefinedData) -> None:
3038
self._send_event(
3139
event="setSettings",
3240
context=context,
3341
payload=payload,
3442
)
3543

36-
def get_settings(self, context: str) -> None:
44+
def get_settings(self, context: ActionInstanceUUIDStr) -> None:
3745
self._send_event(
3846
event="getSettings",
3947
context=context,
4048
)
4149

42-
def set_global_settings(self, payload: dict[str, Any]) -> None:
50+
def set_global_settings(self, payload: PluginDefinedData) -> None:
4351
self._send_event(
4452
event="setGlobalSettings",
4553
context=self._plugin_registration_uuid,
@@ -52,14 +60,14 @@ def get_global_settings(self) -> None:
5260
context=self._plugin_registration_uuid,
5361
)
5462

55-
def open_url(self, context: str, url: str) -> None:
63+
def open_url(self, context: ActionInstanceUUIDStr, url: str) -> None:
5664
self._send_event(
5765
event="openUrl",
5866
context=context,
5967
payload={"url": url},
6068
)
6169

62-
def log_message(self, context: str, message: str) -> None:
70+
def log_message(self, context: ActionInstanceUUIDStr, message: str) -> None:
6371
self._send_event(
6472
event="logMessage",
6573
context=context,
@@ -68,10 +76,10 @@ def log_message(self, context: str, message: str) -> None:
6876

6977
def set_title(
7078
self,
71-
context: str,
79+
context: ActionInstanceUUIDStr,
7280
state: int | None = None,
73-
target: str | None = None,
74-
title: str | None = None
81+
target: Literal["hardware", "software", "both"] | None = None,
82+
title: str | None = None,
7583
) -> None:
7684
payload = {}
7785

@@ -90,10 +98,10 @@ def set_title(
9098

9199
def set_image(
92100
self,
93-
context: str,
94-
image: str, # base64 encoded image,
95-
target: Literal["hardware", "software", "both"], # software, hardware, or both,
96-
state: int, # 0-based integer
101+
context: ActionInstanceUUIDStr,
102+
image: str, # base64 encoded image,
103+
target: Literal["hardware", "software", "both"],
104+
state: int,
97105
) -> None:
98106
"""...
99107
@@ -117,14 +125,26 @@ def set_image(
117125
},
118126
)
119127

120-
def set_feedback(self, context: str, payload: dict[str, Any]) -> None:
128+
def set_feedback(self, context: ActionInstanceUUIDStr, payload: PluginDefinedData) -> None:
129+
"""Set's the feedback of an existing layout associated with an action instance.
130+
131+
Args:
132+
context (str): Defines the context of the command, e.g. which action instance the command is intended for.
133+
payload (PluginDefinedData): Additional information supplied as part of the command.
134+
"""
121135
self._send_event(
122136
event="setFeedback",
123137
context=context,
124138
payload=payload,
125139
)
126140

127-
def set_feedback_layout(self, context: str, layout: str) -> None:
141+
def set_feedback_layout(self, context: ActionInstanceUUIDStr, layout: str) -> None:
142+
"""Sets the layout associated with an action instance.
143+
144+
Args:
145+
context (str): Defines the context of the command, e.g. which action instance the command is intended for.
146+
layout (str): Name of a pre-defined layout, or relative path to a custom one.
147+
"""
128148
self._send_event(
129149
event="setFeedbackLayout",
130150
context=context,
@@ -133,7 +153,7 @@ def set_feedback_layout(self, context: str, layout: str) -> None:
133153

134154
def set_trigger_description(
135155
self,
136-
context: str,
156+
context: ActionInstanceUUIDStr,
137157
rotate: str | None = None,
138158
push: str | None = None,
139159
touch: str | None = None,
@@ -170,21 +190,21 @@ def set_trigger_description(
170190
},
171191
)
172192

173-
def show_alert(self, context: str) -> None:
193+
def show_alert(self, context: ActionInstanceUUIDStr) -> None:
174194
"""Temporarily show an alert icon on the image displayed by an instance of an action."""
175195
self._send_event(
176196
event="showAlert",
177197
context=context,
178198
)
179199

180-
def show_ok(self, context: str) -> None:
200+
def show_ok(self, context: ActionInstanceUUIDStr) -> None:
181201
"""Temporarily show an OK checkmark icon on the image displayed by an instance of an action."""
182202
self._send_event(
183203
event="showOk",
184204
context=context,
185205
)
186206

187-
def set_state(self, context: str, state: int) -> None:
207+
def set_state(self, context: ActionInstanceUUIDStr, state: int) -> None:
188208
self._send_event(
189209
event="setState",
190210
context=context,
@@ -193,8 +213,8 @@ def set_state(self, context: str, state: int) -> None:
193213

194214
def switch_to_profile(
195215
self,
196-
context: str,
197-
device: str,
216+
context: ActionInstanceUUIDStr,
217+
device: DeviceUUIDStr,
198218
profile: str | None = None,
199219
page: int = 0,
200220
) -> None:
@@ -211,7 +231,7 @@ def switch_to_profile(
211231
page (int): Page to show when switching to the profile; indexed from 0.
212232
"""
213233
# TODO: Should validation happen that ensures the specified profile is declared in manifest.yaml?
214-
payload = {}
234+
payload: dict[str, str | int | None] = {}
215235

216236
if profile is not None:
217237
payload = {
@@ -226,18 +246,17 @@ def switch_to_profile(
226246
payload=payload,
227247
)
228248

229-
def send_to_property_inspector(self, context: str, payload: dict[str, Any]) -> None:
249+
def send_to_property_inspector(
250+
self, context: ActionInstanceUUIDStr, payload: PluginDefinedData
251+
) -> None:
230252
self._send_event(
231253
event="sendToPropertyInspector",
232254
context=context,
233255
payload=payload,
234256
)
235257

236258
def send_to_plugin(
237-
self,
238-
context: str,
239-
action: str,
240-
payload: dict[str, Any]
259+
self, context: ActionInstanceUUIDStr, action: ActionUUIDStr, payload: PluginDefinedData
241260
) -> None:
242261
"""Send a payload to another plugin.
243262

streamdeck/models/events/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
if TYPE_CHECKING:
3232
from typing import Final
3333

34+
from streamdeck.types import EventNameStr
35+
36+
3437

3538
DEFAULT_EVENT_MODELS: Final[list[type[EventBase]]] = [
3639
ApplicationDidLaunch,
@@ -56,16 +59,16 @@
5659
]
5760

5861

59-
def _get_default_event_names() -> set[str]:
60-
default_event_names: set[str] = set()
62+
def _get_default_event_names() -> set[EventNameStr]:
63+
default_event_names: set[EventNameStr] = set()
6164

6265
for event_model in DEFAULT_EVENT_MODELS:
6366
default_event_names.update(event_model.get_model_event_names())
6467

6568
return default_event_names
6669

6770

68-
DEFAULT_EVENT_NAMES: Final[set[str]] = _get_default_event_names()
71+
DEFAULT_EVENT_NAMES: Final[set[EventNameStr]] = _get_default_event_names()
6972

7073

7174

streamdeck/models/events/adapter.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88

99

1010
if TYPE_CHECKING:
11+
from typing_extensions import TypeIs
12+
1113
from streamdeck.models.events.base import EventBase
14+
from streamdeck.types import EventNameStr
1215

1316

1417
class EventAdapter:
@@ -17,7 +20,7 @@ def __init__(self) -> None:
1720
self._models: list[type[EventBase]] = []
1821
self._type_adapter: TypeAdapter[EventBase] | None = None
1922

20-
self._event_names: set[str] = set()
23+
self._event_names: set[EventNameStr] = set()
2124
"""A set of all event names that have been registered with the adapter.
2225
This set starts out containing the default event models defined by the library.
2326
"""
@@ -32,7 +35,7 @@ def add_model(self, model: type[EventBase]) -> None:
3235
# so `get_model_event_names()` returns a tuple of all event names, even if there is only one.
3336
self._event_names.update(model.get_model_event_names())
3437

35-
def event_name_exists(self, event_name: str) -> bool:
38+
def event_name_exists(self, event_name: str) -> TypeIs[EventNameStr]:
3639
"""Check if an event name has been registered with the adapter."""
3740
return event_name in self._event_names
3841

0 commit comments

Comments
 (0)