import WebSocket from 'ws'; /** * A simple client for the TischlerCtrl WebSocket API. */ export class TischlerClient { /** * @param {string} url - The WebSocket server URL (e.g., 'ws://localhost:3000') * @param {string} apiKey - Your Agent API Key */ constructor(url, apiKey) { this.url = url; this.apiKey = apiKey; this.ws = null; this.authenticated = false; this.onAuthenticated = null; // Callback when authenticated this.onCommand = null; // Callback for incoming commands this.shouldReconnect = true; this.reconnectInterval = 5000; this.pingIntervalId = null; } /** * Connect to the WebSocket server. * @returns {Promise} Resolves when connected (but not yet authenticated) */ connect() { this.shouldReconnect = true; return new Promise((resolve, reject) => { this.ws = new WebSocket(this.url); this.ws.on('open', () => { console.log('[Client] Connected to server.'); this.startPing(); this.authenticate(); resolve(); }); this.ws.on('message', (data) => this.handleMessage(data)); this.ws.on('error', (err) => { console.error('[Client] Connection error:', err.message); // Don't reject here if we want to rely on 'close' for reconnect logic flow, // but for the initial Promise usage, we might want to know. // However, 'close' is always called after 'error'. }); this.ws.on('close', (code, reason) => { console.log(`[Client] Disconnected. Code: ${code}, Reason: ${reason}`); this.authenticated = false; this.stopPing(); if (this.shouldReconnect) { console.log(`[Client] Reconnecting in ${this.reconnectInterval / 1000}s...`); setTimeout(() => this.connect(), this.reconnectInterval); } }); }); } startPing() { this.stopPing(); this.pingIntervalId = setInterval(() => { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.ping(); } }, 30000); } stopPing() { if (this.pingIntervalId) { clearInterval(this.pingIntervalId); this.pingIntervalId = null; } } /** * Send authentication message. */ authenticate() { console.log('[Client] Authenticating...'); this.send({ type: 'auth', apiKey: this.apiKey }); } /** * Send sensor readings. * @param {Array} readings - Array of reading objects * @example * client.sendReadings([ * { device: 'temp-sensor-1', channel: 'temp', value: 24.5 }, * { device: 'temp-sensor-1', channel: 'config', data: { mode: 'eco' } } * ]); */ sendReadings(readings) { if (!this.authenticated) { // console.warn('[Client] Cannot send data: Not authenticated.'); return; } // console.log(`[Client] Sending ${readings.length} readings...`); this.send({ type: 'data', readings: readings }); } /** * Handle incoming messages. */ handleMessage(data) { try { const message = JSON.parse(data.toString()); switch (message.type) { case 'auth': if (message.success) { this.authenticated = true; console.log(`[Client] Authenticated as "${message.name}" (Prefix: ${message.devicePrefix})`); if (this.onAuthenticated) this.onAuthenticated(); } else { console.error('[Client] Authentication failed:', message.error); this.ws.close(); } break; case 'ack': // console.log(`[Client] Server acknowledged ${message.count} readings.`); break; case 'command': console.log(`[Client] Command received:`, message); if (this.onCommand) { this.onCommand(message); } break; case 'error': console.error('[Client] Server error:', message.error); break; default: console.log('[Client] Received:', message); } } catch (err) { console.error('[Client] Failed to parse message:', err); } } /** * Helper to send JSON object. */ send(obj) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(obj)); } } /** * Close connection. */ close() { this.shouldReconnect = false; this.stopPing(); if (this.ws) this.ws.close(); } }