J. Rogers, SE Ohio
Overview
This guide describes how to build a plugin-based universal TV remote that automatically discovers and supports new TV brands by simply adding controller files to a directory.
Architecture Principles
- Plugin-based: Each TV brand is a separate controller module
- Auto-discovery: New controllers are automatically detected and loaded
- Standardized interface: All controllers implement the same command set
- Dynamic routing: Flask routes adapt to available controllers
- Zero-configuration expansion: Drop in a new controller file → it just works
Directory Structure
universal_remote/
├── app.py # Main Flask application
├── config.json # TV configuration file
├── controllers/ # TV controller plugins (auto-discovered)
│ ├── __init__.py # Controller loader
│ ├── base.py # Base controller interface
│ ├── vizio.py # Vizio implementation
│ ├── samsung.py # Samsung implementation
│ ├── lg.py # LG implementation (future)
│ └── sony.py # Sony implementation (future)
├── templates/
│ └── remote.html # Web interface
└── requirements.txt
Step 1: Base Controller Interface
File: controllers/base.py
This defines the contract that ALL controllers must implement.
from abc import ABC, abstractmethod
class TVController(ABC):
"""
Base class for all TV controllers.
All TV brands must implement these methods.
"""
def __init__(self, config):
"""
Initialize controller with TV configuration
Args:
config (dict): TV configuration containing:
- ip: TV IP address
- mac: MAC address (optional, for WoL)
- brand-specific keys (auth_token, api_key, etc.)
"""
self.config = config
self.ip = config['ip']
self.mac = config.get('mac')
# ============ POWER COMMANDS ============
@abstractmethod
def power_on(self):
"""Turn TV on. Returns True if successful."""
pass
@abstractmethod
def power_off(self):
"""Turn TV off. Returns True if successful."""
pass
@abstractmethod
def power_toggle(self):
"""Toggle power state. Returns True if successful."""
pass
# ============ VOLUME COMMANDS ============
@abstractmethod
def volume_up(self):
"""Increase volume. Returns True if successful."""
pass
@abstractmethod
def volume_down(self):
"""Decrease volume. Returns True if successful."""
pass
@abstractmethod
def mute(self):
"""Toggle mute. Returns True if successful."""
pass
@abstractmethod
def set_volume(self, level):
"""
Set volume to specific level (0-100)
Returns True if successful.
"""
pass
# ============ CHANNEL COMMANDS ============
@abstractmethod
def channel_up(self):
"""Next channel. Returns True if successful."""
pass
@abstractmethod
def channel_down(self):
"""Previous channel. Returns True if successful."""
pass
# ============ NAVIGATION COMMANDS ============
@abstractmethod
def key_up(self):
"""Navigate up. Returns True if successful."""
pass
@abstractmethod
def key_down(self):
"""Navigate down. Returns True if successful."""
pass
@abstractmethod
def key_left(self):
"""Navigate left. Returns True if successful."""
pass
@abstractmethod
def key_right(self):
"""Navigate right. Returns True if successful."""
pass
@abstractmethod
def key_ok(self):
"""OK/Select button. Returns True if successful."""
pass
@abstractmethod
def key_back(self):
"""Back button. Returns True if successful."""
pass
@abstractmethod
def key_home(self):
"""Home button. Returns True if successful."""
pass
@abstractmethod
def key_menu(self):
"""Menu button. Returns True if successful."""
pass
@abstractmethod
def key_exit(self):
"""Exit button. Returns True if successful."""
pass
@abstractmethod
def key_info(self):
"""Info button. Returns True if successful."""
pass
# ============ INPUT/APP COMMANDS ============
@abstractmethod
def get_inputs_list(self):
"""
Get list of available inputs.
Returns list of dicts: [{"name": "HDMI1", "id": "hdmi1"}, ...]
"""
pass
@abstractmethod
def set_input(self, input_name):
"""
Switch to input by name.
Returns True if successful.
"""
pass
@abstractmethod
def get_apps_list(self):
"""
Get list of available apps.
Returns list of app names: ["Netflix", "YouTube", ...]
"""
pass
@abstractmethod
def launch_app(self, app_name):
"""
Launch app by name.
Returns True if successful.
"""
pass
# ============ OPTIONAL: BRAND-SPECIFIC COMMANDS ============
def get_capabilities(self):
"""
Return dict of supported features.
Override to indicate unsupported features.
"""
return {
"power": True,
"volume": True,
"channels": True,
"navigation": True,
"inputs": True,
"apps": True,
}
Step 2: Auto-Discovery Controller Loader
File: controllers/__init__.py
This automatically finds and loads all controller modules.
import os
import importlib
import inspect
from .base import TVController
def discover_controllers():
"""
Automatically discover all TV controller classes in this directory.
Returns dict: {"vizio": VizioController, "samsung": SamsungController, ...}
"""
controllers = {}
# Get directory of this file
controllers_dir = os.path.dirname(__file__)
# Find all .py files except __init__ and base
for filename in os.listdir(controllers_dir):
if not filename.endswith('.py'):
continue
if filename in ['__init__.py', 'base.py']:
continue
# Module name is filename without .py
module_name = filename[:-3]
try:
# Import the module
module = importlib.import_module(f'.{module_name}', package='controllers')
# Find all classes in the module that inherit from TVController
for name, obj in inspect.getmembers(module, inspect.isclass):
if issubclass(obj, TVController) and obj is not TVController:
# Use module name as brand identifier
controllers[module_name] = obj
print(f"✓ Loaded controller: {module_name} ({name})")
except Exception as e:
print(f"✗ Failed to load controller {module_name}: {e}")
return controllers
# Auto-load all controllers when this module is imported
CONTROLLERS = discover_controllers()
def get_controller(brand, config):
"""
Get controller instance for a specific TV brand.
Args:
brand (str): TV brand name (e.g., "vizio", "samsung")
config (dict): TV configuration
Returns:
TVController instance
Raises:
ValueError if brand not supported
"""
if brand not in CONTROLLERS:
available = ', '.join(CONTROLLERS.keys())
raise ValueError(f"Unsupported brand '{brand}'. Available: {available}")
controller_class = CONTROLLERS[brand]
return controller_class(config)
def get_supported_brands():
"""Return list of supported TV brands."""
return list(CONTROLLERS.keys())
Step 3: Implement Vizio Controller
File: controllers/vizio.py
from .base import TVController
from vizio_control import VizioTV
class VizioController(TVController):
"""Vizio TV implementation using SmartCast API"""
def __init__(self, config):
super().__init__(config)
if 'auth_token' not in config:
raise ValueError("Vizio requires 'auth_token' in config")
self.tv = VizioTV(
config['ip'],
config['auth_token'],
config.get('mac')
)
def power_on(self):
return self.tv.power_on()
def power_off(self):
return self.tv.power_off()
def power_toggle(self):
return self.tv.power_toggle()
def volume_up(self):
return self.tv.volume_up()
def volume_down(self):
return self.tv.volume_down()
def mute(self):
return self.tv.mute()
def set_volume(self, level):
# Vizio doesn't support direct volume set
return False
def channel_up(self):
return self.tv.channel_up()
def channel_down(self):
return self.tv.channel_down()
def key_up(self):
return self.tv.key_up()
def key_down(self):
return self.tv.key_down()
def key_left(self):
return self.tv.key_left()
def key_right(self):
return self.tv.key_right()
def key_ok(self):
return self.tv.key_ok()
def key_back(self):
return self.tv.key_back()
def key_home(self):
return self.tv.key_home()
def key_menu(self):
return self.tv.key_menu()
def key_exit(self):
return self.tv.key_exit()
def key_info(self):
return self.tv.key_info()
def get_inputs_list(self):
inputs = self.tv.get_inputs_list()
if not inputs:
return []
return [
{
"name": inp.get('CNAME', ''),
"id": inp.get('CNAME', '').lower()
}
for inp in inputs
]
def set_input(self, input_name):
return self.tv.set_input(input_name)
def get_apps_list(self):
return self.tv.list_available_apps()
def launch_app(self, app_name):
return self.tv.launch_app(app_name)
def get_capabilities(self):
return {
"power": True,
"volume": True,
"channels": True,
"navigation": True,
"inputs": True,
"apps": True,
"volume_set": False, # Vizio doesn't support direct volume
}
Step 4: Implement Samsung Controller
File: controllers/samsung.py
Samsung TVs use two different protocols depending on age:
Option A: Legacy Samsung (2014-2015) - Port 55000
from .base import TVController
import socket
import base64
import time
class SamsungController(TVController):
"""Samsung TV implementation (Legacy protocol, pre-2016)"""
# Samsung key codes
KEYS = {
'power': 'KEY_POWER',
'vol_up': 'KEY_VOLUP',
'vol_down': 'KEY_VOLDOWN',
'mute': 'KEY_MUTE',
'ch_up': 'KEY_CHUP',
'ch_down': 'KEY_CHDOWN',
'up': 'KEY_UP',
'down': 'KEY_DOWN',
'left': 'KEY_LEFT',
'right': 'KEY_RIGHT',
'ok': 'KEY_ENTER',
'back': 'KEY_RETURN',
'home': 'KEY_HOME',
'menu': 'KEY_MENU',
'exit': 'KEY_EXIT',
'info': 'KEY_INFO',
}
def __init__(self, config):
super().__init__(config)
self.port = config.get('port', 55000)
self.remote_name = config.get('remote_name', 'PythonRemote')
def _send_key(self, key):
"""Send key command to Samsung TV"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(3)
sock.connect((self.ip, self.port))
# Encode remote name
remote_b64 = base64.b64encode(self.remote_name.encode()).decode()
# Authentication message
auth_msg = (
f"\x00\x13\x00{chr(len(remote_b64))}\x00{remote_b64}"
f"\x10\x00\x07\x00localhost"
)
sock.send(auth_msg.encode('latin1'))
# Key command message
key_b64 = base64.b64encode(key.encode()).decode()
key_msg = (
f"\x00\x13\x00{chr(len(remote_b64))}\x00{remote_b64}"
f"\x00\x00\x00{chr(len(key_b64))}\x00{key_b64}"
)
sock.send(key_msg.encode('latin1'))
sock.close()
return True
except Exception as e:
print(f"Samsung key send failed: {e}")
return False
def power_on(self):
# Samsung legacy doesn't support network power on
# Use Wake-on-LAN if MAC is available
if self.mac:
return self._send_wol()
return False
def power_off(self):
return self._send_key(self.KEYS['power'])
def power_toggle(self):
return self._send_key(self.KEYS['power'])
def volume_up(self):
return self._send_key(self.KEYS['vol_up'])
def volume_down(self):
return self._send_key(self.KEYS['vol_down'])
def mute(self):
return self._send_key(self.KEYS['mute'])
def set_volume(self, level):
# Not supported on legacy Samsung
return False
def channel_up(self):
return self._send_key(self.KEYS['ch_up'])
def channel_down(self):
return self._send_key(self.KEYS['ch_down'])
def key_up(self):
return self._send_key(self.KEYS['up'])
def key_down(self):
return self._send_key(self.KEYS['down'])
def key_left(self):
return self._send_key(self.KEYS['left'])
def key_right(self):
return self._send_key(self.KEYS['right'])
def key_ok(self):
return self._send_key(self.KEYS['ok'])
def key_back(self):
return self._send_key(self.KEYS['back'])
def key_home(self):
return self._send_key(self.KEYS['home'])
def key_menu(self):
return self._send_key(self.KEYS['menu'])
def key_exit(self):
return self._send_key(self.KEYS['exit'])
def key_info(self):
return self._send_key(self.KEYS['info'])
def get_inputs_list(self):
# Legacy Samsung doesn't provide input list via API
return [
{"name": "HDMI1", "id": "hdmi1"},
{"name": "HDMI2", "id": "hdmi2"},
{"name": "HDMI3", "id": "hdmi3"},
]
def set_input(self, input_name):
# Send HDMI key codes
input_map = {
'hdmi1': 'KEY_HDMI1',
'hdmi2': 'KEY_HDMI2',
'hdmi3': 'KEY_HDMI3',
}
key = input_map.get(input_name.lower())
if key:
return self._send_key(key)
return False
def get_apps_list(self):
# Legacy Samsung doesn't support app listing
return []
def launch_app(self, app_name):
# Not supported
return False
def _send_wol(self):
"""Send Wake-on-LAN magic packet"""
if not self.mac:
return False
try:
mac = self.mac.replace(':', '').replace('-', '')
data = 'FF' * 6 + mac * 16
packet = bytes.fromhex(data)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.sendto(packet, ('255.255.255.255', 9))
sock.close()
time.sleep(2) # Wait for TV to wake
return True
except:
return False
def get_capabilities(self):
return {
"power": True,
"volume": True,
"channels": True,
"navigation": True,
"inputs": True,
"apps": False,
"volume_set": False,
}
Option B: Modern Samsung (2016+) - WebSocket API
For modern Samsung TVs, you would use the samsungtvws library:
# pip install samsungtvws
from .base import TVController
from samsungtvws import SamsungTVWS
class SamsungController(TVController):
"""Samsung TV implementation (2016+ WebSocket API)"""
def __init__(self, config):
super().__init__(config)
# Modern Samsung uses WebSocket on port 8002
self.tv = SamsungTVWS(
host=self.ip,
port=config.get('port', 8002),
token_file=config.get('token_file', '/tmp/samsung_token.txt'),
name=config.get('remote_name', 'PythonRemote')
)
def power_on(self):
# Modern Samsung supports WoL
if self.mac:
return self._send_wol()
return False
def power_off(self):
self.tv.send_key('KEY_POWER')
return True
def power_toggle(self):
self.tv.send_key('KEY_POWER')
return True
def volume_up(self):
self.tv.send_key('KEY_VOLUP')
return True
# ... implement all other methods similarly
def get_apps_list(self):
try:
apps = self.tv.app_list()
return [app['name'] for app in apps]
except:
return []
def launch_app(self, app_name):
try:
apps = self.tv.app_list()
for app in apps:
if app['name'].lower() == app_name.lower():
self.tv.run_app(app['appId'])
return True
except:
pass
return False
Step 5: Configuration File
File: config.json
{
"tvs": [
{
"id": "living_room",
"name": "Living Room TV",
"brand": "vizio",
"ip": "192.168.1.100",
"auth_token": "Zjkxxxxxxx",
"mac": "e8:38:a0:26:74:51"
},
{
"id": "bedroom",
"name": "Bedroom TV",
"brand": "samsung",
"ip": "192.168.1.101",
"mac": "aa:bb:cc:dd:ee:ff",
"port": 55000,
"remote_name": "MyRemote"
},
{
"id": "basement",
"name": "Basement TV",
"brand": "lg",
"ip": "192.168.1.102",
"mac": "11:22:33:44:55:66"
}
]
}
Step 6: Flask Application with Auto-Discovery
File: app.py
from flask import Flask, render_template, jsonify, request
import json
from controllers import get_controller, get_supported_brands
app = Flask(__name__)
# Load configuration
def load_config():
with open('config.json', 'r') as f:
return json.load(f)
config = load_config()
tvs = {tv['id']: tv for tv in config['tvs']}
# Create controller instances
controllers = {}
for tv_id, tv_config in tvs.items():
try:
controllers[tv_id] = get_controller(tv_config['brand'], tv_config)
print(f"✓ Initialized {tv_config['name']} ({tv_config['brand']})")
except Exception as e:
print(f"✗ Failed to initialize {tv_id}: {e}")
@app.route('/')
def index():
return render_template('remote.html', tvs=tvs)
@app.route('/api/tvs', methods=['GET'])
def get_tvs():
"""List all configured TVs"""
tv_list = [
{
"id": tv_id,
"name": tv_config['name'],
"brand": tv_config['brand']
}
for tv_id, tv_config in tvs.items()
]
return jsonify({"success": True, "tvs": tv_list})
@app.route('/api/brands', methods=['GET'])
def get_brands():
"""List all supported TV brands"""
return jsonify({
"success": True,
"brands": get_supported_brands()
})
@app.route('/api/<tv_id>/command/<command>', methods=['POST'])
def execute_command(tv_id, command):
"""Execute a command on a specific TV"""
if tv_id not in controllers:
return jsonify({"success": False, "message": f"TV '{tv_id}' not found"})
controller = controllers[tv_id]
try:
# Map command to controller method
command_map = {
'toggle': controller.power_toggle,
'on': controller.power_on,
'off': controller.power_off,
'vol_up': controller.volume_up,
'vol_down': controller.volume_down,
'mute': controller.mute,
'ch_up': controller.channel_up,
'ch_down': controller.channel_down,
'up': controller.key_up,
'down': controller.key_down,
'left': controller.key_left,
'right': controller.key_right,
'ok': controller.key_ok,
'back': controller.key_back,
'exit': controller.key_exit,
'menu': controller.key_menu,
'home': controller.key_home,
'info': controller.key_info,
}
if command not in command_map:
return jsonify({"success": False, "message": f"Unknown command: {command}"})
success = command_map[command]()
if success:
return jsonify({"success": True, "message": f"{command.upper()}"})
else:
return jsonify({"success": False, "message": f"Failed: {command}"})
except Exception as e:
return jsonify({"success": False, "message": str(e)})
@app.route('/api/<tv_id>/inputs', methods=['GET'])
def get_inputs(tv_id):
"""Get available inputs for a TV"""
if tv_id not in controllers:
return jsonify({"success": False, "message": f"TV '{tv_id}' not found"})
try:
inputs = controllers[tv_id].get_inputs_list()
return jsonify({"success": True, "inputs": inputs})
except Exception as e:
return jsonify({"success": False, "message": str(e)})
@app.route('/api/<tv_id>/apps', methods=['GET'])
def get_apps(tv_id):
"""Get available apps for a TV"""
if tv_id not in controllers:
return jsonify({"success": False, "message": f"TV '{tv_id}' not found"})
try:
apps = controllers[tv_id].get_apps_list()
return jsonify({"success": True, "apps": apps})
except Exception as e:
return jsonify({"success": False, "message": str(e)})
@app.route('/api/<tv_id>/input/<input_name>', methods=['POST'])
def set_input(tv_id, input_name):
"""Set input on a TV"""
if tv_id not in controllers:
return jsonify({"success": False, "message": f"TV '{tv_id}' not found"})
try:
success = controllers[tv_id].set_input(input_name)
if success:
return jsonify({"success": True, "message": f"Input: {input_name}"})
return jsonify({"success": False, "message": "Failed to change input"})
except Exception as e:
return jsonify({"success": False, "message": str(e)})
@app.route('/api/<tv_id>/app/<app_name>', methods=['POST'])
def launch_app(tv_id, app_name):
"""Launch app on a TV"""
if tv_id not in controllers:
return jsonify({"success": False, "message": f"TV '{tv_id}' not found"})
try:
success = controllers[tv_id].launch_app(app_name)
if success:
return jsonify({"success": True, "message": f"Launched: {app_name}"})
return jsonify({"success": False, "message": f"Failed to launch {app_name}"})
except Exception as e:
return jsonify({"success": False, "message": str(e)})
@app.route('/api/<tv_id>/capabilities', methods=['GET'])
def get_capabilities(tv_id):
"""Get capabilities of a specific TV"""
if tv_id not in controllers:
return jsonify({"success": False, "message": f"TV '{tv_id}' not found"})
try:
caps = controllers[tv_id].get_capabilities()
return jsonify({"success": True, "capabilities": caps})
except Exception as e:
return jsonify({"success": False, "message": str(e)})
if __name__ == '__main__':
import warnings
warnings.filterwarnings('ignore', message='Unverified HTTPS request')
print(f"\n🖥️ Universal TV Remote Control Server")
print(f"📺 Configured TVs:")
for tv_id, tv_config in tvs.items():
status = "✓" if tv_id in controllers else "✗"
print(f" {status} {tv_config['name']} ({tv_config['brand']}) - {tv_config['ip']}")
print(f"\n🎯 Supported brands: {', '.join(get_supported_brands())}")
print(f"\n🌐 Access from any device:")
print(f" http://localhost:5000")
print(f" http://<your-ip>:5000")
print(f"\n Press Ctrl+C to stop\n")
app.run(host='0.0.0.0', port=5000, debug=False)
Step 7: Update HTML for Multi-TV Support
Add TV selector to the remote interface:
<!-- Add this after the <h1> tag -->
<div class="tv-selector">
<label for="tv-select">Select TV:</label>
<select id="tv-select" onchange="selectTV()">
<!-- Populated by JavaScript -->
</select>
</div>
<script>
let currentTV = null;
// Load TV list on page load
async function loadTVs() {
try {
const response = await fetch('/api/tvs');
const data = await response.json();
if (data.success) {
const select = document.getElementById('tv-select');
data.tvs.forEach(tv => {
const option = document.createElement('option');
option.value = tv.id;
option.textContent = `${tv.name} (${tv.brand})`;
select.appendChild(option);
});
// Select first TV by default
if (data.tvs.length > 0) {
currentTV = data.tvs[0].id;
}
}
} catch (error) {
showStatus('Error loading TVs: ' + error.message, 'error');
}
}
function selectTV() {
currentTV = document.getElementById('tv-select').value;
showStatus(`Controlling ${currentTV}`, 'info');
}
// Update sendCommand to use currentTV
async function sendCommand(command) {
if (!currentTV) {
showStatus('Please select a TV', 'error');
return;
}
try {
const response = await fetch(`/api/${currentTV}/command/${command}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showStatus('✓ ' + data.message, 'success');
} else {
showStatus('✗ ' + data.message, 'error');
}
} catch (error) {
showStatus('Error: ' + error.message, 'error');
}
}
// Load TVs when page loads
document.addEventListener('DOMContentLoaded', loadTVs);
</script>
Adding New TV Brands - Step by Step
Example: Adding LG WebOS Support
1. Install library:
pip install pylgtv
2. Create controller file: controllers/lg.py
from .base import TVController
from pylgtv import WebOsClient
class LGController(TVController):
"""LG WebOS TV implementation"""
def __init__(self, config):
super().__init__(config)
store = config.get('token_file', '/tmp/lg_token.txt')
self.client = WebOsClient(self.ip, key_file_path=store)
self.client.connect()
def power_on(self):
# LG supports WoL
if self.mac:
return self._send_wol()
return False
def power_off(self):
self.client.power_off()
return True
def power_toggle(self):
# Check current state and toggle
return self.power_off()
def volume_up(self):
self.client.volume_up()
return True
def volume_down(self):
self.client.volume_down()
return True
def mute(self):
self.client.set_mute(True)
return True
def set_volume(self, level):
self.client.set_volume(level)
return True
# ... implement remaining methods ...
def get_apps_list(self):
apps = self.client.get_apps()
return [app['title'] for app in apps]
def launch_app(self, app_name):
apps = self.client.get_apps()
for app in apps:
if app['title'].lower() == app_name.lower():
self.client.launch_app(app['id'])
return True
return False
def _send_wol(self):
"""Standard Wake-on-LAN implementation"""
# Same as other controllers
pass
3. Done! The controller is automatically discovered and available.
4. Add to config.json:
{
"id": "basement",
"name": "Basement TV",
"brand": "lg",
"ip": "192.168.1.102",
"mac": "11:22:33:44:55:66",
"token_file": "/tmp/lg_basement_token.txt"
}
5. Restart server - LG support is now live.
Testing New Controllers
File: test_controller.py
#!/usr/bin/env python3
"""Test a TV controller implementation"""
import sys
import json
from controllers import get_controller
def test_controller(brand, ip):
"""Test basic functionality of a controller"""
print(f"\n=== Testing {brand.upper()} Controller ===\n")
config = {
"ip": ip,
"brand": brand,
# Add any brand-specific config here
}
try:
# Create controller
print(f"Creating {brand} controller...")
controller = get_controller(brand, config)
print("✓ Controller created")
# Test capabilities
print("\nCapabilities:")
caps = controller.get_capabilities()
for feature, supported in caps.items():
status = "✓" if supported else "✗"
print(f" {status} {feature}")
# Test basic commands
print("\nTesting commands:")
tests = [
("Volume Up", controller.volume_up),
("Volume Down", controller.volume_down),
("Channel Up", controller.channel_up),
("Get Inputs", controller.get_inputs_list),
("Get Apps", controller.get_apps_list),
]
for name, func in tests:
try:
result = func()
print(f" ✓ {name}: {result}")
except Exception as e:
print(f" ✗ {name}: {e}")
print("\n=== Test Complete ===\n")
except Exception as e:
print(f"✗ Failed: {e}")
sys.exit(1)
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python test_controller.py <brand> <ip>")
print("Example: python test_controller.py samsung 192.168.1.101")
sys.exit(1)
brand = sys.argv[1]
ip = sys.argv[2]
test_controller(brand, ip)
Common TV Brand Resources
Samsung
- Legacy (pre-2016): Port 55000, custom protocol
- Modern (2016+): Port 8002, WebSocket
- Library:
samsungtvws(modern), custom implementation (legacy) - Docs: https://github.com/jaruba/samsung-remote
LG WebOS
- Port: 3000 (WebSocket)
- Library:
pylgtv - Docs: https://github.com/TheRealLink/pylgtv
Sony Bravia
- Protocol: REST API + IRCC
- Library:
pybravia - Docs: https://github.com/aparraga/braviaproapi
Roku
- Protocol: HTTP REST API (simple!)
- Port: 8060
- Library:
rokuor custom implementation - Docs: https://developer.roku.com/docs/developer-program/debugging/external-control-api.md
Philips (Android TV)
- Protocol: JointSpace API
- Port: 1926
- Library: Custom implementation
- Docs: https://github.com/eslavnov/pylips
Deployment Checklist
- [ ] All TVs configured in
config.json - [ ] All required libraries installed (
pip install -r requirements.txt) - [ ] Controllers tested individually with
test_controller.py - [ ] Flask app runs without errors
- [ ] Web interface accessible from all devices
- [ ] Multi-TV selector works correctly
- [ ] All buttons functional for each TV brand
- [ ] Status messages display correctly
Benefits of This Architecture
- Zero-configuration expansion: Drop a new
.pyfile incontrollers/and it's automatically available - Standardized interface: All TVs work the same way from the app's perspective
- Easy testing: Test each controller independently
- Graceful degradation: If one TV controller fails, others still work
- Clear separation: Brand-specific code isolated in individual files
- Easy debugging: Each brand's implementation is self-contained
- Community contributions: Others can add brands without touching core code
Future Enhancements
- Auto-discovery: Scan network for TVs and auto-configure
- Authentication helpers: Wizard for pairing with each brand
- Macros: Define sequences of commands ("Movie Mode" = dim lights + switch input + adjust volume)
- Scheduling: Time-based automation
- MCP integration: Let AI control TVs
- Mobile app: Native iOS/Android instead of web interface
- Voice control: Integration with speech recognition
No comments:
Post a Comment