#!/bin/bash # Test TV power intent MQTT message flow (Phase 1 contract v1) # Requires: mosquitto_pub / mosquitto_sub set -euo pipefail SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # ── Load .env ──────────────────────────────────────────────────────────────── ENV_FILE="$PROJECT_ROOT/.env" if [ -f "$ENV_FILE" ]; then # Strip inline comments and surrounding whitespace before export. while IFS='=' read -r key value; do key="${key//[$'\t\r\n']}" key="$(echo "$key" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" # Skip comments/empty lines/invalid keys [[ -z "$key" ]] && continue [[ "$key" =~ ^# ]] && continue [[ "$key" =~ ^[A-Z_][A-Z0-9_]*$ ]] || continue value="${value%%#*}" # strip inline comments value="${value//[$'\t\r\n']}" value="$(echo "$value" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" export "$key=$value" done < "$ENV_FILE" fi BROKER="${MQTT_BROKER:-localhost}" PORT="${MQTT_PORT:-1883}" MQTT_USERNAME="${MQTT_USERNAME:-}" MQTT_PASSWORD="${MQTT_PASSWORD:-}" MQTT_TLS_ENABLED="${MQTT_TLS_ENABLED:-0}" MQTT_TLS_CA_CERT="${MQTT_TLS_CA_CERT:-}" MQTT_TLS_CERT="${MQTT_TLS_CERT:-}" MQTT_TLS_KEY="${MQTT_TLS_KEY:-}" MQTT_TLS_INSECURE="${MQTT_TLS_INSECURE:-0}" MQTT_AUTH_ARGS=() MQTT_TLS_ARGS=() if [[ -n "$MQTT_USERNAME" ]]; then MQTT_AUTH_ARGS+=( -u "$MQTT_USERNAME" ) fi if [[ -n "$MQTT_PASSWORD" ]]; then MQTT_AUTH_ARGS+=( -P "$MQTT_PASSWORD" ) fi if [[ "$MQTT_TLS_ENABLED" == "1" || "$MQTT_TLS_ENABLED" == "true" || "$MQTT_TLS_ENABLED" == "yes" ]]; then [[ -n "$MQTT_TLS_CA_CERT" ]] && MQTT_TLS_ARGS+=( --cafile "$MQTT_TLS_CA_CERT" ) [[ -n "$MQTT_TLS_CERT" ]] && MQTT_TLS_ARGS+=( --cert "$MQTT_TLS_CERT" ) [[ -n "$MQTT_TLS_KEY" ]] && MQTT_TLS_ARGS+=( --key "$MQTT_TLS_KEY" ) if [[ "$MQTT_TLS_INSECURE" == "1" || "$MQTT_TLS_INSECURE" == "true" || "$MQTT_TLS_INSECURE" == "yes" ]]; then MQTT_TLS_ARGS+=( --insecure ) fi fi # ── Read runtime IDs ───────────────────────────────────────────────────────── GROUP_ID_FILE="$PROJECT_ROOT/src/config/last_group_id.txt" CLIENT_UUID_FILE="$PROJECT_ROOT/src/config/client_uuid.txt" GROUP_ID="" CLIENT_UUID="" [ -f "$GROUP_ID_FILE" ] && GROUP_ID="$(cat "$GROUP_ID_FILE" 2>/dev/null | tr -d '[:space:]')" [ -f "$CLIENT_UUID_FILE" ] && CLIENT_UUID="$(cat "$CLIENT_UUID_FILE" 2>/dev/null | tr -d '[:space:]')" echo -e "${BLUE}================================================${NC}" echo -e "${BLUE}TV Power Intent Test (Phase 1 Contract)${NC}" echo -e "${BLUE}================================================${NC}" echo " Broker : $BROKER:$PORT" echo " Group : ${GROUP_ID:-}" echo " Client : ${CLIENT_UUID:-}" echo "" # ── Check tools ────────────────────────────────────────────────────────────── if ! command -v mosquitto_pub &>/dev/null; then echo -e "${RED}mosquitto_pub not found. Install with: sudo apt-get install mosquitto-clients${NC}" exit 1 fi # ── Helpers ────────────────────────────────────────────────────────────────── now_iso() { date -u +"%Y-%m-%dT%H:%M:%S.000Z"; } # expires_at = now + expires_iso() { local secs="${1:-90}" date -u -d "@$(( $(date +%s) + secs ))" +"%Y-%m-%dT%H:%M:%S.000Z" } group_topic() { echo "infoscreen/groups/${GROUP_ID}/power/intent" } publish_intent() { local state="$1" local reason="$2" local issued="${3:-$(now_iso)}" local expires="${4:-$(expires_iso 90)}" local intent_id intent_id="$(python3 -c 'import uuid; print(uuid.uuid4())')" local topic topic="$(group_topic)" if [ -z "$GROUP_ID" ]; then echo -e "${RED}No group_id found. Subscribe a client and assign a group first.${NC}" return 1 fi local payload payload=$(cat </dev/null || echo "$payload" echo "" mosquitto_pub -h "$BROKER" -p "$PORT" "${MQTT_AUTH_ARGS[@]}" "${MQTT_TLS_ARGS[@]}" -t "$topic" -q 1 --retain -m "$payload" echo -e "${GREEN}Published (retained, QoS 1)${NC}" echo "intent_id: $intent_id" } clear_intent() { local topic topic="$(group_topic)" if [ -z "$GROUP_ID" ]; then echo -e "${RED}No group_id found.${NC}" return 1 fi mosquitto_pub -h "$BROKER" -p "$PORT" "${MQTT_AUTH_ARGS[@]}" "${MQTT_TLS_ARGS[@]}" -t "$topic" -q 1 --retain --null-message echo -e "${GREEN}Retained intent cleared from broker${NC}" } show_state_files() { echo "" local intent_file="$PROJECT_ROOT/src/power_intent_state.json" local state_file="$PROJECT_ROOT/src/power_state.json" local health_file="$PROJECT_ROOT/src/current_process_health.json" for f in "$intent_file" "$state_file" "$health_file"; do local label label="$(basename "$f")" if [ -f "$f" ]; then echo -e "${BLUE}── $label ──────────────────${NC}" python3 -m json.tool "$f" 2>/dev/null || cat "$f" else echo -e "${YELLOW}$label not found${NC}" fi echo "" done } watch_logs() { echo -e "${YELLOW}Following power-related log entries (Ctrl-C to stop)...${NC}" local dm_log="$PROJECT_ROOT/logs/display_manager.log" local sc_log="$PROJECT_ROOT/src/simclient.log" if [ -f "$dm_log" ] && [ -f "$sc_log" ]; then tail -f "$dm_log" "$sc_log" | grep --line-buffered -i \ -E "(power|intent|cec|turn|desired_state|mqtt_intent|local_fallback|POWER)" elif [ -f "$dm_log" ]; then tail -f "$dm_log" | grep --line-buffered -i \ -E "(power|intent|cec|turn|desired_state|mqtt_intent|local_fallback|POWER)" else echo -e "${RED}No log files found. Have both processes run at least once?${NC}" fi } subscribe_power_state() { if [ -z "$CLIENT_UUID" ]; then echo -e "${RED}No client_uuid found.${NC}" return 1 fi local topic="infoscreen/${CLIENT_UUID}/power/state" echo -e "${YELLOW}Subscribing to: $topic${NC}" echo "(Ctrl-C to stop)" echo "" mosquitto_sub -h "$BROKER" -p "$PORT" "${MQTT_AUTH_ARGS[@]}" "${MQTT_TLS_ARGS[@]}" -t "$topic" | \ python3 -c " import sys, json for line in sys.stdin: line = line.strip() if line: try: print(json.dumps(json.loads(line), indent=2)) except Exception: print(line) " } # ── Menu ───────────────────────────────────────────────────────────────────── while true; do echo -e "${BLUE}================================================${NC}" echo "Choose a test:" echo " 1) Publish ON intent (valid 90s, group ${GROUP_ID:-?})" echo " 2) Publish OFF intent (valid 90s, group ${GROUP_ID:-?})" echo " 3) Publish stale intent (already expired) — expect rejection" echo " 4) Publish malformed intent (missing fields) — expect rejection" echo " 5) Clear retained intent from broker (sends empty retained payload)" echo " 6) Show power_intent_state / power_state / health JSON files" echo " 7) Follow power-related log entries (display_manager + simclient)" echo " 8) Subscribe to infoscreen/{client}/power/state topic" echo " q) Quit" echo "" read -rp "Enter choice: " choice echo "" case "$choice" in 1) publish_intent "on" "active_event" ;; 2) publish_intent "off" "no_active_event" ;; 3) # issued and expired both in the past STALE_ISSUED=$(date -u -d '5 minutes ago' +"%Y-%m-%dT%H:%M:%S.000Z") STALE_EXPIRES=$(date -u -d '2 minutes ago' +"%Y-%m-%dT%H:%M:%S.000Z") publish_intent "on" "active_event" "$STALE_ISSUED" "$STALE_EXPIRES" echo -e "${YELLOW}⚠ This intent is expired - client must reject it and show 'intent expired' in log${NC}" ;; 4) if [ -z "$GROUP_ID" ]; then echo -e "${RED}No group_id.${NC}" else TOPIC="$(group_topic)" mosquitto_pub -h "$BROKER" -p "$PORT" "${MQTT_AUTH_ARGS[@]}" "${MQTT_TLS_ARGS[@]}" -t "$TOPIC" -q 1 --retain \ -m '{"schema_version":"1.0","desired_state":"on"}' echo -e "${YELLOW}⚠ Malformed intent published - client must reject with 'missing required field' in log${NC}" fi ;; 5) clear_intent ;; 6) show_state_files read -rp "Press Enter to continue..." ;; 7) watch_logs ;; 8) subscribe_power_state ;; q|Q) echo "Exiting." exit 0 ;; *) echo -e "${RED}Invalid choice${NC}" ;; esac echo "" done