added button state management
This commit is contained in:
@@ -16,3 +16,5 @@ For this to work, the following software is required:
|
|||||||
|
|
||||||
- LibUSB HIDAPI [Installation instructions](https://python-elgato-streamdeck.readthedocs.io/en/stable/pages/backend_libusb_hidapi.html)
|
- LibUSB HIDAPI [Installation instructions](https://python-elgato-streamdeck.readthedocs.io/en/stable/pages/backend_libusb_hidapi.html)
|
||||||
- cairo [Installation instructions for Windows](https://stackoverflow.com/a/73913080)
|
- cairo [Installation instructions for Windows](https://stackoverflow.com/a/73913080)
|
||||||
|
|
||||||
|
The event `doubleTap` is not working with this server software.
|
||||||
|
|||||||
@@ -6,4 +6,7 @@ PLUGIN_PORT = 6153
|
|||||||
PLUGIN_INFO = "/sd/info"
|
PLUGIN_INFO = "/sd/info"
|
||||||
PLUGIN_ICON = "/sd/icon"
|
PLUGIN_ICON = "/sd/icon"
|
||||||
|
|
||||||
|
DB_FILE = "streamdeckapi.db"
|
||||||
SD_SSDP = "urn:home-assistant.io:device:stream-deck"
|
SD_SSDP = "urn:home-assistant.io:device:stream-deck"
|
||||||
|
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f"
|
||||||
|
LONG_PRESS_SECONDS = 2
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import asyncio
|
|||||||
import platform
|
import platform
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import base64
|
import base64
|
||||||
|
from datetime import datetime
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import human_readable_ids as hri
|
import human_readable_ids as hri
|
||||||
from jsonpickle import encode
|
from jsonpickle import encode
|
||||||
@@ -17,11 +18,17 @@ import cairosvg
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
from ssdpy import SSDPServer
|
from ssdpy import SSDPServer
|
||||||
|
|
||||||
from streamdeckapi.const import PLUGIN_ICON, PLUGIN_INFO, PLUGIN_PORT, SD_SSDP
|
from streamdeckapi.const import (
|
||||||
|
DATETIME_FORMAT,
|
||||||
|
DB_FILE,
|
||||||
|
LONG_PRESS_SECONDS,
|
||||||
|
PLUGIN_ICON,
|
||||||
|
PLUGIN_INFO,
|
||||||
|
PLUGIN_PORT,
|
||||||
|
SD_SSDP
|
||||||
|
)
|
||||||
from streamdeckapi.types import SDApplication, SDButton, SDButtonPosition, SDDevice
|
from streamdeckapi.types import SDApplication, SDButton, SDButtonPosition, SDDevice
|
||||||
|
|
||||||
# TODO: Websocket broadcast not working yet
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_ICON = re.sub(
|
DEFAULT_ICON = re.sub(
|
||||||
"\r\n|\n|\r",
|
"\r\n|\n|\r",
|
||||||
@@ -53,8 +60,8 @@ devices: list[SDDevice] = []
|
|||||||
# Database
|
# Database
|
||||||
#
|
#
|
||||||
|
|
||||||
database = sqlite3.connect("streamdeckapi.db")
|
database_first = sqlite3.connect(DB_FILE)
|
||||||
table_cursor = database.cursor()
|
table_cursor = database_first.cursor()
|
||||||
table_cursor.execute("""
|
table_cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS buttons(
|
CREATE TABLE IF NOT EXISTS buttons(
|
||||||
key integer PRIMARY KEY,
|
key integer PRIMARY KEY,
|
||||||
@@ -64,11 +71,19 @@ table_cursor.execute("""
|
|||||||
y integer,
|
y integer,
|
||||||
svg text
|
svg text
|
||||||
)""")
|
)""")
|
||||||
|
table_cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS button_states(
|
||||||
|
key integer PRIMARY KEY,
|
||||||
|
state integer,
|
||||||
|
state_update text
|
||||||
|
)""")
|
||||||
table_cursor.close()
|
table_cursor.close()
|
||||||
|
database_first.close()
|
||||||
|
|
||||||
|
|
||||||
def save_button(key: int, button: SDButton):
|
def save_button(key: int, button: SDButton):
|
||||||
"""Save button to database."""
|
"""Save button to database."""
|
||||||
|
database = sqlite3.connect(DB_FILE)
|
||||||
cursor = database.cursor()
|
cursor = database.cursor()
|
||||||
svg_bytes = button.svg.encode()
|
svg_bytes = button.svg.encode()
|
||||||
base64_bytes = base64.b64encode(svg_bytes)
|
base64_bytes = base64.b64encode(svg_bytes)
|
||||||
@@ -88,10 +103,12 @@ def save_button(key: int, button: SDButton):
|
|||||||
database.commit()
|
database.commit()
|
||||||
print(f"Saved button {button.uuid} with key {key} to database")
|
print(f"Saved button {button.uuid} with key {key} to database")
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
database.close()
|
||||||
|
|
||||||
|
|
||||||
def get_button(key: int) -> SDButton | None:
|
def get_button(key: int) -> SDButton | None:
|
||||||
"""Get a button from the database."""
|
"""Get a button from the database."""
|
||||||
|
database = sqlite3.connect(DB_FILE)
|
||||||
cursor = database.cursor()
|
cursor = database.cursor()
|
||||||
result = cursor.execute(
|
result = cursor.execute(
|
||||||
f"SELECT key,uuid,device,x,y,svg FROM buttons WHERE key={key}")
|
f"SELECT key,uuid,device,x,y,svg FROM buttons WHERE key={key}")
|
||||||
@@ -109,11 +126,13 @@ def get_button(key: int) -> SDButton | None:
|
|||||||
"svg": svg_string,
|
"svg": svg_string,
|
||||||
})
|
})
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
database.close()
|
||||||
return button
|
return button
|
||||||
|
|
||||||
|
|
||||||
def get_button_by_uuid(uuid: str) -> SDButton | None:
|
def get_button_by_uuid(uuid: str) -> SDButton | None:
|
||||||
"""Get a button from the database."""
|
"""Get a button from the database."""
|
||||||
|
database = sqlite3.connect(DB_FILE)
|
||||||
cursor = database.cursor()
|
cursor = database.cursor()
|
||||||
result = cursor.execute(
|
result = cursor.execute(
|
||||||
f"SELECT key,uuid,device,x,y,svg FROM buttons WHERE uuid=\"{uuid}\"")
|
f"SELECT key,uuid,device,x,y,svg FROM buttons WHERE uuid=\"{uuid}\"")
|
||||||
@@ -131,11 +150,13 @@ def get_button_by_uuid(uuid: str) -> SDButton | None:
|
|||||||
"svg": svg_string,
|
"svg": svg_string,
|
||||||
})
|
})
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
database.close()
|
||||||
return button
|
return button
|
||||||
|
|
||||||
|
|
||||||
def get_button_key(uuid: str) -> int:
|
def get_button_key(uuid: str) -> int:
|
||||||
"""Get a button key from the database."""
|
"""Get a button key from the database."""
|
||||||
|
database = sqlite3.connect(DB_FILE)
|
||||||
cursor = database.cursor()
|
cursor = database.cursor()
|
||||||
result = cursor.execute(f"SELECT key FROM buttons WHERE uuid=\"{uuid}\"")
|
result = cursor.execute(f"SELECT key FROM buttons WHERE uuid=\"{uuid}\"")
|
||||||
matching_buttons = result.fetchall()
|
matching_buttons = result.fetchall()
|
||||||
@@ -144,12 +165,14 @@ def get_button_key(uuid: str) -> int:
|
|||||||
row = matching_buttons[0]
|
row = matching_buttons[0]
|
||||||
key = row[0]
|
key = row[0]
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
database.close()
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
def get_buttons() -> dict[str, SDButton]:
|
def get_buttons() -> dict[str, SDButton]:
|
||||||
"""Load all buttons from the database."""
|
"""Load all buttons from the database."""
|
||||||
result: dict[str, SDButton] = {}
|
result: dict[str, SDButton] = {}
|
||||||
|
database = sqlite3.connect(DB_FILE)
|
||||||
cursor = database.cursor()
|
cursor = database.cursor()
|
||||||
for row in cursor.execute("SELECT key,uuid,device,x,y,svg FROM buttons"):
|
for row in cursor.execute("SELECT key,uuid,device,x,y,svg FROM buttons"):
|
||||||
base64_bytes = row[5].encode()
|
base64_bytes = row[5].encode()
|
||||||
@@ -162,10 +185,57 @@ def get_buttons() -> dict[str, SDButton]:
|
|||||||
"svg": svg_string,
|
"svg": svg_string,
|
||||||
})
|
})
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
database.close()
|
||||||
print(f"Loaded {len(result)} buttons from DB")
|
print(f"Loaded {len(result)} buttons from DB")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def write_button_state(key: int, state: bool, update: str):
|
||||||
|
"""Write button state to database."""
|
||||||
|
state_int = 0
|
||||||
|
if state is True:
|
||||||
|
state_int = 1
|
||||||
|
|
||||||
|
database = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = database.cursor()
|
||||||
|
|
||||||
|
# Check if exists
|
||||||
|
result = cursor.execute(f"SELECT state FROM button_states WHERE key={key}")
|
||||||
|
matching_states = result.fetchall()
|
||||||
|
if len(matching_states) > 0:
|
||||||
|
# Perform update
|
||||||
|
cursor.execute(
|
||||||
|
f"UPDATE button_states SET state={state_int}, state_update=\"{update}\" WHERE key={key}")
|
||||||
|
else:
|
||||||
|
# Create new row
|
||||||
|
cursor.execute(
|
||||||
|
f"INSERT INTO button_states VALUES ({key}, {state_int}, \"{update}\")")
|
||||||
|
database.commit()
|
||||||
|
print(f"Saved button_state with key {key} to database")
|
||||||
|
cursor.close()
|
||||||
|
database.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_button_state(key: int) -> tuple | None:
|
||||||
|
"""Load button_state from database."""
|
||||||
|
result = ()
|
||||||
|
database = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = database.cursor()
|
||||||
|
result = cursor.execute(
|
||||||
|
f"SELECT key,state,state_update FROM button_states WHERE key={key}")
|
||||||
|
matching_states = result.fetchall()
|
||||||
|
if len(matching_states) == 0:
|
||||||
|
return None
|
||||||
|
row = matching_states[0]
|
||||||
|
state = False
|
||||||
|
if row[1] == 1:
|
||||||
|
state = True
|
||||||
|
result = (state, row[2])
|
||||||
|
cursor.close()
|
||||||
|
database.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# API
|
# API
|
||||||
#
|
#
|
||||||
@@ -232,6 +302,12 @@ async def websocket_handler(request: web.Request):
|
|||||||
return web_socket
|
return web_socket
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Websocket broadcast not working yet
|
||||||
|
def websocket_broadcast(message: str):
|
||||||
|
"""Send a message to each websocket client."""
|
||||||
|
print(f"BROADCAST: {message}")
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Functions
|
# Functions
|
||||||
#
|
#
|
||||||
@@ -265,10 +341,46 @@ def get_position(deck: StreamDeck, key: int) -> SDButtonPosition:
|
|||||||
return SDButtonPosition({"x": int(key / deck.KEY_COLS), "y": key % deck.KEY_COLS})
|
return SDButtonPosition({"x": int(key / deck.KEY_COLS), "y": key % deck.KEY_COLS})
|
||||||
|
|
||||||
|
|
||||||
def on_key_change(deck: StreamDeck, key: int, state: bool):
|
def on_key_change(_: StreamDeck, key: int, state: bool):
|
||||||
"""Handle key change callbacks."""
|
"""Handle key change callbacks."""
|
||||||
position = get_position(deck, key)
|
button = get_button(key)
|
||||||
print(f"Key at {position.x_pos}|{position.y_pos} is state {state}")
|
if button is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if state is True:
|
||||||
|
websocket_broadcast(encode(
|
||||||
|
{"event": "keyDown", "args": button.uuid}))
|
||||||
|
else:
|
||||||
|
websocket_broadcast(encode(
|
||||||
|
{"event": "keyUp", "args": button.uuid}))
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
db_button_state = get_button_state(key)
|
||||||
|
|
||||||
|
if db_button_state is None:
|
||||||
|
write_button_state(key, state, now.strftime(DATETIME_FORMAT))
|
||||||
|
return
|
||||||
|
|
||||||
|
last_state: bool = db_button_state[0]
|
||||||
|
last_update: str = db_button_state[1]
|
||||||
|
last_update_datetime = datetime.strptime(last_update, DATETIME_FORMAT)
|
||||||
|
diff = now - last_update_datetime
|
||||||
|
|
||||||
|
if last_state is True and state is False and diff.seconds < LONG_PRESS_SECONDS:
|
||||||
|
websocket_broadcast(
|
||||||
|
encode({"event": "singleTap", "args": button.uuid}))
|
||||||
|
write_button_state(key, state, now.strftime(DATETIME_FORMAT))
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO: Work with timer instead
|
||||||
|
if last_state is True and state is False and diff.seconds >= LONG_PRESS_SECONDS:
|
||||||
|
websocket_broadcast(
|
||||||
|
encode({"event": "longPress", "args": button.uuid}))
|
||||||
|
write_button_state(key, state, now.strftime(DATETIME_FORMAT))
|
||||||
|
return
|
||||||
|
|
||||||
|
write_button_state(key, state, now.strftime(DATETIME_FORMAT))
|
||||||
|
|
||||||
|
|
||||||
def update_button_icon(uuid: str, svg: str):
|
def update_button_icon(uuid: str, svg: str):
|
||||||
@@ -292,7 +404,7 @@ def set_icon(deck: StreamDeck, key: int, svg: str):
|
|||||||
"""Draw an icon to the button."""
|
"""Draw an icon to the button."""
|
||||||
png_bytes = io.BytesIO()
|
png_bytes = io.BytesIO()
|
||||||
cairosvg.svg2png(svg.encode("utf-8"), write_to=png_bytes)
|
cairosvg.svg2png(svg.encode("utf-8"), write_to=png_bytes)
|
||||||
|
|
||||||
# Debug
|
# Debug
|
||||||
cairosvg.svg2png(svg.encode("utf-8"), write_to=f"icon_{key}.png")
|
cairosvg.svg2png(svg.encode("utf-8"), write_to=f"icon_{key}.png")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user