hottohts
Version:
TypeScript library for HottoH pellet stoves
341 lines • 11.6 kB
JavaScript
"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