@switchbot/homebridge-switchbot
Version:
The SwitchBot plugin allows you to access your SwitchBot device(s) from HomeKit.
1,041 lines • 54.8 kB
JavaScript
/**
* Indicates which device types should prefer Matter if available.
* Based on HAP service mappings: device implementations use specific HomeKit services
* that map to corresponding Matter clusters when Matter is enabled.
*
* @property {boolean} [deviceType] - True if the device type supports Matter, false otherwise.
* @example
* DEVICE_MATTER_SUPPORTED['bot'] // true
*/
/**
* Factory function to create Matter handlers with Homebridge logger integration.
* Returns handler objects for supported device types, mapping Matter cluster actions to SwitchBot API calls.
*
* @param log - Homebridge logger instance
* @param deviceId - SwitchBot device ID
* @param type - Device type string
* @param client - SwitchBot client instance
* @returns Handler object for Matter clusters
*/
export const DEVICE_MATTER_SUPPORTED = {
// Core devices
'bot': true, // Switch → OnOff
'curtain': true, // WindowCovering → WindowCovering
'fan': true, // Fan → FanControl
'light': true, // Lightbulb → OnOff + LevelControl
'lightstrip': true, // Lightbulb (color) → OnOff + LevelControl + ColorControl
'motion': true, // MotionSensor → OccupancySensing
'contact': true, // ContactSensor → BooleanState
'vacuum': true, // Switch → RobotVacuumCleaner
'lock': true, // LockMechanism → DoorLock
'humidifier': true, // Fan + Humidity → OnOff + FanControl + RelativeHumidityMeasurement
'temperature': true, // TemperatureSensor → TemperatureMeasurement
// Switch devices
'relay': true, // Switch → OnOff
'relay switch 1': true, // Switch → OnOff
'relay switch 1pm': true, // Switch → OnOff
'plug': true, // Outlet → OnOff
'plug mini (jp)': true, // Outlet → OnOff
'plug mini (us)': true, // Outlet → OnOff
// Window covering variants
'blindtilt': true, // WindowCovering → WindowCovering
'blind tilt': true, // WindowCovering → WindowCovering
'curtain3': true, // WindowCovering → WindowCovering
'rollershade': true, // WindowCovering → WindowCovering
'roller shade': true, // WindowCovering → WindowCovering
'worollershade': true, // WindowCovering → WindowCovering
'wo rollershade': true, // WindowCovering → WindowCovering
// Vacuum variants (normalized to 'vacuum' before lookup)
'wosweeper': true, // VacuumDevice → RobotVacuumCleaner
'wosweepermini': true, // VacuumDevice → RobotVacuumCleaner
'wosweeperminipro': true, // VacuumDevice → RobotVacuumCleaner
'k10+': true, // VacuumDevice → RobotVacuumCleaner
'k10+ pro': true, // VacuumDevice → RobotVacuumCleaner
// Sensors
'meter': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
'meterplus': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
'meter plus (jp)': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
'meterpro': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
'meterpro(co2)': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
'waterdetector': true, // LeakSensor → BooleanState
'water detector': true, // LeakSensor → BooleanState
// Other devices
'smart fan': true, // Fan → FanControl
'strip light': true, // Lightbulb (color) → OnOff + LevelControl + ColorControl
'hub 2': false, // Hub device - not exposed as accessory
'walletfinder': false, // Button device - Matter support TBD
};
/**
* Default Matter cluster configurations by device type.
* Maps device types to their Matter cluster states (used when device doesn't provide clusters).
* Note: wosweeper/curtain/plug variants are normalized before cluster lookup (see loadDevices).
*
* @property {object} [deviceType] - The default cluster state for the device type.
* @example
* DEVICE_MATTER_CLUSTERS['bot'] // { onOff: { onOff: false } }
*/
export const DEVICE_MATTER_CLUSTERS = {
// Core devices - aligned with HAP service implementations
bot: { onOff: { onOff: false } }, // Switch → OnOff
vacuum: {
rvcRunMode: {
supportedModes: [
{ label: 'Idle', mode: 0, modeTags: [{ value: 16384 }] },
{ label: 'Cleaning', mode: 1, modeTags: [{ value: 16385 }] },
],
currentMode: 0,
},
rvcCleanMode: {
supportedModes: [
{ label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] },
],
currentMode: 0,
},
rvcOperationalState: {
operationalStateList: [
{ operationalStateId: 0 }, // Stopped
{ operationalStateId: 1 }, // Running
{ operationalStateId: 2 }, // Paused
{ operationalStateId: 3 }, // Error (required)
{ operationalStateId: 64 }, // Seeking charger
{ operationalStateId: 65 }, // Charging
{ operationalStateId: 66 }, // Docked
],
operationalState: 66,
},
}, // Switch in HAP, RobotVacuumCleaner in Matter
curtain: {
windowCovering: {
currentPositionLiftPercent100ths: 0,
targetPositionLiftPercent100ths: 0,
operationalStatus: {
global: 0,
lift: 0,
tilt: 0,
},
endProductType: 0,
configStatus: {
operational: true,
onlineReserved: true,
liftMovementReversed: false,
liftPositionAware: true,
tiltPositionAware: false,
liftEncoderControlled: true,
tiltEncoderControlled: false,
},
},
}, // WindowCovering → WindowCovering (includes curtain3, rollershade variants via normalization)
blindtilt: {
windowCovering: {
currentPositionLiftPercent100ths: 0,
targetPositionLiftPercent100ths: 0,
currentPositionTiltPercent100ths: 0,
targetPositionTiltPercent100ths: 0,
operationalStatus: {
global: 0,
lift: 0,
tilt: 0,
},
endProductType: 8,
configStatus: {
operational: true,
onlineReserved: true,
liftMovementReversed: false,
liftPositionAware: true,
tiltPositionAware: true,
liftEncoderControlled: true,
tiltEncoderControlled: true,
},
},
}, // WindowCovering with tilt → WindowCovering
fan: {
onOff: { onOff: false },
fanControl: {
fanMode: 0,
percentCurrent: 0,
percentSetting: 0,
speedCurrent: 0,
speedMax: 100,
},
}, // Fan → OnOff + FanControl
light: {
onOff: { onOff: false },
levelControl: {
currentLevel: 0,
minLevel: 0,
maxLevel: 254,
},
}, // Lightbulb → OnOff + LevelControl
lightstrip: {
onOff: { onOff: false },
levelControl: {
currentLevel: 0,
minLevel: 0,
maxLevel: 254,
},
colorControl: {
colorMode: 0,
},
}, // Lightbulb with color → OnOff + LevelControl + ColorControl
lock: {
doorLock: {
lockState: 0,
lockType: 0,
actuatorEnabled: true,
operatingMode: 0,
},
}, // LockMechanism → DoorLock
motion: {
occupancySensing: {
occupancy: 0,
occupancySensorType: 0,
},
}, // MotionSensor → OccupancySensing
contact: {
booleanState: {
stateValue: false,
},
}, // ContactSensor → BooleanState
humidifier: {
onOff: { onOff: false },
fanControl: {
fanMode: 0,
percentCurrent: 0,
},
relativeHumidityMeasurement: {
measuredValue: 0,
minMeasuredValue: 0,
maxMeasuredValue: 100,
},
}, // HumidifierDehumidifier → OnOff + FanControl + RelativeHumidityMeasurement
temperature: {
temperatureMeasurement: {
measuredValue: 0,
minMeasuredValue: -27315,
maxMeasuredValue: 32767,
},
}, // TemperatureSensor → TemperatureMeasurement
// Switch/Outlet devices
relay: { onOff: { onOff: false } }, // Switch → OnOff
plug: {
onOff: { onOff: false },
electricalMeasurement: {
activePower: 0,
rmsCurrent: 0,
rmsVoltage: 0,
},
}, // Outlet → OnOff + ElectricalMeasurement (for PM models)
// Sensors
meter: {
temperatureMeasurement: {
measuredValue: 0,
minMeasuredValue: -27315,
maxMeasuredValue: 32767,
},
relativeHumidityMeasurement: {
measuredValue: 0,
minMeasuredValue: 0,
maxMeasuredValue: 100,
},
}, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
waterdetector: {
booleanState: {
stateValue: false,
},
}, // LeakSensor → BooleanState
};
export function createMatterHandlers(log, deviceId, type, client) {
const lowerType = type.toLowerCase();
switch (lowerType) {
case 'vacuum':
return {
rvcRunMode: {
changeToMode: async (request) => {
const modeNames = ['Idle', 'Cleaning', 'Mapping'];
const modeName = modeNames[request?.newMode] || `Unknown (${request?.newMode})`;
log.info(`[${deviceId}] RVC run mode change requested: ${modeName}`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
// For K10+ family: use 'start' to begin cleaning (mode 1 = Cleaning)
// For older K10+: only supports start/stop/dock
// For newer K20+/S10/S20: supports startClean with more parameters
// Map Matter mode to SwitchBot command
const switchBotCommand = request?.newMode === 1 ? 'start' : 'stop';
const body = {
command: switchBotCommand,
parameter: 'default',
commandType: 'command',
};
log.debug(`[${deviceId}] Sending RVC mode change request:`, JSON.stringify(body));
const result = await client.setDeviceState(deviceId, body);
log.debug(`[${deviceId}] RVC mode change API response:`, JSON.stringify(result));
log.info(`[${deviceId}] RVC mode changed successfully to ${switchBotCommand}`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to change RVC mode:`, e);
return { success: false, error: e };
}
},
},
rvcCleanMode: {
changeToMode: async (request) => {
const modeName = request?.newMode !== undefined ? `Mode ${request.newMode}` : 'Unknown';
log.info(`[${deviceId}] RVC clean mode change requested: ${modeName}`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
// Clean mode (vacuum/mop/etc) not directly supported via Matter for K10+
// K20+ Pro and newer models support via startClean action parameter
log.info(`[${deviceId}] Clean mode change requires startClean command (not yet implemented for Matter)`);
return { success: true };
}
catch (e) {
log.error(`[${deviceId}] Failed to change RVC clean mode:`, e);
return { success: false, error: e };
}
},
},
rvcOperationalState: {
pause: async () => {
log.info(`[${deviceId}] RVC pause command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const body = {
command: 'stop',
parameter: 'default',
commandType: 'command',
};
log.debug(`[${deviceId}] Sending RVC pause request:`, JSON.stringify(body));
const result = await client.setDeviceState(deviceId, body);
log.debug(`[${deviceId}] RVC pause API response:`, JSON.stringify(result));
log.info(`[${deviceId}] RVC paused successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to pause RVC:`, e);
return { success: false, error: e };
}
},
resume: async () => {
log.info(`[${deviceId}] RVC resume command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const body = {
command: 'start',
parameter: 'default',
commandType: 'command',
};
log.debug(`[${deviceId}] Sending RVC resume request:`, JSON.stringify(body));
const result = await client.setDeviceState(deviceId, body);
log.debug(`[${deviceId}] RVC resume API response:`, JSON.stringify(result));
log.info(`[${deviceId}] RVC resumed successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to resume RVC:`, e);
return { success: false, error: e };
}
},
goHome: async () => {
log.info(`[${deviceId}] RVC goHome command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const body = {
command: 'dock',
parameter: 'default',
commandType: 'command',
};
log.debug(`[${deviceId}] Sending RVC goHome request:`, JSON.stringify(body));
const result = await client.setDeviceState(deviceId, body);
log.debug(`[${deviceId}] RVC goHome API response:`, JSON.stringify(result));
log.info(`[${deviceId}] RVC sent to dock successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to send goHome command:`, e);
return { success: false, error: e };
}
},
},
};
case 'bot':
return {
onOff: {
on: async () => {
log.info(`[${deviceId}] Bot ON command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOn',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Bot turned on successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn on Bot:`, e);
return { success: false, error: e };
}
},
off: async () => {
log.info(`[${deviceId}] Bot OFF command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOff',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Bot turned off successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn off Bot:`, e);
return { success: false, error: e };
}
},
},
};
case 'curtain':
case 'blindtilt':
return {
windowCovering: {
goToLiftPercentage: async (request) => {
const percentage = request?.liftPercent100thsValue;
log.info(`[${deviceId}] Curtain position change requested: ${percentage}`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
// Convert Matter percentage (0-10000) to SwitchBot (0-100)
const position = Math.max(0, Math.min(100, Math.round((percentage || 0) / 100)));
const result = await client.setDeviceState(deviceId, {
command: 'setPosition',
parameter: String(position),
commandType: 'command',
});
log.info(`[${deviceId}] Curtain position set to ${position}% successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to set curtain position:`, e);
return { success: false, error: e };
}
},
upOrOpen: async () => {
log.info(`[${deviceId}] Curtain open command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'open',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Curtain opened successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to open curtain:`, e);
return { success: false, error: e };
}
},
downOrClose: async () => {
log.info(`[${deviceId}] Curtain close command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'close',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Curtain closed successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to close curtain:`, e);
return { success: false, error: e };
}
},
stopMotion: async () => {
log.info(`[${deviceId}] Curtain stop command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'pause',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Curtain motion stopped successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to stop curtain:`, e);
return { success: false, error: e };
}
},
},
};
case 'plug':
return {
onOff: {
on: async () => {
log.info(`[${deviceId}] Plug ON command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOn',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Plug turned on successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn on plug:`, e);
return { success: false, error: e };
}
},
off: async () => {
log.info(`[${deviceId}] Plug OFF command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOff',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Plug turned off successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn off plug:`, e);
return { success: false, error: e };
}
},
},
};
case 'lock':
return {
doorLock: {
setLockState: async (request) => {
const state = request?.lockState === 1 ? 'LOCKED' : 'UNLOCKED';
log.info(`[${deviceId}] Lock state change requested: ${state}`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const command = request?.lockState === 1 ? 'lock' : 'unlock';
const result = await client.setDeviceState(deviceId, {
command,
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Lock ${state} successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to change lock state:`, e);
return { success: false, error: e };
}
},
},
};
case 'fan':
return {
onOff: {
on: async () => {
log.info(`[${deviceId}] Fan ON command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOn',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Fan turned on successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn on fan:`, e);
return { success: false, error: e };
}
},
off: async () => {
log.info(`[${deviceId}] Fan OFF command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOff',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Fan turned off successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn off fan:`, e);
return { success: false, error: e };
}
},
},
fanControl: {
setFanSpeed: async (request) => {
const speed = request?.percentSetting || 0;
log.info(`[${deviceId}] Fan speed change requested: ${speed}%`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
// Convert percentage to SwitchBot fan speed parameter
const speedParam = Math.max(1, Math.min(100, speed));
const result = await client.setDeviceState(deviceId, {
command: 'setFanSpeed',
parameter: String(speedParam),
commandType: 'command',
});
log.info(`[${deviceId}] Fan speed set to ${speedParam}% successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to set fan speed:`, e);
return { success: false, error: e };
}
},
},
};
case 'light':
return {
onOff: {
on: async () => {
log.info(`[${deviceId}] Light ON command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOn',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Light turned on successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn on light:`, e);
return { success: false, error: e };
}
},
off: async () => {
log.info(`[${deviceId}] Light OFF command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOff',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Light turned off successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn off light:`, e);
return { success: false, error: e };
}
},
},
levelControl: {
moveToLevel: async (request) => {
const level = request?.level || 0;
// Convert from 0-254 to 0-100
const brightness = Math.round((level / 254) * 100);
log.info(`[${deviceId}] Light brightness change requested: ${brightness}%`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const param = Math.max(0, Math.min(100, brightness));
const result = await client.setDeviceState(deviceId, {
command: 'setBrightness',
parameter: String(param),
commandType: 'command',
});
log.info(`[${deviceId}] Light brightness set to ${param}% successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to set light brightness:`, e);
return { success: false, error: e };
}
},
},
};
case 'lightstrip':
return {
onOff: {
on: async () => {
log.info(`[${deviceId}] Lightstrip ON command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOn',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Lightstrip turned on successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn on lightstrip:`, e);
return { success: false, error: e };
}
},
off: async () => {
log.info(`[${deviceId}] Lightstrip OFF command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOff',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Lightstrip turned off successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn off lightstrip:`, e);
return { success: false, error: e };
}
},
},
levelControl: {
moveToLevel: async (request) => {
const level = request?.level || 0;
// Convert from 0-254 to 0-100
const brightness = Math.round((level / 254) * 100);
log.info(`[${deviceId}] Lightstrip brightness change requested: ${brightness}%`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const param = Math.max(0, Math.min(100, brightness));
const result = await client.setDeviceState(deviceId, {
command: 'setBrightness',
parameter: String(param),
commandType: 'command',
});
log.info(`[${deviceId}] Lightstrip brightness set to ${param}% successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to set lightstrip brightness:`, e);
return { success: false, error: e };
}
},
},
colorControl: {
moveToHueAndSaturation: async (request) => {
const hue = request?.hue || 0;
const saturation = request?.saturation || 0;
log.info(`[${deviceId}] Lightstrip color change requested: hue=${hue}, sat=${saturation}`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
// Convert hue (0-254) and saturation (0-254) to combined color parameter
// SwitchBot typically expects RGB or HSV format as parameter
const colorParam = `${Math.round(hue)},${Math.round(saturation)}`;
const result = await client.setDeviceState(deviceId, {
command: 'setColor',
parameter: colorParam,
commandType: 'command',
});
log.info(`[${deviceId}] Lightstrip color set successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to set lightstrip color:`, e);
return { success: false, error: e };
}
},
moveToColorTemperature: async (request) => {
const mireds = request?.colorTemperatureMireds || 400;
// Convert mireds (158-500 typical range) to Kelvin: K = 1000000 / mireds
const kelvin = Math.round(1000000 / mireds);
log.info(`[${deviceId}] Lightstrip color temperature change requested: ${mireds} mireds (${kelvin}K)`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
// Map Kelvin to SwitchBot color temperature parameter (typically 0-100 or specific values)
// Normalize to 0-100 scale where 0=warm (2700K) and 100=cool (6500K)
const colorTempParam = Math.max(0, Math.min(100, Math.round(((kelvin - 2700) / 3800) * 100)));
const result = await client.setDeviceState(deviceId, {
command: 'setColorTemperature',
parameter: String(colorTempParam),
commandType: 'command',
});
log.info(`[${deviceId}] Lightstrip color temperature set to ${kelvin}K successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to set lightstrip color temperature:`, e);
return { success: false, error: e };
}
},
},
};
case 'humidifier':
return {
onOff: {
on: async () => {
log.info(`[${deviceId}] Humidifier ON command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOn',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Humidifier turned on successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn on humidifier:`, e);
return { success: false, error: e };
}
},
off: async () => {
log.info(`[${deviceId}] Humidifier OFF command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOff',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Humidifier turned off successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn off humidifier:`, e);
return { success: false, error: e };
}
},
},
fanControl: {
setFanSpeed: async (request) => {
const speed = request?.percentSetting || 0;
log.info(`[${deviceId}] Humidifier speed change requested: ${speed}%`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
// Convert percentage to SwitchBot humidifier speed parameter
const speedParam = Math.max(1, Math.min(100, speed));
const result = await client.setDeviceState(deviceId, {
command: 'setFanSpeed',
parameter: String(speedParam),
commandType: 'command',
});
log.info(`[${deviceId}] Humidifier speed set to ${speedParam}% successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to set humidifier speed:`, e);
return { success: false, error: e };
}
},
},
};
case 'relay':
return {
onOff: {
on: async () => {
log.info(`[${deviceId}] Relay ON command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOn',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Relay turned on successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn on relay:`, e);
return { success: false, error: e };
}
},
off: async () => {
log.info(`[${deviceId}] Relay OFF command received`);
if (!client) {
log.warn(`[${deviceId}] No SwitchBot client available`);
return { success: false };
}
try {
const result = await client.setDeviceState(deviceId, {
command: 'turnOff',
parameter: 'default',
commandType: 'command',
});
log.info(`[${deviceId}] Relay turned off successfully`);
return { success: true, result };
}
catch (e) {
log.error(`[${deviceId}] Failed to turn off relay:`, e);
return { success: false, error: e };
}
},
},
};
default:
return undefined;
}
}
/**
* Resolves the Matter device type for a given device, using the Matter API and device type mappings.
* Used to map normalized device types to Matter device type definitions.
*
* @param matterApi - The Matter API object containing deviceTypes
* @param type - The normalized device type string
* @param createdDeviceType - Optionally, an already created device type object
* @param clusters - Optionally, the clusters object for the device
* @returns The resolved Matter device type object
*/
const DEVICE_MATTER_DEVICE_TYPE_KEYS = {
bot: 'OnOffSwitch',
vacuum: 'RoboticVacuumCleaner',
curtain: 'WindowCovering',
blindtilt: 'WindowCovering',
fan: 'Fan',
light: 'DimmableLight',
lightstrip: 'ExtendedColorLight',
lock: 'DoorLock',
motion: 'MotionSensor',
contact: 'ContactSensor',
humidifier: 'Fan',
temperature: 'TemperatureSensor',
relay: 'OnOffSwitch',
plug: 'OnOffOutlet',
meter: 'TemperatureSensor',
waterdetector: 'LeakSensor',
};
export function resolveMatterDeviceType(matterApi, type, createdDeviceType, clusters) {
if (createdDeviceType && typeof createdDeviceType === 'object' && typeof createdDeviceType.with === 'function') {
return createdDeviceType;
}
const lowerType = (typeof createdDeviceType === 'string' && createdDeviceType) ? createdDeviceType.toLowerCase() : (type || '').toLowerCase();
// Cluster-based upgrade for color lights if descriptor omitted device type.
const hasColorControl = !!clusters?.colorControl;
const inferredType = hasColorControl && lowerType === 'light'
? 'lightstrip'
: lowerType;
const mappedKey = DEVICE_MATTER_DEVICE_TYPE_KEYS[inferredType] || 'OnOffSwitch';
return matterApi?.deviceTypes?.[mappedKey] || matterApi?.deviceTypes?.OnOffSwitch;
}
/**
* Canonical Matter cluster ID mapping (from matter.js clusters).
* Maps cluster names to their numeric cluster IDs.
*
* @example
* MATTER_CLUSTER_IDS.OnOff // 0x0006
*/
export const MATTER_CLUSTER_IDS = {
OnOff: 0x0006,
LevelControl: 0x0008,
ColorControl: 0x0300,
WindowCovering: 0x0102,
DoorLock: 0x0101,
FanControl: 0x0202,
RelativeHumidityMeasurement: 0x0405,
};
/**
* Common Matter attribute IDs grouped by cluster.
* Maps cluster names to objects mapping attribute names to their numeric attribute IDs.
*
* @example
* MATTER_ATTRIBUTE_IDS.OnOff.OnOff // 0x0000
*/
export const MATTER_ATTRIBUTE_IDS = {
OnOff: { OnOff: 0x0000 },
LevelControl: { CurrentLevel: 0x0000 },
ColorControl: { CurrentHue: 0x0000, CurrentSaturation: 0x0001, ColorTemperatureMireds: 0x0002 },
WindowCovering: { CurrentPosition: 0x0000, TargetPosition: 0x0001 },
FanControl: { SpeedCurrent: 0x0000 },
DoorLock: { LockState: 0x0000 },
RelativeHumidityMeasurement: { MeasuredValue: 0x0000 },
};
/**
* Normalizes a device type string for Matter integration.
* Maps