Documentation Index
Fetch the complete documentation index at: https://docs.mixlarlabs.com/llms.txt
Use this file to discover all available pages before exploring further.
Web Controller API
The Web Controller provides a complete REST API and WebSocket interface for remote control of your Mixlar device from web browsers or mobile apps.
Base URL
Port: Configurable in plugin settings (default: 5000)
Authentication
The Web Controller supports optional password protection.
Session-Based Authentication
When authentication is enabled:
- Login Required: All endpoints require valid session
- Login Endpoint:
POST /api/auth/login
- Session Cookie:
mixlar_session (HttpOnly)
- Session Timeout: Configurable (default: 24 hours)
Login Flow
// 1. Check auth status
const status = await fetch('/api/auth/status').then(r => r.json());
if (status.auth_enabled && !status.authenticated) {
// 2. Show login form
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'admin',
password: 'your-password',
remember: true
})
});
// 3. Redirect to main page
if (response.ok) {
window.location.href = '/';
}
}
API Endpoints
Authentication
Check Auth Status
Authentication: Not required
Response: 200 OK
{
"auth_enabled": true,
"authenticated": false,
"username": null
}
Login
Request Body:
{
"username": "admin",
"password": "your-password",
"remember": true
}
Response: 200 OK
{
"success": true,
"redirect": "/"
}
Error Response: 401 Unauthorized
{
"error": "Invalid username or password"
}
Logout
Response: 200 OK
System Status
Get Overall Status
Authentication: Required
Response: 200 OK
{
"connected": true,
"port": "COM3",
"page": "MAIN",
"firmware_version": "1.2.0",
"app_version": "1.5"
}
Sliders
Get All Sliders
Response: 200 OK
{
"sliders": [
{
"index": 1,
"value": 75,
"assignment": {
"type": "app",
"names": ["Spotify.exe"]
},
"label": "Spotify",
"muted": false
},
{
"index": 2,
"value": 50,
"assignment": {
"type": "system_default_output"
},
"label": "System",
"muted": false
}
]
}
Set Slider Value
POST /api/sliders/{index}
Path Parameters:
index (integer): Slider index (1-4)
Request Body:
Constraints:
Response: 200 OK
{
"success": true,
"index": 1,
"value": 75
}
Get Slider Assignment
GET /api/sliders/{index}/assignment
Response: 200 OK
{
"type": "app",
"names": ["Spotify.exe", "Discord.exe"]
}
Assignment Types:
system_default_output - System output volume
system_default_input - Microphone volume
app - Specific application(s)
unassigned - No assignment
Set Slider Assignment
POST /api/sliders/{index}/assignment
Request Body:
{
"type": "specific_app",
"names": ["Spotify.exe", "Discord.exe"]
}
For System Assignments:
{
"type": "system_default_output"
}
Response: 200 OK
{
"success": true,
"assignment": {
"type": "app",
"names": ["Spotify.exe"]
}
}
Get Available Audio Sessions
Response: 200 OK
{
"sessions": [
{
"name": "Spotify.exe",
"displayName": "Spotify"
},
{
"name": "Discord.exe",
"displayName": "Discord"
}
],
"systemOptions": [
{
"name": "system_default_output",
"displayName": "System Output"
},
{
"name": "system_default_input",
"displayName": "System Input (Mic)"
},
{
"name": "unassigned",
"displayName": "Unassigned"
}
]
}
Mute Controls
Get All Mute States
Response: 200 OK
{
"individual": [false, true, false, false],
"master": false
}
Toggle Individual Mute
Path Parameters:
index (integer): Mute button index (1-4)
Response: 200 OK
{
"success": true,
"index": 1,
"muted": true,
"message": "Mute toggle requested"
}
Toggle Master Mute
Response: 200 OK
{
"success": true,
"muted": true,
"message": "Master mute toggle requested"
}
Master Volume
Get Master Volume
Response: 200 OK
Set Master Volume
Request Body:
Constraints:
Response: 200 OK
{
"success": true,
"volume": 75
}
Macros
Get All Macros
Response: 200 OK
{
"current_page": 0,
"page_names": {
"0": "Main",
"1": "OBS",
"2": "Smart Home",
"3": "Custom"
},
"pages": {
"0": [
{
"type": "hotkey",
"name": "Screenshot",
"key": "PrintScreen",
"icon": "camera"
}
]
}
}
Get Macro Page Info
Response: 200 OK
{
"current": 0,
"names": {
"0": "Main",
"1": "OBS",
"2": "Smart Home",
"3": "Custom"
}
}
Change Macro Page
Note: This endpoint returns 403 Forbidden. Use the desktop app or hardware to switch pages.
Response: 403 Forbidden
{
"error": "Web controller cannot change macro page. Use desktop app or hardware to switch pages."
}
Trigger Macro
Request Body:
{
"index": 0,
"page": 0
}
Parameters:
index (integer): Macro index (0-8)
page (integer, optional): Page index (0-3), defaults to current page
Response: 200 OK
{
"success": true,
"index": 0,
"page": 0,
"macro": "Screenshot"
}
Smart Home
Get All Devices
Response: 200 OK
{
"connected": true,
"devices": [
{
"entity_id": "light.living_room",
"friendly_name": "Living Room Light",
"state": "on",
"brightness": 200,
"domain": "light"
}
]
}
Toggle Device
POST /api/smarthome/toggle
Request Body:
{
"entity_id": "light.living_room"
}
Response: 200 OK
{
"success": true,
"entity_id": "light.living_room",
"state": "off"
}
Set Light Brightness
POST /api/smarthome/brightness
Request Body:
{
"entity_id": "light.living_room",
"value": 75
}
Constraints:
Response: 200 OK
{
"success": true,
"entity_id": "light.living_room",
"brightness": 75
}
OBS Studio
Get Scenes
Response: 200 OK
{
"scenes": [
"Main Scene",
"Gaming",
"BRB"
]
}
Error Response: 400 Bad Request
{
"error": "OBS not connected"
}
Switch Scene
Request Body:
{
"scene_name": "Gaming"
}
Response: 200 OK
{
"success": true,
"scene": "Gaming"
}
Get Sources
Response: 200 OK
{
"sources": [
"Webcam",
"Screen Capture",
"Alert Box"
]
}
Toggle Source Visibility
Request Body:
{
"source_name": "Webcam",
"visible": true
}
Response: 200 OK
{
"success": true,
"source": "Webcam",
"visible": true
}
Audio Profiles
Get All Profiles
Response: 200 OK
{
"profiles": [
{
"name": "Default Profile",
"icon": "fa5s.sliders-h",
"created": "2025-01-01T12:00:00",
"updated": "2025-01-15T08:30:00",
"is_current": true
},
{
"name": "Gaming",
"icon": "fa5s.gamepad",
"created": "2025-01-10T15:20:00",
"updated": "2025-01-10T15:20:00",
"is_current": false
}
],
"current": "Default Profile"
}
Get Current Profile
GET /api/profiles/current
Response: 200 OK
{
"name": "Default Profile"
}
Load Profile
Request Body:
Response: 200 OK
{
"success": true,
"profile": "Gaming",
"message": "Profile load requested"
}
Error Response: 404 Not Found
{
"error": "Profile \"Gaming\" not found"
}
WebSocket API
Real-time bidirectional communication for live updates.
Connection
const socket = io('http://localhost:5000');
// Connection established
socket.on('connect', () => {
console.log('Connected to Web Controller');
});
// Connection lost
socket.on('disconnect', () => {
console.log('Disconnected from Web Controller');
});
Events (Server → Client)
initial_state
Sent when client connects or requests state.
socket.on('initial_state', (state) => {
console.log('Full state received:', state);
// state contains: sliders, macros, profiles, status, etc.
});
slider_change
Emitted when any slider value changes.
socket.on('slider_change', (data) => {
console.log(`Slider ${data.index} changed to ${data.value}`);
// { index: 1, value: 75 }
});
assignment_change
Emitted when slider assignment changes.
socket.on('assignment_change', (data) => {
console.log(`Slider ${data.index} assignment changed:`, data.assignment);
// {
// index: 1,
// assignment: { type: 'app', names: ['Spotify.exe'] },
// label: 'Spotify'
// }
});
mute_change
Emitted when mute state changes.
socket.on('mute_change', (data) => {
console.log(`Mute ${data.index}: ${data.muted}`);
// { index: 1, muted: true }
});
volume_change
Emitted when master volume changes.
socket.on('volume_change', (data) => {
console.log(`Master volume: ${data.volume}`);
// { volume: 75 }
});
profile_change
Emitted when audio profile changes.
socket.on('profile_change', (data) => {
console.log(`Profile changed to: ${data.profile}`);
// { profile: 'Gaming' }
});
device_status
Emitted when device connection status changes.
socket.on('device_status', (data) => {
console.log(`Device ${data.connected ? 'connected' : 'disconnected'}`);
// { connected: true, port: 'COM3' }
});
Events (Client → Server)
get_state
Request full state update.
socket.emit('get_state');
// Server will respond with 'initial_state' event
Complete Example
React Application
import { useEffect, useState } from 'react';
import io from 'socket.io-client';
function WebController() {
const [sliders, setSliders] = useState([]);
const [connected, setConnected] = useState(false);
const socket = io('http://localhost:5000');
useEffect(() => {
// Initial state
socket.on('initial_state', (state) => {
setSliders(state.sliders || []);
setConnected(state.connected);
});
// Real-time updates
socket.on('slider_change', ({ index, value }) => {
setSliders(prev => prev.map((s, i) =>
i === index - 1 ? { ...s, value } : s
));
});
socket.on('device_status', ({ connected }) => {
setConnected(connected);
});
return () => socket.disconnect();
}, []);
const setSlider = async (index, value) => {
await fetch(`/api/sliders/${index}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ value })
});
};
return (
<div>
<h1>Mixlar Web Controller</h1>
<p>Status: {connected ? 'Connected' : 'Disconnected'}</p>
{sliders.map((slider, i) => (
<div key={i}>
<label>{slider.label}</label>
<input
type="range"
min="0"
max="100"
value={slider.value}
onChange={(e) => setSlider(slider.index, e.target.value)}
/>
<span>{slider.value}%</span>
</div>
))}
</div>
);
}
export default WebController;
Vue.js Application
<template>
<div class="web-controller">
<h1>Mixlar Web Controller</h1>
<p>Status: {{ connected ? 'Connected' : 'Disconnected' }}</p>
<div v-for="slider in sliders" :key="slider.index" class="slider">
<label>{{ slider.label }}</label>
<input
type="range"
min="0"
max="100"
:value="slider.value"
@input="setSlider(slider.index, $event.target.value)"
/>
<span>{{ slider.value }}%</span>
</div>
</div>
</template>
<script>
import { io } from 'socket.io-client';
export default {
data() {
return {
sliders: [],
connected: false,
socket: null
};
},
mounted() {
this.socket = io('http://localhost:5000');
this.socket.on('initial_state', (state) => {
this.sliders = state.sliders || [];
this.connected = state.connected;
});
this.socket.on('slider_change', ({ index, value }) => {
const slider = this.sliders.find(s => s.index === index);
if (slider) slider.value = value;
});
this.socket.on('device_status', ({ connected }) => {
this.connected = connected;
});
},
beforeUnmount() {
if (this.socket) {
this.socket.disconnect();
}
},
methods: {
async setSlider(index, value) {
await fetch(`/api/sliders/${index}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ value: parseInt(value) })
});
}
}
};
</script>
Mobile App (Flutter)
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'dart:convert';
class WebController extends StatefulWidget {
@override
_WebControllerState createState() => _WebControllerState();
}
class _WebControllerState extends State<WebController> {
final String baseUrl = 'http://192.168.1.100:5000';
late IO.Socket socket;
List<dynamic> sliders = [];
bool connected = false;
@override
void initState() {
super.initState();
connectSocket();
}
void connectSocket() {
socket = IO.io(baseUrl, <String, dynamic>{
'transports': ['websocket'],
'autoConnect': true,
});
socket.on('initial_state', (data) {
setState(() {
sliders = data['sliders'] ?? [];
connected = data['connected'] ?? false;
});
});
socket.on('slider_change', (data) {
setState(() {
final index = data['index'] - 1;
if (index < sliders.length) {
sliders[index]['value'] = data['value'];
}
});
});
socket.connect();
}
Future<void> setSlider(int index, double value) async {
final response = await http.post(
Uri.parse('$baseUrl/api/sliders/$index'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'value': value.toInt()}),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Mixlar Web Controller'),
),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(16),
child: Text(
'Status: ${connected ? "Connected" : "Disconnected"}',
style: TextStyle(fontSize: 18),
),
),
Expanded(
child: ListView.builder(
itemCount: sliders.length,
itemBuilder: (context, i) {
final slider = sliders[i];
return ListTile(
title: Text(slider['label'] ?? 'Slider ${i + 1}'),
subtitle: Slider(
value: (slider['value'] ?? 0).toDouble(),
min: 0,
max: 100,
onChanged: (value) {
setSlider(slider['index'], value);
},
),
);
},
),
),
],
),
);
}
@override
void dispose() {
socket.disconnect();
super.dispose();
}
}
Security
HTTPS Setup
For secure remote access, enable HTTPS with Tailscale certificates:
- Install Tailscale
- Get certificates:
tailscale cert yourhostname.tailnet.ts.net
- Place in:
main/certs/ folder
- Restart plugin
Access via: https://yourhostname.tailnet.ts.net:5000
Authentication
Enable password protection in Web Controller settings:
{
"auth_enabled": true,
"auth_username": "admin",
"auth_password_hash": "hashed-password",
"session_timeout": 86400
}
CORS
CORS is enabled by default for all origins (*). To restrict:
Edit web_server.py:
CORS(app, resources={r"/*": {"origins": "https://your-domain.com"}})
Documentation Version: 1.0
Last Updated: 2025-12-10
Default Port: 5000