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.
307 lines
9.9 KiB
Python
307 lines
9.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 StreamDockN4Pro(StreamDock):
|
|
"""StreamDockN4Pro device class - supports 15 keys, 4 knobs, and swipe gestures"""
|
|
|
|
KEY_COUNT = 15
|
|
KEY_MAP = False
|
|
|
|
# Image key mapping: logical key -> hardware key (for setting images)
|
|
_IMAGE_KEY_MAP = {
|
|
# Main keys 1-10
|
|
ButtonKey.KEY_1: 11,
|
|
ButtonKey.KEY_2: 12,
|
|
ButtonKey.KEY_3: 13,
|
|
ButtonKey.KEY_4: 14,
|
|
ButtonKey.KEY_5: 15,
|
|
ButtonKey.KEY_6: 6,
|
|
ButtonKey.KEY_7: 7,
|
|
ButtonKey.KEY_8: 8,
|
|
ButtonKey.KEY_9: 9,
|
|
ButtonKey.KEY_10: 10,
|
|
# Secondary screen keys 11-14 (176x112)
|
|
ButtonKey.KEY_11: 1,
|
|
ButtonKey.KEY_12: 2,
|
|
ButtonKey.KEY_13: 3,
|
|
ButtonKey.KEY_14: 4,
|
|
ButtonKey.KEY_15: 5,
|
|
}
|
|
|
|
# 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"StreamDockN4Pro: 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: 1-15
|
|
- Secondary screen keys: 0x40-0x43
|
|
- Knob rotation: 0xA0, 0xA1(Knob1), 0x50, 0x51(Knob2), 0x90, 0x91(Knob3), 0x70, 0x71(Knob4)
|
|
- Knob press: 0x37(Knob1), 0x35(Knob2), 0x33(Knob3), 0x36(Knob4)
|
|
- Swipe: 0x38 (left), 0x39 (right)
|
|
"""
|
|
# Handle state value: 0x02=release, 0x01=press
|
|
normalized_state = 1 if state == 0x01 else 0
|
|
|
|
# Regular button events (1-15)
|
|
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,
|
|
)
|
|
|
|
# Secondary screen key events
|
|
secondary_key_map = {
|
|
0x40: ButtonKey.KEY_11,
|
|
0x41: ButtonKey.KEY_12,
|
|
0x42: ButtonKey.KEY_13,
|
|
0x43: ButtonKey.KEY_14,
|
|
}
|
|
if hardware_code in secondary_key_map:
|
|
return InputEvent(
|
|
event_type=EventType.BUTTON,
|
|
key=secondary_key_map[hardware_code],
|
|
state=normalized_state,
|
|
)
|
|
|
|
# Knob rotation event
|
|
knob_rotate_map = {
|
|
0xA0: (KnobId.KNOB_1, Direction.LEFT),
|
|
0xA1: (KnobId.KNOB_1, Direction.RIGHT),
|
|
0x50: (KnobId.KNOB_2, Direction.LEFT),
|
|
0x51: (KnobId.KNOB_2, Direction.RIGHT),
|
|
0x90: (KnobId.KNOB_3, Direction.LEFT),
|
|
0x91: (KnobId.KNOB_3, Direction.RIGHT),
|
|
0x70: (KnobId.KNOB_4, Direction.LEFT),
|
|
0x71: (KnobId.KNOB_4, 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
|
|
)
|
|
|
|
# Knob press event
|
|
knob_press_map = {
|
|
0x37: KnobId.KNOB_1,
|
|
0x35: KnobId.KNOB_2,
|
|
0x33: KnobId.KNOB_3,
|
|
0x36: KnobId.KNOB_4,
|
|
}
|
|
if hardware_code in knob_press_map:
|
|
return InputEvent(
|
|
event_type=EventType.KNOB_PRESS,
|
|
knob_id=knob_press_map[hardware_code],
|
|
state=normalized_state,
|
|
)
|
|
|
|
# Swipe gesture
|
|
if hardware_code == 0x38:
|
|
return InputEvent(event_type=EventType.SWIPE, direction=Direction.LEFT)
|
|
if hardware_code == 0x39:
|
|
return InputEvent(event_type=EventType.SWIPE, direction=Direction.RIGHT)
|
|
|
|
# Unknown event
|
|
return InputEvent(event_type=EventType.UNKNOWN)
|
|
|
|
# Set device screen brightness
|
|
def set_brightness(self, percent):
|
|
return self.transport.setBrightness(percent)
|
|
|
|
# Set device background image 800 * 480
|
|
def set_touchscreen_image(self, path):
|
|
try:
|
|
if not os.path.exists(path):
|
|
print(f"Error: The image file '{path}' does not exist.")
|
|
return -1
|
|
|
|
image = Image.open(path)
|
|
image = to_native_touchscreen_format(self, image)
|
|
temp_image_path = (
|
|
"rotated_touchscreen_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.setBackgroundImgDualDevice(c_path)
|
|
os.remove(temp_image_path)
|
|
return res
|
|
|
|
except Exception as e:
|
|
print(f"Error: {e}")
|
|
return -1
|
|
def set_frame_background(self, path):
|
|
try:
|
|
if not os.path.exists(path):
|
|
print(f"Error: The image file '{path}' does not exist.")
|
|
return -1
|
|
|
|
image = Image.open(path)
|
|
image = to_native_touchscreen_format(self, image)
|
|
temp_image_path = (
|
|
"rotated_touchscreen_image_"
|
|
+ str(random.randint(9999, 999999))
|
|
+ ".jpg"
|
|
)
|
|
image.save(temp_image_path, quality=80)
|
|
|
|
# encode send
|
|
path_bytes = temp_image_path.encode("utf-8")
|
|
c_path = ctypes.c_char_p(path_bytes)
|
|
res = self.transport.setBackgroundImgFrame(
|
|
c_path,
|
|
800,
|
|
480,
|
|
)
|
|
os.remove(temp_image_path)
|
|
return res
|
|
except Exception as e:
|
|
print(f"Error: {e}")
|
|
return -1
|
|
|
|
# Set device key icon image 112 * 112
|
|
def set_key_image(self, key, path):
|
|
try:
|
|
if isinstance(key, int):
|
|
if key not in range(1, 16):
|
|
print(f"key '{key}' out of range. you should set (1 ~ 15)")
|
|
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
|
|
|
|
# Secondary screen keys use a different image format
|
|
if logical_key.value in range(11, 15):
|
|
return self.set_seondscreen_image(logical_key.value, path)
|
|
|
|
# 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
|
|
|
|
# Set device secondary screen key icon image 176 * 112
|
|
def set_seondscreen_image(self, key, path):
|
|
try:
|
|
if key not in range(11, 15):
|
|
print(f"key '{key}' out of range. you should set (11 ~ 14)")
|
|
return -1
|
|
|
|
logical_key = ButtonKey(key)
|
|
hardware_key = self.get_image_key(logical_key)
|
|
|
|
if not os.path.exists(path):
|
|
print(f"Error: The image file '{path}' does not exist.")
|
|
return -1
|
|
|
|
# open formatter
|
|
image = Image.open(path)
|
|
image = to_native_seondscreen_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": (112, 112),
|
|
"format": "JPEG",
|
|
"rotation": 180,
|
|
"flip": (False, False),
|
|
}
|
|
|
|
def secondscreen_image_format(self):
|
|
return {
|
|
"size": (176, 112),
|
|
"format": "JPEG",
|
|
"rotation": 180,
|
|
"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.feature_option.hasRGBLed = True
|
|
self.feature_option.ledCounts = 4
|
|
self.feature_option.deviceType = device_type.dock_n4pro
|
|
pass
|