homebridge-pentair-intellicenter-ai
Version:
Homebridge plugin for the Pentair IntelliCenter pool control system (maintained with AI assistance)
445 lines • 19.3 kB
JavaScript
;
/**
* Configuration validation and sanitization for Pentair Platform
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigValidator = void 0;
const types_1 = require("./types");
class ConfigValidator {
static validateRequiredFields(config, errors, warnings, sanitizedConfig) {
this.validateRequiredIpAddress(config, errors, sanitizedConfig);
this.validateRequiredUsername(config, errors, sanitizedConfig);
this.validateRequiredPassword(config, errors, warnings, sanitizedConfig);
}
static validateRequiredIpAddress(config, errors, sanitizedConfig) {
if (!config.ipAddress || typeof config.ipAddress !== 'string') {
errors.push('ipAddress is required and must be a string');
}
else {
const ipValidation = this.validateIpAddress(config.ipAddress);
if (!ipValidation.isValid) {
errors.push(`ipAddress is invalid: ${ipValidation.error}`);
}
else {
sanitizedConfig.ipAddress = config.ipAddress;
}
}
}
static validateRequiredUsername(config, errors, sanitizedConfig) {
if (!config.username || typeof config.username !== 'string') {
errors.push('username is required and must be a string');
}
else {
const usernameValidation = this.validateUsername(config.username);
if (!usernameValidation.isValid) {
errors.push(`username is invalid: ${usernameValidation.error}`);
}
else {
sanitizedConfig.username = usernameValidation.sanitized || config.username;
}
}
}
static validateRequiredPassword(config, errors, warnings, sanitizedConfig) {
if (!config.password || typeof config.password !== 'string') {
errors.push('password is required and must be a string');
}
else {
const passwordValidation = this.validatePassword(config.password);
if (!passwordValidation.isValid) {
errors.push(`password is invalid: ${passwordValidation.error}`);
}
else {
sanitizedConfig.password = passwordValidation.sanitized || config.password;
if (passwordValidation.warning) {
warnings.push(passwordValidation.warning);
}
}
}
}
static validateTemperatureUnits(config, errors, warnings, sanitizedConfig) {
if (!config.temperatureUnits) {
warnings.push('temperatureUnits not specified, defaulting to Fahrenheit');
sanitizedConfig.temperatureUnits = types_1.TemperatureUnits.F;
}
else if (!Object.values(types_1.TemperatureUnits).includes(config.temperatureUnits)) {
errors.push(`temperatureUnits must be '${types_1.TemperatureUnits.F}' or '${types_1.TemperatureUnits.C}'`);
}
else {
sanitizedConfig.temperatureUnits = config.temperatureUnits;
}
}
static validateTemperatureRangeConfig(config, errors, sanitizedConfig) {
const tempValidation = this.validateTemperatureRange(config.minimumTemperature, config.maximumTemperature, config.temperatureUnits || types_1.TemperatureUnits.F);
if (!tempValidation.isValid) {
errors.push(tempValidation.error);
}
if (tempValidation.sanitizedMin !== undefined) {
sanitizedConfig.minimumTemperature = tempValidation.sanitizedMin;
}
if (tempValidation.sanitizedMax !== undefined) {
sanitizedConfig.maximumTemperature = tempValidation.sanitizedMax;
}
}
static validateOptionalFields(config, warnings, sanitizedConfig) {
// Boolean fields with defaults
sanitizedConfig.supportVSP = this.validateBoolean(config.supportVSP, false);
sanitizedConfig.airTemp = this.validateBoolean(config.airTemp, true);
sanitizedConfig.includeAllCircuits = this.validateBoolean(config.includeAllCircuits, false);
// Buffer size validation
this.validateBufferSizeConfig(config, warnings, sanitizedConfig);
}
static validateBufferSizeConfig(config, warnings, sanitizedConfig) {
if (config.maxBufferSize !== undefined) {
const bufferValidation = this.validateBufferSize(config.maxBufferSize);
if (!bufferValidation.isValid) {
warnings.push(`Invalid maxBufferSize: ${bufferValidation.error}. Using default.`);
sanitizedConfig.maxBufferSize = 1048576; // 1MB default
}
else {
sanitizedConfig.maxBufferSize = bufferValidation.sanitizedValue;
}
}
else {
sanitizedConfig.maxBufferSize = 1048576; // 1MB default
}
}
static performSecurityChecks(config, warnings) {
if (config.ipAddress && this.isPrivateNetwork(config.ipAddress)) {
// This is good - internal network
}
else if (config.ipAddress) {
warnings.push('IP address appears to be on a public network. Ensure your IntelliCenter is properly secured.');
}
}
static finalizeSanitizedConfig(sanitizedConfig) {
if (sanitizedConfig.ipAddress) {
sanitizedConfig.ipAddress = sanitizedConfig.ipAddress.trim();
}
}
static validate(config) {
const errors = [];
const warnings = [];
// Handle null or undefined config
if (!config || typeof config !== 'object') {
errors.push('Configuration is required and must be an object');
return { isValid: false, errors, warnings };
}
// Create a working copy for sanitization - only copy known fields
const sanitizedConfig = {
platform: config.platform,
name: config.name,
};
// Validate all sections
this.validateRequiredFields(config, errors, warnings, sanitizedConfig);
this.validateTemperatureUnits(config, errors, warnings, sanitizedConfig);
this.validateTemperatureRangeConfig(config, errors, sanitizedConfig);
this.validateOptionalFields(config, warnings, sanitizedConfig);
this.performSecurityChecks(config, warnings);
this.finalizeSanitizedConfig(sanitizedConfig);
const isValid = errors.length === 0;
return {
isValid,
errors,
warnings,
sanitizedConfig: isValid ? sanitizedConfig : undefined,
};
}
static validateIpAddress(ip) {
// Basic format check - allow broader pattern to enable octet range validation
const basicFormatRegex = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
if (!basicFormatRegex.test(ip.trim())) {
return { isValid: false, error: 'Must be a valid IPv4 address (e.g., 192.168.1.100)' };
}
const octets = ip.trim().split('.').map(Number);
if (octets.some(octet => octet < 0 || octet > 255)) {
return { isValid: false, error: 'IP address octets must be between 0 and 255' };
}
return { isValid: true };
}
static validateUsername(username) {
const sanitized = this.sanitizeInput(username);
if (sanitized.length < 3) {
return { isValid: false, error: 'Username must be at least 3 characters long' };
}
if (sanitized.length > 100) {
return { isValid: false, error: 'Username must be less than 100 characters long' };
}
return { isValid: true, sanitized };
}
static validatePassword(password) {
// Check length before sanitization to catch overly long passwords
if (password.length > 200) {
return { isValid: false, error: 'Password must be less than 200 characters long' };
}
const sanitized = this.sanitizeInput(password);
if (sanitized.length < 6) {
return { isValid: false, error: 'Password must be at least 6 characters long' };
}
// Warn if password was significantly changed by sanitization
const sanitizationReduction = password.length - sanitized.length;
if (sanitizationReduction > 5) {
return {
isValid: true,
sanitized,
warning: `Password contained ${sanitizationReduction} potentially unsafe characters that were removed`,
};
}
return { isValid: true, sanitized };
}
static parseTemperatureValue(value, units, isMin) {
if (typeof value === 'string') {
return parseFloat(value);
}
if (typeof value === 'number') {
return value;
}
// Set defaults based on units
if (isMin) {
return units === types_1.TemperatureUnits.F ? 40 : 4;
}
return units === types_1.TemperatureUnits.F ? 104 : 40;
}
static validateTemperatureBounds(temp, units, isMin) {
if (units === types_1.TemperatureUnits.F) {
const bounds = isMin ? { min: 32, max: 120, label: 'Minimum' } : { min: 50, max: 120, label: 'Maximum' };
if (temp < bounds.min || temp > bounds.max) {
return { isValid: false, error: `${bounds.label} temperature must be between ${bounds.min}°F and ${bounds.max}°F` };
}
}
else {
const bounds = isMin ? { min: 0, max: 50, label: 'Minimum' } : { min: 10, max: 50, label: 'Maximum' };
if (temp < bounds.min || temp > bounds.max) {
return { isValid: false, error: `${bounds.label} temperature must be between ${bounds.min}°C and ${bounds.max}°C` };
}
}
return { isValid: true };
}
static validateTemperatureRange(min, max, units) {
const minTemp = this.parseTemperatureValue(min, units, true);
const maxTemp = this.parseTemperatureValue(max, units, false);
if (isNaN(minTemp) || isNaN(maxTemp)) {
return { isValid: false, error: 'Temperature values must be valid numbers' };
}
if (minTemp >= maxTemp) {
return { isValid: false, error: 'Minimum temperature must be less than maximum temperature' };
}
const minValidation = this.validateTemperatureBounds(minTemp, units, true);
if (!minValidation.isValid) {
return minValidation;
}
const maxValidation = this.validateTemperatureBounds(maxTemp, units, false);
if (!maxValidation.isValid) {
return maxValidation;
}
return {
isValid: true,
sanitizedMin: Math.round(minTemp * 10) / 10, // Round to 1 decimal
sanitizedMax: Math.round(maxTemp * 10) / 10,
};
}
static validateBoolean(value, defaultValue) {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'string') {
const lower = value.toLowerCase();
if (lower === 'true' || lower === '1' || lower === 'yes') {
return true;
}
if (lower === 'false' || lower === '0' || lower === 'no') {
return false;
}
}
return defaultValue;
}
static validateBufferSize(size) {
let bufferSize;
if (typeof size === 'string') {
bufferSize = parseInt(size, 10);
}
else if (typeof size === 'number') {
bufferSize = size;
}
else {
return { isValid: false, error: 'Buffer size must be a number' };
}
if (isNaN(bufferSize) || bufferSize <= 0) {
return { isValid: false, error: 'Buffer size must be a positive number' };
}
// Minimum 64KB, maximum 16MB
if (bufferSize < 65536) {
return { isValid: false, error: 'Buffer size must be at least 64KB (65536 bytes)' };
}
if (bufferSize > 16777216) {
return { isValid: false, error: 'Buffer size must be at most 16MB (16777216 bytes)' };
}
return { isValid: true, sanitizedValue: bufferSize };
}
static isPrivateNetwork(ip) {
const octets = ip.split('.').map(Number);
// 10.0.0.0/8
if (octets[0] === 10) {
return true;
}
// 172.16.0.0/12
if (octets[0] === 172 && octets[1] !== undefined && octets[1] >= 16 && octets[1] <= 31) {
return true;
}
// 192.168.0.0/16
if (octets[0] === 192 && octets[1] !== undefined && octets[1] === 168) {
return true;
}
// 127.0.0.0/8 (loopback)
if (octets[0] === 127) {
return true;
}
return false;
}
static sanitizeInput(input) {
return input
.trim()
.replace(/[<>"'&]/g, '') // Remove potentially dangerous characters
.substring(0, 200); // Limit length
}
/**
* Runtime validation of temperature unit consistency with IntelliCenter
* Analyzes temperature readings to detect if they match the configured units
*/
static validateTemperatureUnitConsistency(temperatureReadings, configuredUnit) {
if (temperatureReadings.length === 0) {
return this.createValidationResult(true, null, configuredUnit, 0);
}
const validReadings = this.filterValidTemperatureReadings(temperatureReadings);
if (validReadings.length < 3) {
return this.createValidationResult(true, null, configuredUnit, validReadings.length);
}
const processedReadings = this.processValidTemperatureReadings(validReadings);
if (!processedReadings) {
return this.createValidationResult(true, null, configuredUnit, validReadings.length);
}
return this.validateDetectedTemperatureUnit(processedReadings, configuredUnit, validReadings.length);
}
static processValidTemperatureReadings(validReadings) {
const tempStats = this.calculateTemperatureStats(validReadings);
const detectionResult = this.detectTemperatureUnit(tempStats);
return detectionResult ? { validReadings, tempStats, detectionResult } : null;
}
static validateDetectedTemperatureUnit(processed, configuredUnit, validReadingsCount) {
const { tempStats, detectionResult } = processed;
const isConsistent = detectionResult.unit === configuredUnit;
const isHighConfidence = detectionResult.confidence > 0.7;
if (!isConsistent && isHighConfidence) {
return this.createInconsistentResult(detectionResult.unit, configuredUnit, validReadingsCount, tempStats);
}
return this.createValidationResult(true, detectionResult.unit, configuredUnit, validReadingsCount);
}
static filterValidTemperatureReadings(readings) {
return readings.filter(temp => !isNaN(temp) && temp !== null && temp !== undefined && temp > -50 && temp < 200);
}
static calculateTemperatureStats(readings) {
const avgTemp = readings.reduce((sum, temp) => sum + temp, 0) / readings.length;
const minTemp = Math.min(...readings);
const maxTemp = Math.max(...readings);
return { avgTemp, minTemp, maxTemp };
}
static detectTemperatureUnit(stats) {
const fahrenheitResult = this.tryDetectFahrenheit(stats);
if (fahrenheitResult) {
return fahrenheitResult;
}
const celsiusResult = this.tryDetectCelsius(stats);
if (celsiusResult) {
return celsiusResult;
}
return null;
}
static tryDetectFahrenheit(stats) {
const { avgTemp, minTemp, maxTemp } = stats;
const fahrenheitRanges = { avg: [65, 110], min: [50, 120], max: [50, 120] };
if (this.isWithinTemperatureRanges(avgTemp, minTemp, maxTemp, fahrenheitRanges)) {
return {
unit: types_1.TemperatureUnits.F,
confidence: this.calculateConfidence([avgTemp, minTemp, maxTemp], types_1.TemperatureUnits.F),
};
}
return null;
}
static tryDetectCelsius(stats) {
const { avgTemp, minTemp, maxTemp } = stats;
const celsiusRanges = { avg: [15, 45], min: [5, 50], max: [5, 50] };
if (this.isWithinTemperatureRanges(avgTemp, minTemp, maxTemp, celsiusRanges)) {
return {
unit: types_1.TemperatureUnits.C,
confidence: this.calculateConfidence([avgTemp, minTemp, maxTemp], types_1.TemperatureUnits.C),
};
}
return null;
}
static isWithinTemperatureRanges(avgTemp, minTemp, maxTemp, ranges) {
return (avgTemp >= ranges.avg[0] &&
avgTemp <= ranges.avg[1] &&
minTemp >= ranges.min[0] &&
minTemp <= ranges.min[1] &&
maxTemp >= ranges.max[0] &&
maxTemp <= ranges.max[1]);
}
static createValidationResult(isConsistent, detectedUnit, configuredUnit, analysisCount) {
return {
isConsistent,
detectedUnit,
configuredUnit,
analysisCount,
};
}
static createInconsistentResult(detectedUnit, configuredUnit, analysisCount, stats) {
const expectedRange = configuredUnit === types_1.TemperatureUnits.F ? '70-104°F for pools/spas' : '21-40°C for pools/spas';
const actualRange = `${stats.minTemp.toFixed(1)}-${stats.maxTemp.toFixed(1)}°${detectedUnit}`;
const warning = `Temperature unit mismatch detected. Configured: ${configuredUnit}, ` +
`but readings appear to be in ${detectedUnit} (${actualRange}). ` +
`Expected range: ${expectedRange}. Please verify your temperatureUnits setting.`;
return {
isConsistent: false,
detectedUnit,
configuredUnit,
analysisCount,
warning,
};
}
/**
* Calculate confidence level for temperature unit detection
*/
static calculateConfidence(readings, unit) {
if (readings.length === 0) {
return 0;
}
const avgTemp = readings.reduce((sum, temp) => sum + temp, 0) / readings.length;
const confidenceRanges = this.getConfidenceRanges(unit);
return this.determineConfidenceLevel(avgTemp, confidenceRanges);
}
static getConfidenceRanges(unit) {
if (unit === types_1.TemperatureUnits.F) {
return [
{ range: [70, 104], confidence: 0.9 },
{ range: [65, 110], confidence: 0.8 },
{ range: [60, 115], confidence: 0.6 },
];
}
else {
return [
{ range: [21, 40], confidence: 0.9 },
{ range: [18, 43], confidence: 0.8 },
{ range: [15, 45], confidence: 0.6 },
];
}
}
static determineConfidenceLevel(avgTemp, ranges) {
for (const { range, confidence } of ranges) {
if (avgTemp >= range[0] && avgTemp <= range[1]) {
return confidence;
}
}
return 0.3;
}
}
exports.ConfigValidator = ConfigValidator;
//# sourceMappingURL=configValidation.js.map