Xiaomi Cloud Vacuum Map Extractor

You can also check out these issues: #157, #180

I think you are right!
After several cleanups rooms appeared:

Earlier there were no rooms:

And the most important thing - I do not observe misalignment so far!

Here are some rules to provide same mutual alignment between a floorplan & a vacuum’s map:

  1. Do not shift a charging dock.
  2. Enable “Save map” feature (post).
  3. Do not start cleaning by a vacuum’s “hardware” button.
  4. Do not start cleaning by using a vacuum.start service.
  5. Always start cleaning by using a “zone cleaning” (use this card) or a “segment cleaning” feature by calling this service:
service: vacuum.send_command
service_data:
  entity_id: '[[SENSOR_VACUUM]]'
  command: app_segment_clean
  params: [2, 3, 16, 5, 17]

Room numbers may be retrieved from a vacuum’s map attributes.

Piotr, thank you again for the job!

@ 3_14
Piotr, may be you know:
is it possible to use a template for the params option?

service: vacuum.send_command
service_data:
  entity_id: '[[SENSOR_VACUUM]]'
  command: app_segment_clean
  params: [2, 3, 16, 5, 17]

I would like to generate the params option dynamically dependingly on which rooms are selected for cleaning & how many passes are selected for each room.
I know that there is fantastic Vacuum Interactive Map Card - but so far I cannot integrate it to my floorplan and wanted to select rooms from UI.

Where do you want to achieve it? It should be possible to do in a script

To achieve it:

  1. Create 3 set of entities:
  • input_boolean for each room - whether to add the room to the “cleaning list” or not;
  • input_number for each room - for keeping / changing room numbers (taken from camera);
  • input_number for each room - for keeping counts of passes (1…3, for example).
  1. Create a group containing these input_boolean entities.

  2. Create template sensor:

sensor:
  - platform: template
    sensors:
      vacuum_clean_command:
        value_template: >-
          {% set ns = namespace(COMMAND = "") -%}
          {%- set ROOM_FLAGS = expand('group.vacuum_clean_rooms') -%}

          {%- for flag in ROOM_FLAGS -%}
            {%- if flag.entity_id | regex_match("input_boolean.vacuum_clean_room_", ignorecase=False) and
                   is_state(flag.entity_id,'on') -%}
              {%- set ROOM = flag.entity_id.split("vacuum_clean_room_")[1] -%}
              {%- set ROOM_NUMBER = states('input_number.vacuum_clean_room_number_' + ROOM)|int -%}
              {%- set CLEAN_COUNT = states('input_number.vacuum_clean_room_count_' + ROOM)|int -%}
              {%- set ROOM_NUMBER_STRING = (ROOM_NUMBER|string + ",") * CLEAN_COUNT -%}
              {%- set ns.COMMAND = ns.COMMAND + ROOM_NUMBER_STRING -%}
            {%- endif -%}
          {%- endfor -%}

          {%- set ns.COMMAND = (ns.COMMAND)[:-1] -%}
          [{{ ns.COMMAND }}]

where:

  • input_boolean.vacuum_clean_room_* - names for input_boolean entities;
  • input_number.vacuum_clean_room_number_* - names for room numbers;
  • input_number.vacuum_clean_room_count_* - names for counts;
  • group.vacuum_clean_rooms - name of the group.
  1. Create smth like this:
    image
    You make hide this card inside fold-entity-row and expand it when needed.

So, the question is - is it possible to use smth like this:

service: vacuum.send_command
service_data:
  entity_id: '[[SENSOR_VACUUM]]'
  command: app_segment_clean
  params: "{{ states('sensor.vacuum_clean_command') }}"

Yeah, it should be possible to do in a script:

vacuum_rooms:
  sequence:
    - service: vacuum.send_command
      data:
        entity_id: vacuum.xiaomi
        command: app_segment_clean
        params:  "{{ states('sensor.vacuum_clean_command') }}"

or without template sensor:

vacuum_rooms:
  sequence:
    - service: vacuum.send_command
      data:
        entity_id: vacuum.xiaomi
        command: app_segment_clean
        params:  >-
          {% set ns = namespace(COMMAND = "") -%}
          {%- set ROOM_FLAGS = expand('group.vacuum_clean_rooms') -%}

          {%- for flag in ROOM_FLAGS -%}
            {%- if flag.entity_id | regex_match("input_boolean.vacuum_clean_room_", ignorecase=False) and
                   is_state(flag.entity_id,'on') -%}
              {%- set ROOM = flag.entity_id.split("vacuum_clean_room_")[1] -%}
              {%- set ROOM_NUMBER = states('input_number.vacuum_clean_room_number_' + ROOM)|int -%}
              {%- set CLEAN_COUNT = states('input_number.vacuum_clean_room_count_' + ROOM)|int -%}
              {%- set ROOM_NUMBER_STRING = (ROOM_NUMBER|string + ",") * CLEAN_COUNT -%}
              {%- set ns.COMMAND = ns.COMMAND + ROOM_NUMBER_STRING -%}
            {%- endif -%}
          {%- endfor -%}

          {%- set ns.COMMAND = (ns.COMMAND)[:-1] -%}
          [{{ ns.COMMAND }}]
1 Like

Great, will try it next day! Thank you!

@3_14
Piotr, is it possible to support templates for yaml config file?
I think that it would be great to switch on/off displaying rooms etc…

maybe someday :wink:

1 Like

Piotr, I got some questions about your script, could you clarify?
I made a small chart of the script:

1) python_script.vacuum_send_command
   send first 5 segments

if command == 'app_zoned_clean':
     if `there more than 5 segments to clean`
          2) Wait until `vacuum = CLEANING`
          3) Wait until `vacuum != CLEANING`  (waiting for completion of cleaning the first 5 segments)
          4) Pause the vacuum
          5) script.vacuum_send_command_multiple_zones (recursive call of the same script)
             send rest of the segments
     endif
endif

Questions:

  1. What is a format of the params variable? Should it be like
[1, 2, 3, 45, 67, 8]

I see that you provided some checks & parsing & cleanup to ensure that the input data format is valid.

  1. Regarding the python_script.vacuum_send_command:
    According to the script, you are passing first 5 segments to the vacuum - does it mean that the vacuum can process max 5 segments per service call?

  2. According to the py script definition, that params value (i.e. first 5 segments) will be converted to

['1', '2', '3', '45', '67']

before sending to the vacuum. Does it mean that a vacuum accepts this format only?

  1. I think this line is not required for the first call:
    {%- set rest = cleaned | replace(firstBatch, "") | replace("[,[", "[[") -%}

  2. I think that the firstBatch and rest may be defined as variables. Just a speculation, I am still learning and never used variables in scripts so far…

Well, the script has been created over a year ago and some stuff has changed in HA . Right now it should be possible to make this script simpler - I think usage of python script can be avoided.

Answers:

  1. Yes
  2. Yes, as far as I know. You can try to check it in Xiaomi Home (my vacuum doesn’t have rooms)
  3. Actually it should be [1, 2, 3, 45, 67]
  4. Yes, it isn’t. I have left it there for consistency reasons
  5. Yes, the script has been created before variables in script appeared in HA :wink:

Thank you very much for your answers!
Now - about my problem.

I described my approach to generate a command for cleaning segments.
The template sensor with this code

          {% set ns = namespace(COMMAND = "") -%}
          {%- set ROOM_FLAGS = expand('group.vacuum_clean_rooms') -%}

          {%- for flag in ROOM_FLAGS -%}
            {%- if flag.entity_id | regex_match("input_boolean.vacuum_clean_room_", ignorecase=False) and
                   is_state(flag.entity_id,'on') -%}
              {%- set ROOM = flag.entity_id.split("vacuum_clean_room_")[1] -%}
              {%- set ROOM_NUMBER = states('input_number.vacuum_clean_room_number_' + ROOM)|int -%}
              {%- set CLEAN_COUNT = states('input_number.vacuum_clean_room_count_' + ROOM)|int -%}
              {%- set ROOM_NUMBER_STRING = (ROOM_NUMBER|string + ",") * CLEAN_COUNT -%}
              {%- set ns.COMMAND = ns.COMMAND + ROOM_NUMBER_STRING -%}
            {%- endif -%}
          {%- endfor -%}

          {%- set ns.COMMAND = (ns.COMMAND)[:-1] -%}
          {{ ns.COMMAND }}

generates this output:
image
Note the “Result type: list” text.
But using this script

script:
  vacuum_clean_rooms:
    alias: 'vacuum: Clean rooms'
    sequence:
      - service: vacuum.send_command
        data:
          entity_id: vacuum.xiaomi_roborock_s50
          command: app_segment_clean
          params: "{{states('sensor.vacuum_clean_command')}}"

gives an error:
Unable to send command to the vacuum: {'code': -10000, 'message': 'data for segment is not a number'}
Tried to discuss this issue, no solution so far.
Using the code directly in script (w/o that template sensor like you proposed before) does not help…

I started learning your script to find answers, not succeeded so far.

Can you try using service xiaomi_miio.vacuum_clean_segment?

service: xiaomi_miio.vacuum_clean_segment
data:
  entity_id: vacuum.xiaomi_roborock_s50
  segments: "{{states('sensor.vacuum_clean_command')}}"

Error message:
Failed to call service xiaomi_miio.vacuum_clean_segment. expected int for dictionary value @ data['segments']. Got None
image

:confused: it seems like a bug in HA template engine… So probably the only way is to launch it via python script

I haven’t played with that for a long time, but last time I checked, the segments were not “templatable”.

There is a service input_select.set_option which accepts a list.
A correct syntax with a template is:

service: input_select.set_options
target:
  entity_id: input_select.test_input_select_templatable
data:
  options: "{{['one','two','three','four']}}"

image
Calling a service for vacuum with the same format

entity_id: vacuum.xiaomi_roborock_s50
command: app_segment_clean
params: "{{['2','3','3']}}"

causes same error
Unable to send command to the vacuum: {'code': -10000, 'message': 'data for segment is not a number'}
I think that the problem is in the service itself - it accepts a number, not a string.
I tried this call:

entity_id: vacuum.xiaomi_roborock_s50
command: app_segment_clean
params: "{{[2|int,3|int,3|int]}}"

it works!!!

1 Like

Soooo, maybe this will work as well?

          ...
          {%- set ns.COMMAND = (ns.COMMAND)[:-1] -%}
          {{ ns.COMMAND | from_json }}

Made this, gonna test it after completion of the started cleaning:

          params: >-
            {% set ns = namespace(COMMAND = "") -%}
            {%- set ROOM_FLAGS = expand('group.vacuum_clean_rooms') -%}

            {%- for flag in ROOM_FLAGS -%}
              {%- if flag.entity_id | regex_match("input_boolean.vacuum_clean_room_", ignorecase=False) and
                    is_state(flag.entity_id,'on') -%}
                {%- set ROOM = flag.entity_id.split("vacuum_clean_room_")[1] -%}
                {%- set ROOM_NUMBER = states('input_number.vacuum_clean_room_number_' + ROOM)|int -%}
                {%- set CLEAN_COUNT = states('input_number.vacuum_clean_room_count_' + ROOM)|int -%}
                {%- set ROOM_NUMBER_STRING = (ROOM_NUMBER|string +"|int" + ",") * CLEAN_COUNT -%}
                {%- set ns.COMMAND = ns.COMMAND + ROOM_NUMBER_STRING -%}
              {%- endif -%}
            {%- endfor -%}

            {%- set ns.COMMAND = (ns.COMMAND)[:-1] -%}
            [{{ ns.COMMAND }}]

image

Actually, I have never used from_json things, need some time for testing…


Update:
Because of unknown reasons the script above did not work - same error as before…
So I went back to the previous version with this expression:
[{{ ns.COMMAND }}]
and without this expression:
+"|int"

Code:

  vacuum_clean_rooms:
    alias: 'vacuum: Clean rooms'
    sequence:
      - service: vacuum.send_command
        data:
          entity_id: vacuum.xiaomi_roborock_s50
          command: app_segment_clean
          params: >-
            {% set ns = namespace(COMMAND = "") -%}
            {%- set ROOM_FLAGS = expand('group.vacuum_clean_rooms') -%}
            {%- for flag in ROOM_FLAGS -%}
              {%- if flag.entity_id | regex_match("input_boolean.vacuum_clean_room_", ignorecase=False) and
                    is_state(flag.entity_id,'on') -%}
                {%- set ROOM = flag.entity_id.split("vacuum_clean_room_")[1] -%}
                {%- set ROOM_NUMBER = states('input_number.vacuum_clean_room_number_' + ROOM)|int -%}
                {%- set CLEAN_COUNT = states('input_number.vacuum_clean_room_count_' + ROOM)|int -%}
                {%- set ROOM_NUMBER_STRING = (ROOM_NUMBER|string +",") * CLEAN_COUNT -%}
                {%- set ns.COMMAND = ns.COMMAND + ROOM_NUMBER_STRING -%}
              {%- endif -%}
            {%- endfor -%}
            {%- set ns.COMMAND = (ns.COMMAND)[:-1] -%}
            [{{ ns.COMMAND }}]

Probably earlier I started the script before it was actually updated in HA after correction.
So to be sure I restarted HA - and the script works!

Now everything looks OK:
expanded control panel:
image

expanded settings with restricted access:
image

1 Like

hey, I’m new in HA so sorry for probably stupid questions. questions are related to both Xiaomi Cloud Vacuum Map Extractor and Lovelace Xiaomi Vacuum Map card:

  1. it is nowhere said how vacuum itself should be integrated in HA (to get entity for Lovelace Xiaomi Vacuum Map card). I understand, that probably it is out of scope, but for new users it can be very helpful. tried first via Vacuum integration, but entity didn’t appear. then tried via Xiaomi Miio and finally get entity. but this integration cannot be configured via config. is it correct way?
  2. how can I clean specific rooms? example in config is based only on coordinates, not sure if it is proper way, because in MiHome I can clean specific rooms with complex shapes.
  3. how can I get room numbers? there is said in the docs add rooms/room_numbers attribute - and what next? in tons of comments I found that I should check it in dev tools. how? what should I do to see it? https://i.imgur.com/OECBfta.png why docs is so not new-user-friendly :frowning:
  4. I have HA in russian, obviously set russian language in Lovelace Xiaomi Vacuum Map card, but bottom row still has mixed texts from english and russian. is it a bug? missing translation? misconfiguration? https://i.imgur.com/XJyWlIE.png

thanks in advance.

1 Like