Skip to content

Commit bfafdc0

Browse files
authored
add basic auth implementation reusing roborock's logic (#10)
* add basic auth implementation reusing roborock's logic * improve auth * fix auth so that it runs properly for onboarding and existing vacuums * enable disabling auth, added protocol session management * improve documentation * address copilot reviews * Add docker build for images * Add license, update version * allow custom ports * fix onboarding bug * improve auth * address copilot comments * build on github release * change to rc1 * fix typo
1 parent 436f5db commit bfafdc0

48 files changed

Lines changed: 4587 additions & 232 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
__pycache__/
2-
*.pyc
3-
*.pyo
4-
*.pyd
5-
.pytest_cache/
6-
data/
7-
secrets/
8-
tests/
1+
*
2+
!README.md
3+
!pyproject.toml
4+
!src/
5+
!src/**
6+
src/**/__pycache__/
7+
src/**/*.pyc
8+
src/**/*.pyo
9+
src/**/*.pyd
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
name: docker-release
2+
3+
on:
4+
pull_request:
5+
release:
6+
types:
7+
- published
8+
workflow_dispatch:
9+
10+
env:
11+
REGISTRY: ghcr.io
12+
IMAGE_NAME: ${{ github.repository }}
13+
14+
permissions:
15+
contents: read
16+
17+
concurrency:
18+
group: docker-release-${{ github.ref }}
19+
cancel-in-progress: true
20+
21+
jobs:
22+
test:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v6
27+
28+
- name: Set up Python
29+
uses: actions/setup-python@v6
30+
with:
31+
python-version-file: pyproject.toml
32+
33+
- name: Install uv
34+
uses: astral-sh/setup-uv@v7
35+
with:
36+
enable-cache: true
37+
38+
- name: Install project
39+
run: uv sync --locked --extra dev
40+
41+
- name: Run tests
42+
run: uv run pytest -q
43+
44+
docker-validate:
45+
needs: test
46+
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
47+
runs-on: ubuntu-latest
48+
steps:
49+
- name: Checkout
50+
uses: actions/checkout@v6
51+
52+
- name: Set up Docker Buildx
53+
uses: docker/setup-buildx-action@v4
54+
55+
- name: Build Docker image
56+
uses: docker/build-push-action@v7
57+
with:
58+
context: .
59+
file: ./Dockerfile
60+
platforms: linux/amd64
61+
push: false
62+
cache-from: type=gha
63+
cache-to: type=gha,mode=max
64+
65+
docker-release:
66+
needs: test
67+
if: github.event_name == 'release'
68+
runs-on: ubuntu-latest
69+
permissions:
70+
contents: read
71+
packages: write
72+
attestations: write
73+
id-token: write
74+
steps:
75+
- name: Checkout
76+
uses: actions/checkout@v6
77+
78+
- name: Set up Python
79+
uses: actions/setup-python@v6
80+
with:
81+
python-version-file: pyproject.toml
82+
83+
- name: Validate release tag matches package version
84+
shell: python
85+
run: |
86+
import re
87+
import tomllib
88+
from pathlib import Path
89+
90+
tag = "${{ github.event.release.tag_name }}"
91+
pyproject = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
92+
project_version = str(pyproject["project"]["version"]).strip()
93+
94+
init_text = Path("src/roborock_local_server/__init__.py").read_text(encoding="utf-8")
95+
match = re.search(r'__version__\s*=\s*"([^"]+)"', init_text)
96+
if match is None:
97+
raise SystemExit("Could not find __version__ in src/roborock_local_server/__init__.py")
98+
99+
module_version = match.group(1).strip()
100+
expected_tag = f"v{project_version}"
101+
if module_version != project_version:
102+
raise SystemExit(
103+
f"Version mismatch: pyproject.toml={project_version}, __init__.py={module_version}"
104+
)
105+
if tag != expected_tag:
106+
raise SystemExit(f"Git tag {tag} does not match package version {expected_tag}")
107+
108+
- name: Log in to GHCR
109+
uses: docker/login-action@v3
110+
with:
111+
registry: ${{ env.REGISTRY }}
112+
username: ${{ github.actor }}
113+
password: ${{ secrets.GITHUB_TOKEN }}
114+
115+
- name: Set up QEMU
116+
uses: docker/setup-qemu-action@v3
117+
118+
- name: Set up Docker Buildx
119+
uses: docker/setup-buildx-action@v4
120+
121+
- name: Extract metadata
122+
id: meta
123+
uses: docker/metadata-action@v5
124+
with:
125+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
126+
flavor: latest=false
127+
tags: |
128+
type=semver,pattern={{version}},value=${{ github.event.release.tag_name }}
129+
type=semver,pattern={{major}}.{{minor}},value=${{ github.event.release.tag_name }}
130+
type=semver,pattern={{major}},value=${{ github.event.release.tag_name }}
131+
type=raw,value=latest,enable=${{ startsWith(github.event.release.tag_name, 'v') && !contains(github.event.release.tag_name, '-') }}
132+
type=sha,prefix=sha-
133+
labels: |
134+
org.opencontainers.image.title=roborock-local-server
135+
org.opencontainers.image.description=Private Roborock HTTPS and MQTT stack you run on your own system.
136+
137+
- name: Build and push Docker image
138+
id: push
139+
uses: docker/build-push-action@v7
140+
with:
141+
context: .
142+
file: ./Dockerfile
143+
platforms: linux/amd64,linux/arm64
144+
push: true
145+
tags: ${{ steps.meta.outputs.tags }}
146+
labels: ${{ steps.meta.outputs.labels }}
147+
cache-from: type=gha
148+
cache-to: type=gha,mode=max
149+
150+
- name: Generate artifact attestation
151+
uses: actions/attest@v4
152+
with:
153+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
154+
subject-digest: ${{ steps.push.outputs.digest }}
155+
push-to-registry: true

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ RUN mkdir -p /opt/acme.sh \
1616

1717
WORKDIR /app
1818

19-
COPY . /app
19+
COPY pyproject.toml README.md /app/
20+
COPY src /app/src
2021

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

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Luke Lashley
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# Roborock Local Server
2+
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.
23

3-
If this project helps, you can support it or next time you buy a Roborock device, come back here and use my affiliate links!
4+
[![Amazon Affiliate][badge-amazon]][link-amazon]
5+
[![Roborock 5 Off][badge-roborock-discount]][link-roborock-discount]
6+
[![Roborock Affiliate][badge-roborock-affiliate]][link-roborock-affiliate]
7+
8+
9+
You can also support via BMAC or paypal:
410

511
[![Buy Me a Coffee][badge-bmac]][link-bmac]
612
[![PayPal][badge-paypal]][link-paypal]
7-
[![Roborock 5 Off][badge-roborock-discount]][link-roborock-discount]
8-
[![Roborock Affiliate][badge-roborock-affiliate]][link-roborock-affiliate]
9-
[![Amazon Affiliate][badge-amazon]][link-amazon]
1013

1114
NOTE: if you have not already setup this project, i would recommend waiting a few days. i will be pushing a number of changes that are partially backwards incompatible and the new version should be a bit easier to use!
1215

@@ -46,12 +49,21 @@ Additional docs:
4649
## Acknowledgements
4750

4851
- [Dennis Giese (@dgiese)](https://dontvacuum.me/) whose research and papers inspired much of the work on reverse-engineering Roborock vacuums
52+
- [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.
4953
- [@rovo89](https://github.com/rovo89) who has been VERY helpful through this process, giving lots of tips and advice.
5054
- [python-miio](https://github.com/rytilahti/python-miio) - Their repo was the basis for a lot of python-roborock's logic.
5155
- [@humbertogontijo](https://github.com/humbertogontijo) who first created the python-roborock repo.
5256
- [@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.
5357
- [@rccoleman](https://github.com/rccoleman) who was the first beta tester and helped work out some kinks!
5458

59+
## Disclaimer
60+
61+
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.
62+
63+
## License
64+
65+
This project is licensed under the MIT License — see [LICENSE](LICENSE) for details.
66+
5567
[link-bmac]: https://buymeacoffee.com/lashl
5668
[badge-bmac]: https://img.shields.io/badge/Buy%20Me%20a%20Coffee-donate-yellow?style=for-the-badge&logo=buymeacoffee&logoColor=black
5769
[link-paypal]: https://paypal.me/LLashley304
@@ -60,5 +72,5 @@ Additional docs:
6072
[badge-roborock-discount]: https://img.shields.io/badge/Roborock-5%25%20Off-C00000?style=for-the-badge
6173
[link-roborock-affiliate]: https://roborock.pxf.io/B0VYV9
6274
[badge-roborock-affiliate]: https://img.shields.io/badge/Roborock-affiliate-B22222?style=for-the-badge
63-
[link-amazon]: https://amzn.to/4bGfG6B
75+
[link-amazon]: https://amzn.to/4cx8zg3
6476
[badge-amazon]: https://img.shields.io/badge/Amazon-affiliate-FF9900?style=for-the-badge&logo=amazon&logoColor=white

compose.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ services:
66
container_name: roborock-local-server
77
restart: unless-stopped
88
ports:
9-
- "443:443"
10-
- "8883:8883"
9+
- "${ROBOROCK_SERVER_HTTPS_PORT:-555}:${ROBOROCK_SERVER_HTTPS_PORT:-555}"
10+
- "${ROBOROCK_SERVER_MQTT_TLS_PORT:-8881}:${ROBOROCK_SERVER_MQTT_TLS_PORT:-8881}"
1111
volumes:
1212
- ./config.toml:/app/config.toml:ro
1313
- ./data:/data
1414
- ./secrets/cloudflare_token:/run/secrets/cloudflare_token:ro
1515
healthcheck:
16-
test: ["CMD", "curl", "-skf", "https://127.0.0.1/admin"]
16+
test: ["CMD", "curl", "-skf", "https://127.0.0.1:${ROBOROCK_SERVER_HTTPS_PORT:-555}/admin"]
1717
interval: 30s
1818
timeout: 5s
1919
retries: 5

config.example.toml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
[network]
2-
# The one hostname the stack will serve.
2+
# The one hostname the stack will serve. Keep this as the hostname only.
33
stack_fqdn = "roborock.example.com"
44
bind_host = "0.0.0.0"
5-
https_port = 443
6-
mqtt_tls_port = 8883
5+
# Change these if you need the stack to advertise and listen on custom ports.
6+
https_port = 555
7+
mqtt_tls_port = 8881
78
region = "us"
89

910
[broker]
@@ -31,3 +32,7 @@ acme_server = "zerossl"
3132
password_hash = "pbkdf2_sha256$600000$replace_me$replace_me"
3233
session_secret = "replace-with-at-least-24-random-characters"
3334
session_ttl_seconds = 86400
35+
protocol_auth_enabled = true
36+
# Home Assistant/app logins use this email plus a local 6-digit PIN entered as the "code".
37+
protocol_login_email = "you@example.com"
38+
protocol_login_pin_hash = "pbkdf2_sha256$600000$replace_me$replace_me"

docs/home_assistant.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ To use this server with Home Assistant, edit your config entry at `config/.stora
66

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

9-
- `base_url` -> `https://api-roborock.example.com`
10-
- `"a"` -> `https://api-roborock.example.com`
11-
- `"l"` -> `https://api-roborock.example.com`
12-
- `"m"` -> `ssl://mqtt-roborock.example.com:8883`
9+
- `base_url` -> `https://api-roborock.example.com:555`
10+
- `"a"` -> `https://api-roborock.example.com:555`
11+
- `"l"` -> `https://api-roborock.example.com:555`
12+
- `"m"` -> `ssl://mqtt-roborock.example.com:8881`
13+
14+
If you changed `network.https_port` or `network.mqtt_tls_port`, use those values instead.
1315

1416
## Related Docs
1517

docs/installation.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Start here for a first-time setup. After the stack is running, continue with [On
88
- Python (I recommend installing [uv](https://docs.astral.sh/uv/getting-started/installation/))
99
- Two machines - one to run the server and one to do the onboarding
1010
- A domain name that you own
11-
- A machine that can host this with ports `443` and `8883` exposed internally on your network
11+
- A machine that can host the stack's HTTPS and MQTT TLS ports internally on your network. The defaults are `555` and `8881`.
1212
- 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).
1313

1414
## Network Setup
@@ -48,9 +48,11 @@ uv run roborock-local-server configure
4848
The wizard asks only for:
4949

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

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

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

67-
8. Go to the admin dashboard: https://api-roborock.example.com/admin (Replace with your real domain.)
69+
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:
70+
71+
```bash
72+
ROBOROCK_SERVER_HTTPS_PORT=8443
73+
ROBOROCK_SERVER_MQTT_TLS_PORT=9443
74+
docker compose up -d --build
75+
```
76+
77+
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.
6878

6979
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.
7080

docs/onboarding.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Run onboarding from a second machine, not from the machine hosting the local ser
1212
uv run start_onboarding.py --server api-roborock.example.com
1313
```
1414

15+
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`.
16+
1517
This is a standalone script — you can copy `start_onboarding.py` to any machine and run it with just `uv`.
1618

1719
The guided CLI will:
@@ -44,7 +46,7 @@ You can still pass them explicitly if you prefer:
4446
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
4547
```
4648

47-
`server` should be your real stack hostname, usually the same `api-...` hostname you use for `/admin`.
49+
`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`.
4850

4951
## CST Examples
5052

0 commit comments

Comments
 (0)