Ability to use Shuffle to randomly order script sequence

Hi there,

I’ve made a script for Home Assistant which sets up my Vacuum cleaner to mop specific rooms. I want to have another script which can run all the specific room mopping scripts, but in a random order (so that the water in the reservoir will run out at different spots instead of the same place each time).

Each room-mopping script looks like this:

alias: Mop Bedroom
sequence:
  - service: vacuum.set_fan_speed
    data:
      fan_speed: Gentle
    entity_id: vacuum.xiaomi_vacuum_cleaner
  - service: vacuum.send_command
    data:
      command: app_zoned_clean
      params:
        - - 29144
          - 24140
          - 31928
          - 29208
          - 1
    entity_id: vacuum.xiaomi_vacuum_cleaner
mode: queued
icon: 'mdi:bed'
max: 10

As an initial implmentation, I’d like to get something like this working (string list should probably become a group eventually):

alias: Mop Everywhere
sequence:
{% set services = [ "scripts.mop_kitchen" "scripts.mop_hallway", "scripts.mop_bedroom", "scripts.mop_living_room"] %}
{% for srv in services | shuffle %}
 - service: {{srv}}
   data: {}
{% endfor %}
mode: queued
max: 3

Unfortunately the shuffle command is not available, and I can’t import random in order to provide it. Am I thinking about this wrong? Is there a more idiomatic way of handling this?

I had a need to shuffle a list of colors (for use with pool lights). I think the same shuffling technique will also serve your requirement.

Paste this into the Template Editor and see if it meets your expectations:

{% set services = [ "scripts.mop_kitchen", "scripts.mop_hallway",
                    "scripts.mop_bedroom", "scripts.mop_living_room"] %}

{% set ns = namespace(x = services) %}
{% for i in range(ns.x | length - 1, 0, -1) %}
    {% set j = range(0, i + 1) | random %}
    {% if j != i %}
      {% set ns.x = ns.x[:j]+[ns.x[i]]+ns.x[j+1:i]+[ns.x[j]]+ns.x[i+1:] %}
    {% endif %}
{% endfor %}
{{ ns.x }}


EDIT

If you’re interested, the example I provided implements the Fisher-Yates shuffle algorithm.

1 Like

Fisher-Yates! I’ve not heard of that one before, thank you :slight_smile:

With a few minor modifications:

{% set services = [ "scripts.mop_kitchen", "scripts.mop_hallway",
                    "scripts.mop_bedroom", "scripts.mop_living_room"] %}

{% set ns = namespace(x = services) %}
{% for i in range(ns.x | length - 1, 0, -1) %}
    {% set j = range(0, i + 1) | random %}
    {% if j != i %}
      {% set ns.x = ns.x[:j]+[ns.x[i]]+ns.x[j+1:i]+[ns.x[j]]+ns.x[i+1:] %}
    {% endif %}
{% endfor %}

alias: Mop Everywhere (Random Order)
sequence: {% for room in ns.x %}
 - service: {{ room }}
   data: {}{% endfor %}
mode: queued
max: 3

The template produces valid yaml, however when I paste the above into a script (inserting as yaml, naturally), then I get the following error when I attempt to save:

Message malformed: required key not provided @ data['sequence']

For Home Assistant, this is invalid YAML:

alias: Mop Everywhere (Random Order)
sequence: {% for room in ns.x %}
 - service: {{ room }}
   data: {}{% endfor %}

I believe you are trying to iterate through the shuffled list but it cannot be written like that.

I know how to sequentially call each script in the shuffled list. However, I have my doubts your robot vacuum cleaner will work the way you expect.

Try this experiment:

  1. Go to Configuration > Scripts
  2. Execute all four mopping scripts in any order.
  3. Don’t wait for the robot to complete a room before executing the next mopping script. Simply execute all four scripts.

What does your robot vacuum do when given four different rooms to do at once? Does it do them in the order they were received or does it simply do the last one it received?

Did you have time to try the experiment I suggested?

My suspicion is that you cannot execute four mopping scripts in rapid succession. I may be wrong but I think you will have to wait for the vacuum to finish cleaning a room before you can execute the next script to clean another room.

Apologies for not getting back sooner - Christmas/New Year happened, and I’ve been unwell (not covid) for a while as well.

So I tried this last night, and exactly what I assume you were expecting to happen happened: that is, the script will skip through each command and exit without waiting for any task to complete.

I have figured out how to make it wait until the cleaning has completed:

wait_template: '{{ is_state("vacuum.xiaomi_vacuum_cleaner", "returning") }}'

However when I try to combine this with the use of templates, the is_state method is interpolated at the point where the template is compiled, and not at the point that the script is evaluated - so the wait will never resolve to true. I’m unsure how I might signal to jinja that the wait_template value should not be compiled yet?

That aside, this is the script I ended up with, and this does indeed wait for each task to complete before moving on to the next one:

alias: Mop Everywhere
sequence:
  - service: script.mop_bedroom
    data: {}
  - wait_template: '{{ is_state(''vacuum.xiaomi_vacuum_cleaner'', ''returning'') }}'
  - service: script.mop_living_room
    data: {}
  - wait_template: '{{ is_state(''vacuum.xiaomi_vacuum_cleaner'', ''returning'') }}'
  - service: script.mop_hallway
    data: {}
mode: queued
icon: 'mdi:broom'
max: 10

As expected, you must wait for it to finish cleaning a room before instructing it to start cleaning another room.

In that script, I don’t see the use of the shuffling behavior you had requested. Have you decided you no longer need it?

Yes, the script I shared below does not have the shuffling behaviour because I could not get it to validate cleanly, but I still want to mop my floor :smiley:

I shared that script so that it was clear that a resultant YAML such as this would fit requirements, I just need to figure out how to get the shuffling to produce YAML like this.

I hope that’s clearer :slight_smile:

If I try to add the following script:

{% set services = [ "scripts.mop_kitchen", "scripts.mop_hallway",
                    "scripts.mop_bedroom", "scripts.mop_living_room"] %}

{% set ns = namespace(x = services) %}
{% for i in range(ns.x | length - 1, 0, -1) %}
    {% set j = range(0, i + 1) | random %}
    {% if j != i %}
      {% set ns.x = ns.x[:j]+[ns.x[i]]+ns.x[j+1:i]+[ns.x[j]]+ns.x[i+1:] %}
    {% endif %}
{% endfor %}

alias: Mop Everywhere (Random Order)
sequence:
{% for room in ns.x %}
 - service: {{ room }}
   data: {}
 - wait_template: {{ is_state('vacuum.xiaomi_vacuum_cleaner', 'returning') }}
{% endfor %}
mode: queued
max: 3

The home assistant UI returns Message malformed: Integration '' not found

Try this script:

random_room_mopping:
  alias: Random room mopping
  sequence:
  - variables:
      rooms: >
        {% set services = [ "scripts.mop_kitchen", "scripts.mop_hallway",
                            "scripts.mop_bedroom", "scripts.mop_living_room"] %}

        {% set ns = namespace(x = services) %}
        {% for i in range(ns.x | length - 1, 0, -1) %}
            {% set j = range(0, i + 1) | random %}
            {% if j != i %}
              {% set ns.x = ns.x[:j]+[ns.x[i]]+ns.x[j+1:i]+[ns.x[j]]+ns.x[i+1:] %}
            {% endif %}
        {% endfor %}
        {{ ns.x }}
  - repeat:
      count: '{{ rooms | length }}'
      sequence:
      - service: '{{ rooms[repeat.index-1] }}'
      - wait_template: "{{ is_state('vacuum.xiaomi_vacuum_cleaner', 'returning') }}"

I can’t test it because I don’t have a robot vacuum. However, I did test the following script, which simply posts a notification, and it worked correctly.

Click to show test script
mopping_test:
  alias: Mopping Test
  sequence:
  - variables:
      rooms: >
        {% set services = [ "scripts.mop_kitchen", "scripts.mop_hallway",
                            "scripts.mop_bedroom", "scripts.mop_living_room"] %}
        {% set ns = namespace(x = services) %}
        {% for i in range(ns.x | length - 1, 0, -1) %}
            {% set j = range(0, i + 1) | random %}
            {% if j != i %}
              {% set ns.x = ns.x[:j]+[ns.x[i]]+ns.x[j+1:i]+[ns.x[j]]+ns.x[i+1:] %}
            {% endif %}
        {% endfor %}
        {{ ns.x }}
  - repeat:
      count: '{{ rooms | length }}'
      sequence:
      - service: persistent_notification.create
        data:
          title: '{{ repeat.index-1 }}'
          message: '{{ rooms[repeat.index-1] }}'
      - delay: "00:00:05"

Success! If I try to use the UI to create this script, it fails to validate (even when I remove the random_room_mopping key), but it does indeed work as expected when I insert it into the scripts.yml

Thank you very much for taking the time to help me with this!

1 Like

Sorry, I can’t help you with that because I never use the Script Editor.

You’re welcome!

Please consider marking my post (above) with the Solution tag. It will automatically place a check-mark next to the topic’s title, signaling to other users that this topic has an accepted solution. It will also place a link below your first post that leads directly to the Solution post. All of this helps users find answers to similar questions.

Done! Thanks again :slight_smile:

For reference, and the sake of completion:

groups.yml:

mopping_scripts:
    name: Mopping Scripts
    entities:
      - script.mop_bedroom
      - script.mop_hallway
      - script.mop_living_room
      - script.mop_kitchen

scripts.yml:

mop_living_room:
  alias: Mop Living Room
  sequence:
  - service: vacuum.set_fan_speed
    data:
      fan_speed: Gentle
    entity_id: vacuum.xiaomi_vacuum_cleaner
  - service: vacuum.send_command
    data:
      command: app_zoned_clean
      params:
      - - 22185
        - 24097
        - 26252
        - 29099
        - 1
    entity_id: vacuum.xiaomi_vacuum_cleaner
  mode: queued
  icon: mdi:television-classic
  max: 10

random_room_mopping:
  alias: Mop Everywhere (Random Order)
  sequence:
  - variables:
      rooms: >
        {% set ns = namespace(x = expand('group.mopping_scripts')) %}
        {% for i in range(ns.x | length - 1, 0, -1) %}
            {% set j = range(0, i + 1) | random %}
            {% if j != i %}
              {% set ns.x = ns.x[:j]+[ns.x[i]]+ns.x[j+1:i]+[ns.x[j]]+ns.x[i+1:] %}
            {% endif %}
        {% endfor %}
        {{ ns.x }}
  - repeat:
      count: '{{ rooms | length }}'
      sequence:
      - service: '{{ rooms[repeat.index-1] }}'
      - wait_template: '{{ is_state(''vacuum.xiaomi_vacuum_cleaner'', ''returning'') }}'
  mode: queued
  icon: mdi:broom
  max: 10

Update: I moved to using Valetudo, and now I use the following script to mop all “segments” in random order, making sure to leave out any segments that I don’t want to mop (carpeted rooms).

  • The ignored segment in this case is number 18.
  • All the segment configuration comes from Valetudo directly.
  • I used the notification section to prove that the order is randomised - you will probably want to remove this if you plan on using it seriously.
  • My vacuum is called henry, make sure the MQTT topic is correct for your device.
mop_everywhere_random_order:
  mode: queued
  icon: mdi:broom
  max: 10
  alias: Mop Everywhere (Random Order)
  sequence:
  - variables:
      segments: "{#- Use the Fisher-Yates shuffle algorithm -#} {#- https://www.geeksforgeeks.org/shuffle-a-given-array-using-fisher-yates-shuffle-algorithm/\
        \ -#} {%- set ns = namespace( x = (expand('sensor.map_segments')[0].attributes\
        \ | list), y = [], denylist = [18]) -%} {%- for\
        \ i in range(ns.x | length - 1, 0, -1) %}\n  {%- set j = range(0, i + 1) |\
        \ random %}\n  {%- if j != i %}\n    {%- set ns.x = ns.x[:j]+[ns.x[i]]+ns.x[j+1:i]+[ns.x[j]]+ns.x[i+1:]\
        \ %}\n  {%- endif %}\n{%- endfor %} {#- Remove the non-numeric and denylisted\
        \ elements from the list -#} {%- for i in range(ns.x | length - 1, 0, -1)\
        \ %}\n  {%- if ns.x[i] | int != 0 and ns.x[i] | int not in ns.denylist %}\n\
        \    {%- set ns.y = ns.y + [ns.x[i] | int] %}\n  {%- endif %}\n{%- endfor\
        \ %} {{ ns.y }}\n"
  - service: notify.mobile_app_iphone_12_pro
    data:
      message: Cleaning in random order {{ segments }}
      title: Henry
  - service: mqtt.publish
    data:
      topic: valetudo/henry/MapSegmentationCapability/clean/set
      payload: '{"segment_ids": {{ segments }}, "customOrder": true }'
1 Like

This is great, thanks for this (and to Taras too obviously).

One thing I noticed from my own use though: when your script iterates through the randomised list the second time to remove the non-integer elements, I believe the way you have it at the moment means it doesn’t include the last element in the randomised list - I therefore changed it to simply iterate upwards using the length of the list as the iteration range i.e. {%- for i in range(ns.x | length) %}.

The other thing is that HA complains about not setting a default value for int, so I also added that: if ns.x[i] | int(0) != 0 etc.

But really appreciate what you posted here, and it solved a real headscratcher for me.

1 Like