[Tutorial] Bash and robot vacuum on Valetudo

Hello,

I propose a tutorial on sending data from a Roborock S51 robot vacuum with Valetudo to Gladys via MQTT.

If your robot is not compatible with Valetudo, the code can be adapted to your needs; it will serve as a basis.


TUTORIAL #4: Managing your robot vacuum with Valetudo in Gladys.
EDIT 28/06/2023: Complete overhaul of the RobotAspiToGladys.sh file code, simplifying its use
EDIT 25/06/2023: Code modification (JsonArrayToString, etc.), information on Valetudo RE, and the card


Robot Vacuum Side:
You need to install Valetudo RE on it.

Valetudo (for the installation part): https://valetudo.cloud/pages/general/getting-started.html
Valetudo RE: GitHub - rand256/valetudo: Valetudo RE - experimental vacuum software, cloud free · GitHub
Valetudo RE MQTT Commands: MQTT API · rand256/valetudo Wiki · GitHub

I had to configure the MQTT connection in Valetudo RE.
URL: mqtt://gladys:XYZ@192.168.XXX.XXX:1883

I didn’t change the default configuration:
Username: rockrobo
Topic Prefix: valetudo
Autoconfiguration Prefix: homeassistant


BASH Side:

  1. Shared file: /opt/mosquitto/MQTTConfig.sh

I will use the shared file proposed with my other tutorials: [TUTO] Partage d'informations entre BASH et Gladys
It allows testing the presence of commands, sharing Gladys connection variables, and adding color to messages.

You need to add the following code in the « Customizable Variables » block:

# Gladys topic shortcuts
GladysSet="gladys/master/device"
GladysGet="gladys/device"

# Shortcuts to MQTT devices
AspiRobot="mqtt:salle:aspirobot/feature/mqtt:aspirobot"

Adapting AspiRobot with your topic.

It also seems necessary to modify the ORANGE variable to:

ORANGE="\e[1m\e[38;5;202m"
  1. BASH Script: /opt/mosquitto/RobotAspiToGladys.sh

The idea is to create a loop that monitors the arrival of information to display it in Gladys.
The script will send all the information it finds to Gladys, which will only update the displayed ones.
jq will fill an associative array with all the keys and values it finds for each topic.
This notion of topic is important as it will be used in Gladys.

It is possible to customize the values (date format, for example) and the types (text or state).
You need to look a bit at the code which is normally quite detailed.

You will need to automate its launch at the startup of the pc/raspberry.

Requires the commands mosquitto_pub and mosquitto_sub (debian package mosquitto-clients) as well as jq (debian package jq).

#!/bin/bash

#########################################################
## Sending robot vacuum information to Gladys ##
#########################################################
# Requires packages: mosquitto-clients jq
# To be executed at startup, its infinite loop will continuously monitor what is happening

# Loading the common file
source /opt/mosquitto/MQTTConfig.sh

# Checking for the presence of dependencies
! CommandCheck "mosquitto_pub:mosquitto-clients" "mosquitto_sub:mosquitto-clients" "jq:jq" && exit 1

### Variables to customize

# Shortcut variable defined in Valétudo
ValeRock="valetudo/rockrobo"

# Variables for concatenation into text when retrieving values in json array format
ValueSeparator=", "
LineSeparator="@"

### Variables to customize

# Calculations for creating the table
TerminalWidth=$(($(tput cols) - 2 - 54))
printf -v ColumnValueDash '─%.0s' $(seq 1 ${TerminalWidth})
printf -v ColumnValueSpace ' %.0s' $(seq 1 $(( ${TerminalWidth} - 8 )))

# Displaying the table headers
echo "Base topic: ${GladysSet}/${AspiRobot}:"
echo "┌─────────────────────────────────────────────┬───────┬${ColumnValueDash}┐"
echo -e "│ ${ORANGE}Topic                                      ${RAZ} │ ${FUCHSIA}Type${RAZ}  │ ${BLEUFONCE}Value${RAZ} ${ColumnValueSpace}│"
echo "├─────────────────────────────────────────────┼───────┼${ColumnValueDash}┤"



# Descending function in objects {} and adds keys and sub-keys in an array with its value
function AutoJsonReader()
{
    # $1: Topic / Mother object
    # $2: JSON code
    # AllValues: Name of the global variable that will be filled

    # Defining local variables
    local Key Value Line
    
    # Using NULL as separators, the -j allows not to have a line break between entries
    mapfile -td '' Lines < <(jq -rj 'to_entries[] | "\(.key)@@@\(.value)\u0000"' <<< "${2}")

    # Looping through the JSON code and returning key@@@value
    for Line in "${Lines[@]}"
    do
        # Key value with addition of the mother key if present
        Key="${1}${Line%%@@@*}"

        # Key value
        Value="${Line##*@@@}"

        # If the value is an object {}
        if [[ "${Value:0:1}${Value: -1:1}" == '{}' ]]
        then
            # Restart the function with this code to fill the sub-keys
            AutoJsonReader "${Key}." "${Value}"

        else
            # Adding the (sub-)key and its value to the array
            AllValues["${Key}"]="${Value}"
        fi
    done
}



# Function converting a json array to text
# The array can contain others but no {}
function JsonArrayToString()
{
    # Local variables
    local NewString Line

    # If the array contains a sub-array
    if [[ "${1:0:2}" == '[[' ]]
    then
        # Loop processing arrays one by one
        while read Line
        do
            # Calling the function with a sub-array and retrieving its text value
            NewString+="$(JsonArrayToString "${Line}")${LineSeparator:-@}"
        done < <(jq -c '.[]' <<< "${1}")

        # Returning the final text with removal of the last LineSeparator
        echo "${NewString/%${LineSeparator:-@}}"

    elif [[ "${1:0:1}" == "[" ]]
    then
        # Converting the array to text
        NewString=$(jq -r --arg Separator "${ValueSeparator:-:}" 'join($Separator)' <<< "${1}")

        # Returning the text either final or of a sub-array
        echo "${NewString}"
    fi
}


# Looping through the information sent by the robot vacuum
# ${ValeRock}/map_data not used, replaced by the "camera"
while read Topic JSONCode
do
    # Only keeping the last level of the topic for the case
    # ex: valetudo/rockrobo/state => state
    Topic="${Topic##*/}"

    # Cleaning the array
    unset AllValues
    declare -A AllValues

    # Filling the array
    AutoJsonReader "${Topic}:" "${JSONCode}"

    # In the case of the error field, it is only returned if it is the case, so I give it a default value
    if [[ ${Topic} == "state" ]]
    then
        [[ -z ${AllValues["state:error"]} ]] && AllValues["state:error"]="No error"
    fi

    # Looping through the field names by retrieving.
    for Field in "${!AllValues[@]}"
    do
        # Retrieving the corresponding value.
        Value="${AllValues[${Field}]}"

        # Reprocessing the value of some information, just use their Topic as a key
        case "${Field}" in
            # Date type format
            "attributes:last_bin_out"|"attributes:last_bin_full"|"command_status:updated"|"attributes:last_run_stats.endTime"|"attributes:last_run_stats.startTime")
                Value=$((Value / 1000 ))
                Value=$(date +'%d/%m/%Y' -d "@$Value") ;;

            "attributes:last_run_stats.duration") Value=$((Value / 60 )) ;;
        esac

        # Text type by default
        ValueType="text"
        TypeSpace=" "

        # If you want to force a text type, just add keys separated by | in the @()
        if [[ "${Field}" != @() ]]
        then
            # If the type is numeric, it is passed to state
            [[ "${Value}" == ?(+|-)+([0-9])?(.|,)*([0-9]) ]] && { ValueType="state"; unset TypeSpace; }
        fi

        # Does not process null values (not found)
        if [[ ${Value} != "null" ]]
        then
            # Converting json arrays to text if the value starts with a [ and ends with a ]
            [[ "${Value:0:1}${Value: -1:1}" == '[]' ]] && Value="$(JsonArrayToString "${Value}")"

            # Calculations for the table
            printf -v FieldSpace ' %.0s' $(seq 1 $(( 43 - ${#Field} )))
            printf -v ValueSpace ' %.0s' $(seq 1 $(( ${#ColumnValueSpace} - ${#Value} + 7)))

            # Displaying information in the terminal in a table
            echo -e "│ ${ORANGE}${Field}${RAZ}${FieldSpace} │ ${FUCHSIA}${ValueType}${RAZ}${TypeSpace} │ ${BLEUFONCE}${Value}${RAZ}${ValueSpace}│"

            # Sending the value of the correct topic to Gladys
            # Using shortcuts to define the entire topic
            # It is important to properly configure the MQTT devices in Gladys to have the correct Topic format
            mosquitto_pub -u "${User}" -P "${Pass}" -t "${GladysSet}/${AspiRobot}:${Field}/${ValueType}" -m "${Value}"

            # Retrieving the return value of the mosquitto_pub command
            MosquittoReturns=${?}

            # Displaying a message in case of mosquitto error
            (( ${MosquittoReturns} )) && echo -e "[${ROUGE}Error${RAZ}] The mosquitto_sub command returned code ${MosquittoReturns}." 1>2
        fi
    done

    # Separator between topics
    echo "├─────────────────────────────────────────────┼───────┼${ColumnValueDash}┤"
done < <(mosquitto_sub -v -u "${User}" -P "${Pass}" -t ${ValeRock}/state \
                                                    -t ${ValeRock}/attributes \
                                                    -t ${ValeRock}/command_status \
                                                    -t homeassistant/vacuum/valetudo_rockrobo/config)

If you run it, it will display for example:
bash RobotAspiToGladys.sh


The topics are important, you will need to indicate them to Gladys in the following steps.

  1. BASH Script: /opt/mosquitto/GladysToRobotAspi.sh

It is possible to send MQTT commands to Valetudo.
In the following code, I propose 3 actions that will be launched when I press a Xiaomi button.
1 click (code 1): start
2 clicks (code 2): stop
long click (code 5): return to base

To change the actions, simply modify the Actions variable.

You will need to automate its launch at PC/Raspberry startup.

Requires the mosquitto_pub and mosquitto_sub commands (Debian package mosquitto-clients).

#!/bin/bash

#####################################################################################################
## Monitoring of the special vacuum cleaner button and triggering of the appropriate actions of the vacuum cleaner ##
#####################################################################################################
# Requires the package: mosquitto-clients

# Loading the common file
source /opt/mosquitto/MQTTConfig.sh

# Checking the presence of commands
! CommandCheck "mosquitto_pub:mosquitto-clients" "mosquitto_sub:mosquitto-clients" && exit 1

### Variables to customize

# List of actions, correspondence of MQTT codes and commands
Actions=(null start stop pause locate return_to_base clean_spot)

### Variables to customize

# Infinite loop to retrieve the value of the MQTT button
while read Value
do
    # Blocking if the sent code is unknown
    if [[ ${Value} != @([1-6]) ]]
    then
        echo -e "[${ORANGE}Warning${RAZ}] Value ${Value} unknown..." 1>&2
        continue
    fi

    # Retrieving the command associated with the button code
    Action="${Actions[${Value}]}"

    # Sending the command to the vacuum cleaner via the broker
    echo -e "Executing the action '${BLEUFONCE}${Action}${RAZ}'."
    mosquitto_pub -u "${User}" -P "${Pass}" -t valetudo/rockrobo/command -m ${Action}

    # Retrieving the return value of the mosquitto_pub command
    MosquittoReturns=${?}

    # Display a message in case of mosquitto error
    (( ${MosquittoReturns} )) && echo -e "[${ROUGE}Error${RAZ}] The mosquitto_sub command returned the code ${MosquittoReturns}." 1>&2
done < <(mosquitto_sub -u "${User}" -P "${Pass}" -t "${GladysGet}/${AspiRobot}:action/state")

If you run it, it gives for example:
bash GladysToRobotAspi.sh


Gladys side:

  1. Creating the vacuum cleaner at the MQTT level:

Name: Vacuum Cleaner
External ID: mqtt:room:vacuumrobot (mqtt:Room:Element)
Room: Room

In my case it looks like this:

You need to adapt it to what you put in the AspiRobot variable of the bash file MQTTConfig.sh

  1. Creating the desired features with the correct type:

This step will be long since you need to create all the features to manage with Gladys…

I created blocks to gather the information.
You need to be careful to use the correct feature types so that the received data is compatible.
Battery, text, area or duration…

Examples of external ID formatting for features:
The important thing is to use the topics indicated by the bash script RobotAspiToGladys.sh and give them to Gladys.

In my case it looks like this:

  1. Creating the camera displaying the map:

It is possible to use http://VacuumIP/api/simple_map to retrieve the map, for example:

wget http://VacuumRobotIP/api/simple_map -O "/tmp/VacuumRobotMap.png"

But this one requests an image every minute, which seems heavy for the vacuum cleaner.
It is possible to install GitHub - rand256/valetudo-mapper: Valetudo companion service · GitHub which will generate a map from the MQTT data.
You can then give the address http://serverIP:3000/api/map/image (or port 3002?) to the camera type device.

In my case it looks like this:

  1. Adding the different features to the dashboard:

Nothing specific here, you need to proceed as usual.

Personally, here’s what I did:

  1. Managing the action button:

You just need to have a specific feature in the MQTT device (visible in my screenshot)

  • Type: Unknown
  • Name: Action
  • ID: mqtt:vacuumrobot:action
  • Values: from 1 to 10
  • Is it a sensor? No

To have a trigger like a button (VacuumRobot button in my case, with a nice typo :slight_smile: )

To create a scene at the button that will send the action code to the feature above:


The code will be received by the file /opt/mosquitto/GladysToRobotAspi.sh.


Hopefully this can be useful, at least as a base or starting idea for other vacuum cleaners.


Valetudo:
If you use this version, there seem to be a lot of information: https://valetudo.cloud/pages/integrations/mqtt.html#capabilities

Thanks to: Faire communiquer votre aspirateur robot Xiaomi Roborock en MQTT


Other tutorials:

TUTORIAL n°1: Display the Raspberry temperature in Gladys.

TUTORIAL n°2: Launch a BASH command via a Gladys action.

TUTORIAL n°3: Indicate the presence of a user via the WI-FI of their phone.

Argh, I see that I had 2 bash files and I may have used the old version, I’ll need to check that…
I’ll update the code if needed.

And for the image, I’ll need to specify that you need GitHub - rand256/valetudo-mapper: Valetudo companion service

So I’ll update the tutorial.

I love it! I own an S6 — I think I’ll replace it one of these days with a model that has a mop.
Maybe before its retirement I’ll switch it to Valetudo :+1:

Thank you for this great tutorial @Hizo :folded_hands: I’ll share it on Gladys’ social media.

I updated the code with my other version; this mainly adds the JsonArrayToString function and its usage, and the topic destinations which doesn’t exist in my setup.
I just noticed that I had started some work to automatically retrieve all available fields without needing to specify them.
I need to see whether it’s worth it or not…

I added clarifications regarding Valetudo VR and the retrieval of the map.

I had also started some work on using REST API · rand256/valetudo Wiki · GitHub

So much work… :slight_smile:

I have just rewritten the code of the bash file RobotAspiToGladys.sh

It automatically retrieves all the keys and values it finds and sends them to Gladys; it is still possible to process the values to convert them to dates, for example.

It automatically detects the data type (state or text), but it is possible to force it.

It displays a nice table showing the topics, the data types, and the data.