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