UNPKG

homebridge-roborock-vacuum-update

Version:

Comprehensive Homebridge plugin for Roborock vacuum cleaners with full HomeKit integration including mopping, dock features, and advanced controls.

909 lines (908 loc) 82.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** * An instance of this class is created for each accessory the platform registers. * Each accessory may expose multiple services of different service types. */ class RoborockVacuumAccessory { constructor(platform, accessory) { var _a; this.platform = platform; this.accessory = accessory; this.services = []; this.sceneServices = new Map(); this.currentScenes = []; this.roomServices = new Map(); this.currentRooms = []; this.currentCleaningMode = 'vac&mop'; const self = this; // Accessory Information // https://developers.homebridge.io/#/service/AccessoryInformation (_a = this.accessory.getService(this.platform.Service.AccessoryInformation)) === null || _a === void 0 ? void 0 : _a.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Roborock').setCharacteristic(this.platform.Characteristic.Model, this.platform.roborockAPI.getProductAttribute(accessory.context, "model") || 'Unknown').setCharacteristic(this.platform.Characteristic.SerialNumber, this.platform.roborockAPI.getVacuumDeviceInfo(accessory.context, "sn") || 'Unknown').setCharacteristic(this.platform.Characteristic.FirmwareRevision, this.platform.roborockAPI.getVacuumDeviceInfo(accessory.context, "fv") || 'Unknown'); this.services['Fan'] = this.accessory.getService(this.platform.Service.Fanv2) || this.accessory.addService(this.platform.Service.Fanv2); // This is what is displayed as the default name on the Home app this.services['Fan'].setCharacteristic(this.platform.Characteristic.Name, this.platform.roborockAPI.getVacuumDeviceInfo(accessory.context, "name") || 'Roborock Vacuum'); this.services['Fan'].getCharacteristic(this.platform.Characteristic.Active) .onSet(this.setActive.bind(this)) .onGet(this.getActive.bind(this)); // Add Fan Speed Control (Suction Power) this.services['Fan'].getCharacteristic(this.platform.Characteristic.RotationSpeed) .onSet(this.setRotationSpeed.bind(this)) .onGet(this.getRotationSpeed.bind(this)) .setProps({ minValue: 0, maxValue: 100, minStep: 25, // 0%, 25%, 50%, 75%, 100% (maps to Off, Quiet, Balanced, Turbo, Max) }); // Add Pause/Resume (TargetFanState) this.services['Fan'].getCharacteristic(this.platform.Characteristic.TargetFanState) .onSet(this.setTargetFanState.bind(this)) .onGet(this.getTargetFanState.bind(this)); // Return to Dock Switch this.services['ReturnToDock'] = this.accessory.getServiceById(this.platform.Service.Switch, 'ReturnToDock') || this.accessory.addService(this.platform.Service.Switch, 'Return to Dock', 'ReturnToDock'); this.services['ReturnToDock'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setReturnToDock.bind(this)) .onGet(this.getReturnToDock.bind(this)); // Spot Cleaning Switch this.services['SpotClean'] = this.accessory.getServiceById(this.platform.Service.Switch, 'SpotClean') || this.accessory.addService(this.platform.Service.Switch, 'Spot Clean', 'SpotClean'); this.services['SpotClean'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setSpotClean.bind(this)) .onGet(this.getSpotClean.bind(this)); // Find Me Switch this.services['FindMe'] = this.accessory.getServiceById(this.platform.Service.Switch, 'FindMe') || this.accessory.addService(this.platform.Service.Switch, 'Find Me', 'FindMe'); this.services['FindMe'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setFindMe.bind(this)) .onGet(this.getFindMe.bind(this)); // Error Status Contact Sensor this.services['ErrorStatus'] = this.accessory.getServiceById(this.platform.Service.ContactSensor, 'ErrorStatus') || this.accessory.addService(this.platform.Service.ContactSensor, 'Error Status', 'ErrorStatus'); this.services['ErrorStatus'].getCharacteristic(this.platform.Characteristic.ContactSensorState) .onGet(this.getErrorStatus.bind(this)); // Water Level Control (Scrub Intensity) - Using Lightbulb service for level control // Only add if device supports it if (this.platform.roborockAPI.supportsWaterLevel(this.accessory.context)) { this.services['WaterLevel'] = this.accessory.getServiceById(this.platform.Service.Lightbulb, 'WaterLevel') || this.accessory.addService(this.platform.Service.Lightbulb, 'Scrub Intensity', 'WaterLevel'); this.services['WaterLevel'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setWaterLevelOn.bind(this)) .onGet(this.getWaterLevelOn.bind(this)); this.services['WaterLevel'].getCharacteristic(this.platform.Characteristic.Brightness) .onSet(this.setWaterLevelBrightness.bind(this)) .onGet(this.getWaterLevelBrightness.bind(this)) .setProps({ minValue: 0, maxValue: 100, minStep: 33, // 0%, 33%, 67%, 100% (maps to Off, Mild, Standard, Intense) }); } // Cleaning Mode Selector - Using Lightbulb service for mode selection // Only add if device supports water level (needed for vac&mop and mop-only modes) if (this.platform.roborockAPI.supportsWaterLevel(this.accessory.context)) { this.services['CleaningMode'] = this.accessory.getServiceById(this.platform.Service.Lightbulb, 'CleaningMode') || this.accessory.addService(this.platform.Service.Lightbulb, 'Cleaning Mode', 'CleaningMode'); this.services['CleaningMode'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setCleaningModeOn.bind(this)) .onGet(this.getCleaningModeOn.bind(this)); this.services['CleaningMode'].getCharacteristic(this.platform.Characteristic.Brightness) .onSet(this.setCleaningModeBrightness.bind(this)) .onGet(this.getCleaningModeBrightness.bind(this)) .setProps({ minValue: 0, maxValue: 100, minStep: 33, // 0-33% = Vacuum, 34-66% = Vac&mop, 67-100% = Mop only }); } // Mop Mode Control - Using Lightbulb service for mop route selection // Only add if device supports it if (this.platform.roborockAPI.supportsMopMode(this.accessory.context)) { this.services['MopMode'] = this.accessory.getServiceById(this.platform.Service.Lightbulb, 'MopMode') || this.accessory.addService(this.platform.Service.Lightbulb, 'Mop Route', 'MopMode'); this.services['MopMode'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setMopModeOn.bind(this)) .onGet(this.getMopModeOn.bind(this)); this.services['MopMode'].getCharacteristic(this.platform.Characteristic.Brightness) .onSet(this.setMopModeBrightness.bind(this)) .onGet(this.getMopModeBrightness.bind(this)) .setProps({ minValue: 0, maxValue: 100, minStep: 25, // 0%, 25%, 50%, 75%, 100% (maps to Standard, Deep, Deep+, Fast, etc.) }); } // Room Cleaning Switches - Will be created dynamically this.roomServices = new Map(); // LED Status Control if (this.platform.roborockAPI.supportsLedStatus(this.accessory.context)) { this.services['LedStatus'] = this.accessory.getServiceById(this.platform.Service.Switch, 'LedStatus') || this.accessory.addService(this.platform.Service.Switch, 'LED Status', 'LedStatus'); this.services['LedStatus'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setLedStatus.bind(this)) .onGet(this.getLedStatus.bind(this)); } // Sound Volume Control this.services['SoundVolume'] = this.accessory.getServiceById(this.platform.Service.Lightbulb, 'SoundVolume') || this.accessory.addService(this.platform.Service.Lightbulb, 'Sound Volume', 'SoundVolume'); this.services['SoundVolume'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setSoundVolumeOn.bind(this)) .onGet(this.getSoundVolumeOn.bind(this)); this.services['SoundVolume'].getCharacteristic(this.platform.Characteristic.Brightness) .onSet(this.setSoundVolumeBrightness.bind(this)) .onGet(this.getSoundVolumeBrightness.bind(this)) .setProps({ minValue: 0, maxValue: 100, minStep: 1, }); // Child Lock Control if (this.platform.roborockAPI.supportsChildLock(this.accessory.context)) { this.services['ChildLock'] = this.accessory.getServiceById(this.platform.Service.Switch, 'ChildLock') || this.accessory.addService(this.platform.Service.Switch, 'Child Lock', 'ChildLock'); this.services['ChildLock'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setChildLock.bind(this)) .onGet(this.getChildLock.bind(this)); } // Do Not Disturb (DND) Timer this.services['DoNotDisturb'] = this.accessory.getServiceById(this.platform.Service.Switch, 'DoNotDisturb') || this.accessory.addService(this.platform.Service.Switch, 'Do Not Disturb', 'DoNotDisturb'); this.services['DoNotDisturb'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setDoNotDisturb.bind(this)) .onGet(this.getDoNotDisturb.bind(this)); // Dust Collection Control (Auto-Empty Dock) if (this.platform.roborockAPI.supportsDustCollection(this.accessory.context)) { this.services['DustCollection'] = this.accessory.getServiceById(this.platform.Service.Switch, 'DustCollection') || this.accessory.addService(this.platform.Service.Switch, 'Dust Collection', 'DustCollection'); this.services['DustCollection'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setDustCollection.bind(this)) .onGet(this.getDustCollection.bind(this)); } // Carpet Mode Control if (this.platform.roborockAPI.supportsCarpetMode(this.accessory.context)) { this.services['CarpetMode'] = this.accessory.getServiceById(this.platform.Service.Switch, 'CarpetMode') || this.accessory.addService(this.platform.Service.Switch, 'Carpet Boost', 'CarpetMode'); this.services['CarpetMode'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setCarpetMode.bind(this)) .onGet(this.getCarpetMode.bind(this)); // Carpet Clean Mode (Avoidance) this.services['CarpetCleanMode'] = this.accessory.getServiceById(this.platform.Service.Lightbulb, 'CarpetCleanMode') || this.accessory.addService(this.platform.Service.Lightbulb, 'Carpet Mode', 'CarpetCleanMode'); this.services['CarpetCleanMode'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setCarpetCleanModeOn.bind(this)) .onGet(this.getCarpetCleanModeOn.bind(this)); this.services['CarpetCleanMode'].getCharacteristic(this.platform.Characteristic.Brightness) .onSet(this.setCarpetCleanModeBrightness.bind(this)) .onGet(this.getCarpetCleanModeBrightness.bind(this)) .setProps({ minValue: 0, maxValue: 100, minStep: 33, // 0-33% = Avoid, 34-66% = Rise, 67-100% = Ignore }); } // Flow LED Status if (this.platform.roborockAPI.supportsFlowLed(this.accessory.context)) { this.services['FlowLed'] = this.accessory.getServiceById(this.platform.Service.Switch, 'FlowLed') || this.accessory.addService(this.platform.Service.Switch, 'Flow LED', 'FlowLed'); this.services['FlowLed'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setFlowLed.bind(this)) .onGet(this.getFlowLed.bind(this)); } // Mop Washing (Wash Dock) if (this.platform.roborockAPI.supportsWashing(this.accessory.context)) { this.services['MopWash'] = this.accessory.getServiceById(this.platform.Service.Switch, 'MopWash') || this.accessory.addService(this.platform.Service.Switch, 'Mop Washing', 'MopWash'); this.services['MopWash'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setMopWash.bind(this)) .onGet(this.getMopWash.bind(this)); // Wash Towel Mode (Wash Intensity) this.services['WashTowelMode'] = this.accessory.getServiceById(this.platform.Service.Lightbulb, 'WashTowelMode') || this.accessory.addService(this.platform.Service.Lightbulb, 'Wash Intensity', 'WashTowelMode'); this.services['WashTowelMode'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setWashTowelModeOn.bind(this)) .onGet(this.getWashTowelModeOn.bind(this)); this.services['WashTowelMode'].getCharacteristic(this.platform.Characteristic.Brightness) .onSet(this.setWashTowelModeBrightness.bind(this)) .onGet(this.getWashTowelModeBrightness.bind(this)) .setProps({ minValue: 0, maxValue: 100, minStep: 33, // 0-33% = Eco, 34-66% = Medium, 67-100% = Intense }); } // Drying (Dry Dock) if (this.platform.roborockAPI.supportsDrying(this.accessory.context)) { this.services['Dryer'] = this.accessory.getServiceById(this.platform.Service.Switch, 'Dryer') || this.accessory.addService(this.platform.Service.Switch, 'Dryer', 'Dryer'); this.services['Dryer'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setDryer.bind(this)) .onGet(this.getDryer.bind(this)); // Dryer Duration (Dry Time) this.services['DryerDuration'] = this.accessory.getServiceById(this.platform.Service.Lightbulb, 'DryerDuration') || this.accessory.addService(this.platform.Service.Lightbulb, 'Dryer Duration', 'DryerDuration'); this.services['DryerDuration'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setDryerDurationOn.bind(this)) .onGet(this.getDryerDurationOn.bind(this)); this.services['DryerDuration'].getCharacteristic(this.platform.Characteristic.Brightness) .onSet(this.setDryerDurationBrightness.bind(this)) .onGet(this.getDryerDurationBrightness.bind(this)) .setProps({ minValue: 0, maxValue: 100, minStep: 33, // 0-33% = 2h, 34-66% = 3h, 67-100% = 4h }); } // Zone Cleaning (Basic - requires saved zones or coordinates) // Note: Zone cleaning requires coordinates, so this is a basic trigger // For full zone control, zones need to be saved/configured separately this.services['ZoneClean'] = this.accessory.getServiceById(this.platform.Service.Switch, 'ZoneClean') || this.accessory.addService(this.platform.Service.Switch, 'Zone Clean', 'ZoneClean'); this.services['ZoneClean'].getCharacteristic(this.platform.Characteristic.On) .onSet(this.setZoneClean.bind(this)) .onGet(this.getZoneClean.bind(this)); this.services['Battery'] = this.accessory.getService(this.platform.Service.Battery) || this.accessory.addService(this.platform.Service.Battery); this.services['Battery'].setCharacteristic(this.platform.Characteristic.BatteryLevel, this.platform.roborockAPI.getVacuumDeviceStatus(accessory.context, "battery") || 0); this.services['Battery'].setCharacteristic(this.platform.Characteristic.StatusLowBattery, this.platform.roborockAPI.getVacuumDeviceStatus(accessory.context, "battery") < 20 ? this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); this.services['Battery'].setCharacteristic(this.platform.Characteristic.ChargingState, this.platform.roborockAPI.getVacuumDeviceStatus(accessory.context, "charge_status") == 1 ? this.platform.Characteristic.ChargingState.CHARGING : this.platform.Characteristic.ChargingState.NOT_CHARGING); // Initialize scene switches this.updateSceneSwitches(); // Initialize room switches and all states this.updateRoomSwitches(); if (this.services['WaterLevel']) { this.updateWaterLevelState(); } if (this.services['MopMode']) { this.updateMopModeState(); } if (this.services['LedStatus']) { this.updateLedStatusState(); } this.updateSoundVolumeState(); if (this.services['ChildLock']) { this.updateChildLockState(); } this.updateDoNotDisturbState(); if (this.services['DustCollection']) { this.updateDustCollectionState(); } if (this.services['CarpetMode']) { this.updateCarpetModeState(); } if (this.services['CarpetCleanMode']) { this.updateCarpetCleanModeState(); } if (this.services['FlowLed']) { this.updateFlowLedState(); } if (this.services['MopWash']) { this.updateMopWashState(); } if (this.services['WashTowelMode']) { this.updateWashTowelModeState(); } if (this.services['Dryer']) { this.updateDryerState(); } if (this.services['DryerDuration']) { this.updateDryerDurationState(); } // Retry room switches after a delay (in case room mapping isn't ready yet) setTimeout(() => { this.updateRoomSwitches(); }, 5000); } updateDeviceState() { try { this.services['Fan'].updateCharacteristic(this.platform.Characteristic.Active, this.platform.roborockAPI.isCleaning(this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "state")) ? this.platform.Characteristic.Active.ACTIVE : this.platform.Characteristic.Active.INACTIVE); this.services['Battery'].updateCharacteristic(this.platform.Characteristic.BatteryLevel, this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "battery") || 0); this.services['Battery'].updateCharacteristic(this.platform.Characteristic.StatusLowBattery, this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "battery") < 20 ? this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); this.services['Battery'].updateCharacteristic(this.platform.Characteristic.ChargingState, this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "charge_status") != 0 ? this.platform.Characteristic.ChargingState.CHARGING : this.platform.Characteristic.ChargingState.NOT_CHARGING); // Update Fan Speed (RotationSpeed) const fanPower = this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "fan_power"); if (fanPower !== undefined) { const rotationSpeed = this.fanPowerToRotationSpeed(fanPower); this.services['Fan'].updateCharacteristic(this.platform.Characteristic.RotationSpeed, rotationSpeed); } // Update Target Fan State (Pause/Resume) const state = this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "state"); if (state === 10) { // Paused this.services['Fan'].updateCharacteristic(this.platform.Characteristic.TargetFanState, this.platform.Characteristic.TargetFanState.MANUAL); } else { this.services['Fan'].updateCharacteristic(this.platform.Characteristic.TargetFanState, this.platform.Characteristic.TargetFanState.AUTO); } // Update Error Status const errorCode = this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "error_code"); if (errorCode !== undefined) { const hasError = errorCode !== 0; this.services['ErrorStatus'].updateCharacteristic(this.platform.Characteristic.ContactSensorState, hasError ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED); } // Update Water Level (if supported) if (this.services['WaterLevel']) { this.updateWaterLevelState(); } // Update Mop Mode (if supported) if (this.services['MopMode']) { this.updateMopModeState(); } // Update LED Status (if supported) if (this.services['LedStatus']) { this.updateLedStatusState(); } // Update Sound Volume this.updateSoundVolumeState(); // Update Child Lock (if supported) if (this.services['ChildLock']) { this.updateChildLockState(); } // Update Do Not Disturb this.updateDoNotDisturbState(); // Update Dust Collection (if supported) if (this.services['DustCollection']) { this.updateDustCollectionState(); } // Update Carpet Mode (if supported) if (this.services['CarpetMode']) { this.updateCarpetModeState(); } if (this.services['CarpetCleanMode']) { this.updateCarpetCleanModeState(); } // Update Flow LED (if supported) if (this.services['FlowLed']) { this.updateFlowLedState(); } // Update Mop Washing (if supported) if (this.services['MopWash']) { this.updateMopWashState(); } if (this.services['WashTowelMode']) { this.updateWashTowelModeState(); } // Update Drying (if supported) if (this.services['Dryer']) { this.updateDryerState(); } if (this.services['DryerDuration']) { this.updateDryerDurationState(); } this.platform.log.debug("Device state is " + this.state_code_to_state(this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "state"))); } catch (e) { this.platform.log.error("Error updating device state: " + e); } } /** * Update scene switches based on available scenes for this device */ updateSceneSwitches() { try { // Get scenes for this device const deviceScenes = this.platform.roborockAPI.getScenesForDevice(this.accessory.context); // Check if scenes have changed if (this.scenesChanged(deviceScenes)) { this.platform.log.debug(`Updating scene switches for device ${this.accessory.context}`); // Remove existing scene switches that are no longer available // Add new scene switches for (const scene of deviceScenes) { try { const sceneId = scene.id.toString(); const sceneName = scene.name.replaceAll(" ", "_"); ; if (!this.sceneServices.has(sceneId) && scene.enabled) { this.platform.log.debug(`Adding scene switch for: ${scene.name} (ID: ${sceneId})`); const switchService = this.accessory.getServiceById(this.platform.Service.Switch, `scene-${sceneId}`) || this.accessory.addService(this.platform.Service.Switch, sceneName, `scene-${sceneId}`); switchService.setCharacteristic(this.platform.Characteristic.Name, sceneName); switchService.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName); switchService.setCharacteristic(this.platform.Characteristic.ConfiguredName, sceneName); switchService.getCharacteristic(this.platform.Characteristic.On) .onSet(this.setSceneSwitch.bind(this, sceneId)) .onGet(this.getSceneSwitch.bind(this, sceneId)); this.sceneServices.set(sceneId, switchService); } } catch (e) { this.platform.log.error(`Error processing scene ${scene.name}: ${e}`); } } //Remove scene switches that are no longer available this.accessory.services.forEach((service) => { if (service instanceof this.platform.Service.Switch && service.UUID.startsWith('scene-')) { const sceneId = service.UUID.replace('scene-', ''); // Check if the scene id in deviceScenes if (!deviceScenes.some(scene => scene.id.toString() === sceneId)) { this.platform.log.debug(`Removing scene switch for: ${service.displayName} (ID: ${sceneId})`); this.accessory.removeService(service); this.sceneServices.delete(sceneId); } } }); // Update current scenes this.currentScenes = deviceScenes; } } catch (error) { this.platform.log.error(`Error updating scene switches: ${error}`); } } /** * Check if scenes have changed */ scenesChanged(newScenes) { if (this.currentScenes.length !== newScenes.length) { return true; } const currentIds = this.currentScenes.map(scene => scene.id).sort(); const newIds = newScenes.map(scene => scene.id).sort(); return JSON.stringify(currentIds) !== JSON.stringify(newIds); } /** * Handle scene switch activation */ async setSceneSwitch(sceneId, value) { try { this.platform.log.debug(`Scene switch ${sceneId} set to: ${value}`); if (value) { // Execute the scene await this.platform.roborockAPI.executeScene({ val: sceneId }); this.platform.log.info(`Executed scene ID: ${sceneId}`); // Turn off the switch after execution (momentary switch behavior) setTimeout(() => { const service = this.sceneServices.get(sceneId); if (service) { service.updateCharacteristic(this.platform.Characteristic.On, false); } }, 1000); } } catch (error) { this.platform.log.error(`Error executing scene ${sceneId}: ${error}`); // Turn off the switch if there was an error const service = this.sceneServices.get(sceneId); if (service) { service.updateCharacteristic(this.platform.Characteristic.On, false); } } } /** * Get scene switch state (always returns false for momentary behavior) */ async getSceneSwitch(sceneId) { return false; // Momentary switch - always return false } notifyDeviceUpdater(id, data) { try { if (id == 'CloudMessage' || id == 'LocalMessage') { this.platform.log.debug(`Updating accessory with ${id} data: ` + JSON.stringify(data)); if (data.length > 0) { const messages = data[0]; if (messages.hasOwnProperty('state')) { this.services['Fan'].updateCharacteristic(this.platform.Characteristic.Active, this.isCleaningState(messages.state) ? this.platform.Characteristic.Active.ACTIVE : this.platform.Characteristic.Active.INACTIVE); } if (messages.hasOwnProperty('battery')) { this.services['Battery'].updateCharacteristic(this.platform.Characteristic.BatteryLevel, messages.battery); this.services['Battery'].updateCharacteristic(this.platform.Characteristic.StatusLowBattery, messages.battery < 20 ? this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); } if (messages.hasOwnProperty('charge_status')) { this.services['Battery'].updateCharacteristic(this.platform.Characteristic.ChargingState, messages.charge_status != 0 ? this.platform.Characteristic.ChargingState.CHARGING : this.platform.Characteristic.ChargingState.NOT_CHARGING); } if (messages.hasOwnProperty('in_cleaning')) { this.services['Fan'].updateCharacteristic(this.platform.Characteristic.Active, messages.in_cleaning != 0 ? this.platform.Characteristic.Active.ACTIVE : this.platform.Characteristic.Active.INACTIVE); } if (messages.hasOwnProperty('fan_power')) { const rotationSpeed = this.fanPowerToRotationSpeed(messages.fan_power); this.services['Fan'].updateCharacteristic(this.platform.Characteristic.RotationSpeed, rotationSpeed); } if (messages.hasOwnProperty('state')) { // Update Target Fan State based on pause state const isPaused = messages.state === 10; this.services['Fan'].updateCharacteristic(this.platform.Characteristic.TargetFanState, isPaused ? this.platform.Characteristic.TargetFanState.MANUAL : this.platform.Characteristic.TargetFanState.AUTO); } if (messages.hasOwnProperty('error_code')) { const hasError = messages.error_code !== 0; this.services['ErrorStatus'].updateCharacteristic(this.platform.Characteristic.ContactSensorState, hasError ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED); } if (messages.hasOwnProperty('water_box_custom_mode') && this.services['WaterLevel']) { this.updateWaterLevelState(); } if (messages.hasOwnProperty('mop_mode') && this.services['MopMode']) { this.updateMopModeState(); } if (messages.hasOwnProperty('led_status') && this.services['LedStatus']) { this.updateLedStatusState(); } if (messages.hasOwnProperty('sound_volume')) { this.updateSoundVolumeState(); } if (messages.hasOwnProperty('child_lock_status') && this.services['ChildLock']) { this.updateChildLockState(); } if (messages.hasOwnProperty('dnd_timer')) { this.updateDoNotDisturbState(); } if ((messages.hasOwnProperty('dust_collection_switch_status') || messages.hasOwnProperty('dust_collection_mode')) && this.services['DustCollection']) { this.updateDustCollectionState(); } if ((messages.hasOwnProperty('carpet_mode') || messages.hasOwnProperty('carpet_clean_mode')) && this.services['CarpetMode']) { this.updateCarpetModeState(); if (this.services['CarpetCleanMode']) { this.updateCarpetCleanModeState(); } } if (messages.hasOwnProperty('flow_led_status') && this.services['FlowLed']) { this.updateFlowLedState(); } if ((messages.hasOwnProperty('wash_status') || messages.hasOwnProperty('wash_towel_mode')) && this.services['MopWash']) { this.updateMopWashState(); if (this.services['WashTowelMode']) { this.updateWashTowelModeState(); } } if ((messages.hasOwnProperty('dry_status') || messages.hasOwnProperty('dryer_setting')) && this.services['Dryer']) { this.updateDryerState(); if (this.services['DryerDuration']) { this.updateDryerDurationState(); } } if ((messages.hasOwnProperty('fan_power') || messages.hasOwnProperty('water_box_custom_mode')) && this.services['CleaningMode']) { // Update cleaning mode based on fan and water settings const fanPower = messages.fan_power !== undefined ? messages.fan_power : this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "fan_power"); const waterLevel = messages.water_box_custom_mode !== undefined ? messages.water_box_custom_mode : this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "water_box_custom_mode"); const isFanOn = fanPower !== undefined && fanPower !== 105; const isWaterOn = waterLevel !== undefined && waterLevel !== 200; let brightness; if (!isFanOn && isWaterOn) { brightness = 100; // Mop only } else if (isFanOn && isWaterOn) { brightness = 50; // Vac&mop } else { brightness = 0; // Vacuum only } this.services['CleaningMode'].updateCharacteristic(this.platform.Characteristic.Brightness, brightness); } } if (data.hasOwnProperty('dps') && data.dps.hasOwnProperty('121')) { this.platform.log.debug(`${this.platform.roborockAPI.getVacuumDeviceInfo(this.accessory.context, "name")} state update to: ${this.state_code_to_state(data.dps['121'])}`); this.services['Fan'].updateCharacteristic(this.platform.Characteristic.Active, this.isCleaningState(data.dps['121']) ? this.platform.Characteristic.Active.ACTIVE : this.platform.Characteristic.Active.INACTIVE); } if (data.hasOwnProperty('dps') && data.dps.hasOwnProperty('122')) { this.platform.log.debug(`${this.platform.roborockAPI.getVacuumDeviceInfo(this.accessory.context, "name")} battery update to: ${data.dps['122']}`); this.services['Battery'].updateCharacteristic(this.platform.Characteristic.BatteryLevel, data.dps['122']); this.services['Battery'].updateCharacteristic(this.platform.Characteristic.StatusLowBattery, data.dps['122'] < 20 ? this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); } } else if (id == 'HomeData') { this.updateDeviceState(); // Update scene switches when home data changes this.updateSceneSwitches(); // Update room switches when home data changes this.updateRoomSwitches(); } } catch (e) { this.platform.log.error("Error notifying device updater: " + e); } } async setActive(value) { try { this.platform.log.debug("Setting active to " + value); if (value == this.platform.Characteristic.Active.ACTIVE) { await this.platform.roborockAPI.app_start(this.accessory.context); } else { await this.platform.roborockAPI.app_stop(this.accessory.context); await this.platform.roborockAPI.app_charge(this.accessory.context); } this.services['Fan'].updateCharacteristic(this.platform.Characteristic.Active, value); } catch (e) { this.platform.log.error("Error setting active: " + e); } } async getActive() { this.updateDeviceState(); return this.isCleaning() ? this.platform.Characteristic.Active.ACTIVE : this.platform.Characteristic.Active.INACTIVE; } state_code_to_state(code) { const RoborockStateCodes = { 0: "Unknown", 1: "Initiating", 2: "Sleeping", 3: "Idle", 4: "Remote Control", 5: "Cleaning", 6: "Returning Dock", 7: "Manual Mode", 8: "Charging", 9: "Charging Error", 10: "Paused", 11: "Spot Cleaning", 12: "In Error", 13: "Shutting Down", 14: "Updating", 15: "Docking", 16: "Go To", 17: "Zone Clean", 18: "Room Clean", 22: "Empying dust container", 23: "Washing the mop", 26: "Going to wash the mop", 28: "In call", 29: "Mapping", 100: "Fully Charged", }; return RoborockStateCodes[code] || "Unknown"; } isCleaning() { return this.isCleaningState(this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "state")); } isCleaningState(state) { switch (state) { case 4: // Remote Control case 5: // Cleaning case 6: // Returning Dock case 7: // Manual Mode case 11: // Spot Cleaning case 15: // Docking case 16: // Go To case 17: // Zone Clean case 18: // Room Clean case 23: // Washing the mop case 26: // Going to wash the mop return true; default: return false; } } // Fan Speed Control (Suction Power) async setRotationSpeed(value) { try { this.platform.log.debug(`Setting rotation speed to ${value}%`); // Map percentage to fan power levels // 0% = Off (105), 25% = Quiet (101), 50% = Balanced (102), 75% = Turbo (103), 100% = Max (104) const numValue = Number(value); let fanPower; if (numValue === 0) { fanPower = 105; // Off } else if (numValue <= 25) { fanPower = 101; // Quiet } else if (numValue <= 50) { fanPower = 102; // Balanced } else if (numValue <= 75) { fanPower = 103; // Turbo } else { fanPower = 104; // Max } await this.platform.roborockAPI.set_custom_mode(this.accessory.context, fanPower); this.services['Fan'].updateCharacteristic(this.platform.Characteristic.RotationSpeed, value); } catch (e) { this.platform.log.error(`Error setting rotation speed: ${e}`); } } async getRotationSpeed() { const fanPower = this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "fan_power"); return this.fanPowerToRotationSpeed(fanPower); } fanPowerToRotationSpeed(fanPower) { // Map fan power to percentage switch (fanPower) { case 105: return 0; // Off case 101: return 25; // Quiet case 102: return 50; // Balanced case 103: return 75; // Turbo case 104: return 100; // Max case 108: return 100; // Max+ (some models) default: return 50; // Default to Balanced } } // Pause/Resume Control async setTargetFanState(value) { try { this.platform.log.debug(`Setting target fan state to ${value}`); const state = this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "state"); const isPaused = state === 10; if (value === this.platform.Characteristic.TargetFanState.MANUAL && !isPaused) { // Pause await this.platform.roborockAPI.app_pause(this.accessory.context); } else if (value === this.platform.Characteristic.TargetFanState.AUTO && isPaused) { // Resume (start cleaning) await this.platform.roborockAPI.app_start(this.accessory.context); } this.services['Fan'].updateCharacteristic(this.platform.Characteristic.TargetFanState, value); } catch (e) { this.platform.log.error(`Error setting target fan state: ${e}`); } } async getTargetFanState() { const state = this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "state"); return state === 10 ? this.platform.Characteristic.TargetFanState.MANUAL : this.platform.Characteristic.TargetFanState.AUTO; } // Return to Dock async setReturnToDock(value) { try { if (value) { this.platform.log.debug("Returning to dock"); await this.platform.roborockAPI.app_charge(this.accessory.context); // Turn off switch after a delay (momentary behavior) setTimeout(() => { this.services['ReturnToDock'].updateCharacteristic(this.platform.Characteristic.On, false); }, 1000); } } catch (e) { this.platform.log.error(`Error returning to dock: ${e}`); this.services['ReturnToDock'].updateCharacteristic(this.platform.Characteristic.On, false); } } async getReturnToDock() { return false; // Always return false for momentary switch } // Spot Cleaning async setSpotClean(value) { try { if (value) { this.platform.log.debug("Starting spot cleaning"); await this.platform.roborockAPI.app_spot(this.accessory.context); // Turn off switch after a delay (momentary behavior) setTimeout(() => { this.services['SpotClean'].updateCharacteristic(this.platform.Characteristic.On, false); }, 1000); } } catch (e) { this.platform.log.error(`Error starting spot cleaning: ${e}`); this.services['SpotClean'].updateCharacteristic(this.platform.Characteristic.On, false); } } async getSpotClean() { return false; // Always return false for momentary switch } // Find Me async setFindMe(value) { try { if (value) { this.platform.log.debug("Finding robot"); await this.platform.roborockAPI.find_me(this.accessory.context); // Turn off switch after a delay (momentary behavior) setTimeout(() => { this.services['FindMe'].updateCharacteristic(this.platform.Characteristic.On, false); }, 1000); } } catch (e) { this.platform.log.error(`Error finding robot: ${e}`); this.services['FindMe'].updateCharacteristic(this.platform.Characteristic.On, false); } } async getFindMe() { return false; // Always return false for momentary switch } // Error Status async getErrorStatus() { const errorCode = this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "error_code"); const hasError = errorCode !== undefined && errorCode !== 0; return hasError ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED; } // Water Level Control (Scrub Intensity) async setWaterLevelOn(value) { // When turned on, set to default (Standard/Moderate) if (value) { await this.setWaterLevelBrightness(67); // Standard } else { await this.setWaterLevelBrightness(0); // Off } } async getWaterLevelOn() { const waterLevel = this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "water_box_custom_mode"); return waterLevel !== undefined && waterLevel !== 200; // On if not Off (200) } async setWaterLevelBrightness(value) { try { this.platform.log.debug(`Setting water level (scrub intensity) to ${value}%`); // Check if device uses MILD/STANDARD/INTENSE (shake mop) or LOW/MEDIUM/HIGH const usesShakeMop = this.platform.roborockAPI.usesShakeMopWaterModes(this.accessory.context); // Map percentage to water level values // For shake mop devices: 0% = Off (200), 1-33% = Mild (201), 34-66% = Standard (202), 67-100% = Intense (203) // For other devices: 0% = Off (200), 1-33% = Low (201), 34-66% = Medium (202), 67-100% = High (203) const numValue = Number(value); let waterLevel; if (numValue === 0) { waterLevel = 200; // Off } else if (numValue <= 33) { waterLevel = 201; // Mild/Low } else if (numValue <= 66) { waterLevel = 202; // Standard/Medium } else { waterLevel = 203; // Intense/High } await this.platform.roborockAPI.set_water_box_custom_mode(this.accessory.context, waterLevel); if (this.services['WaterLevel']) { this.services['WaterLevel'].updateCharacteristic(this.platform.Characteristic.Brightness, value); this.services['WaterLevel'].updateCharacteristic(this.platform.Characteristic.On, waterLevel !== 200); } } catch (e) { this.platform.log.error(`Error setting water level: ${e}`); } } async getWaterLevelBrightness() { const waterLevel = this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "water_box_custom_mode"); if (waterLevel === undefined || waterLevel === "") { return 67; // Default to Standard } return this.waterLevelToBrightness(waterLevel); } waterLevelToBrightness(waterLevel) { // Map water level to percentage switch (waterLevel) { case 200: return 0; // Off case 201: return 33; // Mild/Low case 202: return 67; // Standard/Medium case 203: return 100; // Intense/High default: return 67; // Default to Standard } } updateWaterLevelState() { if (!this.services['WaterLevel']) { return; } const waterLevel = this.platform.roborockAPI.getVacuumDeviceStatus(this.accessory.context, "water_box_custom_mode"); if (waterLevel !== undefined && waterLevel !== "") { const brightness = this.waterLevelToBrightness(waterLevel); this.services['WaterLevel'].updateCharacteristic(this.platform.Characteristic.Brightness, brightness); this.services['WaterLevel'].updateCharacteristic(this.platform.Characteristic.On, waterLevel !== 200); } } // Cleaning Mode Selector async setCleaningModeOn(value) { // When turned on, set to default (Vac&mop) if (value) { await this.setCleaningModeBrightness(50); // Vac&mop } } async getCleaningModeOn() { // Always return true if a mode is set return true; } async setCleaningModeBrightness(value) { try { this.platform.log.debug(`Setting cleaning mode to ${value}%`); // Map percentage to cleaning mode // 0-33% = Vacuum only, 34-66% = Vac&mop, 67-100% = Mop only const numValue = Number(value); let fanPower; let waterLevel; let mode; if (numValue <= 33) { // Vacuum only: Fan on, Water off fanPower = 102; // Balanced (default) waterLevel = 200; // Off mode = 'vacuum'; } else if (numValue <= 66) { // Vac&mop: Fan on, Water on fanPower = 102; // Balanced (default) waterLevel = 202; // Standard (default) mode = 'vac&mop'; } else { // Mop only: Fan off, Water on