Skill: serdarsalim/roborock-conversational-control (version 1.0.0)

--- name: jojo-bot description: "Turn a Roborock into a conversational OpenClaw companion. Use when the user wants natural-language vacuum control like clean the kitchen, go home, pause, status, or room-specific cleaning, especially in Telegram. This is a higher-level companion layer, not just raw Roborock control. Execute the local shell wrapper in scripts/jojo.sh via exec; do not use ACP or sessions_spawn for routine control actions." version: 1.0.1 user-invocable: true metadata: openclaw: emoji: "🤖" requires: bins: - bash - roborock - python3 --- # Jojo Bot > **Execution rule**: use the `exec` tool to run the shell wrapper in `scripts/jojo.sh`. Do not use `sessions_spawn` or ACP for routine Jojo commands. This skill turns a Roborock vacuum into a conversational device companion. It is designed for: - OpenClaw chat - Telegram bots - room-aware vacuum control - a lightweight Jojo-style persona on top of real device actions It is not a replacement for low-level Roborock tooling. It is the higher-level layer that makes a vacuum feel like its own agent. Use it for: - `clean the kitchen` - `clean the living area` - `clean the house` - `pause` - `go home` - `status` - `which rooms do you know` ## Required local config Create `jojo.env` in this skill folder from `jojo.env.example`. Minimum required value: - `JOJO_DEVICE_ID` Optional: - `JOJO_DEVICE_NAME` - `ROOM_<ALIAS>` mappings such as `ROOM_KITCHEN="17"` ## Wrapper Resolve the skill directory, then call: ```bash bash <skill_dir>/scripts/jojo.sh status bash <skill_dir>/scripts/jojo.sh start bash <skill_dir>/scripts/jojo.sh pause bash <skill_dir>/scripts/jojo.sh stop bash <skill_dir>/scripts/jojo.sh home bash <skill_dir>/scripts/jojo.sh rooms bash <skill_dir>/scripts/jojo.sh room kitchen bash <skill_dir>/scripts/jojo.sh room "master bedroom" ``` ## Intent mapping - `status`, `what are you doing`, `battery`, `where are you` -> `status` - `clean the house`, `start cleaning`, `go clean` -> `start` - `pause`, `wait`, `hold on` -> `pause` - `stop cleaning` -> `stop` - `go home`, `dock`, `charge`, `return to dock` -> `home` - `which rooms`, `what rooms`, `rooms` -> `rooms` - `clean the <room>` -> `room "<room>"` If room intent is ambiguous, ask one short follow-up. ## Room aliases The wrapper supports user-defined aliases through env vars: ```bash ROOM_KITCHEN="17" ROOM_MASTER_BEDROOM="19" ROOM_LIVING_AREA="21" ``` To bootstrap aliases from the live Roborock map: ```bash bash <skill_dir>/scripts/refresh_room_aliases.sh ``` ## Positioning - Use `robo-rock` or the raw `roborock` CLI when you only want direct device control. - Use `jojo-bot` when you want a conversational robot companion with room aliases, short replies, and a dedicated device identity. ## Notes - Current `python-roborock` builds use `roborock login --email ...` - Verified command verbs are `app_start`, `app_pause`, `app_stop`, `app_charge`, and `app_segment_clean` - Keep this skill device-scoped. If the user wants a dedicated Telegram bot, bind a dedicated OpenClaw agent to a separate Telegram account.

path: SETUP.md
content: # Setup

This is the shortest reliable setup for a real user.

The goal is not just "control the vacuum." The goal is to give the vacuum its own conversational control layer.

## 1. Install the skill

Put this skill at:

```bash
~/.openclaw/skills/jojo-bot
```

## 2. Install the Roborock CLI

Recommended:

```bash
pipx install python-roborock
```

If the CLI has missing runtime deps in your environment, inject them:

```bash
pipx inject python-roborock PyYAML pyshark
```

## 3. Login

```bash
roborock login --email "[REDACTED_SECRET]"
```

Current `python-roborock` versions usually use an email code flow by default.

## 4. Find the device id

```bash
roborock list-devices
```

## 5. Create local config

Create `~/.openclaw/skills/jojo-bot/jojo.env` with:

```bash
JOJO_DEVICE_ID="your-device-id"
JOJO_DEVICE_NAME="jojo"
```

Optional room aliases:

```bash
# ROOM_KITCHEN="17"
# ROOM_MASTER_BEDROOM="19"
# ROOM_LIVING_AREA="21"
```

## 6. Bootstrap room aliases

Run:

```bash
bash ~/.openclaw/skills/jojo-bot/scripts/refresh_room_aliases.sh
```

That prints suggested `ROOM_*` lines based on the live Roborock room map.

Copy the ones you want into `jojo.env`.

## 7. Restart OpenClaw

```bash
openclaw daemon restart
```

## 8. Smoke test

```bash
bash ~/.openclaw/skills/jojo-bot/scripts/jojo.sh status
bash ~/.openclaw/skills/jojo-bot/scripts/jojo.sh rooms
bash ~/.openclaw/skills/jojo-bot/scripts/jojo.sh start
bash ~/.openclaw/skills/jojo-bot/scripts/jojo.sh home
```

## 9. Optional: dedicated Telegram bot

This skill works best with a separate Telegram bot bound to a dedicated OpenClaw agent if you want Jojo to feel like a standalone character.

Typical flow:

```bash
openclaw channels add --channel telegram --account jojo --name Jojo --token <telegram-bot-token>
openclaw agents add jojo --workspace ~/.openclaw/workspace-jojo --bind telegram:jojo --non-interactive
openclaw daemon restart
```

Then give that agent a Jojo-only workspace and prompt.

This is optional. The skill also works inside a shared main bot if you reserve clear phrases like `jojo clean the kitchen` or `jojo go home`.

## Common failures

`You must login first`
- run `roborock login --email ...`

`The method called is not recognized by the device`
- use the verified command verbs from this skill (`app_start`, `app_pause`, `app_stop`, `app_charge`)

`room not found`
- the alias is missing from `jojo.env`
- or the live room name from Roborock does not match what the user said

`command requires approval`
- allowlist the wrapper path in OpenClaw exec approvals

## Publish guidance

Publish this as:
- a conversational Roborock companion
- a Telegram-friendly vacuum agent for OpenClaw
- a higher-level layer above raw Roborock control

Do not publish it as:
- an official Roborock integration
- a camera or microphone bridge
- a replacement for every low-level Roborock skill


path: README.md
content: # Jojo Bot

Jojo Bot turns a Roborock vacuum into a conversational device companion for OpenClaw and Telegram.

It supports:
- status
- start / pause / stop / dock
- room cleaning by alias
- room alias bootstrap from the live Roborock map
- a dedicated Jojo persona layer on top of Roborock control

## What this skill is

This is not just a raw Roborock control wrapper.

It is a higher-level companion skill built on top of the `roborock` CLI so the bot can:
- talk as the vacuum
- understand room-oriented intent
- stay scoped to Jojo-only tasks

If you only want direct device commands, use a lower-level Roborock skill.
If you want a vacuum with its own voice, room aliases, and Telegram-friendly behavior, use this skill.

## What the user needs

- `python-roborock` installed
- a logged-in Roborock account
- a device id from `roborock list-devices`
- a local `jojo.env` created using the config block in `SETUP.md`

Optional:
- a dedicated Telegram bot and OpenClaw agent binding

## Install summary

1. Install `python-roborock`
2. Run `roborock login --email "..."`
3. Run `roborock list-devices`
4. Create `jojo.env`
5. Set `JOJO_DEVICE_ID`
6. Run `bash ./scripts/refresh_room_aliases.sh`
7. Restart OpenClaw

For the full process, read [SETUP.md](./SETUP.md).

## Good test prompts

- `status`
- `clean the house`
- `pause`
- `go home`
- `which rooms do you know`
- `clean the kitchen`

## What to say on ClawHub

Position this as:
- a conversational Roborock companion
- a Jojo-style agent layer for OpenClaw
- useful for Telegram or direct OpenClaw chat

Do not position it as:
- a drop-in replacement for every Roborock skill
- low-level device protocol tooling
- microphone or camera integration

## For agents

Agent-specific operating rules live in [AGENTS.md](./AGENTS.md).


path: AGENTS.md
content: # Jojo Bot Notes

This skill is for a Roborock companion persona, not general assistant work.

## Runtime assumptions

- `roborock` CLI is installed and authenticated
- the user has a valid `JOJO_DEVICE_ID`
- room cleaning by natural-language name works best when `jojo.env` contains `ROOM_*` aliases

## Operational rules

- Prefer the wrapper in `scripts/jojo.sh`
- Keep replies short and device-focused
- If the user asks for unrelated help, route them back to their main assistant
- Do not publish local `jojo.env`, Telegram tokens, or device IDs



path: scripts/refresh_room_aliases.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
CONFIG_FILE="${JOJO_CONFIG_FILE:-$SKILL_DIR/jojo.env}"

if [[ -f "$CONFIG_FILE" ]]; then
  # shellcheck disable=SC1090
  source "$CONFIG_FILE"
fi

DEVICE_ID="${JOJO_DEVICE_ID:-}"
ROBOROCK_BIN="${ROBOROCK_BIN:-}"

if [[ -z "$ROBOROCK_BIN" ]]; then
  if command -v roborock >/dev/null 2>&1; then
    ROBOROCK_BIN="$(command -v roborock)"
  elif [[ -x "$HOME/.local/bin/roborock" ]]; then
    ROBOROCK_BIN="$HOME/.local/bin/roborock"
  else
    echo "Could not find roborock CLI." >&2
    exit 1
  fi
fi

if [[ -z "$DEVICE_ID" ]]; then
  echo "Missing JOJO_DEVICE_ID. Set it in $CONFIG_FILE or your shell." >&2
  exit 1
fi

"$ROBOROCK_BIN" rooms --device_id "$DEVICE_ID" | python3 -c '
import json, re, sys
data = json.load(sys.stdin)
rooms = data.get("rooms", [])
for room in rooms:
    raw_name = str(room.get("rawName", "")).strip()
    segment = room.get("segmentId")
    env_name = re.sub(r"[^A-Z0-9]+", "_", raw_name.upper()).strip("_")
    print(f"ROOM_{env_name}=\"{segment}\"")
'


path: scripts/jojo.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
CONFIG_FILE="${JOJO_CONFIG_FILE:-$SKILL_DIR/jojo.env}"

if [[ -f "$CONFIG_FILE" ]]; then
  # shellcheck disable=SC1090
  source "$CONFIG_FILE"
fi

DEVICE_ID="${JOJO_DEVICE_ID:-}"
DEVICE_NAME="${JOJO_DEVICE_NAME:-jojo}"
ROBOROCK_BIN="${ROBOROCK_BIN:-}"

if [[ -z "$ROBOROCK_BIN" ]]; then
  if command -v roborock >/dev/null 2>&1; then
    ROBOROCK_BIN="$(command -v roborock)"
  elif [[ -x "$HOME/.local/bin/roborock" ]]; then
    ROBOROCK_BIN="$HOME/.local/bin/roborock"
  else
    echo "Could not find roborock CLI." >&2
    exit 1
  fi
fi

if [[ -z "$DEVICE_ID" ]]; then
  echo "Missing JOJO_DEVICE_ID. Set it in $CONFIG_FILE or your shell." >&2
  exit 1
fi

DRY_RUN=0
if [[ "${1:-}" == "--dry-run" ]]; then
  DRY_RUN=1
  shift
fi

usage() {
  cat <<'EOF'
Usage:
  jojo.sh status
  jojo.sh start
  jojo.sh pause
  jojo.sh stop
  jojo.sh home
  jojo.sh rooms
  jojo.sh room kitchen
  jojo.sh room "master bedroom"
  jojo.sh segment 17
  jojo.sh segment 17,21
EOF
}

run_cmd() {
  local -a cmd=("$@")
  if (( DRY_RUN )); then
    printf 'DRY RUN:'
    printf ' %q' "${cmd[@]}"
    printf '\n'
    return 0
  fi
  "${cmd[@]}"
}

normalize_label() {
  printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//'
}

env_room_json() {
  local tmpfile
  tmpfile="$(mktemp)"
  {
    printf '%s\n' '{'
    printf '  "device_name": "%s",\n' "$DEVICE_NAME"
    printf '  "device_id": "%s",\n' "$DEVICE_ID"
    printf '%s\n' '  "rooms": ['
    local first=1
    local var_name
    while IFS= read -r var_name; do
      local value label
      value="${!var_name}"
      [[ -n "$value" ]] || continue
      label="$(printf '%s' "${var_name#ROOM_}" | tr '[:upper:]' '[:lower:]' | tr '_' ' ')"
      if (( first )); then
        first=0
      else
        printf '%s\n' ','
      fi
      printf '    {"name": "%s", "segmentId": %s}' "$label" "$value"
    done < <(compgen -A variable ROOM_ | sort)
    printf '\n'
    printf '%s\n' '  ]'
    printf '%s\n' '}'
  } > "$tmpfile"
  python3 -m json.tool "$tmpfile"
  rm -f "$tmpfile"
}

lookup_room_id_from_env() {
  local target="$1"
  local var_name
  while IFS= read -r var_name; do
    local value label short
    value="${!var_name}"
    [[ -n "$value" ]] || continue
    label="$(printf '%s' "${var_name#ROOM_}" | tr '[:upper:]' '[:lower:]' | tr '_' ' ')"
    short="$label"
    if [[ "$label" == *" room" ]]; then
      short="${label% room}"
    fi
    if [[ "$target" == "$label" || "$target" == "$short" ]]; then
      printf '%s\n' "$value"
      return 0
    fi
  done < <(compgen -A variable ROOM_)
  return 1
}

lookup_room_id_live() {
  local target="$1"
  "$ROBOROCK_BIN" rooms --device_id "$DEVICE_ID" | python3 - "$target" <<'PY'
import json, sys

target = sys.argv[1].strip().lower()
data = json.load(sys.stdin)
rooms = data.get("rooms", [])

def norm(s: str) -> str:
    return " ".join(s.lower().split())

for room in rooms:
    raw = norm(str(room.get("rawName", "")))
    if raw == target:
        print(room["segmentId"])
        raise SystemExit(0)
    if raw.endswith(" room") and raw[:-5].strip() == target:
        print(room["segmentId"])
        raise SystemExit(0)
raise SystemExit(1)
PY
}

segment_params() {
  local raw="$1"
  raw="${raw// /}"
  printf '[%s]' "$raw"
}

command_name="${1:-help}"
shift || true

case "$command_name" in
  help|-h|--help)
    usage
    ;;
  status)
    run_cmd "$ROBOROCK_BIN" status --device_id "$DEVICE_ID"
    ;;
  start|clean|house)
    run_cmd "$ROBOROCK_BIN" command --device_id "$DEVICE_ID" --cmd app_start
    ;;
  pause)
    run_cmd "$ROBOROCK_BIN" command --device_id "$DEVICE_ID" --cmd app_pause
    ;;
  stop)
    run_cmd "$ROBOROCK_BIN" command --device_id "$DEVICE_ID" --cmd app_stop
    ;;
  home|dock|charge)
    run_cmd "$ROBOROCK_BIN" command --device_id "$DEVICE_ID" --cmd app_charge
    ;;
  rooms)
    if compgen -A variable ROOM_ >/dev/null 2>&1; then
      env_room_json
    else
      run_cmd "$ROBOROCK_BIN" rooms --device_id "$DEVICE_ID"
    fi
    ;;
  room)
    if [[ $# -lt 1 ]]; then
      echo "Missing room name." >&2
      exit 1
    fi
    room_name="$*"
    room_norm="$(normalize_label "$room_name")"
    room_id="$(lookup_room_id_from_env "$room_norm" || true)"
    if [[ -z "$room_id" ]]; then
      room_id="$(lookup_room_id_live "$room_norm" || true)"
    fi
    if [[ -z "$room_id" ]]; then
      echo "Unknown room: $room_name" >&2
      exit 1
    fi
    run_cmd "$ROBOROCK_BIN" command --device_id "$DEVICE_ID" --cmd app_segment_clean --params "$(segment_params "$room_id")"
    ;;
  segment)
    if [[ $# -lt 1 ]]; then
      echo "Missing segment id list." >&2
      exit 1
    fi
    run_cmd "$ROBOROCK_BIN" command --device_id "$DEVICE_ID" --cmd app_segment_clean --params "$(segment_params "$1")"
    ;;
  *)
    echo "Unknown command: $command_name" >&2
    usage >&2
    exit 1
    ;;
esac


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)