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

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