UNPKG

homebridge

Version:
241 lines 9.53 kB
/** * Matter Configuration Validation * * Provides validation utilities and orchestration for Matter configuration objects. * Includes both low-level validation primitives and high-level validation methods. */ import { Logger } from '../logger.js'; const log = Logger.withPrefix('Matter/Config'); const COLON_RE = /:/g; // ============================================================================ // Low-level validation utilities // ============================================================================ /** * Validate a Matter port number * * @param port - Port number to validate * @param checkConflicts - Whether to check for known conflicting ports * @returns Validation result with error/warning messages */ export function validatePort(port, checkConflicts = false) { if (!Number.isInteger(port)) { return { valid: false, error: `Port must be an integer (got: ${port})` }; } if (port < 1025 || port > 65534) { return { valid: false, error: `Port must be between 1025 and 65534 (got: ${port})` }; } // Check for common conflicts if (checkConflicts) { const conflictPorts = [5353, 8080, 8443]; // mDNS, common HTTP ports if (conflictPorts.includes(port)) { return { valid: true, warning: `Port ${port} may conflict with other services. Consider using a different port.`, }; } } return { valid: true }; } /** * Sanitize a unique ID for Matter filesystem storage * * Removes colons from MAC addresses and converts to uppercase for consistency. * Example: "AB:CD:EF:12:34:56" -> "ABCDEF123456" * * @param uniqueId - Unique identifier to sanitize (typically a MAC address) * @returns Sanitized unique ID and any warnings */ export function sanitizeUniqueId(uniqueId) { const warnings = []; const original = uniqueId; if (uniqueId.trim().length === 0) { return { value: '', warnings: ['uniqueId is empty after trimming'], }; } // Remove colons and convert to uppercase for Matter storage paths const sanitized = uniqueId.replace(COLON_RE, '').toUpperCase(); if (sanitized !== original) { warnings.push(`uniqueId was sanitized from "${original}" to "${sanitized}"`); } if (sanitized.length === 0) { warnings.push('uniqueId resulted in empty string after sanitization'); } return { value: sanitized, warnings }; } /** * Truncate a string to a maximum length with warning * * @param value - String to truncate * @param maxLength - Maximum allowed length * @param fieldName - Name of the field (for warnings) * @returns Truncated value and any warnings */ export function truncateString(value, maxLength, fieldName) { const warnings = []; if (value.length > maxLength) { warnings.push(`${fieldName} exceeds ${maxLength} characters, truncating: ${value}`); log.warn(`${fieldName} exceeds ${maxLength} characters, truncating: ${value}`); return { value: value.slice(0, maxLength), warnings, }; } return { value, warnings }; } /** * Check for port conflicts between HAP and Matter ports * * @param hapPort - HAP bridge port * @param matterPort - Matter bridge port * @returns Warning message if ports are too close, undefined otherwise */ export function checkPortProximity(hapPort, matterPort) { const MIN_PORT_SEPARATION = 10; if (Math.abs(hapPort - matterPort) < MIN_PORT_SEPARATION) { return `HAP port ${hapPort} and Matter port ${matterPort} are very close. Consider spacing them further apart.`; } return undefined; } // ============================================================================ // High-level validation orchestration // ============================================================================ /** * Validate Matter configuration for production readiness */ export class MatterConfigValidator { /** * Validate a Matter configuration object */ static validate(config) { const result = { isValid: true, errors: [], warnings: [], }; // Validate port configuration this.validatePort(config, result); result.isValid = result.errors.length === 0; if (result.warnings.length > 0) { log.warn('Matter configuration warnings:'); result.warnings.forEach(warning => log.warn(` - ${warning}`)); } if (result.errors.length > 0) { log.error('Matter configuration errors:'); result.errors.forEach(error => log.error(` - ${error}`)); } return result; } static validatePort(config, result) { const port = config.port; if (port !== undefined && port !== null) { if (typeof port === 'number') { const validation = validatePort(port, true); if (!validation.valid) { result.errors.push(validation.error); } else if (validation.warning) { result.warnings.push(validation.warning); } } else { result.errors.push(`Port must be a number, got ${typeof port}.`); } } } /** * Validate child Matter configuration (_bridge.matter property) */ static validateChildMatterConfig(config, configType, identifier) { const result = { isValid: true, errors: [], warnings: [], }; // If no _bridge.matter property, no validation needed if (!config._bridge?.matter) { return result; } const matterConfig = config._bridge.matter; const prefix = `Child Matter bridge for ${configType} "${identifier}"`; // Validate port if specified if (matterConfig.port !== undefined) { const validation = validatePort(matterConfig.port, false); if (!validation.valid) { result.errors.push(`${prefix}: ${validation.error}`); result.isValid = false; } } else { result.warnings.push(`${prefix}: No port specified. Port will be auto-allocated.`); } // Check for port conflicts with HAP bridge if (config._bridge && config._bridge.matter) { // Ensure ports don't conflict if both HAP and Matter are configured if (config._bridge.port && matterConfig.port) { const proximityWarning = checkPortProximity(config._bridge.port, matterConfig.port); if (proximityWarning) { result.warnings.push(`${prefix}: ${proximityWarning}`); } } } // Log validation results if (result.errors.length > 0) { log.error(`${prefix} validation errors:`); result.errors.forEach(error => log.error(` - ${error}`)); } if (result.warnings.length > 0) { log.warn(`${prefix} validation warnings:`); result.warnings.forEach(warning => log.warn(` - ${warning}`)); } return result; } /** * Validate all child Matter configurations in a config */ static validateAllChildMatterConfigs(platforms, accessories) { const result = { isValid: true, errors: [], warnings: [], }; const usedPorts = new Set(); // Validate platform _bridge.matter configs for (const platform of platforms) { if (platform._bridge?.matter) { const validation = this.validateChildMatterConfig(platform, 'platform', platform.platform || 'unknown'); result.errors.push(...validation.errors); result.warnings.push(...validation.warnings); result.isValid = result.isValid && validation.isValid; // Check for port conflicts if (platform._bridge.matter.port) { if (usedPorts.has(platform._bridge.matter.port)) { result.errors.push(`Duplicate Matter port ${platform._bridge.matter.port} detected. Each Matter bridge must use a unique port.`); result.isValid = false; } usedPorts.add(platform._bridge.matter.port); } } } // Validate accessory _bridge.matter configs for (const accessory of accessories) { if (accessory._bridge?.matter) { const validation = this.validateChildMatterConfig(accessory, 'accessory', accessory.accessory || 'unknown'); result.errors.push(...validation.errors); result.warnings.push(...validation.warnings); result.isValid = result.isValid && validation.isValid; // Check for port conflicts if (accessory._bridge.matter.port) { if (usedPorts.has(accessory._bridge.matter.port)) { result.errors.push(`Duplicate Matter port ${accessory._bridge.matter.port} detected. Each Matter bridge must use a unique port.`); result.isValid = false; } usedPorts.add(accessory._bridge.matter.port); } } } return result; } } //# sourceMappingURL=configValidator.js.map