UNPKG

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
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