Karl 06f4c2768c feat: initial implementation of StreamDock Home Assistant integration
This commit introduces a complete integration for Mirabox StreamDock
devices with Home Assistant, allowing users to control entities and
monitor states directly from the hardware.

Key features included:
- Support for multiple Mirabox models: N1, N3, N4, N4Pro, XL, M3, M18,
  K1Pro, and various 293 variants.
- Home Assistant WebSocket integration for real-time entity updates and
  service execution.
- Dynamic LCD key rendering with support for custom icons, labels, and
  entity-state aware colors.
- Input handling for physical buttons and rotary encoders (knobs).
- Multi-page navigation support with configurable cycle keys and knobs.
- Cross-platform transport layer using a custom HID library.
- Configuration system using YAML with page-based layouts.
- Linux udev rules for non-root USB access.
2026-05-07 19:14:07 +01:00

226 lines
6.9 KiB
Python

from StreamDock.FeatrueOption import device_type
from .StreamDock import StreamDock
from ..InputTypes import InputEvent, ButtonKey, EventType, KnobId, Direction
from PIL import Image
import ctypes
import ctypes.util
import os, io
from ..ImageHelpers.PILHelper import *
import random
class K1Pro(StreamDock):
"""K1Pro device class - supports 6 keys and 3 knobs"""
KEY_COUNT = 6
KEY_MAP = False
# Image key mapping: logical key -> hardware key (for setting images)
_IMAGE_KEY_MAP = {
ButtonKey.KEY_1: 0x05,
ButtonKey.KEY_2: 0x03,
ButtonKey.KEY_3: 0x01,
ButtonKey.KEY_4: 0x06,
ButtonKey.KEY_5: 0x04,
ButtonKey.KEY_6: 0x02,
}
# Reverse mapping: hardware key -> logical key (for event decoding)
_HW_TO_LOGICAL_KEY = {v: k for k, v in _IMAGE_KEY_MAP.items()}
def __init__(self, transport1, devInfo):
super().__init__(transport1, devInfo)
def get_image_key(self, logical_key: ButtonKey) -> int:
"""
Convert logical key value to hardware key value (for setting images)
Args:
logical_key: Logical key enum
Returns:
int: Hardware key value
"""
if logical_key in self._IMAGE_KEY_MAP:
return self._IMAGE_KEY_MAP[logical_key]
raise ValueError(f"K1Pro: Unsupported key {logical_key}")
def decode_input_event(self, hardware_code: int, state: int) -> InputEvent:
"""
Decode hardware event codes into a unified InputEvent
Hardware code mapping:
- Keys: 0x05, 0x03, 0x01, 0x06, 0x04, 0x02
- Knob 1 press: 0x25
- Knob 2 press: 0x30
- Knob 3 press: 0x31
- Knob 1 rotation: 0x50 (left), 0x51 (right)
- Knob 2 rotation: 0x60 (left), 0x61 (right)
- Knob 3 rotation: 0x90 (left), 0x91 (right)
"""
# Handle state value: 0x02=release, 0x01=press
normalized_state = 1 if state == 0x01 else 0
# Regular button events
if hardware_code in self._HW_TO_LOGICAL_KEY:
return InputEvent(
event_type=EventType.BUTTON,
key=self._HW_TO_LOGICAL_KEY[hardware_code],
state=normalized_state,
)
# Knob press event
knob_press_map = {
0x25: KnobId.KNOB_1,
0x30: KnobId.KNOB_2,
0x31: KnobId.KNOB_3,
}
if hardware_code in knob_press_map:
return InputEvent(
event_type=EventType.KNOB_PRESS,
knob_id=knob_press_map[hardware_code],
state=normalized_state,
)
# Knob rotation event
knob_rotate_map = {
0x50: (KnobId.KNOB_1, Direction.LEFT),
0x51: (KnobId.KNOB_1, Direction.RIGHT),
0x60: (KnobId.KNOB_2, Direction.LEFT),
0x61: (KnobId.KNOB_2, Direction.RIGHT),
0x90: (KnobId.KNOB_3, Direction.LEFT),
0x91: (KnobId.KNOB_3, Direction.RIGHT),
}
if hardware_code in knob_rotate_map:
knob_id, direction = knob_rotate_map[hardware_code]
return InputEvent(
event_type=EventType.KNOB_ROTATE, knob_id=knob_id, direction=direction
)
# Unknown event
return InputEvent(event_type=EventType.UNKNOWN)
# Set device screen brightness
def set_brightness(self, percent):
return self.transport.setBrightness(percent)
def set_touchscreen_image(self, path):
"""Background setting not supported"""
return 0
# Set device key icon image 64 * 64
def set_key_image(self, key, path):
try:
if isinstance(key, int):
if key not in range(1, 7):
print(f"key '{key}' out of range. you should set (1 ~ 6)")
return -1
logical_key = ButtonKey(key)
else:
logical_key = key
if not os.path.exists(path):
print(f"Error: The image file '{path}' does not exist.")
return -1
# Get hardware key value
hardware_key = self.get_image_key(logical_key)
# open formatter
image = Image.open(path)
image = to_native_key_format(self, image)
temp_image_path = (
"rotated_key_image_" + str(random.randint(9999, 999999)) + ".jpg"
)
image.save(temp_image_path)
# encode send
path_bytes = temp_image_path.encode("utf-8")
c_path = ctypes.c_char_p(path_bytes)
res = self.transport.setKeyImgDualDevice(c_path, hardware_key)
os.remove(temp_image_path)
return res
except Exception as e:
print(f"Error: {e}")
return -1
# TODO
def set_key_imageData(self, key, path):
pass
# Get device serial number
def get_serial_number(self):
return self.serial_number
def key_image_format(self):
return {
"size": (64, 64),
"format": "JPEG",
"rotation": -90,
"flip": (False, False),
}
def touchscreen_image_format(self):
return {
"size": (800, 480),
"format": "JPEG",
"rotation": 180,
"flip": (False, False),
}
# Set device parameters
def set_device(self):
self.transport.set_report_size(513, 1025, 0)
self.transport.set_report_id(0x04)
self.feature_option.deviceType = device_type.k1pro
pass
def set_keyboard_backlight_brightness(self, brightness):
"""
Set the keyboard backlight brightness.
Args:
brightness: Brightness value (0-6)
"""
self.transport.set_keyboard_backlight_brightness(brightness)
def set_keyboard_lighting_effects(self, effect: int):
"""
Set the keyboard lighting effect.
0 is static lighting.
Args:
effect: Effect mode identifier (0-9)
"""
if(effect==0):
self.set_keyboard_lighting_speed(0)
self.transport.set_keyboard_lighting_effects(effect)
def set_keyboard_lighting_speed(self, speed: int):
"""
Set the keyboard lighting effect speed.
Args:
speed: Speed value for lighting effects (0-7)
"""
self.transport.set_keyboard_lighting_speed(speed)
def set_keyboard_rgb_backlight(self, red: int, green: int, blue: int):
"""
Set the keyboard RGB backlight color.
Args:
red: Red component (0-255)
green: Green component (0-255)
blue: Blue component (0-255)
"""
self.transport.set_keyboard_rgb_backlight(red, green, blue)
def keyboard_os_mode_switch(self, os_mode: int):
"""
Set the keyboard OS mode.
Args:
os_mode: OS mode identifier
"""
self.transport.keyboard_os_mode_switch(os_mode)