172 lines
5.1 KiB
JavaScript
172 lines
5.1 KiB
JavaScript
|
|
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<void>} 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<Object>} 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();
|
|
}
|
|
}
|