amaran-light-cli
Version:
Command line tool for controlling Aputure Amaran lights via WebSocket to a local Amaran desktop app.
138 lines • 7.94 kB
JavaScript
import chalk from 'chalk';
import { CURVE_HELP_TEXT } from '../../daylightSimulation/constants.js';
import { ScheduleMaker } from '../../daylightSimulation/scheduleMaker.js';
import { commandCallbackPromise, getLightDevices } from '../cmdUtils.js';
export function registerSimulateSchedule(program, deps) {
const { asyncCommand } = deps;
program
.command('simulate')
.description('Simulate a CCT schedule curve in real-time on a specific device')
.argument('<device>', 'Device name, ID, or node_id to control')
.option('-u, --url <url>', 'WebSocket URL')
.option('-c, --client-id <id>', 'Client ID')
.option('-d, --debug', 'Enable debug mode')
.option('--lat <latitude>', 'Manual latitude (-90 to 90)')
.option('--lon <longitude>', 'Manual longitude (-180 to 180)')
.option('-C, --curve <curve>', CURVE_HELP_TEXT, 'hann')
.option('-L, --max-lux <value>', 'Simulation peak in lux (scales the whole day)')
.option('--duration <seconds>', 'Simulation duration to compress full day (default: 10 seconds)', '10')
.option('--cloud-cover <value>', 'Cloud cover (0-1)')
.option('--precipitation <type>', 'Precipitation type')
.option('--privacy-off', 'Show full IP address and precise coordinates', false)
.action(asyncCommand(handleSimulateSchedule(deps)));
}
function handleSimulateSchedule(deps) {
const { createController, findDevice, loadConfig } = deps;
return async (deviceQuery, options) => {
const { DEVICE_DEFAULTS, ERROR_MESSAGES } = await import('../../deviceControl/constants.js');
const { formatLocation } = await import('../../daylightSimulation/privacyUtil.js');
// 1. Make the schedule
const _maker = new ScheduleMaker(deps);
// We want a high-resolution schedule for smooth simulation
// The previous implementation calculated updates based on (duration * 1000) / updateInterval
// Let's stick to that but use the schedule maker to provide the points.
const durationCount = parseInt((options.duration ?? '10'), 10);
const updateInterval = DEVICE_DEFAULTS.updateInterval;
const totalUpdates = Math.floor((durationCount * 1000) / updateInterval);
const tempTimesMaker = new ScheduleMaker(deps);
let schedule;
try {
// For simulation, we need the "full day" bounds
// The old code used nightEnd to night
const baseInfo = await tempTimesMaker.makeSchedule({
lat: options.lat,
lon: options.lon,
curves: options.curve,
cloudCover: options.cloudCover, // ScheduleMaker handles parsing
precipitation: options.precipitation,
maxLuxLimit: options.maxLux ? parseFloat(options.maxLux) : undefined,
});
const nightEnd = baseInfo.times.nightEnd;
const night = baseInfo.times.night;
if (!nightEnd || !night || Number.isNaN(nightEnd.getTime()) || Number.isNaN(night.getTime())) {
throw new Error('Night times unavailable for this location/date');
}
const dayDurationMs = night.getTime() - nightEnd.getTime();
const timeStepMs = dayDurationMs / totalUpdates;
schedule = await tempTimesMaker.makeSchedule({
lat: options.lat,
lon: options.lon,
curves: options.curve,
startTime: nightEnd,
endTime: night,
intervalMinutes: timeStepMs / (60 * 1000), // convert ms to minutes for the maker's interval
includeSpecialTimes: false, // Smooth simulation doesn't need jumps to special times
maxLuxLimit: options.maxLux ? parseFloat(options.maxLux) : undefined,
});
}
catch (error) {
console.error(chalk.red(error.message));
process.exit(1);
}
// 2. Connect to controller and find device
const controller = await createController(options.url, options.clientId, options.debug);
let devices;
if (deviceQuery.toLowerCase() === 'all') {
devices = getLightDevices(controller.getDevices());
}
else {
const device = findDevice(controller, deviceQuery);
devices = device ? [device] : [];
}
if (devices.length === 0) {
console.error(chalk.red(ERROR_MESSAGES.deviceNotFound(deviceQuery)));
await controller.disconnect();
process.exit(1);
}
if (devices.some((device) => !device.node_id)) {
console.error(chalk.red(ERROR_MESSAGES.deviceNotFound(deviceQuery)));
await controller.disconnect();
process.exit(1);
}
const targetLabel = devices.length === 1
? devices[0].device_name || devices[0].name || devices[0].id || devices[0].node_id || 'Unknown'
: `${devices.length} lights`;
console.log(chalk.blue('\n═══════════════════════════════════════════════════════════'));
console.log(chalk.blue(' CCT Schedule Simulation'));
console.log(chalk.blue('═══════════════════════════════════════════════════════════\n'));
console.log(chalk.cyan(`Device: ${targetLabel}`));
console.log(chalk.cyan(`Location: ${formatLocation(schedule.lat, schedule.lon, schedule.source, options.privacyOff)}`));
console.log(chalk.cyan(`Simulation Duration: ${durationCount} second(s)`));
console.log(chalk.cyan(`Update Interval: ${updateInterval}ms`));
console.log(chalk.cyan(`Curve: ${schedule.curves[0]}\n`));
// 3. Render the schedule by making the lights execute it
console.log(chalk.yellow(`Turning on ${targetLabel}...`));
await Promise.all(devices.map((device) => commandCallbackPromise((callback) => controller.turnLightOn(device.node_id, callback))));
const _cfg = (loadConfig?.() ?? {});
const runSimulation = async () => {
const curve = schedule.curves[0];
for (let i = 0; i < schedule.points.length; i++) {
const point = schedule.points[i];
const val = point.values.get(curve);
if (!val)
continue;
const percent = Math.round((val.intensity / 10) * 10) / 10;
const progress = Math.round((i / (schedule.points.length - 1)) * 100);
const timeStr = point.time.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
process.stdout.write(`\r${chalk.gray(`[${progress}% | ${timeStr}] `)}${chalk.green(`Setting ${targetLabel} to ${val.cct}K at ${percent}%`)} `);
await Promise.all(devices.map((device) => commandCallbackPromise((callback) => controller.setCCT(device.node_id, val.cct, val.intensity, callback))));
if (i < schedule.points.length - 1) {
await new Promise((resolve) => setTimeout(resolve, updateInterval));
}
}
console.log();
};
const sigintHandler = async () => {
console.log(chalk.yellow('\n\nSimulation stopped by user'));
await controller.disconnect();
process.exit(0);
};
process.on('SIGINT', sigintHandler);
await runSimulation();
process.off('SIGINT', sigintHandler);
console.log(chalk.green('\nSimulation completed'));
await controller.disconnect();
};
}
export default registerSimulateSchedule;
//# sourceMappingURL=simulateSchedule.js.map