Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
data/
secrets/
tests/
*
!README.md
!pyproject.toml
!src/
!src/**
src/**/__pycache__/
src/**/*.pyc
src/**/*.pyo
src/**/*.pyd
155 changes: 155 additions & 0 deletions .github/workflows/docker-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
name: docker-release

on:
pull_request:
release:
types:
- published
workflow_dispatch:

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

permissions:
contents: read

concurrency:
group: docker-release-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version-file: pyproject.toml

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- name: Install project
run: uv sync --locked --extra dev

- name: Run tests
run: uv run pytest -q

docker-validate:
needs: test
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Build Docker image
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: false
cache-from: type=gha
cache-to: type=gha,mode=max

docker-release:
needs: test
if: github.event_name == 'release'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version-file: pyproject.toml

- name: Validate release tag matches package version
shell: python
run: |
import re
import tomllib
from pathlib import Path

tag = "${{ github.event.release.tag_name }}"
pyproject = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
project_version = str(pyproject["project"]["version"]).strip()

init_text = Path("src/roborock_local_server/__init__.py").read_text(encoding="utf-8")
match = re.search(r'__version__\s*=\s*"([^"]+)"', init_text)
if match is None:
raise SystemExit("Could not find __version__ in src/roborock_local_server/__init__.py")

module_version = match.group(1).strip()
expected_tag = f"v{project_version}"
if module_version != project_version:
raise SystemExit(
f"Version mismatch: pyproject.toml={project_version}, __init__.py={module_version}"
)
if tag != expected_tag:
raise SystemExit(f"Git tag {tag} does not match package version {expected_tag}")

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: latest=false
tags: |
type=semver,pattern={{version}},value=${{ github.event.release.tag_name }}
type=semver,pattern={{major}}.{{minor}},value=${{ github.event.release.tag_name }}
type=semver,pattern={{major}},value=${{ github.event.release.tag_name }}
type=raw,value=latest,enable=${{ startsWith(github.event.release.tag_name, 'v') && !contains(github.event.release.tag_name, '-') }}
type=sha,prefix=sha-
labels: |
org.opencontainers.image.title=roborock-local-server
org.opencontainers.image.description=Private Roborock HTTPS and MQTT stack you run on your own system.

- name: Build and push Docker image
id: push
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Generate artifact attestation
uses: actions/attest@v4
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ RUN mkdir -p /opt/acme.sh \

WORKDIR /app

COPY . /app
COPY pyproject.toml README.md /app/
COPY src /app/src

RUN pip install --no-cache-dir /app

Expand Down
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Luke Lashley

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# Roborock Local Server
The best way to support this project is the next time you are buying a Roborock device come back here and use one of my affiliate links where I will receive a commission.

If this project helps, you can support it or next time you buy a Roborock device, come back here and use my affiliate links!
[![Amazon Affiliate][badge-amazon]][link-amazon]
[![Roborock 5 Off][badge-roborock-discount]][link-roborock-discount]
[![Roborock Affiliate][badge-roborock-affiliate]][link-roborock-affiliate]


You can also support via BMAC or paypal:

[![Buy Me a Coffee][badge-bmac]][link-bmac]
[![PayPal][badge-paypal]][link-paypal]
[![Roborock 5 Off][badge-roborock-discount]][link-roborock-discount]
[![Roborock Affiliate][badge-roborock-affiliate]][link-roborock-affiliate]
[![Amazon Affiliate][badge-amazon]][link-amazon]

Roborock Local Server is a private Roborock HTTPS and MQTT stack you run on your own system.

Expand Down Expand Up @@ -44,12 +47,21 @@ Additional docs:
## Acknowledgements

- [Dennis Giese (@dgiese)](https://dontvacuum.me/) whose research and papers inspired much of the work on reverse-engineering Roborock vacuums
- [Sören Beye (@Hypfer)](https://github.com/Hypfer) creator of [Valetudo](https://valetudo.cloud/), whose work on cloud-free vacuum control has been foundational for this whole space.
- [@rovo89](https://github.com/rovo89) who has been VERY helpful through this process, giving lots of tips and advice.
- [python-miio](https://github.com/rytilahti/python-miio) - Their repo was the basis for a lot of python-roborock's logic.
- [@humbertogontijo](https://github.com/humbertogontijo) who first created the python-roborock repo.
- [@allenporter](https://github.com/allenporter) who has taken up a significant role in the maintenance of the python-roborock library as well as the Roborock integration. The improvements Allen has made to the repository cannot be overstated.
- [@rccoleman](https://github.com/rccoleman) who was the first beta tester and helped work out some kinks!

## Disclaimer

This software is provided "as is", without warranty of any kind. Running this stack involves modifying how your Roborock vacuum communicates with the network. You are solely responsible for any damage to your hardware, data loss, network exposure, or other consequences. Use at your own risk. This project is not affiliated with, endorsed by, or sponsored by Roborock.

## License

This project is licensed under the MIT License — see [LICENSE](LICENSE) for details.

[link-bmac]: https://buymeacoffee.com/lashl
[badge-bmac]: https://img.shields.io/badge/Buy%20Me%20a%20Coffee-donate-yellow?style=for-the-badge&logo=buymeacoffee&logoColor=black
[link-paypal]: https://paypal.me/LLashley304
Expand All @@ -58,5 +70,5 @@ Additional docs:
[badge-roborock-discount]: https://img.shields.io/badge/Roborock-5%25%20Off-C00000?style=for-the-badge
[link-roborock-affiliate]: https://roborock.pxf.io/B0VYV9
[badge-roborock-affiliate]: https://img.shields.io/badge/Roborock-affiliate-B22222?style=for-the-badge
[link-amazon]: https://amzn.to/4bGfG6B
[link-amazon]: https://amzn.to/4cx8zg3
[badge-amazon]: https://img.shields.io/badge/Amazon-affiliate-FF9900?style=for-the-badge&logo=amazon&logoColor=white
6 changes: 3 additions & 3 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ services:
container_name: roborock-local-server
restart: unless-stopped
ports:
- "443:443"
- "8883:8883"
- "${ROBOROCK_SERVER_HTTPS_PORT:-555}:${ROBOROCK_SERVER_HTTPS_PORT:-555}"
- "${ROBOROCK_SERVER_MQTT_TLS_PORT:-8881}:${ROBOROCK_SERVER_MQTT_TLS_PORT:-8881}"
volumes:
- ./config.toml:/app/config.toml:ro
- ./data:/data
- ./secrets/cloudflare_token:/run/secrets/cloudflare_token:ro
healthcheck:
test: ["CMD", "curl", "-skf", "https://127.0.0.1/admin"]
test: ["CMD", "curl", "-skf", "https://127.0.0.1:${ROBOROCK_SERVER_HTTPS_PORT:-555}/admin"]
interval: 30s
timeout: 5s
retries: 5
Expand Down
11 changes: 8 additions & 3 deletions config.example.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
[network]
# The one hostname the stack will serve.
# The one hostname the stack will serve. Keep this as the hostname only.
stack_fqdn = "roborock.example.com"
bind_host = "0.0.0.0"
https_port = 443
mqtt_tls_port = 8883
# Change these if you need the stack to advertise and listen on custom ports.
https_port = 555
mqtt_tls_port = 8881
region = "us"

[broker]
Expand Down Expand Up @@ -31,3 +32,7 @@ acme_server = "zerossl"
password_hash = "pbkdf2_sha256$600000$replace_me$replace_me"
session_secret = "replace-with-at-least-24-random-characters"
session_ttl_seconds = 86400
protocol_auth_enabled = true
# Home Assistant/app logins use this email plus a local 6-digit PIN entered as the "code".
protocol_login_email = "you@example.com"
protocol_login_pin_hash = "pbkdf2_sha256$600000$replace_me$replace_me"
10 changes: 6 additions & 4 deletions docs/home_assistant.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ To use this server with Home Assistant, edit your config entry at `config/.stora

Find `"roborock.com"` and replace the endpoint values with your local stack URLs:

- `base_url` -> `https://api-roborock.example.com`
- `"a"` -> `https://api-roborock.example.com`
- `"l"` -> `https://api-roborock.example.com`
- `"m"` -> `ssl://mqtt-roborock.example.com:8883`
- `base_url` -> `https://api-roborock.example.com:555`
- `"a"` -> `https://api-roborock.example.com:555`
- `"l"` -> `https://api-roborock.example.com:555`
- `"m"` -> `ssl://mqtt-roborock.example.com:8881`

If you changed `network.https_port` or `network.mqtt_tls_port`, use those values instead.

## Related Docs

Expand Down
14 changes: 12 additions & 2 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Start here for a first-time setup. After the stack is running, continue with [On
- Python (I recommend installing [uv](https://docs.astral.sh/uv/getting-started/installation/))
- Two machines - one to run the server and one to do the onboarding
- A domain name that you own
- A machine that can host this with ports `443` and `8883` exposed internally on your network
- A machine that can host the stack's HTTPS and MQTT TLS ports internally on your network. The defaults are `555` and `8881`.
- A Cloudflare API token with DNS edit access for the zone if you want Cloudflare DNS-01 auto-renew. See [Cloudflare setup](cloudflare_setup.md).

## Network Setup
Expand Down Expand Up @@ -48,9 +48,11 @@ uv run roborock-local-server configure
The wizard asks only for:

- your `stack_fqdn` (the URL for your server - must start with `api-`)
- your HTTPS and MQTT TLS ports if you do not want the defaults `555` and `8881`
- embedded MQTT or your own broker
- whether to use Cloudflare DNS-01 auto-renew
- your admin password
- your Home Assistant/app login email and 6-digit PIN

It then writes `config.toml`, generates `admin.password_hash` and `admin.session_secret`, and if you chose Cloudflare it also writes `secrets/cloudflare_token`.

Expand All @@ -64,7 +66,15 @@ It then writes `config.toml`, generates `admin.password_hash` and `admin.session
docker compose up -d --build
```

8. Go to the admin dashboard: https://api-roborock.example.com/admin (Replace with your real domain.)
If you changed `network.https_port` or `network.mqtt_tls_port` in `config.toml`, set matching Docker Compose variables before you start the stack so the published ports stay aligned. For example:

```bash
ROBOROCK_SERVER_HTTPS_PORT=8443
ROBOROCK_SERVER_MQTT_TLS_PORT=9443
docker compose up -d --build
```

8. Go to the admin dashboard: `https://api-roborock.example.com:555/admin` by default, or `https://api-roborock.example.com:YOUR_HTTPS_PORT/admin` if you chose a custom HTTPS port.

9. Import your data from the cloud so things like routines and rooms will work. Enter your email in under cloud import, then hit send code. Once the code is returned enter the code and hit fetch data.

Expand Down
4 changes: 3 additions & 1 deletion docs/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Run onboarding from a second machine, not from the machine hosting the local ser
uv run start_onboarding.py --server api-roborock.example.com
```

If you omit the port, the CLI assumes the default local stack HTTPS port `555`. If your stack uses a custom HTTPS port, include it in `--server`, for example `api-roborock.example.com:8443`.

This is a standalone script — you can copy `start_onboarding.py` to any machine and run it with just `uv`.

The guided CLI will:
Expand Down Expand Up @@ -44,7 +46,7 @@ You can still pass them explicitly if you prefer:
uv run start_onboarding.py --server api-roborock.example.com --ssid "My Wifi" --password "Password123" --timezone "America/New_York" --cst EST5EDT,M3.2.0,M11.1.0 --country-domain us
```

`server` should be your real stack hostname, usually the same `api-...` hostname you use for `/admin`.
`server` should be your real stack hostname, usually the same `api-...` hostname you use for `/admin`. If you omit the port, the CLI assumes `:555`. Explicit ports are supported, so if your admin page is at `https://api-roborock.example.com:8443/admin`, use `--server api-roborock.example.com:8443`.

## CST Examples

Expand Down
Loading