Simulate ReefBeat devices like ReefATO+, ReefDose, ReefLed, ReefRun and ReefWave
This repo has two distinct parts:
| Part | Entrypoint | Reads | Writes | Purpose |
|---|---|---|---|---|
| Simulator | reefbeat-devices.py |
config.json, devices/ fixtures |
In-memory state only | Serve fixtures over HTTP like real devices |
| Fixture exporter | run.py |
Real device/cloud endpoints | devices/ fixtures |
Capture + sanitize fixtures for the simulator |
Typical workflow:
- Use
run.pyto export/sanitize fixtures intodevices/. - Run
reefbeat-devices.pyto serve those fixtures as simulated devices.
Both tools use the same Python environment and dependencies.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtOptional .env file for cloud access (used by run.py):
REEFBEAT_USERNAME=you@example.com
REEFBEAT_PASSWORD=yourpassword
reefbeat-devices.py runs the actual simulator.
It starts one HTTP server per configured device and serves responses from the
fixture tree under devices/.
Requests that include a JSON payload (POST/PUT) can either:
- Merge the payload into the in-memory state (using
jsonmerge), or - Trigger a configured
post_actionthat computes a derived update.
Run from the repo root so it can find config.json and the devices/ tree:
./reefbeat-devices.pyNotes:
- If you bind to port
80or need to add the configured IP to the host, you’ll typically need root privileges. The script attempts to re-run itself viasudoif startup fails. - The IP auto-add logic is Linux-focused and assumes the interface is
eth0(it usesip addr show/add).
The simulator reads config.json and expects a top-level devices array.
Each entry controls a single device server:
enabled: Whether to start the server.name: Label used in logs.base_url: Fixture root for the device (e.g.devices/DOSE4).ip/port: Bind address.access: Per-endpoint HTTP method allow-list.no_GET: List of paths that must not allow GET.PUT/POST: Optional lists of paths that allow those methods.
post_actions: Optional computed updates keyed by request path.
post_actions example conceptually:
Security note: action is evaluated with eval(). Only run configs you trust.
The simulator serves one fixture file per endpoint:
devices/<TYPE>/<endpoint>/data
For example:
devices/DOSE4/device-info/data
devices/DOSE4/dashboard/data
devices/DOSE4/description.xml/data
description.xml is served as raw text; other endpoints are served as JSON.
run.py is a Python-based fixture exporter for the ReefBeat Devices Simulator.
It snapshots:
- Local ReefBeat device HTTP endpoints (by IP)
- Optionally ReefBeat cloud account endpoints
All outputs are written under the devices/ directory, with one folder per device type. Each endpoint is stored as a data file.
Payloads are sanitized to remove secrets and personal data while preserving stable relationships (aquarium ↔ device ↔ user), making the fixtures safe to commit and suitable for automated testing.
Cloud-only scan (fast, no LAN probing):
python run.py scanLAN scan using a CIDR:
python run.py scan --cidr 192.168.1.0/24The output looks like this:
❯ python run.py scan --cidr 192.168.1.1/24
INFO : LAN scanning 192.168.1.1/24...
INFO : Enriching scan results from cloud...| From | Aquarium | Device | Type | IP | Model | FW |
|---|---|---|---|---|---|---|
| LAN | RSATO+000000000 | 192.168.1.92 | ||||
| LAN+CLOUD | 80g Frag Tank | RSATO+0000000000 | reef-ato | 192.168.1.96 | RSATO+ | 1.11.1 |
| LAN+CLOUD | 80g Frag Tank | RSDOSE4-000000000 | reef-dosing | 192.168.1.94 | RSDOSE4 | 3.0.0 |
| LAN+CLOUD | 80g Frag Tank | RSMAT-0000000000 | reef-mat | 192.168.1.95 | RSMAT500 | 1.10.2 |
| LAN+CLOUD | Reefer 200XL | RSATO+0000000000 | reef-ato | 192.168.1.98 | RSATO+ | 1.11.0 |
| LAN+CLOUD | Reefer 200XL | RSDOSE2-000000000 | reef-dosing | 192.168.1.93 | RSDOSE2 | 3.0.0 |
| LAN+CLOUD | Reefer 200XL | RSMAT-000000000 | reef-mat | 192.168.1.97 | RSMAT250 | 1.10.2 |
Multiple CIDRs are supported:
python run.py scan --cidr 192.168.1.0/24 --cidr 192.168.1.0/24Scan output is displayed as a table showing:
- Aquarium
- Device name
- Device type
- IP address
- Model
- Firmware
Snapshot all supported endpoints from a device by IP:
python run.py --ip 192.168.1.95The device type is auto-detected from /device-info.
To force a specific device type:
python run.py --ip 192.168.1.95 --type DOSE2Resulting structure:
devices/
DOSE2/
device-info/
data
firmware/
data
description.xml/
data
...
Each endpoint is stored in its own directory containing a data file.
python run.py --cloudCloud fixtures are written to:
devices/CLOUD/
user/
data
aquarium/
data
device/
data
meta.json
Identifiers are sanitized, but relationships between user, aquarium, and device are preserved.
The script maintains a local sanitize-map file to keep identifiers stable and unique across runs.
This ensures:
- aquarium_id matches across cloud and device payloads
- aquarium_uid and user_uid remain consistent
- device hwid, mac, ip, and serials are deterministic but anonymized
The sanitize map is local-only and should be gitignored:
.reefbeat_sanitize_map.json
The file contains no recoverable personal data and exists only to keep fixtures internally consistent for testing.
ATO
DOSE2
DOSE4
MAT
LED
RUN
WAVE
Existing fixture trees are also used to infer endpoint lists, allowing snapshots to remain config-driven.
scan → discover devices (LAN and/or cloud)
--ip → snapshot a local device
--cloud → snapshot cloud endpoints only
devices/ → final simulator fixture tree
{ "request": "/head/1/manual", "action": { "target": "/dashboard", "action": "{...python expression...}" } }