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

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

Good evening, I have a Roborock s7 and would also like to try the great project. Unfortunately I can not get the miio binding updated to snapshot. I use openhab 3.0.1. Do I have to update openhab to snapshot as well?

No, I think with 3.0.1 you should be fine

Edit: if you have the Chanel

room_mapping

You are definitively ok.

No, there is no channel called raum_mapping here. I can only send the command “get_room_mapping” via actionCommand.

can’t you update binding in karaf?

If not, you can use an older version in this thread - but latest one is stable compared to the older ones.

Unfortunately, an update to snapshot does not work. I will test an older version first.

good evening, I am now on snapshot and have done everything according to your instructions. Unfortunately, the vacuum cleaner only starts the entire cleaning of the floor when I press a switch and not the individual room. I have the feeling that it does not translate the name of the switch correctly. I also get an error in the log “Could not transform state ‘[]’ with function ‘$.[0][1]’ and format ‘%s’”.
Here is my configuration:
Robi_Etage.map

389001024771=EG
389001025060=OG
EG_WG=16
EG_KU=17
EG_WZ=18
EG_WC=19
EG_FL=20
EG_Komlett=16,17,18,19,20
OG_JO=17
OG_JA=16
OG_SCH=19
OG_BD=20
OG_FL=18
OG_Komlett=16,17,18,19,20
Esstisch=3.8,3.0,2.1,2.9,1
Couch=4.0,-2.5,2.1,2.4,1
grundreinigung=esstisch,couch
=UNDEF

robi.rules:

// this is to en- or disable  debugging of a specific set of rules. 
val logName = "Robi" // 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 XiaomiRobotVacuum_RoomMapping changed
then
	// update the current floor	
	Robi_Etage.postUpdate(transform("MAP", "Robi_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
	XiaomiRobotVacuum_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
		XiaomiRobotVacuum_RoomMapping.postUpdate("[]")
	 ])
	
end 

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

	val ruleLogName = "Go to your room " 
	if (loggingInfo == true ) logInfo(logName, ruleLogName + "Triggered; Robi_Etage.state = " + Robi_Etage.state )
	
	if ( Robi_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  ( (XiaomiRobotVacuum_ControlVacuum.state != "pause" ) &&  (XiaomiRobotVacuum_ControlVacuum.state != "dock" ) ) {
			XiaomiRobotVacuum_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 == Robi_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", "Robi_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
					XiaomiRobotVacuum_VacuumRoomroom.sendCommand(RaumNummer)
				 ])
			}
		} else {
			if (loggingInfo == true ) logInfo(logName, ruleLogName + "Falsche Etage. Ist= " + Robi_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", "Robi_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", "Robi_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)
	XiaomiRobotVacuum_ExecuteRPCcloudCommand.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 Robi_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

I have created all the items as described.
All switches have the same principle as described (HH_EG_KU etc.). If I set a switch to on, it is not set to off afterwards.

About your help I would be very grateful

I was able to solve the problem. I had created the group incorrectly.
Now I have the problem, when I want to clean a zone, the vacuum goes offline. Is there something wrong with the conversion of the coordinates?
Map:

Esstisch=3.8,3.0,2.1,2.9,1

state of the item: app_zoned_clean[[29300,28500,31400,31400,1]]

currently I cannot help you. I had to re-installed and now I am also not able to update the binding to snapshot anymore. Apparently the snapshot-version needs 3.1

Therefore I am stuck with the old binding and cannot use my own rule.

How dd you solve the problem regarding installing the binding?

Unfortunately there are some breaking changes in the dependencies (the gson library version is changed in oh3.1) which is causing that indeed the 3.1 snapshot version binding is not usable in 3.0.

I am not (yet) brave enough to install snapshot 3.1 :slight_smile:

Perhaps someone installed the binding prior to the breaking change and/or can provide a link to the older Version? That would help me :innocent:

[Edit] I felt lucky today and now I am on M4. Everything works now

can you show the log file?

Please ensure, that in the third line of the rule, you configure:

var loggingInfo = true

in order to see what is happening.

i have completely updated openhab to snapshot

here is the log:

2021-05-05 21:27:27.098 [INFO ] [org.openhab.core.model.script.Rocky ] - Go to your room Los gehts zu Zone Esstisch

2021-05-05 21:27:27.106 [INFO ] [org.openhab.core.model.script.Rocky ] - Vacuum command: Esstisch

2021-05-05 21:27:27.125 [INFO ] [org.openhab.core.model.script.Rocky ] - Clean Esstisch zone coordinates [[29700,28500,32000,31700,1]]

and then the vacuum goes offline:

2021-05-05 21:27:27.853 [INFO ] [ab.event.ThingStatusInfoChangedEvent] - Thing 'miio:vacuum:64ee8a7d53' changed from ONLINE to OFFLINE (COMMUNICATION_ERROR)

the excerpt from the map looks like this:

Esstisch=4.2,3.0,2.3,3.2,1

and the item is called:
HH_EG_ZO_Esstisch

Karte

my card looks like this

Might be the binding… Perhaps @marcel_verpaalen can help you.

Just to recap:

item:

String Rocky_ExecuteCommand   ... linked to miio:vacuum:xxxxxxx:actions#rpc

command

Rocky_ExecuteCommand.sendCommand("app_zoned_clean[[29700,28500,32000,31700,1]]")

result

Thing 'miio:vacuum:xxxxxxx' changed from ONLINE to OFFLINE (COMMUNICATION_ERROR)

That is correct!
Do I have to select “Cloud” for Communication Method in the thing?
When I do this, the vacuum goes offline.

No, the cloud option should be avoided if possible. Local communication is faster, more secure and more stable. Only if there is real need for cloud connectivity (e.g. your device is in different network, or does not support direct (e.g. gateway v3)) then you would enable cloud connectivity.

Now it works. I have entered my access data for the cloud in the configuration of the binding and selected “Cloud” for the communication method.

Edit: Communication method “Cloud” does not have to be selected.