added compatibility for python 3.8
This commit is contained in:
15
.vscode/settings.json
vendored
Normal file
15
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||||
|
},
|
||||||
|
"python.formatting.provider": "none",
|
||||||
|
"python.testing.unittestArgs": [
|
||||||
|
"-v",
|
||||||
|
"-s",
|
||||||
|
"./tests",
|
||||||
|
"-p",
|
||||||
|
"*test.py"
|
||||||
|
],
|
||||||
|
"python.testing.pytestEnabled": false,
|
||||||
|
"python.testing.unittestEnabled": true
|
||||||
|
}
|
||||||
6
setup.py
6
setup.py
@@ -1,6 +1,8 @@
|
|||||||
from setuptools import setup, find_packages
|
"""Setup for pypi package"""
|
||||||
import codecs
|
|
||||||
import os
|
import os
|
||||||
|
import codecs
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Stream Deck API."""
|
"""Stream Deck API."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable
|
from typing import Callable
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -22,13 +22,44 @@ class StreamDeckApi:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
host: str,
|
host: str,
|
||||||
on_button_press: Callable[[str], None] | None = None,
|
on_button_press: any = None,
|
||||||
on_button_release: Callable[[str], None] | None = None,
|
on_button_release: any = None,
|
||||||
on_status_update: Callable[[SDInfo], None] | None = None,
|
on_status_update: any = None,
|
||||||
on_ws_message: Callable[[SDWebsocketMessage], None] | None = None,
|
on_ws_message: any = None,
|
||||||
on_ws_connect: Callable[[], None] | None = None,
|
on_ws_connect: any = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Init Stream Deck API object."""
|
"""Init Stream Deck API object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
on_button_press (Callable[[str], None] or None): Callback if button pressed
|
||||||
|
on_button_release (Callable[[str], None] or None): Callback if button released
|
||||||
|
on_status_update (Callable[[SDInfo], None] or None): Callback if status update received
|
||||||
|
on_ws_message (Callable[[SDWebsocketMessage], None] or None): Callback if websocket message received
|
||||||
|
on_ws_connect (Callable[[], None] or None): Callback on websocket connected
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Type checks
|
||||||
|
if on_button_press is not None and not isinstance(
|
||||||
|
on_button_press, Callable[[str], None]
|
||||||
|
):
|
||||||
|
raise TypeError()
|
||||||
|
if on_button_release is not None and not isinstance(
|
||||||
|
on_button_release, Callable[[str], None]
|
||||||
|
):
|
||||||
|
raise TypeError()
|
||||||
|
if on_status_update is not None and not isinstance(
|
||||||
|
on_status_update, Callable[[SDInfo], None]
|
||||||
|
):
|
||||||
|
raise TypeError()
|
||||||
|
if on_ws_message is not None and not isinstance(
|
||||||
|
on_ws_message, Callable[[SDWebsocketMessage], None]
|
||||||
|
):
|
||||||
|
raise TypeError()
|
||||||
|
if on_ws_connect is not None and not isinstance(
|
||||||
|
on_ws_connect, Callable[[], None]
|
||||||
|
):
|
||||||
|
raise TypeError()
|
||||||
|
|
||||||
self._host = host
|
self._host = host
|
||||||
self._on_button_press = on_button_press
|
self._on_button_press = on_button_press
|
||||||
self._on_button_release = on_button_release
|
self._on_button_release = on_button_release
|
||||||
@@ -37,7 +68,7 @@ class StreamDeckApi:
|
|||||||
self._on_ws_connect = on_ws_connect
|
self._on_ws_connect = on_ws_connect
|
||||||
self._loop = asyncio.get_event_loop()
|
self._loop = asyncio.get_event_loop()
|
||||||
self._running = False
|
self._running = False
|
||||||
self._task: asyncio.Task | None = None
|
self._task: any = None
|
||||||
|
|
||||||
#
|
#
|
||||||
# Properties
|
# Properties
|
||||||
@@ -68,8 +99,13 @@ class StreamDeckApi:
|
|||||||
#
|
#
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_request(url: str) -> None | requests.Response:
|
def _get_request(url: str) -> any:
|
||||||
"""Handle GET requests."""
|
"""Handle GET requests.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
requests.Response or None
|
||||||
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = requests.get(url, timeout=5)
|
res = requests.get(url, timeout=5)
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
@@ -85,13 +121,17 @@ class StreamDeckApi:
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _post_request(url: str, data: str, headers) -> None | requests.Response:
|
def _post_request(url: str, data: str, headers) -> any:
|
||||||
"""Handle POST requests."""
|
"""Handle POST requests.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
requests.Response or None
|
||||||
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = requests.post(url, data, headers=headers, timeout=5)
|
res = requests.post(url, data, headers=headers, timeout=5)
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug("Error sending data to Stream Deck Plugin (exception)")
|
||||||
"Error sending data to Stream Deck Plugin (exception)")
|
|
||||||
return None
|
return None
|
||||||
if res.status_code != 200:
|
if res.status_code != 200:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@@ -101,9 +141,14 @@ class StreamDeckApi:
|
|||||||
return None
|
return None
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def get_info(self, in_executor: bool = True) -> None | SDInfo:
|
async def get_info(self, in_executor: bool = True) -> any:
|
||||||
"""Get info about Stream Deck."""
|
"""Get info about Stream Deck.
|
||||||
res: requests.Response | None = None
|
|
||||||
|
Returns:
|
||||||
|
SDInfo or None
|
||||||
|
"""
|
||||||
|
|
||||||
|
res: any = None
|
||||||
if in_executor:
|
if in_executor:
|
||||||
res = await self._loop.run_in_executor(
|
res = await self._loop.run_in_executor(
|
||||||
None, self._get_request, self._info_url
|
None, self._get_request, self._info_url
|
||||||
@@ -120,13 +165,17 @@ class StreamDeckApi:
|
|||||||
try:
|
try:
|
||||||
info = SDInfo(rjson)
|
info = SDInfo(rjson)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug("Error parsing response from %s to SDInfo", self._info_url)
|
||||||
"Error parsing response from %s to SDInfo", self._info_url)
|
|
||||||
return None
|
return None
|
||||||
return info
|
return info
|
||||||
|
|
||||||
async def get_icon(self, btn: str) -> None | str:
|
async def get_icon(self, btn: str) -> any:
|
||||||
"""Get svg icon from Stream Deck button."""
|
"""Get svg icon from Stream Deck button.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str or None
|
||||||
|
"""
|
||||||
|
|
||||||
url = f"{self._icon_url}{btn}"
|
url = f"{self._icon_url}{btn}"
|
||||||
res = await self._loop.run_in_executor(None, self._get_request, url)
|
res = await self._loop.run_in_executor(None, self._get_request, url)
|
||||||
if res is None or res.status_code != 200:
|
if res is None or res.status_code != 200:
|
||||||
@@ -152,8 +201,14 @@ class StreamDeckApi:
|
|||||||
# Websocket Methods
|
# Websocket Methods
|
||||||
#
|
#
|
||||||
|
|
||||||
def _on_button_change(self, uuid: str | dict, state: bool):
|
def _on_button_change(self, uuid: any, state: bool):
|
||||||
"""Handle button down event."""
|
"""Handle button down event.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
uuid (str or dict): UUID of the button
|
||||||
|
state (bool): State of the button
|
||||||
|
"""
|
||||||
|
|
||||||
if not isinstance(uuid, str):
|
if not isinstance(uuid, str):
|
||||||
_LOGGER.debug("Method _on_button_change: uuid is not str")
|
_LOGGER.debug("Method _on_button_change: uuid is not str")
|
||||||
return
|
return
|
||||||
@@ -162,8 +217,13 @@ class StreamDeckApi:
|
|||||||
elif state is False and self._on_button_release is not None:
|
elif state is False and self._on_button_release is not None:
|
||||||
self._on_button_release(uuid)
|
self._on_button_release(uuid)
|
||||||
|
|
||||||
def _on_ws_status_update(self, info: SDInfo | str | dict):
|
def _on_ws_status_update(self, info: any):
|
||||||
"""Handle Stream Deck status update event."""
|
"""Handle Stream Deck status update event.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
info (SDInfo or str or dict): Stream Deck Info
|
||||||
|
"""
|
||||||
|
|
||||||
if not isinstance(info, SDInfo):
|
if not isinstance(info, SDInfo):
|
||||||
_LOGGER.debug("Method _on_ws_status_update: info is not SDInfo")
|
_LOGGER.debug("Method _on_ws_status_update: info is not SDInfo")
|
||||||
return
|
return
|
||||||
@@ -180,8 +240,7 @@ class StreamDeckApi:
|
|||||||
try:
|
try:
|
||||||
datajson = json.loads(msg)
|
datajson = json.loads(msg)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug("Method _on_message: Websocket message couldn't get parsed")
|
||||||
"Method _on_message: Websocket message couldn't get parsed")
|
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
data = SDWebsocketMessage(datajson)
|
data = SDWebsocketMessage(datajson)
|
||||||
@@ -226,8 +285,7 @@ class StreamDeckApi:
|
|||||||
)
|
)
|
||||||
self._on_message(data)
|
self._on_message(data)
|
||||||
await websocket.close()
|
await websocket.close()
|
||||||
_LOGGER.debug(
|
_LOGGER.debug("Method _websocket_loop: Websocket closed")
|
||||||
"Method _websocket_loop: Websocket closed")
|
|
||||||
except WebSocketException:
|
except WebSocketException:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Method _websocket_loop: Websocket client crashed. Restarting it"
|
"Method _websocket_loop: Websocket client crashed. Restarting it"
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import platform
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import base64
|
import base64
|
||||||
import socket
|
import socket
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import List, Dict
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import human_readable_ids as hri
|
import human_readable_ids as hri
|
||||||
from jsonpickle import encode
|
from jsonpickle import encode
|
||||||
@@ -28,7 +28,7 @@ from streamdeckapi.const import (
|
|||||||
PLUGIN_ICON,
|
PLUGIN_ICON,
|
||||||
PLUGIN_INFO,
|
PLUGIN_INFO,
|
||||||
PLUGIN_PORT,
|
PLUGIN_PORT,
|
||||||
SD_SSDP
|
SD_SSDP,
|
||||||
)
|
)
|
||||||
from streamdeckapi.types import SDApplication, SDButton, SDButtonPosition, SDDevice
|
from streamdeckapi.types import SDApplication, SDButton, SDButtonPosition, SDDevice
|
||||||
|
|
||||||
@@ -57,10 +57,10 @@ application: SDApplication = SDApplication(
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
devices: list[SDDevice] = []
|
devices: List[SDDevice] = []
|
||||||
websocket_connections: list[web.WebSocketResponse] = []
|
websocket_connections: List[web.WebSocketResponse] = []
|
||||||
|
|
||||||
streamdecks: list[StreamDeck] = DeviceManager().enumerate()
|
streamdecks: List[StreamDeck] = DeviceManager().enumerate()
|
||||||
|
|
||||||
#
|
#
|
||||||
# Database
|
# Database
|
||||||
@@ -68,7 +68,8 @@ streamdecks: list[StreamDeck] = DeviceManager().enumerate()
|
|||||||
|
|
||||||
database_first = sqlite3.connect(DB_FILE)
|
database_first = sqlite3.connect(DB_FILE)
|
||||||
table_cursor = database_first.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,
|
||||||
uuid text NOT NULL,
|
uuid text NOT NULL,
|
||||||
@@ -76,13 +77,16 @@ table_cursor.execute("""
|
|||||||
x integer,
|
x integer,
|
||||||
y integer,
|
y integer,
|
||||||
svg text
|
svg text
|
||||||
);""")
|
);"""
|
||||||
table_cursor.execute("""
|
)
|
||||||
|
table_cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS button_states(
|
CREATE TABLE IF NOT EXISTS button_states(
|
||||||
key integer PRIMARY KEY,
|
key integer PRIMARY KEY,
|
||||||
state integer,
|
state integer,
|
||||||
state_update text
|
state_update text
|
||||||
);""")
|
);"""
|
||||||
|
)
|
||||||
table_cursor.execute("DELETE FROM button_states;")
|
table_cursor.execute("DELETE FROM button_states;")
|
||||||
database_first.commit()
|
database_first.commit()
|
||||||
table_cursor.close()
|
table_cursor.close()
|
||||||
@@ -102,12 +106,12 @@ def save_button(key: int, button: SDButton):
|
|||||||
matching_buttons = result.fetchall()
|
matching_buttons = result.fetchall()
|
||||||
if len(matching_buttons) > 0:
|
if len(matching_buttons) > 0:
|
||||||
# Perform update
|
# Perform update
|
||||||
cursor.execute(
|
cursor.execute(f'UPDATE buttons SET svg="{base64_string}" WHERE key={key}')
|
||||||
f"UPDATE buttons SET svg=\"{base64_string}\" WHERE key={key}")
|
|
||||||
else:
|
else:
|
||||||
# Create new row
|
# Create new row
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
f"INSERT INTO buttons VALUES ({key}, \"{button.uuid}\", \"{button.device}\", {button.position.x_pos}, {button.position.y_pos}, \"{base64_string}\")")
|
f'INSERT INTO buttons VALUES ({key}, "{button.uuid}", "{button.device}", {button.position.x_pos}, {button.position.y_pos}, "{base64_string}")'
|
||||||
|
)
|
||||||
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()
|
||||||
@@ -119,7 +123,8 @@ def get_button(key: int) -> any:
|
|||||||
database = sqlite3.connect(DB_FILE)
|
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}"
|
||||||
|
)
|
||||||
matching_buttons = result.fetchall()
|
matching_buttons = result.fetchall()
|
||||||
if len(matching_buttons) == 0:
|
if len(matching_buttons) == 0:
|
||||||
return None
|
return None
|
||||||
@@ -127,12 +132,14 @@ def get_button(key: int) -> any:
|
|||||||
base64_bytes = row[5].encode()
|
base64_bytes = row[5].encode()
|
||||||
svg_bytes = base64.b64decode(base64_bytes)
|
svg_bytes = base64.b64decode(base64_bytes)
|
||||||
svg_string = svg_bytes.decode()
|
svg_string = svg_bytes.decode()
|
||||||
button = SDButton({
|
button = SDButton(
|
||||||
"uuid": row[1],
|
{
|
||||||
"device": row[2],
|
"uuid": row[1],
|
||||||
"position": {"x": row[3], "y": row[4]},
|
"device": row[2],
|
||||||
"svg": svg_string,
|
"position": {"x": row[3], "y": row[4]},
|
||||||
})
|
"svg": svg_string,
|
||||||
|
}
|
||||||
|
)
|
||||||
cursor.close()
|
cursor.close()
|
||||||
database.close()
|
database.close()
|
||||||
return button
|
return button
|
||||||
@@ -143,7 +150,8 @@ def get_button_by_uuid(uuid: str) -> any:
|
|||||||
database = sqlite3.connect(DB_FILE)
|
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}"'
|
||||||
|
)
|
||||||
matching_buttons = result.fetchall()
|
matching_buttons = result.fetchall()
|
||||||
if len(matching_buttons) == 0:
|
if len(matching_buttons) == 0:
|
||||||
return None
|
return None
|
||||||
@@ -151,12 +159,14 @@ def get_button_by_uuid(uuid: str) -> any:
|
|||||||
base64_bytes = row[5].encode()
|
base64_bytes = row[5].encode()
|
||||||
svg_bytes = base64.b64decode(base64_bytes)
|
svg_bytes = base64.b64decode(base64_bytes)
|
||||||
svg_string = svg_bytes.decode()
|
svg_string = svg_bytes.decode()
|
||||||
button = SDButton({
|
button = SDButton(
|
||||||
"uuid": row[1],
|
{
|
||||||
"device": row[2],
|
"uuid": row[1],
|
||||||
"position": {"x": row[3], "y": row[4]},
|
"device": row[2],
|
||||||
"svg": svg_string,
|
"position": {"x": row[3], "y": row[4]},
|
||||||
})
|
"svg": svg_string,
|
||||||
|
}
|
||||||
|
)
|
||||||
cursor.close()
|
cursor.close()
|
||||||
database.close()
|
database.close()
|
||||||
return button
|
return button
|
||||||
@@ -166,7 +176,7 @@ 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)
|
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()
|
||||||
if len(matching_buttons) == 0:
|
if len(matching_buttons) == 0:
|
||||||
return -1
|
return -1
|
||||||
@@ -177,21 +187,23 @@ def get_button_key(uuid: str) -> int:
|
|||||||
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)
|
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()
|
||||||
svg_bytes = base64.b64decode(base64_bytes)
|
svg_bytes = base64.b64decode(base64_bytes)
|
||||||
svg_string = svg_bytes.decode()
|
svg_string = svg_bytes.decode()
|
||||||
result[row[0]] = SDButton({
|
result[row[0]] = SDButton(
|
||||||
"uuid": row[1],
|
{
|
||||||
"device": row[2],
|
"uuid": row[1],
|
||||||
"position": {"x": row[3], "y": row[4]},
|
"device": row[2],
|
||||||
"svg": svg_string,
|
"position": {"x": row[3], "y": row[4]},
|
||||||
})
|
"svg": svg_string,
|
||||||
|
}
|
||||||
|
)
|
||||||
cursor.close()
|
cursor.close()
|
||||||
database.close()
|
database.close()
|
||||||
print(f"Loaded {len(result)} buttons from DB")
|
print(f"Loaded {len(result)} buttons from DB")
|
||||||
@@ -213,11 +225,13 @@ def write_button_state(key: int, state: bool, update: str):
|
|||||||
if len(matching_states) > 0:
|
if len(matching_states) > 0:
|
||||||
# Perform update
|
# Perform update
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
f"UPDATE button_states SET state={state_int}, state_update=\"{update}\" WHERE key={key}")
|
f'UPDATE button_states SET state={state_int}, state_update="{update}" WHERE key={key}'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Create new row
|
# Create new row
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
f"INSERT INTO button_states VALUES ({key}, {state_int}, \"{update}\")")
|
f'INSERT INTO button_states VALUES ({key}, {state_int}, "{update}")'
|
||||||
|
)
|
||||||
database.commit()
|
database.commit()
|
||||||
print(f"Saved button_state with key {key} to database")
|
print(f"Saved button_state with key {key} to database")
|
||||||
cursor.close()
|
cursor.close()
|
||||||
@@ -230,7 +244,8 @@ def get_button_state(key: int) -> any:
|
|||||||
database = sqlite3.connect(DB_FILE)
|
database = sqlite3.connect(DB_FILE)
|
||||||
cursor = database.cursor()
|
cursor = database.cursor()
|
||||||
result = cursor.execute(
|
result = cursor.execute(
|
||||||
f"SELECT key,state,state_update FROM button_states WHERE key={key}")
|
f"SELECT key,state,state_update FROM button_states WHERE key={key}"
|
||||||
|
)
|
||||||
matching_states = result.fetchall()
|
matching_states = result.fetchall()
|
||||||
if len(matching_states) == 0:
|
if len(matching_states) == 0:
|
||||||
return None
|
return None
|
||||||
@@ -310,7 +325,8 @@ async def websocket_handler(request: web.Request):
|
|||||||
await web_socket.close()
|
await web_socket.close()
|
||||||
elif msg.type == aiohttp.WSMsgType.ERROR:
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
print(
|
print(
|
||||||
f"Websocket connection closed with exception {web_socket.exception()}")
|
f"Websocket connection closed with exception {web_socket.exception()}"
|
||||||
|
)
|
||||||
|
|
||||||
websocket_connections.remove(web_socket)
|
websocket_connections.remove(web_socket)
|
||||||
return web_socket
|
return web_socket
|
||||||
@@ -332,8 +348,8 @@ async def broadcast_status():
|
|||||||
"args": {
|
"args": {
|
||||||
"devices": devices,
|
"devices": devices,
|
||||||
"application": application,
|
"application": application,
|
||||||
"buttons": get_buttons()
|
"buttons": get_buttons(),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
data_str = encode(data, unpicklable=False)
|
data_str = encode(data, unpicklable=False)
|
||||||
@@ -412,14 +428,12 @@ async def on_key_change(_: StreamDeck, key: int, state: bool):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if state is True:
|
if state is True:
|
||||||
await websocket_broadcast(encode(
|
await websocket_broadcast(encode({"event": "keyDown", "args": button.uuid}))
|
||||||
{"event": "keyDown", "args": button.uuid}))
|
|
||||||
print("Waiting for button release")
|
print("Waiting for button release")
|
||||||
# Start timer
|
# Start timer
|
||||||
Timer(LONG_PRESS_SECONDS, lambda: long_press_callback(key), False)
|
Timer(LONG_PRESS_SECONDS, lambda: long_press_callback(key), False)
|
||||||
else:
|
else:
|
||||||
await websocket_broadcast(encode(
|
await websocket_broadcast(encode({"event": "keyUp", "args": button.uuid}))
|
||||||
{"event": "keyUp", "args": button.uuid}))
|
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
@@ -519,10 +533,10 @@ def get_local_ip():
|
|||||||
"""Get local ip address."""
|
"""Get local ip address."""
|
||||||
connection = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
connection = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
try:
|
try:
|
||||||
connection.connect(('192.255.255.255', 1))
|
connection.connect(("192.255.255.255", 1))
|
||||||
address = connection.getsockname()[0]
|
address = connection.getsockname()[0]
|
||||||
except socket.error:
|
except socket.error:
|
||||||
address = '127.0.0.1'
|
address = "127.0.0.1"
|
||||||
finally:
|
finally:
|
||||||
connection.close()
|
connection.close()
|
||||||
return address
|
return address
|
||||||
@@ -533,8 +547,12 @@ class StreamDeckApiSsdpProtocol(ssdp.SimpleServiceDiscoveryProtocol):
|
|||||||
|
|
||||||
def response_received(self, response: ssdp.SSDPResponse, addr: tuple):
|
def response_received(self, response: ssdp.SSDPResponse, addr: tuple):
|
||||||
"""Handle an incoming response."""
|
"""Handle an incoming response."""
|
||||||
print("received response: %s %s %s", response.status_code,
|
print(
|
||||||
response.reason, response.version)
|
"received response: %s %s %s",
|
||||||
|
response.status_code,
|
||||||
|
response.reason,
|
||||||
|
response.version,
|
||||||
|
)
|
||||||
|
|
||||||
for header in response.headers:
|
for header in response.headers:
|
||||||
print("header: %s", header)
|
print("header: %s", header)
|
||||||
@@ -544,9 +562,7 @@ class StreamDeckApiSsdpProtocol(ssdp.SimpleServiceDiscoveryProtocol):
|
|||||||
def request_received(self, request: ssdp.SSDPRequest, addr: tuple):
|
def request_received(self, request: ssdp.SSDPRequest, addr: tuple):
|
||||||
"""Handle an incoming request and respond to it."""
|
"""Handle an incoming request and respond to it."""
|
||||||
print(
|
print(
|
||||||
"received request: %s %s %s",
|
"received request: %s %s %s", request.method, request.uri, request.version
|
||||||
request.method, request.uri, request.version
|
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for header in request.headers:
|
for header in request.headers:
|
||||||
@@ -607,7 +623,6 @@ def start():
|
|||||||
"""Entrypoint."""
|
"""Entrypoint."""
|
||||||
init_all()
|
init_all()
|
||||||
|
|
||||||
executor = ProcessPoolExecutor(2)
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
# SSDP server
|
# SSDP server
|
||||||
@@ -619,7 +634,7 @@ def start():
|
|||||||
family=socket.AF_INET,
|
family=socket.AF_INET,
|
||||||
local_addr=(StreamDeckApiSsdpProtocol.MULTICAST_ADDRESS, 1900),
|
local_addr=(StreamDeckApiSsdpProtocol.MULTICAST_ADDRESS, 1900),
|
||||||
)
|
)
|
||||||
transport, protocol = loop.run_until_complete(connect)
|
transport, _ = loop.run_until_complete(connect)
|
||||||
|
|
||||||
StreamDeckApiSsdpProtocol.transport = transport
|
StreamDeckApiSsdpProtocol.transport = transport
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Stream Deck API types."""
|
"""Stream Deck API types."""
|
||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
|
|
||||||
class SDApplication:
|
class SDApplication:
|
||||||
@@ -82,8 +83,8 @@ class SDInfo(dict):
|
|||||||
|
|
||||||
def __init__(self, obj: dict) -> None:
|
def __init__(self, obj: dict) -> None:
|
||||||
"""Init Stream Deck Info object."""
|
"""Init Stream Deck Info object."""
|
||||||
self.devices: list[SDDevice] = []
|
self.devices: List[SDDevice] = []
|
||||||
self.buttons: dict[str, SDButton] = {}
|
self.buttons: Dict[str, SDButton] = {}
|
||||||
|
|
||||||
dict.__init__(self, obj)
|
dict.__init__(self, obj)
|
||||||
self.application = SDApplication(obj["application"])
|
self.application = SDApplication(obj["application"])
|
||||||
|
|||||||
35
tests/api_test.py
Normal file
35
tests/api_test.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"""Unittests for API client."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import streamdeckapi
|
||||||
|
|
||||||
|
|
||||||
|
class TestApi(unittest.TestCase):
|
||||||
|
"""Api Test Case."""
|
||||||
|
|
||||||
|
def test_constructor(self):
|
||||||
|
"""Constructor test."""
|
||||||
|
host = "host.local"
|
||||||
|
|
||||||
|
# Test valid types
|
||||||
|
api = streamdeckapi.StreamDeckApi(host)
|
||||||
|
self.assertEqual(api.host, host)
|
||||||
|
|
||||||
|
streamdeckapi.StreamDeckApi(host, on_button_press=None)
|
||||||
|
streamdeckapi.StreamDeckApi(host, on_button_release=None)
|
||||||
|
streamdeckapi.StreamDeckApi(host, on_status_update=None)
|
||||||
|
streamdeckapi.StreamDeckApi(host, on_ws_connect=None)
|
||||||
|
streamdeckapi.StreamDeckApi(host, on_ws_message=None)
|
||||||
|
|
||||||
|
# Test some invalid types
|
||||||
|
for i_type in ["string", 2345, [321, 6457], {"key": "value"}]:
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
_ = streamdeckapi.StreamDeckApi(host, on_button_press=i_type)
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
_ = streamdeckapi.StreamDeckApi(host, on_button_release=i_type)
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
_ = streamdeckapi.StreamDeckApi(host, on_status_update=i_type)
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
_ = streamdeckapi.StreamDeckApi(host, on_ws_connect=i_type)
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
_ = streamdeckapi.StreamDeckApi(host, on_ws_message=i_type)
|
||||||
Reference in New Issue
Block a user