UNPKG

homebridge-roborock-vacuum-update

Version:

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

1,122 lines (1,051 loc) 37.4 kB
"use strict"; const errorCodes = { 0: "No error", 1: "Laser sensor fault", 2: "Collision sensor fault", 3: "Wheel floating", 4: "Cliff sensor fault", 5: "Main brush blocked", 6: "Side brush blocked", 7: "Wheel blocked", 8: "Device stuck", 9: "Dust bin missing", 10: "Filter blocked", 11: "Magnetic field detected", 12: "Low battery", 13: "Charging problem", 14: "Battery failure", 15: "Wall sensor fault", 16: "Uneven surface", 17: "Side brush failure", 18: "Suction fan failure", 19: "Unpowered charging station", 20: "Unknown Error", 21: "Laser pressure sensor problem", 22: "Charge sensor problem", 23: "Dock problem", 24: "No-go zone or invisible wall detected", 254: "Bin full", 255: "Internal error", "-1": "Unknown Error", }; const stateCodes = { 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", }; const dockTypes = { 0: "Charging dock", 1: "Auto-Empty Dock", 2: "Empty Wash Fill Dock", 3: "Empty Wash Fill (Dry) Dock", 5: "Auto-Empty Dock (Q8 Max+)", 7: "Empty Wash Fill Dry Dock (S8 Pro Ultra)", 8: "Empty Wash Fill Dry Dock (Q Revo)", 9: "Empty Wash Fill Dry Dock (Q Revo Pro)", }; const firmwareFeatures = { 111: "isSupportFDSEndPoint", 112: "isSupportAutoSplitSegments", 114: "isSupportOrderSegmentClean", 116: "isMapSegmentSupported", 119: "isSupportLedStatusSwitch", 120: "isMultiFloorSupported", 122: "isSupportFetchTimerSummary", 123: "isOrderCleanSupported", 125: "isRemoteSupported", }; const commands = { app_start: { type: "boolean", defaultState: false }, app_segment_clean: { type: "boolean", defaultState: false }, app_stop: { type: "boolean", defaultState: false }, app_pause: { type: "boolean", defaultState: false }, app_charge: { type: "boolean", defaultState: false }, app_spot: { type: "boolean", defaultState: false }, app_zoned_clean: "json", resume_zoned_clean: { type: "boolean", defaultState: false }, stop_zoned_clean: { type: "boolean", defaultState: false }, resume_segment_clean: { type: "boolean", defaultState: false }, stop_segment_clean: { type: "boolean", defaultState: false }, set_custom_mode: { type: "number", defaultState: 102, states: { 101: "Quiet", 102: "Balanced", 103: "Turbo", 104: "Max", 105: "Off" } }, find_me: { type: "boolean", defaultState: false }, app_goto_target: "json", }; const deviceStates = { unsave_map_flag: "number", unsave_map_reason: "number", dock_error_status: "number", debug_mode: "number", auto_dust_collection: "number", dust_collection_status: "number", dock_type: { type: "number", states: dockTypes }, adbumper_status: "string", lock_status: "number", is_locating: "number", map_status: "number", dnd_enabled: "number", lab_status: "number", in_fresh_state: "number", in_returning: "number", in_cleaning: "number", map_present: "number", error_code: { type: "number", states: errorCodes }, clean_area: { type: "number", unit: "m²", divider: 1000000 }, clean_time: { type: "number", unit: "min", divider: 60 }, battery: { type: "number", unit: "%" }, state: { type: "number", states: stateCodes }, msg_seq: "number", msg_ver: "number", fan_power: { type: "number", name: "Fan power", states: { 101: "Quiet", 102: "Balanced", 103: "Turbo", 104: "Max", 105: "Off" } }, is_exploring: "number", events: "string", subdivision_sets: "number", repeat: "number", replenish_mode: "number", rdt: "number", camera_status: "number", distance_off: "number", }; const consumablesInt = { main_brush_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false }, side_brush_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false }, filter_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false }, filter_element_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false }, sensor_dirty_time: { type: "number", unit: "h", divider: 60 * 60, write: false }, dust_collection_work_times: { type: "number", unit: "h", divider: 60 * 60, write: false }, 125: { type: "number", unit: "%", divider: 60 * 60, write: false }, 126: { type: "number", unit: "%", divider: 60 * 60, write: false }, 127: { type: "number", unit: "%", divider: 60 * 60, write: false }, }; const consumablesString = { main_brush_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false }, side_brush_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false }, filter_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false }, filter_element_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false }, sensor_dirty_time: { type: "number", unit: "h", divider: 60 * 60, write: false }, dust_collection_work_times: { type: "number", unit: "h", divider: 60 * 60, write: false }, main_brush_life: { type: "number", unit: "%", divider: 60 * 60, write: false }, side_brush_life: { type: "number", unit: "%", divider: 60 * 60, write: false }, filter_life: { type: "number", unit: "%", divider: 60 * 60, write: false }, }; const resetConsumables = [ "main_brush_work_time", "side_brush_work_time", "filter_work_time", "filter_element_work_time", "sensor_dirty_time", "dust_collection_work_times", ]; const cleaningRecordsInt = { 0: { type: "string", name: "cleaningRecord_begin" }, 1: { type: "string", name: "cleaningRecord_end" }, 2: { type: "number", name: "cleaningRecord_duration", unit: "min" }, 3: { type: "number", name: "cleaningRecord_area", unit: "m²" }, 4: { type: "number", name: "cleaningRecord_error" }, 5: { type: "number", name: "cleaningRecord_complete" }, 6: { type: "number", name: "cleaningRecord_start_type" }, 7: { type: "number", name: "cleaningRecord_finish_reason" }, 8: { type: "number", name: "cleaningRecord_dust_collection_status" }, }; const cleaningRecordsString = { begin: { type: "string", name: "cleaningRecord_begin" }, end: { type: "string", name: "cleaningRecord_end" }, duration: { type: "number", name: "cleaningRecord_duration", unit: "min" }, area: { type: "number", name: "cleaningRecord_area", unit: "m²" }, error: { type: "number", name: "cleaningRecord_error" }, complete: { type: "number", name: "cleaningRecord_complete" }, start_type: { type: "number", name: "cleaningRecord_start_type" }, clean_type: { type: "number", name: "cleaningRecord_clean_type" }, finish_reason: { type: "number", name: "cleaningRecord_finish_reason" }, dust_collection_status: { type: "number", name: "cleaningRecord_dust_collection_status" }, manual_replenish: { type: "number", name: "cleaningRecord_manual_replenish" }, dirty_replenish: { type: "number", name: "cleaningRecord_dirty_replenish" }, clean_times: { type: "number", name: "cleaningRecord_clean_times" }, }; const cleaningInfoInt = { 0: { name: "cleaningInfo_clean_time", type: "number", unit: "h" }, 1: { name: "cleaningInfo_clean_area", type: "number", unit: "m²" }, 2: { name: "cleaningInfo_clean_count", type: "number" }, }; const cleaningInfoString = { clean_time: { name: "cleaningInfo_clean_time", type: "number", unit: "h" }, clean_area: { name: "cleaningInfo_clean_area", type: "number", unit: "m²" }, clean_count: { name: "cleaningInfo_clean_count", type: "number" }, dust_collection_count: { name: "cleaningInfo_dust_collection_count", type: "number" }, }; const actions = { setConsumablesInt: async (context) => { context.consumables = consumablesInt; }, setConsumablesString: async (context) => { context.consumables = consumablesString; }, setCleaningRecordsInt: async (context) => { context.cleaningRecords = cleaningRecordsInt; context.cleaningInfo = cleaningInfoInt; }, setCleaningRecordsString: async (context) => { context.cleaningRecords = cleaningRecordsString; context.cleaningInfo = cleaningInfoString; }, setCleaningRecordsMixed: async (context) => { // this is a special case some olders robots context.cleaningRecords = cleaningRecordsString; context.cleaningInfo = cleaningInfoInt; }, set_charge_status: async () => { deviceStates.charge_status = "number"; }, set_clean_percent: async () => { deviceStates.clean_percent = "number"; }, set_common_status: () => { deviceStates.common_status = "number"; }, set_rss: () => { deviceStates.rss = "number"; }, set_dss: () => { deviceStates.dss = "number"; }, set_kct: () => { deviceStates.kct = "number"; }, set_in_warmup: () => { deviceStates.in_warmup = "number"; }, set_last_clean_t: () => { deviceStates.last_clean_t = "string"; }, set_wash_count: () => { cleaningRecordsString.wash_count = { type: "number", name: "cleaning_record_wash_count" }; }, set_map_flag: (context) => { deviceStates.map_flag = "number"; context.cleaningRecords.map_flag = { type: "number", name: "cleaningRecord_map_flag" }; }, set_back_type: () => { deviceStates.back_type = "number"; }, set_rdt: () => { deviceStates.rdt = "number"; }, set_replenish_mode: () => { cleaningRecordsString.manual_replenish = { type: "number", name: "cleaning_record_manual_replenish" }; cleaningRecordsString.dirty_replenish = { type: "number", name: "cleaning_record_dirty_replenish" }; deviceStates.replenish_mode = "number"; }, set_repeat: () => { deviceStates.repeat = "number"; }, set_cleaned_area: () => { cleaningRecordsString.cleaned_area = { type: "number", name: "cleaning_record_cleaned_area", unit: "m²", divider: 1000000 }; }, set_clean_times: () => { cleaningRecordsString.clean_times = { type: "number", name: "cleaning_record_clean_times" }; }, set_switch_status: () => { deviceStates.switch_status = "number"; }, set_task_id: () => { cleaningRecordsString.task_id = { type: "number", name: "cleaning_record_task_id" }; }, set_monitor_status: () => { deviceStates.monitor_status = "number"; }, set_clean_fluid: () => { deviceStates.clean_fluid = "number"; } }; class deviceFeatures { constructor(adapter, features, featuresStr, duid) { this.adapter = adapter; this.features = features; this.featuresStr = featuresStr; this.duid = duid; this.cleaningInfo = {}; this.cleaningRecords = {}; this.consumables = {}; } isWashThenChargeCmdSupported() { commands.app_start_wash = { type: "boolean", defaultState: false }; commands.app_stop_wash = { type: "boolean", defaultState: false }; this.cleaningRecords.wash_count = { type: "number", name: "cleaning_record_wash_count" }; commands.set_wash_towel_mode = { type: "json", defaultState: '{"wash_mode":2}', states: { '{"wash_mode":0}': "Eco", '{"wash_mode":1}': "Medium", '{"wash_mode":2}': "Intense", }, }; commands.set_smart_wash_params = { type: "json", defaultState: '{"smart_wash":0,"wash_interval":1800}', states: { '{"smart_wash":0,"wash_interval":600}': "10 Min", '{"smart_wash":0,"wash_interval":900}': "15 Min", '{"smart_wash":0,"wash_interval":1200}': "20 Min", '{"smart_wash":0,"wash_interval":1500}': "25 Min", '{"smart_wash":0,"wash_interval":1800}': "30 Min", '{"smart_wash":0,"wash_interval":2100}': "35 Min", '{"smart_wash":0,"wash_interval":2400}': "40 Min", '{"smart_wash":0,"wash_interval":2700}': "45 Min", '{"smart_wash":0,"wash_interval":3000}': "50 Min", '{"smart_wash":1,"wash_interval":1200}': "Per room", }, }; deviceStates.wash_phase = "number"; deviceStates.wash_ready = "number"; deviceStates.back_type = "number"; deviceStates.wash_status = "number"; consumablesString.strainer_work_times = { type: "number", unit: null, divider: null }; consumablesString.cleaning_brush_work_times = { type: "number", unit: null, divider: null }; resetConsumables.push("strainer_work_times"); resetConsumables.push("cleaning_brush_work_times"); } isDustCollectionSettingSupported() { commands.app_start_collect_dust = { type: "boolean", defaultState: false }; commands.app_stop_collect_dust = { type: "boolean", defaultState: false }; commands.set_dust_collection_switch_status = { type: "json", defaultState: '{"status":1}', states: { '{"status":0}': "Off", '{"status":1}': "On" } }; commands.set_dust_collection_mode = { type: "json", defaultState: '{"mode":0}', states: { '{"mode":0}': "Smart", '{"mode":1}': "Low", '{"mode":2}': "Medium", '{"mode":4}': "Max", }, }; } isSupportedDrying() { commands.app_set_dryer_status = { type: "string", defaultState: '{"status": 0}', states: { '{"status": 1}': "On", '{"status": 0}': "Off" } }; commands.app_set_dryer_setting = { type: "json", defaultState: '{"on":{"dry_time":10800},"status":0}', states: { '{"on":{"dry_time":10800},"status":0}': "Off", '{"on":{"dry_time":7200},"status":1}': "2h", '{"on":{"dry_time":10800},"status":1}': "3h", '{"on":{"dry_time":14400},"status":1}': "4h", }, }; deviceStates.dry_status = "number"; } isSupportedWaterMode() { commands.set_mop_mode = { type: "number", defaultState: 300, states: { 300: "Standard", 301: "Deep", 303: "Deep+" } }; commands.set_water_box_custom_mode = { type: "number", defaultState: 201, states: { 200: "Off", 201: "Mild", 202: "Moderate", 203: "Intense" } }; deviceStates.water_box_custom_mode = { type: "number", states: { 200: "Off", 201: "Mild", 202: "Moderate", 203: "Intense" } }; } isShakeMopSetSupported() { this.isSupportedWaterMode(); } isElectronicWaterBoxSupported() { this.isSupportedWaterMode(); } isCarpetSupported() { // nothing for now } isCleanRouteFastModeSupported() { commands.set_mop_mode = { type: "number", defaultState: 300, states: { 300: "Standard", 301: "Deep", 303: "Deep+", 304: "Fast" } }; deviceStates.mop_mode = { type: "number", states: { 300: "Standard", 301: "Deep", 303: "Deep+", 304: "Fast" } }; } isAvoidCollisionSupported() { deviceStates.collision_avoid_status = "number"; deviceStates.avoid_count = "number"; this.cleaningRecords.avoid_count = { type: "number", name: "cleaning_record_avoid_count" }; } isCornerCleanModeSupported() { deviceStates.corner_clean_mode = "number"; } isCameraSupported() { deviceStates.distance_off = "number"; deviceStates.camera_status = "number"; } isVideoLiveCallSupported() { deviceStates.home_sec_enable_password = "number"; deviceStates.home_sec_status = "number"; const ip = this.adapter.config.hostname_ip; const streamTypes = { stream_html: `http://${ip}:1984/stream.html?src=${this.duid}`, webrtc_html: `http://${ip}:1984/webrtc.html?src=${this.duid}&media=video`, stream_mp4: `http://${ip}:1984/api/stream.mp4?src=${this.duid}`, rtsp: `rtsp://${ip}:8554/${this.duid}?video`, }; for (const [name, stream_uri] of Object.entries(streamTypes)) { this.adapter.setObjectAsync(`Devices.${this.duid}.camera.${name}`, { type: "state", common: { name: name, type: "string", role: "value", read: true, write: false, def: stream_uri, }, native: {}, }); } } isVoiceControlSupported() { deviceStates.voice_chat_status = "number"; } isSupportSetSwitchMapMode() { deviceStates.switch_map_mode = "number"; } isMopForbiddenSupported() { deviceStates.mop_forbidden_enable = "number"; } isShakeMopStrengthSupported() { this.isSupportedWaterMode(); } isWaterBoxSupported() { deviceStates.water_box_carriage_status = "number"; deviceStates.water_box_mode = { type: "number", states: { 200: "Off", 201: "Mild", 202: "Moderate", 203: "Intense" } }; deviceStates.water_box_status = "number"; deviceStates.water_shortage_status = "number"; deviceStates.mop_mode = { type: "number", states: { 300: "Standard", 301: "Deep", 303: "Deep+" } }; } isCustomWaterBoxDistanceSupported() { // in this special case, create the command directly instead of the usual way this.adapter.setObjectAsync(`Devices.${this.duid}.commands.set_water_box_distance_off`, { type: "state", common: { name: this.adapter.translations.set_water_box_distance_off, type: "number", role: "value", read: true, write: true, def: 1, min: 1, max: 30, }, native: {}, }); } isBackChargeAutoWashSupported() { // this means the robot can stay reversed into the dock and still charge // nothing for now } isAvoidCarpetSupported() { deviceStates.carpet_mode = { type: "string", states: { '[{"enable":0,"stall_time":10,"current_low":400,"current_high":500,"current_integral":450}]': "off", '[{"enable":1,"stall_time":10,"current_low":400,"current_high":500,"current_integral":450}]': "on", }, }; deviceStates.carpet_clean_mode = { type: "string", states: { '{"carpet_clean_mode":0}': "Avoid", '{"carpet_clean_mode":1}': "Rise", '{"carpet_clean_mode":2}': "Ignore", }, }; } // the functions below are used for processing the firmware features from get_fw_features isSupportFDSEndPoint() { // nothing for now } isSupportAutoSplitSegments() { // nothing for now } isSupportOrderSegmentClean() { // nothing for now } isMapSegmentSupported() { // nothing for now } isSupportLedStatusSwitch() { // nothing for now } isMultiFloorSupported() { const features = ["max_multi_map", "max_bak_map", "multi_map_count"]; for (const [, feature] of Object.entries(features)) { this.adapter.setObjectAsync(`Devices.${this.duid}.floors.${feature}`, { type: "state", common: { name: feature, type: "number", role: "value", read: true, write: false, }, native: {}, }); } } isSupportFetchTimerSummary() { // nothing for now } isOrderCleanSupported() { // nothing for now } isRemoteSupported() { // nothing for now } getFeatureList() { const robotModel = this.adapter.getProductAttribute(this.duid, "model"); return { isWashThenChargeCmdSupported: ((this.features / Math.pow(2, 32)) >> 5) & 1, isDustCollectionSettingSupported: !!(33554432 & this.features), isSupportedDrying: ((this.features / Math.pow(2, 32)) >> 15) & 1, isShakeMopSetSupported: !!(262144 & this.features), isVideoMonitorSupported: !!(8 & this.features), // I tested this for S7 MaxV, S8 MaxV isVideoSettingSupported: !!(64 & this.features), // I tested this for S7 MaxV, S8 MaxV isCarpetSupported: !!(512 & this.features), isPhotoUploadSupported: !!(65536 & this.features), isAvoidCollisionSupported: !!(134217728 & this.features), isCornerCleanModeSupported: !!(2147483648 & this.features), // isCameraSupported: [p.Products.TanosV_CN, p.Products.TanosV_CE, p.Products.TopazSV_CN, p.Products.TopazSV_CE, p.Products.TanosSV].hasElement(p.DMM.currentProduct), isCameraSupported: !!["roborock.vacuum.a10", "roborock.vacuum.a27", "roborock.vacuum.a51", "roborock.vacuum.a87"].includes(robotModel), isSupportSetSwitchMapMode: !!(268435456 & this.features), // isMopForbiddenSupported: !!(p.DMM.isTanosV || p.DMM.isTanos || p.DMM.isTopazSV || p.DMM.isPearlPlus) || !![p.Products.TanosE, p.Products.TanosSL, p.Products.TanosS, p.Products.TanosSPlus, p.Products.TanosSMax, p.Products.Ultron, p.Products.UltronLite, p.Products.Pearl, p.Products.RubysLite].hasElement(p.DMM.currentProduct), isMopForbiddenSupported: [ "roborock.vacuum.s6", // S6 "roborock.vacuum.a10", // S6 MaxV "roborock.vacuum.a40", // Q7 "roborock.vacuum.s5e", // S5 Max "roborock.vacuum.a38", // Q7 Max "roborock.vacuum.a72", // Q5 Pro "roborock.vacuum.a73", // Q8 Max "roborock.vacuum.a75", // Q Revo "roborock.vacuum.a15", // S7 "roborock.vacuum.a51", // S8 "roborock.vacuum.a70", // S8 Pro Ultra "roborock.vacuum.a62", // S7 Pro Ultra "roborock.vacuum.a65", // S7 Max Ultra "roborock.vacuum.a27", // S7 MaxV (Ultra) "roborock.vacuum.a87", // Qrevo MaxV "roborock.vacuum.a101", // Q Revo Pro "roborock.vacuum.a97", // S8 MaxV (Ultra) "roborock.vacuum.a104", // Roborock Qrevo S "roborock.vacuum.a135", // Qrevo Curv "roborock.vacuum.a117", // Qrevo Master "roborock.vacuum.a21", // Qrevo Slim "roborock.vacuum.a144", // Saros 10R ].includes(robotModel), // isShakeMopStrengthSupported: p.DMM.currentProduct == p.Products.TanosS || p.DMM.currentProduct == p.Products.TanosSPlus || p.DMM.isGarnet || p.DMM.isTopazSV || p.DMM.isPearlPlus || p.DMM.isCoral || p.DMM.isTopazS || p.DMM.isTopazSPlus || p.DMM.isTopazSC || p.DMM.isTopazSV || p.DMM.isPearlPlus || p.DMM.isTanosSMax || p.DMM.isUltron || p.DMM.isUltronSPlus || p.DMM.isUltronSMop || p.DMM.isUltronSV || p.DMM.isPearl isShakeMopStrengthSupported: [ "roborock.vacuum.a08", // S6 Pure "roborock.vacuum.a10", // S6 MaxV "roborock.vacuum.a62", // S7 Pro Ultra "roborock.vacuum.a51", // S8 "roborock.vacuum.a15", // S7 "roborock.vacuum.a27", // S7 MaxV (Ultra) "roborock.vacuum.a19", // S4 Max "roborock.vacuum.a40", // Q7 "roborock.vacuum.a65", // S7 Max Ultra "roborock.vacuum.a38", // Q7 Max "roborock.vacuum.a73", // Q8 Max "roborock.vacuum.a75", // Q Revo "roborock.vacuum.a70", // S8 Pro Ultra "roborock.vacuum.s5e", // S5 Max "roborock.vacuum.a87", // Qrevo MaxV "roborock.vacuum.a101", // Q Revo Pro "roborock.vacuum.a144", // Saros 10R ].includes(robotModel), // isWaterBoxSupported: [p.Products.Tanos_CE, p.Products.Tanos_CN].hasElement(p.DMM.currentProduct) isWaterBoxSupported: [ "roborock.vacuum.s5e", // S5 Max "roborock.vacuum.s6", // S6 "roborock.vacuum.a08", // S6 Pure "roborock.vacuum.a10", // S6 MaxV "roborock.vacuum.a15", // S7 "roborock.vacuum.a27", // S7 MaxV (Ultra) "roborock.vacuum.a38", // Q7 Max "roborock.vacuum.a40", // Q7 "roborock.vacuum.a51", // S8 "roborock.vacuum.a62", // S7 Pro Ultra "roborock.vacuum.a65", // S7 Max Ultra "roborock.vacuum.a70", // S8 Pro Ultra "roborock.vacuum.a72", // Q5 Pro "roborock.vacuum.a73", // Q8 Max "roborock.vacuum.a75", // Q Revo "roborock.vacuum.a87", // Qrevo MaxV "roborock.vacuum.a101", // Q Revo Pro "roborock.vacuum.a97", // S8 MaxV (Ultra) "roborock.vacuum.a104", // Roborock Qrevo S "roborock.vacuum.a135", // Qrevo Curv "roborock.vacuum.a117", // Qrevo Master "roborock.vacuum.a21", // Qrevo Slim "roborock.vacuum.a144", // Saros 10R ].includes(robotModel), isCustomWaterBoxDistanceSupported: !!(2147483648 & this.features), isBackChargeAutoWashSupported: this.featuresStr && !!(4096 & parseInt("0x" + this.featuresStr.slice(-8))), isAvoidCarpetSupported: [ "roborock.vacuum.a10", // S6 MaxV "roborock.vacuum.a40", // Q7 "roborock.vacuum.s6", // S6 "roborock.vacuum.a73", // Q8 Max "roborock.vacuum.a38", // Q7 Max "roborock.vacuum.a51", // S8 "roborock.vacuum.a75", // Q Revo "roborock.vacuum.a27", // S7 MaxV (Ultra) "roborock.vacuum.a15", // S7 "roborock.vacuum.a70", // S8 Pro Ultra "roborock.vacuum.a62", // S7 Pro Ultra "roborock.vacuum.a65", // S7 Max Ultra "roborock.vacuum.a87", // Qrevo MaxV "roborock.vacuum.a101", // Q Revo Pro "roborock.vacuum.a97", // S8 MaxV (Ultra) "roborock.vacuum.a104", // Roborock Qrevo S "roborock.vacuum.a135", // Qrevo Curv "roborock.vacuum.a117", // Qrevo Master "roborock.vacuum.a144", // Saros 10R ].includes(robotModel), // this isn't the correct way to use this. This code must be from a different robot // isVoiceControlSupported: !!(parseInt(`0x${this.featuresStr || "0"}`.slice(-10, -9)) & 2), isVoiceControlSupported: [ "roborock.vacuum.a27", // S7 MaxV (Ultra) ], isElectronicWaterBoxSupported: [], // nothing for now. If this is needed, add the models here isCleanRouteFastModeSupported: this.featuresStr && !!(256 & parseInt("0x" + this.featuresStr.slice(-8))), isVideoLiveCallSupported: [ "roborock.vacuum.a10", // S6 MaxV "roborock.vacuum.a27", // S7 MaxV (Ultra) "roborock.vacuum.a97", // S8 MaxV (Ultra) "roborock.vacuum.a87", // Qrevo MaxV "roborock.vacuum.a135", // Qrevo Curv "roborock.vacuum.a117", // Qrevo Master "roborock.vacuum.a21", // Qrevo Slim ].includes(robotModel), }; } async processSupportedFeatures() { const robotModel = this.adapter.getProductAttribute(this.duid, "model"); const productCategory = this.adapter.getProductAttribute(this.duid, "category"); if (productCategory == "robot.vacuum.cleaner") { // process states etc. depending on model const modelConfig = { // S6 Pure "roborock.vacuum.a08": ["setCleaningRecordsMixed", "setConsumablesInt"], // S6 MaxV "roborock.vacuum.a10": ["setCleaningRecordsMixed", "setConsumablesInt"], // S7 "roborock.vacuum.a15": ["setCleaningRecordsString", "setConsumablesInt"], // S4 Max "roborock.vacuum.a19": ["setCleaningRecordsInt", "setConsumablesInt"], // S7 MaxV (Ultra) "roborock.vacuum.a27": [ "setCleaningRecordsString", "setConsumablesInt", "set_map_flag", "set_back_type", "set_charge_status", "set_clean_percent", "set_switch_status", ], // Q7 Max "roborock.vacuum.a38": ["setCleaningRecordsString", "setConsumablesInt", "set_map_flag", "set_charge_status"], // Q7 "roborock.vacuum.a40": ["setCleaningRecordsString", "setConsumablesInt", "set_map_flag", "set_charge_status"], // S8 "roborock.vacuum.a51": [ "setCleaningRecordsString", "setConsumablesInt", "set_dss", "set_rss", "set_last_clean_t", "set_map_flag", "set_charge_status", "set_clean_percent", "set_switch_status", "set_cleaned_area" ], // S7 Pro Ultra "roborock.vacuum.a62": ["setCleaningRecordsString", "setConsumablesInt", "set_dss", "set_rss", "set_map_flag", "set_charge_status", "set_clean_percent"], // S7 Max Ultra "roborock.vacuum.a65": [ "setCleaningRecordsString", "setConsumablesInt", "set_dss", "set_rss", "set_map_flag", "set_back_type", "set_charge_status", "set_clean_percent", "set_switch_status", "set_clean_fluid", "set_cleaned_area", ], // S8 Pro Ultra "roborock.vacuum.a70": [ "setCleaningRecordsString", "setConsumablesInt", "set_common_status", "set_dss", "set_rss", "set_last_clean_t", "set_map_flag", "set_back_type", "set_charge_status", "set_switch_status", "set_clean_percent", "set_custom_mode_max_plus", ], // Q5 Pro "roborock.vacuum.a72": [ "setCleaningRecordsString", "setConsumablesInt", "set_dss", "set_rss", "set_last_clean_t", "set_map_flag", "set_back_type", "set_charge_status", "set_clean_percent", "set_cleaned_area", "set_switch_status", "set_common_status", "set_in_warmup", ], // Q5 Max+ "roborock.vacuum.a125": ["setCleaningRecordsInt", "setConsumablesString"], // S4 Max "roborock.vacuum.a19": ["setCleaningRecordsInt", "setConsumablesInt"], // Q8 Max "roborock.vacuum.a73": [ "setCleaningRecordsString", "setConsumablesInt", "set_common_status", "set_dss", "set_rss", "set_in_warmup", "set_last_clean_t", "set_map_flag", "set_charge_status", "set_clean_percent", "set_switch_status", ], // Q Revo "roborock.vacuum.a75": [ "setCleaningRecordsString", "setConsumablesInt", "set_common_status", "set_dss", "set_rss", "set_kct", "set_in_warmup", "set_last_clean_t", "set_map_flag", "set_back_type", "set_charge_status", "set_clean_percent", "set_rdt", "set_switch_status", "set_cleaned_area", ], // S5 Max "roborock.vacuum.s5e": ["setCleaningRecordsMixed", "setConsumablesInt"], // S6 "roborock.vacuum.s6": ["setCleaningRecordsMixed", "setConsumablesString"], // Qrevo MaxV "roborock.vacuum.a87": [ "setCleaningRecordsString", "setConsumablesInt", "set_in_warmup", "set_charge_status", "set_clean_percent", "set_rss", "set_common_status", "set_last_clean_t", "set_kct", "set_map_flag", "set_replenish_mode", "set_repeat", "set_rdt", "set_cleaned_area", "set_clean_times", "set_switch_status", ], // Q Revo Pro "roborock.vacuum.a101": [ "setCleaningRecordsString", "setConsumablesInt", "set_common_status", "set_dss", "set_rss", "set_kct", "set_in_warmup", "set_last_clean_t", "set_map_flag", "set_back_type", "set_charge_status", "set_clean_percent", "set_cleaned_area", "set_switch_status", ], // S8 MaxV (Ultra) "roborock.vacuum.a97": [ "setCleaningRecordsString", "setConsumablesInt", "set_cleaned_area", "set_last_clean_t", "set_switch_status", "set_monitor_status", "set_kct", "set_common_status", "set_rss", "set_clean_percent", "set_charge_status", "set_in_warmup", "set_map_flag", "set_task_id", "set_dss", ], // Roborock Qrevo S "roborock.vacuum.a104": [ "setCleaningRecordsString", "setConsumablesInt", "set_in_warmup", "set_charge_status", "set_clean_percent", "set_rss", "set_dss", "set_common_status", "set_kct", "set_switch_status", "set_map_flag", "set_cleaned_area", ], // Roborock Qrevo Curv "roborock.vacuum.a135": [ "setCleaningRecordsString", "setConsumablesInt", "set_monitor_status", "set_in_warmup", "set_clean_percent", "set_extra_time", "set_rss", "set_common_status", "set_kct", "set_switch_status", "set_last_clean_t", "set_cleaning_info", "set_exit_dock", "set_charge_status", "set_map_flag", "set_cleaned_area", "set_task_id", "set_dss", ], // Roborock Qrevo Master "roborock.vacuum.a117": [ "setCleaningRecordsString", "setConsumablesInt", "set_last_clean_t", "set_switch_status", "set_kct", "set_common_status", "set_dss", "set_rss", "set_clean_percent", "set_charge_status", "set_in_warmup", "set_monitor_status", "set_task_id", "set_cleaned_area", "set_map_flag", ], // Roborock Qrevo Slim "roborock.vacuum.a21": [ "setCleaningRecordsString", "setConsumablesInt", "set_map_flag", "set_cleaned_area", "set_replenish_mode", "set_clean_times", "set_task_id", "set_monitor_status", "set_in_warmup", "set_charge_status", "set_clean_percent", "set_dss", "set_rss", "set_common_status", "set_kct", "set_switch_status", "set_last_clean_t", ], // Saros 10R "roborock.vacuum.a144": [ "setCleaningRecordsString", "setConsumablesInt", "set_map_flag", "set_cleaned_area", "set_replenish_mode", "set_clean_times", "set_task_id", "set_monitor_status", "set_in_warmup", "set_charge_status", "set_clean_percent", "set_dss", "set_rss", "set_common_status", "set_kct", "set_switch_status", "set_last_clean_t", ], }; // process modelConfig const configActions = modelConfig[robotModel]; if (configActions) { for (const actionName of configActions) { const action = actions[actionName]; if (action) { await action(this); } } } else { this.adapter.catchError(`This robot ${robotModel} is not fully supported just yet. Contact the dev to get this robot fully supported!`); } this.adapter.createBaseRobotObjects(this.duid); const featureList = this.getFeatureList(); this.adapter.log.debug(`Supported features of robot ${this.duid} - ${robotModel}: ${JSON.stringify(featureList)}`); Object.keys(featureList).forEach((feature) => { if (featureList[feature]) { if (typeof this[feature] === "function") { this[feature](); } } }); // process commands for (const [command, type] of Object.entries(commands)) { if (typeof type == "string") { await this.adapter.createCommand(this.duid, command, type); } else { await this.adapter.createCommand(this.duid, command, type.type, type.defaultState, type.states); } } // process device states for (const [state, type] of Object.entries(deviceStates)) { if (typeof type == "string") { await this.adapter.createDeviceStatus(this.duid, state, type); } else { await this.adapter.createDeviceStatus(this.duid, state, type.type, type.states, type.unit); } } // process consumables for (const [consumable, object] of Object.entries(this.consumables)) { await this.adapter.createConsumable(this.duid, consumable, object.type, object.states, object.unit); } // process reset of consumables for (const [, resetConsumable] of Object.entries(resetConsumables)) { await this.adapter.createResetConsumables(this.duid, resetConsumable); } // process cleaning info for (const [cleaningInfoKey, cleaningInfoEntry] of Object.entries(this.cleaningInfo)) { await this.adapter.createCleaningInfo(this.duid, cleaningInfoKey, cleaningInfoEntry); } // process cleaning records for (const [cleaningRecord, object] of Object.entries(this.cleaningRecords)) { await this.adapter.createCleaningRecord(this.duid, cleaningRecord, object.type, object.states, object.unit); } } else if (productCategory == "roborock.vacuum") { // vacuum (not sure if it's actually roborock.vacuum. Might be something else. Haven't testet) this.adapter.createBasicVacuumObjects(this.duid); } else if (productCategory == "roborock.wm") { // washing machine this.adapter.createBasicWashingMachineObjects(this.duid); } } processDockType(dockType) { switch (dockType) { case 0: // Charging dock // nothing to do here break; case 1: // Auto-Empty Dock - Onyx: S7+/S7Plus (Empty Dock) this.isDustCollectionSettingSupported(); break; case 2: // Empty Wash Fill Dock - Onyx2: G10 this.isWashThenChargeCmdSupported(); break; case 3: // Empty Wash Fill (Dry) Dock - Onyx3: S7 Pro, MaxV Ultra this.isDustCollectionSettingSupported(); this.isWashThenChargeCmdSupported(); this.isSupportedDrying(); break; case 5: // Onyx-C: S8, S8 Plus, Q8, Q8 Max this.isDustCollectionSettingSupported(); break; case 6: // Onyx3 Plus: S7 Max Ultra this.isDustCollectionSettingSupported(); this.isWashThenChargeCmdSupported(); this.isSupportedDrying(); break; case 7: // Onyx4: S8 Pro Ultra this.isDustCollectionSettingSupported(); this.isWashThenChargeCmdSupported(); this.isSupportedDrying(); break; case 8: // PEARL: Q Revo, P10 this.isDustCollectionSettingSupported(); this.isWashThenChargeCmdSupported(); this.isSupportedDrying(); break; // Not much info on this one. Might be missing some features case 9: // Unknown codename for now: Q Revo Pro this.isDustCollectionSettingSupported(); this.isWashThenChargeCmdSupported(); this.isSupportedDrying(); break; default: break; } } getConsumablesDivider(consumable) { const robotModel = this.adapter.getProductAttribute(this.duid, "model"); const consumables = robotModel == "roborock.vacuum.s4" ? consumablesInt : consumablesString; if (consumables[consumable]) { return consumables[consumable].divider; } else { return false; } } getStatusDivider(attribute) { const divider = deviceStates[attribute]?.divider; if (divider) { return divider; } else { return false; } } getFirmwareFeature(featureID) { const feature = firmwareFeatures[featureID]; if (feature) { return feature; } else { return "unknown feature"; } } } module.exports = { deviceFeatures, };