homebridge
Version:
HomeKit support for the impatient
308 lines • 12.3 kB
JavaScript
/**
* Maps cluster attributes to Matter.js commands
* This centralizes the logic for converting UI attribute updates to behavior commands
*
* Supports all Matter device types including:
* - Lights (OnOff, Dimmable, Color Temperature, Color, Extended Color)
* - Switches and Outlets
* - Fans
* - Window Coverings (Blinds)
* - Door Locks
* - Thermostats
* - Robotic Vacuums
*/
/**
* Central registry of attribute-to-command mappings for each cluster
*/
const CLUSTER_COMMAND_MAPPINGS = {
// ============================================================================
// BASIC LIGHTING & POWER
// ============================================================================
// OnOff Cluster - Used by lights, switches, outlets
onOff: {
map: (attributes) => {
if ('onOff' in attributes) {
return {
command: attributes.onOff ? 'on' : 'off',
};
}
return null;
},
},
// LevelControl Cluster - Used by dimmable lights
levelControl: {
map: (attributes) => {
if ('currentLevel' in attributes) {
return {
command: 'moveToLevelWithOnOff',
args: { level: attributes.currentLevel },
};
}
return null;
},
},
// ============================================================================
// COLOR CONTROL
// ============================================================================
// ColorControl Cluster - Used by color temperature and RGB lights
colorControl: {
map: (attributes) => {
// Color temperature control (in mireds)
if ('colorTemperatureMireds' in attributes) {
return {
command: 'moveToColorTemperature',
args: {
colorTemperatureMireds: attributes.colorTemperatureMireds,
transitionTime: attributes.transitionTime ?? 0,
optionsMask: 1, // Bit 0 = ExecuteIfOff
optionsOverride: 1, // Execute even if light is off
},
};
}
// XY color space control
if ('colorX' in attributes && 'colorY' in attributes) {
return {
command: 'moveToColor',
args: {
colorX: attributes.colorX,
colorY: attributes.colorY,
transitionTime: attributes.transitionTime ?? 0,
optionsMask: 1, // Bit 0 = ExecuteIfOff
optionsOverride: 1, // Execute even if light is off
},
};
}
// Hue & Saturation control
if ('currentHue' in attributes || 'currentSaturation' in attributes) {
return {
command: 'moveToHueAndSaturation',
args: {
hue: attributes.currentHue ?? 0,
saturation: attributes.currentSaturation ?? 0,
transitionTime: attributes.transitionTime ?? 0,
optionsMask: 1, // Bit 0 = ExecuteIfOff
optionsOverride: 1, // Execute even if light is off
},
};
}
return null;
},
},
// ============================================================================
// FAN CONTROL
// ============================================================================
// FanControl Cluster - Used by fans
// Fan mode and percent changes trigger change handlers automatically via attribute updates
fanControl: {
map: (attributes) => {
// Fan mode change
if ('fanMode' in attributes) {
// This triggers fanModeChange handler automatically
// No explicit command needed - just update the attribute
return null;
}
// Percent setting change
if ('percentSetting' in attributes) {
// This triggers percentSettingChange handler automatically
// No explicit command needed - just update the attribute
return null;
}
return null;
},
},
// ============================================================================
// WINDOW COVERINGS (BLINDS)
// ============================================================================
// WindowCovering Cluster - Used by blinds, shades, curtains
windowCovering: {
map: (attributes) => {
// Direct commands via _command attribute
if ('_command' in attributes) {
const cmd = attributes._command;
if (cmd === 'upOrOpen' || cmd === 'downOrClose' || cmd === 'stopMotion') {
return { command: cmd };
}
}
// Position control (lift)
if ('targetPositionLiftPercent100ths' in attributes) {
return {
command: 'goToLiftPercentage',
args: {
liftPercent100thsValue: attributes.targetPositionLiftPercent100ths,
},
};
}
// Position control (tilt) - for venetian blinds
if ('targetPositionTiltPercent100ths' in attributes) {
return {
command: 'goToTiltPercentage',
args: {
tiltPercent100thsValue: attributes.targetPositionTiltPercent100ths,
},
};
}
return null;
},
},
// ============================================================================
// DOOR LOCK
// ============================================================================
// DoorLock Cluster
doorLock: {
map: (attributes) => {
// Direct command via _command attribute
if ('_command' in attributes) {
const cmd = attributes._command;
if (cmd === 'lockDoor' || cmd === 'unlockDoor') {
return { command: cmd };
}
}
// Or via lockState attribute
if ('lockState' in attributes) {
// 1 = Locked, 2 = Unlocked
return {
command: attributes.lockState === 1 ? 'lockDoor' : 'unlockDoor',
};
}
return null;
},
},
// ============================================================================
// THERMOSTAT
// ============================================================================
// Thermostat Cluster
thermostat: {
map: (attributes) => {
// Setpoint raise/lower command
if ('_command' in attributes && attributes._command === 'setpointRaiseLower') {
return {
command: 'setpointRaiseLower',
args: {
mode: attributes.mode ?? 0,
amount: attributes.amount ?? 10,
},
};
}
// System mode change
if ('systemMode' in attributes) {
// This triggers systemModeChange handler automatically
// No explicit command needed - just update the attribute
return null;
}
// Heating setpoint change
if ('occupiedHeatingSetpoint' in attributes) {
// This triggers occupiedHeatingSetpointChange handler automatically
return null;
}
// Cooling setpoint change
if ('occupiedCoolingSetpoint' in attributes) {
// This triggers occupiedCoolingSetpointChange handler automatically
return null;
}
return null;
},
},
// ============================================================================
// ROBOTIC VACUUM
// ============================================================================
// Robotic Vacuum Operational State - for action buttons (pause, resume, goHome)
// Note: Start/stop is controlled via rvcRunMode.changeToMode, not rvcOperationalState.
// The behavior supports: pause, resume, goHome
rvcOperationalState: {
map: (attributes) => {
// Direct command invocation via _command attribute
if ('_command' in attributes) {
const cmd = attributes._command;
if (cmd === 'pause' || cmd === 'resume' || cmd === 'goHome') {
return { command: cmd };
}
return null;
}
// Handle operationalState attribute changes
// Map state values to available commands:
// 1 = Running → resume, 2 = Paused → pause
// Other states (0=Stopped, 64+=dock states) → state-only update
if ('operationalState' in attributes) {
const state = attributes.operationalState;
switch (state) {
case 1: // Running
return { command: 'resume' };
case 2: // Paused
return { command: 'pause' };
default:
return null; // State-only update for stopped/docked/charging etc.
}
}
return null;
},
},
// Robotic Vacuum Run Mode - for mode selection (Idle, Cleaning, Mapping)
rvcRunMode: {
map: (attributes) => {
if ('currentMode' in attributes) {
return {
command: 'changeToMode',
args: { newMode: attributes.currentMode },
};
}
return null;
},
},
// Robotic Vacuum Clean Mode - for cleaning method selection (Vacuum, Mop, etc.)
rvcCleanMode: {
map: (attributes) => {
if ('currentMode' in attributes) {
return {
command: 'changeToMode',
args: { newMode: attributes.currentMode },
};
}
return null;
},
},
// Service Area - for room/zone selection
serviceArea: {
map: (attributes) => {
// Select multiple areas
if ('selectedAreas' in attributes && Array.isArray(attributes.selectedAreas)) {
return {
command: 'selectAreas',
args: { newAreas: attributes.selectedAreas },
};
}
// Skip a single area
if ('skipArea' in attributes) {
return {
command: 'skipArea',
args: { skippedArea: attributes.skipArea },
};
}
return null;
},
},
};
/**
* Maps attributes to a Matter.js command for a given cluster
* @param cluster The cluster name (e.g., 'onOff', 'levelControl')
* @param attributes The attributes to map
* @returns Command name and optional arguments, or throws if mapping not found
*
* Note: Some attribute changes trigger handlers automatically without explicit commands.
* In these cases, the mapper returns null, which should be handled by the caller
* to update state directly instead of invoking a command.
*/
export function mapAttributesToCommand(cluster, attributes) {
const mapping = CLUSTER_COMMAND_MAPPINGS[cluster];
if (!mapping) {
throw new Error(`Command mapping not implemented for cluster: ${cluster}`);
}
const result = mapping.map(attributes);
if (result === null) {
// Some clusters trigger change handlers automatically via attribute updates
// These don't need explicit commands (e.g., fanMode, systemMode changes)
// The caller should handle null by updating state directly
return null;
}
return result;
}
//# sourceMappingURL=ClusterCommandMapper.js.map