homebridge-pentair-intellicenter-ai
Version:
Homebridge plugin for the Pentair IntelliCenter pool control system (maintained with AI assistance)
2,438 lines • 117 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;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PentairPlatform = void 0;
const net = __importStar(require("net"));
const settings_1 = require("./settings");
const circuitAccessory_1 = require("./circuitAccessory");
const telnet_client_1 = require("telnet-client");
const types_1 = require("./types");
const uuid_1 = require("uuid");
const util_1 = require("./util");
const constants_1 = require("./constants");
const heaterAccessory_1 = require("./heaterAccessory");
const events_1 = __importDefault(require("events"));
const temperatureAccessory_1 = require("./temperatureAccessory");
const pumpRpmAccessory_1 = require("./pumpRpmAccessory");
const pumpGpmAccessory_1 = require("./pumpGpmAccessory");
const pumpWattsAccessory_1 = require("./pumpWattsAccessory");
const errorHandling_1 = require("./errorHandling");
const configValidation_1 = require("./configValidation");
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
class PentairPlatform {
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.Service = this.api.hap.Service;
this.Characteristic = this.api.hap.Characteristic;
// this is used to track restored cached accessories
this.accessoryMap = new Map();
this.heaters = new Map();
this.heaterInstances = new Map();
this.discoveryBuffer = null;
this.discoveryTimeout = null;
this.buffer = '';
// Temperature unit validation tracking
this.temperatureReadings = [];
this.temperatureUnitValidated = false;
this.temperatureValidationInterval = null;
// Telnet connection status
this.lastMessageReceived = Date.now();
this.isSocketAlive = false;
// Used by "maybereconnect" logic
this.reconnecting = false;
this.lastReconnectTime = 0;
// Error tracking for ParseError issues
this.parseErrorCount = 0;
this.parseErrorResetTime = Date.now();
// Command queue to prevent overwhelming IntelliCenter
this.commandQueue = [];
this.processingQueue = false;
// Heartbeat interval for cleanup
this.heartbeatInterval = null;
this.validatedConfig = null;
this.log.debug('Finished initializing platform:', this.config.name);
if (!this.validateConfiguration()) {
return;
}
this.initializeComponents();
this.initializeDataStructures();
this.setupGracefulShutdown();
this.setupApiEventHandlers();
this.setupHeartbeatMonitoring();
}
validateConfiguration() {
var _a;
const validation = configValidation_1.ConfigValidator.validate(this.config);
if (!validation.isValid) {
this.log.error('Configuration validation failed:');
validation.errors.forEach(error => this.log.error(` - ${error}`));
return false;
}
(_a = validation.warnings) === null || _a === void 0 ? void 0 : _a.forEach(warning => this.log.warn(`Config Warning: ${warning}`));
this.validatedConfig = validation.sanitizedConfig;
this.log.info('Configuration validated successfully');
return true;
}
initializeComponents() {
this.circuitBreaker = new errorHandling_1.CircuitBreaker({
failureThreshold: 5,
resetTimeout: 300000, // 5 minutes
monitoringPeriod: 60000, // 1 minute
});
this.healthMonitor = new errorHandling_1.HealthMonitor();
this.rateLimiter = new errorHandling_1.RateLimiter(40, 60000); // 40 requests per minute - more reasonable for normal operation
this.deadLetterQueue = new errorHandling_1.DeadLetterQueue(100, 24 * 60 * 60 * 1000); // 100 items, 24 hour retention
this.connection = new telnet_client_1.Telnet();
this.setupSocketEventHandlers();
}
initializeDataStructures() {
this.maxBufferSize = this.validatedConfig.maxBufferSize;
this.discoverCommandsSent = [];
this.discoverCommandsFailed = [];
this.discoveryBuffer = null;
this.discoveryTimeout = null;
this.pumpIdToCircuitMap = new Map();
// Initialize new pump-circuit association mappings
this.pumpToCircuitsMap = new Map();
this.circuitToPumpMap = new Map();
this.pumpCircuitToPumpMap = new Map();
this.activePumpCircuits = new Map();
}
setupApiEventHandlers() {
this.api.on('didFinishLaunching', async () => {
await this.connectToIntellicenter();
});
}
setupHeartbeatMonitoring() {
this.heartbeatInterval = setInterval(() => {
const now = Date.now();
const silence = now - this.lastMessageReceived;
if (this.isSocketAlive && silence > 4 * 60 * 60 * 1000 /* 4 hours */) {
this.log.warn('No data from IntelliCenter in over 4 hours. Closing and restarting connection.');
this.connection.destroy();
this.isSocketAlive = false;
this.delay(30 * 1000).then(async () => {
await this.maybeReconnect();
});
}
}, 60000);
}
/**
* Validate network connectivity to IntelliCenter before attempting Telnet connection
*/
async validateNetworkConnectivity(host, port) {
return new Promise(resolve => {
const context = this.createNetworkCheckContext(host, port, resolve);
this.setupNetworkCheckHandlers(context);
this.initiateNetworkConnection(context);
});
}
createNetworkCheckContext(host, port, resolve) {
const socket = new net.Socket();
const timeout = 5000; // 5 second timeout
let hasResolved = false;
const resolveOnce = (result) => {
if (!hasResolved) {
hasResolved = true;
socket.destroy();
resolve(result);
}
};
const timer = setTimeout(() => {
this.log.warn(`Network connectivity check timeout for ${host}:${port}`);
resolveOnce(false);
}, timeout);
return { socket, timer, host, port, resolveOnce };
}
setupNetworkCheckHandlers(context) {
const { socket, timer, host, port, resolveOnce } = context;
socket.on('connect', () => {
clearTimeout(timer);
this.log.debug(`Network connectivity confirmed for ${host}:${port}`);
resolveOnce(true);
});
socket.on('error', error => {
clearTimeout(timer);
this.log.warn(`Network connectivity check failed for ${host}:${port}: ${error.message}`);
resolveOnce(false);
});
}
initiateNetworkConnection(context) {
const { socket, timer, host, port, resolveOnce } = context;
try {
socket.connect(port, host);
}
catch (error) {
clearTimeout(timer);
this.log.warn(`Network connectivity check error for ${host}:${port}: ${error instanceof Error ? error.message : String(error)}`);
resolveOnce(false);
}
}
async connectToIntellicenter() {
if (!this.validatedConfig) {
this.log.error('Cannot connect: Configuration validation failed');
return;
}
const telnetParams = this.buildTelnetParams();
if (!(await this.validateNetworkConnectivityIfNeeded(telnetParams))) {
return;
}
await this.attemptConnection(telnetParams);
}
buildTelnetParams() {
return {
host: this.validatedConfig.ipAddress,
port: 6681, // Standard IntelliCenter port
negotiationMandatory: false,
timeout: 1500,
debug: true,
username: this.validatedConfig.username,
password: this.validatedConfig.password,
};
}
async validateNetworkConnectivityIfNeeded(telnetParams) {
// Skip network validation in test environments to avoid timeouts
/* eslint-disable-next-line no-undef */
const isTestEnvironment = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
if (isTestEnvironment) {
return true;
}
this.log.debug(`Validating network connectivity to ${telnetParams.host}:${telnetParams.port}...`);
const isReachable = await this.validateNetworkConnectivity(telnetParams.host, telnetParams.port);
if (!isReachable) {
const errorMessage = `IntelliCenter at ${telnetParams.host}:${telnetParams.port} is not reachable. ` + 'Check network connectivity and configuration.';
this.log.error(errorMessage);
this.healthMonitor.recordFailure(errorMessage);
return false;
}
return true;
}
async attemptConnection(telnetParams) {
try {
const startTime = Date.now();
await this.circuitBreaker.execute(async () => {
await errorHandling_1.RetryManager.withRetry(async () => {
this.log.debug(`Attempting connection to IntelliCenter at ${telnetParams.host}:${telnetParams.port}`);
await this.connection.connect(telnetParams);
}, {
maxAttempts: 3,
baseDelay: 1000,
maxDelay: 5000,
backoffFactor: 2,
retryableErrors: ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND', 'EHOSTUNREACH'],
}, message => this.log.warn(`Connection retry: ${message}`));
});
const responseTime = Date.now() - startTime;
this.healthMonitor.recordSuccess(responseTime);
this.log.info(`Successfully connected to IntelliCenter (${responseTime}ms)`);
}
catch (error) {
this.handleConnectionError(error);
}
}
handleConnectionError(error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.healthMonitor.recordFailure(errorMessage);
if (this.circuitBreaker.getState() === errorHandling_1.CircuitBreakerState.OPEN) {
this.log.error('Circuit breaker is OPEN - connection attempts are being rejected. Will retry after cooldown period.');
}
else {
this.log.error(`Connection to IntelliCenter failed after retries: ${errorMessage}`);
}
// Log health status for debugging
const health = this.healthMonitor.getHealth();
this.log.debug(`Connection health: ${health.consecutiveFailures} consecutive failures, ` +
`last success: ${new Date(health.lastSuccessfulOperation)}`);
}
setupSocketEventHandlers() {
events_1.default.defaultMaxListeners = 50;
this.connection.on('data', this.handleDataReceived.bind(this));
this.connection.on('connect', this.handleConnectionEstablished.bind(this));
this.connection.on('ready', this.handleConnectionReady.bind(this));
this.connection.on('failedlogin', this.handleLoginFailed.bind(this));
this.connection.on('close', this.handleConnectionClosed.bind(this));
this.connection.on('error', this.handleSocketError.bind(this));
this.connection.on('end', this.handleConnectionEnded.bind(this));
this.connection.on('responseready', this.handleResponseReady.bind(this));
}
async handleDataReceived(chunk) {
if (this.isCompleteMessage(chunk)) {
await this.processCompleteMessage(chunk);
}
else {
this.bufferIncompleteData(chunk);
}
}
isCompleteMessage(chunk) {
return chunk.length > 0 && chunk[chunk.length - 1] === 10;
}
async processCompleteMessage(chunk) {
this.lastMessageReceived = Date.now();
const bufferedData = this.buffer + chunk;
this.buffer = '';
const lines = bufferedData.split(/\n/);
for (const line of lines) {
await this.processMessageLine(line);
}
}
async processMessageLine(line) {
if (!(line === null || line === void 0 ? void 0 : line.trim())) {
return;
}
const trimmedLine = line.trim();
if (!this.isValidJsonStructure(trimmedLine)) {
this.log.warn(`Skipping malformed JSON line (not properly bracketed): ${trimmedLine}`);
return;
}
try {
const response = JSON.parse(trimmedLine);
await this.handleUpdate(response);
}
catch (error) {
this.log.error(`Failed to parse JSON from IntelliCenter. Line length: ${line.length}, ` +
`First 50 chars: "${line.substring(0, 50)}", Last 50 chars: "${line.substring(Math.max(0, line.length - 50))}"`, error);
}
}
isValidJsonStructure(line) {
return line.startsWith('{') && line.endsWith('}');
}
bufferIncompleteData(chunk) {
if (this.buffer.length + chunk.length > this.maxBufferSize) {
this.log.error(`Exceeded max buffer size ${this.maxBufferSize} without a newline. Discarding buffer.`);
this.buffer = '';
}
else {
this.log.debug('Received incomplete data in data handler.');
this.buffer += chunk;
}
}
handleConnectionEstablished() {
this.isSocketAlive = true;
this.log.debug('IntelliCenter socket connection has been established.');
this.resetDiscoveryState();
this.startDeviceDiscovery();
}
resetDiscoveryState() {
this.discoverCommandsSent.length = 0;
this.discoveryBuffer = null;
this.commandQueue.length = 0;
this.processingQueue = false;
}
startDeviceDiscovery() {
try {
this.discoverDevices();
}
catch (error) {
this.log.error('IntelliCenter device discovery failed.', error);
}
}
handleConnectionReady() {
this.isSocketAlive = true;
this.log.debug('IntelliCenter socket connection is ready.');
}
handleLoginFailed(data) {
this.isSocketAlive = false;
this.log.error(`IntelliCenter login failed. Check configured username/password. ${data}`);
}
handleConnectionClosed() {
this.isSocketAlive = false;
this.log.error('IntelliCenter socket has been closed. Waiting 30 seconds and attempting to reconnect...');
this.delay(30000).then(async () => {
this.log.info('Finished waiting. Attempting reconnect...');
await this.maybeReconnect();
});
}
handleSocketError(data) {
this.isSocketAlive = false;
this.log.error(`IntelliCenter socket error has been detected. Socket will be closed. ${data}`);
}
handleConnectionEnded(data) {
this.isSocketAlive = false;
this.log.error(`IntelliCenter socket connection has ended. ${data}`);
}
handleResponseReady(data) {
this.log.error(`IntelliCenter responseready. ${data}`);
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory) {
this.log.debug('Loading accessory from cache:', accessory.displayName);
// const config = this.getConfig();
// const sensor = accessory.context.sensor;
const heater = accessory.context.heater;
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessoryMap.set(accessory.UUID, accessory);
if (heater) {
this.heaters.set(accessory.UUID, heater);
}
}
logPumpCircuitUpdate(objnam, circuitId, controllingPumpId, params) {
this.log.debug(`[PUMP CIRCUIT UPDATE] ${objnam} -> Circuit ${circuitId} ` + `(Pump: ${controllingPumpId || 'unknown'}): Initial parameter data:`);
this.log.debug(` - STATUS: ${params['STATUS'] || 'N/A'}`);
this.log.debug(` - SPEED: ${params['SPEED'] || 'N/A'}`);
this.log.debug(` - SELECT: ${params['SELECT'] || 'N/A'}`);
this.log.debug(` - RPM: ${params['RPM'] || 'N/A'}`);
this.log.debug(` - GPM: ${params['GPM'] || 'N/A'}`);
this.log.debug(` - WATTS: ${params['WATTS'] || 'N/A'}`);
this.log.debug(` - All parameters: ${this.json(params)}`);
}
logStandalonePumpUpdate(objnam, controllingPumpId, params) {
this.log.debug(`[STANDALONE PUMP UPDATE] ${objnam} (Pump: ${controllingPumpId || 'unknown'}): Full parameter data:`);
this.log.debug(` - STATUS: ${params['STATUS'] || 'N/A'}`);
this.log.debug(` - SPEED: ${params['SPEED'] || 'N/A'}`);
this.log.debug(` - SELECT: ${params['SELECT'] || 'N/A'}`);
this.log.debug(` - RPM: ${params['RPM'] || 'N/A'}`);
this.log.debug(` - GPM: ${params['GPM'] || 'N/A'}`);
this.log.debug(` - WATTS: ${params['WATTS'] || 'N/A'}`);
this.log.debug(` - All parameters: ${this.json(params)}`);
}
handlePumpCircuitUpdate(change) {
const circuit = this.pumpIdToCircuitMap.get(change.objnam);
if (!circuit) {
return false;
}
const controllingPumpId = this.getPumpForPumpCircuit(change.objnam);
this.log.debug(`Update is for pump circuit ${change.objnam} -> Circuit ${circuit.id} ` + `(controlled by pump ${controllingPumpId || 'unknown'})`);
this.logPumpCircuitUpdate(change.objnam, circuit.id, controllingPumpId, change.params);
const uuid = this.api.hap.uuid.generate(circuit.id);
const existingAccessory = this.accessoryMap.get(uuid);
this.updatePump(existingAccessory, change.params);
return true;
}
handleExistingAccessoryUpdate(change) {
var _a, _b;
const uuid = this.api.hap.uuid.generate(change.objnam);
const existingAccessory = this.accessoryMap.get(uuid);
if (!existingAccessory) {
return false;
}
if (types_1.CircuitTypes.has((_a = existingAccessory.context.circuit) === null || _a === void 0 ? void 0 : _a.objectType)) {
this.log.debug(`Object is a circuit. Updating circuit: ${change.objnam}`);
this.updateCircuit(existingAccessory, change.params);
}
else if (types_1.SensorTypes.has((_b = existingAccessory.context.sensor) === null || _b === void 0 ? void 0 : _b.objectType)) {
this.log.debug(`Object is a sensor. Updating sensor: ${change.objnam}`);
this.updateSensor(existingAccessory, change.params);
}
else {
this.log.warn(`Unhandled object type on accessory: ${JSON.stringify(existingAccessory.context)}`);
}
return true;
}
handleStandalonePumpUpdate(change) {
const speed = change.params['SPEED'];
const select = change.params['SELECT'];
if (!speed || !select) {
return false;
}
const controllingPumpId = this.getPumpForPumpCircuit(change.objnam);
this.log.debug(`Standalone pump ${change.objnam} update: ${speed} ${select} (controlled by pump ${controllingPumpId || 'unknown'})`);
this.log.debug(`All pump parameters for ${change.objnam}: ${JSON.stringify(change.params)}`);
this.logStandalonePumpUpdate(change.objnam, controllingPumpId, change.params);
if (controllingPumpId) {
this.log.debug(`[STANDALONE PUMP SENSOR UPDATE] Updating sensors for pump ${controllingPumpId} ` + `due to circuit ${change.objnam} change`);
const pumpCircuit = {
id: change.objnam,
speed: parseInt(speed, 10),
speedType: select,
circuitId: change.params['CIRCUIT'] || 'unknown',
pump: {},
};
this.activePumpCircuits.set(change.objnam, pumpCircuit);
this.log.debug(`Updated activePumpCircuits map for ${change.objnam} with new speed ${speed}`);
this.updatePumpObjectCircuits(controllingPumpId, change.objnam, parseInt(speed, 10));
this.updateAllPumpSensorsForChangedCircuit(pumpCircuit);
}
return true;
}
handleUnregisteredDevice(change) {
this.log.warn(`Device ${change.objnam} sending updates but not registered as accessory. ` + `Params: ${JSON.stringify(change.params)}`);
const objType = change.params['OBJTYP'];
const subType = change.params['SUBTYP'];
const name = change.params['SNAME'];
const feature = change.params['FEATR'];
this.log.info(`Unregistered device details - ID: ${change.objnam}, ` + `Type: ${objType}, SubType: ${subType}, Name: ${name}, Feature: ${feature}`);
}
processChange(change) {
if (!change.objnam || !change.params) {
if (change.objnam) {
this.log.warn(`Device ${change.objnam} sending updates but not registered as accessory. ` + 'No params available for identification.');
}
return;
}
this.log.debug(`Handling update for ${change.objnam}`);
// Try pump circuit update first
if (this.handlePumpCircuitUpdate(change)) {
return;
}
// Try existing accessory update
if (this.handleExistingAccessoryUpdate(change)) {
return;
}
// Try standalone pump update
if (this.handleStandalonePumpUpdate(change)) {
return;
}
// Handle unregistered device
this.handleUnregisteredDevice(change);
}
handleParseError(response) {
const now = Date.now();
if (now - this.parseErrorResetTime > 300000) {
// Reset counter every 5 minutes
this.parseErrorCount = 0;
this.parseErrorResetTime = now;
}
this.parseErrorCount++;
if (this.parseErrorCount <= 3) {
this.log.warn(`IntelliCenter ParseError (${this.parseErrorCount}/3 in 5min): ${response.description}`);
}
else if (this.parseErrorCount === 4) {
this.log.error(`Frequent IntelliCenter ParseErrors detected (${this.parseErrorCount} in 5min). ` +
'This indicates a firmware issue. Consider rebooting your IntelliCenter device.');
}
else if (this.parseErrorCount >= 10) {
this.log.error(`Excessive ParseErrors (${this.parseErrorCount}). Attempting to reconnect...`);
this.maybeReconnect();
}
}
handleErrorResponse(response) {
var _a;
if (!response.response || response.response === types_1.IntelliCenterResponseStatus.Ok) {
return false; // Not an error
}
// Handle specific known error cases
if (response.command === types_1.IntelliCenterResponseCommand.Error && response.response === '400') {
if ((_a = response.description) === null || _a === void 0 ? void 0 : _a.includes('ParseError')) {
this.handleParseError(response);
return true; // Handled
}
}
this.log.error(`Received unsuccessful response code ${response.response} from IntelliCenter. Message: ${this.json(response)}`);
return true; // Handled
}
handleNotifyListResponse(response) {
this.log.debug(`Handling IntelliCenter ${response.response} response to` +
`${response.command}.${response.queryName} for message ID ${response.messageID}: ${this.json(response)}`);
if (!response.objectList) {
this.log.error('Object list missing in NotifyList response.');
return;
}
response.objectList.forEach(objListResponse => {
const changes = (objListResponse.changes || [objListResponse]);
changes.forEach(change => this.processChange(change));
});
}
async handleUpdate(response) {
// Handle errors first
if (this.handleErrorResponse(response)) {
return;
}
// Handle successful requests
if (Object.values(types_1.IntelliCenterRequestCommand).includes(response.command)) {
this.log.debug(`Request with message ID ${response.messageID} was successful.`);
return;
}
// Handle specific response types
if (types_1.IntelliCenterResponseCommand.SendQuery === response.command &&
types_1.IntelliCenterQueryName.GetHardwareDefinition === response.queryName) {
this.handleDiscoveryResponse(response);
}
else if ([types_1.IntelliCenterResponseCommand.NotifyList, types_1.IntelliCenterResponseCommand.WriteParamList].includes(response.command)) {
this.handleNotifyListResponse(response);
}
else {
this.log.debug(`Unhandled command in handleUpdate: ${this.json(response)}`);
}
}
logPumpUpdateStart(pumpCircuitId, params) {
this.log.debug(`Updating pump circuit ${pumpCircuitId} with params:`, this.json(params));
this.log.debug(`[PUMP UPDATE] ${pumpCircuitId}: Full parameter data:`);
this.log.debug(` - STATUS: ${params['STATUS'] || 'N/A'}`);
this.log.debug(` - SPEED: ${params['SPEED'] || 'N/A'}`);
this.log.debug(` - SELECT: ${params['SELECT'] || 'N/A'}`);
this.log.debug(` - RPM: ${params['RPM'] || 'N/A'}`);
this.log.debug(` - GPM: ${params['GPM'] || 'N/A'}`);
this.log.debug(` - WATTS: ${params['WATTS'] || 'N/A'}`);
this.log.debug(` - All parameters: ${this.json(params)}`);
}
updatePumpCircuitProperties(pumpCircuit, params) {
if (params['STATUS']) {
pumpCircuit.status = params['STATUS'];
}
if (params['SPEED']) {
pumpCircuit.speed = Number(params['SPEED']);
}
if (params['RPM']) {
pumpCircuit.rpm = Number(params['RPM']);
}
if (params['GPM']) {
pumpCircuit.gpm = Number(params['GPM']);
}
if (params['WATTS']) {
pumpCircuit.watts = Number(params['WATTS']);
}
}
logPumpUpdateComplete(pumpCircuit) {
this.log.debug(`After update - pump circuit status: ${pumpCircuit.status}, ` + `speed: ${pumpCircuit.speed}, rpm: ${pumpCircuit.rpm}`);
this.log.debug(`[PUMP UPDATE COMPLETE] ${pumpCircuit.id}: Updated values:`);
this.log.debug(` - Final Status: ${pumpCircuit.status || 'N/A'}`);
this.log.debug(` - Final Speed: ${pumpCircuit.speed || 'N/A'}`);
this.log.debug(` - Final Speed Type: ${pumpCircuit.speedType || 'N/A'}`);
this.log.debug(` - Final RPM: ${pumpCircuit.rpm || 'N/A'}`);
this.log.debug(` - Final GPM: ${pumpCircuit.gpm || 'N/A'}`);
this.log.debug(` - Final WATTS: ${pumpCircuit.watts || 'N/A'}`);
}
updatePump(accessory, params) {
const pumpCircuit = accessory.context.pumpCircuit;
this.logPumpUpdateStart(pumpCircuit.id, params);
this.updatePumpCircuitProperties(pumpCircuit, params);
if (pumpCircuit.pump) {
(0, util_1.updatePump)(pumpCircuit.pump, params);
}
this.logPumpUpdateComplete(pumpCircuit);
this.api.updatePlatformAccessories([accessory]);
new circuitAccessory_1.CircuitAccessory(this, accessory);
this.updateAllPumpSensorsForChangedCircuit(pumpCircuit);
}
updateCircuit(accessory, params) {
this.logCircuitUpdate(accessory, params);
this.performCircuitUpdate(accessory, params);
this.updateAccessoryAndCreateCircuit(accessory);
this.handlePumpSensorUpdates(accessory);
}
logCircuitUpdate(accessory, params) {
this.log.debug(`[CIRCUIT UPDATE] ${accessory.context.circuit.id}: Processing circuit update`);
this.log.debug(` - Circuit Type: ${accessory.context.circuit.objectType}`);
this.log.debug(` - Update params: ${JSON.stringify(params)}`);
}
performCircuitUpdate(accessory, params) {
(0, util_1.updateCircuit)(accessory.context.circuit, params);
if (accessory.context.circuit.objectType === types_1.ObjectType.Body) {
this.updateBodyCircuit(accessory.context.circuit, params);
}
}
updateBodyCircuit(body, params) {
(0, util_1.updateBody)(body, params);
if (body.temperature !== undefined && body.temperature !== null) {
this.collectTemperatureReading(body.temperature);
}
// Always update heater accessories when body data changes
this.updateHeaterStatuses(body);
}
updateAccessoryAndCreateCircuit(accessory) {
this.api.updatePlatformAccessories([accessory]);
new circuitAccessory_1.CircuitAccessory(this, accessory);
}
handlePumpSensorUpdates(accessory) {
const circuitId = accessory.context.circuit.id;
const pumpId = this.getPumpForCircuit(circuitId);
if (pumpId) {
this.handlePumpControlledCircuitUpdate(circuitId);
}
else {
this.handleNonPumpControlledCircuitUpdate(accessory, circuitId);
}
}
handlePumpControlledCircuitUpdate(circuitId) {
this.log.debug(` - Circuit ${circuitId} is controlled by pump, triggering sensor updates`);
const pumpCircuitId = this.findPumpCircuitForCircuit(circuitId);
if (pumpCircuitId) {
const pumpCircuit = { id: pumpCircuitId };
this.updateAllPumpSensorsForChangedCircuit(pumpCircuit);
}
}
handleNonPumpControlledCircuitUpdate(accessory, circuitId) {
if (accessory.context.circuit.objectType === types_1.ObjectType.Body) {
this.handleBodyCircuitHeaterUpdate(accessory.context.circuit, circuitId);
}
else {
this.log.debug(` - Circuit ${circuitId} is not controlled by any pump, no sensor updates needed`);
}
}
handleBodyCircuitHeaterUpdate(body, circuitId) {
if (body.heaterId && body.heaterId !== '00000') {
this.log.info(` - Body circuit ${circuitId} has heater ${body.heaterId}, triggering sensor updates for all pumps`);
this.updateAllPumpSensorsForHeaterChange();
}
else {
this.log.debug(` - Circuit ${circuitId} is not controlled by any pump, no sensor updates needed`);
}
}
updateSensor(accessory, params) {
if (accessory.context.sensor) {
const sensor = accessory.context.sensor;
if (sensor.objectType === types_1.ObjectType.Sensor) {
this.log.debug(`Updating temperature sensor ${sensor.name}`);
if (params[constants_1.PROBE_KEY]) {
const probeValue = parseFloat(String(params[constants_1.PROBE_KEY]));
if (isNaN(probeValue)) {
this.log.warn(`Invalid probe value received for sensor ${sensor.name}: ${params[constants_1.PROBE_KEY]}, skipping update`);
return;
}
sensor.probe = probeValue;
// Collect temperature reading for unit validation
this.collectTemperatureReading(probeValue);
new temperatureAccessory_1.TemperatureAccessory(this, accessory).updateTemperature(probeValue);
}
}
}
this.api.updatePlatformAccessories([accessory]);
}
updateFeatureRpmSensorForPumpCircuit(pumpCircuit) {
const featureRpmAccessory = this.findFeatureRpmAccessory(pumpCircuit);
if (featureRpmAccessory) {
this.updateFeatureRpmAccessory(featureRpmAccessory, pumpCircuit);
}
else {
this.logRpmSensorNotFound(pumpCircuit);
}
}
findFeatureRpmAccessory(pumpCircuit) {
// First try with the pump circuit's circuitId
const primarySensorId = `${pumpCircuit.circuitId}-rpm`;
const uuid = this.api.hap.uuid.generate(primarySensorId);
let featureRpmAccessory = this.accessoryMap.get(uuid);
this.log.debug(`Looking for feature RPM sensor with ID: ${primarySensorId} (pump circuit: ${pumpCircuit.id} -> circuit: ${pumpCircuit.circuitId})`);
// If not found, search through all RPM sensors to find one that uses this pump circuit
if (!featureRpmAccessory) {
featureRpmAccessory = this.searchForMatchingRpmSensor(pumpCircuit);
}
return featureRpmAccessory;
}
searchForMatchingRpmSensor(pumpCircuit) {
this.log.debug(`Primary sensor ID not found, searching for RPM sensor that uses pump circuit ${pumpCircuit.id}`);
let foundAccessory;
this.accessoryMap.forEach(accessory => {
if (this.isMatchingRpmSensor(accessory, pumpCircuit)) {
foundAccessory = accessory;
this.log.debug(`Found matching RPM sensor: ${accessory.displayName} (ID: ${accessory.context.feature.id}-rpm)`);
}
});
return foundAccessory;
}
isMatchingRpmSensor(accessory, pumpCircuit) {
var _a, _b, _c;
return !!(accessory.context.feature &&
accessory.context.pumpCircuit &&
accessory.context.pumpCircuit.id === pumpCircuit.id &&
((_a = accessory.displayName) === null || _a === void 0 ? void 0 : _a.includes('RPM')) &&
!((_b = accessory.displayName) === null || _b === void 0 ? void 0 : _b.includes('Heater')) &&
!((_c = accessory.displayName) === null || _c === void 0 ? void 0 : _c.includes('Gas')));
}
updateFeatureRpmAccessory(featureRpmAccessory, pumpCircuit) {
if (!featureRpmAccessory.context.feature || !featureRpmAccessory.context.pumpCircuit) {
return;
}
this.log.debug(`Found and updating feature RPM sensor for ${featureRpmAccessory.context.feature.name}: ${pumpCircuit.speed} RPM`);
// Update the pump circuit data in the accessory context
featureRpmAccessory.context.pumpCircuit = pumpCircuit;
this.api.updatePlatformAccessories([featureRpmAccessory]);
// Create new PumpRpmAccessory instance and trigger immediate RPM update
const rpmAccessory = new pumpRpmAccessory_1.PumpRpmAccessory(this, featureRpmAccessory);
const isActive = featureRpmAccessory.context.feature.status === types_1.CircuitStatus.On && pumpCircuit.speed > 0;
const rpmValue = isActive ? pumpCircuit.speed : 0.0001; // HomeKit minimum for inactive
rpmAccessory.updateRpm(rpmValue);
}
logRpmSensorNotFound(pumpCircuit) {
const primarySensorId = `${pumpCircuit.circuitId}-rpm`;
this.log.debug(`No feature RPM sensor found for pump circuit ${pumpCircuit.id} -> circuit ${pumpCircuit.circuitId} ` +
`(tried both direct ID ${primarySensorId} and pump circuit matching)`);
}
updateFeatureRpmSensorForCircuit(circuit) {
var _a;
// Find the feature RPM sensor for this circuit
const featureRpmSensorId = `${circuit.id}-rpm`;
const uuid = this.api.hap.uuid.generate(featureRpmSensorId);
const featureRpmAccessory = this.accessoryMap.get(uuid);
if (featureRpmAccessory && featureRpmAccessory.context.feature && featureRpmAccessory.context.pumpCircuit) {
this.log.debug(`Updating feature RPM sensor for circuit change: ${circuit.name}`);
// Update the feature data in the accessory context
featureRpmAccessory.context.feature = circuit;
// Refresh the RPM display (pump circuit data is already current)
const rpmAccessory = new pumpRpmAccessory_1.PumpRpmAccessory(this, featureRpmAccessory);
// Trigger immediate RPM update based on current status
if (circuit.status === types_1.CircuitStatus.On && ((_a = featureRpmAccessory.context.pumpCircuit) === null || _a === void 0 ? void 0 : _a.speed) > 0) {
rpmAccessory.updateRpm(featureRpmAccessory.context.pumpCircuit.speed);
}
else {
rpmAccessory.updateRpm(0.0001); // HomeKit minimum for inactive
}
}
}
updateHeaterStatuses(body) {
this.heaters.forEach(heaterAccessory => {
var _a, _b, _c;
if (((_b = (_a = heaterAccessory.context) === null || _a === void 0 ? void 0 : _a.body) === null || _b === void 0 ? void 0 : _b.id) === body.id) {
this.log.debug(`Updating heater ${heaterAccessory.displayName} with live body data ` + `(temp: ${body.temperature}, heatMode: ${body.heatMode})`);
// Update the accessory context with latest body data
heaterAccessory.context.body = body;
this.api.updatePlatformAccessories([heaterAccessory]);
// Get or create HeaterAccessory instance and update temperature ranges
let heaterInstance = this.heaterInstances.get(heaterAccessory.UUID);
if (!heaterInstance) {
heaterInstance = new heaterAccessory_1.HeaterAccessory(this, heaterAccessory);
this.heaterInstances.set(heaterAccessory.UUID, heaterInstance);
}
else {
// Critical: Update the heater accessory with live body data
heaterInstance.updateTemperatureRanges(body);
}
// Update the corresponding heater RPM sensor
this.updateHeaterRpmSensor(heaterAccessory.context.heater, body);
}
else {
this.log.debug(`Not updating heater because body id of heater ${(_c = heaterAccessory.context.body) === null || _c === void 0 ? void 0 : _c.id} ` + `doesn't match input body ID ${body.id}`);
}
});
}
updateHeaterRpmSensor(heater, body) {
// Guard against undefined heater or body
if (!heater || !body) {
this.log.warn(`Cannot update heater RPM sensor: heater or body is undefined (heater: ${heater}, body: ${body})`);
return;
}
// Find the heater RPM sensor for this heater and body
const heaterRpmSensorId = `${heater.id}.${body.id}-rpm`;
const uuid = this.api.hap.uuid.generate(heaterRpmSensorId);
const heaterRpmAccessory = this.accessoryMap.get(uuid);
if (heaterRpmAccessory && heaterRpmAccessory.context.feature && heaterRpmAccessory.context.pumpCircuit) {
this.log.debug(`Updating heater RPM sensor for ${heater.name}: checking heater status`);
// Determine if this heater is currently active for this body
// A heater is active if it's selected for the body and the body is on
const isHeaterActive = body.heaterId === heater.id && body.status === types_1.CircuitStatus.On;
// Update the feature status to reflect the heater's active state
heaterRpmAccessory.context.feature.status = isHeaterActive ? types_1.CircuitStatus.On : types_1.CircuitStatus.Off;
this.log.debug(` Heater ${heater.name} active: ${isHeaterActive} ` +
`(body status: ${body.status}, body heaterId: ${body.heaterId}, heater id: ${heater.id})`);
// Update the accessory and refresh the RPM display
this.api.updatePlatformAccessories([heaterRpmAccessory]);
new pumpRpmAccessory_1.PumpRpmAccessory(this, heaterRpmAccessory);
}
}
updateHeaterRpmSensorsForPumpCircuit(pumpCircuit) {
// Find all heater RPM sensors that use this pump circuit by matching the pump circuit ID
this.accessoryMap.forEach((accessory, _uuid) => {
var _a, _b;
// Check if this is a heater RPM sensor by examining the context
if (accessory.context.feature &&
accessory.context.pumpCircuit &&
accessory.context.feature.bodyId &&
(((_a = accessory.displayName) === null || _a === void 0 ? void 0 : _a.includes('Heater')) || ((_b = accessory.displayName) === null || _b === void 0 ? void 0 : _b.includes('Gas')))) {
// Check if this heater RPM sensor uses the same pump circuit by ID
// This is more reliable than matching by speed since IDs are unique
if (accessory.context.pumpCircuit.id === pumpCircuit.id) {
this.log.debug(`Updating heater RPM sensor pump circuit for ${accessory.displayName}: ${pumpCircuit.speed} RPM ` +
`(was ${accessory.context.pumpCircuit.speed} RPM)`);
// Update the pump circuit data in the accessory context
accessory.context.pumpCircuit = { ...pumpCircuit };
// Update the accessory and refresh the RPM display
this.api.updatePlatformAccessories([accessory]);
// Create new PumpRpmAccessory instance to refresh the display with updated data
const rpmAccessory = new pumpRpmAccessory_1.PumpRpmAccessory(this, accessory);
// If the heater is currently active, also trigger an immediate RPM update
if (accessory.context.feature.status === types_1.CircuitStatus.On && pumpCircuit.speed > 0) {
rpmAccessory.updateRpm(pumpCircuit.speed);
}
}
}
});
}
/**
* Validate speed value for standalone pump
*/
validateStandalonePumpSpeed(pumpId, speed) {
const speedValue = parseInt(speed, 10);
if (isNaN(speedValue)) {
this.log.warn(`Invalid speed value for standalone pump ${pumpId}: ${speed}`);
return null;
}
return speedValue;
}
/**
* Check if accessory is a heater RPM sensor
*/
isHeaterRpmSensor(accessory) {
var _a, _b;
return !!(accessory.context.feature &&
accessory.context.pumpCircuit &&
accessory.context.feature.bodyId &&
(((_a = accessory.displayName) === null || _a === void 0 ? void 0 : _a.includes('Heater')) || ((_b = accessory.displayName) === null || _b === void 0 ? void 0 : _b.includes('Gas'))));
}
/**
* Check if speed is in heater range
*/
isSpeedInHeaterRange(speedValue, speedType) {
return speedValue >= 2000 && speedValue <= 3500 && speedType === 'RPM';
}
/**
* Update a single heater RPM sensor
*/
updateSingleHeaterRpmSensor(accessory, pumpId, speedValue, speedType) {
this.log.debug(`Updating heater RPM sensor ${accessory.displayName} with standalone pump ${pumpId}: ${speedValue} RPM ` +
`(heater ${accessory.context.feature.status})`);
// Update the pump circuit speed in the accessory context
accessory.context.pumpCircuit.speed = speedValue;
accessory.context.pumpCircuit.speedType = speedType;
// Update the accessory and refresh the RPM display
this.api.updatePlatformAccessories([accessory]);
// Create new PumpRpmAccessory instance and trigger immediate update
const rpmAccessory = new pumpRpmAccessory_1.PumpRpmAccessory(this, accessory);
// Show RPM if heater is active, otherwise show minimum value
if (accessory.context.feature.status === types_1.CircuitStatus.On) {
rpmAccessory.updateRpm(speedValue);
}
else {
rpmAccessory.updateRpm(0.0001);
}
}
updateHeaterRpmSensorsForStandalonePump(pumpId, speed, speedType) {
const speedValue = this.validateStandalonePumpSpeed(pumpId, speed);
if (speedValue === null) {
return;
}
this.log.debug(`Checking heater RPM sensors for standalone pump ${pumpId} at ${speedValue} ${speedType}`);
// Log comprehensive standalone pump update data for heater RPM sensor processing
this.log.info(`[HEATER RPM SENSOR UPDATE] Processing standalone pump ${pumpId}:`);
this.log.info(` - Speed Value: ${speedValue}`);
this.log.info(` - Speed Type: ${speedType}`);
this.log.info(' - Processing heater RPM sensors in range: 2000-3500 RPM');
// Find all heater RPM sensors and check if they should be updated based on this standalone pump
this.accessoryMap.forEach((accessory, _uuid) => {
if (this.isHeaterRpmSensor(accessory) && this.isSpeedInHeaterRange(speedValue, speedType)) {
this.updateSingleHeaterRpmSensor(accessory, pumpId, speedValue, speedType);
}
});
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
discoverDevices() {
const firstCommand = constants_1.DISCOVER_COMMANDS[0];
if (firstCommand) {
this.discoverDeviceType(firstCommand);
}
}
discoverDeviceType(deviceType) {
this.discoverCommandsSent.push(deviceType);
const command = {
command: types_1.IntelliCenterRequestCommand.GetQuery,
queryName: types_1.IntelliCenterQueryName.GetHardwareDefinition,
arguments: deviceType,
messageID: (0, uuid_1.v4)(),
};
// Set discovery timeout for this command
this.discoveryTimeout = setTimeout(() => {
this.handleDiscoveryTimeout(deviceType);
}, 30000); // 30 second timeout per discovery command
this.sendCommandNoWait(command);
}
/**
* Handle discovery command timeout
*/
handleDiscoveryTimeout(deviceType) {
this.log.warn(`Discovery command timeout for device type: ${deviceType}`);
// Add to failed commands list for potential retry
if (!this.discoverCommandsFailed.includes(deviceType)) {
this.discoverCommandsFailed.push(deviceType);
}
// Clear timeout
this.discoveryTimeout = null;
// Continue with next command if available
const nextCommandIndex = this.discoverCommandsSent.length;
if (nextCommandIndex < constants_1.DISCOVER_COMMANDS.length) {
this.log.debug('Timeout occurred, continuing with next discovery command...');
setTimeout(() => {
const nextCommand = constants_1.DISCOVER_COMMANDS[nextCommandIndex];
if (nextCommand) {
this.discoverDeviceType(nextCommand);
}
}, 1000);
}
else {
// All commands sent, check if we have enough data to proceed
this.completeDiscoveryWithPartialData();
}
}
/**
* Complete discovery even with partial data from failed commands
*/
completeDiscoveryWithPartialData() {
if (this.discoverCommandsFailed.length > 0) {
this.log.warn(`Discovery completed with partial data. Failed commands: ${this.discoverCommandsFailed.join(', ')}. ` +
'Proceeding with available device data.');
}
this.log.debug(`Discovery commands completed with partial data. Response: ${this.json(this.discoveryBuffer)}`);
const panels = (0, util_1.transformPanels)(this.discoveryBuffer, this.getConfig().includeAllCircuits, this.log);
this.log.debug(`Transformed panels from IntelliCenter: ${this.json(panels)}`);
this.registerDiscoveredAccessories(panels);
}
handleDiscoveryResponse(response) {
this.clearDiscoveryTimeout();
this.mergeDiscoveryResponse(response);
const commandCounts = this.getDiscoveryCommandCounts();
if (this.shouldContinueDiscovery(commandCounts)) {
this.sendNextDiscoveryCommand(commandCounts);
return;
}
if (this.shouldRetryFailedCommands(commandCounts)) {
return;
}
this.completeDiscovery();
}
clearDiscoveryTimeout() {
if (this.discoveryTimeout) {
clearTimeout(this.discoveryTimeout);
this.discoveryTimeout = null;
}
}
mergeDiscoveryResponse(response) {
var _a;
this.log.debug(`Discovery response from IntelliCenter: ${this.json(response)} ` +
`of type ${this.discoverCommandsSent[this.discoverCommandsSent.length - 1]}`);
if (this.discoveryBuffer === null) {
this.discoveryBuffer = (_a = response.answer) !== null && _a !== void 0 ? _a : null;
}
else if (this.discoveryBuffer && response.answer) {
(0, util_1.mergeResponse)(this.discoveryBuffer, response.answer);
}
}
getDiscoveryCommandCounts() {
return {
total: constants_1.DISCOVER_COMMANDS.length,
completed: this.discoverCommandsSent.length,
failed: this.discoverCommandsFailed.length,
};
}
shouldContinueDiscovery(counts) {
return counts.completed < counts.total;
}
sendNextDiscoveryCommand(counts) {
this.log.debug(`Merged ${counts.completed} of ${counts.total} so far. Sending next command..`);
// Add conservative delay between discovery commands to avoid overwhelming IntelliCenter
setTimeout(() => {
const nextCommand = constants_1.DISCOVER_COMMANDS[counts.completed];
if (nextCommand) {
this.discoverDeviceType(nextCommand);
}
}, 500);
}
shouldRetryFailedCommands(counts) {
if (counts.failed === 0 || counts.completed !== counts.total) {
return false;
}
for (const failedCommand of this.discoverCommandsFailed) {
if (this.shouldRetryCommand(failedCommand)) {
this.retryFailedCommand(failedCommand);
return true;
}
}
return false;
}
shouldRetryCommand(failedCommand) {
const retryAttempts = this.discoverCommandsSent.filter(cmd => cmd === failedCommand).length;
return retryAttempts === 1;
}
retryFailedCommand(failedCommand) {
this.log.warn(`Retrying failed discovery command: ${failedCommand}`);
setTimeout(() => {
this.discoverDeviceType(failedCommand);
}, 1000);
}
completeDiscovery() {
this.log.debug(`Discovery commands completed. Response: ${this.json(this.discoveryBuffer)}`);
const panels = (0, util_1.transformPanels)(this.discoveryBuffer, this.getConfig().includeAllCircuits, this.log);
this.log.debug(`Transformed panels from IntelliCenter: ${this.json(panels)}`);
this.registerDiscoveredAccessories(panels);
// Start temperature unit validation monitoring after discovery
this.startTemperatureUnitValidation();
}
initializeDiscoveryState() {
this.pumpIdToCircuitMap.clear();
this.pumpToCircuitsMap.clear();
this.circuitToPumpMap.clear();
this.pumpCircuitToPumpMap.clear();
this.activePumpCircuits.clear();
}
processPanelSensors(panel, discoveredAccessoryIds) {
for (const sensor of panel.sensors) {
discoveredAccessoryIds.add(sensor.id);
this.discoverTemperatureSensor(panel, null, sensor);
}
}
processPumpCircuits(pump, circuitIdPumpMap) {
for (const pumpCircuit of pump.circuits) {
circuitIdPumpMap.set(pumpCircuit.circuitId, pumpCircuit);
this.activePumpCircuits.set(pumpCircuit.id, pumpCircuit);
this.subscribeForUpdates(pumpCircuit, [constants_1.STATUS_KEY, constants_1.ACT_KEY, constants_1.SPEED_KEY, constants_1.SELECT_KEY, 'RPM', 'GPM', 'WATTS']);
this.buildPumpCircuitAssociations(pump.id, pumpCircuit);
}
}
createPumpSensors(pump, panel, discoveredAccessoryIds) {
const pumpRpmSensorId = `${pump.id}-rpm`;
const pumpGpmSensorId = `${pump.id}-gpm`;
const pumpWattsSensorId = `${pump.id}-watts`;
discoveredAccessoryIds.add(pumpRpmSensorId);
discoveredAccessoryIds.add(pumpGpmSensorId);
discoveredAccessoryIds.add(pumpWattsSensorId);
this.discoverPumpRpmSensor(panel, pump);
this.discoverPumpGpmSensor(panel, pump);
this.discoverPumpWattsSensor(panel, pump);
}
processPanelPumps(panel, discoveredAccessoryIds, circuitIdPumpMap) {
for (const pump of panel.pumps) {
this.processPumpCircuits(pump, circuitIdPumpMap);
this.logPumpDiscoveryMapping(pump, panel);
this.createPumpSensors(pump, panel, discoveredAccessoryIds);
}
this.logPumpCircuitAssociations();
}
/**
* Register discovered accessories with HomeKit
*/
registerDiscoveredAccessories(panels) {
const context = this.createDiscoveryContext();
this.processAllPanels(panels, context);
this.finalizeDiscovery(context);
}
createDiscoveryContext() {
this.initializeDiscoveryState();
return {
discoveredAccessoryIds: new Set(),
circuitIdPumpMap: new Map(),
bodyIdMap: new Map(),
heaters: [],
};
}
processAllPanels(panels, context) {
for (const panel of panels) {
this.processSinglePanel(panel, context);
}
}
processSinglePanel(panel, context) {
this.processPanelSensors(panel, context.discoveredAccessoryIds);
this.processPanelPumps(panel, context.discoveredAccessoryIds, context.circuitIdPumpMap);
this.processModuleBodies(panel, context.discoveredAccessoryIds, context.circuitIdPumpMap, context.bodyIdMap);
this.processModuleFeatures(panel, context.discoveredAccessoryIds, context.circuitIdPumpMap);
this.processPanelFeatures(panel, context.discoveredAccessoryIds, context.circuitIdPumpMap);
context.heaters = this.collectModuleHeaters(panel, context.heaters);
}
finalizeDiscovery(context) {
this.processHeaters(context.heaters, context.discoveredAccessoryIds, context.circuitIdPumpMap, context.bodyIdMap);
this.cleanupOrphanedAccessories(context.discoveredAccessoryIds);
}
processModuleBodies(panel, discoveredAccessoryIds, circuitIdPumpMap, bodyIdMap) {
var _a;
for (const module of panel.modules) {
for (const body of module.bodies) {
discoveredAccessoryIds.add(body.id);
const pumpCircuit = circuitIdPumpMap.get((_a = body.circuit) === null || _a === void 0 ? void 0 : _a.id);
this.discoverCircuit(panel, module, body, pumpCircuit);
this.associateBodyWithPump(body, pumpCircuit);
this.subscribeForUpdates(body, [constants_1.STATUS_KEY, constants_1.LAST_TEMP_KEY, constants_1.HEAT_SOURCE_KEY, constants_1.HEATER_KEY, constants_1.HTMODE_KEY, constants_1.HIGH_TEMP_KEY, constants_1.LOW_TEMP_KEY]);
bodyIdMap.set(body.id, body);
}
}
}
associateBodyWithPump(body, pumpCircuit) {
var _a;
if (pumpCircuit && ((_a = body.circuit) === null || _a === void 0 ? void 0 : _a.id)) {
const pumpId = this.getPumpForPumpCircuit(pumpCircuit.id);
if (pumpId) {
this.circuitToPumpMap.set(body.circuit.id, pumpId);
if (!this.pumpToCircuitsMap.has(pumpId)) {
this.pumpToCircuitsMap.set(pumpId, new Set());
}
this.pumpToCircuitsMap.get(pumpId).add(body.circuit.id);
}
}
}
processModuleFeatures(panel, discoveredAccessoryIds, circuitIdPumpMap) {
for (const module of panel.modules) {
for (const feature of module.features) {
discoveredAccessoryIds.add(feature.id);
const pumpCircuit = circuitIdPumpMap.get(feature.id);
this.discoverCircuit(panel, module, feature, pumpCircuit);
this.subscribeForUpdates(feature, [constants_1.STATUS_KEY, constants_1.ACT_KEY]);
}
}
}
processPanelFeatures(panel, discoveredAccessoryIds, circuitIdPumpMap) {
for (const feature of panel.features) {
discoveredAccessoryIds.add(feature.id);
const pumpCircuit = circuitIdPumpMap.get(feature.id);
this.discoverCircuit(panel, null, feature, pumpCircuit);
this.subscribeForUpdates(feature, [constants_1.STATUS_KEY, constants_1.ACT_KEY]);
}
}
collectModuleHeaters(panel, heaters) {
for (const module of panel.modules) {
heaters = heaters.concat(module.heaters);
}
return heaters;
}
processHeaters(heaters, discoveredAccessoryIds, circuitIdPumpMap, bodyIdMap) {
for (const heater of heaters) {
heater.bodyIds.forEach(bodyId => {
discoveredAccessoryIds.add(`${heater.id}.${bodyId}`);
this.findHeaterPumpCircuit(heater, bodyId, circuitIdPumpMap, bodyIdMap);
});
this.discoverHeater(heater, bodyIdMap);
}
}
calculateHeaterPumpPriority(pumpCircuit, body) {
var _a, _b, _c;
if (((_b = (_a = pumpCircuit.pump) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('heater')) || ((_c = body === null || body === void 0 ? void 0 : body.name) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes('heater'))) {
return 100;
}
if (pumpCircuit.speed >= 2500 && pumpCircuit.speed <= 3200) {
return 90;
}
if (pumpCircuit.speed >= 2000 && pumpCircuit.speed < 2500) {
return 85;
}
return 0;
}
isValidHeaterPumpCircuit(pumpCircuit) {
return pumpCircuit.speedType === 'RPM' && pumpCircuit.speed >= 1000;
}
findHeaterPumpCircuit(heater, bodyId, circuitIdPumpMap, bodyIdMap) {
const body = bodyIdMap.get(bodyId);
const heaterRpmCandidates = [];
for (const [, pumpCircuit] of circuitIdPumpMap.entries()) {
if (!this.isValidHeaterPumpCircuit(pumpCircuit)) {
continue;
}
const priority = this.calculateHeaterPumpPriority(pumpCircuit, body);
if (priority > 0) {
heaterRpmCandidates.push({ circuit: pumpCircuit, priority });
}
}
if (heaterRpmCandidates.length > 0) {
heaterRpmCandidates.sort((a, b) => b.priority - a.priority);
}
}
getExpectedAccessoryId(accessory) {
if (accessory.context.circuit) {
return accessory.context.circuit.id;
}
if (accessory.context.sensor) {
return accessory.context.sensor.id;
}
if (accessory.context.heater && accessory.context.body) {
return `${accessory.context.heater.id}.${accessory.context.body.id}`;
}
if (accessory.context.feature && accessory.context.pumpCircuit) {
this.log.info(`Removing old feature/circuit RPM sensor (now pump-level): ${accessory.displayName}`);
return 'REMOVE_OLD_FEATURE_RPM_SENSORS';
}
return this.handlePumpAccessoryId(accessory);
}
handlePumpAccessoryId(accessory) {
var _a, _b, _c;
if (accessory.context.pump && ((_a = accessory.displayName) === null || _a === void 0 ? void 0 : _a.includes('GPM'))) {
return this.handlePumpGpmSensor(accessory);
}
if (accessory.context.pump && ((_b = accessory.displayName) === null || _b === void 0 ? void 0 : _b.includes('RPM'))) {
return `${accessory.context.pump.id}-rpm`;
}
if (accessory.context.pump && ((_c = accessory.displayName) === null || _c === void 0 ? void 0 : _c.includes('WATTS'))) {
return `${accessory.context.pump.id}-watts`;
}
if (accessory.context.pumpCircuit) {
this.log.info(`Removing old pump circuit sensor: ${accessory.displayName}`);
return 'REMOVE_OLD_PUMP_CIRCUIT_SENSORS';
}
return null;
}
handlePumpGpmSensor(accessory) {
const pumpType = constants_1.PUMP_TYPE_MAPPING.get(accessory.context.pump.type) || accessory.context.pump.type;
if (pumpType === 'VF' || pumpType === 'VS') {
this.log.info(`Removing ${pumpType} pump GPM sensor (no longer supported): ${accessory.displayName}`);
return 'REMOVE_VS_VF_GPM_SENSORS';
}
return `${accessory.context.pump.id}-gpm`;
}
removeOrphanedAccessory(accessory, accessoryUuid, accessoriesToRemove, expectedId) {
this.log.info(`Removing orphaned accessory: ${accessory.displayName} (expected ID: ${expectedId})`);
accessoriesToRemove.push(accessory);
this.accessoryMap.delete(accessoryUuid);
this.heaters.delete(accessoryUuid);
this.heaterInstances.delete(accessoryUuid);
}
cleanupOrphanedAccessories(discoveredAccessoryIds) {
const accessoriesToRemove = [];
this.accessoryMap.forEach((accessory, accessoryUuid) => {
const expectedId = this.getExpectedAccessoryId(accessory);
if (expectedId && !discoveredAccessoryIds.has(expectedId)) {
this.removeOrphanedAccessory(accessory, accessoryUuid, accessoriesToRemove, expectedId);
}
});
if (accessoriesToRemove.length > 0) {
this.log.info(`Cleaning up ${accessoriesToRemove.length} orphaned accessories`);
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, accessoriesToRemove);
}
}
discoverHeater(heater, bodyMap) {
heater.bodyIds.forEach(bodyId => {
const body = bodyMap.get(bodyId);
if (body) {
const uuid = this.api.hap.uuid.generate(`${heater.id}.${bodyId}`);
let accessory = this.accessoryMap.get(uuid);
const name = `${body.name} ${heater.name}`;
if (accessory) {
this.log.debug(`Restoring existing heater from cache: ${accessory.displayName}`);
accessory.context.body = body;
accessory.context.heater = heater;
this.api.updatePlatformAccessories([accessory]);
const heaterInstance = new heaterAccessory_1.HeaterAccessory(this, accessory);
this.heaterInstances.set(accessory.UUID, heaterInstance);
}
else {
this.log.debug(`Adding new heater: ${heater.name}`);
accessory = new this.api.platformAccessory(name, uuid);
accessory.context.body = body;
accessory.context.heater = heater;
const heaterInstance = new heaterAccessory_1.HeaterAccessory(this, accessory);
this.heaterInstances.set(accessory.UUID, heaterInstance);
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
this.accessoryMap.set(accessory.UUID, accessory);
}
this.heaters.set(uuid, accessory);
}
else {
this.log.error(`Body not in bodyMap for ID ${bodyId}. Map: ${this.json(bodyMap)}`);
}
});
}
discoverCircuit(panel, module, circuit, pumpCircuit) {
const uuid = this.api.hap.uuid.generate(circuit.id);
const existingAccessory = this.accessoryMap.get(uuid);
// Get pump association for this circuit
const controllingPumpId = this.getPumpForCircuit(circuit.id);
if (existingAccessory) {
this.log.debug(`Restoring existing circuit from cache: ${existingAccessory.displayName}`);
existingAccessory.context.circuit = circuit;
existingAccessory.context.module = module;
existingAccessory.context.panel = panel;
existingAccessory.context.pumpCircuit = pumpCircuit;
existingAccessory.context.controllingPumpId = controllingPumpId;
this.api.updatePlatformAccessories([existingAccessory]);
new circuitAccessory_1.CircuitAccessory(this, existingAccessory);
}
else {
this.log.debug(`Adding new circuit: ${circuit.name}${controllingPumpId ? ` (controlled by pump ${controllingPumpId})` : ''}`);
const accessory = new this.api.platformAccessory(circuit.name, uuid);
accessory.context.circuit = circuit;
accessory.context.module = module;
accessory.context.panel = panel;
accessory.context.pumpCircuit = pumpCircuit;
accessory.context.controllingPumpId = controllingPumpId;
new circuitAccessory_1.CircuitAccessory(this, accessory);
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
this.accessoryMap.set(accessory.UUID, accessory);
}
if (pumpCircuit) {
this.pumpIdToCircuitMap.set(pumpCircuit.id, circuit);
}
}
discoverTemperatureSensor(panel, module, sensor) {
const uuid = this.api.hap.uuid.generate(sensor.id);
const hasHeater = panel.modules.some(m => m.heaters.length > 0);
const existingAccessory = this.accessoryMap.get(uuid);
let remove = false;
this.log.debug(`Config ${this.json(this.getConfig())}`);
if (!this.getConfig().airTemp && sensor.type === types_1.TemperatureSensorType.Air) {
this.log.debug(`Skipping air temperature sensor ${sensor.name} because air temperature is disabled in config`);
remove = true;
}
if (sensor.type === types_1.TemperatureSensorType.Pool && hasHeater) {
this.log.debug(`Skipping water temperature sensor ${sensor.name} because a heater is installed`);
remove = true;
}
if (remove) {
if (existingAccessory) {
this.accessoryMap.delete(uuid);
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [existingAccessory]);
}
return;
}
if (existingAccessory) {
this.log.debug(`Restoring existing temperature sensor from cache: ${existingAccessory.displayName}`);
existingAccessory.context.sensor = sensor;
existingAccessory.context.module = module;
existingAccessory.context.panel = panel;
this.api.updatePlatformAccessories([existingAccessory]);
new temperatureAccessory_1.TemperatureAccessory(this, existingAccessory);
}
else {
this.log.debug(`Adding new temperature sensor: ${sensor.name} of type ${sensor.type}`);
const accessory = new this.api.platformAccessory(sensor.name, uuid);
accessory.context.sensor = sensor;
accessory.context.module = module;
accessory.context.panel = panel;
new temperatureAccessory_1.TemperatureAccessory(this, accessory);
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
this.accessoryMap.set(accessory.UUID, accessory);
}
this.subscribeForUpdates(sensor, [constants_1.PROBE_KEY]);
}
discoverFeatureRpmSensor(panel, feature, pumpCircuit) {
const featureRpmSensorId = `${feature.id}-rpm`;
const uuid = this.api.hap.uuid.generate(featureRpmSensorId);
const existingAccessory = this.accessoryMap.get(uuid);
// Get the pump object from the pumpCircuit
const pump = pumpCircuit.pump;
// Use the feature name directly - much cleaner!
const displayName = `${feature.name} RPM`;
if (existingAccessory) {
this.log.debug(`Restoring existing feature RPM sensor from cache: ${existingAccessory.displayName}`);
existingAccessory.context.feature = feature;
existingAccessory.context.pumpCircuit = pumpCircuit;
existingAccessory.context.pump = pump;
existingAccessory.context.panel = panel;
this.api.updatePlatformAccessories([existingAccessory]);
new pumpRpmAccessory_1.PumpRpmAccessory(this, existingAccessory);
}
else {
this.log.debug(`Adding new feature RPM sensor: ${displayName}`);
const accessory = new this.api.platformAccessory(displayName, uuid);
accessory.context.feature = feature;
accessory.context.pumpCircuit = pumpCircuit;
accessory.context.pump = pump;
accessory.context.panel = panel;
new pumpRpmAccessory_1.PumpRpmAccessory(this, accessory);
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
this.accessoryMap.set(accessory.UUID, accessory);
}
}
discoverBodyRpmSensor(panel, body, pumpCircuit) {
const bodyRpmSensorId = `${body.id}-rpm`;
const uuid = this.api.hap.uuid.generate(bodyRpmSensorId);
const existingAccessory = this.accessoryMap.get(uuid);
// Get the pump object from the pumpCircuit
const pump = pumpCircuit.pump;
// Use the body name directly (e.g., "Pool RPM", "Spa RPM")
const displayName = `${body.name} RPM`;
if (existingAccessory) {
this.log.debug(`Restoring existing body RPM sensor from cache: ${existingAccessory.displayName}`);
existingAccessory.context.feature = body; // Bodies act like features for RPM sensors
existingAccessory.context.pumpCircuit = pumpCircuit;
existingAccessory.context.pump = pump;
existingAccessory.context.panel = panel;
this.api.updatePlatformAccessories([existingAccessory]);
new pumpRpmAccessory_1.PumpRpmAccessory(this, existingAccessory);
}
else {
this.log.debug(`Adding new body RPM sensor: ${displayName}`);
const accessory = new this.api.platformAccessory(displayName, uuid);
accessory.context.feature = body; // Bodies act like features for RPM sensors
accessory.context.pumpCircuit = pumpCircuit;
accessory.context.pump = pump;
accessory.context.panel = panel;
new pumpRpmAccessory_1.PumpRpmAccessory(this, accessory);
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
this.accessoryMap.set(accessory.UUID, accessory);
}
}
discoverHeaterRpmSensor(panel, heater, body, pumpCircuit) {
const heaterRpmSensorId = `${heater.id}.${body.id}-rpm`;
const uuid = this.api.hap.uuid.generate(heaterRpmSensorId);
const existingAccessory = this.accessoryMap.get(uuid);
// Get the pump object from the pumpCircuit
const pump = pumpCircuit.pump;
// Use the heater name directly (e.g., "Spa Gas Heater RPM")
const displayName = `${heater.name} RPM`;
// Determine initial heater status - active if heater is selected for this body and body is on
const initialStatus = body.heaterId === heater.id && body.status === types_1.CircuitStatus.On ? types_1.CircuitStatus.On : types_1.CircuitStatus.Off;
if (existingAccessory) {
this.log.debug(`Restoring existing heater RPM sensor from cache: ${existingAccessory.displayName}`);
// Create feature-like object with bodyId
existingAccessory.context.feature = { id: heater.id, name: heater.name, status: initialStatus, bodyId: body.id };
existingAccessory.context.pumpCircuit = pumpCircuit;
existingAccessory.context.pump = pump;
existingAccessory.context.panel = panel;
this.api.updatePlatformAccessories([existingAccessory]);
new pumpRpmAccessory_1.PumpRpmAccessory(this, existingAccessory);
}
else {
this.log.debug(`Adding new heater RPM sensor: ${displayName}`);
const accessory = new this.api.platformAccessory(displayName, uuid);
// Create feature-like object with bodyId
accessory.context.feature = { id: heater.id, name: heater.name, status: initialStatus, bodyId: body.id };
accessory.context.pumpCircuit = pumpCircuit;
accessory.context.pump = pump;
accessory.context.panel = panel;
new pumpRpmAccessory_1.PumpRpmAccessory(this, accessory);
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
this.accessoryMap.set(accessory.UUID, accessory);
}
}
discoverPumpGpmSensor(panel, pump) {
// Skip GPM sensors for VS and VF pumps - only create for VSF pumps
const pumpType = constants_1.PUMP_TYPE_MAPPING.get(pump.type) || pump.type;
if (pumpType === 'VF' || pumpType === 'VS') {
this.log.debug(`Skipping GPM sensor creation for ${pumpType} pump: ${pump.name} (type: ${pump.type})`);
return;
}
const pumpGpmSensorId = `${pump.id}-gpm`;
const uuid = this.api.hap.uuid.generate(pumpGpmSensorId);
const existingAccessory = this.accessoryMap.get(uuid);
if (existingAccessory) {
this.log.debug('Restoring existing pump GPM sensor from cache:', existingAccessory.displayName);
existingAccessory.context.pump = pump;
existingAccessory.context.panel = panel;
new pumpGpmAccessory_1.PumpGpmAccessory(this, existingAccessory);
}
else {
this.log.info('Adding new pump GPM sensor:', `${pump.name} GPM`);
const accessory = new this.api.platformAccessory(`${pump.name} GPM`, uuid);
accessory.context.pump = pump;
accessory.context.panel = panel;
new pumpGpmAccessory_1.PumpGpmAccessory(this, accessory);
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
this.accessoryMap.set(accessory.UUID, accessory);
}
}
discoverPumpRpmSensor(panel, pump) {
const pumpRpmSensorId = `${pump.id}-rpm`;
const uuid = this.api.hap.uuid.generate(pumpRpmSensorId);
const existingAccessory = this.accessoryMap.get(uuid);
if (existingAccessory) {
this.log.debug('Restoring existing pump RPM sensor from cache:', existingAccessory.displayName);
existingAccessory.context.pump = pump;
existingAccessory.context.panel = panel;
new pumpRpmAccessory_1.PumpRpmAccessory(this, existingAccessory);
}
else {
this.log.info('Adding new pump RPM sensor:', `${pump.name} RPM`);
const accessory = new this.api.platformAccessory(`${pump.name} RPM`, uuid);
accessory.context.pump = pump;
accessory.context.panel = panel;
new pumpRpmAccessory_1.PumpRpmAccessory(this, accessory);
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
this.accessoryMap.set(accessory.UUID, accessory);
}
}
discoverPumpWattsSensor(panel, pump) {
const pumpWattsSensorId = `${pump.id}-watts`;
const uuid = this.api.hap.uuid.generate(pumpWattsSensorId);
const existingAccessory = this.accessoryMap.get(uuid);
if (existingAccessory) {
this.log.debug('Restoring existing pump WATTS sensor from cache:', existingAccessory.displayName);
existingAccessory.context.pump = pump;
existingAccessory.context.panel = panel;
new pumpWattsAccessory_1.PumpWattsAccessory(this, existingAccessory);
}
else {
this.log.info('Adding new pump WATTS sensor:', `${pump.name} WATTS`);
const accessory = new this.api.platformAccessory(`${pump.name} WATTS`, uuid);
accessory.context.pump = pump;
accessory.context.panel = panel;
new pumpWattsAccessory_1.PumpWattsAccessory(this, accessory);
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
this.accessoryMap.set(accessory.UUID, accessory);
}
}
updatePumpSensors(pumpCircuit) {
this.log.debug(`[PUMP SENSOR UPDATE] Processing pump circuit ${pumpCircuit.id}:`);
this.log.debug(` - Status: ${pumpCircuit.status}`);
this.log.debug(` - RPM: ${pumpCircuit.rpm}`);
this.log.debug(` - Speed: ${pumpCircuit.speed}`);
this.log.debug(` - Speed Type: ${pumpCircuit.speedType}`);
this.log.debug(` - WATTS: ${pumpCircuit.watts}`);
this.log.debug(` - GPM: ${pumpCircuit.gpm}`);
// Store/update the pump circuit data
this.activePumpCircuits.set(pumpCircuit.id, pumpCircuit);
this.log.debug(` - Stored in activePumpCircuits map (total: ${this.activePumpCircuits.size})`);
// Find the pump that contains this pump circuit
const pumpId = this.getPumpForPumpCircuit(pumpCircuit.id);
if (!pumpId) {
this.log.warn(`No pump found for pump circuit ${pumpCircuit.id} - cannot update pump sensors`);
return;
}
this.log.info(` - Associated with pump: ${pumpId}`);
// Get the highest RPM among all enabled circuits for this pump
const highestRpm = this.getHighestRpmForPump(pumpId);
if (!highestRpm) {
this.log.info(`No active circuits found for pump ${pumpId}, setting sensors to minimum`);
// Set sensors to minimum values when no circuits are active
this.updatePumpSensorsWithRpm(pumpId, 0.0001);
return;
}
this.log.info(`Updating pump ${pumpId} sensors with highest active RPM: ${highestRpm}`);
// Update all pump sensors with the highest RPM
this.updatePumpSensorsWithRpm(pumpId, highestRpm);
}
/**
* Update all pump sensors when any circuit changes for that pump
*/
async updateAllPumpSensorsForChangedCircuit(pumpCircuit) {
// Find the pump that contains this pump circuit
const pumpId = this.getPumpForPumpCircuit(pumpCircuit.id);
if (!pumpId) {
this.log.warn(`No pump found for pump circuit ${pumpCircuit.id} - cannot update pump sensors`);
return;
}
this.log.debug(`[PUMP SENSOR UPDATE] Circuit ${pumpCircuit.id} changed, updating all sensors for pump ${pumpId}`);
// Find and update RPM sensor
const rpmSensorId = `${pumpId}-rpm`;
const rpmUuid = this.api.hap.uuid.generate(rpmSensorId);
const rpmAccessory = this.accessoryMap.get(rpmUuid);
if (rpmAccessory) {
// Get fresh RPM value from the sensor's dynamic calculation
const rpmSensor = new pumpRpmAccessory_1.PumpRpmAccessory(this, rpmAccessory);
const currentRpm = await rpmSensor.getRpm();
rpmSensor.updateRpm(currentRpm);
this.log.debug(` Updated RPM sensor: ${currentRpm} RPM`);
}
// Find and update GPM sensor (only for VSF pumps)
const gpmSensorId = `${pumpId}-gpm`;
const gpmUuid = this.api.hap.uuid.generate(gpmSensorId);
const gpmAccessory = this.accessoryMap.get(gpmUuid);
if (gpmAccessory) {
// Get fresh GPM value from the sensor's dynamic calculation
const gpmSensor = new pumpGpmAccessory_1.PumpGpmAccessory(this, gpmAccessory);
const currentGpm = await gpmSensor.getGpm();
gpmSensor.updateGpm(currentGpm);
this.log.debug(` Updated GPM sensor: ${currentGpm} GPM`);
}
// Find and update WATTS sensor
const wattsSensorId = `${pumpId}-watts`;
const wattsUuid = this.api.hap.uuid.generate(wattsSensorId);
const wattsAccessory = this.accessoryMap.get(wattsUuid);
if (wattsAccessory) {
// Get fresh WATTS value from the sensor's dynamic calculation
const wattsSensor = new pumpWattsAccessory_1.PumpWattsAccessory(this, wattsAccessory);
const currentWatts = await wattsSensor.getWatts();
wattsSensor.updateWatts(currentWatts);
this.log.debug(` Updated WATTS sensor: ${currentWatts} WATTS`);
}
}
/**
* Find pump object by ID in discovered accessories
*/
findPumpObjectById(pumpId) {
for (const [, accessory] of this.accessoryMap) {
if (accessory.context.pump && accessory.context.pump.id === pumpId) {
return accessory.context.pump;
}
}
return null;
}
/**
* Process a single pump circuit for RPM calculation
*/
processPumpCircuitForRpm(pumpCircuit) {
const rpm = pumpCircuit.rpm || pumpCircuit.speed || 0;
this.log.debug(` Checking pump circuit ${pumpCircuit.id} (circuitId: ${pumpCircuit.circuitId}):`);
this.log.debug(` - RPM: ${pumpCircuit.rpm}`);
this.log.debug(` - Speed: ${pumpCircuit.speed}`);
this.log.debug(` - Final RPM: ${rpm}`);
if (rpm > 0) {
const isActive = this.isPumpCircuitActive(pumpCircuit.circuitId);
this.log.info(` - Is Active: ${isActive}`);
return { rpm, isActive };
}
else {
this.log.info(` - Circuit has no/zero RPM (${rpm})`);
return { rpm: 0, isActive: false };
}
}
/**
* Get the highest RPM among all enabled circuits for a given pump
*/
getHighestRpmForPump(pumpId) {
let highestRpm = 0;
let activeCircuitCount = 0;
this.log.info(`[RPM CALCULATION] Finding highest RPM for pump ${pumpId}`);
const pumpObject = this.findPumpObjectById(pumpId);
if (!pumpObject || !pumpObject.circuits || pumpObject.circuits.length === 0) {
this.log.info(` No pump object or circuits found for pump ${pumpId}`);
return null;
}
this.log.info(` Found pump ${pumpObject.name} with ${pumpObject.circuits.length} circuits`);
for (const pumpCircuit of pumpObject.circuits) {
const { rpm, isActive } = this.processPumpCircuitForRpm(pumpCircuit);
if (isActive) {
activeCircuitCount++;
if (rpm > highestRpm) {
highestRpm = rpm;
this.log.info(` - NEW HIGHEST RPM: ${highestRpm} from circuit ${pumpCircuit.circuitId}`);
}
}
}
this.log.info(`[RPM RESULT] Pump ${pumpId}: ${activeCircuitCount} active circuits, highest RPM: ${highestRpm}`);
return highestRpm > 0 ? highestRpm : null;
}
/**
* Check circuit context for activity status
*/
checkCircuitContext(accessory, circuitId) {
if (accessory.context.circuit && accessory.context.circuit.id === circuitId) {
const isOn = accessory.context.circuit.status === types_1.CircuitStatus.On;
this.log.info(` Found circuit ${circuitId}: status = ${accessory.context.circuit.status}, active = ${isOn}`);
return isOn;
}
return null;
}
/**
* Check feature context for activity status
*/
checkFeatureContext(accessory, circuitId) {
if (accessory.context.feature && accessory.context.feature.id === circuitId) {
const isOn = accessory.context.feature.status === types_1.CircuitStatus.On;
this.log.info(` Found feature ${circuitId}: status = ${accessory.context.feature.status}, active = ${isOn}`);
return isOn;
}
return null;
}
/**
* Check body context for activity status
*/
checkBodyContext(accessory, circuitId) {
var _a;
if (accessory.context.body && ((_a = accessory.context.body.circuit) === null || _a === void 0 ? void 0 : _a.id) === circuitId) {
const isOn = accessory.context.body.status === types_1.CircuitStatus.On;
this.log.info(` Found body circuit ${circuitId}: status = ${accessory.context.body.status}, active = ${isOn}`);
return isOn;
}
return null;
}
/**
* Check if a pump circuit is currently active by looking for corresponding feature/circuit status
* (Same logic as WATTS sensor)
*/
isPumpCircuitActive(circuitId) {
for (const [, accessory] of this.accessoryMap) {
// Check circuit context
const circuitResult = this.checkCircuitContext(accessory, circuitId);
if (circuitResult !== null) {
return circuitResult;
}
// Check feature context
const featureResult = this.checkFeatureContext(accessory, circuitId);
if (featureResult !== null) {
return featureResult;
}
// Check body context
const bodyResult = this.checkBodyContext(accessory, circuitId);
if (bodyResult !== null) {
return bodyResult;
}
}
this.log.info(` Circuit ${circuitId} not found in accessories, assuming inactive`);
return false;
}
/**
* Update all pump sensors (RPM, GPM, WATTS) with the specified RPM value
*/
updatePumpSensorsWithRpm(pumpId, rpm) {
// Update RPM sensor
const rpmSensorId = `${pumpId}-rpm`;
const rpmUuid = this.api.hap.uuid.generate(rpmSensorId);
const rpmAccessory = this.accessoryMap.get(rpmUuid);
if (rpmAccessory) {
this.log.debug(`Found RPM sensor ${rpmSensorId}, updating to ${rpm} RPM`);
// Update the pump's RPM value
if (rpmAccessory.context.pump) {
rpmAccessory.context.pump.rpm = rpm;
}
const rpmSensor = new pumpRpmAccessory_1.PumpRpmAccessory(this, rpmAccessory);
rpmSensor.updateRpm(rpm);
}
else {
this.log.debug(`RPM sensor not found for ${rpmSensorId} (UUID: ${rpmUuid})`);
}
// Update GPM sensor
const gpmSensorId = `${pumpId}-gpm`;
const gpmUuid = this.api.hap.uuid.generate(gpmSensorId);
const gpmAccessory = this.accessoryMap.get(gpmUuid);
if (gpmAccessory) {
this.log.debug(`Found GPM sensor ${gpmSensorId}, updating to ${rpm} RPM`);
const gpmSensor = new pumpGpmAccessory_1.PumpGpmAccessory(this, gpmAccessory);
gpmSensor.updateSpeed(rpm);
}
else {
this.log.debug(`GPM sensor not found for ${gpmSensorId} (UUID: ${gpmUuid})`);
}
// Update WATTS sensor
const wattsSensorId = `${pumpId}-watts`;
const wattsUuid = this.api.hap.uuid.generate(wattsSensorId);
const wattsAccessory = this.accessoryMap.get(wattsUuid);
if (wattsAccessory) {
this.log.debug(`Found WATTS sensor ${wattsSensorId}, updating to ${rpm} RPM`);
const wattsSensor = new pumpWattsAccessory_1.PumpWattsAccessory(this, wattsAccessory);
wattsSensor.updateSpeed(rpm);
}
else {
this.log.debug(`WATTS sensor not found for ${wattsSensorId} (UUID: ${wattsUuid})`);
}
}
updatePumpSensorsForStandalonePump(pumpId, speed, speedType) {
const speedValue = parseInt(speed);
if (!speedValue || speedType !== 'RPM') {
this.log.debug(`Skipping standalone pump ${pumpId} sensors update - invalid speed: ${speed} ${speedType}`);
return;
}
this.log.debug(`Updating standalone pump ${pumpId} sensors with speed: ${speedValue} RPM`);
// Try to map standalone pump ID to platform pump ID format
// e.g., "p0102" might need to be mapped to "PMP01" or "PMP02"
let mappedPumpId = pumpId;
if (pumpId.startsWith('p0')) {
const pumpIdMatch = pumpId.match(/^p(\d{2})(\d{2})$/);
if (pumpIdMatch) {
const pumpNum = pumpIdMatch[1];
mappedPumpId = `PMP${pumpNum}`;
this.log.debug(`Mapped standalone pump ID ${pumpId} to platform pump ID ${mappedPumpId}`);
}
}
// Update RPM sensor using mapped pump ID
const rpmSensorId = `${mappedPumpId}-rpm`;
const rpmUuid = this.api.hap.uuid.generate(rpmSensorId);
const rpmAccessory = this.accessoryMap.get(rpmUuid);
if (rpmAccessory) {
this.log.debug(`Found RPM sensor ${rpmSensorId}, updating to ${speedValue} RPM (standalone pump)`);
// Update the pump's RPM value
if (rpmAccessory.context.pump) {
rpmAccessory.context.pump.rpm = speedValue;
}
const rpmSensor = new pumpRpmAccessory_1.PumpRpmAccessory(this, rpmAccessory);
rpmSensor.updateRpm(speedValue);
}
else {
this.log.debug(`RPM sensor not found for ${rpmSensorId} (UUID: ${rpmUuid})`);
}
// Update GPM sensor using mapped pump ID
const gpmSensorId = `${mappedPumpId}-gpm`;
const gpmUuid = this.api.hap.uuid.generate(gpmSensorId);
const gpmAccessory = this.accessoryMap.get(gpmUuid);
if (gpmAccessory) {
this.log.debug(`Found GPM sensor ${gpmSensorId}, updating to ${speedValue} RPM (standalone pump)`);
const gpmSensor = new pumpGpmAccessory_1.PumpGpmAccessory(this, gpmAccessory);
gpmSensor.updateSpeed(speedValue);
}
else {
this.log.debug(`GPM sensor not found for ${gpmSensorId} (UUID: ${gpmUuid}). Available accessories: ${Array.from(this.accessoryMap.keys())
.map(k => { var _a; return (_a = this.accessoryMap.get(k)) === null || _a === void 0 ? void 0 : _a.displayName; })
.join(', ')}`);
}
// Update WATTS sensor using mapped pump ID
const wattsSensorId = `${mappedPumpId}-watts`;
const wattsUuid = this.api.hap.uuid.generate(wattsSensorId);
const wattsAccessory = this.accessoryMap.get(wattsUuid);
if (wattsAccessory) {
this.log.debug(`Found WATTS sensor ${wattsSensorId}, updating to ${speedValue} RPM (system-driven)`);
const wattsSensor = new pumpWattsAccessory_1.PumpWattsAccessory(this, wattsAccessory);
wattsSensor.updateSystemSpeed(speedValue);
}
else {
this.log.debug(`WATTS sensor not found for ${wattsSensorId} (UUID: ${wattsUuid})`);
}
}
subscribeForUpdates(circuit, keys) {
const command = {
command: types_1.IntelliCenterRequestCommand.RequestParamList,
messageID: (0, uuid_1.v4)(),
objectList: [
{
objnam: circuit.id,
keys: keys,
},
],
};
// No need to await. We'll handle in the update handler.
this.sendCommandNoWait(command);
}
getConfig() {
if (!this.validatedConfig) {
throw new Error('Configuration has not been validated. Cannot return config.');
}
return this.validatedConfig;
}
json(data) {
try {
return JSON.stringify(data, null, 2);
}
catch (_a) {
// Handle circular references and other JSON serialization errors
return JSON.stringify(data, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (this.jsonSeenObjects && this.jsonSeenObjects.has(value)) {
return '[Circular]';
}
if (!this.jsonSeenObjects) {
this.jsonSeenObjects = new WeakSet();
}
this.jsonSeenObjects.add(value);
}
return value;
}, 2);
}
}
/**
* Get system health and status information
*/
getSystemHealth() {
const health = this.healthMonitor.getHealth();
const circuitBreakerStats = this.circuitBreaker.getStats();
const rateLimiterStats = this.rateLimiter.getStats();
return {
isHealthy: health.isHealthy,
lastSuccessfulOperation: new Date(health.lastSuccessfulOperation),
consecutiveFailures: health.consecutiveFailures,
lastError: health.lastError,
averageResponseTime: health.responseTime,
circuitBreaker: {
state: circuitBreakerStats.state,
failureCount: circuitBreakerStats.failureCount,
lastFailureTime: circuitBreakerStats.lastFailureTime ? new Date(circuitBreakerStats.lastFailureTime) : null,
},
rateLimiter: rateLimiterStats,
connection: {
isSocketAlive: this.isSocketAlive,
lastMessageReceived: new Date(this.lastMessageReceived),
reconnecting: this.reconnecting,
commandQueueLength: this.commandQueue.length,
},
};
}
/**
* Reset error handling components (useful for testing or manual recovery)
*/
resetErrorHandling() {
if (this.circuitBreaker) {
this.circuitBreaker.reset();
}
if (this.healthMonitor) {
this.healthMonitor.reset();
}
if (this.deadLetterQueue) {
this.deadLetterQueue.clear();
}
this.log.info('Error handling components have been reset');
}
sendCommandNoWait(command) {
// Rate limiting check
if (!this.rateLimiter.recordRequest()) {
this.log.debug('Rate limit exceeded. Command dropped to prevent overwhelming IntelliCenter.');
this.log.debug(`Rate limiter stats: ${JSON.stringify(this.rateLimiter.getStats())}`);
return;
}
if (!this.isSocketAlive) {
this.log.warn(`Cannot send command, socket is not alive: ${this.json(command)}`);
this.maybeReconnect();
return;
}
// Sanitize command before sending
const sanitizedCommand = this.sanitizeCommand(command);
// Add to queue and process
this.commandQueue.push(sanitizedCommand);
this.processCommandQueue();
}
sanitizeCommand(command) {
const sanitized = { ...command };
// Sanitize string fields to prevent injection attacks
if (sanitized.arguments) {
sanitized.arguments = sanitized.arguments.replace(/[<>"'&;]/g, '');
}
// Validate messageID format (should be UUID)
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sanitized.messageID)) {
this.log.warn(`Invalid messageID format: ${sanitized.messageID}. Regenerating...`);
sanitized.messageID = (0, uuid_1.v4)();
}
// Validate object list parameters
if (sanitized.objectList) {
sanitized.objectList = sanitized.objectList.map(obj => {
const sanitizedObj = { ...obj };
if (sanitizedObj.objnam) {
// Object names should be alphanumeric with some allowed characters
sanitizedObj.objnam = sanitizedObj.objnam.replace(/[^a-zA-Z0-9_-]/g, '');
}
return sanitizedObj;
});
}
return sanitized;
}
async processCommandQueue() {
if (this.processingQueue || this.commandQueue.length === 0) {
return;
}
this.processingQueue = true;
while (this.commandQueue.length > 0 && this.isSocketAlive) {
const command = this.commandQueue.shift();
try {
// Ensure clean JSON serialization
const commandString = JSON.stringify(command);
// Validate the JSON before sending
JSON.parse(commandString); // This will throw if invalid
this.log.debug(`Sending command to IntelliCenter: ${commandString}`);
// Send with proper line termination
await this.connection.send(commandString + '\n');
// Conservative delay between commands to prevent overwhelming the device
await this.delay(200);
}
catch (error) {
this.log.error(`Failed to send command to IntelliCenter: ${error}. Command: ${this.json(command)}`);
// Add failed command to Dead Letter Queue
this.deadLetterQueue.add(command, 1, // First attempt (could be enhanced to track retries)
String(error), command.messageID || 'unknown');
const errorString = String(error);
if (errorString.includes('connection') || errorString.includes('socket')) {
this.maybeReconnect();
break;
}
}
}
this.processingQueue = false;
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async maybeReconnect() {
const now = Date.now();
if (this.reconnecting) {
this.log.warn('Reconnect already in progress. Skipping.');
return;
}
if (now - this.lastReconnectTime < 30 * 1000) {
this.log.warn('Reconnect suppressed: too soon after last one.');
return;
}
this.reconnecting = true;
this.lastReconnectTime = now;
try {
this.log.warn('Attempting reconnect to IntelliCenter...');
this.connection.destroy();
await this.connectToIntellicenter();
this.log.info('Reconnect requested.');
}
catch (error) {
this.log.error('Reconnect failed.', error);
}
finally {
this.reconnecting = false;
}
}
/**
* Build pump-circuit associations for proper device management
* Maps pumps to their controlled circuits and vice versa
*/
buildPumpCircuitAssociations(pumpId, pumpCircuit) {
var _a;
this.log.debug(`Building associations for pump ${pumpId} -> circuit ${pumpCircuit.circuitId} (pump circuit: ${pumpCircuit.id})`);
// Map pump circuit ID to pump ID (e.g., "p0101" -> "PMP01")
this.pumpCircuitToPumpMap.set(pumpCircuit.id, pumpId);
// Map circuit ID to pump ID (e.g., "C0006" -> "PMP01")
this.circuitToPumpMap.set(pumpCircuit.circuitId, pumpId);
// Map pump ID to set of circuit IDs (e.g., "PMP01" -> {"C0006", "C0001", ...})
if (!this.pumpToCircuitsMap.has(pumpId)) {
this.pumpToCircuitsMap.set(pumpId, new Set());
}
this.pumpToCircuitsMap.get(pumpId).add(pumpCircuit.circuitId);
this.log.debug(` Associations built: pump ${pumpId} now controls ${((_a = this.pumpToCircuitsMap.get(pumpId)) === null || _a === void 0 ? void 0 : _a.size) || 0} circuits`);
}
/**
* Get the pump ID that controls a specific circuit
*/
getPumpForCircuit(circuitId) {
return this.circuitToPumpMap.get(circuitId);
}
/**
* Get all circuit IDs controlled by a specific pump
*/
getCircuitsForPump(pumpId) {
return this.pumpToCircuitsMap.get(pumpId);
}
/**
* Get the pump ID for a specific pump circuit
*/
getPumpForPumpCircuit(pumpCircuitId) {
return this.pumpCircuitToPumpMap.get(pumpCircuitId);
}
/**
* Find the pump circuit ID that controls a specific circuit
*/
findPumpCircuitForCircuit(circuitId) {
var _a, _b;
// Search through all pump circuits to find one that controls this circuit
for (const [pumpCircuitId] of this.pumpCircuitToPumpMap) {
// Check if this pump circuit is associated with our circuit
for (const [, accessory] of this.accessoryMap) {
if (((_a = accessory.context.pumpCircuit) === null || _a === void 0 ? void 0 : _a.circuitId) === circuitId && ((_b = accessory.context.pumpCircuit) === null || _b === void 0 ? void 0 : _b.id) === pumpCircuitId) {
return pumpCircuitId;
}
}
}
return undefined;
}
/**
* Log detailed pump discovery mapping in requested format
*/
logPumpDiscoveryMapping(pump, panel) {
// Get circuit names by looking up in all features and bodies
const getCircuitName = (circuitId) => {
// Search in panel features first
for (const feature of panel.features) {
if (feature.id === circuitId) {
return feature.name;
}
}
// Search in module features and bodies
for (const module of panel.modules) {
for (const feature of module.features) {
if (feature.id === circuitId) {
return feature.name;
}
}
for (const body of module.bodies) {
if (body.id === circuitId) {
return body.name;
}
}
}
return circuitId; // fallback to ID if name not found
};
// Build circuit descriptions with names and speeds
const circuitDescriptions = [];
(pump.circuits || []).forEach((pumpCircuit, index) => {
const circuitName = getCircuitName(pumpCircuit.circuitId);
const speedType = pumpCircuit.speedType || 'RPM';
const speed = pumpCircuit.speed || 0;
const speedDesc = speedType === 'RPM' ? `${speed}rpm` : `${speed}${speedType.toLowerCase()}`;
circuitDescriptions.push(`circuit${index + 1}: ${circuitName} (${speedDesc})`);
});
// Format final discovery message
const circuitList = circuitDescriptions.join('. ');
const pumpTypeDisplay = (pump.type || 'unknown').toLowerCase();
this.log.debug(`Found pump. name: ${pump.name || 'unknown'}. type: ${pumpTypeDisplay}. ${circuitList}.`);
}
/**
* Log current pump-circuit associations for debugging
*/
logPumpCircuitAssociations() {
this.log.debug('=== Pump-Circuit Associations ===');
this.pumpToCircuitsMap.forEach((circuits, pumpId) => {
this.log.debug(`Pump ${pumpId} controls circuits: ${Array.from(circuits).join(', ')}`);
});
this.log.debug('=== Circuit-to-Pump Mappings ===');
this.circuitToPumpMap.forEach((pumpId, circuitId) => {
this.log.debug(`Circuit ${circuitId} is controlled by pump ${pumpId}`);
});
}
/**
* Update all pump sensors when heater status changes
*/
updateAllPumpSensorsForHeaterChange() {
this.log.info('[HEATER CHANGE] Updating all pump sensors due to heater status change');
// Find all pump sensors and trigger their updates directly
for (const [, accessory] of this.accessoryMap) {
if (accessory.context.pump) {
const pumpId = accessory.context.pump.id;
this.log.debug(`Updating sensors for pump ${pumpId} due to heater change`);
// Directly update pump sensors by finding them in the accessory map
this.updatePumpSensorsDirectly(pumpId);
}
}
}
/**
* Directly update pump sensors by pump ID (used for heater changes)
*/
updatePumpSensorsDirectly(pumpId) {
this.log.debug(`[DIRECT PUMP UPDATE] Updating sensors for pump ${pumpId}`);
// Update RPM sensor
const rpmSensorId = `${pumpId}-rpm`;
const rpmUuid = this.api.hap.uuid.generate(rpmSensorId);
const rpmAccessory = this.accessoryMap.get(rpmUuid);
if (rpmAccessory) {
this.log.debug(`Found RPM sensor ${rpmSensorId}, triggering update`);
const rpmSensor = new pumpRpmAccessory_1.PumpRpmAccessory(this, rpmAccessory);
rpmSensor.getRpm().then(currentRpm => {
rpmSensor.updateRpm(currentRpm);
this.log.debug(` Updated RPM sensor: ${currentRpm} RPM`);
});
}
// Update GPM sensor
const gpmSensorId = `${pumpId}-gpm`;
const gpmUuid = this.api.hap.uuid.generate(gpmSensorId);
const gpmAccessory = this.accessoryMap.get(gpmUuid);
if (gpmAccessory) {
this.log.debug(`Found GPM sensor ${gpmSensorId}, triggering update`);
const gpmSensor = new pumpGpmAccessory_1.PumpGpmAccessory(this, gpmAccessory);
gpmSensor.getGpm().then(currentGpm => {
gpmSensor.updateGpm(currentGpm);
this.log.debug(` Updated GPM sensor: ${currentGpm} GPM`);
});
}
// Update WATTS sensor
const wattsSensorId = `${pumpId}-watts`;
const wattsUuid = this.api.hap.uuid.generate(wattsSensorId);
const wattsAccessory = this.accessoryMap.get(wattsUuid);
if (wattsAccessory) {
this.log.debug(`Found WATTS sensor ${wattsSensorId}, triggering update`);
const wattsSensor = new pumpWattsAccessory_1.PumpWattsAccessory(this, wattsAccessory);
wattsSensor.getWatts().then(currentWatts => {
wattsSensor.updateWatts(currentWatts);
this.log.debug(` Updated WATTS sensor: ${currentWatts} WATTS`);
});
}
}
/**
* Update pump object circuits array when standalone pump circuit changes
*/
updatePumpObjectCircuits(pumpId, pumpCircuitId, newSpeed) {
this.log.debug(`Updating pump ${pumpId} circuits array - circuit ${pumpCircuitId} speed to ${newSpeed}`);
// Find the pump accessory and update its circuits array
for (const [, accessory] of this.accessoryMap) {
if (accessory.context.pump && accessory.context.pump.id === pumpId) {
const pump = accessory.context.pump;
// Find the specific circuit in the pump's circuits array and update its speed
if (pump.circuits) {
for (const circuit of pump.circuits) {
if (circuit.id === pumpCircuitId) {
this.log.debug(`Found circuit ${pumpCircuitId} in pump ${pumpId}, updating speed from ${circuit.speed} to ${newSpeed}`);
circuit.speed = newSpeed;
// Update the accessory context
this.api.updatePlatformAccessories([accessory]);
return;
}
}
}
this.log.debug(`Circuit ${pumpCircuitId} not found in pump ${pumpId} circuits array`);
return;
}
}
this.log.debug(`Pump ${pumpId} not found in accessory map`);
}
/**
* Start temperature unit validation monitoring
*/
startTemperatureUnitValidation() {
// Skip validation in test environments
/* eslint-disable-next-line no-undef */
const isTestEnvironment = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
if (isTestEnvironment || this.temperatureUnitValidated) {
return;
}
// Monitor temperature readings every 30 seconds for first 5 minutes
this.temperatureValidationInterval = setInterval(() => {
this.validateTemperatureUnits();
}, 30000);
// Stop monitoring after 5 minutes
setTimeout(() => {
if (this.temperatureValidationInterval) {
clearInterval(this.temperatureValidationInterval);
this.temperatureValidationInterval = null;
}
}, 300000); // 5 minutes
}
/**
* Collect temperature reading for validation
*/
collectTemperatureReading(temperature) {
if (this.temperatureUnitValidated || this.temperatureReadings.length >= 50) {
return;
}
if (!isNaN(temperature) && temperature !== null && temperature !== undefined) {
this.temperatureReadings.push(temperature);
}
}
/**
* Validate temperature unit consistency with IntelliCenter readings
*/
validateTemperatureUnits() {
if (this.temperatureUnitValidated || this.temperatureReadings.length < 3) {
return;
}
const validation = configValidation_1.ConfigValidator.validateTemperatureUnitConsistency(this.temperatureReadings, this.getConfig().temperatureUnits);
if (!validation.isConsistent && validation.warning) {
this.log.warn(validation.warning);
this.temperatureUnitValidated = true; // Only warn once
// Stop monitoring after validation
if (this.temperatureValidationInterval) {
clearInterval(this.temperatureValidationInterval);
this.temperatureValidationInterval = null;
}
}
else if (validation.analysisCount >= 10 && validation.isConsistent) {
// Stop monitoring after successful validation with sufficient data
this.log.debug(`Temperature unit validation successful. Analyzed ${validation.analysisCount} readings. ` +
`Detected unit: ${validation.detectedUnit || 'unknown'}, Configured: ${validation.configuredUnit}`);
this.temperatureUnitValidated = true;
if (this.temperatureValidationInterval) {
clearInterval(this.temperatureValidationInterval);
this.temperatureValidationInterval = null;
}
}
}
/**
* Setup graceful shutdown handlers for SIGTERM and SIGINT signals
*/
setupGracefulShutdown() {
// Prevent duplicate listeners being added
if (PentairPlatform.shutdownHandlersSetup) {
return;
}
PentairPlatform.shutdownHandlersSetup = true;
/* eslint-disable no-undef */
const shutdownHandler = (signal) => {
this.log.info(`Received ${signal}, performing graceful shutdown...`);
this.cleanup()
.then(() => {
this.log.info('Graceful shutdown completed');
process.exit(0);
})
.catch(error => {
this.log.error(`Error during graceful shutdown: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});
};
// Handle graceful shutdown signals
process.on('SIGTERM', () => shutdownHandler('SIGTERM'));
process.on('SIGINT', () => shutdownHandler('SIGINT'));
// Handle uncaught exceptions and unhandled rejections
process.on('uncaughtException', error => {
this.log.error(`Uncaught Exception: ${error.message}`);
this.log.debug(`Stack trace: ${error.stack}`);
this.cleanup()
.then(() => process.exit(1))
.catch(() => process.exit(1));
});
process.on('unhandledRejection', (reason, promise) => {
this.log.error(`Unhandled Promise Rejection at: ${promise}, reason: ${reason}`);
this.cleanup()
.then(() => process.exit(1))
.catch(() => process.exit(1));
});
/* eslint-enable no-undef */
}
clearTimersAndIntervals() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
this.log.debug('Heartbeat interval cleared');
}
if (this.discoveryTimeout) {
clearTimeout(this.discoveryTimeout);
this.discoveryTimeout = null;
this.log.debug('Discovery timeout cleared');
}
if (this.temperatureValidationInterval) {
clearInterval(this.temperatureValidationInterval);
this.temperatureValidationInterval = null;
this.log.debug('Temperature validation interval cleared');
}
}
cleanupConnection() {
if (this.connection) {
try {
this.log.debug('Removing connection event listeners...');
this.connection.removeAllListeners();
this.log.debug('Connection event listeners removed');
}
catch (error) {
this.log.warn(`Error removing connection event listeners: ${error instanceof Error ? error.message : String(error)}`);
}
}
if (this.connection && this.isSocketAlive) {
try {
this.log.debug('Closing Telnet connection...');
this.connection.destroy();
this.isSocketAlive = false;
this.log.debug('Telnet connection closed');
}
catch (error) {
this.log.warn(`Error closing Telnet connection: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
clearDataStructures() {
var _a, _b, _c, _d, _e, _f, _g, _h;
this.commandQueue = [];
this.processingQueue = false;
(_a = this.accessoryMap) === null || _a === void 0 ? void 0 : _a.clear();
(_b = this.heaters) === null || _b === void 0 ? void 0 : _b.clear();
(_c = this.heaterInstances) === null || _c === void 0 ? void 0 : _c.clear();
(_d = this.pumpIdToCircuitMap) === null || _d === void 0 ? void 0 : _d.clear();
(_e = this.pumpToCircuitsMap) === null || _e === void 0 ? void 0 : _e.clear();
(_f = this.circuitToPumpMap) === null || _f === void 0 ? void 0 : _f.clear();
(_g = this.pumpCircuitToPumpMap) === null || _g === void 0 ? void 0 : _g.clear();
(_h = this.activePumpCircuits) === null || _h === void 0 ? void 0 : _h.clear();
}
resetState() {
this.buffer = '';
this.discoveryBuffer = null;
if (this.discoverCommandsSent) {
this.discoverCommandsSent.length = 0;
}
if (this.discoverCommandsFailed) {
this.discoverCommandsFailed.length = 0;
}
this.reconnecting = false;
this.parseErrorCount = 0;
this.temperatureReadings = [];
this.temperatureUnitValidated = false;
}
/**
* Cleanup method for tests and graceful shutdown
* Clears intervals, closes connections, and removes event listeners
*/
async cleanup() {
this.log.debug('Starting cleanup process...');
this.clearTimersAndIntervals();
this.cleanupConnection();
this.clearDataStructures();
this.resetState();
this.resetErrorHandling();
this.log.debug('Cleanup process completed');
}
}
exports.PentairPlatform = PentairPlatform;
// Track if shutdown handlers have been setup to prevent duplicates
PentairPlatform.shutdownHandlersSetup = false;
//# sourceMappingURL=platform.js.map