Skip to content

Instantly share code, notes, and snippets.

@zsprackett
Last active October 27, 2025 16:33
Show Gist options
  • Select an option

  • Save zsprackett/29334b9be1e2bd90c1737bd0ba0eaf5c to your computer and use it in GitHub Desktop.

Select an option

Save zsprackett/29334b9be1e2bd90c1737bd0ba0eaf5c to your computer and use it in GitHub Desktop.
Interact with the amaran Desktop app via WebSocket to control lights.
/*
MIT License
Copyright (c) 2024 S. Zachariah Sprackett <[email protected]>
Control Aputure Amaran Lights via websocket to the amaran Desktop application.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import WebSocket from 'ws';
type CommandType =
| 'get_device_list'
| 'get_scene_list'
| 'get_node_config'
| 'get_sleep'
| 'get_preset_list'
| 'get_system_effect_list'
| 'set_sleep'
| 'toggle_sleep'
| 'set_intensity'
| 'increment_intensity'
| 'set_cct'
| 'increment_cct'
| 'set_hsi'
| 'set_color'
| 'set_system_effect';
interface Command {
version: number;
client_id: string;
type: CommandType;
node_id?: string;
args?: any;
}
type CommandCallback = (success: boolean, message: string, data?: any) => void;
class LightController {
private ws: WebSocket;
private clientId: string = 'unknown_client';
private deviceList: any[] = [];
private sceneList: any[] = [];
private nodeConfigs: Map<string, any> = new Map();
private debug: boolean;
private onInitializedCallback?: () => void;
private commandCallbacks: Map<string, CommandCallback> = new Map();
constructor(wsUrl: string, clientId?: string, onInitialized?: () => void, debug: boolean = false) {
this.ws = new WebSocket(wsUrl);
if (clientId) {
this.clientId = clientId;
}
this.debug = debug;
if (onInitialized) {
this.onInitializedCallback = onInitialized;
}
this.ws.on('open', () => {
this.log('Connected to WebSocket server');
this.onConnectionOpen();
});
this.ws.on('message', (data) => {
try {
const parsedData = JSON.parse(data.toString());
this.log('Received message from server:', parsedData);
const requestId = parsedData.request?.type;
if (parsedData.code !== 0) {
console.error('Error from server:', parsedData.message);
if (requestId && this.commandCallbacks.has(requestId)) {
this.commandCallbacks.get(requestId)?.(false, parsedData.message);
this.commandCallbacks.delete(requestId);
}
return;
}
if (requestId && this.commandCallbacks.has(requestId)) {
this.commandCallbacks.get(requestId)?.(true, parsedData.message, parsedData.data);
this.commandCallbacks.delete(requestId);
}
if (parsedData.request?.type) {
switch (parsedData.request.type) {
case 'get_device_list':
this.handleDeviceList(parsedData.data);
break;
case 'get_scene_list':
this.handleSceneList(parsedData.data);
break;
case 'get_node_config':
this.handleNodeConfig(parsedData.data);
break;
default:
this.log('Unknown response type');
}
}
} catch (error) {
console.error('Error parsing message:', error);
}
});
this.ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
this.ws.on('close', () => {
this.log('Disconnected from WebSocket server');
});
}
setClientId(clientId: string) {
this.clientId = clientId;
}
private onConnectionOpen() {
this.getDeviceList();
}
private sendCommand(nodeId: string | undefined, type: CommandType, args?: any, callback?: CommandCallback) {
if (this.ws.readyState === WebSocket.OPEN) {
const command: Command = {
version: 1,
client_id: this.clientId,
type,
node_id: nodeId,
args,
};
if (callback) {
this.commandCallbacks.set(type, callback);
}
this.ws.send(JSON.stringify(command));
this.log(`Sent command: ${type}`);
} else {
console.error('WebSocket is not open');
if (callback) {
callback(false, 'WebSocket is not open');
}
}
}
private handleDeviceList(data: any) {
this.deviceList = data.data;
this.log('Device List:', JSON.stringify(this.deviceList, null, 2));
this.getSceneList();
}
private handleSceneList(data: any) {
this.sceneList = data.data;
this.log('Scene List:', JSON.stringify(this.sceneList, null, 2));
this.getNodeConfigs();
}
private handleNodeConfig(data: any) {
const nodeId = data.node_id;
if (nodeId) {
this.nodeConfigs.set(nodeId, data.data);
this.log('Node Config:', JSON.stringify(data.data, null, 2));
if (this.nodeConfigs.size === this.deviceList.length) {
this.log('All node configurations have been gathered');
if (this.onInitializedCallback) {
this.onInitializedCallback();
}
}
}
}
private getNodeConfigs() {
this.deviceList.forEach((device) => {
if (device.node_id) {
this.getNodeConfig(device.node_id);
}
});
}
getDeviceList(callback?: CommandCallback) {
this.sendCommand(undefined, 'get_device_list', {}, callback);
}
getSceneList(callback?: CommandCallback) {
this.sendCommand(undefined, 'get_scene_list', {}, callback);
}
getNodeConfig(nodeId: string, callback?: CommandCallback) {
this.sendCommand(nodeId, 'get_node_config', {}, callback);
}
turnLightOn(nodeId: string, callback?: CommandCallback) {
this.sendCommand(nodeId, 'set_sleep', { sleep: false }, callback);
}
turnLightOff(nodeId: string, callback?: CommandCallback) {
this.sendCommand(nodeId, 'set_sleep', { sleep: true }, callback);
}
getLightSleepStatus(nodeId: string, callback?: CommandCallback) {
this.sendCommand(nodeId, 'get_sleep', {}, callback);
}
toggleLight(nodeId: string, callback?: CommandCallback) {
this.sendCommand(nodeId, 'toggle_sleep', undefined, callback);
}
setIntensity(nodeId: string, intensity: number, callback?: CommandCallback) {
this.sendCommand(nodeId, 'set_intensity', { intensity }, callback);
}
incrementIntensity(nodeId: string, delta: number, callback?: CommandCallback) {
this.sendCommand(nodeId, 'increment_intensity', { delta }, callback);
}
setCCT(nodeId: string, cct: number, intensity?: number, callback?: CommandCallback) {
const args: any = { cct };
if (intensity !== undefined) {
args.intensity = intensity;
}
this.sendCommand(nodeId, 'set_cct', args, callback);
}
incrementCCT(nodeId: string, delta: number, intensity?: number, callback?: CommandCallback) {
const args: any = { delta };
if (intensity !== undefined) {
args.intensity = intensity;
}
this.sendCommand(nodeId, 'increment_cct', args, callback);
}
setHSI(nodeId: string, hue: number, sat: number, intensity: number, cct?: number, gm?: number, callback?: CommandCallback) {
const args: any = { hue, sat, intensity };
if (cct !== undefined) {
args.cct = cct;
}
if (gm !== undefined) {
args.gm = gm;
}
this.sendCommand(nodeId, 'set_hsi', args, callback);
}
setColor(nodeId: string, color: string, intensity?: number, callback?: CommandCallback) {
const args: any = { color };
if (intensity !== undefined) {
args.intensity = intensity;
}
this.sendCommand(nodeId, 'set_color', args, callback);
}
setSystemEffect(nodeId: string, effectType: string, intensity?: number, callback?: CommandCallback) {
const args: any = { effect_type: effectType };
if (intensity !== undefined) {
args.intensity = intensity;
}
this.sendCommand(nodeId, 'set_system_effect', args, callback);
}
async disconnect() {
if (this.ws.readyState === WebSocket.OPEN) {
await this.waitForPendingCommands(5000);
this.ws.close();
this.log('WebSocket connection closed');
} else {
console.error('WebSocket is not open or already closed');
}
}
private waitForPendingCommands(timeout: number): Promise<void> {
return new Promise((resolve) => {
const start = Date.now();
const checkPendingCommands = () => {
if (this.commandCallbacks.size === 0 || Date.now() - start >= timeout) {
resolve();
} else {
setTimeout(checkPendingCommands, 100);
}
};
checkPendingCommands();
});
}
// Getters
public getDevices(): any[] {
return this.deviceList;
}
public getScenes(): any[] {
return this.sceneList;
}
public getNode(nodeId: string) {
return this.nodeConfigs.get(nodeId);
}
private log(...args: any[]) {
if (this.debug) {
console.log(...args);
}
}
}
export default LightController;
@theontho
Copy link

Hey thanks for making this, I extended this into a cli tool: https://github.com/theontho/amaran-cli

It also has a circadian lighting feature called auto-cct on top of that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment