Skip to main content

Unofficial Python toolkit for configuring and automating Elgato Stream Deck devices (and virtual decks in a browser)

Project description

MyDeck

English | 日本語

MyDeck allows you to configure a real STREAM DECK. It also allows you to view and control the status of the actual STREAM DECK in your browser. Even if you don't have a real STREAM DECK device, you can use it as a virtual external keyboard in the browser.

MyDeck Demo

  • To configure STREAM DECK easily
  • To use virtual devices compatible with STREAM DECK
  • Web Interface for virtual devices and real device

Check the instruction at first when you haven't setup STREAM DECK.

This package is intended primarily for Linux environments. However, with the exception of one module (app_window_check_linux), it does not depend on Linux at all. It has been tested on Ubuntu 22.04, 24.04, and 26.04 (pre-release), and should work on other Linux distributions as well.

Dependency

System packages:

  • xdotool for active window checking (app_window_check_linux)
  • ImageMagick libraries for python3-wand
  • cairo / libzbar0 (QR scanning for TOTP registration)
  • GNOME Keyring (or any secretstorage-compatible backend) for TOTP secret storage

Ubuntu:

apt install xdotool libmagickwand-dev libcairo2-dev libzbar0 gnome-keyring

Python dependencies (installed automatically via pip install .):

  • streamdeck, Pillow, wand, cairosvg
  • pyyaml, requests, qrcode, netifaces
  • python-daemon, pidfile, psutil
  • pyotp, pyzbar, keyring (TOTP 2FA app)

CAUTION

This is still alpha-quality software.

  • The API around MyDeck / MyDecksManager may change
  • Test coverage is growing but far from comprehensive — manual smoke-test any workflow that matters to you

How to use?

If you don't have STREAM DECK device, no worry.

  1. clone the code
  2. do pip install . and mydeck command is installed
  3. do mydeck
  4. open http://127.0.0.1:3000 to configure deck

Install with uv (alternative)

Instead of pip install ., you can use uv tool install for an isolated, globally-available install:

uv tool install .

mydeck will be available at ~/.local/bin/mydeck, backed by a dedicated virtualenv at ~/.local/share/uv/tools/mydeck/.

To add a plugin to that isolated environment later, target its Python directly:

uv pip install --python ~/.local/share/uv/tools/mydeck/bin/python3 <plugin-package-or-path>

Or reinstall the tool with the plugin included from the start:

uv tool install --reinstall --with <plugin-package> .

After installing a new plugin, restart mydeck (mydeck --restart -d) so the new package is picked up — Python scans site-packages only at interpreter startup.

mydeck CLI options

Option Default Description
--port 3000 Web server port
--config-path ~/.config/mydeck Directory holding per-device YAML configs
--log-level INFO One of DEBUG / INFO / WARNING / ERROR / CRITICAL
--vdeck off Start with virtual decks even when no physical device is connected
--no-qr off Skip printing the QR code for the Web UI URL at startup
-d off Run as a daemon (PID file written under --config-path)
--stop Stop a running daemon
--restart Stop the running daemon and start a fresh one

Device resilience

If a STREAM DECK is unplugged (or the machine suspends/resumes), mydeck stays alive. A background supervisor re-enumerates every 3 seconds and reattaches the device as soon as it comes back, restoring the page and apps that were active before the disconnect. Devices whose serial is already known can also be started without being plugged in — they will attach automatically when connected.

TOTP 2FA app (AppTotp)

Register TOTP accounts from the Web UI (manual secret, otpauth:// URI, or QR image upload / camera scan), then display the current 6-digit code and a countdown ring across keys on the STREAM DECK. Secrets are stored in GNOME Keyring; account metadata under ~/.config/mydeck/totp_accounts.json. A dedicated page @TOTP_ACCOUNTS is auto-created; link to it with change_page: '@TOTP_ACCOUNTS' on any key.

Third-party apps (plugins)

app: and game: values in YAML accept either a short built-in name (Clockmydeck.app_clock.AppClock) or a fully-qualified dotted path for out-of-tree packages:

apps:
  - app: my_plugin.apps.Weather      # resolves to my_plugin.apps.Weather
    option:
      page_key:
        '@HOME': 3
games:
  - game: my_plugin.games.MyGame     # resolves to my_plugin.games.MyGame

To ship a plugin as its own package, install it into the same Python environment as mydeck (e.g. pip install my_plugin) and reference it by its import path. Your plugin class should subclass one of the base classes in mydeck.my_decks_app_base (ThreadAppBase, BackgroundAppBase, HookAppBase, TouchAppBase, GameAppBase) — see docs/make_your_app.md for the contract and lifecycle.

A working example of an external plugin (live CPU usage pie chart) is at ktat/mydeck-hello-plugin.

How to run example without install mydeck command

If you have STREAM DECK device

PYTHONPATH=src python3 example/main.py

If you don't have STREAM DECK device

PYTHONPATH=src python3 example/main_virtual.py

and open the following URL with browser.

http://localhost:3000/

If you want to change default port number 3000, pass port number as 1st argument.

PYTHONPATH=src python3 example/main_virtual.py 3001

Configuration

Virutal Deck Configuration

mydeck command configure it at first time when you use virtual deck. So you don't need to edit this manually.

1: # ID
  key_count: 4
  columns: 2
  serial_number: 'dummy1'
2: # ID
  key_count: 6
  columns: 3
  serial_number: 'dummy2'

Page Configuration Rule

You can edit config file with web interface.

page_config:
  PAGE_LABEL:
    keys:
      KEY_NUMBER:
        "command": ["command", "options"]
        "chrome": ["profile name", "url"]
        "image_url": "https://example.com/path/to/image"
        "change_page: "ANOTHER_PAGE_NAME"
        "image": "image to display"
        "label": "label to display"
        "background_color": "white"
        "exit": 1
  • PAGE_LABEL ... Name of the page or name of active window
  • KEY_NUMBER ... It should be number, start from 0
  • command ... OS command
  • chrome ... launch chrome with profile. if image & image_url is not set, check url root path + /faviocn.ico and use it as image if it exists.
  • image_url ... use url instead of image file path
  • change_page ... change page when the button is pushed
  • image ... an image shown on button
  • label ... a label shown on button below the image
  • background_color ... background color of the key
  • exit ... can set 1 only. Exit app when the button is pushed

command and change_page can be used in same time. In the case, command is executed and then page is changed.

Live reload of the YAML config

Config files are re-read automatically when their mtime changes, but only at the point key_touchscreen_setup() runs — which is on page change. There's no inotify-style filesystem watcher.

  • Edits to an already-installed app (added/removed/repositioned keys, option changes): take effect the next time you switch pages on that deck.
  • Installing a new Python package (e.g., a plugin under a new dotted path) requires restarting mydeck (mydeck --restart -d). Python only scans site-packages / .pth files at interpreter startup, so a running process can't discover a freshly-pip installed plugin until it restarts.
  • Editing code inside an already-loaded app (e.g., tweaking a built-in app_*.py file) likewise needs a restart — modules are cached in importlib.

PAGE_LABEL

  • @HOME is special label. This configuration is used for first page.
  • @GAME is reserved label for the page to collect games.
  • @previous is also special label. It can be used for the value of change_page. When the button is pushed, go back to the previous page whose name isn't started with ~.

If you set window title as PAGE_LABEL, page is changed according to active window.

example

configuration
---
"apps":
  - app: Clock
    option:
      page_key:
        '@HOME': 5
        '@JOB': 12
  - app: StopWatch
    option:
      page_key:
        '@HOME': 6
  - app: Calendar
    option:
      page_key:
        '@home': 7
"alert":
   retry_interval: 60
   check_interval: 180
   key_config:
      7:
        command: ["google-chrome", '--profile-directory=Profile 1', 'https://example.com/nagios/cgi-bin/status.cgi?host=all&servicestatustypes=16&hoststatustypes=15']
        image: "./src/Assets/nagios.ico"
        change_page: '@previous'
"games":
  - game: RandomNumber
  - game: Memory
  - game: TicTackToe
  - game: WhacAMole
"page_config":
  "@HOME":
    keys:
      0:
        "change_page": "@PRIVATE"
        "label": "Private"
        "image": "./src/Assets/ktat.png"
      1:
        "change_page": "@JOB"
        "label": "Job"
        "image": "./src/Assets/job.png"
      2:
        "change_page": "@GAME"
        "label": "Game"
        "image": "./src/Assets/game.png"
      10:
        "label": "Config"
        "image": "./src/Assets/settings.png"
        "change_page": "@CONFIG"
      14:
        "exit": 1
        "image": "./src/Assets/exit.png"
        "label": "Exit"
  "@PRIVATE":
    keys:
      0:
        "command": ["google-chrome", "--profile-directory=Default"]
        "image": "/usr/share/icons/hicolor/256x256/apps/google-chrome.png"
        "label": "Chrome(PRIVATE)"
      10:
        "label": "Config"
        "image": "./src/Assets/settings.png"
        "change_page": "@CONFIG"
      14:
        "change_page": "@HOME"
        "image": "./src/Assets/home.png"
  "@JOB":
    keys:
      0:
        "command": ["google-chrome", '--profile-directory=Profile 1']
        "image": "/usr/share/icons/hicolor/256x256/apps/google-chrome.png"
        "label": "Chrome(JOB)"
  "@CONFIG":
    keys:
      0:
        "label": "Audio"
        "command": ["pavucontrol", "--tab=4"]
        "image": "./src/Assets/audio.png"
      1:
        "label": "Sound"
        "command": ["gnome-control-center", "sound"]
        "image": "./src/Assets/sound.png"
      2:
        "label": "Display"
        "command": ["gnome-control-center", "display"]
        "image": "./src/Assets/display.png"
      14:
        "change_page": "@previous"
        "image": "./src/Assets/back.png"
        "label": "Back"
  "Meet - Google Chrome":
    keys:
      0:
        "command": ["echo", "meet"]
        "image": "./src/Assets/meet.png"
        "label": "Google Meet"
      1:
        "command": ["xdotool", "key", "ctrl+d"]
        "image": "./src/Assets/mute.png"
        "label": "mute"
      2:
        "command": ["xdotool", "key", "ctrl+e"]
        "image": "./src/Assets/video.png"
        "label": "camera"
      10:
        "label": "Audio"
        "command": ["pavucontrol", "--tab=4"]
        "image": "./src/Assets/audio.png"
      11:
        "label": "Sound"
        "command": ["gnome-control-center", "sound"]
        "image": "./src/Assets/sound.png"
      14:
        "change_page": "@JOB"
        "label": "Back"
        "image": "./src/Assets/back.png"
  "Zoom Meeting":
    kyes:
      0:
        "command": ["echo", "zoom"]
        "image": "./src/Assets/zoom.png"
        "label": "Zoom"
      1:
        "command": ["xdotool", "key", "alt+a"]
        "image": "./src/Assets/mute.png"
        "label": "mute"
      2:
        "command": ["xdotool", "key", "alt+v"]
        "image": "./src/Assets/video.png"
        "label": "camera"
      10:
        "label": "Audio"
        "command": ["pavucontrol", "--tab=4"]
        "image": "./src/Assets/audio.png"
      11:
        "label": "Sound"
        "command": ["gnome-control-center", "sound"]
        "image": "./src/Assets/sound.png"
      14:
        "change_page": "@JOB"
        "label": "Back"
        "image": "./src/Assets/back.png"

Custom script (when mydeck command is not enough)

The mydeck command covers most use cases, but if you need customizations not supported by the command — such as alert_func for custom alert logic — you can write your own script using the MyDecks class directly.

from mydeck import MyDecks

import requests

CHECK_URL = 'https://example.com/'

def check_alert():
    res = requests.get(CHECK_URL)
    if res.status_code != requests.codes.ok:
        return True
    return False

if __name__ == "__main__":
    mydecks = MyDecks(
        {
            'server_port': 3001, # default is 3000
            'config': {
               'file': "./example/config/config.yml",
               'alert_func': check_alert,
            },
        }
    )

    mydecks.start_decks()

If you have multiple devices:

if __name__ == "__main__":
    mydecks = MyDecks({
        'server_port': 3001, # default is 3000
        'decks': {
            'SERIAL_KEY_1': 'name1',
            'SERIAL_KEY_2': 'name2',
        },
        'configs': {
            'name1': {
                'file': "/path/to/config1.yml",
                'alert_func': check_alert,
            },
            'name2': {
                'file': "/path/to/config2.yml",
            },
        }
    })

    mydecks.start_decks()

If you don't have real devices.

if __name__ == "__main__":
    mydecks = MyStreamDecks({
        'vdeck_config': "example/config/vdeck.yml",
        'decks': {
            'dummy1': '4key-dummy',
            'dummy2': '6key-dummy',
            'dummy3': '15key-dummy',
        },
        'configs': {
            '6key': {
                'file': "example/config/config2.yml",
            },
            '4key-dummy': {
                'file': "example/config/config-d1.yml",
            },
            '6key-dummy': {
                'file': "example/config/config-d2.yml",
            },
            '15key-dummy': {
                'file': "example/config/config.yml",
                'alert_func': check_alert,
            },
        }

    })

    mydecks.start_decks(True)

LICENSE

MIT: https://ktat.mit-license.org/2016

See also LICENSE shipped with the package.

Releasing

Maintainers: see docs/releasing.md for the PyPI release workflow.

Trademarks

Elgato and Stream Deck are trademarks of Corsair Memory, Inc. This project is an independent, unofficial, community-maintained toolkit and is not affiliated with, endorsed by, or sponsored by Elgato or Corsair.

SEE ALSO

python-elgato-streamdeck repository

https://github.com/abcminiuser/python-elgato-streamdeck

icons

Some of icon are from the fowlloing: https://remixicon.com/

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mydeck-0.1.1.tar.gz (1.1 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mydeck-0.1.1-py3-none-any.whl (1.1 MB view details)

Uploaded Python 3

File details

Details for the file mydeck-0.1.1.tar.gz.

File metadata

  • Download URL: mydeck-0.1.1.tar.gz
  • Upload date:
  • Size: 1.1 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for mydeck-0.1.1.tar.gz
Algorithm Hash digest
SHA256 79a7b6a2f503b57467e327e7f3185cf04c9c17ed998483686fc89b66790586b5
MD5 01f2fbe20d515cf35fe2a6a0f77e0edc
BLAKE2b-256 45703638f6bcd3189603a6252b69a37188609a53f76d53bdaa194b813779ab41

See more details on using hashes here.

File details

Details for the file mydeck-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: mydeck-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 1.1 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for mydeck-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 01b52aa8d79d662adbce9fb92308add9394a1056d92dfe602d325f05b4fcc4f7
MD5 3a8941cd82f9c244565f31b8c35bf65c
BLAKE2b-256 facb7eefd452aff8452d5db5e8080fc91cd65646d0d25831d7366ca24fdc7800

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page