gri-gus / streamdeck-python-sdk

Library for creating Stream Deck plugins in Python.
Apache License 2.0
19 stars 4 forks source link
python streamdeck streamdeck-plugin streamdeck-python-sdk streamdeck-sdk streamdeckpython streamdeckpythonsdk streamdecksdk

streamdeck-python-sdk

PyPI PyPI Apache Elgato

lang-ru lang-ru

streamdeck-python-sdk

Library for creating Stream Deck plugins in Python.

PyPi: https://pypi.org/project/streamdeck-sdk/

Supported operating systems:

Supported Stream Deck versions: 6.0, 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7

Supported Python versions: 3.8 or later

Installation

⚠️ For correct operation on Windows, it is recommended to enable LongPaths support in the system: manual. Without this setting, problems with installation and use may occur!

pip install "streamdeck-sdk[dev]"

During this installation, additional libraries needed for development are installed:

⚠️ These libraries should not be present in the plugin's requirements.txt.

Features

⚠️ Limitations

  1. During installation and update of the plugin, the Internet must be available.

Windows

  1. Plugin requirements should take no longer than 30 seconds to install. This is a feature of Stream Deck on Windows, since the program restarts the plugin if a websocket connection has not been established within 30 seconds. Therefore, you need a good Internet connection when installing and updating the plugin.

Quick Start

  1. Install Python.
  2. Create a project folder and venv.
  3. Go to the project folder, activate venv and install the streamdeck-sdk library:
pip install "streamdeck-sdk[dev]"
  1. Run the command to start the project:
streamdeck_sdk startproject

After completing this step, the folder com.bestdeveloper.mytestplugin.sdPlugin will appear with a test project.

  1. Build the plugin using the command:

⚠️ The requirements.txt file should not contain the streamdeck-sdk-* libraries. If there are any, remove them.

streamdeck_sdk build -i com.bestdeveloper.mytestplugin.sdPlugin

⚠️ If you are using Windows and receive an error, then use the command:

streamdeck_sdk build -i com.bestdeveloper.mytestplugin.sdPlugin -F
  1. Install the releases/{date}/com.bestdeveloper.mytestplugin.streamDeckPlugin plugin into the Stream Deck application. Usually installed via double click.

⚠️ After installation, you need to wait about 40 seconds. At this time, requirements are installed.

  1. Go to the Stream Deck application in the MyTestCategory category and set the My action action to any button.
  2. Check the operation of the My action action. When clicked, the following happens:
    1. The GitHub page opens.
    2. ✅ appears on the button.

Next, edit the project in accordance with the official documentation Stream Deck SDK.

⚠️ Don't forget to edit the manifest.json file and the plugin name.

Debug mode

  1. Follow steps 1-4 from the "Quick Start" section.
  2. In the file com.bestdeveloper.mytestplugin.sdPlugin/code/main.py in the StreamDeck parameters, specify debug=True, as in the example:
if __name__ == '__main__':
    StreamDeck(
        actions=[
            MyAction(),
        ],
        debug=True,
        log_file=settings.LOG_FILE_PATH,
        log_level=settings.LOG_LEVEL,
        log_backup_count=1,
    ).run()
  1. Follow steps 5-7 from the "Quick Start" section.
  2. Place a breakpoint where you are interested in PyCharm. For example, on the line:
self.open_url("https://github.com/gri-gus/streamdeck-python-sdk")
  1. Run the file com.bestdeveloper.mytestplugin.sdPlugin/code/main.py in Debug mode in PyCharm.

⚠️ If you run only the plugin in the Stream Deck application, there will be no reaction to pressing the button.

  1. Click the button on Stream Deck. The code execution will stop at the line from point 4.

⚠️ Don't forget to set debug=False when building the finished plugin.

Understanding usage

To get started, check out the sample plugins below, then continue on to this section.

Let's look at an example of how the self.send_to_property_inspector method works.

Let's look at the documentation from Elgato:

Here is the object sent from the plugin when calling self.send_to_property_inspector: click

Here is the resulting object in the Property inspector when calling self.send_to_property_inspector: click

Here is the source code for the method self.send_to_property_inspector method:

def send_to_property_inspector(
        self,
        action: str,
        context: str,
        payload: dict
):
    message = events_sent_objs.SendToPropertyInspector(
        action=action,
        context=context,
        payload=payload
    )
    self.send(message)

As we can see, it takes function parameters and passes them to the object events_sent_objs.SendToPropertyInspector:

class SendToPropertyInspector(BaseModel):
    action: str
    context: str
    payload: dict
    event: str = "sendToPropertyInspector"

Next, in the self.send method, the pydantic object is converted to json and sent to the plugin’s Property Inspector.

What is payload?

This is any dict that can be converted to json.

How does Property Inspector get data from payload?

To answer this question, you need to look at the source code streamdeck-javascript-sdk. There is a method in the sdk onSendToPropertyInspector and most likely it should be used like this:

$PI.onSendToPropertyInspector("com.ggusev.keyboard.write", jsn => {
    payload = jsn.payload; // I'm not sure about this, you need to test it
...
});

Instead of "com.ggusev.keyboard.write" you need to substitute the name of your action.

Property Inspector Generator

streamdeck-python-sdk-pi

With this tool you can quickly write your Property Inspector in Python. HTML and JS code will be generated.

In the application template from the Quick Start section there is a file com.bestdeveloper.mytestplugin.sdPlugin/property_inspector/myaction_pi.py, which provides an example of generating a Property Inspector into the file com.bestdeveloper.mytestplugin.sdPlugin/property_inspector/myaction_pi.html.

Here are the elements available for generation in Python:

from pathlib import Path

from streamdeck_sdk_pi import *

OUTPUT_DIR = Path(__file__).parent
TEMPLATE = Path(__file__).parent / "pi_template.html"

class LoremFlickrStatus(BasePIElement):
    def get_html_element(self) -> str:
        res = """
    <div class="sdpi-item" style="max-height: 60px">
        <div class="sdpi-item-label">Website status</div>
        <div class="sdpi-item-value"
             style="background: #3D3D3D; height:26px; max-width: 56px; margin: 0 0 0 5px; padding: 0">
            <img src="https://img.shields.io/website?down_color=%233d3d3d&down_message=offline&label=&style=flat-square&up_color=%233d3d3d&up_message=online&url=https%3A%2F%2Floremflickr.com%2F"
                 alt="" style="height: 26px; max-width: 56px; margin: 0">
        </div>
    </div>
        """
        return res

def main():
    pi = PropertyInspector(
        action_uuid="com.ggusev.example.exampleaction",
        elements=[
            Heading(label="TEXT"),
            Textfield(
                label="Name",
                uid="name",
                required=True,
                pattern=".{2,}",
                placeholder="Input your name",
            ),
            Textfield(
                label="Text with default",
                uid="text_with_default",
                placeholder="Input default",
                default_value="default"
            ),
            Textfield(
                label="IP-Address",
                uid="my_ip_address",
                required=True,
                pattern=r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}",
                placeholder="e.g. 192.168.61.1",
            ),
            Textarea(
                label="Textarea",
                uid="textarea",
                placeholder="Input",
                max_length=100,
                info_text="100 max"
            ),
            Textarea(label="Textarea", uid="textarea_1", placeholder="Input", ),
            Password(
                label="Password",
                uid="password_input",
                required=True,
                pattern=".{2,}",
                placeholder="Input password",
                default_value="kitten",
            ),
            Heading(label="CHECKBOX & RADIO"),
            Radio(
                label="Union type",
                uid="union_type",
                items=[
                    RadioItem(
                        value="or",
                        label="or",
                        checked=False,
                    ),
                    RadioItem(
                        value="and",
                        label="and",
                        checked=True,
                    )
                ]
            ),
            Checkbox(
                label="Grayscale",
                items=[
                    CheckboxItem(
                        uid="grayscale_flag",
                        label="on",
                        checked=False,
                    ),
                ],
            ),
            Heading(label="FILE"),
            File(
                label="Select file",
                uid="my_file",
                accept=[".jpg", ".jpeg", ".png"],
            ),
            Heading(label="DATE & TIME"),
            DateTimeLocal(
                label="Select datetime",
                uid="my_datetime",
                default_value="2024-09-13T19:39",
            ),
            Date(
                label="Select date",
                uid="my_date",
                default_value="2019-01-15",
            ),
            Date(
                label="Select date",
                uid="my_date_1",
            ),
            Month(
                label="Select month",
                uid="my_month",
                default_value="2024-07",
            ),
            Week(
                label="Select week",
                uid="my_week",
                default_value="2024-W38",
            ),
            Time(
                label="Select time",
                uid="my_time",
                default_value="19:39",
            ),
            Heading(label="GROUP"),
            Group(
                label="My group",
                items=[
                    Radio(
                        label="Union type",
                        uid="my_group_union_type",
                        items=[
                            RadioItem(
                                value="or",
                                label="or",
                                checked=True,
                            ),
                            RadioItem(
                                value="and",
                                label="and",
                                checked=False,
                            )
                        ]
                    ),
                    Date(
                        label="Select date",
                        uid="my_group_my_date",
                        default_value="2019-01-15",
                    ),
                ]
            ),
            Heading(label="LINE"),
            Line(),
            Heading(label="COLOR"),
            Color(
                label="Color",
                uid="my_color",
                default_value="#240bda",
            ),
            Heading(label="PROGRESS & METER"),
            Meter(
                label="Meter",
                uid="my_meter_2",
                default_value=8,
                max_value=100,
            ),
            Meter(
                label="Meter",
                uid="my_meter_1",
                default_value=0.5,
                left_label="0",
                right_label="100",
            ),
            Progress(
                label="Progress",
                uid="my_progress_1",
                default_value=0.5,
            ),
            Progress(
                label="Progress",
                uid="my_progress_2",
                left_label="Min",
                right_label="Max",
                default_value=0.5,
            ),
            Heading(label="RANGE"),
            Range(
                label="Range",
                uid="my_range_2",
                min_value=0,
                max_value=50,
                default_value=20,
            ),
            Range(
                label="Range (+ll + rl)",
                uid="my_range_1",
                left_label="0",
                right_label="50",
                min_value=0,
                max_value=50,
                default_value=20,
            ),
            Range(
                label="Range(+stp)",
                uid="my_range_3",
                min_value=0,
                max_value=50,
                default_value=25,
                step=25,
            ),
            Range(
                label="Range(+dl +stp)",
                uid="my_range_4",
                min_value=0,
                max_value=50,
                default_value=25,
                datalist=["", "", "", ],
                step=25,
            ),
            Range(
                label="Range(+dl +lbl)",
                uid="my_range_5",
                min_value=0,
                max_value=100,
                default_value=25,
                datalist=["0", "50", "100", ],
            ),
            Heading(label="DETAILS"),
            Details(
                uid="my_full_width_details",
                full_width=True,
                heading="Full width details",
                text="""
                default open
                """,
                default_open=True,
            ),
            Details(
                uid="my_details_with_label",
                label="Details",
                heading="Info",
                text="My test info\nMy test info"
            ),
            Heading(label="MESSAGE"),
            Message(
                uid="my_message",
                heading="Example message",
                message_type=MessageTypes.INFO,
                text="Example message text",
            ),
            Message(
                uid="last_error",
                heading="Last error",
                message_type=MessageTypes.CAUTION,
                text="",
            ),
            Heading(label="SELECT"),
            Select(
                uid="my_select_1",
                label="Select",
                values=["1", "2", "3", "4"],
                default_value="2",
            ),
            Select(
                uid="my_select_2",
                label="Select 2",
                values=[],
            ),
            Heading(label="CUSTOM"),
            LoremFlickrStatus(),
        ]
    )
    pi.build(output_dir=OUTPUT_DIR, template=TEMPLATE)

if __name__ == '__main__':
    main()

You can write your custom elements, for example as LoremFlickrStatus, and also edit pi_template.html at your discretion.

uid is a future key in the obj.payload.settings dictionary.

The same rules apply to uid as to variable naming in Python.

uid must be unique within a single file for Property Inspector generation.

All elements that contain uid are manipulable.

Example of creating a Property Inspector and managing a field

For example, you made a Property Inspector like this and generated it:

from pathlib import Path

from streamdeck_sdk_pi import *

OUTPUT_DIR = Path(__file__).parent
TEMPLATE = Path(__file__).parent / "pi_template.html"

def main():
    pi = PropertyInspector(
        action_uuid="com.bestdeveloper.mytestplugin.myaction",
        elements=[
            Textfield(
                uid="my_input",
                label="My input",
                placeholder="Input text",
            ),
        ]
    )
    pi.build(output_dir=OUTPUT_DIR, template=TEMPLATE)

if __name__ == '__main__':
    # Run to generate Property Inspector
    main()

Then in the plugin's Action you can use code something like this to get the value from the My input field and then change the value if needed:

 def on_key_down(self, obj: events_received_objs.KeyDown):
    my_input_value = obj.payload.settings["my_input"]
    print(my_input_value)

    stngs = obj.payload.settings.copy()
    stngs["my_input"] = "12345"
    self.set_settings(
        context=obj.context,
        payload=stngs,
    )

After executing self.set_settings the value of the My input field will change to 12345.

Examples

LoremFlickr - Plugin for installing images on a button from the LoremFlickr site. Supports MacOS and Windows.

Proxy Manager - Plugin for enabling and disabling proxies on MacOS. Periodically polls the proxy status through a separate thread.

One-time password - Plugin for generating one-time passwords, like in Google Authenticator. Has several Actions. Supports MacOS and Windows.