UNPKG

homebridge-pentair-intellicenter-ai

Version:

Homebridge plugin for the Pentair IntelliCenter pool control system (maintained with AI assistance)

2,438 lines 117 kB
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
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