eerste versie
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Local Kodi EventServer (Yatse UDP 9777) test utility and CLI.
|
||||
|
||||
This script allows you to test the Kodi EventServer locally by sending
|
||||
valid Kodi EventServer UDP packets (HELO, PING, BUTTON, ACTION) and
|
||||
verifying handshake responses.
|
||||
|
||||
Usage examples:
|
||||
# 1. Test HELO and PING handshake
|
||||
python3 tests/test_eventserver_client.py ping
|
||||
|
||||
# 2. Send navigation buttons
|
||||
python3 tests/test_eventserver_client.py up
|
||||
python3 tests/test_eventserver_client.py down
|
||||
python3 tests/test_eventserver_client.py left
|
||||
python3 tests/test_eventserver_client.py right
|
||||
python3 tests/test_eventserver_client.py select
|
||||
python3 tests/test_eventserver_client.py back
|
||||
|
||||
# 3. Interactive keyboard control mode:
|
||||
python3 tests/test_eventserver_client.py interactive
|
||||
|
||||
# 4. Run automated test suite against running server:
|
||||
python3 tests/test_eventserver_client.py test
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import random
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Kodi EventServer Constants
|
||||
HEADER_FORMAT = "!4sBBHIIHI10s"
|
||||
HEADER_SIZE = struct.calcsize(HEADER_FORMAT) # 32 bytes
|
||||
|
||||
PT_HELO = 0x01
|
||||
PT_BYE = 0x02
|
||||
PT_BUTTON = 0x03
|
||||
PT_PING = 0x05
|
||||
PT_ACTION = 0x0A
|
||||
|
||||
PTB_DOWN = 0x02
|
||||
PTB_UP = 0x04
|
||||
|
||||
|
||||
class EventServerClient:
|
||||
"""UDP Client simulating Yatse / Kodi remote."""
|
||||
|
||||
def __init__(self, host: str = "127.0.0.1", port: int = 9777, timeout: float = 2.0):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
self.token = random.randint(100000, 999999)
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.sock.settimeout(timeout)
|
||||
|
||||
def close(self) -> None:
|
||||
self.sock.close()
|
||||
|
||||
def _build_header(self, pkttype: int, paylen: int) -> bytes:
|
||||
return struct.pack(
|
||||
HEADER_FORMAT,
|
||||
b"XBMC", # Signature
|
||||
2, # Major version
|
||||
0, # Minor version
|
||||
pkttype, # Packet type
|
||||
1, # Seq
|
||||
1, # MaxSeq
|
||||
paylen, # Payload length
|
||||
self.token,
|
||||
b"\x00" * 10,
|
||||
)
|
||||
|
||||
def send_helo(self, device_name: str = "Local Yatse Test") -> bool:
|
||||
"""Send HELO packet and wait for PING ACK response."""
|
||||
payload = (
|
||||
device_name.encode("utf-8") + b"\x00" +
|
||||
b"\x00" + # Icon type
|
||||
struct.pack("!H", 0) + # Port
|
||||
b"\x00" * 8 # Reserved
|
||||
)
|
||||
packet = self._build_header(PT_HELO, len(payload)) + payload
|
||||
self.sock.sendto(packet, (self.host, self.port))
|
||||
|
||||
try:
|
||||
data, addr = self.sock.recvfrom(1024)
|
||||
if len(data) >= HEADER_SIZE:
|
||||
sig, maj, minv, pkttype, seq, maxseq, plen, token, _ = struct.unpack(
|
||||
HEADER_FORMAT, data[:HEADER_SIZE]
|
||||
)
|
||||
if sig == b"XBMC" and pkttype == PT_PING and token == self.token:
|
||||
return True
|
||||
except socket.timeout:
|
||||
return False
|
||||
return False
|
||||
|
||||
def send_ping(self) -> bool:
|
||||
"""Send PING packet and wait for PING ACK response."""
|
||||
packet = self._build_header(PT_PING, 0)
|
||||
self.sock.sendto(packet, (self.host, self.port))
|
||||
|
||||
try:
|
||||
data, addr = self.sock.recvfrom(1024)
|
||||
if len(data) >= HEADER_SIZE:
|
||||
sig, _, _, pkttype, _, _, _, token, _ = struct.unpack(
|
||||
HEADER_FORMAT, data[:HEADER_SIZE]
|
||||
)
|
||||
if sig == b"XBMC" and pkttype == PT_PING and token == self.token:
|
||||
return True
|
||||
except socket.timeout:
|
||||
return False
|
||||
return False
|
||||
|
||||
def send_button(self, button_name: str, map_name: str = "R1", is_down: bool = True) -> None:
|
||||
"""Send BUTTON packet (e.g. up, down, left, right, select, back)."""
|
||||
flags = PTB_DOWN if is_down else PTB_UP
|
||||
payload = (
|
||||
struct.pack("!HHH", 0, flags, 0) +
|
||||
map_name.encode("utf-8") + b"\x00" +
|
||||
button_name.encode("utf-8") + b"\x00"
|
||||
)
|
||||
packet = self._build_header(PT_BUTTON, len(payload)) + payload
|
||||
self.sock.sendto(packet, (self.host, self.port))
|
||||
|
||||
def send_action(self, action_str: str) -> None:
|
||||
"""Send ACTION packet (e.g. Action(1), Action(7), Up)."""
|
||||
payload = b"\x01" + action_str.encode("utf-8") + b"\x00"
|
||||
packet = self._build_header(PT_ACTION, len(payload)) + payload
|
||||
self.sock.sendto(packet, (self.host, self.port))
|
||||
|
||||
def send_bye(self) -> None:
|
||||
"""Send BYE disconnect packet."""
|
||||
packet = self._build_header(PT_BYE, 0)
|
||||
self.sock.sendto(packet, (self.host, self.port))
|
||||
|
||||
|
||||
def run_automated_tests(host: str, port: int) -> bool:
|
||||
print(f"Testing Kodi EventServer at {host}:{port}...")
|
||||
client = EventServerClient(host, port, timeout=2.0)
|
||||
|
||||
# Test 1: HELO Handshake
|
||||
print(" [1/4] Testing HELO handshake (client registration)...", end=" ", flush=True)
|
||||
if client.send_helo("EventServer Test Suite"):
|
||||
print("OK (Received PING-ACK)")
|
||||
else:
|
||||
print("FAILED (No PING-ACK response received)")
|
||||
print(f" Ensure jellyfin-kiosk is running and listening on UDP {port}")
|
||||
client.close()
|
||||
return False
|
||||
|
||||
# Test 2: PING Heartbeat
|
||||
print(" [2/4] Testing PING heartbeat...", end=" ", flush=True)
|
||||
if client.send_ping():
|
||||
print("OK (Received PING-ACK)")
|
||||
else:
|
||||
print("FAILED")
|
||||
client.close()
|
||||
return False
|
||||
|
||||
# Test 3: Button Events
|
||||
print(" [3/4] Sending navigation buttons (Up, Down, Left, Right, Select, Back)...", end=" ", flush=True)
|
||||
for btn in ["up", "down", "left", "right", "select", "back"]:
|
||||
client.send_button(btn)
|
||||
time.sleep(0.05)
|
||||
print("OK (Packets sent)")
|
||||
|
||||
# Test 4: Action Events
|
||||
print(" [4/4] Sending action strings (Action(1), Action(7))...", end=" ", flush=True)
|
||||
for act in ["Action(1)", "Action(7)"]:
|
||||
client.send_action(act)
|
||||
time.sleep(0.05)
|
||||
print("OK (Packets sent)")
|
||||
|
||||
client.send_bye()
|
||||
client.close()
|
||||
print("\nAll EventServer tests passed successfully!")
|
||||
return True
|
||||
|
||||
|
||||
def run_interactive_mode(client: EventServerClient) -> None:
|
||||
print("\n--- EventServer Interactive Remote Mode ---")
|
||||
print("Controls:")
|
||||
print(" [W] or [Up] : Up")
|
||||
print(" [S] or [Down] : Down")
|
||||
print(" [A] or [Left] : Left")
|
||||
print(" [D] or [Right] : Right")
|
||||
print(" [Enter] / [O] : Select / OK")
|
||||
print(" [B] / [Esc] : Back")
|
||||
print(" [Space] / [P] : Play / Pause")
|
||||
print(" [X] : Stop")
|
||||
print(" [Q] : Quit\n")
|
||||
|
||||
import termios
|
||||
import tty
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
while True:
|
||||
ch = sys.stdin.read(1)
|
||||
if ch == "\x1b": # Escape or arrow sequence
|
||||
# Non-blocking check for arrow keys
|
||||
import select
|
||||
r, _, _ = select.select([sys.stdin], [], [], 0.05)
|
||||
if r:
|
||||
seq = sys.stdin.read(2)
|
||||
if seq == "[A":
|
||||
client.send_button("up")
|
||||
elif seq == "[B":
|
||||
client.send_button("down")
|
||||
elif seq == "[C":
|
||||
client.send_button("right")
|
||||
elif seq == "[D":
|
||||
client.send_button("left")
|
||||
else:
|
||||
client.send_button("back")
|
||||
elif ch in ("q", "Q", "\x03"): # Quit or Ctrl+C
|
||||
break
|
||||
elif ch in ("w", "W"):
|
||||
client.send_button("up")
|
||||
elif ch in ("s", "S"):
|
||||
client.send_button("down")
|
||||
elif ch in ("a", "A"):
|
||||
client.send_button("left")
|
||||
elif ch in ("d", "D"):
|
||||
client.send_button("right")
|
||||
elif ch in ("\r", "\n", "o", "O"):
|
||||
client.send_button("select")
|
||||
elif ch in ("b", "B"):
|
||||
client.send_button("back")
|
||||
elif ch in (" ", "p", "P"):
|
||||
client.send_button("playpause")
|
||||
elif ch in ("x", "X"):
|
||||
client.send_button("stop")
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
print("\nExited interactive mode.")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Kodi EventServer Local Test Client")
|
||||
parser.add_argument("command", nargs="?", default="test",
|
||||
help="Command: test, ping, helo, up, down, left, right, select, back, play, stop, interactive")
|
||||
parser.add_argument("--host", default="127.0.0.1", help="EventServer host (default: 127.0.0.1)")
|
||||
parser.add_argument("--port", type=int, default=9777, help="EventServer UDP port (default: 9777)")
|
||||
|
||||
args = parser.parse_args()
|
||||
cmd = args.command.lower()
|
||||
|
||||
if cmd == "test":
|
||||
success = run_automated_tests(args.host, args.port)
|
||||
return 0 if success else 1
|
||||
|
||||
client = EventServerClient(args.host, args.port)
|
||||
try:
|
||||
if cmd == "ping":
|
||||
print(f"Sending PING to {args.host}:{args.port}...")
|
||||
if client.send_ping():
|
||||
print("Success: Received PING-ACK from EventServer!")
|
||||
else:
|
||||
print("Failed: No response (timeout)")
|
||||
return 1
|
||||
elif cmd == "helo":
|
||||
print(f"Sending HELO to {args.host}:{args.port}...")
|
||||
if client.send_helo():
|
||||
print("Success: EventServer accepted HELO and replied with PING-ACK!")
|
||||
else:
|
||||
print("Failed: No response (timeout)")
|
||||
return 1
|
||||
elif cmd in ("up", "down", "left", "right", "select", "back", "play", "pause", "playpause", "stop"):
|
||||
client.send_button(cmd)
|
||||
print(f"Sent button '{cmd}' to {args.host}:{args.port}")
|
||||
elif cmd == "interactive":
|
||||
run_interactive_mode(client)
|
||||
else:
|
||||
print(f"Unknown command: {cmd}")
|
||||
return 1
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user