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 (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):
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…
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 …
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
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
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.
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.
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.
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
Item 'Rocky_ExecuteCommand' changed from get_multi_map [1] to {"code":0,"exe_time":145,"id":1057,"otlocalts":1613071961310518,"result":["roboroommap%2F322134263%2F20"]}
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.
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.
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 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.
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
*/
// 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