UNPKG

hottohts

Version:

TypeScript library for HottoH pellet stoves

341 lines 11.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.HottohRemoteClient = void 0; /** * hottohts - TCP/IP client for HottoH pellet stoves (TypeScript) * * Provides a low-level TCP client for communicating with HottoH stoves using the official protocol. * Handles connection, polling, and command sending. Used internally by the Hottoh high-level API. * * @module client */ const net = __importStar(require("net")); const request_1 = require("./request"); /** * Low-level TCP client for HottoH stoves. Handles protocol communication and polling. * Not intended for direct use; use the Hottoh class for most applications. */ class HottohRemoteClient { constructor(address = "192.168.4.10", port = 5001, id = 0) { this._info = null; this._data = null; this._data2 = null; this._raw = null; this._writeRequest = false; this._writeParameters = []; this._disconnectRequest = false; this.__isConnected = false; this.__endRequest = false; this._responseBuffer = ''; this._pendingResponse = null; this._loopTimeout = null; this._eventHandlers = new Map(); this.address = address; this.port = port; this._socket = new net.Socket(); this._setupSocketEventHandlers(); } /** * Set up persistent event handlers for the socket * These handlers remain throughout the lifecycle of the client */ _setupSocketEventHandlers() { const errorHandler = (err) => { this.__isConnected = false; }; const closeHandler = () => { this.__isConnected = false; }; const endHandler = () => { this.__isConnected = false; }; this._socket.on('close', closeHandler); this._socket.on('end', endHandler); this._socket.on('error', errorHandler); // Store handlers for potential cleanup this._eventHandlers.set('close', closeHandler); this._eventHandlers.set('end', endHandler); this._eventHandlers.set('error', errorHandler); } /** * Clean up and reset the socket */ _resetSocket() { // Create a new socket and set up handlers again if (this._socket) { // Remove all listeners this._socket.removeAllListeners(); // Ensure socket is closed try { this._socket.destroy(); } catch (e) { // Ignore errors on destroy } } this._socket = new net.Socket(); this._setupSocketEventHandlers(); } /** * Connect to the stove over TCP. * @returns Promise that resolves when connected. */ connect() { return new Promise((resolve, reject) => { // Clean up any existing connection first if (this.__isConnected) { this._resetSocket(); } const connectHandler = () => { this.__isConnected = true; this._socket.removeListener('connect', connectHandler); this._socket.removeListener('error', errorHandler); resolve(); }; const errorHandler = (err) => { this.__isConnected = false; this._socket.removeListener('connect', connectHandler); this._socket.removeListener('error', errorHandler); reject(err); }; this._socket.once('connect', connectHandler); this._socket.once('error', errorHandler); this._socket.connect(this.port, this.address); }); } /** * Check if the stove is reachable and protocol is working. * @returns Promise<boolean> */ async check() { try { await this.connect(); const data = await this._get_data("DAT", ["0"]); this._socket.end(); return data.length > 0; } catch (err) { return false; } } /** * Returns true if the TCP socket is connected. */ isConnected() { return this.__isConnected; } _extractData(data) { return data.split(';'); } async _sendRequestAndWaitForResponse(request) { return new Promise((resolve, reject) => { let timeout = null; const onData = (data) => { this._responseBuffer += data.toString('utf-8'); if (this._responseBuffer.includes('\n')) { const [response] = this._responseBuffer.split('\n'); this._responseBuffer = ''; cleanup(); resolve(this._extractData(response)); } }; const onError = (err) => { cleanup(); reject(err); }; const cleanup = () => { if (timeout) { clearTimeout(timeout); timeout = null; } this._socket.removeListener('data', onData); this._socket.removeListener('error', onError); }; // Add temporary listeners for this request only this._socket.on('data', onData); this._socket.on('error', onError); // Send the request this._socket.write(request); // Set a timeout to prevent hanging timeout = setTimeout(() => { cleanup(); reject(new Error('Socket timeout')); }, 60000); }); } async _get_data(command, parameters) { const request = new request_1.Request(command, request_1.CommandMode.READ, parameters); return this._sendRequestAndWaitForResponse(request.getRequest()); } async _set_data(parameters) { const request = new request_1.Request('DAT', request_1.CommandMode.WRITE, parameters); return this._sendRequestAndWaitForResponse(request.getRequest()); } async _set_raw(command, mode, parameters) { const request = new request_1.Request(command, mode, parameters); return this._sendRequestAndWaitForResponse(request.getRequest()); } /** * Send a command to the stove (queued for next poll cycle). * @param parameters Command parameters as string array * @returns true if queued */ sendCommand(parameters) { this._writeRequest = true; this._writeParameters.push(parameters); return true; } /** * Start polling the stove for data. */ start() { this.__endRequest = false; this.loop(); } /** * Stop polling and disconnect. */ stop() { this.__endRequest = true; // Clear any pending timeouts if (this._loopTimeout) { clearTimeout(this._loopTimeout); this._loopTimeout = null; } // Clean up connection this.__disconnect(); } loop() { // Clear any existing timeout if (this._loopTimeout) { clearTimeout(this._loopTimeout); this._loopTimeout = null; } if (this.__endRequest) return; if (this.__isConnected) { this.__dial(); } else { this.__connect(); } } __connect() { try { const connectHandler = () => { this.__isConnected = true; this._socket.removeListener('connect', connectHandler); this.loop(); }; this._socket.once('connect', connectHandler); this._socket.connect(this.port, this.address); } catch (error) { this.__isConnected = false; if (error && error.code === 'EISCONN') { // Already connected, continue with loop this.__isConnected = true; this._loopTimeout = setTimeout(() => this.loop(), 1000); return; } // Retry connection after delay this._loopTimeout = setTimeout(() => this.loop(), 5000); } } async __dial() { if (!this.__isConnected || this.__endRequest) return; try { this._info = await this._get_data('INF', ['']); this._data = await this._get_data('DAT', ['0']); this._data2 = await this._get_data('DAT', ['2']); // Process any queued write commands while (this._writeParameters.length > 0 && !this.__endRequest) { const param = this._writeParameters[0]; await this._set_data(param); this._writeParameters.shift(); } this._writeRequest = false; // Schedule next poll if not stopping if (!this.__endRequest) { this._loopTimeout = setTimeout(() => this.loop(), 1000); } } catch (error) { this.__isConnected = false; // If we're not stopping, try to reconnect if (!this.__endRequest) { this._loopTimeout = setTimeout(() => this.loop(), 5000); } } } __disconnect() { this.__isConnected = false; try { this._socket.end(); } catch (e) { // Ignore errors on end } } /** * Clean up all resources * Call this when done with the client to prevent memory leaks */ dispose() { this.stop(); // Ensure all event handlers are removed if (this._socket) { this._socket.removeAllListeners(); try { this._socket.destroy(); } catch (e) { // Ignore errors on destroy } } // Clear all references this._socket = null; this._eventHandlers.clear(); this._info = null; this._data = null; this._data2 = null; this._raw = null; this._writeParameters = []; } } exports.HottohRemoteClient = HottohRemoteClient; //# sourceMappingURL=client.js.map