Cleaning specific room and zones with multi level map (Xiaomi roborock)

Hi PeterK,
first: Many thanks for the tutorial. I’ve replicated/adopted to my setup and it worked immediatly. Not yet tried with the second floor level - but the local floor is already working fine!

I’m sure, you are aware of the multi-room option already. If not, I’d like to add it here - just in case :wink:
(and for reference…)

I’ve simply put multiple rooms in my MAP-file for a single vacuum switch switch e.g. <...>_EG_Fliesen (notice the last two lines):

123456722020=OG
123456721998=EG
EG_Eingang=17
EG_Kueche=16
EG_WoZi=19
EG_BiBo=18
EG_Fliesen=16,17
EG_All=16,17,18,19

This concatenates multiple rooms in the ground floor.

Obvious intention is starting a combined cleaning of connected rooms, e.g. mopping the whole tiled floor in the kitchen and the entrance, while leaving out the wooden floor in the living rooms…

Hi PeterK,
I think I’ve spotted a little flaw in “A new map is available” rule…

I had some occasions, where the map changed to UNDEF after starting the vaccum. If we do a get_room_mapping in that case, we get an empty room list which let the room mapping fail.

Therefore I added a second condition && Rocky_CleaningMap != UNDEF before asking for the mapping:

	if  (RockyEtagensuche.state == ON && Rocky_CleaningMap != UNDEF ) {
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "New Map and openhab should check for Level - I get the Floorplan")
		Rocky_ExecuteCommand.sendCommand("get_room_mapping")

Seems, that this solved it… :wink:

EDIT: I found another situation which was not yet covered and lead to unwanted results. I made some more modifications on your rules. I’m going to share those here, once I have tested them more …

Hi,

I will later try both inputs. They are very interesting.

I already programmed the rules to do also zones as e.g. “under the table” or “where the small one eats cookies because she thinks I don’t know.”.

I did not have the time to do documentation, but as soon as I am ready, I will post the new rules and also put your suggestions in it

BR
Peter

I’ve sent you a PM with my latest rule code. Meanwhile it’s tested even with trying to trigger some corner cases:

  • loading the wrong map through the app
  • closed doors
  • starting from dock with wrong map selected

The UNDEF-map is now handled properly I think. For now, I’m quite happy with the set of rules.

Let me know, if you want to check the provided code first and probably merge with your latest additions and then updating your starting post.

Don’t want to confuse with my rules in your tutorial thread :wink:

My last post has been lost due to the forum’s upgrade issue… I will re-post asap.

The new rules do have the possibilitiy to clean segments and/or room

BR PeterK

1 Like

Hi,

the new rules are now able to do

  • room-cleaning,
  • multi-room cleaning,
  • zone cleaning and
  • multi-zone cleaning.

The method to find the current floor is adapted from here:

The method for the zone cleaning is from here:

In order to use the rule, you need newest binding. You can upgrade with karaf using

update org.openhab.binding.miio https://ci.openhab.org/job/openHAB-Addons/lastSuccessfulBuild/artifact/bundles/org.openhab.binding.miio/target/org.openhab.binding.miio-3.1.0-SNAPSHOT.jar

You need the map file holding the roomID of room 16 and the mapping of the single rooms as described in the first post. Additionally you can now add zones by defining:

  • meters from the dock to the starting point of the zone
  • dimension of the zone in meters.

The map file will then look like:

838001020681=EG
838001020689=UG
838001020686=E1
EG_BU=19
EG_FL=24
EG_ES=21
EG_KU=18
EG_GT=17
EG_SA=16
EG_BUFLGT=19,24,17
esstisch=-8.9,2.2,2.8,2.5,1
arbeitsflaeche=-4.2,1.3,4.2,1.3,1
grundreinigung=esstisch,arbeitsflaeche
=NULL

Example how to find the values of above zone “esstisch” using the original App:

  • draw your zone and you will see the dimensions in the App. In this case 2,8x2,5m
  • draw another zone starting from the dock just to the starting point. The app shows the values in meter of the the starting point. In this case 8,9x2,2m

This results in

esstisch=-8.9,2.2,2.8,2.5,1

as seen in the map-file. The last number is to control the number of repeats. If you want to clean twice, it is

esstisch=-8.9,2.2,2.8,2.5,2

You will need these items

Switch Rocky_VacuumOnOff  		// linked to :actions#vacuum
String Rocky_State 						// linked to :status#state
String Rocky_ExecuteCommand 				// linked to :actions#rpc
Switch Rocky_RobotLocating					// linked to :status#is_locating
Image Rocky_CleaningMap

Group:Switch:OR(ON,OFF)	gSaugSwitches		"Group with one switch for each room"
Switch	HH_UG_GZ_Room1  (gSaugSwitches) 
Switch	HH_UG_KE_Room2  (gSaugSwitches)


String Rocky_Etage "The current level"
String Vacuum_Zone "The zone to clean"
String Rocky_Saugbefehle "Please clean..."

Please check the first post for the naming convention of the switches and details how to find the different IDs.

Additionally you can now also have switches for zones as e.g. HH_EG_ZO_arbeitsflaeche in the group. The “ZO” will be recognized as Zone and the following string “arbeitsfläche” will then be used as zone.

The string Rocky_Saugbefehle is optional. The single switches are best for e.g. alexa but in a UI it is sometimes nicer to have one single item controlling all rooms and zones. In order to have this, you have to configure (in the UI) stateDescription & Command options es .e.g.

EG_KU=Küche
EG_EZ=Esszimmer
EG_FL=Eingang
EG_GT=Gästetoilette
EG_BU=Büro
SP_UT=Unterm Tisch
EG_ZO_esstisch=unterm Tisch
EG_ZO_arbeitsflaeche=Küchenzeile

You will then have a one Item with a nice drop-down in the UI. Using the drop down will trigger a rule which finds the correct switch => If you do not need the drop-down, the rules will work without it.

The rules

// this is to en- or disable  debugging of a specific set of rules. 
val logName = "Rocky" // Name of the set of rules
var loggingInfo = true // if loggingInfo =true , this set of rules can be debugged without getting debug-messages from every rule.
var Timer Pause_Timer = null

val Dockfloor = "EG" // Floor where the docking station is. This will assume that the correct map is loaded when vacuum is in dock. This can speed up things, but might produce error if someone chages map in app.  Set to "NO" to disable


///////////////////////////////////////////////////////////////////////////////
// Function to calculate from m to values the vacuum understands
// see https://community.openhab.org/t/xiaomi-roborock-zone-cleanup-rule/55138/2?u=peterk
///////////////////////////////////////////////////////////////////////////////

val Functions$Function1<String, String> getZoneCoordinates =
[zone |
  var parameters = zone.split(',')
  // Docking point start position
  val double x = 25500.0;
  val double y = 25500.0;
  // Bottom (y) left (x)
  val double b = Double::parseDouble(parameters.get(0)) * 1000.0 + x
  val double l = Double::parseDouble(parameters.get(1)) * 1000.0 + y
  // Top (y) right (x)
  val double t = b + Double::parseDouble(parameters.get(2)) * 1000.0
  val double r = l + Double::parseDouble(parameters.get(3)) * 1000.0
  // Build zone coordinates (and number of times to scan)
  val coordinates = String::format("[%.0f,%.0f,%.0f,%.0f,%s]", b, l, t, r, parameters.get(4));
  coordinates
]

/////////////////////////////////////////////////////////////////////
// The rules to check for the floor each time when positioning is done
// The idea is from here https://community.openhab.org/t/xiaomi-roborock-room-clean-multi-level-map/108850/13
///////////////////////////////////////////////////////////////////

// when a  new map is available, the rule will request a floorplan
rule "A new map is available"
when
	Item Rocky_RobotLocating changed to OFF or // vacuum stops searchin
	Item Rocky_CleaningMap changed from UNDEF // if Rocky_RobotLocating is buggy
then
	val ruleLogName = "A new map is available (" + triggeringItemName + ") "
	// A new map is available. I the floor has to be determined, we need the room_mapping. 
	// The rule will ask for a room_mapping and another rule will wait for the result 
	if (loggingInfo == true ) logInfo(logName, ruleLogName + "New Map and openhab should check for Level - I get the Floorplan")
	Rocky_ExecuteCommand.sendCommand("get_room_mapping")
end

rule "When room_mapping is available"
when
	Item Rocky_ExecuteCommand changed from get_room_mapping
then
	val ruleLogName = "room_mapping is available: "
	if (loggingInfo == true ) logInfo(logName, ruleLogName + "Triggered. The room_mapping is " + newState.toString)
	
	var String firstRoom = transform("JSONPATH", "$.result[0][0]", newState.toString)
	if (loggingInfo == true ) logInfo(logName, ruleLogName + "First room on room_mapping is " + firstRoom)
	
	// first room should be 16
	if (firstRoom == "16"){
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "Check the ID of Room 16 (unique for each floor) and use map in order to find floor")
		
		var Etage = transform("MAP", "Rocky_Etage.map", transform("JSONPATH", "$.result[0][1]",newState.toString))		
		
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "Drumroll... we present you the new floor: " + Etage)
		Rocky_Etage.sendCommand(Etage)
		
	} else {
		// If the check is not succesfull, the floor will be set to UNDEF
		Rocky_Etage.sendCommand("UNDEF")
	}
end

/////////////////////////////////////////////////////////////////////
// The rules to control the vacuum
///////////////////////////////////////////////////////////////////

rule "Vacuum should go to work"
when
	Member of gSaugSwitches  received command ON
then
	// For debugging: Set the name of this rule
	val ruleLogName = "Vacuum should go to work (" + triggeringItemName + ") "
	// This is how a debuglog looks like.... I also use this for documentation of the code
	if (loggingInfo == true ) logInfo(logName, ruleLogName )
	
	// This rule starts if one switch in the group receives the command to clean a specific room 
	
	// Check if floor is already known
	if (( Rocky_State.state == "Charging"  )  && (  Rocky_Etage.state == Dockfloor ) && (  Dockfloor != "NO" ) ) {
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "Vacuum in Dock => Level is known")
		// If the current floor is know, dont wait for new map-update.This will be wrong if someone changed the map in the app and did not start cleaning (which would have updated Rocky_Etage).
		Rocky_Etage.sendCommand(Dockfloor) 
	} else {
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "Vacuum not in Dock => Start cleaning and positioning")
		// No Command (only update), as a command would directly trigger next rule. Using update, the next rule will be triggered after a map-update
		Rocky_VacuumOnOff.sendCommand(ON)
		// Start vacuum. The vacuum will automatically start positioning. 
		// After positioning (or at least after starting cleaning) this will automatically result in a new map which will trigger next rule	
	}

end 

// When the "check the floor" is done
rule "Go to your room"
when
	Item Rocky_Etage received command 
then
	// We know the current floor - now we check if the room in in this floor and then go to this room
	val ruleLogName = "Go to your room " 
	if (loggingInfo == true ) logInfo(logName, ruleLogName + "Triggered")
	
	// If there is a command to clean a room
	if (gSaugSwitches.state == ON) {
		
		// stop cleaning for new instructions
		if  ( (Rocky_ControlVacuum.state != "pause" ) &&  (Rocky_ControlVacuum.state != "dock" ) ) {
			Rocky_ControlVacuum.sendCommand("pause")
		}
		
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "Check which switch was used latest")
		val WelcherSaugSwitch = gSaugSwitches.members.filter[i|i.state==ON].sortBy[lastUpdate].last

		if (loggingInfo == true ) logInfo(logName, ruleLogName + "found: " + WelcherSaugSwitch.name)
		
		// get level and room from name of the switch
		val String SollEtage = WelcherSaugSwitch.name.toString.split('_').get(1) 
		val String SollRaum = WelcherSaugSwitch.name.toString.split('_').get(2)
		
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "Floor: " + SollEtage + "; Room: " + SollRaum)

		// only do the cleaning once
		WelcherSaugSwitch.postUpdate(OFF)

		//if vacuum is in correct floor. If floor is UNDEF, this will stop the vacuum and exit
		if ( SollEtage != Rocky_Etage.state.toString ) {
			if (loggingInfo == true ) logInfo(logName, ruleLogName + "Falsche Etage. Ist= " + Rocky_Etage.state + ", Soll: " + SollEtage )
			return
		} 
		
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "Etage OK")
		
		// if Zonecleaning was activated go to this rule and exit
		if ( SollRaum ==  "ZO" ) {
			val String SollZone = WelcherSaugSwitch.name.toString.split('_').get(3)
			if (loggingInfo == true ) logInfo(logName, ruleLogName + "Zonenreinigung von " + SollZone)
			Vacuum_Zone.sendCommand(SollZone)
			return
		} 
		
		
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "Raumreinigung")
		// OK... Roomcleaning it is

		// get room ID from MAP
		val RaumNummer =  transform("MAP", "Rocky_Etage.map", SollEtage + "_" + SollRaum)		
		
		
		Pause_Timer = createTimer( now.plusSeconds(3) ,[|
			if (loggingInfo == true ) logInfo(logName, ruleLogName + "Go to your room " + RaumNummer)
			// Finally.... send the room
			Rocky_VacuumRoom.sendCommand(RaumNummer)
		 ])
	} else {
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "Thank you for the information of the current floor, but I just keep cleaning")
	}
end

rule "Vacuum Zone control"
when
  Item Vacuum_Zone received command
then
	// From https://community.openhab.org/t/xiaomi-roborock-zone-cleanup-rule/55138/2?u=peterk
	if (loggingInfo == true ) logInfo(logName, ruleLogName + "Vacuum command: "+ receivedCommand.toString)

	// The final command send to the vacuum
	var String command = ""
	// Get zone coordinates
	var String zone = transform("MAP", "Rocky_Etage.map", receivedCommand.toString)
	// We still don't know if this is single or multiple zone
	var parameters = zone.split(',')
	try {
	  // Single zone only have numbers
	  Double::parseDouble(parameters.get(0))
	  command = getZoneCoordinates.apply(zone)
	} catch (Throwable t) {
	  // This is multi zone
	  parameters.forEach[string z|
		zone = transform("MAP", "Rocky_Etage.map.map", z)
		command = command + getZoneCoordinates.apply(zone) + ","
	  ]
	  // Remove last ','
	  command = command.substring(0, command.length - 1)
	}
	command = String::format("[%s]", command)
	if (loggingInfo == true ) logInfo(logName, ruleLogName + "Clean {} zone coordinates {}", receivedCommand.toString, command)
	Rocky_ExecuteCommand.sendCommand('app_zoned_clean' + command)

end

// The sinlge switches are nice if you use e.g. Alexa. In the UI, it is nicer to have one string with command options
// This rlule will get the correct switch from the string command
rule "Saugbefehl generieren aus String"
when
	Item Rocky_Saugbefehle received command
then
	val ruleLogName = "Saugbefehl generieren aus String (" + receivedCommand + ") "
	if (loggingInfo == true ) logInfo(logName, ruleLogName + "suche HH_" + receivedCommand + "*")
	
	val SaugbefehlItem = gSaugSwitches.members.filter[s| s.name.startsWith("HH_" + receivedCommand)].head as SwitchItem
	
	if (loggingInfo == true ) logInfo(logName, ruleLogName + "Item gefunden: " + SaugbefehlItem.name)
	SaugbefehlItem.sendCommand(ON)
end

As said earlier… my original post was lost - I hope this one covers all.
BR
PeterK

Can you share the exact response your vacuum is giving to the get_room_mapping command… or rather do you know the structure and meanining.
I understand it is a roomID and some numeric code. Can the code be translated to the coordinates to be send?

Is in the MiHome app the possibility to specify a specific floor & room? Does it show you which floor it is on?
Do you see readable room names in the mihome app? (meaning it has some command to map from room to roomID)

My vacuum does not have such functionality. Maybe that can help implementing something in the binding. I certainly can add a channel for the room mappings if that helps, so it gets refreshed with each regular refresh of the data.

2 Likes

Hi,

On one map i send the command twice:

Item 'Rocky_ExecuteCommand' changed from get_room_mapping to {"code":0,"exe_time":2,"id":96,"otlocalts":1612988004148910,"result":[[16,"838001020681"],[17,"838001020684"],[18,"838001020664"],[19,"838001020685"],[21,"838001020717"],[24,"838001020683"]]}
Item 'Rocky_ExecuteCommand' changed from get_room_mapping to {"code":0,"exe_time":3,"id":113,"otlocalts":1612988068925085,"result":[[16,"838001020681"],[17,"838001020684"],[18,"838001020664"],[19,"838001020685"],[21,"838001020717"],[24,"838001020683"]]}

I then changed the map in the App:

Item 'Rocky_CleaningMap' changed from raw type (image/jpeg): 48897 bytes to raw type (image/jpeg): 30242 bytes

and send the command twice again:

Item 'Rocky_ExecuteCommand' changed from get_room_mapping to {"code":0,"exe_time":1,"id":131,"otlocalts":1612988183068802,"result":[[16,"838001020686"],[18,"838001020687"],[20,"838001020682"]]}
Item 'Rocky_ExecuteCommand' changed from get_room_mapping to {"code":0,"exe_time":2,"id":143,"otlocalts":1612988229385109,"result":[[16,"838001020686"],[18,"838001020687"],[20,"838001020682"]]}

For me it just looks like that every room has a unique ID and I use this to check if the floor changed. I assume that the IDs will change if I change the rooms in the map… but I most likely will never do that.

In the App I can choose the floor and I see names of the rooms (Picture will follow to you directly). I don’t know if the room names are readable or stored in the cloud.

When the robot starts cleaning, he sometimes start positioning (driving around and comparing the LIDAR-data to the stored maps) and chooses the correct map himself.

The functionality was added to a lot of older vacuums too… Perhaps you got the feature too without realizing.
Having a channel for the ID of room 16 would already be nice. Every time this ID changes, the rule to map the floor can be triggered… perhaps using a profile would be possible. In this case one would directly get the floor.

Br
PeterK

If someone has time and has a v6 type of model could you try these commands and share the responses from the vacuum

get_camera_status []
get_timer_summary []
get_timer_detail []
get_prop []
get_map []
get_map_v2 []
get_clean_record_map_v2 []
get_carpet_mode []
get_fw_features []
get_fresh_map []
get_persist_map []
get_recover_maps []
get_recover_map []
get_map_status []
get_segment_status []
get_network_info []
get_led_status []
get_customize_clean_mode []
get_multi_maps_list []
get_multi_map  []

the get_segment_status prob needs the segment id between the brackets

note: Not all of them are related to the map list, but may help the development of the robot specific features in the binding.

Item 'Rocky_ExecuteCommand' changed from get_camera_status to {"code":0,"exe_time":77,"id":14098,"otlocalts":1613064379290759,"result":[3461]}

Item 'Rocky_ExecuteCommand' changed from get_timer_summary to {"code":0,"exe_time":1,"id":14110,"otlocalts":1613064431199644,"result":[]}

get_timer_detail → Nothing

get_prop → Nothing

On "Erdgeschoss"
Item 'Rocky_ExecuteCommand' changed from get_map to {"code":0,"exe_time":2,"id":14143,"otlocalts":1613064579899600,"result":["roboroommap%2F322134263%2F10"]}
On "Keller" (tried three times)
Item 'Rocky_ExecuteCommand' changed from get_map to {"code":0,"exe_time":2,"id":545,"otlocalts":1613069178529137,"result":["roboroommap%2F322134263%2F14"]}
Item 'Rocky_ExecuteCommand' changed from get_map to {"code":0,"exe_time":2,"id":552,"otlocalts":1613069226522861,"result":["roboroommap%2F322134263%2F16"]}
Item 'Rocky_ExecuteCommand' changed from get_map to {"code":0,"exe_time":2,"id":558,"otlocalts":1613069239112651,"result":["roboroommap%2F322134263%2F0"]}

Item 'Rocky_ExecuteCommand' changed from get_map_v2 to {"code":0,"id":14149,"otlocalts":1613064614370327,"result":"unknown_method"}

get_clean_record_map_v2 → Nothing

Item 'Rocky_ExecuteCommand' changed from get_carpet_mode to {"code":0,"exe_time":3,"id":14162,"otlocalts":1613064662985121,"result":[{"current_high":500,"current_integral":450,"current_low":400,"enable":1,"stall_time":10}]}

Item 'Rocky_ExecuteCommand' changed from get_fw_features to {"code":0,"exe_time":2,"id":14169,"otlocalts":1613064686220393,"result":[111,112,113,114,115,116,117,118,119,120,121,122,123,124,125]}

Item 'Rocky_ExecuteCommand' changed from get_fresh_map to {"code":0,"exe_time":1,"id":14176,"otlocalts":1613064715584676,"result":"unknown_method"}

Item 'Rocky_ExecuteCommand' changed from get_persist_map to {"code":0,"exe_time":1,"id":14177,"otlocalts":1613064734820496,"result":"unknown_method"}

Item 'Rocky_ExecuteCommand' changed from get_recover_maps to {"code":0,"exe_time":3,"id":14183,"otlocalts":1613064753962541,"result":[[1,1611430630],[2,1611430708],[3,1611430817]]}

get_recover_map → Nothing

Item 'Rocky_ExecuteCommand' changed from get_map_status to {"code":0,"id":14191,"otlocalts":1613064794680825,"result":[1]}

Item 'Rocky_ExecuteCommand' changed from get_segment_status to {"code":0,"exe_time":2,"id":14197,"otlocalts":1613064829989360,"result":[1]}

Item 'Rocky_ExecuteCommand' changed from get_network_info to {"code":0,"exe_time":59,"id":14204,"otlocalts":1613064851128826,"result":{"bssid":"xx:xx:xx:xx:xx:xx","ip":"192.168.xxx.xx","mac":"xx:xx:xx:xx:xx:xx","rssi":-51,"ssid":"xxxx"}} (This one is censored... the real values were visible)

Item 'Rocky_ExecuteCommand' changed from get_led_status to {"code":0,"exe_time":3,"id":14211,"otlocalts":1613064880545279,"result":[1]}

Item 'Rocky_ExecuteCommand' changed from get_customize_clean_mode to {"code":0,"exe_time":1,"id":14218,"otlocalts":1613064904587312,"result":[{"fan_power":102,"segment":16,"water_box_mode":203},{"fan_power":102,"segment":17,"water_box_mode":203},{"fan_power":102,"segment":18,"water_box_mode":203},{"fan_power":101,"segment":22,"water_box_mode":201},{"fan_power":101,"segment":23,"water_box_mode":201},{"fan_power":101,"segment":20,"water_box_mode":201},{"fan_power":104,"segment":19,"water_box_mode":201},{"fan_power":104,"segment":21,"water_box_mode":203},{"fan_power":104,"segment":24,"water_box_mode":202}]}

Item 'Rocky_ExecuteCommand' changed from get_multi_maps_list to {"code":0,"exe_time":4,"id":14225,"otlocalts":1613064936112662,"result":[{"map_info":[{"add_time":1613030910,"bak_maps":[],"length":11,"mapFlag":0,"name":"Erdgeschoss"},{"add_time":1611430630,"bak_maps":[],"length":8,"mapFlag":1,"name":"1. Etage"},{"add_time":1611430708,"bak_maps":[],"length":6,"mapFlag":2,"name":"Keller"},{"add_time":1611430817,"bak_maps":[],"length":8,"mapFlag":3,"name":"2. Etage"}],"max_bak_map":0,"max_multi_map":4,"multi_map_count":4}]}

get_multi_map → Nothing

1 Like

The get_multi_maps_list is interesting.
I can imagine the response of get_multi_map [1] or so with the name give some more details

Item 'Rocky_ExecuteCommand' changed from get_multi_map [1] to {"code":0,"exe_time":145,"id":1057,"otlocalts":1613071961310518,"result":["roboroommap%2F322134263%2F20"]}

This is unfortunate… I was hoping for a list of room names… but this points to a robo map instead.

A channel always presenting the ID of room 16 would already help. AFAIK a profile can be used to map this to the current floor using map-transformation.

In my case “Rocky_Etage” would be updated automatically and this would make two of the rules above obsolete.

What do you think?

Yes, of you don’t mind making a GitHub issue/request for it I’ll add a channel for it when I have a chance

2 Likes

Done

BTW… Thanks for your great work.

PR for this has been submitted

1 Like

Thanks for sharing this list. Some commands seem to be new compared to this list (or I just didn’t find the commands in there):
https://github.com/marcelrv/XiaomiRobotVacuumProtocol
Would it be possible to share a full list of all commands you know? Is the linked list also comming from you?

I’m searching for a command to select/restore a map from a level. I have a Roborock S6. The command get_multi_maps_list works and returns:
[{'max_multi_map': 4, 'max_bak_map': 0, 'multi_map_count': 2, 'map_info': [{'mapFlag': 0, 'add_time': 1610144299, 'length': 11, 'name': 'Erdgeschoss', 'bak_maps': []}, {'mapFlag': 1, 'add_time': 1613669204, 'length': 12, 'name': 'Obergeschoss', 'bak_maps': []}]}]
Very interesting and exactly the map description I see in the Android App from Xiaomi.

With the help of your list, I found a new command through try and error, which might help to recover a dedicated map of a floor/level:
recover_multi_map []
It seems to be a valid method, but I could not figure out which parameters I have to provide. I called the commands with the mirobo tool in a Debian console (mirobo --ip 192.168.x.x --token xxxxx raw-command recover_multi_map). Responses of command:
recover_multi_map -> Error: No response from device
recover_multi_map 1 -> Error: {'code': -10005, 'message': 'Params is not an Array'}
recover_multi_map [1] -> Error: {'code': -10005, 'message': 'First element in array is not an object'}

Maybe this helps somebody to come to new ideas and we finally directly get the information which map is loaded and load the map we want.

2 Likes

Yes the linked list on githum is mine.
My intention is indeed to add the newly found commands when I have some spare time & lust.
But where you have output for not yet listed commands feel free to add them :slight_smile: I only have the v1 device, so most of the newer commands are not availabe for mine.

I expect the recover_multi_map has as parameter a map id, or a array with map id e.g. [[mapid]]

Here is my longest list of available commands for the roborock vacuums.
To my knowledge the get_ you can always try without breaking anything.

{
“model”: “roborock.vacuum.s5e”,
“miot_items”: 0,
“miio_items”: 88,
“commands”: [
“set_clean_motor_mode”,
“set_camera_status”,
“set_clean_sequence”,
“set_homesec_password”,
“set_custom_mode”,
“set_water_box_custom_mode”,
“set_timer”,
“set_dnd_timer”,
“set_timezone”,
“set_app_timezone”,
“set_carpet_mode”,
“set_lab_status”,
“set_server_timer”,
“set_fds_endpoint”,
“set_led_status”,
“set_customize_clean_mode”,
“set_ignore_identify_area”,
“set_timer_dup”,
“set_map_name”,
“get_camera_status”,
“get_timer_summary”,
“get_timer_detail”,
“get_testid”,
“get_clean_sequence”,
“get_turn_server”,
“get_device_sdp”,
“get_device_ice”,
“get_multi_map”,
“get_prop”,
“get_status”,
“get_map”,
“get_map_v1”,
“get_map_v2”,
“get_custom_mode”,
“get_water_box_custom_mode”,
“get_clean_summary”,
“get_clean_record”,
“get_clean_record_map”,
“get_clean_record_map_v2”,
“get_consumable”,
“get_timer”,
“get_dnd_timer”,
“get_serial_number”,
“get_log_upload_status”,
“get_sound_progress”,
“get_current_sound”,
“get_timezone”,
“get_carpet_mode”,
“get_sound_volume”,
“get_fw_features”,
“get_fresh_map”,
“get_persist_map”,
“get_room_mapping”,
“get_recover_maps”,
“get_recover_map”,
“get_map_status”,
“get_segment_status”,
“get_server_timer”,
“get_network_info”,
“get_led_status”,
“get_customize_clean_mode”,
“get_multi_maps_list”
]

1 Like

Thanks to the updates of the binding, some rules are obsolete as the binding is doing the heavy lifting. The rules need the newest binding.

Please check earlier posts - there are many answers.

karaf
[Edit: Update of the binding allone is not possible anymore due to braking changes in OH 3.1. You have to install OH 3.1 (Milestone)]

update org.openhab.binding.miio https://ci.openhab.org/job/openHAB-Addons/lastSuccessfulBuild/artifact/bundles/org.openhab.binding.miio/target/org.openhab.binding.miio-3.1.0-SNAPSHOT.jar

Items

Switch Rocky_VacuumOnOff  		// linked to :actions#vacuum
String Rocky_State 						// linked to :status#state
String Rocky_ExecuteCommand 				// linked to :actions#rpc
String Rocky_Room16					// linked to :info#room_mapping with profile JSONPATH; Profile Configuration: $.[0][1]

Group:Switch:OR(ON,OFF)	gSaugSwitches		"Group with one switch for each room"
Switch	HH_UG_GZ_Room1  (gSaugSwitches) 
Switch	HH_UG_KE_Room2  (gSaugSwitches)

And some Switches for the Zones
Switch	HH_EG_ZO_esstisch  (gSaugSwitches)
Switch	HH_EG_ZO_arbeitsflaeche  (gSaugSwitches)


String Rocky_Etage "The current level"
String Vacuum_Zone "The zone to clean"


String Rocky_Saugbefehle "Please clean..." 
/* in the UI, you have to configure Rocky_Saugbefehle with stateDescription & Command options
		EG_KU=Küche
		EG_EZ=Esszimmer
		EG_FL=Eingang
		EG_GT=Gästetoilette
		EG_BU=Büro
		SP_UT=Unterm Tisch
		EG_ZO_esstisch=unterm Tisch
		EG_ZO_arbeitsflaeche=Küchenzeile
*/

map

838001020681=EG
838001020689=UG
838001020686=E1
838001020694=E2
EG_BU=19
EG_FL=24
EG_ES=21
EG_KU=18
EG_GT=17
EG_SA=16
EG_BUFLGT=19,24,17
esstisch=-8.9,2.2,2.8,2.5,1
arbeitsflaeche=-4.2,1.3,4.2,1.3,1
grundreinigung=esstisch,arbeitsflaeche
=UNDEF

rules

// this is to en- or disable  debugging of a specific set of rules. 
val logName = "Rocky" // Name of the set of rules
var loggingInfo = true // if loggingInfo =true , this set of rules can be debugged without getting debug-messages from every rule.
var Timer Pause_Timer = null

// Function to calcilate from m to values the vacuum understands
// see https://community.openhab.org/t/xiaomi-roborock-zone-cleanup-rule/55138/2?u=peterk


val Functions$Function1<String, String> getZoneCoordinates =
[zone |
  var parameters = zone.split(',')
  // Docking point start position
  val double x = 25500.0;
  val double y = 25500.0;
  // Bottom (y) left (x)
  val double b = Double::parseDouble(parameters.get(0)) * 1000.0 + x
  val double l = Double::parseDouble(parameters.get(1)) * 1000.0 + y
  // Top (y) right (x)
  val double t = b + Double::parseDouble(parameters.get(2)) * 1000.0
  val double r = l + Double::parseDouble(parameters.get(3)) * 1000.0
  // Build zone coordinates (and number of times to scan)
  val coordinates = String::format("[%.0f,%.0f,%.0f,%.0f,%s]", b, l, t, r, parameters.get(4));
  coordinates
]

/////////////////////////////////////////////////////////////////////
// The rules to check for the floor each time when positioning is done
// The idea is from here https://community.openhab.org/t/xiaomi-roborock-room-clean-multi-level-map/108850/13
///////////////////////////////////////////////////////////////////

rule "Rocky_Room16 changed"
when
	Item Rocky_Room16 changed
then
	// update the current floor	
	Rocky_Etage.postUpdate(transform("MAP", "Rocky_Etage.map", newState.toString))
end

/////////////////////////////////////////////////////////////////////
// The rules to control the vacuum
///////////////////////////////////////////////////////////////////

rule "Vacuum should go to work"
when
	Member of gSaugSwitches  received command ON
then
	// For debugging: Set the name of this rule
	val ruleLogName = "Vacuum should go to work ( " + triggeringItem.name + ") "
	// This is how a debuglog looks like.... I also use this for documentation of the code
	if (loggingInfo == true ) logInfo(logName, ruleLogName + "Set the floor to UNDEF and start cleaning.")
	

		// Start cleaning - this will trigger positionin if neccesary. If positioning is not neccessary, the bindig will update Room16 anyway
	Rocky_VacuumOnOff.sendCommand(ON)
	
	Pause_Timer = createTimer( now.plusSeconds(5) ,[|
		// After leaving the dock (5 seconds)- ensure there is a change in the floor if there was already a change prior, this will do nothig bad
		Rocky_Room16.postUpdate("[]")
	 ])
	
end 

// When the "check the floor" is done
rule "Go to your room"
when
	Item Rocky_Etage changed 
then

	val ruleLogName = "Go to your room " 
	if (loggingInfo == true ) logInfo(logName, ruleLogName + "Triggered; Rocky_Etage.state = " + Rocky_Etage.state )
	
	if ( Rocky_Etage.state.toString == "UNDEF" ) {
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "UNDEF - Nothing to do")
		return
	}
	
	
	// If there is a command to clean a room
	if (gSaugSwitches.state == ON) {
		
		// stop cleaning for new instructions
		if  ( (Rocky_ControlVacuum.state != "pause" ) &&  (Rocky_ControlVacuum.state != "dock" ) ) {
			Rocky_ControlVacuum.sendCommand("pause")
		}
		
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "Check which switch was used latest")
		val WelcherSaugSwitch = gSaugSwitches.members.filter[i|i.state==ON].sortBy[lastUpdate].last

		if (loggingInfo == true ) logInfo(logName, ruleLogName + "found: " + WelcherSaugSwitch.name)
		
		// only do the command once
		gSaugSwitches.sendCommand(OFF)
		
		// get level and room from name of the switch
		val String SollEtage = WelcherSaugSwitch.name.toString.split('_').get(1) 
		val String SollRaum = WelcherSaugSwitch.name.toString.split('_').get(2)
		
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "Floor: " + SollEtage + "; Room: " + SollRaum)


		//if vacuum is in correct floor. 
		if ( SollEtage == Rocky_Etage.state.toString ) {
			
			if (loggingInfo == true ) logInfo(logName, ruleLogName + "Etage OK")
			
			// if Zonecleaning was activated go to this rule and exit
			if ( SollRaum ==  "ZO" ) {
				val String SollZone = WelcherSaugSwitch.name.toString.split('_').get(3)
				if (loggingInfo == true ) logInfo(logName, ruleLogName + "Zonenreinigung von " + SollZone)
				
				Pause_Timer = createTimer( now.plusSeconds(3) ,[|
					if (loggingInfo == true ) logInfo(logName, ruleLogName + "Los gehts zu Zone " + SollZone)
					// Finally.... send to the Zone
					Vacuum_Zone.sendCommand(SollZone)
				 ])
			}  else { 
							
				if (loggingInfo == true ) logInfo(logName, ruleLogName + "Raumreinigung")
				// OK... Roomcleaning it is

				// get room ID from MAP
				val RaumNummer =  transform("MAP", "Rocky_Etage.map", SollEtage + "_" + SollRaum)		
				
				
				Pause_Timer = createTimer( now.plusSeconds(3) ,[|
					if (loggingInfo == true ) logInfo(logName, ruleLogName + "Go to your room " + RaumNummer)
					// Finally.... send the room
					Rocky_VacuumRoom.sendCommand(RaumNummer)
				 ])
			}
		} else {
			if (loggingInfo == true ) logInfo(logName, ruleLogName + "Falsche Etage. Ist= " + Rocky_Etage.state + ", Soll: " + SollEtage )
		} 
	} else {
		if (loggingInfo == true ) logInfo(logName, ruleLogName + "Thank you for the information of the current floor.")
	}
end

rule "Vacuum Zone control"
when
  Item Vacuum_Zone received command
then
	logInfo(logName, "Vacuum command: {}", receivedCommand.toString)

	// The final command send to the vacuum
	var String command = ""
	// Get zone coordinates
	var String zone = transform("MAP", "Rocky_Etage.map", receivedCommand.toString)
	// We still don't know if this is single or multiple zone
	var parameters = zone.split(',')
	try {
	  // Single zone only have numbers
	  Double::parseDouble(parameters.get(0))
	  command = getZoneCoordinates.apply(zone)
	} catch (Throwable t) {
	  // This is multi zone
	  parameters.forEach[string z|
		zone = transform("MAP", "Rocky_Etage.map.map", z)
		command = command + getZoneCoordinates.apply(zone) + ","
	  ]
	  // Remove last ','
	  command = command.substring(0, command.length - 1)
	}
	command = String::format("[%s]", command)
	logInfo(logName, "Clean {} zone coordinates {}", receivedCommand.toString, command)
	Rocky_ExecuteCommand.sendCommand('app_zoned_clean' + command)

end

// The sinlge switches are nice if you use e.g. Alexa. In the UI, it is nicer to have one string with command options
// This rlule will get the correct switch from the string command
rule "Saugbefehl generieren aus String"
when
	Item Rocky_Saugbefehle received command
then
	val ruleLogName = "Saugbefehl generieren aus String (" + receivedCommand + ") "
	if (loggingInfo == true ) logInfo(logName, ruleLogName + "suche HH_" + receivedCommand + "*")
	
	val SaugbefehlItem = gSaugSwitches.members.filter[s| s.name.startsWith("HH_" + receivedCommand)].head as SwitchItem
	
	if (loggingInfo == true ) logInfo(logName, ruleLogName + "Item gefunden: " + SaugbefehlItem.name)
	SaugbefehlItem.sendCommand(ON)
end
1 Like