test: characterize contracts and make startup explicit
This commit is contained in:
+73
-47
@@ -10,27 +10,28 @@ from typing import Dict, List, Optional
|
||||
import yaml
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from controller.config import load_config
|
||||
|
||||
# --- Config / paths ---
|
||||
|
||||
MEDIA_BASE = Path(os.environ.get("YTPLAYER_MEDIA_BASE", "/mnt/Media/YTPlayer"))
|
||||
DATA_BASE = Path(os.environ.get("YTPLAYER_DATA_BASE", "/controller-data"))
|
||||
CONFIG = load_config()
|
||||
|
||||
MEDIA_BASE = CONFIG.storage.media_base
|
||||
DATA_BASE = CONFIG.storage.data_base
|
||||
STATE_PATH = DATA_BASE / "playlists.yaml"
|
||||
|
||||
MAX_SLOTS = int(os.environ.get("YTPLAYER_MAX_SLOTS", "10"))
|
||||
RETENTION_SECONDS = int(os.environ.get("YTPLAYER_RETENTION_SECONDS", str(48 * 3600)))
|
||||
MAX_SLOTS = len(CONFIG.slot_ids)
|
||||
RETENTION_SECONDS = CONFIG.storage.retention_seconds
|
||||
|
||||
MQTT_HOST = os.environ.get("MQTT_HOST", "10.40.0.2")
|
||||
MQTT_PORT = int(os.environ.get("MQTT_PORT", "1883"))
|
||||
MQTT_CLIENT_ID = os.environ.get("MQTT_CLIENT_ID", "yt-controller")
|
||||
MQTT_HOST = CONFIG.mqtt.host
|
||||
MQTT_PORT = CONFIG.mqtt.port
|
||||
MQTT_CLIENT_ID = CONFIG.mqtt.client_id
|
||||
|
||||
TOPIC_INGEST_PROGRESS = "youtube/ingest/progress"
|
||||
TOPIC_PLAYBACK_BASE = "youtube/media/playback" # e.g. youtube/media/playback/1/title
|
||||
TOPIC_PLAYER_STATUS = "youtube/player/status" # JSON from player_service
|
||||
TOPIC_PLAYER_CMD = "youtube/player/cmd" # commands to player_service
|
||||
TOPIC_CONTROLLER_CMD = "youtube/controller/cmd" # commands from HA / StreamDeck
|
||||
|
||||
MEDIA_BASE.mkdir(parents=True, exist_ok=True)
|
||||
DATA_BASE.mkdir(parents=True, exist_ok=True)
|
||||
TOPIC_INGEST_PROGRESS = CONFIG.topics.ingest_progress
|
||||
TOPIC_PLAYBACK_BASE = CONFIG.topics.playback_base
|
||||
TOPIC_PLAYER_STATUS = CONFIG.topics.player_status
|
||||
TOPIC_PLAYER_CMD = CONFIG.topics.player_command
|
||||
TOPIC_CONTROLLER_CMD = CONFIG.topics.controller_command
|
||||
|
||||
# --- MQTT client ---
|
||||
|
||||
@@ -43,6 +44,9 @@ def _on_connect(client, userdata, flags, rc, properties=None):
|
||||
client.subscribe(TOPIC_PLAYER_STATUS, qos=1)
|
||||
client.subscribe(TOPIC_CONTROLLER_CMD, qos=1)
|
||||
print(f"[controller] subscribed to {TOPIC_PLAYER_STATUS} and {TOPIC_CONTROLLER_CMD}")
|
||||
if CONFIG.homeassistant.enabled:
|
||||
ha_discovery_publish()
|
||||
reannounce_slots()
|
||||
else:
|
||||
print(f"[mqtt] connect failed rc={rc}")
|
||||
|
||||
@@ -111,26 +115,9 @@ def _handle_control_cmd(payload: str):
|
||||
def _on_disconnect(client, userdata, rc, properties=None):
|
||||
print(f"[mqtt] disconnected rc={rc}")
|
||||
|
||||
mqtt_client = mqtt.Client(client_id=MQTT_CLIENT_ID, clean_session=True)
|
||||
|
||||
# If your broker ever needs auth, you'd uncomment:
|
||||
# mqtt_client.username_pw_set(os.environ.get("MQTT_USERNAME"), os.environ.get("MQTT_PASSWORD"))
|
||||
|
||||
mqtt_client.enable_logger() # send Paho logs to Python logging
|
||||
mqtt_client.on_connect = _on_connect
|
||||
mqtt_client.on_disconnect = _on_disconnect
|
||||
mqtt_client.on_message = _on_message
|
||||
|
||||
print(f"[controller] attempting MQTT connect to {MQTT_HOST}:{MQTT_PORT} …")
|
||||
mqtt_client.connect(MQTT_HOST, MQTT_PORT, keepalive=30)
|
||||
|
||||
mqtt_client.loop_start()
|
||||
print("[controller] MQTT loop started")
|
||||
|
||||
mqtt_client.on_message = _on_message
|
||||
mqtt_client.subscribe(TOPIC_PLAYER_STATUS, qos=1)
|
||||
mqtt_client.subscribe(TOPIC_CONTROLLER_CMD, qos=1)
|
||||
print(f"[controller] subscribed to {TOPIC_PLAYER_STATUS} and {TOPIC_CONTROLLER_CMD}")
|
||||
mqtt_client = None
|
||||
mqtt_started = False
|
||||
mqtt_lifecycle_lock = threading.Lock()
|
||||
|
||||
|
||||
def mqtt_publish(topic: str, payload, **kwargs):
|
||||
@@ -141,7 +128,10 @@ def mqtt_publish(topic: str, payload, **kwargs):
|
||||
if not isinstance(payload, str):
|
||||
payload = json.dumps(payload)
|
||||
print(f"[mqtt] publish {topic} -> {payload}")
|
||||
mqtt_client.publish(topic, payload, **kwargs)
|
||||
if mqtt_client is None:
|
||||
print(f"[mqtt] publish skipped before Controller startup: {topic}")
|
||||
return None
|
||||
return mqtt_client.publish(topic, payload, **kwargs)
|
||||
|
||||
|
||||
# --- State model ---
|
||||
@@ -282,8 +272,6 @@ class ControllerState:
|
||||
|
||||
|
||||
state = ControllerState()
|
||||
state.load()
|
||||
state.prune_expired()
|
||||
|
||||
|
||||
# --- Helpers ---
|
||||
@@ -698,7 +686,7 @@ def handle_play_request(cmd: Dict) -> None:
|
||||
|
||||
# --- HA Discovery ---
|
||||
|
||||
HA_DISCOVERY_PREFIX = os.environ.get("HA_DISCOVERY_PREFIX", "homeassistant")
|
||||
HA_DISCOVERY_PREFIX = CONFIG.homeassistant.discovery_prefix
|
||||
HA_DEVICE = {
|
||||
"identifiers": ["ytplayer-controller"],
|
||||
"name": "YouTube Controller",
|
||||
@@ -826,13 +814,51 @@ def enqueue_download(url: str, submitter: Optional[str], note: Optional[str]) ->
|
||||
}
|
||||
|
||||
|
||||
# --- Post-define initialization that needs functions above ---
|
||||
# --- Explicit lifecycle (imports are intentionally side-effect free) ---
|
||||
|
||||
print("[controller] running HA discovery publish…")
|
||||
ha_discovery_publish()
|
||||
print("[controller] ha_discovery_publish() done")
|
||||
def start_controller() -> None:
|
||||
"""Initialize storage and start non-blocking MQTT retry behavior."""
|
||||
global mqtt_client, mqtt_started
|
||||
with mqtt_lifecycle_lock:
|
||||
if mqtt_started:
|
||||
return
|
||||
|
||||
# Re-announce any existing slots so HA / Deck see them after a restart
|
||||
for s, slot in state.slots.items():
|
||||
publish_slot_topics(slot)
|
||||
print("[controller] re-published slot states")
|
||||
MEDIA_BASE.mkdir(parents=True, exist_ok=True)
|
||||
DATA_BASE.mkdir(parents=True, exist_ok=True)
|
||||
state.load()
|
||||
|
||||
mqtt_client = mqtt.Client(client_id=MQTT_CLIENT_ID, clean_session=True)
|
||||
if CONFIG.mqtt.username is not None:
|
||||
mqtt_client.username_pw_set(CONFIG.mqtt.username, CONFIG.mqtt.password)
|
||||
mqtt_client.enable_logger()
|
||||
mqtt_client.on_connect = _on_connect
|
||||
mqtt_client.on_disconnect = _on_disconnect
|
||||
mqtt_client.on_message = _on_message
|
||||
mqtt_client.reconnect_delay_set(
|
||||
min_delay=CONFIG.mqtt.reconnect_min_seconds,
|
||||
max_delay=CONFIG.mqtt.reconnect_max_seconds,
|
||||
)
|
||||
|
||||
print(f"[controller] attempting asynchronous MQTT connect to {MQTT_HOST}:{MQTT_PORT} …")
|
||||
mqtt_client.connect_async(
|
||||
MQTT_HOST,
|
||||
MQTT_PORT,
|
||||
keepalive=CONFIG.mqtt.keepalive_seconds,
|
||||
)
|
||||
mqtt_client.loop_start()
|
||||
mqtt_started = True
|
||||
print("[controller] MQTT loop started")
|
||||
|
||||
|
||||
def stop_controller() -> None:
|
||||
"""Stop the MQTT client if explicit startup occurred."""
|
||||
global mqtt_client, mqtt_started
|
||||
with mqtt_lifecycle_lock:
|
||||
if not mqtt_started or mqtt_client is None:
|
||||
return
|
||||
try:
|
||||
mqtt_client.disconnect()
|
||||
finally:
|
||||
mqtt_client.loop_stop()
|
||||
mqtt_client = None
|
||||
mqtt_started = False
|
||||
|
||||
Reference in New Issue
Block a user