Repository navigation
[Feature Request] Make button states externally controllable AND remember button states off-screen #381
Description
Description
Similar to the feature of changing a deck's active page through the command line (see command line option --change-page DECK_SERIAL PAGE_NAME), it would be great to have the ability to (both poll and) set a button's active state externally!
This is in relation to the request made on the Deck plugin:
StreamController/DeckPlugin#5
as well as solve the shortcoming mentioned in the discussions:
#319
I am combining both into a single request because I believe implementing the states mentioned in the discussion is a prerequisite to cleanly implement the command line option requested on the mentioned deck plugin issue.
Rationale
This would make the stream deck usable as an interactive control console for generic, volatile "THINGS" (tm), and synchronize the button states to the actual state of the external THINGS, even as that state changes outside of StreamController.
Usecase Example
Have a single button on the Stream Deck that controls a ceiling fan through some a custom API. The Button shall always accurately display the fan's state by background color, and turn it on/off.
However, there is also a (smart) light switch on the wall, that can be used to control the same fan at the same time! The background color on the Stream Deck button shall change accordingly, even when the fan is turned on/off through the light switch on the wall.
This is typically implemented by having one external master that actually operates the fan, and multiple remote controllers (stream decks, light switches, etc.) call an API on the external master to trigger an action. After the master does what needs doing, it actively notifies all connected controllers of the new state. This way, all control points can be used at will to operate the machinery, and all control points always display the machine's current state correctly.
It has the beautiful side effect that the state displayed only changes if the action was actually successful, because the state change is triggered by a callback on success.
Help wanted!
I will update this post to document what needs to be done and how it should be done, as well as what has been done, as my understanding of the matter grows and things progress.
If when reading this page you feel that you can contribute, you are welcome to do so! Please get in touch, so we know that things are being worked on, and who is currently doing what!
###WORKING DOCUMENTATION BELOW THIS LINE!
I am actively working on creating a diff/pull request for this, but as both time and my understanding of the software are limited, it might take a while. During this time, I will use the remaining part of this post as a public documentation of my progress and learning! If you spot errors in this documentation, please speak up!
Definitions (GLOSSARY)
In order to efficiently and effectively talk about the changes to be done, one has to establish a common understanding of what things are and how things work. For the purpose of this thread, we shall hereafter use the terms below according to the definitions given:
DEVICE
A DEVICE is a physical unit attached to the computer, which has (possibly among other things) a defined number of physical SWITCHES. The switches are arranged in a form of rows and columns on that device, so any one switch can be clearly identified by it's row and column LOCATION.
A DEVICE is uniquely identified by its SERIAL NUMBER.
SWITCH
A SWITCH is a single actor unit on a DEVICE. For the purposes of this thread, it consists of an area which can be depressed and which will automatically return to its idle position when no longer depressed, as well as an underlying display.
A switch's current POSITION can be determined by software (polled). By comparing the current POSITION with the last known previous POSITION, different transitional edges can be detected and used to trigger ACTIONS in software. These ACTIONS are defined through the respective BUTTON configuration.
At any point in time, any one SWITCH can use none or exactly one BUTTON configuration.
PAGEs can be used to switch the SWITCHES on a DEVICE between different BUTTON configurations at different times, if desired.
A SWITCH is uniquely identified by the DEVICE it is on, plus it's ROW and COLUMN location on that DEVICE.
POSITION
The POSITION describes whether a SWITCH is depressed or idle.
This is here for the sake of completeness only. There is no intention to touch or modify anyhting related to POSITIONs in this request.
BUTTON
A BUTTON is the logical configuration unit to represent a SWITCH in software. A BUTTON belongs to exactly one PAGE, where it lives on exactly one LOCATION.
BUTTONs can be unconfigured, or can have one or multiple configured STATES, exactly one of which is the ACTIVE STATE.
A BUTTON is uniquely identified by the PAGE it is on, plus the ROW and COLUMN LOCATION on that PAGE.
ORIENTATION
This is a way to easily compensate for mounting a DEVICE upside down or left-side-up or right-side-up.
The ORIENTATION influences how a PAGE's BUTTONs are mapped to a DEVICE's SWITCHes, according to their ROW and COLUMN LOCATION.
ROW LOCATION
This term is ambiguous.
In the context of a DEVICE, it describes the vertical location of a SWITCH. DEVICE ROWs are identified by consecutive integers from the top of the device (the edge where the "STREAM DECK" logo is) to the bottom, starting at 1.
In the context of a PAGE, it describes the vertical location of a BUTTON. PAGE ROWs are identified by consecutive integers from the top of the page to the bottom, starting at 1.
COLUMN LOCATION
This term is ambiguous.
In the context of a DEVICE, it describes the horizontal location of a SWITCH. DEVICE COLUMNs are identified by consecutive integers from the left edge of the device (the short edge closest to the "S" of the "STREAM DECK" logo) to the right edge of the device (the short edge closest to the "K" of the "STREAM DECK" logo), starting at 1.
In the context of a PAGE, it describes the horizontal location of a BUTTON. PAGE COLUMNs are identified by consecutive integers from the left of the page to the right of the page, starting at 1.
STATE
A STATE is the logical configuration unit to group together the properties a BUTTON shall have at a point in time.
This STATE typically determines both the BUTTON's appearance, as well as the BUTTON's behavior through the definition of ACTIONS.
Any one BUTTON can have multiple STATES configured, but only one of those STATEs can be active at any point in time. The other configured states are deemed PASSIVE STATEs.
A STATE is assigned to exactly one BUTTON. Identical STATEs can be configured on multiple BUTTONS if desired, but those will be treated as independent copies.
A STATE is uniquely identified by its ID within the BUTTON it belongs to, which is an integer enumerator automatically assigned during creation of that STATE.
ACTIVE STATE
This is the STATE that a BUTTON is currently in. The ACTIVE STATE determines a BUTTON's current appearance and behavior.
Any configured BUTTON shall have exactly one ACTIVE STATE at any one point in time. BUTTONs can STATE-TRANSITION from one STATE to another. If and when that happens, the new STATE becomes the ACTIVE STATE. At the same time, the previous ACTIVE STATE now becomes a PASSIVE STATE.
PASSIVE STATE
This is a STATE that is configured and available for a BUTTON, but that the BUTTON is not currently in. Therefore, a PASSIVE STATE neither determines a BUTTON's current appearance, nor current behavior.
STATE-TRANSITION
This is when a BUTTON's ACTIVE STATE changes from one STATE to another. STATE-TRANSITIONS can currently (as of 2025-17-04) only be triggered by internal ACTIONs.
The purpose of this thread is to create the possibility to externally trigger STATE-TRANSITIONs by calling StreamController through the command line (or possibly the DBUS interface directly).
Note that in the current interface, it is not possible to trigger a STATE-TRANSITION from an ACTIVE STATE to that very same STATE. This is currently ensured by making the target STATE for the STATE-TRANSITION ACTION a drop-down field in the GUI, and dynamically populating that drop-down field so the STATE the ACTION is defined on is missing from the list.
When a command line parameter for this is implemented, it is theoretically possible to reach this specific STATE-TRANSITION. We must decide how this should be handled.
PAGE
A PAGE is a configuration unit representing a collection of BUTTONs, to be displayed at the same time on a DEVICE.
Through the use of PAGEs, BUTTONs can be assigned to a DEVICE's SWITCHes.
Any DEVICE can display exactly one PAGE at any given time. That PAGE shall be considered the ACTIVE PAGE for that device.
Note that when multiple DEVICEs are connected, the same PAGE can be displayed on none, any one, multiple or all DEVICEs at the same time. Unlike STATEs, a PAGE exists exactly once, regardless of how many DEVICEs are currently showing it (even if none are currently showing it). That means all BUTTONs on that PAGE, as well as their respective ACTIVE STATE, shall always internally synchronize across all connected devices!
Also note that currently PAGEs are typically created through the configuration dialog for a specific DEVICE, but can be used on any other DEVICE, too. This means that the number and layout of BUTTONs on a PAGE may or may not match the physical number and layout of SWITCHes of the DEVICE the PAGE is loaded onto. This is currently handled by ignoring excess BUTTONs, and leaving excess SWITCHES unconfigured.
A PAGE is identified by its unique name, assigned through the configuration dialog while creating the PAGE.
ACTIVE PAGE
This is the PAGE that is currently loaded and displayed on a DEVICE.
Note that this is a property of the individual DEVICE! Each DEVICE has exactly one ACTIVE PAGE at any point in time.
PASSIVE PAGE
This is a PAGE which is allowed for display on a particular DEVICE through the PAGE's configuration, but that is not currently the ACTIVE PAGE on that device.
Note that this is a property of the individual DEVICE. One could in theory make the distinction between a PAGE that is currently not displayed on a specific DEVICE, and a PAGE that is currently not displayed on any DEVICEs.
PAGE-TRANSITION
This describes the act of loading a PAGE onto a DEVICE for display, so that PAGE becomes the ACTIVE PAGE.
The previous ACTIVE PAGE will as a result now become a PASSIVE PAGE and no longer be displayed on the DEVICE.
Note that it is possible to to PAGE-TRANSITION from an ACTIVE PAGE to the very same PAGE. In this case, the DEVICE will continue to display the same PAGE. However, BUTTON STATEs, counters, timeouts etc., may or may not be reset to their initial value when this happens (depending on configuration).
Requirements:
-
For this to be feasible, the current default behavior of streamdeck buttons needs to change.
Currently, all buttons are always reset to state 1, when ever a page is loaded.
For this enhancement, we must track and remember the current state of every button on every page, even when the button's page is not currently loaded! When ever a page is loaded to a device for display, all buttons on that page start out in the last known state, rather than the default state.
This is especially relevant for controlling the state externally, since we have no way to make sure our target button is on the currently active page (one could switch to that page first and then change the state, but since those actions are not atomic, it might create race conditions. Also, it might not be desireable to do so!)
This behavior shall be configurable per-button, since it might not be right for everyone and everything. -
An interface must be created to switch the state from an external entity. Similar to the
--change-page DECK_SERIAL PAGE_NAMEcommand line parameter above, we need a parameter--change-state PAGE_NAME ROWxCOLUMN STATE_ID. Note here that a button is always a property of a page! The same button will have different meaning on a different page, so we must provide the target page within the command.
Architecture considerations, implications and decisions
Should externally setting the active button state make the page the button is on the active page on a selected device?
- I don't think that would be desireable, because then a background change would always interrupt the user's current (possibly unrelated) workflow.
- If the external event is indeed intended to bring up the corresponding page, there are already means available to achieve that!
- Not automatically switching a device to the page would mean the DEVICE SERIAL is not required in the command to switch active state, because the Button the State belongs to is a property of the page, not the device.
Do we need the DEVICE_SERIAL part in the command line parameter?
- This should not be neccessary, since the active state is a property of an individual button, which lives on a page. A page however is device-independent. So in order to uniquely identify the state to make the new active state, it should suffice to provide PAGE_NAME, ROW_POSITION, COLUMN_POSITION, STATE_ID.
Should we allow using different command line parameters simultaneously? Like switching the page and switching a button state in one command?
- This would be nice to have, as it would allow for somewhat "atomic" handling of alert notifications where you want to bring an external state change to the user's attention.
- it significantly increases the complexity of the implementation. Also we would have to define an order of execution, which is not easy to decide globally.
- a workaround for deciding the order would be to execute in the order the parameters were given on the command line, but we currently have no way to evaluate that (the current implementation checks for existence, not sorting)
- if desired, this should be implemented in a way that allows future expansion, should more parameters be added
- requiring the user to do this in two separate commands would be less elegant and likely a little slower, but totally feasible.
Current thought: first we make it work by saying "use one or the other, but not both", then we make it pretty in a second step!
Known limitations
- Switching states through the command line will be relatively slow, compared to using internal triggers, so it is only feasible at a relatively low frequency.
- The numeric ID of states is not globally stable at all time! It is stable during regular operation, but deleting a state with lower numeric ID will make all "following" states shift positions. This means external scripts would need to be adapted after every time a state is deleted in StreamController.
ToDo:
- implement the persistent state machine for all buttons on all pages. When switching to the button's page, load all button's stages from that state machine, instead of simply putting all in the default state.
- Implement GUI option to select whether a button should be in the default state or the last known state when the page loads. While we're at it, it's probably not significantly more work to make the "default state" configurable (instead of it always being 1), but beware of implications of deleting a state which used to be the default!
- implement command line option and handling thereof.
- (optional): implement a way to poll the current state. This is again problematic as it might create race conditions, but for slow-moving things it might be a nice addition.
I have started to look into the code to do this (mainly because I need the functionality myself), but since I'm relatively new to Python and completely new to this project, things might move along rather slowly. Any help is appreciated!
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
G4PLS commentedon Apr 18, 2025
As this is the main SC Repo three things should get added:
Saving the last state the Button was in, the Default State of a Button and if the Button should persist the last state.
SC Itself should not add the ability to edit things externally over http requests.
Idk if the CLI command is already added but calling that should be the job for a Plugin so that It can happen automatically.
We would need two new UI Elements that lets the user decide default state and state persistence.
The default state selection could happen through a context menu, meaning Right Click -> Set as Default State.
Jost-Philip commentedon Apr 19, 2025
I fully agree. If you want a REST server, install a REST server! Plenty of good ones out there to chose from.
But we should have some as-generic-as-possible way to send an external signal to StreamController to change active states, that this (internal or external) helper can facilitate!
For changing the active page on devices, this is currently already implemented through calling the executable again with parameters, which then sends a signal to the running instance through DBUS.
Since both the executable and the DBUS interface are already part of the main application, this would be the place to add additional functionality without needing to re-invent the wheel.
It is not. Getting there is what I'm trying to achieve here.
Possible, but that would mean having to select the "remember last state" mode implicitly through not setting any of the button's states as default, or having a separate UI element for it. While this works technically, it might be difficult to understand for users.
I would therefore favor a drop-down menu in the upper left area, right where one can switch between states, titled something like
default state, that has the ID of all configured states as selectable options (this already exists elsewhere, so the code is there), plusremember previousas the final option. I believe this could be more intuitive to use?Using a drop-down menu would also have the benefit that only one UI element is needed to cover both functionalities.
Persisting active states in the background is meaningless without also restoring them on page load, and persistence is not required when a default state is activated on load anyway, so no need to present these choices to the user separately.(See Tech-Note below)
I would place it near the state selection area, because this UI element must be button-centric, and the rest of the panel below is already state-centric.
Tech-Note: technically under the hood, I would still implement it so that the previous state is always stored and remembered, but configuring a default active state overrides that stored value on page load. We need the data structures for this to be in place anyway, in order to be able to offer the choice to the user. Creating the data structures is what's expensive. Filling and reading them is rather cheap (write one additional integer per state-transition per button). On page load, one would have to modify one function to fetch either the stored or the default page depending on setting, which is also not too expensive (this function currently simply always loads the state with ID=1).
This way, all buttons could still be implemented and represented in memory identically, regardless of whether this option was set or not.
Also, changing the active state externally could then simply modify that stored integer and also there one would not have to differentiate between configurations. The only thing left to do would be to figure out whether the affected page is currently active, in which case one would additionally trigger a state-transition, otherwise simply update the integer and be done with it.
Core447 commentedon Aug 11, 2026
Added with #460 and 2d0dd13