homebridge
Version:
HomeKit support for the impatient
241 lines • 9.53 kB
JavaScript
/**
* 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