eerste versie

This commit is contained in:
bart
2026-11-03 22:21:03 +01:00
parent 8a08620866
commit 99cbc00357
33 changed files with 11157 additions and 2163 deletions
+290
View File
@@ -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())